diff --git a/.changelog/1099.txt b/.changelog/1099.txt new file mode 100644 index 000000000..7d36fc4cb --- /dev/null +++ b/.changelog/1099.txt @@ -0,0 +1,4 @@ +```release-note:new-resource + harness_platform_infra_module - added a new resource for iacm module registry + harness_platform_infra_module - added a new data source for iacm module registry +``` \ No newline at end of file diff --git a/docs/data-sources/platform_infra_module.md b/docs/data-sources/platform_infra_module.md new file mode 100644 index 000000000..757adba5e --- /dev/null +++ b/docs/data-sources/platform_infra_module.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "harness_platform_infra_module Data Source - terraform-provider-harness" +subcategory: "Next Gen" +description: |- + Data source for retrieving modules from the module registry. +--- + +# harness_platform_infra_module (Data Source) + +Data source for retrieving modules from the module registry. + +## Example Usage + +```terraform +data "harness_platform_infra_module" "test" { + identifier = "identifier" + name = "name" + system = "system" +} +``` + + +## Schema + +### Required + +- `id` (String) Identifier of the module +- `name` (String) Name of the module +- `system` (String) Provider of the module + +### Optional + +- `created` (Number) Timestamp when the module was created +- `description` (String) Description of the module +- `repository` (String) For account connectors, the repository where the module is stored +- `repository_branch` (String) Repository Branch in which the module should be accessed +- `repository_commit` (String) Repository Commit in which the module should be accessed +- `repository_connector` (String) Repository Connector is the reference to the connector for the repository +- `repository_path` (String) Repository Path is the path in which the module resides +- `repository_url` (String) URL where the module is stored +- `synced` (Number) Timestamp when the module was last synced +- `tags` (String) Tags associated with the module diff --git a/docs/resources/platform_infra_module.md b/docs/resources/platform_infra_module.md new file mode 100644 index 000000000..91a2500e6 --- /dev/null +++ b/docs/resources/platform_infra_module.md @@ -0,0 +1,50 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "harness_platform_infra_module Resource - terraform-provider-harness" +subcategory: "Next Gen" +description: |- + Resource for managing Terraform/Tofu Modules. +--- + +# harness_platform_infra_module (Resource) + +Resource for managing Terraform/Tofu Modules. + +## Example Usage + +```terraform +resource "harness_platform_infra_module" "example" { + description = "example" + name = "name" + system = "provider" + repository = "https://github.com/org/repo" + repository_branch = "main" + repository_path = "tf/aws/basic" + repository_connector = harness_platform_connector_github.test.id +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the module. +- `system` (String) Provider of the module. + +### Optional + +- `description` (String) Description of the module. +- `repository` (String) For account connectors, the repository where the module can be found +- `repository_branch` (String) Name of the branch to fetch the code from. This cannot be set if repository commit is set. +- `repository_commit` (String) Tag to fetch the code from. This cannot be set if repository branch is set. +- `repository_connector` (String) Reference to the connector to be used to fetch the code. +- `repository_path` (String) Path to the module within the repository. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import harness_platform_infra_module.example / +``` diff --git a/examples/data-sources/harness_platform_infra_module/data-source.tf b/examples/data-sources/harness_platform_infra_module/data-source.tf new file mode 100644 index 000000000..3892d7d74 --- /dev/null +++ b/examples/data-sources/harness_platform_infra_module/data-source.tf @@ -0,0 +1,5 @@ +data "harness_platform_infra_module" "test" { + identifier = "identifier" + name = "name" + system = "system" +} diff --git a/examples/resources/harness_platform_infra_module/import.sh b/examples/resources/harness_platform_infra_module/import.sh new file mode 100644 index 000000000..b75c12b10 --- /dev/null +++ b/examples/resources/harness_platform_infra_module/import.sh @@ -0,0 +1 @@ +terraform import harness_platform_infra_module.example diff --git a/examples/resources/harness_platform_infra_module/resource.tf b/examples/resources/harness_platform_infra_module/resource.tf new file mode 100644 index 000000000..11b6c9ae1 --- /dev/null +++ b/examples/resources/harness_platform_infra_module/resource.tf @@ -0,0 +1,9 @@ +resource "harness_platform_infra_module" "example" { + description = "example" + name = "name" + system = "provider" + repository = "https://github.com/org/repo" + repository_branch = "main" + repository_path = "tf/aws/basic" + repository_connector = harness_platform_connector_github.test.id +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f0933fccb..a2908e3fe 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "github.com/harness/terraform-provider-harness/internal/service/platform/module_registry" cdng_service "github.com/harness/terraform-provider-harness/internal/service/cd_nextgen/service" "github.com/harness/terraform-provider-harness/internal/service/platform/service_account" "log" @@ -294,6 +295,7 @@ func Provider(version string) func() *schema.Provider { "harness_governance_rule": governance_rule.DatasourceRule(), "harness_governance_rule_set": governance_rule_set.DatasourceRuleSet(), "harness_cluster_orchestrator": cluster_orchestrator.DataSourceClusterOrchestrator(), + "harness_platform_infra_module": module_registry.DataSourceInfraModule(), }, ResourcesMap: map[string]*schema.Resource{ "harness_platform_template": pipeline_template.ResourceTemplate(), @@ -441,6 +443,7 @@ func Provider(version string) func() *schema.Provider { "harness_governance_rule": governance_rule.ResourceRule(), "harness_governance_rule_set": governance_rule_set.ResourceRuleSet(), "harness_cluster_orchestrator": cluster_orchestrator.ResourceClusterOrchestrator(), + "harness_platform_infra_module": module_registry.ResourceInfraModule(), }, } diff --git a/internal/service/platform/module_registry/data_source_infra_module.go b/internal/service/platform/module_registry/data_source_infra_module.go new file mode 100644 index 000000000..a581af900 --- /dev/null +++ b/internal/service/platform/module_registry/data_source_infra_module.go @@ -0,0 +1,88 @@ +package module_registry + +import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + +func DataSourceInfraModule() *schema.Resource { + resource := &schema.Resource{ + Description: "Data source for retrieving modules from the module registry.", + ReadContext: resourceInfraModuleRead, + Schema: map[string]*schema.Schema{ + "id": { + Description: "Identifier of the module", + Type: schema.TypeString, + Required: true, + }, + "description": { + Description: "Description of the module", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "name": { + Description: "Name of the module", + Type: schema.TypeString, + Required: true, + }, + "system": { + Description: "Provider of the module", + Type: schema.TypeString, + Required: true, + }, + "repository": { + Description: "For account connectors, the repository where the module is stored", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "repository_branch": { + Description: "Repository Branch in which the module should be accessed", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "repository_commit": { + Description: "Repository Commit in which the module should be accessed", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "repository_connector": { + Description: "Repository Connector is the reference to the connector for the repository", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "repository_path": { + Description: "Repository Path is the path in which the module resides", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "created": { + Description: "Timestamp when the module was created", + Type: schema.TypeInt, + Computed: true, + Optional: true, + }, + "repository_url": { + Description: "URL where the module is stored", + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "synced": { + Description: "Timestamp when the module was last synced", + Type: schema.TypeInt, + Computed: true, + Optional: true, + }, + "tags": { + Description: "Tags associated with the module", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } + return resource +} diff --git a/internal/service/platform/module_registry/data_source_infra_module_test.go b/internal/service/platform/module_registry/data_source_infra_module_test.go new file mode 100644 index 000000000..f5e3fd403 --- /dev/null +++ b/internal/service/platform/module_registry/data_source_infra_module_test.go @@ -0,0 +1,70 @@ +package module_registry_test + +import ( + "fmt" + "github.com/harness/terraform-provider-harness/internal/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "strings" + "testing" +) + +func TestAccDataSourceInfraModule(t *testing.T) { + name := strings.ToLower(t.Name()) + system := strings.ToLower(t.Name()) + resourceName := "harness_platform_infra_module.test" + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceInfraModule(name, system), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "system", system), + ), + }, + }, + }) +} + +func testAccDataSourceInfraModule(name string, system string) string { + return fmt.Sprintf(` + resource "harness_platform_secret_text" "test" { + identifier = "%[1]s" + name = "%[2]s" + description = "test" + tags = ["foo:bar"] + + secret_manager_identifier = "harnessSecretManager" + value_type = "Inline" + value = "secret" + } + resource "harness_platform_connector_github" "test" { + identifier = "%[1]s" + name = "%[2]s" + description = "test" + tags = ["foo:bar"] + + url = "https://github.com/account" + connection_type = "Account" + validation_repo = "some_repo" + delegate_selectors = ["harness-delegate"] + credentials { + http { + username = "admin" + token_ref = "account.${harness_platform_secret_text.test.id}" + } + } + } + + resource "harness_platform_infra_module" "test" { + name = "%[1]s" + system = "%[2]s" + description = "description" + repository = "https://github.com/org/repo" + repository_branch = "main" + repository_path = "tf/aws/basic" + repository_connector = "account.${harness_platform_connector_github.test.id}" + } + `, name, system) +} diff --git a/internal/service/platform/module_registry/resource_infra_module.go b/internal/service/platform/module_registry/resource_infra_module.go new file mode 100644 index 000000000..aed163123 --- /dev/null +++ b/internal/service/platform/module_registry/resource_infra_module.go @@ -0,0 +1,283 @@ +package module_registry + +import ( + "context" + "encoding/json" + "github.com/harness/harness-go-sdk/harness/nextgen" + "github.com/harness/terraform-provider-harness/helpers" + "github.com/harness/terraform-provider-harness/internal" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "log" + "net/http" +) + +func ResourceInfraModule() *schema.Resource { + resource := &schema.Resource{ + Description: "Resource for managing Terraform/Tofu Modules.", + ReadContext: resourceInfraModuleRead, + CreateContext: resourceInfraModuleCreate, + UpdateContext: resourceInfraModuleUpdate, + DeleteContext: resourceInfraModuleDelete, + Importer: helpers.AccountLevelResourceImporter, + + Schema: map[string]*schema.Schema{ + "description": { + Description: "Description of the module.", + Type: schema.TypeString, + Optional: true, + }, + "name": { + Description: "Name of the module.", + Type: schema.TypeString, + Required: true, + }, + "system": { + Description: "Provider of the module.", + Type: schema.TypeString, + Required: true, + }, + "repository": { + Description: "For account connectors, the repository where the module can be found", + Type: schema.TypeString, + Optional: true, + }, + "repository_branch": { + Description: "Name of the branch to fetch the code from. This cannot be set if repository commit is set.", + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"repository_commit", "repository_branch"}, + }, + "repository_commit": { + Description: "Tag to fetch the code from. This cannot be set if repository branch is set.", + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"repository_commit", "repository_branch"}, + }, + "repository_connector": { + Description: "Reference to the connector to be used to fetch the code.", + Type: schema.TypeString, + Optional: true, + }, + "repository_path": { + Description: "Path to the module within the repository.", + Type: schema.TypeString, + Optional: true, + }, + "created": { + Description: "Timestamp when the module was created.", + Type: schema.TypeInt, + Computed: true, + Optional: true, + }, + "id": { + Description: "Unique identifier of the module.", + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "repository_url": { + Description: "URL of the repository where the module is stored.", + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "synced": { + Description: "Timestamp when the module was last synced.", + Type: schema.TypeInt, + Computed: true, + Optional: true, + }, + "tags": { + Description: "Git tags associated with the module.", + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "versions": { + Description: "List of versions of the module.", + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Optional: true, + }, + }, + } + return resource +} + +func resourceInfraModuleRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c, ctx := m.(*internal.Session).GetPlatformClientWithContext(ctx) + id := d.Id() + if id == "" { + d.MarkNewResource() + } + resp, httpRes, err := c.ModuleRegistryApi.ModuleRegistryListModulesById( + ctx, + d.Get("id").(string), + c.AccountId, + ) + if err != nil { + return helpers.HandleApiError(err, d, httpRes) + } + readModule(d, &resp) + return nil +} + +func resourceInfraModuleCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c, ctx := m.(*internal.Session).GetPlatformClientWithContext(ctx) + id := d.Id() + if id == "" { + d.MarkNewResource() + } + createModule, err := buildCreateModuleRequestBody(d) + if err != nil { + return diag.FromErr(err) + } + log.Printf("[DEBUG] Creating module with value %v", createModule) + modRes, httpRes, err := c.ModuleRegistryApi.ModuleRegistryCreateModule(ctx, createModule, c.AccountId) + + if err != nil { + return parseError(err, httpRes) + } + setModuleId(d, &modRes) + resourceInfraModuleRead(ctx, d, m) + return nil +} + +func resourceInfraModuleDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c, ctx := m.(*internal.Session).GetPlatformClientWithContext(ctx) + id := d.Id() + if id == "" { + return nil + } + httpRes, err := c.ModuleRegistryApi.ModuleRegistryDeleteModule(ctx, id, c.AccountId) + if err != nil { + return parseError(err, httpRes) + } + return nil +} + +func resourceInfraModuleUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c, ctx := m.(*internal.Session).GetPlatformClientWithContext(ctx) + id := d.Id() + if id == "" { + d.MarkNewResource() + } + updateModule, err := buildUpdateModuleRequestBody(d) + if err != nil { + return diag.FromErr(err) + } + httpRes, err := c.ModuleRegistryApi.ModuleRegistryUpdateModule(ctx, updateModule, c.AccountId, id) + if err != nil { + return parseError(err, httpRes) + } + resourceInfraModuleRead(ctx, d, m) + return nil +} + +func setModuleId(d *schema.ResourceData, module *nextgen.CreateModuleResponseBody) { + d.SetId(module.Id) +} + +func readModule(d *schema.ResourceData, module *nextgen.ModuleResource2) { + d.SetId(module.Id) + d.Set("id", module.Id) + d.Set("description", module.Description) + d.Set("name", module.Name) + d.Set("system", module.System) + d.Set("repository", module.Repository) + d.Set("repository_branch", module.RepositoryBranch) + d.Set("repository_commit", module.RepositoryCommit) + d.Set("repository_connector", module.RepositoryConnector) + d.Set("repository_path", module.RepositoryPath) + d.Set("created", module.Created) + d.Set("repository_url", module.RepositoryUrl) + d.Set("synced", module.Synced) + d.Set("tags", module.Tags) + d.Set("versions", module.Versions) +} + +func buildCreateModuleRequestBody(d *schema.ResourceData) (nextgen.CreateModuleRequestBody, error) { + module := nextgen.CreateModuleRequestBody{ + Name: d.Get("name").(string), + System: d.Get("system").(string), + } + + if desc, ok := d.GetOk("description"); ok { + module.Description = desc.(string) + } + if repo, ok := d.GetOk("repository"); ok { + module.Repository = repo.(string) + } + if repoBranch, ok := d.GetOk("repository_branch"); ok { + module.RepositoryBranch = repoBranch.(string) + } + if repoCommit, ok := d.GetOk("repository_commit"); ok { + module.RepositoryCommit = repoCommit.(string) + } + if repoConnector, ok := d.GetOk("repository_connector"); ok { + module.RepositoryConnector = repoConnector.(string) + } + if repoPath, ok := d.GetOk("repository_path"); ok { + module.RepositoryPath = repoPath.(string) + } + return module, nil +} + +func buildUpdateModuleRequestBody(d *schema.ResourceData) (nextgen.CreateModuleRequestBody, error) { + module := nextgen.CreateModuleRequestBody{ + Name: d.Get("name").(string), + System: d.Get("system").(string), + } + + if desc, ok := d.GetOk("description"); ok { + module.Description = desc.(string) + } + if repo, ok := d.GetOk("repository"); ok { + module.Repository = repo.(string) + } + if repoBranch, ok := d.GetOk("repository_branch"); ok { + module.RepositoryBranch = repoBranch.(string) + } + if repoCommit, ok := d.GetOk("repository_commit"); ok { + module.RepositoryCommit = repoCommit.(string) + } + if repoConnector, ok := d.GetOk("repository_connector"); ok { + module.RepositoryConnector = repoConnector.(string) + } + if repoPath, ok := d.GetOk("repository_path"); ok { + module.RepositoryPath = repoPath.(string) + } + return module, nil +} + +func parseError(err error, httpResp *http.Response) diag.Diagnostics { + if httpResp != nil && httpResp.StatusCode == 401 { + return diag.Errorf(httpResp.Status + "\n" + "Hint:\n" + + "1) Please check if token has expired or is wrong.\n" + + "2) Harness Provider is misconfigured. For firstgen resources please give the correct api_key and for nextgen resources please give the correct platform_api_key.") + } + if httpResp != nil && httpResp.StatusCode == 403 { + return diag.Errorf(httpResp.Status + "\n" + "Hint:\n" + + "1) Please check if the token has required permission for this operation.\n" + + "2) Please check if the token has expired or is wrong.") + } + + se, ok := err.(nextgen.GenericSwaggerError) + if !ok { + diag.FromErr(err) + } + + iacmErrBody := se.Body() + iacmErr := nextgen.IacmError{} + jsonErr := json.Unmarshal(iacmErrBody, &iacmErr) + if jsonErr != nil { + return diag.Errorf(err.Error()) + } + + return diag.Errorf(httpResp.Status + "\n" + "Hint:\n" + + "1) " + iacmErr.Message) +} diff --git a/internal/service/platform/module_registry/resource_infra_module_test.go b/internal/service/platform/module_registry/resource_infra_module_test.go new file mode 100644 index 000000000..b7d625eaa --- /dev/null +++ b/internal/service/platform/module_registry/resource_infra_module_test.go @@ -0,0 +1,120 @@ +package module_registry_test + +import ( + "fmt" + "github.com/harness/harness-go-sdk/harness/nextgen" + "github.com/harness/terraform-provider-harness/internal/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "strings" + "testing" +) + +func TestAccResourceModule(t *testing.T) { + resourceName := "harness_platform_infra_module.test" + name := strings.ToLower(t.Name()) + system := strings.ToLower(t.Name()) + updatedName := fmt.Sprintf("%s_updated", name) + updatedSystem := fmt.Sprintf("%supdated", system) + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccResourceWorkspaceDestroy(resourceName), + Steps: []resource.TestStep{ + { + Config: testAccResourceModule(name, system), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "system", system), + ), + }, + { + Config: testAccResourceModule(updatedName, updatedSystem), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", updatedName), + resource.TestCheckResourceAttr(resourceName, "system", updatedSystem), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AccountLevelResourceImportStateIdFunc(resourceName), + }, + }, + }) +} + +func testAccResourceWorkspaceDestroy(resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + mod, _ := testAccGetPlatformModule(resourceName, state) + if mod != nil { + return fmt.Errorf("module not found: %s %s", mod.Name, mod.System) + } + return nil + } +} + +func testAccGetPlatformModule(resourceName string, state *terraform.State) (*nextgen.ModuleResource2, error) { + r := acctest.TestAccGetResource(resourceName, state) + c, ctx := acctest.TestAccGetPlatformClientWithContext() + id := r.Primary.ID + + module, resp, err := c.ModuleRegistryApi.ModuleRegistryListModulesById( + ctx, + id, + c.AccountId, + ) + + if err != nil { + return nil, err + } + + if resp == nil { + return nil, nil + } + + return &module, nil +} + +func testAccResourceModule(name, system string) string { + + return fmt.Sprintf(` + resource "harness_platform_secret_text" "test" { + identifier = "%[1]s" + name = "%[2]s" + description = "test" + tags = ["foo:bar"] + + secret_manager_identifier = "harnessSecretManager" + value_type = "Inline" + value = "secret" + } + resource "harness_platform_connector_github" "test" { + identifier = "%[1]s" + name = "%[2]s" + description = "test" + tags = ["foo:bar"] + + url = "https://github.com/account" + connection_type = "Account" + validation_repo = "some_repo" + delegate_selectors = ["harness-delegate"] + credentials { + http { + username = "admin" + token_ref = "account.${harness_platform_secret_text.test.id}" + } + } + } + + resource "harness_platform_infra_module" "test" { + name = "%[1]s" + system = "%[2]s" + description = "description" + repository = "https://github.com/org/repo" + repository_branch = "main" + repository_path = "tf/aws/basic" + repository_connector = "account.${harness_platform_connector_github.test.id}" + } + `, name, system) +}