Skip to content

Commit 677c316

Browse files
authored
feat(jobs): add Jobs StartJobDefinition action (#3504)
* feat: add Jobs StartJobDefinition action * fix: use direct API check instead of audit trail for job definition start test * fix: correct job run verification function to use terraform state * fix: stop and wait for job runs before deleting job definition in test cleanup * fix: cleanup job runs before Terraform destroy using defer * feat: add support for jobs StartJobDefinition action with automatic job run cleanup * fix: correct linting issues (gofumpt, nlreturn, wsl_v5) * chore: run go mod tidy
1 parent 3585c2b commit 677c316

File tree

6 files changed

+1263
-0
lines changed

6 files changed

+1263
-0
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package jobs
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
8+
"github.com/hashicorp/terraform-plugin-framework/action"
9+
"github.com/hashicorp/terraform-plugin-framework/action/schema"
10+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
11+
"github.com/hashicorp/terraform-plugin-framework/types"
12+
jobs "github.com/scaleway/scaleway-sdk-go/api/jobs/v1alpha1"
13+
"github.com/scaleway/scaleway-sdk-go/scw"
14+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"
15+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/regional"
16+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/meta"
17+
)
18+
19+
var (
20+
_ action.Action = (*StartJobDefinitionAction)(nil)
21+
_ action.ActionWithConfigure = (*StartJobDefinitionAction)(nil)
22+
)
23+
24+
type StartJobDefinitionAction struct {
25+
jobsAPI *jobs.API
26+
meta *meta.Meta
27+
}
28+
29+
func (a *StartJobDefinitionAction) Configure(ctx context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) {
30+
if req.ProviderData == nil {
31+
return
32+
}
33+
34+
m, ok := req.ProviderData.(*meta.Meta)
35+
if !ok {
36+
resp.Diagnostics.AddError(
37+
"Unexpected Action Configure Type",
38+
fmt.Sprintf("Expected *meta.Meta, got: %T. Please report this issue to the provider developers.", req.ProviderData),
39+
)
40+
41+
return
42+
}
43+
44+
client := m.ScwClient()
45+
a.jobsAPI = jobs.NewAPI(client)
46+
a.meta = m
47+
}
48+
49+
func (a *StartJobDefinitionAction) Metadata(ctx context.Context, req action.MetadataRequest, resp *action.MetadataResponse) {
50+
resp.TypeName = req.ProviderTypeName + "_job_definition_start_action"
51+
}
52+
53+
type StartJobDefinitionActionModel struct {
54+
JobDefinitionID types.String `tfsdk:"job_definition_id"`
55+
Region types.String `tfsdk:"region"`
56+
Command types.String `tfsdk:"command"`
57+
EnvironmentVariables types.Map `tfsdk:"environment_variables"`
58+
Replicas types.Int64 `tfsdk:"replicas"`
59+
}
60+
61+
func NewStartJobDefinitionAction() action.Action {
62+
return &StartJobDefinitionAction{}
63+
}
64+
65+
func (a *StartJobDefinitionAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
66+
resp.Schema = schema.Schema{
67+
Attributes: map[string]schema.Attribute{
68+
"job_definition_id": schema.StringAttribute{
69+
Required: true,
70+
Description: "ID of the job definition to start. Can be a plain UUID or a regional ID.",
71+
Validators: []validator.String{
72+
stringvalidator.LengthAtLeast(1),
73+
},
74+
},
75+
"region": schema.StringAttribute{
76+
Optional: true,
77+
Description: "Region of the job definition. If not set, the region is derived from the job_definition_id when possible or from the provider configuration.",
78+
},
79+
"command": schema.StringAttribute{
80+
Optional: true,
81+
Description: "Contextual startup command for this specific job run.",
82+
},
83+
"environment_variables": schema.MapAttribute{
84+
Optional: true,
85+
ElementType: types.StringType,
86+
Description: "Contextual environment variables for this specific job run.",
87+
},
88+
"replicas": schema.Int64Attribute{
89+
Optional: true,
90+
Description: "Number of jobs to run.",
91+
},
92+
},
93+
}
94+
}
95+
96+
func (a *StartJobDefinitionAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
97+
var data StartJobDefinitionActionModel
98+
99+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
100+
101+
if resp.Diagnostics.HasError() {
102+
return
103+
}
104+
105+
if a.jobsAPI == nil {
106+
resp.Diagnostics.AddError(
107+
"Unconfigured jobsAPI",
108+
"The action was not properly configured. The Scaleway client is missing. "+
109+
"This is usually a bug in the provider. Please report it to the maintainers.",
110+
)
111+
112+
return
113+
}
114+
115+
if data.JobDefinitionID.IsNull() || data.JobDefinitionID.ValueString() == "" {
116+
resp.Diagnostics.AddError(
117+
"Missing job_definition_id",
118+
"The job_definition_id attribute is required to start a job definition.",
119+
)
120+
121+
return
122+
}
123+
124+
jobDefinitionID := locality.ExpandID(data.JobDefinitionID.ValueString())
125+
126+
var (
127+
region scw.Region
128+
err error
129+
)
130+
131+
if !data.Region.IsNull() && data.Region.ValueString() != "" {
132+
region = scw.Region(data.Region.ValueString())
133+
} else {
134+
// Try to derive region from the job_definition_id if it is a regional ID.
135+
if derivedRegion, id, parseErr := regional.ParseID(data.JobDefinitionID.ValueString()); parseErr == nil {
136+
region = derivedRegion
137+
jobDefinitionID = id
138+
} else {
139+
// Use default region from provider configuration
140+
defaultRegion, exists := a.meta.ScwClient().GetDefaultRegion()
141+
if !exists {
142+
resp.Diagnostics.AddError(
143+
"Missing region",
144+
"The region attribute is required to start a job definition. Please provide it explicitly or configure a default region in the provider.",
145+
)
146+
147+
return
148+
}
149+
150+
region = defaultRegion
151+
}
152+
}
153+
154+
startReq := &jobs.StartJobDefinitionRequest{
155+
Region: region,
156+
JobDefinitionID: jobDefinitionID,
157+
}
158+
159+
if !data.Command.IsNull() && data.Command.ValueString() != "" {
160+
command := data.Command.ValueString()
161+
startReq.Command = &command
162+
}
163+
164+
if !data.EnvironmentVariables.IsNull() {
165+
envVars := make(map[string]string)
166+
resp.Diagnostics.Append(data.EnvironmentVariables.ElementsAs(ctx, &envVars, false)...)
167+
168+
if resp.Diagnostics.HasError() {
169+
return
170+
}
171+
172+
if len(envVars) > 0 {
173+
startReq.EnvironmentVariables = &envVars
174+
}
175+
}
176+
177+
if !data.Replicas.IsNull() {
178+
replicas := uint32(data.Replicas.ValueInt64())
179+
startReq.Replicas = &replicas
180+
}
181+
182+
_, err = a.jobsAPI.StartJobDefinition(startReq, scw.WithContext(ctx))
183+
if err != nil {
184+
resp.Diagnostics.AddError(
185+
"Error executing Jobs StartJobDefinition action",
186+
fmt.Sprintf("Failed to start job definition %s: %s", jobDefinitionID, err),
187+
)
188+
189+
return
190+
}
191+
}

0 commit comments

Comments
 (0)