diff --git a/CHANGELOG.md b/CHANGELOG.md index da030050..7de7e1bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **New data source:** `scalr_module_version` ([#76](https://github.com/Scalr/terraform-provider-scalr/pull/76)) +- **New resource:** `scalr_module` ([#76](https://github.com/Scalr/terraform-provider-scalr/pull/76)) + +### Changed +- `scalr_workspace`: new attribute `module_version_id` ([#76](https://github.com/Scalr/terraform-provider-scalr/pull/76)) + ### Fixed - `data.scalr_access_policy`: return error if access policy is not found ([#83](https://github.com/Scalr/terraform-provider-scalr/pull/83)) diff --git a/docs/data-sources/scalr_module_version.md b/docs/data-sources/scalr_module_version.md new file mode 100644 index 00000000..3444fd17 --- /dev/null +++ b/docs/data-sources/scalr_module_version.md @@ -0,0 +1,32 @@ +--- +layout: "scalr" +page_title: "Scalr: scalr_module_version" +sidebar_current: "docs-datasource-module-version-x" +description: |- Get information on the module version. +--- + +# scalr_module_version Data Source + +This data source is used to retrieve module version data by module source and semantic version. + +## Example Usage + +```hcl +data "scalr_module_version" "example" { + source = "env-xxxxxx/resource-name/scalr" + version = "1.0.0" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `source` - (Required) The module source. +* `version` - (Optional) The semantic version based on module version was created. + +## Attribute Reference + +All arguments plus: + +* `id` - The identifier of а module version. Example: `modver-xxxx` diff --git a/docs/data-sources/scalr_workspace.md b/docs/data-sources/scalr_workspace.md index 63c39d41..f5180da5 100644 --- a/docs/data-sources/scalr_workspace.md +++ b/docs/data-sources/scalr_workspace.md @@ -25,7 +25,6 @@ The following arguments are supported: * `name` - (Required) Name of the workspace. * `environment_id` - (Required) ID of the environment, in the format `env-`. -* `vcs_provider_id` - (Optional) ID of vcs provider, in the format `vcs-`. ## Attribute Reference @@ -36,6 +35,8 @@ All arguments plus: * `operations` - Boolean indicates if the workspace is being used for remote execution. * `terraform_version` - The version of Terraform used for this workspace. * `working_directory` - A relative path that Terraform will execute within. +* `module_version_id` - The identifier of a module version in the format `modver-`. +* `vcs_provider_id` - The identifier of a VCS provider in the format `vcs-`. * `vcs_repo` - If workspace is linked to VCS repository this block shows the details, otherwise `{}` * `created_by` - Details of the user that created the workspace. * `has_resources` - The presence of active terraform resources in the current state version. diff --git a/docs/resources/scalr_module.md b/docs/resources/scalr_module.md new file mode 100644 index 00000000..c451c2f1 --- /dev/null +++ b/docs/resources/scalr_module.md @@ -0,0 +1,59 @@ +--- +layout: "scalr" +page_title: "Scalr: scalr_module" +sidebar_current: "docs-resource-scalr-module" +description: |- + Manages module. +--- + +# scalr_module Resource + +Manages the state of a module in the Private Modules Registry. Create and destroy operations are available only. + +## Example Usage + +Basic usage: + +```hcl +resource "scalr_module" "example" { + account_id = "acc-xxxxxxxxx" + environment_id = "env-xxxxxxxxx" + vcs_provider_id = "vcs-xxxxxxxxx" + vcs_repo { + identifier = "org/repo" + path = "example/terraform--" + tag_prefix = "aws/" + } +} + +``` + +## Argument Reference +* `vcs_provider_id` - (Required) The identifier of a VCS provider in the format `vcs-` +* `account_id` - (Optional) The identifier of the account in the format `acc-`. If it is not specified the module will be registered globally and available across the whole installation. +* `environment_id` - (Optional) The identifier of an environment in the format `env-`. If it is not specified the module will be registered at the account level and available across all environments within the account specified in `account_id` attribute. +* `vcs_repo` - (Required) Source configuration of a VCS repository + + The `vcs_repo` block supports: + + * `identifier` - (Required) The identifier of a VCS repository in the format `:org/:repo` (`:org/:project/:name` is used for Azure DevOps). It refers to a organization and a repository name in a VCS provider. + * `path` - (Optional) The path to the root module folder. It Is expected to have the format '/terraform--', where `` stands for any folder within the repository inclusively a repository root. + * `tag_prefix` - (Optional) Registry ignores tags which do not match specified prefix, e.g. `aws/`. + + +## Attribute Reference + +All arguments plus: + +* `id` - The identifier of a module in the format `mod--`. +* `module_provider` - Module provider name, e.g `aws`, `azurerm`, `google`, etc. +* `name` - Name of the module, e.g. `rds`, `compute`, `kubernetes-engine` + +* `source` - The source of a remote module in the private registry, e.g `env-xxxx/aws/vpc` + +## Import + +To import module use module ID as the import ID. For example: +```shell +terraform import scalr_module.example mod-tk4315k3lofu4i0 +``` diff --git a/docs/resources/scalr_workspace.md b/docs/resources/scalr_workspace.md index e5ede9ff..28df4b2c 100644 --- a/docs/resources/scalr_workspace.md +++ b/docs/resources/scalr_workspace.md @@ -37,6 +37,7 @@ resource "scalr_workspace" "example" { Defaults to `true`. * `terraform_version` - (Optional) The version of Terraform to use for this workspace. Defaults to the latest available version. * `working_directory` - (Optional) A relative path that Terraform will be run in. Defaults to the root of the repository `""`. +* `module_version_id` - (Optional) The identifier of a module version in the format `modver-`. This attribute conflicts with `vcs_provider_id` and `vcs_repo` attributes. * `vcs_provider_id` - (Optional) ID of vcs provider - required if vcs-repo present and vice versa, in the format `vcs-` * `vcs_repo` - (Optional) Settings for the workspace's VCS repository. diff --git a/go.mod b/go.mod index a879ed77..5daf6dec 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/terraform v0.12.0 github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/scalr/go-scalr v0.0.0-20210902095125-9ac71df3a092 + github.com/scalr/go-scalr v0.0.0-20210921104644-11ed590a3eeb ) go 1.13 diff --git a/go.sum b/go.sum index 579f466b..17371527 100644 --- a/go.sum +++ b/go.sum @@ -263,6 +263,12 @@ github.com/scalr/go-scalr v0.0.0-20210813140334-b0bce1821414 h1:F3ecDrzKMrqI701m github.com/scalr/go-scalr v0.0.0-20210813140334-b0bce1821414/go.mod h1:n2HQ6QxqyTySiSFTOpAL18SjCKtP+xUskMygO0KuuQU= github.com/scalr/go-scalr v0.0.0-20210902095125-9ac71df3a092 h1:KZcw4OdOFTGXcZkyd7shvLHWbtIAbH7OmiQn6+9FxnU= github.com/scalr/go-scalr v0.0.0-20210902095125-9ac71df3a092/go.mod h1:n2HQ6QxqyTySiSFTOpAL18SjCKtP+xUskMygO0KuuQU= +github.com/scalr/go-scalr v0.0.0-20210913151726-712e82087ab0 h1:xIpvA2VJm36b/uDqqmerV2zJslox/08yPXasXDQBI6c= +github.com/scalr/go-scalr v0.0.0-20210913151726-712e82087ab0/go.mod h1:n2HQ6QxqyTySiSFTOpAL18SjCKtP+xUskMygO0KuuQU= +github.com/scalr/go-scalr v0.0.0-20210914135105-7cd19c2b6f36 h1:e8jUFEOtMKkuijUz255dMpQyQEDTSCdpcBX8cKx8xKI= +github.com/scalr/go-scalr v0.0.0-20210914135105-7cd19c2b6f36/go.mod h1:n2HQ6QxqyTySiSFTOpAL18SjCKtP+xUskMygO0KuuQU= +github.com/scalr/go-scalr v0.0.0-20210921104644-11ed590a3eeb h1:G1dbk1MikEZEqzfrbiVOxhJcS08xEQHSdiCz1h4RMaM= +github.com/scalr/go-scalr v0.0.0-20210921104644-11ed590a3eeb/go.mod h1:n2HQ6QxqyTySiSFTOpAL18SjCKtP+xUskMygO0KuuQU= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= diff --git a/scalr/data_source_module_version.go b/scalr/data_source_module_version.go new file mode 100644 index 00000000..0246fb7e --- /dev/null +++ b/scalr/data_source_module_version.go @@ -0,0 +1,67 @@ +package scalr + +import ( + "errors" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + scalr "github.com/scalr/go-scalr" +) + +func dataSourceModuleVersion() *schema.Resource { + return &schema.Resource{ + Read: dataSourceModuleVersionRead, + Schema: map[string]*schema.Schema{ + "source": { + Type: schema.TypeString, + Required: true, + }, + "version": { + Type: schema.TypeString, + Optional: true, + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + }} +} + +func dataSourceModuleVersionRead(d *schema.ResourceData, meta interface{}) error { + scalrClient := meta.(*scalr.Client) + + source := d.Get("source").(string) + module, err := scalrClient.Modules.ReadBySource(ctx, source) + if err != nil { + if errors.Is(err, scalr.ErrResourceNotFound{}) { + return fmt.Errorf("Could not find module with source %s", source) + } + return fmt.Errorf("Error retrieving module: %v", err) + } + log.Printf("[DEBUG] Download module by source: %s", source) + + var mv *scalr.ModuleVersion + var version string + if v, ok := d.GetOk("version"); ok { + version = v.(string) + mv, err = scalrClient.ModuleVersions.ReadBySemanticVersion(ctx, module.ID, version) + } else { + if module.LatestModuleVersion == nil { + return errors.New("The module has no version tags") + } + mv, err = scalrClient.ModuleVersions.Read(ctx, module.LatestModuleVersion.ID) + } + + if err != nil { + if errors.Is(err, scalr.ErrResourceNotFound{}) { + return fmt.Errorf("Could not find module with source %s and version %s", source, version) + } + return fmt.Errorf("Error retrieving module version: %v", err) + } + log.Printf("[DEBUG] Download module version by source %s version: %s", source, version) + + d.SetId(mv.ID) + d.Set("version", mv.Version) + return nil +} diff --git a/scalr/data_source_workspace.go b/scalr/data_source_workspace.go index b9ebb045..8fc4f770 100644 --- a/scalr/data_source_workspace.go +++ b/scalr/data_source_workspace.go @@ -28,6 +28,10 @@ func dataSourceScalrWorkspace() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "module_version_id": { + Type: schema.TypeString, + Optional: true, + }, "auto_apply": { Type: schema.TypeBool, @@ -150,6 +154,10 @@ func dataSourceScalrWorkspaceRead(d *schema.ResourceData, meta interface{}) erro d.Set("working_directory", workspace.WorkingDirectory) d.Set("has_resources", workspace.HasResources) + if workspace.ModuleVersion != nil { + d.Set("module_version_id", workspace.ModuleVersion.ID) + } + if workspace.VcsProvider != nil { d.Set("vcs_provider_id", workspace.VcsProvider.ID) } diff --git a/scalr/provider.go b/scalr/provider.go index b719ba21..242bc6a1 100644 --- a/scalr/provider.go +++ b/scalr/provider.go @@ -63,14 +63,15 @@ func Provider() terraform.ResourceProvider { }, DataSourcesMap: map[string]*schema.Resource{ - "scalr_workspace": dataSourceScalrWorkspace(), - "scalr_workspace_ids": dataSourceScalrWorkspaceIDs(), - "scalr_current_run": dataSourceScalrCurrentRun(), - "scalr_endpoint": dataSourceScalrEndpoint(), - "scalr_webhook": dataSourceScalrWebhook(), - "scalr_environment": dataSourceScalrEnvironment(), - "scalr_role": dataSourceScalrRole(), - "scalr_access_policy": dataSourceScalrAccessPolicy(), + "scalr_workspace": dataSourceScalrWorkspace(), + "scalr_workspace_ids": dataSourceScalrWorkspaceIDs(), + "scalr_current_run": dataSourceScalrCurrentRun(), + "scalr_endpoint": dataSourceScalrEndpoint(), + "scalr_webhook": dataSourceScalrWebhook(), + "scalr_environment": dataSourceScalrEnvironment(), + "scalr_role": dataSourceScalrRole(), + "scalr_access_policy": dataSourceScalrAccessPolicy(), + "scalr_module_version": dataSourceModuleVersion(), }, ResourcesMap: map[string]*schema.Resource{ @@ -79,6 +80,7 @@ func Provider() terraform.ResourceProvider { "scalr_endpoint": resourceScalrEndpoint(), "scalr_webhook": resourceScalrWebhook(), "scalr_environment": resourceScalrEnvironment(), + "scalr_module": resourceScalrModule(), "scalr_role": resourceScalrRole(), "scalr_access_policy": resourceScalrAccessPolicy(), }, diff --git a/scalr/resource_scalr_module.go b/scalr/resource_scalr_module.go new file mode 100644 index 00000000..317068f0 --- /dev/null +++ b/scalr/resource_scalr_module.go @@ -0,0 +1,168 @@ +package scalr + +import ( + "errors" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/scalr/go-scalr" +) + +func resourceScalrModule() *schema.Resource { + return &schema.Resource{ + Create: resourceScalrModuleCreate, + Read: resourceScalrModuleRead, + Delete: resourceScalrModuleDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "module_provider": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "source": { + Type: schema.TypeString, + Computed: true, + }, + "vcs_repo": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "path": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "tag_prefix": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + "vcs_provider_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "account_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "environment_id": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + }, + }, + } +} + +func resourceScalrModuleCreate(d *schema.ResourceData, meta interface{}) error { + scalrClient := meta.(*scalr.Client) + + vcsRepo := d.Get("vcs_repo").([]interface{})[0].(map[string]interface{}) + vcsOpt := &scalr.ModuleVCSRepo{ + Identifier: *scalr.String(vcsRepo["identifier"].(string)), + } + if path, ok := vcsRepo["path"].(string); ok && path != "" { + vcsOpt.Path = scalr.String(path) + } + if prefix, ok := vcsRepo["tag_prefix"].(string); ok && prefix != "" { + vcsOpt.TagPrefix = scalr.String(prefix) + } + + opt := scalr.ModuleCreateOptions{ + VCSRepo: vcsOpt, + VcsProvider: &scalr.VcsProviderOptions{ID: d.Get("vcs_provider_id").(string)}, + } + + if accID, ok := d.GetOk("account_id"); ok { + opt.Account = &scalr.Account{ID: accID.(string)} + } + + if envID, ok := d.GetOk("environment_id"); ok { + opt.Environment = &scalr.Environment{ID: envID.(string)} + } + + m, err := scalrClient.Modules.Create(ctx, opt) + if err != nil { + return fmt.Errorf("Error creating module: %v", err) + } + + d.SetId(m.ID) + return resourceScalrModuleRead(d, meta) +} + +func resourceScalrModuleRead(d *schema.ResourceData, meta interface{}) error { + scalrClient := meta.(*scalr.Client) + id := d.Id() + log.Printf("[DEBUG] Read configuration of module: %s", id) + m, err := scalrClient.Modules.Read(ctx, id) + if err != nil { + if errors.Is(err, scalr.ErrResourceNotFound{}) { + log.Printf("[DEBUG] Module %s no longer exists", id) + d.SetId("") + return nil + } + return fmt.Errorf("Error reading configuration of module %s: %v", id, err) + } + + // Update the config. + d.Set("name", m.Name) + d.Set("provider", m.Provider) + d.Set("status", m.Status) + d.Set("source", m.Source) + d.Set("vcs_repo", []map[string]interface{}{{ + "identifier": m.VCSRepo.Identifier, + "path": m.VCSRepo.Path, + "tag_prefix": m.VCSRepo.TagPrefix, + }}) + d.Set("vcs_provider_id", m.VcsProvider.ID) + + if m.Account != nil { + d.Set("account_id", m.Account.ID) + } + if m.Environment != nil { + d.Set("environment_id", m.Environment.ID) + } + + return nil +} + +func resourceScalrModuleDelete(d *schema.ResourceData, meta interface{}) error { + scalrClient := meta.(*scalr.Client) + id := d.Id() + + log.Printf("[DEBUG] Delete module %s", id) + err := scalrClient.Modules.Delete(ctx, id) + if err != nil { + if errors.Is(err, scalr.ErrResourceNotFound{}) { + return nil + } + return fmt.Errorf("Error deleting module %s: %v", id, err) + } + + return nil +} diff --git a/scalr/resource_scalr_workspace.go b/scalr/resource_scalr_workspace.go index 42885188..64330a92 100644 --- a/scalr/resource_scalr_workspace.go +++ b/scalr/resource_scalr_workspace.go @@ -50,10 +50,15 @@ func resourceScalrWorkspace() *schema.Resource { }, "vcs_provider_id": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"module_version_id"}, + }, + "module_version_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"vcs_provider_id", "vcs_repo"}, }, - "auto_apply": { Type: schema.TypeBool, Optional: true, @@ -116,10 +121,11 @@ func resourceScalrWorkspace() *schema.Resource { }, "vcs_repo": { - Type: schema.TypeList, - Optional: true, - MinItems: 1, - MaxItems: 1, + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + ConflictsWith: []string{"module_version_id"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "identifier": { @@ -219,6 +225,10 @@ func resourceScalrWorkspaceCreate(d *schema.ResourceData, meta interface{}) erro options.WorkingDirectory = scalr.String(workingDir.(string)) } + if v, ok := d.GetOk("module_version_id"); ok { + options.ModuleVersion = &scalr.ModuleVersion{ID: v.(string)} + } + if vcsProviderID, ok := d.GetOk("vcs_provider_id"); ok { options.VcsProvider = &scalr.VcsProviderOptions{ ID: vcsProviderID.(string), @@ -297,6 +307,12 @@ func resourceScalrWorkspaceRead(d *schema.ResourceData, meta interface{}) error d.Set("vcs_provider_id", workspace.VcsProvider.ID) } + var mv string + if workspace.ModuleVersion != nil { + mv = workspace.ModuleVersion.ID + } + d.Set("module_version_id", mv) + var createdBy []interface{} if workspace.CreatedBy != nil { createdBy = append(createdBy, map[string]interface{}{ @@ -340,7 +356,8 @@ func resourceScalrWorkspaceUpdate(d *schema.ResourceData, meta interface{}) erro if d.HasChange("name") || d.HasChange("auto_apply") || d.HasChange("terraform_version") || d.HasChange("working_directory") || d.HasChange("vcs_repo") || - d.HasChange("operations") || d.HasChange("vcs_provider_id") || d.HasChange("hooks") { + d.HasChange("operations") || d.HasChange("vcs_provider_id") || d.HasChange("hooks") || + d.HasChange("module_version_id") { // Create a new options struct. options := scalr.WorkspaceUpdateOptions{ Name: scalr.String(d.Get("name").(string)), @@ -398,6 +415,12 @@ func resourceScalrWorkspaceUpdate(d *schema.ResourceData, meta interface{}) erro } } + if v, ok := d.GetOk("module_version_id"); ok { + options.ModuleVersion = &scalr.ModuleVersion{ + ID: v.(string), + } + } + log.Printf("[DEBUG] Update workspace %s", id) _, err := scalrClient.Workspaces.Update(ctx, id, options) if err != nil { @@ -419,8 +442,7 @@ func resourceScalrWorkspaceDelete(d *schema.ResourceData, meta interface{}) erro if errors.Is(err, scalr.ErrResourceNotFound{}) { return nil } - return fmt.Errorf( - "Error deleting workspace %s: %v", id, err) + return fmt.Errorf("Error deleting workspace %s: %v", id, err) } return nil