81a94322f7
Implement AWXClient with Launch, GetJob, and Cancel methods for orchestrating Ansible job templates via AWX REST API. Uses Bearer token auth with 30s timeout. Includes unit tests with mock transport. Relates-to: #97
118 lines
3.0 KiB
Go
118 lines
3.0 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type AWXClient struct {
|
|
baseURL string
|
|
token string
|
|
client *http.Client
|
|
}
|
|
|
|
type AWXLaunchRequest struct {
|
|
InventoryID uint64
|
|
Limit string
|
|
ExtraVars map[string]any
|
|
}
|
|
|
|
type AWXJob struct {
|
|
ID uint64 `json:"id"`
|
|
Status string `json:"status"`
|
|
Failed bool `json:"failed"`
|
|
}
|
|
|
|
func NewAWXClient(baseURL, token string) *AWXClient {
|
|
return &AWXClient{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
token: strings.TrimSpace(token),
|
|
client: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (c *AWXClient) Configured() bool {
|
|
return c.baseURL != "" && c.token != ""
|
|
}
|
|
|
|
func (c *AWXClient) Launch(ctx context.Context, templateID uint64, input AWXLaunchRequest) (*AWXJob, error) {
|
|
body := map[string]any{"inventory": input.InventoryID, "extra_vars": input.ExtraVars}
|
|
if input.Limit != "" {
|
|
body["limit"] = input.Limit
|
|
}
|
|
var job AWXJob
|
|
if err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/job_templates/%d/launch/", templateID), body, &job); err != nil {
|
|
return nil, err
|
|
}
|
|
if job.ID == 0 {
|
|
return nil, fmt.Errorf("AWX launch response did not include a job id")
|
|
}
|
|
return &job, nil
|
|
}
|
|
|
|
func (c *AWXClient) GetJob(ctx context.Context, jobID string) (*AWXJob, error) {
|
|
if _, err := strconv.ParseUint(jobID, 10, 64); err != nil {
|
|
return nil, fmt.Errorf("invalid AWX job id %q", jobID)
|
|
}
|
|
var job AWXJob
|
|
if err := c.request(ctx, http.MethodGet, "/api/v2/jobs/"+jobID+"/", nil, &job); err != nil {
|
|
return nil, err
|
|
}
|
|
return &job, nil
|
|
}
|
|
|
|
func (c *AWXClient) Cancel(ctx context.Context, jobID string) error {
|
|
if _, err := strconv.ParseUint(jobID, 10, 64); err != nil {
|
|
return fmt.Errorf("invalid AWX job id %q", jobID)
|
|
}
|
|
return c.request(ctx, http.MethodPost, "/api/v2/jobs/"+jobID+"/cancel/", map[string]any{}, nil)
|
|
}
|
|
|
|
func (c *AWXClient) request(ctx context.Context, method, path string, payload any, output any) error {
|
|
if !c.Configured() {
|
|
return fmt.Errorf("AWX_BASE_URL and AWX_TOKEN must be configured")
|
|
}
|
|
var body io.Reader
|
|
if payload != nil {
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = bytes.NewReader(raw)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
|
req.Header.Set("Accept", "application/json")
|
|
if payload != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("AWX request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("AWX returned %s: %s", resp.Status, strings.TrimSpace(string(raw)))
|
|
}
|
|
if output != nil && len(raw) > 0 {
|
|
if err := json.Unmarshal(raw, output); err != nil {
|
|
return fmt.Errorf("decode AWX response: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|