diff --git a/cmd/ctrlc/root/apply/cmd.go b/cmd/ctrlc/root/apply/cmd.go index 1fe9d73..687ee25 100644 --- a/cmd/ctrlc/root/apply/cmd.go +++ b/cmd/ctrlc/root/apply/cmd.go @@ -103,9 +103,32 @@ func runApply(ctx context.Context, filePatterns []string, selectorRaw string) er log.Info("Applying resources", "count", len(specs), "files", len(files)) sortedSpecs := sortSpecsByOrder(specs) - results := providers. - DefaultProviderEngine. - BatchApply(applyCtx, sortedSpecs, providers.BatchApplyOptions{}) + + var resourceSpecs []*providers.ResourceItemSpec + var otherSpecs []providers.TypedSpec + for _, ts := range sortedSpecs { + if ts.Type == "Resource" { + if spec, ok := ts.Spec.(*providers.ResourceItemSpec); ok { + resourceSpecs = append(resourceSpecs, spec) + continue + } + } + otherSpecs = append(otherSpecs, ts) + } + + var results []providers.Result + + if len(resourceSpecs) > 0 { + resourceResults := providers.BatchUpsertResources(applyCtx, resourceSpecs) + results = append(results, resourceResults...) + } + + if len(otherSpecs) > 0 { + otherResults := providers. + DefaultProviderEngine. + BatchApply(applyCtx, otherSpecs, providers.BatchApplyOptions{}) + results = append(results, otherResults...) + } printResults(results) diff --git a/internal/api/providers/resource.go b/internal/api/providers/resource.go index 4359ef9..16f1cb1 100644 --- a/internal/api/providers/resource.go +++ b/internal/api/providers/resource.go @@ -147,12 +147,107 @@ func (r *ResourceItemSpec) getProviderID(ctx Context) (string, error) { if err != nil { return "", fmt.Errorf("failed to create resource provider: %w", err) } - if createResp.StatusCode() != http.StatusOK { + if createResp.StatusCode() != http.StatusAccepted { return "", fmt.Errorf("failed to create resource provider: %s", createResp.Status()) } return createResp.JSON202.Id, nil } +// BatchUpsertResources groups resources by provider and makes one +// SetResourceProviderResources call per provider with all resources in that +// group. This avoids the overwrite problem where sequential single-resource +// calls replace the entire provider's resource set. +func BatchUpsertResources(ctx Context, specs []*ResourceItemSpec) []Result { + // Group by provider name + byProvider := make(map[string][]*ResourceItemSpec) + for _, spec := range specs { + providerName := spec.Provider + if providerName == "" { + providerName = "ctrlc-apply" + } + byProvider[providerName] = append(byProvider[providerName], spec) + } + + var results []Result + for providerName, group := range byProvider { + // Resolve provider ID (create if needed) using the first spec + providerID, err := group[0].getProviderID(ctx) + if err != nil { + for _, spec := range group { + results = append(results, Result{ + Type: resourceTypeName, + Name: spec.DisplayName, + Error: fmt.Errorf("failed to get provider %q: %w", providerName, err), + }) + } + continue + } + + // Build the batch resource list + apiResources := make([]api.ResourceProviderResource, 0, len(group)) + for _, spec := range group { + metadata := spec.Metadata + if metadata == nil { + metadata = make(map[string]string) + } + config := spec.Config + if config == nil { + config = make(map[string]any) + } + apiResources = append(apiResources, api.ResourceProviderResource{ + Identifier: spec.Identifier, + Name: spec.DisplayName, + Kind: spec.Kind, + Version: spec.Version, + Config: config, + Metadata: metadata, + }) + } + + // Single API call for all resources under this provider + resp, err := ctx.APIClient().SetResourceProviderResourcesWithResponse( + ctx.Ctx(), ctx.WorkspaceIDValue(), providerID, + api.SetResourceProviderResourcesJSONRequestBody{Resources: apiResources}, + ) + if err != nil { + for _, spec := range group { + results = append(results, Result{ + Type: resourceTypeName, + Name: spec.DisplayName, + Error: fmt.Errorf("failed to upsert resources: %w", err), + }) + } + continue + } + if resp.StatusCode() != http.StatusAccepted { + for _, spec := range group { + results = append(results, Result{ + Type: resourceTypeName, + Name: spec.DisplayName, + Error: fmt.Errorf("failed to upsert resources: %s", resp.Status()), + }) + } + continue + } + + // Sync variables individually (each resource may have different vars) + for _, spec := range group { + result := Result{ + Type: resourceTypeName, + Name: spec.DisplayName, + ID: spec.Identifier, + Action: "upserted", + } + if err := spec.syncVariables(ctx); err != nil { + result.Error = err + } + results = append(results, result) + } + } + + return results +} + func (r *ResourceItemSpec) syncVariables(ctx Context) error { vars := r.Variables if vars == nil {