Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 186 additions & 47 deletions spectrocloud/resource_registry_oci_ecr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -311,17 +299,29 @@ 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)
return diag.FromErr(err)
}
// 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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}
}
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
}
}