feat: sless_job terraform resource + jobs REST API

Operator (v0.1.4):
- internal/api/handler/jobs.go: CreateJob/GetJob/DeleteJob handlers
- internal/api/router.go: POST/GET/DELETE /v1/namespaces/{ns}/jobs/{name}

Terraform provider (v0.1.2):
- client.go: JobRequest/JobResponse + CreateJob/GetJob/DeleteJob/WaitJobDone
- resources/job_resource.go: sless_job resource (Create blocks until Succeeded/Failed)
- provider.go: register NewJobResource

Deploy:
- operator:v0.1.4 deployed and running in cluster
- provider v0.1.2 published to terra.k8c.ru
This commit is contained in:
“Naeel”
2026-03-07 17:43:26 +04:00
parent 22685a72a9
commit f405596d42
6 changed files with 467 additions and 2 deletions
@@ -290,3 +290,102 @@ func (c *Client) DeleteTrigger(ctx context.Context, ns, name string) error {
}
return nil
}
// --- Job CRUD ---
// JobRequest — тело POST /v1/namespaces/{ns}/jobs
type JobRequest struct {
Name string `json:"name"`
FunctionRef string `json:"function"`
EventJSON string `json:"event_json,omitempty"`
}
// JobResponse — ответ GET /v1/namespaces/{ns}/jobs/{name}
type JobResponse struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
FunctionRef string `json:"function"`
EventJSON string `json:"event_json"`
Phase string `json:"phase"`
JobName string `json:"job_name"`
StartTime string `json:"start_time"`
CompletionTime string `json:"completion_time"`
Message string `json:"message"`
}
// CreateJob — POST /v1/namespaces/{ns}/jobs → 201
func (c *Client) CreateJob(ctx context.Context, ns string, req JobRequest) (*JobResponse, error) {
url := fmt.Sprintf("%s/v1/namespaces/%s/jobs", c.endpoint, ns)
resp, err := c.doJSON(ctx, http.MethodPost, url, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("create job: status %d: %s", resp.StatusCode, body)
}
var j JobResponse
return &j, json.NewDecoder(resp.Body).Decode(&j)
}
// GetJob — GET /v1/namespaces/{ns}/jobs/{name} → nil если 404
func (c *Client) GetJob(ctx context.Context, ns, name string) (*JobResponse, error) {
url := fmt.Sprintf("%s/v1/namespaces/%s/jobs/%s", c.endpoint, ns, name)
resp, err := c.doJSON(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("get job: status %d: %s", resp.StatusCode, body)
}
var j JobResponse
return &j, json.NewDecoder(resp.Body).Decode(&j)
}
// DeleteJob — DELETE /v1/namespaces/{ns}/jobs/{name} → 204
func (c *Client) DeleteJob(ctx context.Context, ns, name string) error {
url := fmt.Sprintf("%s/v1/namespaces/%s/jobs/%s", c.endpoint, ns, name)
resp, err := c.doJSON(ctx, http.MethodDelete, url, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("delete job: status %d: %s", resp.StatusCode, body)
}
return nil
}
// WaitJobDone опрашивает job каждые 5 секунд пока phase не Succeeded или Failed.
// Блокирует terraform apply до завершения джоба.
func (c *Client) WaitJobDone(ctx context.Context, ns, name string, timeout time.Duration) (*JobResponse, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
j, err := c.GetJob(ctx, ns, name)
if err != nil {
return nil, err
}
if j == nil {
return nil, fmt.Errorf("job %s/%s not found while waiting", ns, name)
}
switch j.Phase {
case "Succeeded":
return j, nil
case "Failed":
return nil, fmt.Errorf("job failed: %s", j.Message)
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(5 * time.Second):
}
}
return nil, fmt.Errorf("timeout waiting for job %s/%s to complete", ns, name)
}