package client import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "time" ) var ErrNotFound = errors.New("repo not found") type Client struct { baseURL string httpClient *http.Client } func New(baseURL string) *Client { return &Client{ baseURL: baseURL, httpClient: &http.Client{Timeout: 30 * time.Second}, } } type CreateRepoInput struct { Name string `json:"name"` Type string `json:"type"` SourceURL string `json:"source_url"` SyncMode string `json:"sync_mode,omitempty"` AptSuite string `json:"apt_suite,omitempty"` AptComponents []string `json:"apt_components,omitempty"` AptArchitectures []string `json:"apt_architectures,omitempty"` } type Repo struct { ID int64 `json:"id"` Name string `json:"name"` Type string `json:"type"` SourceURL string `json:"source_url"` Frozen bool `json:"frozen"` SyncMode string `json:"sync_mode"` AptSuite string `json:"apt_suite,omitempty"` AptComponents []string `json:"apt_components,omitempty"` AptArchitectures []string `json:"apt_architectures,omitempty"` CreatedAt time.Time `json:"created_at"` } type listReposResponse struct { Items []Repo `json:"items"` Total int `json:"total"` } func (c *Client) CreateRepo(ctx context.Context, in CreateRepoInput) (*Repo, error) { body, _ := json.Marshal(in) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v1/repos", bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") var repo Repo if err := c.do(req, &repo); err != nil { return nil, err } return &repo, nil } func (c *Client) ListRepos(ctx context.Context) ([]Repo, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/v1/repos", nil) if err != nil { return nil, err } var resp listReposResponse if err := c.do(req, &resp); err != nil { return nil, err } return resp.Items, nil } func (c *Client) GetRepo(ctx context.Context, id int64) (*Repo, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/repos/%d", c.baseURL, id), nil) if err != nil { return nil, err } var repo Repo if err := c.do(req, &repo); err != nil { return nil, err } return &repo, nil } type UpdateRepoInput struct { SourceURL *string `json:"source_url,omitempty"` SyncMode *string `json:"sync_mode,omitempty"` AptSuite *string `json:"apt_suite,omitempty"` AptComponents []string `json:"apt_components,omitempty"` AptArchitectures []string `json:"apt_architectures,omitempty"` } func (c *Client) UpdateRepo(ctx context.Context, id int64, in UpdateRepoInput) (*Repo, error) { body, _ := json.Marshal(in) req, err := http.NewRequestWithContext(ctx, http.MethodPatch, fmt.Sprintf("%s/api/v1/repos/%d", c.baseURL, id), bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") var repo Repo if err := c.do(req, &repo); err != nil { return nil, err } return &repo, nil } func (c *Client) DeleteRepo(ctx context.Context, id int64) error { req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fmt.Sprintf("%s/api/v1/repos/%d", c.baseURL, id), nil) if err != nil { return err } return c.do(req, nil) } type StartCloneResponse struct { JobID int64 `json:"job_id"` Status string `json:"status"` } type CloneStatus struct { JobID int64 `json:"job_id"` RepoID int64 `json:"repo_id"` Status string `json:"status"` StartedAt *string `json:"started_at,omitempty"` FinishedAt *string `json:"finished_at,omitempty"` Error *string `json:"error,omitempty"` CreatedAt string `json:"created_at"` } func (c *Client) StartClone(ctx context.Context, repoID int64) (*StartCloneResponse, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/repos/%d/clone", c.baseURL, repoID), nil) if err != nil { return nil, err } var resp StartCloneResponse if err := c.do(req, &resp); err != nil { return nil, err } return &resp, nil } func (c *Client) GetCloneStatus(ctx context.Context, repoID int64) (*CloneStatus, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/repos/%d/clone/status", c.baseURL, repoID), nil) if err != nil { return nil, err } var status CloneStatus if err := c.do(req, &status); err != nil { return nil, err } return &status, nil } type PendingPackage struct { ID int64 `json:"id"` RepoID int64 `json:"repo_id"` Name string `json:"name"` Version string `json:"version"` Arch string `json:"arch"` Location string `json:"location"` Checksum string `json:"checksum"` ChecksumType string `json:"checksum_type"` Size int64 `json:"size"` CreatedAt time.Time `json:"created_at"` } type listPendingResponse struct { Items []PendingPackage `json:"items"` Total int `json:"total"` } type syncSelectionBody struct { IDs []int64 `json:"ids"` } func (c *Client) TriggerSync(ctx context.Context, repoID int64) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/repos/%d/sync/trigger", c.baseURL, repoID), nil) if err != nil { return err } return c.do(req, nil) } func (c *Client) ListPending(ctx context.Context, repoID int64) ([]PendingPackage, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/repos/%d/sync/pending", c.baseURL, repoID), nil) if err != nil { return nil, err } var resp listPendingResponse if err := c.do(req, &resp); err != nil { return nil, err } return resp.Items, nil } func (c *Client) ApprovePending(ctx context.Context, repoID int64, ids []int64) error { body, _ := json.Marshal(syncSelectionBody{IDs: ids}) req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/repos/%d/sync/approve", c.baseURL, repoID), bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") return c.do(req, nil) } func (c *Client) RejectPending(ctx context.Context, repoID int64, ids []int64) error { body, _ := json.Marshal(syncSelectionBody{IDs: ids}) req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/repos/%d/sync/reject", c.baseURL, repoID), bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") return c.do(req, nil) } type Snapshot struct { ID int64 `json:"id"` RepoID int64 `json:"repo_id"` Label string `json:"label"` CreatedAt time.Time `json:"created_at"` } type SnapshotPackage struct { ID int64 `json:"id"` Name string `json:"name"` Version string `json:"version"` Arch string `json:"arch"` Location string `json:"location"` Checksum string `json:"checksum"` ChecksumType string `json:"checksum_type"` Size int64 `json:"size"` } type SnapshotDetail struct { Snapshot Packages []SnapshotPackage `json:"packages"` } type listSnapshotsResponse struct { Items []Snapshot `json:"items"` Total int `json:"total"` } type SnapshotDiff struct { From Snapshot `json:"from"` To Snapshot `json:"to"` Added []SnapshotPackage `json:"added"` Removed []SnapshotPackage `json:"removed"` Unchanged int `json:"unchanged"` } func (c *Client) CreateSnapshot(ctx context.Context, repoID int64, label string) (*Snapshot, error) { body, _ := json.Marshal(map[string]string{"label": label}) req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/repos/%d/snapshots", c.baseURL, repoID), bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") var snap Snapshot if err := c.do(req, &snap); err != nil { return nil, err } return &snap, nil } func (c *Client) ListSnapshots(ctx context.Context, repoID int64) ([]Snapshot, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/repos/%d/snapshots", c.baseURL, repoID), nil) if err != nil { return nil, err } var resp listSnapshotsResponse if err := c.do(req, &resp); err != nil { return nil, err } return resp.Items, nil } func (c *Client) GetSnapshot(ctx context.Context, repoID, snapID int64) (*SnapshotDetail, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/repos/%d/snapshots/%d", c.baseURL, repoID, snapID), nil) if err != nil { return nil, err } var detail SnapshotDetail if err := c.do(req, &detail); err != nil { return nil, err } return &detail, nil } func (c *Client) DeleteSnapshot(ctx context.Context, repoID, snapID int64) error { req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fmt.Sprintf("%s/api/v1/repos/%d/snapshots/%d", c.baseURL, repoID, snapID), nil) if err != nil { return err } return c.do(req, nil) } func (c *Client) DiffSnapshots(ctx context.Context, repoID, fromID, toID int64) (*SnapshotDiff, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/repos/%d/snapshots/diff?from=%d&to=%d", c.baseURL, repoID, fromID, toID), nil) if err != nil { return nil, err } var diff SnapshotDiff if err := c.do(req, &diff); err != nil { return nil, err } return &diff, nil } func (c *Client) RollbackSnapshot(ctx context.Context, repoID, snapID int64) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/repos/%d/snapshots/%d/rollback", c.baseURL, repoID, snapID), nil) if err != nil { return err } return c.do(req, nil) } type BlockedPackage struct { ID int64 `json:"id"` RepoID int64 `json:"repo_id"` Name string `json:"name"` Location string `json:"location"` CreatedAt time.Time `json:"created_at"` } type listBlockedResponse struct { Items []BlockedPackage `json:"items"` Total int `json:"total"` } func (c *Client) BlockPackages(ctx context.Context, repoID int64, pendingIDs []int64) error { body, _ := json.Marshal(syncSelectionBody{IDs: pendingIDs}) req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/repos/%d/sync/block", c.baseURL, repoID), bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") return c.do(req, nil) } func (c *Client) UnblockPackages(ctx context.Context, repoID int64, blockedIDs []int64) error { body, _ := json.Marshal(syncSelectionBody{IDs: blockedIDs}) req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/api/v1/repos/%d/sync/unblock", c.baseURL, repoID), bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") return c.do(req, nil) } func (c *Client) ListBlocked(ctx context.Context, repoID int64) ([]BlockedPackage, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/repos/%d/sync/blocked", c.baseURL, repoID), nil) if err != nil { return nil, err } var resp listBlockedResponse if err := c.do(req, &resp); err != nil { return nil, err } return resp.Items, nil } type errorResponse struct { Error string `json:"error"` } func (c *Client) do(req *http.Request, out any) error { resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return ErrNotFound } if resp.StatusCode < 200 || resp.StatusCode >= 300 { var errResp errorResponse json.NewDecoder(resp.Body).Decode(&errResp) if errResp.Error != "" { return fmt.Errorf("%s", errResp.Error) } return fmt.Errorf("unexpected status %d", resp.StatusCode) } if out != nil { return json.NewDecoder(resp.Body).Decode(out) } return nil }