Skip to content
Merged
Show file tree
Hide file tree
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
88 changes: 66 additions & 22 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,54 +64,98 @@ func (c *EonClient) handleAPIError(err error, httpResp *http.Response, baseError
return nil
}

// ListSourceAccounts retrieves all source accounts for the project
// ListSourceAccounts retrieves all source accounts for the project.
// It paginates through all pages to return the complete list.
func (c *EonClient) ListSourceAccounts(ctx context.Context) ([]externalEonSdkAPI.SourceAccount, error) {
if err := c.tokenRefresher.EnsureValidToken(); err != nil {
return nil, fmt.Errorf("failed to ensure valid token: %w", err)
}

resp, httpResp, err := c.client.AccountsAPI.ListSourceAccounts(ctx, c.projectID).ListSourceAccountsRequest(externalEonSdkAPI.ListSourceAccountsRequest{}).Execute()
var allAccounts []externalEonSdkAPI.SourceAccount
var pageToken string

if apiErr := c.handleAPIError(err, httpResp, "failed to list source accounts"); apiErr != nil {
return nil, apiErr
}
defer httpResp.Body.Close()
for {
req := c.client.AccountsAPI.ListSourceAccounts(ctx, c.projectID).
ListSourceAccountsRequest(externalEonSdkAPI.ListSourceAccountsRequest{})
if pageToken != "" {
req = req.PageToken(pageToken)
}

if httpResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(httpResp.Body)
return nil, fmt.Errorf("API error %d: %s", httpResp.StatusCode, string(body))
resp, httpResp, err := req.Execute()

if apiErr := c.handleAPIError(err, httpResp, "failed to list source accounts"); apiErr != nil {
return nil, apiErr
}
defer httpResp.Body.Close()

if httpResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(httpResp.Body)
return nil, fmt.Errorf("API error %d: %s", httpResp.StatusCode, string(body))
}

accounts := resp.GetAccounts()
if accounts != nil {
allAccounts = append(allAccounts, accounts...)
}

if !resp.HasNextToken() {
break
}
pageToken = resp.GetNextToken()
}

if resp.GetAccounts() == nil {
if allAccounts == nil {
return []externalEonSdkAPI.SourceAccount{}, nil
}

return resp.GetAccounts(), nil
return allAccounts, nil
}

// ListRestoreAccounts retrieves all restore accounts for the project
// ListRestoreAccounts retrieves all restore accounts for the project.
// It paginates through all pages to return the complete list.
func (c *EonClient) ListRestoreAccounts(ctx context.Context) ([]externalEonSdkAPI.RestoreAccount, error) {
if err := c.tokenRefresher.EnsureValidToken(); err != nil {
return nil, fmt.Errorf("failed to ensure valid token: %w", err)
}

resp, httpResp, err := c.client.AccountsAPI.ListRestoreAccounts(ctx, c.projectID).ListRestoreAccountsRequest(externalEonSdkAPI.ListRestoreAccountsRequest{}).Execute()
var allAccounts []externalEonSdkAPI.RestoreAccount
var pageToken string

if apiErr := c.handleAPIError(err, httpResp, "failed to list restore accounts"); apiErr != nil {
return nil, apiErr
}
defer httpResp.Body.Close()
for {
req := c.client.AccountsAPI.ListRestoreAccounts(ctx, c.projectID).
ListRestoreAccountsRequest(externalEonSdkAPI.ListRestoreAccountsRequest{})
if pageToken != "" {
req = req.PageToken(pageToken)
}

if httpResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(httpResp.Body)
return nil, fmt.Errorf("API error %d: %s", httpResp.StatusCode, string(body))
resp, httpResp, err := req.Execute()

if apiErr := c.handleAPIError(err, httpResp, "failed to list restore accounts"); apiErr != nil {
return nil, apiErr
}
defer httpResp.Body.Close()

if httpResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(httpResp.Body)
return nil, fmt.Errorf("API error %d: %s", httpResp.StatusCode, string(body))
}

accounts := resp.GetAccounts()
if accounts != nil {
allAccounts = append(allAccounts, accounts...)
}

if !resp.HasNextToken() {
break
}
pageToken = resp.GetNextToken()
}

if resp.GetAccounts() == nil {
if allAccounts == nil {
return []externalEonSdkAPI.RestoreAccount{}, nil
}

return resp.GetAccounts(), nil
return allAccounts, nil
}

// ConnectSourceAccount connects a new source account
Expand Down
39 changes: 23 additions & 16 deletions internal/provider/resource_restore_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,13 +221,22 @@ func (r *RestoreAccountResource) Create(ctx context.Context, req resource.Create
// Check if this is a 409 Conflict (account already exists)
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 409 {
existingID := r.findExistingAccountID(ctx, cloudProvider, data)
title, detail := conflictErrorMessage("Restore Account", existingID)
resp.Diagnostics.AddError(title, fmt.Sprintf("%s\n\nOriginal error: %s", detail, err.Error()))
// Treat 409 as success — adopt the existing account into state
existing := r.findExistingAccount(ctx, cloudProvider, data)
if existing == nil {
resp.Diagnostics.AddError("Restore Account Already Exists",
fmt.Sprintf("A restore account with this configuration already exists but could not be found via the API.\n\nOriginal error: %s", err.Error()))
return
}
tflog.Info(ctx, "Restore account already exists (409 Conflict), adopting into state", map[string]interface{}{
"id": existing.Id,
"name": existing.GetName(),
})
account = existing
} else {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to connect restore account: %s", err))
return
}
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to connect restore account: %s", err))
return
}

