diff --git a/docs/resources/workflow.md b/docs/resources/workflow.md new file mode 100644 index 0000000..1715e77 --- /dev/null +++ b/docs/resources/workflow.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "ctrlplane_workflow Resource - ctrlplane" +subcategory: "" +description: |- + Manages a workflow in Ctrlplane. +--- + +# ctrlplane_workflow (Resource) + +Manages a workflow in Ctrlplane. + + + + +## Schema + +### Required + +- `name` (String) The name of the workflow. + +### Optional + +- `inputs` (String) JSON-encoded array of workflow input definitions. +- `job_agent` (Block List) Job agents to dispatch when the workflow runs. (see [below for nested schema](#nestedblock--job_agent)) + +### Read-Only + +- `id` (String) The ID of the workflow. + + +### Nested Schema for `job_agent` + +Required: + +- `config` (Map of String) Configuration for the job agent. +- `name` (String) Name of the job agent entry. +- `ref` (String) ID of the job agent to reference. +- `selector` (String) CEL expression to determine if the job agent should dispatch. Use "true" to always dispatch. diff --git a/examples/resources/workflow/providers.tf b/examples/resources/workflow/providers.tf new file mode 100644 index 0000000..d00eb4a --- /dev/null +++ b/examples/resources/workflow/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + ctrlplane = { + source = "ctrlplanedev/ctrlplane" + version = "~> 1" + } + } +} + +provider "ctrlplane" { + workspace = var.workspace + url = var.url + api_key = var.api_key +} diff --git a/examples/resources/workflow/variables.tf b/examples/resources/workflow/variables.tf new file mode 100644 index 0000000..ac125e9 --- /dev/null +++ b/examples/resources/workflow/variables.tf @@ -0,0 +1,15 @@ +variable "workspace" { + type = string + description = "The workspace to use" +} + +variable "url" { + type = string + description = "The URL of the Ctrlplane API" +} + +variable "api_key" { + type = string + description = "The API key for the Ctrlplane API" + sensitive = true +} diff --git a/examples/resources/workflow/workflows.tf b/examples/resources/workflow/workflows.tf new file mode 100644 index 0000000..9b35852 --- /dev/null +++ b/examples/resources/workflow/workflows.tf @@ -0,0 +1,43 @@ +resource "ctrlplane_job_agent" "runner_1" { + name = "workflow-runner-1" + + test_runner { + delay_seconds = 10 + status = "successful" + message = "Test runner job agent" + } +} + +resource "ctrlplane_job_agent" "runner_2" { + name = "workflow-runner-2" + + test_runner { + delay_seconds = 10 + status = "successful" + message = "Test runner job agent" + } +} + +resource "ctrlplane_workflow" "example" { + name = "example-workflow" + + inputs = jsonencode([ + { key = "environment", type = "string", default = "staging" }, + { key = "retries", type = "number", default = 3 }, + { key = "dryRun", type = "boolean", default = true }, + ]) + + job_agent { + name = "workflow-runner-1" + ref = ctrlplane_job_agent.runner_1.id + config = { delaySeconds = "10", message = "Test runner job agent", status = "successful" } + selector = "true" + } + + job_agent { + name = "workflow-runner-2" + ref = ctrlplane_job_agent.runner_2.id + config = { delaySeconds = "10", message = "Test runner job agent", status = "successful" } + selector = "true" + } +} diff --git a/internal/api/client.gen.go b/internal/api/client.gen.go index 3243f9d..7a642dc 100644 --- a/internal/api/client.gen.go +++ b/internal/api/client.gen.go @@ -118,6 +118,13 @@ const ( Prometheus PrometheusMetricProviderType = "prometheus" ) +// Defines values for ReleaseTargetStateResponseLatestJobVerificationsStatus. +const ( + ReleaseTargetStateResponseLatestJobVerificationsStatusFailed ReleaseTargetStateResponseLatestJobVerificationsStatus = "failed" + ReleaseTargetStateResponseLatestJobVerificationsStatusPassed ReleaseTargetStateResponseLatestJobVerificationsStatus = "passed" + ReleaseTargetStateResponseLatestJobVerificationsStatusRunning ReleaseTargetStateResponseLatestJobVerificationsStatus = "running" +) + // Defines values for RetryRuleBackoffStrategy. const ( RetryRuleBackoffStrategyExponential RetryRuleBackoffStrategy = "exponential" @@ -134,6 +141,13 @@ const ( TerraformCloudRun TerraformCloudRunMetricProviderType = "terraformCloudRun" ) +// Defines values for VerificationMeasurementStatus. +const ( + VerificationMeasurementStatusFailed VerificationMeasurementStatus = "failed" + VerificationMeasurementStatusInconclusive VerificationMeasurementStatus = "inconclusive" + VerificationMeasurementStatusPassed VerificationMeasurementStatus = "passed" +) + // Defines values for VerificationRuleTriggerOn. const ( JobCreated VerificationRuleTriggerOn = "jobCreated" @@ -279,23 +293,22 @@ type CreateSystemRequest struct { // CreateWorkflow defines model for CreateWorkflow. type CreateWorkflow struct { - Inputs []WorkflowInput `json:"inputs"` - Jobs []CreateWorkflowJobTemplate `json:"jobs"` - Name string `json:"name"` + Inputs []WorkflowInput `json:"inputs"` + JobAgents []CreateWorkflowJobAgent `json:"jobAgents"` + Name string `json:"name"` } -// CreateWorkflowJobTemplate defines model for CreateWorkflowJobTemplate. -type CreateWorkflowJobTemplate struct { +// CreateWorkflowJobAgent defines model for CreateWorkflowJobAgent. +type CreateWorkflowJobAgent struct { // Config Configuration for the job agent Config map[string]interface{} `json:"config"` - - // If CEL expression to determine if the job should run - If *string `json:"if,omitempty"` - Matrix *WorkflowJobMatrix `json:"matrix,omitempty"` - Name string `json:"name"` + Name string `json:"name"` // Ref Reference to the job agent Ref string `json:"ref"` + + // Selector CEL expression to determine if the job agent should dispatch a job + Selector string `json:"selector"` } // CreateWorkspaceRequest defines model for CreateWorkspaceRequest. @@ -844,6 +857,28 @@ type ReleaseTargetState struct { LatestJob *Job `json:"latestJob,omitempty"` } +// ReleaseTargetStateResponse defines model for ReleaseTargetStateResponse. +type ReleaseTargetStateResponse struct { + CurrentRelease *Release `json:"currentRelease,omitempty"` + DesiredRelease *Release `json:"desiredRelease,omitempty"` + LatestJob *struct { + Job Job `json:"job"` + Verifications []struct { + CreatedAt time.Time `json:"createdAt"` + Id string `json:"id"` + JobId string `json:"jobId"` + Message *string `json:"message,omitempty"` + Metrics []VerificationMetricStatus `json:"metrics"` + + // Status Computed aggregate status of this verification + Status ReleaseTargetStateResponseLatestJobVerificationsStatus `json:"status"` + } `json:"verifications"` + } `json:"latestJob,omitempty"` +} + +// ReleaseTargetStateResponseLatestJobVerificationsStatus Computed aggregate status of this verification +type ReleaseTargetStateResponseLatestJobVerificationsStatus string + // ReleaseTargetWithState defines model for ReleaseTargetWithState. type ReleaseTargetWithState struct { ReleaseTarget ReleaseTarget `json:"releaseTarget"` @@ -1032,9 +1067,9 @@ type UpdateDeploymentVersionRequest struct { // UpdateWorkflow defines model for UpdateWorkflow. type UpdateWorkflow struct { - Inputs []WorkflowInput `json:"inputs"` - Jobs []CreateWorkflowJobTemplate `json:"jobs"` - Name string `json:"name"` + Inputs []WorkflowInput `json:"inputs"` + JobAgents []CreateWorkflowJobAgent `json:"jobAgents"` + Name string `json:"name"` } // UpdateWorkspaceRequest defines model for UpdateWorkspaceRequest. @@ -1145,6 +1180,16 @@ type UpsertResourceProviderRequest struct { Name string `json:"name"` } +// UpsertResourceRequest defines model for UpsertResourceRequest. +type UpsertResourceRequest struct { + Config *map[string]interface{} `json:"config,omitempty"` + Kind string `json:"kind"` + Metadata *map[string]string `json:"metadata,omitempty"` + Name string `json:"name"` + Variables *map[string]interface{} `json:"variables,omitempty"` + Version string `json:"version"` +} + // UpsertSystemRequest defines model for UpsertSystemRequest. type UpsertSystemRequest struct { Description *string `json:"description,omitempty"` @@ -1171,6 +1216,24 @@ type Value struct { union json.RawMessage } +// VerificationMeasurement defines model for VerificationMeasurement. +type VerificationMeasurement struct { + // Data Raw measurement data + Data *map[string]interface{} `json:"data,omitempty"` + + // MeasuredAt When measurement was taken + MeasuredAt time.Time `json:"measuredAt"` + + // Message Measurement result message + Message *string `json:"message,omitempty"` + + // Status Status of a verification measurement + Status VerificationMeasurementStatus `json:"status"` +} + +// VerificationMeasurementStatus Status of a verification measurement +type VerificationMeasurementStatus string + // VerificationMetricSpec defines model for VerificationMetricSpec. type VerificationMetricSpec struct { // Count Number of measurements to take @@ -1196,6 +1259,34 @@ type VerificationMetricSpec struct { SuccessThreshold *int `json:"successThreshold,omitempty"` } +// VerificationMetricStatus defines model for VerificationMetricStatus. +type VerificationMetricStatus struct { + // Count Number of measurements to take + Count int `json:"count"` + + // FailureCondition CEL expression to evaluate measurement failure (e.g., "result.statusCode == 500"), if not provided, a failure is just the opposite of the success condition + FailureCondition *string `json:"failureCondition,omitempty"` + + // FailureThreshold Stop after this many consecutive failures (0 = no limit) + FailureThreshold *int `json:"failureThreshold,omitempty"` + + // IntervalSeconds Interval between measurements in seconds + IntervalSeconds int32 `json:"intervalSeconds"` + + // Measurements Individual verification measurements taken for this metric + Measurements []VerificationMeasurement `json:"measurements"` + + // Name Name of the verification metric + Name string `json:"name"` + Provider MetricProvider `json:"provider"` + + // SuccessCondition CEL expression to evaluate measurement success (e.g., "result.statusCode == 200") + SuccessCondition string `json:"successCondition"` + + // SuccessThreshold Minimum number of consecutive successful measurements required to consider the metric successful + SuccessThreshold *int `json:"successThreshold,omitempty"` +} + // VerificationRule defines model for VerificationRule. type VerificationRule struct { // Metrics Metrics to verify @@ -1225,10 +1316,10 @@ type VersionSelectorRule struct { // Workflow defines model for Workflow. type Workflow struct { - Id string `json:"id"` - Inputs []WorkflowInput `json:"inputs"` - Jobs []WorkflowJobTemplate `json:"jobs"` - Name string `json:"name"` + Id string `json:"id"` + Inputs []WorkflowInput `json:"inputs"` + JobAgents []WorkflowJobAgent `json:"jobAgents"` + Name string `json:"name"` } // WorkflowArrayInput defines model for WorkflowArrayInput. @@ -1263,33 +1354,17 @@ type WorkflowJob struct { WorkflowId string `json:"workflowId"` } -// WorkflowJobMatrix defines model for WorkflowJobMatrix. -type WorkflowJobMatrix map[string]WorkflowJobMatrix_AdditionalProperties - -// WorkflowJobMatrix0 defines model for . -type WorkflowJobMatrix0 = []map[string]interface{} - -// WorkflowJobMatrix1 defines model for . -type WorkflowJobMatrix1 = string - -// WorkflowJobMatrix_AdditionalProperties defines model for WorkflowJobMatrix.AdditionalProperties. -type WorkflowJobMatrix_AdditionalProperties struct { - union json.RawMessage -} - -// WorkflowJobTemplate defines model for WorkflowJobTemplate. -type WorkflowJobTemplate struct { +// WorkflowJobAgent defines model for WorkflowJobAgent. +type WorkflowJobAgent struct { // Config Configuration for the job agent Config map[string]interface{} `json:"config"` - Id string `json:"id"` - - // If CEL expression to determine if the job should run - If *string `json:"if,omitempty"` - Matrix *WorkflowJobMatrix `json:"matrix,omitempty"` - Name string `json:"name"` + Name string `json:"name"` // Ref Reference to the job agent Ref string `json:"ref"` + + // Selector CEL expression to determine if the job agent should dispatch a job + Selector string `json:"selector"` } // WorkflowManualArrayInput defines model for WorkflowManualArrayInput. @@ -1554,6 +1629,12 @@ type ListWorkflowsParams struct { Offset *int `form:"offset,omitempty" json:"offset,omitempty"` } +// CreateWorkflowRunJSONBody defines parameters for CreateWorkflowRun. +type CreateWorkflowRunJSONBody struct { + // Inputs Input values for the workflow run. + Inputs map[string]interface{} `json:"inputs"` +} + // CreateWorkspaceJSONRequestBody defines body for CreateWorkspace for application/json ContentType. type CreateWorkspaceJSONRequestBody = CreateWorkspaceRequest @@ -1620,6 +1701,9 @@ type RequestResourceProviderUpsertJSONRequestBody = UpsertResourceProviderReques // SetResourceProviderResourcesJSONRequestBody defines body for SetResourceProviderResources for application/json ContentType. type SetResourceProviderResourcesJSONRequestBody SetResourceProviderResourcesJSONBody +// UpsertResourceByIdentifierJSONRequestBody defines body for UpsertResourceByIdentifier for application/json ContentType. +type UpsertResourceByIdentifierJSONRequestBody = UpsertResourceRequest + // RequestResourceVariablesUpdateJSONRequestBody defines body for RequestResourceVariablesUpdate for application/json ContentType. type RequestResourceVariablesUpdateJSONRequestBody RequestResourceVariablesUpdateJSONBody @@ -1635,6 +1719,9 @@ type CreateWorkflowJSONRequestBody = CreateWorkflow // UpdateWorkflowJSONRequestBody defines body for UpdateWorkflow for application/json ContentType. type UpdateWorkflowJSONRequestBody = UpdateWorkflow +// CreateWorkflowRunJSONRequestBody defines body for CreateWorkflowRun for application/json ContentType. +type CreateWorkflowRunJSONRequestBody CreateWorkflowRunJSONBody + // AsBooleanValue returns the union data inside the LiteralValue as a BooleanValue func (t LiteralValue) AsBooleanValue() (BooleanValue, error) { var body BooleanValue @@ -2270,68 +2357,6 @@ func (t *WorkflowInput) UnmarshalJSON(b []byte) error { return err } -// AsWorkflowJobMatrix0 returns the union data inside the WorkflowJobMatrix_AdditionalProperties as a WorkflowJobMatrix0 -func (t WorkflowJobMatrix_AdditionalProperties) AsWorkflowJobMatrix0() (WorkflowJobMatrix0, error) { - var body WorkflowJobMatrix0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromWorkflowJobMatrix0 overwrites any union data inside the WorkflowJobMatrix_AdditionalProperties as the provided WorkflowJobMatrix0 -func (t *WorkflowJobMatrix_AdditionalProperties) FromWorkflowJobMatrix0(v WorkflowJobMatrix0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeWorkflowJobMatrix0 performs a merge with any union data inside the WorkflowJobMatrix_AdditionalProperties, using the provided WorkflowJobMatrix0 -func (t *WorkflowJobMatrix_AdditionalProperties) MergeWorkflowJobMatrix0(v WorkflowJobMatrix0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsWorkflowJobMatrix1 returns the union data inside the WorkflowJobMatrix_AdditionalProperties as a WorkflowJobMatrix1 -func (t WorkflowJobMatrix_AdditionalProperties) AsWorkflowJobMatrix1() (WorkflowJobMatrix1, error) { - var body WorkflowJobMatrix1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromWorkflowJobMatrix1 overwrites any union data inside the WorkflowJobMatrix_AdditionalProperties as the provided WorkflowJobMatrix1 -func (t *WorkflowJobMatrix_AdditionalProperties) FromWorkflowJobMatrix1(v WorkflowJobMatrix1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeWorkflowJobMatrix1 performs a merge with any union data inside the WorkflowJobMatrix_AdditionalProperties, using the provided WorkflowJobMatrix1 -func (t *WorkflowJobMatrix_AdditionalProperties) MergeWorkflowJobMatrix1(v WorkflowJobMatrix1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t WorkflowJobMatrix_AdditionalProperties) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *WorkflowJobMatrix_AdditionalProperties) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - // RequestEditorFn is the function signature for the RequestEditor callback function type RequestEditorFn func(ctx context.Context, req *http.Request) error @@ -2632,6 +2657,11 @@ type ClientInterface interface { // GetResourceByIdentifier request GetResourceByIdentifier(ctx context.Context, workspaceId string, identifier string, reqEditors ...RequestEditorFn) (*http.Response, error) + // UpsertResourceByIdentifierWithBody request with any body + UpsertResourceByIdentifierWithBody(ctx context.Context, workspaceId string, identifier string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + UpsertResourceByIdentifier(ctx context.Context, workspaceId string, identifier string, body UpsertResourceByIdentifierJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetDeploymentsForResource request GetDeploymentsForResource(ctx context.Context, workspaceId string, identifier string, params *GetDeploymentsForResourceParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -2701,6 +2731,11 @@ type ClientInterface interface { UpdateWorkflowWithBody(ctx context.Context, workspaceId string, workflowId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) UpdateWorkflow(ctx context.Context, workspaceId string, workflowId string, body UpdateWorkflowJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CreateWorkflowRunWithBody request with any body + CreateWorkflowRunWithBody(ctx context.Context, workspaceId string, workflowId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateWorkflowRun(ctx context.Context, workspaceId string, workflowId string, body CreateWorkflowRunJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) } func (c *Client) ListWorkspaces(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -3699,6 +3734,30 @@ func (c *Client) GetResourceByIdentifier(ctx context.Context, workspaceId string return c.Client.Do(req) } +func (c *Client) UpsertResourceByIdentifierWithBody(ctx context.Context, workspaceId string, identifier string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpsertResourceByIdentifierRequestWithBody(c.Server, workspaceId, identifier, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpsertResourceByIdentifier(ctx context.Context, workspaceId string, identifier string, body UpsertResourceByIdentifierJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpsertResourceByIdentifierRequest(c.Server, workspaceId, identifier, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) GetDeploymentsForResource(ctx context.Context, workspaceId string, identifier string, params *GetDeploymentsForResourceParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGetDeploymentsForResourceRequest(c.Server, workspaceId, identifier, params) if err != nil { @@ -3999,6 +4058,30 @@ func (c *Client) UpdateWorkflow(ctx context.Context, workspaceId string, workflo return c.Client.Do(req) } +func (c *Client) CreateWorkflowRunWithBody(ctx context.Context, workspaceId string, workflowId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateWorkflowRunRequestWithBody(c.Server, workspaceId, workflowId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateWorkflowRun(ctx context.Context, workspaceId string, workflowId string, body CreateWorkflowRunJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateWorkflowRunRequest(c.Server, workspaceId, workflowId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + // NewListWorkspacesRequest generates requests for ListWorkspaces func NewListWorkspacesRequest(server string) (*http.Request, error) { var err error @@ -7149,6 +7232,60 @@ func NewGetResourceByIdentifierRequest(server string, workspaceId string, identi return req, nil } +// NewUpsertResourceByIdentifierRequest calls the generic UpsertResourceByIdentifier builder with application/json body +func NewUpsertResourceByIdentifierRequest(server string, workspaceId string, identifier string, body UpsertResourceByIdentifierJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewUpsertResourceByIdentifierRequestWithBody(server, workspaceId, identifier, "application/json", bodyReader) +} + +// NewUpsertResourceByIdentifierRequestWithBody generates requests for UpsertResourceByIdentifier with any type of body +func NewUpsertResourceByIdentifierRequestWithBody(server string, workspaceId string, identifier string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "workspaceId", runtime.ParamLocationPath, workspaceId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "identifier", runtime.ParamLocationPath, identifier) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/workspaces/%s/resources/identifier/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewGetDeploymentsForResourceRequest generates requests for GetDeploymentsForResource func NewGetDeploymentsForResourceRequest(server string, workspaceId string, identifier string, params *GetDeploymentsForResourceParams) (*http.Request, error) { var err error @@ -8207,6 +8344,60 @@ func NewUpdateWorkflowRequestWithBody(server string, workspaceId string, workflo return req, nil } +// NewCreateWorkflowRunRequest calls the generic CreateWorkflowRun builder with application/json body +func NewCreateWorkflowRunRequest(server string, workspaceId string, workflowId string, body CreateWorkflowRunJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateWorkflowRunRequestWithBody(server, workspaceId, workflowId, "application/json", bodyReader) +} + +// NewCreateWorkflowRunRequestWithBody generates requests for CreateWorkflowRun with any type of body +func NewCreateWorkflowRunRequestWithBody(server string, workspaceId string, workflowId string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "workspaceId", runtime.ParamLocationPath, workspaceId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "workflowId", runtime.ParamLocationPath, workflowId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/workspaces/%s/workflows/%s/runs", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { for _, r := range c.RequestEditors { if err := r(ctx, req); err != nil { @@ -8477,6 +8668,11 @@ type ClientWithResponsesInterface interface { // GetResourceByIdentifierWithResponse request GetResourceByIdentifierWithResponse(ctx context.Context, workspaceId string, identifier string, reqEditors ...RequestEditorFn) (*GetResourceByIdentifierResponse, error) + // UpsertResourceByIdentifierWithBodyWithResponse request with any body + UpsertResourceByIdentifierWithBodyWithResponse(ctx context.Context, workspaceId string, identifier string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpsertResourceByIdentifierResponse, error) + + UpsertResourceByIdentifierWithResponse(ctx context.Context, workspaceId string, identifier string, body UpsertResourceByIdentifierJSONRequestBody, reqEditors ...RequestEditorFn) (*UpsertResourceByIdentifierResponse, error) + // GetDeploymentsForResourceWithResponse request GetDeploymentsForResourceWithResponse(ctx context.Context, workspaceId string, identifier string, params *GetDeploymentsForResourceParams, reqEditors ...RequestEditorFn) (*GetDeploymentsForResourceResponse, error) @@ -8546,6 +8742,11 @@ type ClientWithResponsesInterface interface { UpdateWorkflowWithBodyWithResponse(ctx context.Context, workspaceId string, workflowId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateWorkflowResponse, error) UpdateWorkflowWithResponse(ctx context.Context, workspaceId string, workflowId string, body UpdateWorkflowJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateWorkflowResponse, error) + + // CreateWorkflowRunWithBodyWithResponse request with any body + CreateWorkflowRunWithBodyWithResponse(ctx context.Context, workspaceId string, workflowId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateWorkflowRunResponse, error) + + CreateWorkflowRunWithResponse(ctx context.Context, workspaceId string, workflowId string, body CreateWorkflowRunJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateWorkflowRunResponse, error) } type ListWorkspacesResponse struct { @@ -9882,7 +10083,7 @@ func (r GetJobsForReleaseTargetResponse) StatusCode() int { type GetReleaseTargetStateResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *ReleaseTargetState + JSON200 *ReleaseTargetStateResponse JSON400 *ErrorResponse JSON404 *ErrorResponse } @@ -10134,6 +10335,30 @@ func (r GetResourceByIdentifierResponse) StatusCode() int { return 0 } +type UpsertResourceByIdentifierResponse struct { + Body []byte + HTTPResponse *http.Response + JSON202 *ResourceRequestAccepted + JSON400 *ErrorResponse + JSON404 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r UpsertResourceByIdentifierResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UpsertResourceByIdentifierResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type GetDeploymentsForResourceResponse struct { Body []byte HTTPResponse *http.Response @@ -10558,7 +10783,7 @@ func (r ListWorkflowsResponse) StatusCode() int { type CreateWorkflowResponse struct { Body []byte HTTPResponse *http.Response - JSON202 *Workflow + JSON201 *Workflow JSON400 *ErrorResponse } @@ -10650,6 +10875,30 @@ func (r UpdateWorkflowResponse) StatusCode() int { return 0 } +type CreateWorkflowRunResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *WorkflowRun + JSON400 *ErrorResponse + JSON404 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r CreateWorkflowRunResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateWorkflowRunResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + // ListWorkspacesWithResponse request returning *ListWorkspacesResponse func (c *ClientWithResponses) ListWorkspacesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListWorkspacesResponse, error) { rsp, err := c.ListWorkspaces(ctx, reqEditors...) @@ -11375,6 +11624,23 @@ func (c *ClientWithResponses) GetResourceByIdentifierWithResponse(ctx context.Co return ParseGetResourceByIdentifierResponse(rsp) } +// UpsertResourceByIdentifierWithBodyWithResponse request with arbitrary body returning *UpsertResourceByIdentifierResponse +func (c *ClientWithResponses) UpsertResourceByIdentifierWithBodyWithResponse(ctx context.Context, workspaceId string, identifier string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpsertResourceByIdentifierResponse, error) { + rsp, err := c.UpsertResourceByIdentifierWithBody(ctx, workspaceId, identifier, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpsertResourceByIdentifierResponse(rsp) +} + +func (c *ClientWithResponses) UpsertResourceByIdentifierWithResponse(ctx context.Context, workspaceId string, identifier string, body UpsertResourceByIdentifierJSONRequestBody, reqEditors ...RequestEditorFn) (*UpsertResourceByIdentifierResponse, error) { + rsp, err := c.UpsertResourceByIdentifier(ctx, workspaceId, identifier, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpsertResourceByIdentifierResponse(rsp) +} + // GetDeploymentsForResourceWithResponse request returning *GetDeploymentsForResourceResponse func (c *ClientWithResponses) GetDeploymentsForResourceWithResponse(ctx context.Context, workspaceId string, identifier string, params *GetDeploymentsForResourceParams, reqEditors ...RequestEditorFn) (*GetDeploymentsForResourceResponse, error) { rsp, err := c.GetDeploymentsForResource(ctx, workspaceId, identifier, params, reqEditors...) @@ -11595,6 +11861,23 @@ func (c *ClientWithResponses) UpdateWorkflowWithResponse(ctx context.Context, wo return ParseUpdateWorkflowResponse(rsp) } +// CreateWorkflowRunWithBodyWithResponse request with arbitrary body returning *CreateWorkflowRunResponse +func (c *ClientWithResponses) CreateWorkflowRunWithBodyWithResponse(ctx context.Context, workspaceId string, workflowId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateWorkflowRunResponse, error) { + rsp, err := c.CreateWorkflowRunWithBody(ctx, workspaceId, workflowId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateWorkflowRunResponse(rsp) +} + +func (c *ClientWithResponses) CreateWorkflowRunWithResponse(ctx context.Context, workspaceId string, workflowId string, body CreateWorkflowRunJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateWorkflowRunResponse, error) { + rsp, err := c.CreateWorkflowRun(ctx, workspaceId, workflowId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateWorkflowRunResponse(rsp) +} + // ParseListWorkspacesResponse parses an HTTP response from a ListWorkspacesWithResponse call func ParseListWorkspacesResponse(rsp *http.Response) (*ListWorkspacesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -13661,7 +13944,7 @@ func ParseGetReleaseTargetStateResponse(rsp *http.Response) (*GetReleaseTargetSt switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ReleaseTargetState + var dest ReleaseTargetStateResponse if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -14019,6 +14302,46 @@ func ParseGetResourceByIdentifierResponse(rsp *http.Response) (*GetResourceByIde return response, nil } +// ParseUpsertResourceByIdentifierResponse parses an HTTP response from a UpsertResourceByIdentifierWithResponse call +func ParseUpsertResourceByIdentifierResponse(rsp *http.Response) (*UpsertResourceByIdentifierResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UpsertResourceByIdentifierResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest ResourceRequestAccepted + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + // ParseGetDeploymentsForResourceResponse parses an HTTP response from a GetDeploymentsForResourceWithResponse call func ParseGetDeploymentsForResourceResponse(rsp *http.Response) (*GetDeploymentsForResourceResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -14668,12 +14991,12 @@ func ParseCreateWorkflowResponse(rsp *http.Response) (*CreateWorkflowResponse, e } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: var dest Workflow if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON202 = &dest + response.JSON201 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest ErrorResponse @@ -14806,3 +15129,43 @@ func ParseUpdateWorkflowResponse(rsp *http.Response) (*UpdateWorkflowResponse, e return response, nil } + +// ParseCreateWorkflowRunResponse parses an HTTP response from a CreateWorkflowRunWithResponse call +func ParseCreateWorkflowRunResponse(rsp *http.Response) (*CreateWorkflowRunResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateWorkflowRunResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest WorkflowRun + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a0a2116..624b186 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -129,6 +129,7 @@ func (p *CtrlplaneProvider) Resources(ctx context.Context) []func() resource.Res NewRelationshipRuleResource, NewEnvironmentSystemLinkResource, NewDeploymentSystemLinkResource, + NewWorkflowResource, } } diff --git a/internal/provider/workflow_resource.go b/internal/provider/workflow_resource.go new file mode 100644 index 0000000..ea4d8e9 --- /dev/null +++ b/internal/provider/workflow_resource.go @@ -0,0 +1,324 @@ +// Copyright IBM Corp. 2021, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/ctrlplanedev/terraform-provider-ctrlplane/internal/api" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = &WorkflowResource{} +var _ resource.ResourceWithImportState = &WorkflowResource{} +var _ resource.ResourceWithConfigure = &WorkflowResource{} + +func NewWorkflowResource() resource.Resource { + return &WorkflowResource{} +} + +type WorkflowResource struct { + workspace *api.WorkspaceClient +} + +type WorkflowResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Inputs types.String `tfsdk:"inputs"` + JobAgents []WorkflowJobAgentModel `tfsdk:"job_agent"` +} + +type WorkflowJobAgentModel struct { + Name types.String `tfsdk:"name"` + Ref types.String `tfsdk:"ref"` + Config types.Map `tfsdk:"config"` + Selector types.String `tfsdk:"selector"` +} + +func (r *WorkflowResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_workflow" +} + +func (r *WorkflowResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *WorkflowResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + workspace, ok := req.ProviderData.(*api.WorkspaceClient) + if !ok { + resp.Diagnostics.AddError("Invalid provider data", "The provider data is not a *api.WorkspaceClient") + return + } + r.workspace = workspace +} + +func (r *WorkflowResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages a workflow in Ctrlplane.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of the workflow.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + Description: "The name of the workflow.", + }, + "inputs": schema.StringAttribute{ + Optional: true, + Description: "JSON-encoded array of workflow input definitions.", + }, + }, + Blocks: map[string]schema.Block{ + "job_agent": schema.ListNestedBlock{ + Description: "Job agents to dispatch when the workflow runs.", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Description: "Name of the job agent entry.", + }, + "ref": schema.StringAttribute{ + Required: true, + Description: "ID of the job agent to reference.", + }, + "config": schema.MapAttribute{ + Required: true, + Description: "Configuration for the job agent.", + ElementType: types.StringType, + }, + "selector": schema.StringAttribute{ + Required: true, + Description: "CEL expression to determine if the job agent should dispatch. Use \"true\" to always dispatch.", + }, + }, + }, + }, + }, + } +} + +func (r *WorkflowResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data WorkflowResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + inputs, err := parseWorkflowInputs(data.Inputs) + if err != nil { + resp.Diagnostics.AddError("Invalid inputs", err.Error()) + return + } + + body := api.CreateWorkflowJSONRequestBody{ + Name: data.Name.ValueString(), + Inputs: inputs, + JobAgents: workflowJobAgentsFromModel(data.JobAgents), + } + + createResp, err := r.workspace.Client.CreateWorkflowWithResponse(ctx, r.workspace.ID.String(), body) + if err != nil { + resp.Diagnostics.AddError("Failed to create workflow", err.Error()) + return + } + + if createResp.StatusCode() != http.StatusCreated { + resp.Diagnostics.AddError("Failed to create workflow", formatResponseError(createResp.StatusCode(), createResp.Body)) + return + } + + if createResp.JSON201 == nil { + resp.Diagnostics.AddError("Failed to create workflow", "Empty response from server") + return + } + + setWorkflowModelFromAPI(&data, createResp.JSON201) + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) +} + +func (r *WorkflowResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WorkflowResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + getResp, err := r.workspace.Client.GetWorkflowWithResponse(ctx, r.workspace.ID.String(), data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Failed to read workflow", err.Error()) + return + } + + switch getResp.StatusCode() { + case http.StatusOK: + if getResp.JSON200 == nil { + resp.Diagnostics.AddError("Failed to read workflow", "Empty response from server") + return + } + case http.StatusNotFound: + resp.State.RemoveResource(ctx) + return + default: + resp.Diagnostics.AddError("Failed to read workflow", formatResponseError(getResp.StatusCode(), getResp.Body)) + return + } + + setWorkflowModelFromAPI(&data, getResp.JSON200) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *WorkflowResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data WorkflowResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + inputs, err := parseWorkflowInputs(data.Inputs) + if err != nil { + resp.Diagnostics.AddError("Invalid inputs", err.Error()) + return + } + + body := api.UpdateWorkflowJSONRequestBody{ + Name: data.Name.ValueString(), + Inputs: inputs, + JobAgents: workflowJobAgentsFromModel(data.JobAgents), + } + + updateResp, err := r.workspace.Client.UpdateWorkflowWithResponse(ctx, r.workspace.ID.String(), data.ID.ValueString(), body) + if err != nil { + resp.Diagnostics.AddError("Failed to update workflow", err.Error()) + return + } + + if updateResp.StatusCode() != http.StatusAccepted { + resp.Diagnostics.AddError("Failed to update workflow", formatResponseError(updateResp.StatusCode(), updateResp.Body)) + return + } + + if updateResp.JSON202 == nil { + resp.Diagnostics.AddError("Failed to update workflow", "Empty response from server") + return + } + + setWorkflowModelFromAPI(&data, updateResp.JSON202) + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) +} + +func (r *WorkflowResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data WorkflowResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + deleteResp, err := r.workspace.Client.DeleteWorkflowWithResponse(ctx, r.workspace.ID.String(), data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Failed to delete workflow", err.Error()) + return + } + + switch deleteResp.StatusCode() { + case http.StatusAccepted, http.StatusNoContent: + return + case http.StatusNotFound: + return + default: + resp.Diagnostics.AddError("Failed to delete workflow", formatResponseError(deleteResp.StatusCode(), deleteResp.Body)) + } +} + +// --- helpers --- + +// normalizeInputsJSON re-marshals workflow inputs through a generic structure +// so that JSON key order is deterministic (Go sorts map keys alphabetically). +// This prevents Terraform from detecting spurious diffs due to key ordering. +func normalizeInputsJSON(inputs []api.WorkflowInput) string { + raw, err := json.Marshal(inputs) + if err != nil { + return "[]" + } + + var normalized []map[string]interface{} + if err := json.Unmarshal(raw, &normalized); err != nil { + return "[]" + } + + out, err := json.Marshal(normalized) + if err != nil { + return "[]" + } + + return string(out) +} + +func parseWorkflowInputs(raw types.String) ([]api.WorkflowInput, error) { + if raw.IsNull() || raw.IsUnknown() { + return []api.WorkflowInput{}, nil + } + str := raw.ValueString() + if str == "" || str == "[]" { + return []api.WorkflowInput{}, nil + } + var inputs []api.WorkflowInput + if err := json.Unmarshal([]byte(str), &inputs); err != nil { + return nil, fmt.Errorf("failed to parse inputs JSON: %w", err) + } + return inputs, nil +} + +func workflowJobAgentsFromModel(agents []WorkflowJobAgentModel) []api.CreateWorkflowJobAgent { + result := make([]api.CreateWorkflowJobAgent, len(agents)) + for i, a := range agents { + config := make(map[string]interface{}) + if !a.Config.IsNull() && !a.Config.IsUnknown() { + var decoded map[string]string + _ = a.Config.ElementsAs(context.Background(), &decoded, false) + for k, v := range decoded { + config[k] = v + } + } + result[i] = api.CreateWorkflowJobAgent{ + Name: a.Name.ValueString(), + Ref: a.Ref.ValueString(), + Config: config, + Selector: a.Selector.ValueString(), + } + } + return result +} + +func setWorkflowModelFromAPI(data *WorkflowResourceModel, w *api.Workflow) { + data.ID = types.StringValue(w.Id) + data.Name = types.StringValue(w.Name) + + data.Inputs = types.StringValue(normalizeInputsJSON(w.Inputs)) + + agents := make([]WorkflowJobAgentModel, len(w.JobAgents)) + for i, a := range w.JobAgents { + agents[i] = WorkflowJobAgentModel{ + Name: types.StringValue(a.Name), + Ref: types.StringValue(a.Ref), + Config: interfaceMapStringValue(a.Config), + Selector: types.StringValue(a.Selector), + } + } + data.JobAgents = agents +} diff --git a/internal/provider/workflow_resource_test.go b/internal/provider/workflow_resource_test.go new file mode 100644 index 0000000..2b183db --- /dev/null +++ b/internal/provider/workflow_resource_test.go @@ -0,0 +1,83 @@ +// Copyright IBM Corp. 2021, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccWorkflowResource(t *testing.T) { + name := fmt.Sprintf("tf-acc-wf-%d", time.Now().UnixNano()) + updatedName := name + "-updated" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccWorkflowConfig(name), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "ctrlplane_workflow.test", + tfjsonpath.New("id"), + knownvalue.NotNull(), + ), + statecheck.ExpectKnownValue( + "ctrlplane_workflow.test", + tfjsonpath.New("name"), + knownvalue.StringExact(name), + ), + }, + }, + { + Config: testAccWorkflowConfig(updatedName), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "ctrlplane_workflow.test", + tfjsonpath.New("name"), + knownvalue.StringExact(updatedName), + ), + }, + }, + { + ResourceName: "ctrlplane_workflow.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccWorkflowConfig(name string) string { + return fmt.Sprintf(` +%s + +resource "ctrlplane_job_agent" "test" { + name = %q + + test_runner { + delay_seconds = 5 + status = "successful" + } +} + +resource "ctrlplane_workflow" "test" { + name = %q + + job_agent { + name = "test-agent" + ref = ctrlplane_job_agent.test.id + config = { "delaySeconds" = "5", "status" = "successful" } + selector = "true" + } +} +`, testAccProviderConfig(), name+"-agent", name) +}