diff --git a/spectrocloud/resource_registry_oci_ecr.go b/spectrocloud/resource_registry_oci_ecr.go index c17b7360..5a76ad2b 100644 --- a/spectrocloud/resource_registry_oci_ecr.go +++ b/spectrocloud/resource_registry_oci_ecr.go @@ -221,6 +221,14 @@ func resourceRegistryEcrCreate(ctx context.Context, d *schema.ResourceData, m in return diag.FromErr(err) } d.SetId(uid) + + // Wait for sync if requested and provider_type is helm (ECR supports helm only for wait_for_sync) + if providerType == "helm" && d.Get("wait_for_sync") != nil && d.Get("wait_for_sync").(bool) { + diags, isError := waitForOCIRegistrySyncAndSetStatus(ctx, d, uid, diags, c, schema.TimeoutCreate, "ecr") + if isError { + return diags + } + } case "basic": registry := toRegistryBasic(d) if err := validateRegistryCred(c, registryType, providerType, isSync, registry.Spec, nil); err != nil { @@ -235,29 +243,12 @@ func resourceRegistryEcrCreate(ctx context.Context, d *schema.ResourceData, m in // Wait for sync if requested and provider_type is zarf or helm if (providerType == "zarf" || providerType == "helm") && d.Get("wait_for_sync") != nil && d.Get("wait_for_sync").(bool) { - diagnostics, isError := waitForOciRegistrySync(ctx, d, uid, diags, c, schema.TimeoutCreate) - if len(diagnostics) > 0 { - diags = append(diags, diagnostics...) - } - // Fetch final sync status and set wait_for_status_message - syncStatus, statusErr := c.GetOciBasicRegistrySyncStatus(uid) - if statusErr == nil && syncStatus != nil { - statusMessage := "" - if syncStatus.Message != "" { - statusMessage = syncStatus.Message - } else if syncStatus.Status != "" { - statusMessage = fmt.Sprintf("Status: %s", syncStatus.Status) - } - if err := d.Set("wait_for_status_message", statusMessage); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - } + diags, isError := waitForOCIRegistrySyncAndSetStatus(ctx, d, uid, diags, c, schema.TimeoutCreate, "basic") if isError { - return diagnostics + return diags } } } - return diags } @@ -298,9 +289,6 @@ func resourceRegistryEcrRead(ctx context.Context, d *schema.ResourceData, m inte return diag.FromErr(err) } - if err := d.Set("wait_for_sync", false); err != nil { - return diag.FromErr(err) - } credentials := make([]interface{}, 0, 1) acc := make(map[string]interface{}) switch *registry.Spec.Credentials.CredentialType { @@ -311,6 +299,16 @@ func resourceRegistryEcrRead(ctx context.Context, d *schema.ResourceData, m inte case models.V1AwsCloudAccountCredentialTypeSecret: acc["access_key"] = registry.Spec.Credentials.AccessKey acc["credential_type"] = models.V1AwsCloudAccountCredentialTypeSecret + // Preserve secret_key from state to avoid drift when API does not return it + if currentCredsRaw := d.Get("credentials"); currentCredsRaw != nil { + if currentCredsList, ok := currentCredsRaw.([]interface{}); ok && len(currentCredsList) > 0 { + if currentCredMap, ok := currentCredsList[0].(map[string]interface{}); ok { + if secretKey, exists := currentCredMap["secret_key"]; exists && secretKey != nil { + acc["secret_key"] = secretKey + } + } + } + } default: errMsg := fmt.Sprintf("Registry type %s not implemented.", *registry.Spec.Credentials.CredentialType) err = errors.New(errMsg) @@ -318,10 +316,12 @@ func resourceRegistryEcrRead(ctx context.Context, d *schema.ResourceData, m inte } // tls configuration handling tlsConfig := make([]interface{}, 0, 1) - tls := make(map[string]interface{}) - tls["certificate"] = registry.Spec.TLS.Certificate - tls["insecure_skip_verify"] = registry.Spec.TLS.InsecureSkipVerify - tlsConfig = append(tlsConfig, tls) + if registry.Spec.TLS != nil { + tls := make(map[string]interface{}) + tls["certificate"] = registry.Spec.TLS.Certificate + tls["insecure_skip_verify"] = registry.Spec.TLS.InsecureSkipVerify + tlsConfig = append(tlsConfig, tls) + } acc["tls_config"] = tlsConfig credentials = append(credentials, acc) @@ -367,9 +367,6 @@ func resourceRegistryEcrRead(ctx context.Context, d *schema.ResourceData, m inte return diag.FromErr(err) } - if err := d.Set("wait_for_sync", false); err != nil { - return diag.FromErr(err) - } credentials := make([]interface{}, 0, 1) acc := make(map[string]interface{}) // Read the actual auth type from the API response @@ -445,6 +442,14 @@ func resourceRegistryEcrUpdate(ctx context.Context, d *schema.ResourceData, m in if err != nil { return diag.FromErr(err) } + + // Wait for sync if requested and provider_type is helm + if providerType == "helm" && d.Get("wait_for_sync") != nil && d.Get("wait_for_sync").(bool) { + diags, isError := waitForOCIRegistrySyncAndSetStatus(ctx, d, d.Id(), diags, c, schema.TimeoutUpdate, "ecr") + if isError { + return diags + } + } case "basic": registry := toRegistryBasic(d) if err := validateRegistryCred(c, registryType, providerType, isSync, registry.Spec, nil); err != nil { @@ -457,25 +462,9 @@ func resourceRegistryEcrUpdate(ctx context.Context, d *schema.ResourceData, m in // Wait for sync if requested and provider_type is zarf or helm if (providerType == "zarf" || providerType == "helm") && d.Get("wait_for_sync") != nil && d.Get("wait_for_sync").(bool) { - diagnostics, isError := waitForOciRegistrySync(ctx, d, d.Id(), diags, c, schema.TimeoutUpdate) - if len(diagnostics) > 0 { - diags = append(diags, diagnostics...) - } - // Fetch final sync status and set wait_for_status_message - syncStatus, statusErr := c.GetOciBasicRegistrySyncStatus(d.Id()) - if statusErr == nil && syncStatus != nil { - statusMessage := "" - if syncStatus.Message != "" { - statusMessage = syncStatus.Message - } else if syncStatus.Status != "" { - statusMessage = fmt.Sprintf("Status: %s", syncStatus.Status) - } - if err := d.Set("wait_for_status_message", statusMessage); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - } + diags, isError := waitForOCIRegistrySyncAndSetStatus(ctx, d, d.Id(), diags, c, schema.TimeoutUpdate, "basic") if isError { - return diagnostics + return diags } } } @@ -607,6 +596,51 @@ func toRegistryAwsAccountCredential(regCred map[string]interface{}) *models.V1Aw return account } +// waitForOCIRegistrySyncAndSetStatus runs the appropriate wait-for-sync for the given registry type, +// then sets wait_for_status_message from the API. Returns (combined diagnostics, true if caller should return). +func waitForOCIRegistrySyncAndSetStatus(ctx context.Context, d *schema.ResourceData, uid string, diags diag.Diagnostics, c *client.V1Client, timeoutType string, registryType string) (diag.Diagnostics, bool) { + switch registryType { + case "ecr": + diagnostics, isError := waitForOciEcrRegistrySync(ctx, d, uid, diags, c, timeoutType) + if len(diagnostics) > 0 { + diags = append(diags, diagnostics...) + } + registry, statusErr := c.GetOciEcrRegistry(uid) + if statusErr == nil && registry != nil && registry.Status != nil && registry.Status.SyncStatus != nil { + statusMessage := "" + if registry.Status.SyncStatus.Message != "" { + statusMessage = registry.Status.SyncStatus.Message + } else if registry.Status.SyncStatus.Status != "" { + statusMessage = fmt.Sprintf("Status: %s", registry.Status.SyncStatus.Status) + } + if err := d.Set("wait_for_status_message", statusMessage); err != nil { + diags = append(diags, diag.FromErr(err)...) + } + } + return diags, isError + case "basic": + diagnostics, isError := waitForOciRegistrySync(ctx, d, uid, diags, c, timeoutType) + if len(diagnostics) > 0 { + diags = append(diags, diagnostics...) + } + syncStatus, statusErr := c.GetOciBasicRegistrySyncStatus(uid) + if statusErr == nil && syncStatus != nil { + statusMessage := "" + if syncStatus.Message != "" { + statusMessage = syncStatus.Message + } else if syncStatus.Status != "" { + statusMessage = fmt.Sprintf("Status: %s", syncStatus.Status) + } + if err := d.Set("wait_for_status_message", statusMessage); err != nil { + diags = append(diags, diag.FromErr(err)...) + } + } + return diags, isError + default: + return diags, false + } +} + // waitForOciRegistrySync waits for an OCI registry to complete its synchronization func waitForOciRegistrySync(ctx context.Context, d *schema.ResourceData, uid string, diags diag.Diagnostics, c *client.V1Client, timeoutType string) (diag.Diagnostics, bool) { stateConf := &retry.StateChangeConf{ @@ -727,3 +761,108 @@ func resourceOciRegistrySyncRefreshFunc(c *client.V1Client, uid string) retry.St } } } + +// waitForOciEcrRegistrySync waits for an OCI ECR registry to complete its synchronization by polling GetOciEcrRegistry. +func waitForOciEcrRegistrySync(ctx context.Context, d *schema.ResourceData, uid string, diags diag.Diagnostics, c *client.V1Client, timeoutType string) (diag.Diagnostics, bool) { + stateConf := &retry.StateChangeConf{ + Pending: []string{ + "InProgress", + "Pending", + "Unknown", + "", + }, + Target: []string{ + "Success", + "Completed", + }, + Refresh: resourceOciEcrRegistrySyncRefreshFunc(c, uid), + Timeout: d.Timeout(timeoutType) - 1*time.Minute, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + _, err := stateConf.WaitForStateContext(ctx) + if err != nil { + var timeoutErr *retry.TimeoutError + if errors.As(err, &timeoutErr) { + currentStatus := timeoutErr.LastState + statusMessage := "" + registry, statusErr := c.GetOciEcrRegistry(uid) + if statusErr == nil && registry != nil && registry.Status != nil && registry.Status.SyncStatus != nil { + if registry.Status.SyncStatus.Status != "" { + currentStatus = registry.Status.SyncStatus.Status + } + if registry.Status.SyncStatus.Message != "" { + statusMessage = fmt.Sprintf(" Message: %s", registry.Status.SyncStatus.Message) + } + } + if currentStatus == "" { + currentStatus = "Unknown" + } + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "OCI ECR registry sync timeout", + Detail: fmt.Sprintf( + "OCI ECR registry synchronization timed out after waiting for %v. Current sync status is '%s'.%s "+ + "The registry sync may still be in progress. You may need to increase the timeout or wait for the sync to complete manually.", + d.Timeout(timeoutType)-1*time.Minute, currentStatus, statusMessage), + }) + return diags, false + } + + registry, statusErr := c.GetOciEcrRegistry(uid) + if statusErr == nil && registry != nil && registry.Status != nil && registry.Status.SyncStatus != nil { + status := registry.Status.SyncStatus.Status + if status == "Failed" || status == "Error" || status == "failed" || status == "error" { + errorDetail := fmt.Sprintf("OCI ECR registry synchronization failed with status '%s'.", status) + if registry.Status.SyncStatus.Message != "" { + errorDetail += fmt.Sprintf("\n\nError details: %s", registry.Status.SyncStatus.Message) + } + errorDetail += "\n\nPlease check the registry configuration (endpoint, credentials) and try again." + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "OCI ECR registry sync failed", + Detail: errorDetail, + }) + return diags, false + } + } + + return diag.FromErr(err), true + } + return nil, false +} + +// resourceOciEcrRegistrySyncRefreshFunc returns a retry.StateRefreshFunc that checks the sync status of an OCI ECR registry via GetOciEcrRegistry. +func resourceOciEcrRegistrySyncRefreshFunc(c *client.V1Client, uid string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + registry, err := c.GetOciEcrRegistry(uid) + if err != nil { + return nil, "", err + } + if registry == nil || registry.Status == nil || registry.Status.SyncStatus == nil { + return nil, "", nil + } + syncStatus := registry.Status.SyncStatus + if !syncStatus.IsSyncSupported { + return syncStatus, "Success", nil + } + status := syncStatus.Status + if status == "" { + return syncStatus, "", nil + } + switch status { + case "Success", "Completed", "success", "completed": + return syncStatus, "Success", nil + case "Failed", "Error", "failed", "error": + if syncStatus.Message != "" { + return syncStatus, status, fmt.Errorf("registry sync failed: %s", syncStatus.Message) + } + return syncStatus, status, fmt.Errorf("registry sync failed") + case "InProgress", "Running", "Syncing", "inprogress", "running", "syncing": + return syncStatus, "InProgress", nil + default: + return syncStatus, status, nil + } + } +}