// Update state from response
Expand Down Expand Up @@ -467,15 +476,15 @@ func (r *RestoreAccountResource) ImportState(ctx context.Context, req resource.I
})
}

// findExistingAccountID attempts to find the ID of an existing restore account
// that matches the given configuration. Returns empty string if not found.
func (r *RestoreAccountResource) findExistingAccountID(ctx context.Context, cloudProvider CloudProvider, data RestoreAccountResourceModel) string {
// findExistingAccount attempts to find an existing restore account
// that matches the given configuration. Returns nil if not found.
func (r *RestoreAccountResource) findExistingAccount(ctx context.Context, cloudProvider CloudProvider, data RestoreAccountResourceModel) *externalEonSdkAPI.RestoreAccount {
accounts, err := r.client.ListRestoreAccounts(ctx)
if err != nil {
tflog.Debug(ctx, "Failed to list restore accounts to find existing ID", map[string]any{
tflog.Debug(ctx, "Failed to list restore accounts to find existing account", map[string]any{
"error": err.Error(),
})
return ""
return nil
}

for _, account := range accounts {
Expand All @@ -488,25 +497,23 @@ func (r *RestoreAccountResource) findExistingAccountID(ctx context.Context, clou
if data.Aws != nil && account.RestoreAccountAttributes.HasAws() {
awsAttrs := account.RestoreAccountAttributes.GetAws()
if awsAttrs.GetRoleArn() == data.Aws.RoleArn.ValueString() {
return account.Id
return &account
}
}
case CloudProviderAzure:
if data.Azure != nil && account.RestoreAccountAttributes.HasAzure() {
// Match by subscription_id (the unique identifier for Azure accounts)
if account.GetProviderAccountId() == data.Azure.SubscriptionId.ValueString() {
return account.Id
return &account
}
}
case CloudProviderGCP:
if data.Gcp != nil && account.RestoreAccountAttributes.HasGcp() {
// Match by project_id (the unique identifier for GCP accounts)
if account.GetProviderAccountId() == data.Gcp.ProjectId.ValueString() {
return account.Id
return &account
}
}
}
}

return ""
return nil
}
39 changes: 23 additions & 16 deletions internal/provider/resource_source_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,13 +221,22 @@ func (r *SourceAccountResource) Create(ctx context.Context, req resource.CreateR
// Check if this is a 409 Conflict (account already exists)
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 409 {
existingID := r.findExistingAccountID(ctx, cloudProvider, data)
title, detail := conflictErrorMessage("Source Account", existingID)
resp.Diagnostics.AddError(title, fmt.Sprintf("%s\n\nOriginal error: %s", detail, err.Error()))
// Treat 409 as success — adopt the existing account into state
existing := r.findExistingAccount(ctx, cloudProvider, data)
if existing == nil {
resp.Diagnostics.AddError("Source Account Already Exists",
fmt.Sprintf("A source account with this configuration already exists but could not be found via the API.\n\nOriginal error: %s", err.Error()))
return
}
tflog.Info(ctx, "Source account already exists (409 Conflict), adopting into state", map[string]interface{}{
"id": existing.Id,
"name": existing.GetName(),
})
account = existing
} else {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to connect source account: %s", err))
return
}
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to connect source account: %s", err))
return
}

// Update state from response
Expand Down Expand Up @@ -449,15 +458,15 @@ func (r *SourceAccountResource) ImportState(ctx context.Context, req resource.Im
})
}

// findExistingAccountID attempts to find the ID of an existing source account
// that matches the given configuration. Returns empty string if not found.
func (r *SourceAccountResource) findExistingAccountID(ctx context.Context, cloudProvider CloudProvider, data SourceAccountResourceModel) string {
// findExistingAccount attempts to find an existing source account
// that matches the given configuration. Returns nil if not found.
func (r *SourceAccountResource) findExistingAccount(ctx context.Context, cloudProvider CloudProvider, data SourceAccountResourceModel) *externalEonSdkAPI.SourceAccount {
accounts, err := r.client.ListSourceAccounts(ctx)
if err != nil {
tflog.Debug(ctx, "Failed to list source accounts to find existing ID", map[string]interface{}{
tflog.Debug(ctx, "Failed to list source accounts to find existing account", map[string]interface{}{
"error": err.Error(),
})
return ""
return nil
}

for _, account := range accounts {
Expand All @@ -470,25 +479,23 @@ func (r *SourceAccountResource) findExistingAccountID(ctx context.Context, cloud
if data.Aws != nil && account.SourceAccountAttributes.HasAws() {
awsAttrs := account.SourceAccountAttributes.GetAws()
if awsAttrs.GetRoleArn() == data.Aws.RoleArn.ValueString() {
return account.Id
return &account
}
}
case CloudProviderAzure:
if data.Azure != nil && account.SourceAccountAttributes.HasAzure() {
// Match by subscription_id (the unique identifier for Azure accounts)
if account.GetProviderAccountId() == data.Azure.SubscriptionId.ValueString() {
return account.Id
return &account
}
}
case CloudProviderGCP:
if data.Gcp != nil && account.SourceAccountAttributes.HasGcp() {
// Match by project_id (the unique identifier for GCP accounts)
if account.GetProviderAccountId() == data.Gcp.ProjectId.ValueString() {
return account.Id
return &account
}
}
}
}

return ""
return nil
}
Loading