diff --git a/internal/services/containerapps/container_app_environment_resource.go b/internal/services/containerapps/container_app_environment_resource.go index 5f253a19ad27..c89593ffd7b4 100644 --- a/internal/services/containerapps/container_app_environment_resource.go +++ b/internal/services/containerapps/container_app_environment_resource.go @@ -10,9 +10,8 @@ import ( "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-helpers/resourcemanager/tags" "github.com/hashicorp/go-azure-sdk/resource-manager/containerapps/2022-03-01/managedenvironments" + "github.com/hashicorp/go-azure-sdk/resource-manager/operationalinsights/2020-08-01/workspaces" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" - loganalyticsParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/loganalytics/parse" - loganalyticsValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/loganalytics/validate" networkValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/network/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" @@ -77,7 +76,7 @@ func (r ContainerAppEnvironmentResource) Arguments() map[string]*pluginsdk.Schem Type: pluginsdk.TypeString, Required: true, ForceNew: true, - ValidateFunc: loganalyticsValidate.LogAnalyticsWorkspaceID, + ValidateFunc: workspaces.ValidateWorkspaceID, Description: "The ID for the Log Analytics Workspace to link this Container Apps Managed Environment to.", }, @@ -178,7 +177,6 @@ func (r ContainerAppEnvironmentResource) Create() sdk.ResourceFunc { Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { client := metadata.Client.ContainerApps.ManagedEnvironmentClient logAnalyticsClient := metadata.Client.LogAnalytics.WorkspacesClient - sharedKeyClient := metadata.Client.LogAnalytics.SharedKeysClient subscriptionId := metadata.Client.Account.SubscriptionId var containerAppEnvironment ContainerAppEnvironmentModel @@ -200,26 +198,26 @@ func (r ContainerAppEnvironmentResource) Create() sdk.ResourceFunc { return metadata.ResourceRequiresImport(r.ResourceType(), id) } - logAnalyticsId, err := loganalyticsParse.LogAnalyticsWorkspaceID(containerAppEnvironment.LogAnalyticsWorkspaceId) + logAnalyticsId, err := workspaces.ParseWorkspaceID(containerAppEnvironment.LogAnalyticsWorkspaceId) if err != nil { return err } - workspace, err := logAnalyticsClient.Get(ctx, logAnalyticsId.ResourceGroup, logAnalyticsId.WorkspaceName) - if err != nil || workspace.WorkspaceProperties == nil { + workspace, err := logAnalyticsClient.Get(ctx, *logAnalyticsId) + if err != nil || workspace.Model == nil || workspace.Model.Properties == nil { return fmt.Errorf("retrieving %s for %s: %+v", logAnalyticsId, id, err) } - if workspace.WorkspaceProperties.CustomerID == nil { + if workspace.Model.Properties.CustomerId == nil { return fmt.Errorf("reading customer ID from %s", logAnalyticsId) } - keys, err := sharedKeyClient.GetSharedKeys(ctx, logAnalyticsId.ResourceGroup, logAnalyticsId.WorkspaceName) - if err != nil { + keys, err := logAnalyticsClient.SharedKeysGetSharedKeys(ctx, *logAnalyticsId) + if err != nil || keys.Model == nil { return fmt.Errorf("retrieving access keys to %s for %s: %+v", logAnalyticsId, id, err) } - if keys.PrimarySharedKey == nil { + if keys.Model.PrimarySharedKey == nil { return fmt.Errorf("reading shared key for %s in %s", logAnalyticsId, id) } @@ -230,8 +228,8 @@ func (r ContainerAppEnvironmentResource) Create() sdk.ResourceFunc { AppLogsConfiguration: &managedenvironments.AppLogsConfiguration{ Destination: utils.String("log-analytics"), LogAnalyticsConfiguration: &managedenvironments.LogAnalyticsConfiguration{ - CustomerId: workspace.WorkspaceProperties.CustomerID, - SharedKey: keys.PrimarySharedKey, + CustomerId: workspace.Model.Properties.CustomerId, + SharedKey: keys.Model.PrimarySharedKey, }, }, VnetConfiguration: &managedenvironments.VnetConfiguration{}, diff --git a/internal/services/containerapps/container_app_resource.go b/internal/services/containerapps/container_app_resource.go index 7dddfb42fee8..8d8e061a8f8e 100644 --- a/internal/services/containerapps/container_app_resource.go +++ b/internal/services/containerapps/container_app_resource.go @@ -49,7 +49,7 @@ type ContainerAppModel struct { var _ sdk.ResourceWithUpdate = ContainerAppResource{} -//var _ sdk.ResourceWithCustomizeDiff = ContainerAppResource{} +var _ sdk.ResourceWithCustomizeDiff = ContainerAppResource{} func (r ContainerAppResource) ModelObject() interface{} { return &ContainerAppModel{} @@ -372,6 +372,11 @@ func (r ContainerAppResource) Update() sdk.ResourceFunc { } model.Properties.Template = helpers.ExpandContainerAppTemplate(state.Template, metadata) + + // Zero R/O - API rejects the request if eny of these are set + model.SystemData = nil + model.Properties.OutboundIPAddresses = nil + if err := client.CreateOrUpdateThenPoll(ctx, *id, *model); err != nil { return fmt.Errorf("updating %s: %+v", *id, err) } @@ -381,32 +386,34 @@ func (r ContainerAppResource) Update() sdk.ResourceFunc { } } -//func (r ContainerAppResource) CustomizeDiff() sdk.ResourceFunc { -// return sdk.ResourceFunc{ -// Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { -// if metadata.ResourceData.HasChange("secret") { -// stateSecretsRaw, configSecretsRaw := metadata.ResourceData.GetChange("secret") -// stateSecrets := stateSecretsRaw.([]helpers.Secret) -// configSecret := configSecretsRaw.([]helpers.Secret) -// // Check there's not less -// if len(configSecret) < len(stateSecrets) { -// return fmt.Errorf("cannot remove secrets from Container Apps at this time") -// } -// // Check secrets names in state are all present in config, the values don't matter -// for _, s := range stateSecrets { -// found := false -// for _, c := range configSecret { -// if s.Name == c.Name { -// found = true -// break -// } -// if !found { -// return fmt.Errorf("previously configured secret %q was removed. Removing secrets is not supported at this time", s.Name) -// } -// } -// } -// } -// return nil -// }, -// } -//} +func (r ContainerAppResource) CustomizeDiff() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + if metadata.ResourceDiff != nil && metadata.ResourceDiff.HasChange("secret") { + stateSecretsRaw, configSecretsRaw := metadata.ResourceDiff.GetChange("secret") + stateSecrets := stateSecretsRaw.([]interface{}) + configSecrets := configSecretsRaw.([]interface{}) + // Check there's not less + if len(configSecrets) < len(stateSecrets) { + return fmt.Errorf("cannot remove secrets from Container Apps at this time. Please see `https://github.com/microsoft/azure-container-apps/issues/395` for more details") + } + // Check secrets names in state are all present in config, the values don't matter + if len(stateSecrets) > 0 { + for _, s := range stateSecrets { + found := false + for _, c := range configSecrets { + if s.(map[string]interface{})["name"] == c.(map[string]interface{})["name"] { + found = true + break + } + if !found { + return fmt.Errorf("previously configured secret %q was removed. Removing secrets is not supported at this time, see `https://github.com/microsoft/azure-container-apps/issues/395` for more details", s.(map[string]interface{})["name"]) + } + } + } + } + } + return nil + }, + } +} diff --git a/internal/services/containerapps/container_app_resource_test.go b/internal/services/containerapps/container_app_resource_test.go index 8c21a5f97ccd..2f69e6e43db2 100644 --- a/internal/services/containerapps/container_app_resource_test.go +++ b/internal/services/containerapps/container_app_resource_test.go @@ -3,6 +3,7 @@ package containerapps_test import ( "context" "fmt" + "regexp" "testing" "github.com/hashicorp/go-azure-helpers/lang/response" @@ -102,21 +103,59 @@ func TestAccContainerAppResource_completeUpdate(t *testing.T) { ), }, data.ImportStep(), - // TODO - Uncomment the following stages when https://github.com/Azure/azure-rest-api-specs/issues/19285 is resolved + // TODO - Uncomment the following stages when https://github.com/Azure/azure-rest-api-specs/issues/19285 / https://github.com/microsoft/azure-container-apps/issues/395 are resolved and secrets can be managed? + // { + // Config: r.complete(data, "rev3"), + // Check: acceptance.ComposeTestCheckFunc( + // check.That(data.ResourceName).ExistsInAzure(r), + // ), + // }, + // data.ImportStep(), + // { + // Config: r.completeUpdate2(data, "rev4"), + // Check: acceptance.ComposeTestCheckFunc( + // check.That(data.ResourceName).ExistsInAzure(r), + // ), + // }, + // data.ImportStep(), + }) +} + +func TestAccContainerAppResource_secretRemoveShouldFail(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_container_app", "test") + r := ContainerAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.complete(data, "rev3"), + Config: r.completeUpdate(data, "rev1"), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep(), { - Config: r.completeUpdate2(data, "rev4"), + Config: r.complete(data, "rev2"), + ExpectError: regexp.MustCompile("cannot remove secrets from Container Apps at this time"), + }, + }) +} + +func TestAccContainerAppResource_secretRemoveWithAddShouldFail(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_container_app", "test") + r := ContainerAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.completeUpdate(data, "rev1"), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep(), + { + Config: r.completeChangedSecret(data, "rev2"), + ExpectError: regexp.MustCompile("previously configured secret \"rick\" was removed. Removing secrets is not supported at this time"), + }, }) } @@ -304,6 +343,126 @@ resource "azurerm_container_app" "test" { `, r.templatePlusExtras(data), data.RandomInteger, revisionSuffix) } +func (r ContainerAppResource) completeChangedSecret(data acceptance.TestData, revisionSuffix string) string { + return fmt.Sprintf(` +%s + +resource "azurerm_container_app" "test" { + name = "acctest-capp-%[2]d" + resource_group_name = azurerm_resource_group.test.name + container_app_environment_id = azurerm_container_app_environment.test.id + revision_mode = "Multiple" + + template { + container { + name = "acctest-cont-%[2]d" + image = "jackofallops/azure-containerapps-python-acctest:v0.0.1" + cpu = 0.5 + memory = "1Gi" + //args = ["HOSTNAME"] // TODO - Add a container Image where args and command can be used + //command = ["node"] + + readiness_probe { + transport = "http" + port = 5000 + path = "/uptime" + timeout = 2 + failure_threshold = 1 + success_threshold = 1 + + header { + name = "Cache-Control" + value = "no-cache" + } + } + + liveness_probe { + transport = "http" + port = 5000 + path = "/health" + + header { + name = "Cache-Control" + value = "no-cache" + } + + initial_delay = 5 + interval = 20 + timeout = 2 + failure_threshold = 3 + } + + startup_probe { + transport = "tcp" + port = 5000 + timeout = 5 + failure_threshold = 1 + } + + //volume_mounts { + // name = "testVol" + // path = "/tmp/testdata" + //} + } + + //volume { + // name = "testVol" + // storage_type = "EmptyDir" + //} + + min_replicas = 1 + max_replicas = 4 + + revision_suffix = "%[3]s" + } + + ingress { + allow_insecure_connections = true + is_external = true + target_port = 5000 + transport = "auto" + + traffic_weight { + latest_revision = true + weight = 20 + } + + traffic_weight { + revision_suffix = "rev1" + weight = 80 + } + } + + registry { + server = azurerm_container_registry.test.login_server + username = azurerm_container_registry.test.admin_username + password_secret_reference = "registry-password" + } + + secret { + name = "registry-password" + value = azurerm_container_registry.test.admin_password + } + + secret { + name = "pickle" + value = "morty" + } + + dapr { + app_id = "acctest-cont-%[2]d" + app_port = 5000 + app_protocol = "http" + } + + tags = { + foo = "Bar" + accTest = "1" + } +} +`, r.templatePlusExtras(data), data.RandomInteger, revisionSuffix) +} + func (r ContainerAppResource) completeUpdate(data acceptance.TestData, revisionSuffix string) string { return fmt.Sprintf(` %s @@ -441,14 +600,13 @@ resource "azurerm_container_app" "test" { cpu = 1.0 memory = "2Gi" } + revision_suffix = "%[3]s" + } secret { - name = "doesntMatter" + name = "doesnt-matter" value = "anything" } - - revision_suffix = "%[3]s" - } } `, r.templatePlusExtras(data), data.RandomInteger, revisionSuffix) } diff --git a/internal/services/containerapps/helpers/validate.go b/internal/services/containerapps/helpers/validate.go index 6b8957c20dde..4bb21cbb1179 100644 --- a/internal/services/containerapps/helpers/validate.go +++ b/internal/services/containerapps/helpers/validate.go @@ -42,7 +42,7 @@ func ValidateSecretName(i interface{}, k string) (warnings []string, errors []er return } - if matched := regexp.MustCompile(`^[a-z0-9][a-z0-9-.]*[a-z0-9]?$`).Match([]byte(v)); !matched || strings.HasSuffix(v, "-") || strings.HasSuffix(v, ".") { + if matched := regexp.MustCompile(`^[a-z0-9][a-z0-9-]*[a-z0-9]?$`).Match([]byte(v)); !matched || strings.HasSuffix(v, "-") || strings.HasSuffix(v, ".") { errors = append(errors, fmt.Errorf("%q must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", k)) } return