f1bcb8cd3a
repospawner turns JSON new-repo requests into terraform-git pull requests via kubernetes Jobs, follows those PRs to merge and optionally activates the repository in Woodpecker.
103 lines
2.5 KiB
Go
103 lines
2.5 KiB
Go
// Package woodpecker activates a repository in Woodpecker CI by its forge
|
|
// remote id.
|
|
package woodpecker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Client talks to one Woodpecker server.
|
|
type Client struct {
|
|
BaseURL string
|
|
Token string
|
|
HTTP *http.Client
|
|
}
|
|
|
|
// New builds a Client with a bounded HTTP client.
|
|
func New(baseURL, token string) *Client {
|
|
return &Client{
|
|
BaseURL: strings.TrimSuffix(baseURL, "/"),
|
|
Token: token,
|
|
HTTP: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
// Repo is the subset of a Woodpecker repository record that matters here.
|
|
type Repo struct {
|
|
ID int64 `json:"id"`
|
|
FullName string `json:"full_name"`
|
|
Active bool `json:"active"`
|
|
}
|
|
|
|
// Enable activates the repository identified by its forge remote id. An
|
|
// already-active repository answers 409, which counts as success.
|
|
func (c *Client) Enable(ctx context.Context, forgeRemoteID int64) (Repo, error) {
|
|
var out Repo
|
|
path := "/api/repos?forge_remote_id=" + strconv.FormatInt(forgeRemoteID, 10)
|
|
err := c.do(ctx, http.MethodPost, path, &out)
|
|
var se *StatusError
|
|
if errors.As(err, &se) && se.Status == http.StatusConflict {
|
|
return out, nil
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
// Lookup fetches a repository by "owner/name", used to confirm activation.
|
|
func (c *Client) Lookup(ctx context.Context, fullName string) (Repo, error) {
|
|
var out Repo
|
|
err := c.do(ctx, http.MethodGet, "/api/repos/lookup/"+fullName, &out)
|
|
return out, err
|
|
}
|
|
|
|
// StatusError carries a non-2xx response.
|
|
type StatusError struct {
|
|
Status int
|
|
Path string
|
|
Body string
|
|
}
|
|
|
|
func (e *StatusError) Error() string {
|
|
msg := fmt.Sprintf("woodpecker %s: status %d", e.Path, e.Status)
|
|
if e.Body != "" {
|
|
msg += ": " + e.Body
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func (c *Client) do(ctx context.Context, method, path string, out any) error {
|
|
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
if c.Token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.Token)
|
|
}
|
|
client := c.HTTP
|
|
if client == nil {
|
|
client = http.DefaultClient
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
|
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
|
return &StatusError{Status: resp.StatusCode, Path: path, Body: strings.TrimSpace(string(snippet))}
|
|
}
|
|
if out == nil {
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
return nil
|
|
}
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|