diff --git a/env0apiclient/httpverbs.go b/env0apiclient/httpverbs.go index 179b9082..0e2e1398 100644 --- a/env0apiclient/httpverbs.go +++ b/env0apiclient/httpverbs.go @@ -78,7 +78,7 @@ func (self *ApiClient) do(req *http.Request) ([]byte, error) { if resp.StatusCode == 204 { return nil, nil } - if resp.StatusCode != 200 { + if resp.StatusCode != 200 && resp.StatusCode != 201 { return nil, errors.New(resp.Status + ": " + string(body)) } diff --git a/env0apiclient/model.go b/env0apiclient/model.go index 5b588ff9..d446dcc7 100644 --- a/env0apiclient/model.go +++ b/env0apiclient/model.go @@ -84,39 +84,62 @@ const ( TemplateTypeTerragrunt TemplateType = "terragrunt" ) +type TemplateSshKey struct { + Id string `json:"id"` + Name string `json:"name"` +} + type TemplateCreatePayload struct { - Retry *TemplateRetry `json:"retry,omitempty"` - SshKeys []string `json:"sshKeys,omitempty"` - Type TemplateType `json:"type"` - Description string `json:"description,omitempty"` - Name string `json:"name"` - Repository string `json:"repository"` - Path string `json:"path,omitempty"` - IsGitLab bool `json:"isGitLab"` - TokenName string `json:"tokenName"` - TokenId string `json:"tokenId"` - GithubInstallationId int `json:"githubInstallationId"` - Revision string `json:"revision"` - ProjectIds []string `json:"projectIds,omitempty"` - OrganizationId string `json:"organizationId"` + Retry *TemplateRetry `json:"retry,omitempty"` + SshKeys []TemplateSshKey `json:"sshKeys,omitempty"` + Type TemplateType `json:"type"` + Description string `json:"description,omitempty"` + Name string `json:"name"` + Repository string `json:"repository"` + Path string `json:"path,omitempty"` + IsGitLab bool `json:"isGitLab"` + TokenName string `json:"tokenName"` + TokenId string `json:"tokenId"` + GithubInstallationId int `json:"githubInstallationId"` + Revision string `json:"revision"` + ProjectIds []string `json:"projectIds,omitempty"` + OrganizationId string `json:"organizationId"` } type Template struct { - Author User `json:"author"` - AuthorId string `json:"authorId"` - CreatedAt string `json:"createdAt"` - Href string `json:"href"` - Id string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - OrganizationId string `json:"organizationId"` - Path string `json:"path"` - Revision string `json:"revision"` - ProjectId string `json:"projectId"` - ProjectIds []string `json:"projectIds"` - Repository string `json:"repository"` - Retry TemplateRetry `json:"retry"` - SshKeys []string `json:"sshKeys"` - Type string `json:"type"` - UpdatedAt string `json:"updatedAt"` + Author User `json:"author"` + AuthorId string `json:"authorId"` + CreatedAt string `json:"createdAt"` + Href string `json:"href"` + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + OrganizationId string `json:"organizationId"` + Path string `json:"path"` + Revision string `json:"revision"` + ProjectId string `json:"projectId"` + ProjectIds []string `json:"projectIds"` + Repository string `json:"repository"` + Retry TemplateRetry `json:"retry"` + SshKeys []TemplateSshKey `json:"sshKeys"` + Type string `json:"type"` + UpdatedAt string `json:"updatedAt"` +} + +type SshKey struct { + User User `json:"user"` + UserId string `json:"userId"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + OrganizationId string `json:"organizationId"` + Value string `json:"value"` +} + +type SshKeyCreatePayload struct { + Name string `json:"name"` + OrganizationId string `json:"organizationId"` + Value string `json:"value"` } diff --git a/env0apiclient/sshkey.go b/env0apiclient/sshkey.go new file mode 100644 index 00000000..7add27e8 --- /dev/null +++ b/env0apiclient/sshkey.go @@ -0,0 +1,49 @@ +package env0apiclient + +import ( + "errors" + "net/url" +) + +func (self *ApiClient) SshKeyCreate(payload SshKeyCreatePayload) (SshKey, error) { + if payload.Name == "" { + return SshKey{}, errors.New("Must specify ssh key name on creation") + } + if payload.Value == "" { + return SshKey{}, errors.New("Must specify ssh key value (private key in PEM format) on creation") + } + if payload.OrganizationId != "" { + return SshKey{}, errors.New("Must not specify organizationId") + } + organizationId, err := self.organizationId() + if err != nil { + return SshKey{}, nil + } + payload.OrganizationId = organizationId + + var result SshKey + err = self.postJSON("/ssh-keys", payload, &result) + if err != nil { + return SshKey{}, err + } + return result, nil +} + +func (self *ApiClient) SshKeyDelete(id string) error { + return self.delete("/ssh-keys/" + id) +} + +func (self *ApiClient) SshKeys() ([]SshKey, error) { + organizationId, err := self.organizationId() + if err != nil { + return nil, err + } + var result []SshKey + params := url.Values{} + params.Add("organizationId", organizationId) + err = self.getJSON("/ssh-keys", params, &result) + if err != nil { + return nil, err + } + return result, err +} diff --git a/env0tfprovider/data_configuration_variable.go b/env0tfprovider/data_configuration_variable.go index 873565c7..c40674f0 100644 --- a/env0tfprovider/data_configuration_variable.go +++ b/env0tfprovider/data_configuration_variable.go @@ -20,10 +20,9 @@ func dataConfigurationVariable() *schema.Resource { ExactlyOneOf: []string{"name", "id"}, }, "type": { - Type: schema.TypeString, - Description: "'terraform' or 'environment'. If specified as an argument, limits searching by variable name only to variables of this type.", - Optional: true, - AtLeastOneOf: []string{"name"}, + Type: schema.TypeString, + Description: "'terraform' or 'environment'. If specified as an argument, limits searching by variable name only to variables of this type.", + Optional: true, }, "id": { Type: schema.TypeString, @@ -104,6 +103,9 @@ func dataConfigurationVariableRead(ctx context.Context, d *schema.ResourceData, name, nameOk := d.GetOk("name") type_ := int64(-1) if typeString, ok := d.GetOk("type"); ok { + if !nameOk { + return diag.Errorf("Specify 'type' only when searching configuration variables by 'name' (not by 'id')") + } switch typeString.(string) { case "environment": type_ = int64(env0apiclient.ConfigurationVariableTypeEnvironment) diff --git a/env0tfprovider/data_sshkey.go b/env0tfprovider/data_sshkey.go new file mode 100644 index 00000000..c701d3bc --- /dev/null +++ b/env0tfprovider/data_sshkey.go @@ -0,0 +1,73 @@ +package env0tfprovider + +import ( + "context" + + "github.com/env0/terraform-provider-env0/env0apiclient" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSshKey() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSshKeyRead, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "the name of the ssh key", + Optional: true, + ExactlyOneOf: []string{"name", "id"}, + }, + "id": { + Type: schema.TypeString, + Description: "id of the ssh key", + Optional: true, + ExactlyOneOf: []string{"name", "id"}, + }, + }, + } +} + +func dataSshKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(*env0apiclient.ApiClient) + + name, nameSpecified := d.GetOk("name") + var sshKey env0apiclient.SshKey + if nameSpecified { + sshKeys, err := apiClient.SshKeys() + if err != nil { + return diag.Errorf("Could not query ssh keys: %v", err) + } + for _, candidate := range sshKeys { + if candidate.Name == name { + sshKey = candidate + } + } + if sshKey.Name == "" { + return diag.Errorf("Could not find an env0 ssh key with name %s", name) + } + } else { + id, idSpecified := d.GetOk("id") + if !idSpecified { + return diag.Errorf("At lease one of 'id', 'name' must be specified") + } + sshKeys, err := apiClient.SshKeys() + if err != nil { + return diag.Errorf("Could not query ssh keys: %v", err) + } + for _, candidate := range sshKeys { + if candidate.Id == id.(string) { + sshKey = candidate + } + } + if sshKey.Name == "" { + return diag.Errorf("Could not find an env0 ssh key with id %s", id) + } + } + + d.SetId(sshKey.Id) + d.Set("name", sshKey.Name) + + return nil +} diff --git a/env0tfprovider/data_template.go b/env0tfprovider/data_template.go index 2b895303..0a014c3e 100644 --- a/env0tfprovider/data_template.go +++ b/env0tfprovider/data_template.go @@ -54,6 +54,15 @@ func dataTemplate() *schema.Resource { Description: "env0_project.id for each project", }, }, + "ssh_key_names": { + Type: schema.TypeList, + Description: "which ssh keys are used for accessing git over ssh", + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + Description: "env0_ssh_key.name for each project", + }, + }, "retries_on_deploy": { Type: schema.TypeInt, Description: "number of times to retry when deploying an environment based on this template", @@ -98,7 +107,7 @@ func dataTemplateRead(ctx context.Context, d *schema.ResourceData, meta interfac return diag.Errorf("Could not find an env0 template with name %s", name) } } else { - template, err = apiClient.Template(d.Id()) + template, err = apiClient.Template(d.Get("id").(string)) if err != nil { return diag.Errorf("Could not query template: %v", err) } @@ -111,6 +120,11 @@ func dataTemplateRead(ctx context.Context, d *schema.ResourceData, meta interfac d.Set("revision", template.Revision) d.Set("type", template.Type) d.Set("project_ids", template.ProjectIds) + sshKeyNames := []string{} + for _, sshKey := range template.SshKeys { + sshKeyNames = append(sshKeyNames, sshKey.Name) + } + d.Set("ssh_key_names", sshKeyNames) if template.Retry.OnDeploy != nil { d.Set("retries_on_deploy", template.Retry.OnDeploy.Times) d.Set("retry_on_deploy_only_when_matches_regex", template.Retry.OnDeploy.ErrorRegex) diff --git a/env0tfprovider/provider.go b/env0tfprovider/provider.go index 38492133..d725c450 100644 --- a/env0tfprovider/provider.go +++ b/env0tfprovider/provider.go @@ -37,11 +37,13 @@ func Provider() *schema.Provider { "env0_project": dataProject(), "env0_configuration_variable": dataConfigurationVariable(), "env0_template": dataTemplate(), + "env0_ssh_key": dataSshKey(), }, ResourcesMap: map[string]*schema.Resource{ "env0_project": resourceProject(), "env0_configuration_variable": resourceConfigurationVariable(), "env0_template": resourceTemplate(), + "env0_ssh_key": resourceSshKey(), }, ConfigureFunc: configureProvider, } diff --git a/env0tfprovider/resource_sshkey.go b/env0tfprovider/resource_sshkey.go new file mode 100644 index 00000000..49190b11 --- /dev/null +++ b/env0tfprovider/resource_sshkey.go @@ -0,0 +1,98 @@ +package env0tfprovider + +import ( + "context" + "errors" + + "github.com/env0/terraform-provider-env0/env0apiclient" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceSshKey() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSshKeyCreate, + ReadContext: resourceSshKeyRead, + DeleteContext: resourceSshKeyDelete, + + Importer: &schema.ResourceImporter{StateContext: resourceSshKeyImport}, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "name to give the ssh key", + Required: true, + ForceNew: true, + }, + "value": { + Type: schema.TypeString, + Description: "value is a private key in PEM format (first line usually looks like -----BEGIN OPENSSH PRIVATE KEY-----)", + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceSshKeyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(*env0apiclient.ApiClient) + + request := env0apiclient.SshKeyCreatePayload{ + Name: d.Get("name").(string), + Value: d.Get("value").(string), + } + sshKey, err := apiClient.SshKeyCreate(request) + if err != nil { + return diag.Errorf("could not create ssh key: %v", err) + } + + d.SetId(sshKey.Id) + + return nil +} + +func resourceSshKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(*env0apiclient.ApiClient) + + sshKeys, err := apiClient.SshKeys() + if err != nil { + return diag.Errorf("could not query ssh keys: %v", err) + } + found := false + for _, candidate := range sshKeys { + if candidate.Id == d.Id() { + found = true + } + } + if !found { + return diag.Errorf("ssh key %s not found", d.Id()) + } + + return nil +} + +func resourceSshKeyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(*env0apiclient.ApiClient) + + id := d.Id() + err := apiClient.SshKeyDelete(id) + if err != nil { + return diag.Errorf("could not delete ssh key: %v", err) + } + return nil +} + +func resourceSshKeyImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + return nil, errors.New("Not implemented") + // apiClient := meta.(*env0apiclient.ApiClient) + + // id := d.Id() + // ssh key, err := apiClient.SshKey(id) + // if err != nil { + // return nil, err + // } + + // d.Set("name", ssh key.Name) + + // return []*schema.ResourceData{d}, nil +} diff --git a/env0tfprovider/resource_template.go b/env0tfprovider/resource_template.go index 56f79296..7e41f0ae 100644 --- a/env0tfprovider/resource_template.go +++ b/env0tfprovider/resource_template.go @@ -59,16 +59,24 @@ func resourceTemplate() *schema.Resource { Description: "env0_project.id for each project", }, }, + "ssh_key_names": { + Type: schema.TypeList, + Description: "names of env0 defined ssh keys to use when accessing git over ssh", + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + Description: "env0_ssh_key.name for each ssh key", + }, + }, "retries_on_deploy": { Type: schema.TypeInt, Description: "number of times to retry when deploying an environment based on this template", Optional: true, }, "retry_on_deploy_only_when_matches_regex": { - Type: schema.TypeString, - Description: "if specified, will only retry (on deploy) if error matches specified regex", - Optional: true, - AtLeastOneOf: []string{"retries_on_deploy"}, + Type: schema.TypeString, + Description: "if specified, will only retry (on deploy) if error matches specified regex", + Optional: true, }, "retries_on_destroy": { Type: schema.TypeInt, @@ -76,10 +84,9 @@ func resourceTemplate() *schema.Resource { Optional: true, }, "retry_on_destroy_only_when_matches_regex": { - Type: schema.TypeString, - Description: "if specified, will only retry (on destroy) if error matches specified regex", - Optional: true, - AtLeastOneOf: []string{"retries_on_destroy"}, + Type: schema.TypeString, + Description: "if specified, will only retry (on destroy) if error matches specified regex", + Optional: true, }, }, } @@ -114,6 +121,12 @@ func templateCreatePayloadFromParameters(d *schema.ResourceData) (env0apiclient. result.ProjectIds = append(result.ProjectIds, projectId.(string)) } } + if sshKeyNames, ok := d.GetOk("ssh_key_names"); ok { + result.SshKeys = []env0apiclient.TemplateSshKey{} + for _, sshKeyName := range sshKeyNames.([]interface{}) { + result.SshKeys = append(result.SshKeys, env0apiclient.TemplateSshKey{Name: sshKeyName.(string)}) + } + } onDeployRetries, hasRetriesOnDeploy := d.GetOk("retries_on_deploy") if hasRetriesOnDeploy { if result.Retry == nil { diff --git a/examples/003_configuration_variable/main.tf b/examples/003_configuration_variable/main.tf index a93a0137..595f10ce 100644 --- a/examples/003_configuration_variable/main.tf +++ b/examples/003_configuration_variable/main.tf @@ -35,3 +35,7 @@ data "env0_configuration_variable" "tested1" { output "tested1_value" { value = data.env0_configuration_variable.tested1.value } + +data "env0_configuration_variable" "tested2" { + id = env0_configuration_variable.tested1.id +} diff --git a/examples/004_template/main.tf b/examples/004_template/main.tf index b344f160..6001639f 100644 --- a/examples/004_template/main.tf +++ b/examples/004_template/main.tf @@ -46,3 +46,7 @@ output "tested2_template_repository" { output "tested2_template_path" { value = data.env0_template.tested2.path } + +data "env0_template" "tested3" { + id = env0_template.tested1.id +} diff --git a/examples/005_ssh_key/conf.tf b/examples/005_ssh_key/conf.tf new file mode 100644 index 00000000..58dc45e8 --- /dev/null +++ b/examples/005_ssh_key/conf.tf @@ -0,0 +1,13 @@ +terraform { + backend "local" { + } + required_providers { + env0 = { + source = "terraform-registry.env0.com/env0/env0" + } + } +} + +provider "env0" {} + +variable "second_run" {} diff --git a/examples/005_ssh_key/expected_outputs.json b/examples/005_ssh_key/expected_outputs.json new file mode 100644 index 00000000..f0c0fe8e --- /dev/null +++ b/examples/005_ssh_key/expected_outputs.json @@ -0,0 +1,3 @@ +{ + "name": "test key" +} \ No newline at end of file diff --git a/examples/005_ssh_key/main.tf b/examples/005_ssh_key/main.tf new file mode 100644 index 00000000..6eb2689b --- /dev/null +++ b/examples/005_ssh_key/main.tf @@ -0,0 +1,32 @@ +resource "tls_private_key" "throwaway" { + algorithm = "RSA" +} +output "public_key_you_need_to_add_to_github_ssh_keys" { + value = tls_private_key.throwaway.public_key_openssh +} + +resource "env0_ssh_key" "tested" { + name = "test key" + value = tls_private_key.throwaway.private_key_pem +} + +data "env0_ssh_key" "tested" { + name = "test key" + depends_on = [env0_ssh_key.tested] +} + +data "env0_ssh_key" "tested2" { + id = env0_ssh_key.tested.id +} + +output "name" { + value = data.env0_ssh_key.tested2.name +} + +resource "env0_template" "usage" { + name = "use a ssh key" + description = "use a ssh key" + type = "terraform" + repository = "https://github.com/shlomimatichin/env0-template-jupyter-gpu" + ssh_key_names = [env0_ssh_key.tested.name] +} diff --git a/tests/harness.go b/tests/harness.go index dd533564..12df3ec4 100644 --- a/tests/harness.go +++ b/tests/harness.go @@ -31,7 +31,7 @@ func main() { } func runTest(testName string, destroy bool) bool { - testDir := "tests/" + testName + testDir := "examples/" + testName toDelete := []string{ ".terraform", ".terraform.lock.hcl", @@ -49,13 +49,13 @@ func runTest(testName string, destroy bool) bool { return false } terraformCommand(testName, "fmt") + if destroy { + defer terraformDestory(testName) + } _, err = terraformCommand(testName, "apply", "-auto-approve", "-var", "second_run=0") if err != nil { return false } - if destroy { - defer terraformDestory(testName) - } _, err = terraformCommand(testName, "apply", "-auto-approve", "-var", "second_run=1") if err != nil { return false @@ -206,8 +206,11 @@ func buildFakeTerraformRegistry() { terraformRc := fmt.Sprintf(` provider_installation { filesystem_mirror { - path = "%s/tests/fake_registry" - include = ["terraform-registry.env0.com/*/*"] + path = "%s/tests/fake_registry" + include = ["terraform-registry.env0.com/*/*"] + } + direct { + exclude = ["terraform-registry.env0.com/*/*"] } }`, cwd) err = ioutil.WriteFile("tests/terraform.rc", []byte(terraformRc), 0644)