diff --git a/internal/services/devcenter/dev_center_catalogs_resource.go b/internal/services/devcenter/dev_center_catalogs_resource.go new file mode 100644 index 000000000000..56477c245676 --- /dev/null +++ b/internal/services/devcenter/dev_center_catalogs_resource.go @@ -0,0 +1,289 @@ +package devcenter + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-sdk/resource-manager/devcenter/2023-04-01/catalogs" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" +) + +type DevCenterCatalogsResourceModel struct { + Name string `tfschema:"name"` + ResourceGroupName string `tfschema:"resource_group_name"` + DevCenterID string `tfschema:"dev_center_id"` + CatalogGitHub []CatalogPropertiesModel `tfschema:"catalog_github"` + CatalogAdoGit []CatalogPropertiesModel `tfschema:"catalog_adogit"` +} + +type CatalogPropertiesModel struct { + URI string `tfschema:"uri"` + Branch string `tfschema:"branch"` + KeyVaultKeyUrl string `tfschema:"key_vault_key_url"` + Path string `tfschema:"path"` +} + +type DevCenterCatalogsResource struct{} + +var _ sdk.Resource = DevCenterCatalogsResource{} + +func (r DevCenterCatalogsResource) ModelObject() interface{} { + return &DevCenterCatalogsResourceModel{} +} + +func (r DevCenterCatalogsResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return catalogs.ValidateCatalogID +} + +func (r DevCenterCatalogsResource) ResourceType() string { + return "azurerm_dev_center_catalog" +} + +func (r DevCenterCatalogsResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Required: true, + Type: pluginsdk.TypeString, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "resource_group_name": commonschema.ResourceGroupName(), + + "dev_center_id": { + Required: true, + Type: pluginsdk.TypeString, + ForceNew: true, + ValidateFunc: catalogs.ValidateDevCenterID, + }, + + "catalog_github": CatalogPropertiesSchema(), + + "catalog_adogit": CatalogPropertiesSchema(), + } +} + +func (r DevCenterCatalogsResource) Attributes() map[string]*schema.Schema { + return map[string]*schema.Schema{} +} + +func (r DevCenterCatalogsResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var model DevCenterCatalogsResourceModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding %+v", err) + } + + client := metadata.Client.DevCenter.V20230401.Catalogs + subscriptionId := metadata.Client.Account.SubscriptionId + devCenterId, err := catalogs.ParseDevCenterID(model.DevCenterID) + if err != nil { + return fmt.Errorf("parsing dev center id: %+v", err) + } + devCenterName := devCenterId.DevCenterName + id := catalogs.NewCatalogID(subscriptionId, model.ResourceGroupName, devCenterName, model.Name) + + existing, err := client.Get(ctx, id) + if err != nil { + if !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for the presence of an existing %s: %+v", id, err) + } + } + if !response.WasNotFound(existing.HttpResponse) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + catalogProperties := catalogs.Catalog{ + Properties: &catalogs.CatalogProperties{ + AdoGit: expandCatalogProperties(model.CatalogAdoGit), + GitHub: expandCatalogProperties(model.CatalogGitHub), + }, + } + + if _, err := client.CreateOrUpdate(ctx, id, catalogProperties); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r DevCenterCatalogsResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var model DevCenterCatalogsResourceModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding %+v", err) + } + + client := metadata.Client.DevCenter.V20230401.Catalogs + id, err := catalogs.ParseCatalogID(metadata.ResourceData.Id()) + if err != nil { + return fmt.Errorf("parsing catalog id: %+v", err) + } + + resp, err := client.Get(ctx, *id) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + properties := resp.Model + if properties == nil { + return fmt.Errorf("retrieving %s: model was nil", id) + } + + if metadata.ResourceData.HasChange("catalog_github") { + properties.Properties.GitHub = expandCatalogProperties(model.CatalogGitHub) + } + + if metadata.ResourceData.HasChange("catalog_adogit") { + properties.Properties.AdoGit = expandCatalogProperties(model.CatalogAdoGit) + } + + if _, err := client.CreateOrUpdate(ctx, *id, *properties); err != nil { + return fmt.Errorf("updating %s: %+v", *id, err) + } + + return nil + }, + } +} + +func (r DevCenterCatalogsResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.DevCenter.V20230401.Catalogs + + id, err := catalogs.ParseCatalogID(metadata.ResourceData.Id()) + if err != nil { + return fmt.Errorf("parsing catalog id: %+v", err) + } + + resp, err := client.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + model := resp.Model + if model == nil { + return fmt.Errorf("retrieving %s: model was nil", id) + } + + state := DevCenterCatalogsResourceModel{ + Name: id.CatalogName, + ResourceGroupName: id.ResourceGroupName, + } + state.DevCenterID = catalogs.NewDevCenterID(id.SubscriptionId, id.ResourceGroupName, id.DevCenterName).ID() + + if properties := model.Properties; properties != nil { + if gitHub := properties.GitHub; gitHub != nil { + state.CatalogGitHub = []CatalogPropertiesModel{ + { + URI: pointer.From(gitHub.Uri), + Branch: pointer.From(gitHub.Branch), + KeyVaultKeyUrl: pointer.From(gitHub.SecretIdentifier), + Path: pointer.From(gitHub.Path), + }, + } + } + + if adoGit := properties.AdoGit; adoGit != nil { + state.CatalogAdoGit = []CatalogPropertiesModel{ + { + URI: pointer.From(adoGit.Uri), + Branch: pointer.From(adoGit.Branch), + KeyVaultKeyUrl: pointer.From(adoGit.SecretIdentifier), + Path: pointer.From(adoGit.Path), + }, + } + } + } + + return metadata.Encode(&state) + }, + } +} + +func (r DevCenterCatalogsResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.DevCenter.V20230401.Catalogs + + id, err := catalogs.ParseCatalogID(metadata.ResourceData.Id()) + if err != nil { + return fmt.Errorf("parsing catalog id: %+v", err) + } + + if err := client.DeleteThenPoll(ctx, *id); err != nil { + return fmt.Errorf("deleting %s: %+v", *id, err) + } + + return nil + }, + } +} + +func CatalogPropertiesSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + MaxItems: 1, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "uri": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "branch": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "key_vault_key_url": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "path": { + Type: pluginsdk.TypeString, + Required: true, + }, + }, + }, + } +} + +func expandCatalogProperties(input []CatalogPropertiesModel) *catalogs.GitCatalog { + if len(input) == 0 { + return nil + } + + return &catalogs.GitCatalog{ + Uri: pointer.To(input[0].URI), + Branch: pointer.To(input[0].Branch), + SecretIdentifier: pointer.To(input[0].KeyVaultKeyUrl), + Path: pointer.To(input[0].Path), + } +} diff --git a/internal/services/devcenter/dev_center_catalogs_resource_test.go b/internal/services/devcenter/dev_center_catalogs_resource_test.go new file mode 100644 index 000000000000..1ba4a8001a13 --- /dev/null +++ b/internal/services/devcenter/dev_center_catalogs_resource_test.go @@ -0,0 +1,167 @@ +package devcenter_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/go-azure-sdk/resource-manager/devcenter/2023-04-01/catalogs" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type DevCenterCatalogsResource struct{} + +func (r DevCenterCatalogsResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := catalogs.ParseCatalogID(state.ID) + if err != nil { + return nil, err + } + + resp, err := client.DevCenter.V20230401.Catalogs.Get(ctx, *id) + if err != nil { + return nil, fmt.Errorf("reading %s: %+v", *id, err) + } + + return utils.Bool(resp.Model != nil), nil +} + +func TestAccDevCenterCatalogs_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_dev_center_catalog", "test") + r := DevCenterCatalogsResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccDevCenterCatalogs_adoGit(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_dev_center_catalog", "test") + r := DevCenterCatalogsResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.adoGit(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccDevCenterCatalogs_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_dev_center_catalog", "test") + r := DevCenterCatalogsResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r DevCenterCatalogsResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_dev_center_catalog" "test" { + name = "acctest-catalog-%d" + resource_group_name = azurerm_resource_group.test.name + dev_center_id = azurerm_dev_center.test.id + catalog_github { + branch = "main" + path = "/template" + uri = "https://github.com/am-lim/deployment-environments.git" + key_vault_key_url = "https://amlim-kv.vault.azure.net/secrets/envTest/0a79f15246ce4b35a13957367b422cab" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r DevCenterCatalogsResource) adoGit(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_dev_center_catalog" "test" { + name = "acctest-catalog-%d" + resource_group_name = azurerm_resource_group.test.name + dev_center_id = azurerm_dev_center.test.id + catalog_adogit { + branch = "main" + path = "/template" + uri = "https://amlim@dev.azure.com/amlim/testCatalog/_git/testCatalog" + key_vault_key_url = "https://amlim-kv.vault.azure.net/secrets/ado/6279752c2bdd4a38a3e79d958cc36a75" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r DevCenterCatalogsResource) update(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_dev_center_catalog" "test" { + name = "acctest-catalog-%d" + resource_group_name = azurerm_resource_group.test.name + dev_center_id = azurerm_dev_center.test.id + catalog_github { + branch = "foo" + path = "" + uri = "https://github.com/am-lim/deployment-environments.git" + key_vault_key_url = "https://amlim-kv.vault.azure.net/secrets/envTest/0a79f15246ce4b35a13957367b422cab" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r DevCenterCatalogsResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctest-rg-%[1]d" + location = "%s" +} + +resource "azurerm_dev_center" "test" { + name = "acctdc-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + identity { + type = "SystemAssigned" + } +} +`, data.RandomInteger, "West Europe") +} diff --git a/internal/services/devcenter/registration.go b/internal/services/devcenter/registration.go index a02d120e2300..b243e5f276e0 100644 --- a/internal/services/devcenter/registration.go +++ b/internal/services/devcenter/registration.go @@ -32,6 +32,7 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { resources := []sdk.Resource{ DevCenterGalleryResource{}, + DevCenterCatalogsResource{}, } return append(resources, r.autoRegistration.Resources()...) } diff --git a/website/docs/r/dev_center_catalogs.html.markdown b/website/docs/r/dev_center_catalogs.html.markdown new file mode 100644 index 000000000000..5ebf1f117e26 --- /dev/null +++ b/website/docs/r/dev_center_catalogs.html.markdown @@ -0,0 +1,108 @@ +--- +subcategory: "Dev Center" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_dev_center_catalog" +description: |- + Manages a Dev Center Catalog. +--- +# azurerm_dev_center_catalog + +Manages a Dev Center Catalog. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} +resource "azurerm_dev_center" "example" { + location = azurerm_resource_group.example.location + name = "example" + resource_group_name = azurerm_resource_group.example.name + + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_dev_center_catalog" "example" { + name = "example" + resource_group_name = azurerm_resource_group.test.name + dev_center_id = azurerm_dev_center.test.id + catalog_github { + branch = "foo" + path = "" + uri = "example URI" + key_vault_key_url = "secret" + } +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of this Dev Center Catalog. Changing this forces a new Dev Center to be created. + +* `resource_group_name` - (Required) Specifies the name of the Resource Group within which this Dev Center Catalog should exist. Changing this forces a new Dev Center to be created. + +* `dev_center_id` - (Required) Specifies the Dev Center Id within which this Dev Center Catalog should exist. Changing this forces a new Dev Center Catalog to be created. + +* `catalog_github` - (Optional) A `catalog_github` block as defined below. + +* `catalog_adogit` - (Optional) A `catalog_adogit` block as defined below. + +--- + +* `catalog_github` supports the following: + +* `branch` - (Required) The Git branch of the Dev Center Catalog. + +* `path` - (Required) The folder where the catalog items can be found inside the repository. + +* `key_vault_key_url` - (Required) A reference to the Key Vault secret containing a security token to authenticate to a Git repository. + +* `uri` - (Required) The Git URI of the Dev Center Catalog. + +--- + +* `catalog_adogit` supports the following: + +* `branch` - (Required) The Git branch of the Dev Center Catalog. + +* `path` - (Required) The folder where the catalog items can be found inside the repository. + +* `key_vault_key_url` - (Required) A reference to the Key Vault secret containing a security token to authenticate to a Git repository. + +* `uri` - (Required) The Git URI of the Dev Center Catalog. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Dev Center Catalog. + +--- + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating this Dev Center Catalog. +* `delete` - (Defaults to 30 minutes) Used when deleting this Dev Center Catalog. +* `read` - (Defaults to 5 minutes) Used when retrieving this Dev Center Catalog. +* `update` - (Defaults to 30 minutes) Used when updating this Dev Center Catalog. + +## Import + +An existing Dev Center Catalog can be imported into Terraform using the `resource id`, e.g. + +```shell +terraform import azurerm_dev_center.example /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DevCenter/devCenters/{devCenterName}/catalogs/{catalogName} +``` + +* Where `{subscriptionId}` is the ID of the Azure Subscription where the Dev Center exists. For example `12345678-1234-9876-4563-123456789012`. +* Where `{resourceGroupName}` is the name of Resource Group where this Dev Center exists. For example `example-resource-group`. +* Where `{devCenterName}` is the name of the Dev Center. For example `devCenterValue`. +* Where `{catalogName}` is the name of the Dev Center Catalog. For example `catalogValue`.