diff --git a/cmd/ctrlc/root/api/get/deployment/deployment.go b/cmd/ctrlc/root/api/get/deployment/deployment.go index 4ad4cb2..23e083c 100644 --- a/cmd/ctrlc/root/api/get/deployment/deployment.go +++ b/cmd/ctrlc/root/api/get/deployment/deployment.go @@ -25,7 +25,10 @@ func NewDeploymentCmd() *cobra.Command { return fmt.Errorf("failed to create API client: %w", err) } - workspaceID := client.GetWorkspaceID(cmd.Context(), workspace) + workspaceID, err := client.GetWorkspaceID(cmd.Context(), workspace) + if err != nil { + return err + } resp, err := client.GetDeploymentByName(cmd.Context(), workspaceID.String(), name) if err != nil { return fmt.Errorf("failed to get deployment: %w", err) diff --git a/cmd/ctrlc/root/api/get/release/release.go b/cmd/ctrlc/root/api/get/release/release.go index 2c954fb..18d42b1 100644 --- a/cmd/ctrlc/root/api/get/release/release.go +++ b/cmd/ctrlc/root/api/get/release/release.go @@ -25,7 +25,10 @@ func NewReleaseCmd() *cobra.Command { return fmt.Errorf("failed to create API client: %w", err) } - workspaceID := client.GetWorkspaceID(cmd.Context(), workspace) + workspaceID, err := client.GetWorkspaceID(cmd.Context(), workspace) + if err != nil { + return err + } resp, err := client.GetRelease(cmd.Context(), workspaceID.String(), releaseID) if err != nil { return fmt.Errorf("failed to get release: %w", err) diff --git a/cmd/ctrlc/root/api/get/releasetargets/releasetargets.go b/cmd/ctrlc/root/api/get/releasetargets/releasetargets.go index fa83acc..b223518 100644 --- a/cmd/ctrlc/root/api/get/releasetargets/releasetargets.go +++ b/cmd/ctrlc/root/api/get/releasetargets/releasetargets.go @@ -61,7 +61,10 @@ func NewReleaseTargetsCmd() *cobra.Command { return fmt.Errorf("failed to create API client: %w", err) } - workspaceID := client.GetWorkspaceID(cmd.Context(), workspace) + workspaceID, err := client.GetWorkspaceID(cmd.Context(), workspace) + if err != nil { + return err + } params := &api.PreviewReleaseTargetsForResourceParams{} if limit > 0 { diff --git a/cmd/ctrlc/root/api/get/resources/resources.go b/cmd/ctrlc/root/api/get/resources/resources.go index 523a2be..ddd3abc 100644 --- a/cmd/ctrlc/root/api/get/resources/resources.go +++ b/cmd/ctrlc/root/api/get/resources/resources.go @@ -29,7 +29,10 @@ func NewResourcesCmd() *cobra.Command { return fmt.Errorf("failed to create API client: %w", err) } - workspaceID := client.GetWorkspaceID(cmd.Context(), workspace) + workspaceID, err := client.GetWorkspaceID(cmd.Context(), workspace) + if err != nil { + return err + } params := &api.GetAllResourcesParams{} if limit > 0 { diff --git a/cmd/ctrlc/root/api/plan/version/version.go b/cmd/ctrlc/root/api/plan/version/version.go index ef09d40..697b557 100644 --- a/cmd/ctrlc/root/api/plan/version/version.go +++ b/cmd/ctrlc/root/api/plan/version/version.go @@ -50,7 +50,10 @@ func NewPlanVersionCmd() *cobra.Command { metadata["ctrlplane/links"] = string(linksJSON) } - workspaceID := client.GetWorkspaceID(cmd.Context(), workspace) + workspaceID, err := client.GetWorkspaceID(cmd.Context(), workspace) + if err != nil { + return err + } config := cliutil.ConvertConfigArrayToNestedMap(configArray) diff --git a/cmd/ctrlc/root/api/upsert/deploymentversion/deployment-version.go b/cmd/ctrlc/root/api/upsert/deploymentversion/deployment-version.go index d6f6eab..5dd7064 100644 --- a/cmd/ctrlc/root/api/upsert/deploymentversion/deployment-version.go +++ b/cmd/ctrlc/root/api/upsert/deploymentversion/deployment-version.go @@ -93,7 +93,10 @@ func NewUpsertDeploymentVersionCmd() *cobra.Command { stat = &s } - workspaceID := client.GetWorkspaceID(cmd.Context(), workspace) + workspaceID, err := client.GetWorkspaceID(cmd.Context(), workspace) + if err != nil { + return err + } config := cliutil.ConvertConfigArrayToNestedMap(configArray) diff --git a/cmd/ctrlc/root/apply/cmd.go b/cmd/ctrlc/root/apply/cmd.go index 3b0ea14..c889d32 100644 --- a/cmd/ctrlc/root/apply/cmd.go +++ b/cmd/ctrlc/root/apply/cmd.go @@ -11,7 +11,6 @@ import ( "github.com/ctrlplanedev/cli/internal/api/providers" "github.com/ctrlplanedev/cli/internal/api/resolver" "github.com/fatih/color" - "github.com/google/uuid" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -76,9 +75,9 @@ func runApply(ctx context.Context, filePatterns []string, selectorRaw string) er return fmt.Errorf("failed to create API client: %w", err) } - workspaceID := client.GetWorkspaceID(ctx, workspace) - if workspaceID == uuid.Nil { - return fmt.Errorf("workspace not found: %s", workspace) + workspaceID, err := client.GetWorkspaceID(ctx, workspace) + if err != nil { + return err } resolver := resolver.NewAPIResolver(client, workspaceID) diff --git a/cmd/ctrlc/root/root.go b/cmd/ctrlc/root/root.go index 527809f..9845ac3 100644 --- a/cmd/ctrlc/root/root.go +++ b/cmd/ctrlc/root/root.go @@ -14,6 +14,7 @@ import ( "github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync" "github.com/ctrlplanedev/cli/cmd/ctrlc/root/ui" "github.com/ctrlplanedev/cli/cmd/ctrlc/root/version" + "github.com/ctrlplanedev/cli/cmd/ctrlc/root/workflow" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -63,6 +64,7 @@ func NewRootCmd() *cobra.Command { cmd.AddCommand(run.NewRunCmd()) cmd.AddCommand(ui.NewUICmd()) cmd.AddCommand(version.NewVersionCmd()) + cmd.AddCommand(workflow.NewWorkflowCmd()) return cmd } diff --git a/cmd/ctrlc/root/sync/pipe/pipe.go b/cmd/ctrlc/root/sync/pipe/pipe.go index f4272dd..bd22d2e 100644 --- a/cmd/ctrlc/root/sync/pipe/pipe.go +++ b/cmd/ctrlc/root/sync/pipe/pipe.go @@ -101,7 +101,11 @@ func NewSyncPipeCmd() *cobra.Command { return fmt.Errorf("failed to upsert resources: %w", err) } - workspaceID := ctrlplaneClient.GetWorkspaceID(ctx, workspace).String() + workspaceUUID, err := ctrlplaneClient.GetWorkspaceID(ctx, workspace) + if err != nil { + return err + } + workspaceID := workspaceUUID.String() if err := syncResourceVariables(ctx, ctrlplaneClient, workspaceID, resourceInputs); err != nil { return err } diff --git a/cmd/ctrlc/root/ui/cmd.go b/cmd/ctrlc/root/ui/cmd.go index 9e0b952..0fad314 100644 --- a/cmd/ctrlc/root/ui/cmd.go +++ b/cmd/ctrlc/root/ui/cmd.go @@ -36,9 +36,9 @@ func NewUICmd() *cobra.Command { } // Resolve workspace slug to UUID - workspaceID := client.GetWorkspaceID(cmd.Context(), workspace) - if workspaceID.String() == "00000000-0000-0000-0000-000000000000" { - return fmt.Errorf("failed to resolve workspace: %s", workspace) + workspaceID, err := client.GetWorkspaceID(cmd.Context(), workspace) + if err != nil { + return err } // Load last-viewed resource type (default: resources) diff --git a/cmd/ctrlc/root/workflow/list.go b/cmd/ctrlc/root/workflow/list.go new file mode 100644 index 0000000..02aacc3 --- /dev/null +++ b/cmd/ctrlc/root/workflow/list.go @@ -0,0 +1,63 @@ +package workflow + +import ( + "fmt" + + "github.com/ctrlplanedev/cli/internal/api" + "github.com/ctrlplanedev/cli/internal/cliutil" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewListCmd() *cobra.Command { + var limit int + var offset int + + cmd := &cobra.Command{ + Use: "list", + Short: "List workflows", + Long: `List all workflows in the workspace.`, + RunE: func(cmd *cobra.Command, args []string) error { + apiURL := viper.GetString("url") + apiKey := viper.GetString("api-key") + workspace := viper.GetString("workspace") + + client, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey) + if err != nil { + return fmt.Errorf("failed to create API client: %w", err) + } + + workspaceID, err := client.GetWorkspaceID(cmd.Context(), workspace) + if err != nil { + return err + } + + if limit < 0 { + return fmt.Errorf("invalid --limit %d, must be non-negative", limit) + } + if offset < 0 { + return fmt.Errorf("invalid --offset %d, must be non-negative", offset) + } + + params := &api.ListWorkflowsParams{} + if limit > 0 { + params.Limit = &limit + } + if offset > 0 { + params.Offset = &offset + } + + resp, err := client.ListWorkflows(cmd.Context(), workspaceID.String(), params) + if err != nil { + return fmt.Errorf("failed to list workflows: %w", err) + } + + return cliutil.HandleResponseOutput(cmd, resp) + }, + } + + cmd.Flags().IntVarP(&limit, "limit", "l", 50, "Limit the number of results") + cmd.Flags().IntVarP(&offset, "offset", "o", 0, "Offset the results") + + return cmd +} diff --git a/cmd/ctrlc/root/workflow/trigger.go b/cmd/ctrlc/root/workflow/trigger.go new file mode 100644 index 0000000..ea4e036 --- /dev/null +++ b/cmd/ctrlc/root/workflow/trigger.go @@ -0,0 +1,66 @@ +package workflow + +import ( + "fmt" + "strings" + + "github.com/ctrlplanedev/cli/internal/api" + "github.com/ctrlplanedev/cli/internal/cliutil" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewTriggerCmd() *cobra.Command { + var inputFlags []string + + cmd := &cobra.Command{ + Use: "trigger ", + Short: "Trigger a workflow run", + Long: `Trigger a workflow run with the given inputs.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + workflowID := args[0] + + apiURL := viper.GetString("url") + apiKey := viper.GetString("api-key") + workspace := viper.GetString("workspace") + + client, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey) + if err != nil { + return fmt.Errorf("failed to create API client: %w", err) + } + + workspaceID, err := client.GetWorkspaceID(cmd.Context(), workspace) + if err != nil { + return err + } + + inputs := make(map[string]interface{}) + for _, input := range inputFlags { + key, value, found := strings.Cut(input, "=") + if !found { + return fmt.Errorf("invalid input format %q, expected key=value", input) + } + if key == "" { + return fmt.Errorf("invalid input format %q, empty key, expected key=value", input) + } + inputs[key] = value + } + + body := api.CreateWorkflowRunJSONRequestBody{ + Inputs: inputs, + } + + resp, err := client.CreateWorkflowRun(cmd.Context(), workspaceID.String(), workflowID, body) + if err != nil { + return fmt.Errorf("failed to trigger workflow: %w", err) + } + + return cliutil.HandleResponseOutput(cmd, resp) + }, + } + + cmd.Flags().StringArrayVarP(&inputFlags, "input", "i", nil, "Input key=value pair (can be specified multiple times)") + + return cmd +} diff --git a/cmd/ctrlc/root/workflow/workflow.go b/cmd/ctrlc/root/workflow/workflow.go new file mode 100644 index 0000000..c768127 --- /dev/null +++ b/cmd/ctrlc/root/workflow/workflow.go @@ -0,0 +1,21 @@ +package workflow + +import ( + "github.com/spf13/cobra" +) + +func NewWorkflowCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "workflow ", + Short: "Manage workflows", + Long: `Commands for listing and triggering workflows.`, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + + cmd.AddCommand(NewListCmd()) + cmd.AddCommand(NewTriggerCmd()) + + return cmd +} diff --git a/internal/api/client.gen.go b/internal/api/client.gen.go index f47e156..73cb541 100644 --- a/internal/api/client.gen.go +++ b/internal/api/client.gen.go @@ -553,6 +553,25 @@ type DeploymentVersionDependency struct { // DeploymentVersionStatus defines model for DeploymentVersionStatus. type DeploymentVersionStatus string +// DeploymentVersionWithDependencies defines model for DeploymentVersionWithDependencies. +type DeploymentVersionWithDependencies struct { + Config map[string]interface{} `json:"config"` + CreatedAt time.Time `json:"createdAt"` + + // Dependencies Map of dependency deployment ID to its CEL version selector evaluated against that deployment's current release on the same resource. + Dependencies map[string]struct { + VersionSelector string `json:"versionSelector"` + } `json:"dependencies"` + DeploymentId string `json:"deploymentId"` + Id string `json:"id"` + JobAgentConfig map[string]interface{} `json:"jobAgentConfig"` + Message *string `json:"message,omitempty"` + Metadata *map[string]string `json:"metadata,omitempty"` + Name string `json:"name"` + Status DeploymentVersionStatus `json:"status"` + Tag string `json:"tag"` +} + // DeploymentWindowRule defines model for DeploymentWindowRule. type DeploymentWindowRule struct { // AllowWindow If true, deployments are only allowed during the window. If false, deployments are blocked during the window (deny window) @@ -10394,7 +10413,7 @@ type ListDeploymentVersionsResponse struct { Body []byte HTTPResponse *http.Response JSON200 *struct { - Items []DeploymentVersion `json:"items"` + Items []DeploymentVersionWithDependencies `json:"items"` // Limit Maximum number of items returned Limit int `json:"limit"` @@ -14374,7 +14393,7 @@ func ParseListDeploymentVersionsResponse(rsp *http.Response) (*ListDeploymentVer switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { - Items []DeploymentVersion `json:"items"` + Items []DeploymentVersionWithDependencies `json:"items"` // Limit Maximum number of items returned Limit int `json:"limit"` diff --git a/internal/api/client.go b/internal/api/client.go index 9da03e1..c60244f 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -23,20 +23,22 @@ func NewAPIKeyClientWithResponses(server string, apiKey string) (*ClientWithResp ) } -func (c *ClientWithResponses) GetWorkspaceID(ctx context.Context, workspace string) uuid.UUID { - id, err := uuid.Parse(workspace) - if err == nil { - return id +func (c *ClientWithResponses) GetWorkspaceID(ctx context.Context, workspace string) (uuid.UUID, error) { + if id, err := uuid.Parse(workspace); err == nil { + return id, nil } resp, err := c.GetWorkspaceBySlugWithResponse(ctx, workspace) if err != nil { - return uuid.Nil + return uuid.Nil, fmt.Errorf("failed to look up workspace %q: %w", workspace, err) } if resp.JSON200 == nil { - return uuid.Nil + if resp.StatusCode() == http.StatusNotFound { + return uuid.Nil, fmt.Errorf("workspace %q not found", workspace) + } + return uuid.Nil, fmt.Errorf("failed to look up workspace %q: %s: %s", workspace, resp.Status(), strings.TrimSpace(string(resp.Body))) } - return resp.JSON200.Id + return resp.JSON200.Id, nil } diff --git a/internal/api/resolver/resolver.go b/internal/api/resolver/resolver.go index 50f582f..4a52995 100644 --- a/internal/api/resolver/resolver.go +++ b/internal/api/resolver/resolver.go @@ -25,9 +25,9 @@ func NewAPIResolver(client *api.ClientWithResponses, workspaceID uuid.UUID) *API } func NewAPIResolverFromWorkspace(ctx context.Context, client *api.ClientWithResponses, workspace string) (*APIResolver, error) { - workspaceID := client.GetWorkspaceID(ctx, workspace) - if workspaceID == uuid.Nil { - return nil, fmt.Errorf("workspace not found: %s", workspace) + workspaceID, err := client.GetWorkspaceID(ctx, workspace) + if err != nil { + return nil, err } return NewAPIResolver(client, workspaceID), nil } diff --git a/internal/resources/service.go b/internal/resources/service.go index f164795..15ce9c2 100644 --- a/internal/resources/service.go +++ b/internal/resources/service.go @@ -26,7 +26,10 @@ func NewAPIResourceService(ctx context.Context, apiURL, apiKey, workspace string return nil, fmt.Errorf("failed to create API client: %w", err) } - workspaceID := client.GetWorkspaceID(ctx, workspace) + workspaceID, err := client.GetWorkspaceID(ctx, workspace) + if err != nil { + return nil, err + } log.Debug("resolved workspace", "input", workspace, "workspaceID", workspaceID.String()) return &APIResourceService{ diff --git a/pkg/resourceprovider/resourceprovider.go b/pkg/resourceprovider/resourceprovider.go index ddde245..b3fc590 100644 --- a/pkg/resourceprovider/resourceprovider.go +++ b/pkg/resourceprovider/resourceprovider.go @@ -12,7 +12,11 @@ import ( func New(client *api.ClientWithResponses, workspace string, name string) (*ResourceProvider, error) { ctx := context.Background() - workspaceId := client.GetWorkspaceID(ctx, workspace).String() + workspaceUUID, err := client.GetWorkspaceID(ctx, workspace) + if err != nil { + return nil, err + } + workspaceId := workspaceUUID.String() log.Debug("Upserting resource provider", "workspaceId", workspaceId, "name", name)