All import paths updated to git.wntrmute.dev/mc/. Bumps mcdsl to v1.2.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
753 lines
18 KiB
Go
753 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
mcrv1 "git.wntrmute.dev/mc/mcr/gen/mcr/v1"
|
|
)
|
|
|
|
var version = "dev"
|
|
|
|
// Global flags, resolved in PersistentPreRunE.
|
|
var (
|
|
flagServer string
|
|
flagGRPC string
|
|
flagToken string
|
|
flagCACert string
|
|
flagJSON bool
|
|
|
|
client *apiClient
|
|
)
|
|
|
|
func main() {
|
|
root := &cobra.Command{
|
|
Use: "mcrctl",
|
|
Short: "Metacircular Container Registry admin CLI",
|
|
Version: version,
|
|
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
|
// Resolve token: flag overrides env.
|
|
token := flagToken
|
|
if token == "" {
|
|
token = os.Getenv("MCR_TOKEN")
|
|
}
|
|
|
|
var err error
|
|
client, err = newClient(flagServer, flagGRPC, token, flagCACert)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
PersistentPostRun: func(_ *cobra.Command, _ []string) {
|
|
if client != nil {
|
|
client.close()
|
|
}
|
|
},
|
|
}
|
|
|
|
root.PersistentFlags().StringVar(&flagServer, "server", "", "REST API base URL (e.g. https://registry.example.com)")
|
|
root.PersistentFlags().StringVar(&flagGRPC, "grpc", "", "gRPC server address (e.g. registry.example.com:9443)")
|
|
root.PersistentFlags().StringVar(&flagToken, "token", "", "bearer token (fallback: MCR_TOKEN env)")
|
|
root.PersistentFlags().StringVar(&flagCACert, "ca-cert", "", "custom CA certificate PEM file")
|
|
root.PersistentFlags().BoolVar(&flagJSON, "json", false, "output as JSON instead of table")
|
|
|
|
root.AddCommand(statusCmd())
|
|
root.AddCommand(repoCmd())
|
|
root.AddCommand(gcCmd())
|
|
root.AddCommand(purgeCmd())
|
|
root.AddCommand(policyCmd())
|
|
root.AddCommand(auditCmd())
|
|
root.AddCommand(snapshotCmd())
|
|
|
|
if err := root.Execute(); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// ---------- status ----------
|
|
|
|
func statusCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "status",
|
|
Short: "Query server health",
|
|
RunE: runStatus,
|
|
}
|
|
}
|
|
|
|
func runStatus(_ *cobra.Command, _ []string) error {
|
|
if client.useGRPC() {
|
|
resp, err := client.admin.Health(context.Background(), &mcrv1.HealthRequest{})
|
|
if err != nil {
|
|
return fmt.Errorf("health check failed: %w", err)
|
|
}
|
|
if flagJSON {
|
|
return printJSON(resp)
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Status: %s\n", resp.GetStatus())
|
|
return nil
|
|
}
|
|
|
|
data, err := client.restDo("GET", "/v1/health", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("health check failed: %w", err)
|
|
}
|
|
|
|
if flagJSON {
|
|
// Pass through raw JSON with re-indent.
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
return printJSON(v)
|
|
}
|
|
|
|
var resp struct {
|
|
Status string `json:"status"`
|
|
}
|
|
if err := json.Unmarshal(data, &resp); err != nil {
|
|
return err
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Status: %s\n", resp.Status)
|
|
return nil
|
|
}
|
|
|
|
// ---------- repo ----------
|
|
|
|
func repoCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "repo",
|
|
Short: "Repository management",
|
|
}
|
|
|
|
cmd.AddCommand(repoListCmd())
|
|
cmd.AddCommand(repoDeleteCmd())
|
|
return cmd
|
|
}
|
|
|
|
func repoListCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "list",
|
|
Short: "List repositories",
|
|
RunE: runRepoList,
|
|
}
|
|
}
|
|
|
|
func runRepoList(_ *cobra.Command, _ []string) error {
|
|
type repoRow struct {
|
|
Name string `json:"name"`
|
|
TagCount int `json:"tag_count"`
|
|
ManifestCount int `json:"manifest_count"`
|
|
TotalSize int64 `json:"total_size"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
if client.useGRPC() {
|
|
resp, err := client.registry.ListRepositories(context.Background(), &mcrv1.ListRepositoriesRequest{})
|
|
if err != nil {
|
|
return fmt.Errorf("list repositories: %w", err)
|
|
}
|
|
repos := resp.GetRepositories()
|
|
if flagJSON {
|
|
return printJSON(repos)
|
|
}
|
|
rows := make([][]string, 0, len(repos))
|
|
for _, r := range repos {
|
|
rows = append(rows, []string{
|
|
r.GetName(),
|
|
strconv.Itoa(int(r.GetTagCount())),
|
|
strconv.Itoa(int(r.GetManifestCount())),
|
|
formatSize(r.GetTotalSize()),
|
|
r.GetCreatedAt(),
|
|
})
|
|
}
|
|
printTable(os.Stdout, []string{"NAME", "TAGS", "MANIFESTS", "SIZE", "CREATED"}, rows)
|
|
return nil
|
|
}
|
|
|
|
data, err := client.restDo("GET", "/v1/repositories", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("list repositories: %w", err)
|
|
}
|
|
|
|
if flagJSON {
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
return printJSON(v)
|
|
}
|
|
|
|
var repos []repoRow
|
|
if err := json.Unmarshal(data, &repos); err != nil {
|
|
return err
|
|
}
|
|
|
|
rows := make([][]string, 0, len(repos))
|
|
for _, r := range repos {
|
|
rows = append(rows, []string{
|
|
r.Name,
|
|
strconv.Itoa(r.TagCount),
|
|
strconv.Itoa(r.ManifestCount),
|
|
formatSize(r.TotalSize),
|
|
r.CreatedAt,
|
|
})
|
|
}
|
|
printTable(os.Stdout, []string{"NAME", "TAGS", "MANIFESTS", "SIZE", "CREATED"}, rows)
|
|
return nil
|
|
}
|
|
|
|
func repoDeleteCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "delete [name]",
|
|
Short: "Delete a repository",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runRepoDelete,
|
|
}
|
|
}
|
|
|
|
func runRepoDelete(_ *cobra.Command, args []string) error {
|
|
name := args[0]
|
|
|
|
if !confirmPrompt(fmt.Sprintf("Are you sure you want to delete repository %q?", name)) {
|
|
_, _ = fmt.Fprintln(os.Stdout, "Cancelled.")
|
|
return nil
|
|
}
|
|
|
|
if client.useGRPC() {
|
|
_, err := client.registry.DeleteRepository(context.Background(), &mcrv1.DeleteRepositoryRequest{Name: name})
|
|
if err != nil {
|
|
return fmt.Errorf("delete repository: %w", err)
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Repository %q deleted.\n", name)
|
|
return nil
|
|
}
|
|
|
|
_, err := client.restDo("DELETE", "/v1/repositories/"+name, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("delete repository: %w", err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stdout, "Repository %q deleted.\n", name)
|
|
return nil
|
|
}
|
|
|
|
// ---------- policy ----------
|
|
|
|
func policyCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "policy",
|
|
Short: "Policy rule management",
|
|
}
|
|
|
|
cmd.AddCommand(policyListCmd())
|
|
cmd.AddCommand(policyCreateCmd())
|
|
cmd.AddCommand(policyUpdateCmd())
|
|
cmd.AddCommand(policyDeleteCmd())
|
|
return cmd
|
|
}
|
|
|
|
func policyListCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "list",
|
|
Short: "List policy rules",
|
|
RunE: runPolicyList,
|
|
}
|
|
}
|
|
|
|
func runPolicyList(_ *cobra.Command, _ []string) error {
|
|
type policyRow struct {
|
|
ID int64 `json:"id"`
|
|
Priority int `json:"priority"`
|
|
Description string `json:"description"`
|
|
Effect string `json:"effect"`
|
|
Enabled bool `json:"enabled"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
if client.useGRPC() {
|
|
resp, err := client.policy.ListPolicyRules(context.Background(), &mcrv1.ListPolicyRulesRequest{})
|
|
if err != nil {
|
|
return fmt.Errorf("list policy rules: %w", err)
|
|
}
|
|
rules := resp.GetRules()
|
|
if flagJSON {
|
|
return printJSON(rules)
|
|
}
|
|
rows := make([][]string, 0, len(rules))
|
|
for _, r := range rules {
|
|
rows = append(rows, []string{
|
|
strconv.FormatInt(r.GetId(), 10),
|
|
strconv.Itoa(int(r.GetPriority())),
|
|
r.GetDescription(),
|
|
r.GetEffect(),
|
|
strconv.FormatBool(r.GetEnabled()),
|
|
r.GetCreatedAt(),
|
|
})
|
|
}
|
|
printTable(os.Stdout, []string{"ID", "PRIORITY", "DESCRIPTION", "EFFECT", "ENABLED", "CREATED"}, rows)
|
|
return nil
|
|
}
|
|
|
|
data, err := client.restDo("GET", "/v1/policy/rules", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("list policy rules: %w", err)
|
|
}
|
|
|
|
if flagJSON {
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
return printJSON(v)
|
|
}
|
|
|
|
var rules []policyRow
|
|
if err := json.Unmarshal(data, &rules); err != nil {
|
|
return err
|
|
}
|
|
|
|
rows := make([][]string, 0, len(rules))
|
|
for _, r := range rules {
|
|
rows = append(rows, []string{
|
|
strconv.FormatInt(r.ID, 10),
|
|
strconv.Itoa(r.Priority),
|
|
r.Description,
|
|
r.Effect,
|
|
strconv.FormatBool(r.Enabled),
|
|
r.CreatedAt,
|
|
})
|
|
}
|
|
printTable(os.Stdout, []string{"ID", "PRIORITY", "DESCRIPTION", "EFFECT", "ENABLED", "CREATED"}, rows)
|
|
return nil
|
|
}
|
|
|
|
func policyCreateCmd() *cobra.Command {
|
|
var ruleBody string
|
|
cmd := &cobra.Command{
|
|
Use: "create",
|
|
Short: "Create a policy rule",
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
return runPolicyCreate(cmd, ruleBody)
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&ruleBody, "rule", "", "policy rule as JSON string")
|
|
_ = cmd.MarkFlagRequired("rule")
|
|
return cmd
|
|
}
|
|
|
|
func runPolicyCreate(_ *cobra.Command, ruleBody string) error {
|
|
if client.useGRPC() {
|
|
var req mcrv1.CreatePolicyRuleRequest
|
|
if err := json.Unmarshal([]byte(ruleBody), &req); err != nil {
|
|
return fmt.Errorf("invalid rule JSON: %w", err)
|
|
}
|
|
rule, err := client.policy.CreatePolicyRule(context.Background(), &req)
|
|
if err != nil {
|
|
return fmt.Errorf("create policy rule: %w", err)
|
|
}
|
|
if flagJSON {
|
|
return printJSON(rule)
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Policy rule created (ID: %d)\n", rule.GetId())
|
|
return nil
|
|
}
|
|
|
|
var body any
|
|
if err := json.Unmarshal([]byte(ruleBody), &body); err != nil {
|
|
return fmt.Errorf("invalid rule JSON: %w", err)
|
|
}
|
|
|
|
data, err := client.restDo("POST", "/v1/policy/rules", body)
|
|
if err != nil {
|
|
return fmt.Errorf("create policy rule: %w", err)
|
|
}
|
|
|
|
if flagJSON {
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
return printJSON(v)
|
|
}
|
|
|
|
var created struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(data, &created); err != nil {
|
|
return err
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Policy rule created (ID: %d)\n", created.ID)
|
|
return nil
|
|
}
|
|
|
|
func policyUpdateCmd() *cobra.Command {
|
|
var ruleBody string
|
|
cmd := &cobra.Command{
|
|
Use: "update [id]",
|
|
Short: "Update a policy rule",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runPolicyUpdate(cmd, args, ruleBody)
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&ruleBody, "rule", "", "partial update as JSON string")
|
|
_ = cmd.MarkFlagRequired("rule")
|
|
return cmd
|
|
}
|
|
|
|
func runPolicyUpdate(_ *cobra.Command, args []string, ruleBody string) error {
|
|
id, err := strconv.ParseInt(args[0], 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid rule ID: %w", err)
|
|
}
|
|
|
|
if client.useGRPC() {
|
|
var req mcrv1.UpdatePolicyRuleRequest
|
|
if err := json.Unmarshal([]byte(ruleBody), &req); err != nil {
|
|
return fmt.Errorf("invalid rule JSON: %w", err)
|
|
}
|
|
req.Id = id
|
|
rule, err := client.policy.UpdatePolicyRule(context.Background(), &req)
|
|
if err != nil {
|
|
return fmt.Errorf("update policy rule: %w", err)
|
|
}
|
|
if flagJSON {
|
|
return printJSON(rule)
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Policy rule %d updated.\n", rule.GetId())
|
|
return nil
|
|
}
|
|
|
|
var body any
|
|
if err := json.Unmarshal([]byte(ruleBody), &body); err != nil {
|
|
return fmt.Errorf("invalid rule JSON: %w", err)
|
|
}
|
|
|
|
data, err := client.restDo("PATCH", "/v1/policy/rules/"+strconv.FormatInt(id, 10), body)
|
|
if err != nil {
|
|
return fmt.Errorf("update policy rule: %w", err)
|
|
}
|
|
|
|
if flagJSON {
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
return printJSON(v)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stdout, "Policy rule %d updated.\n", id)
|
|
return nil
|
|
}
|
|
|
|
func policyDeleteCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "delete [id]",
|
|
Short: "Delete a policy rule",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runPolicyDelete,
|
|
}
|
|
}
|
|
|
|
func runPolicyDelete(_ *cobra.Command, args []string) error {
|
|
id, err := strconv.ParseInt(args[0], 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid rule ID: %w", err)
|
|
}
|
|
|
|
if !confirmPrompt(fmt.Sprintf("Are you sure you want to delete policy rule %d?", id)) {
|
|
_, _ = fmt.Fprintln(os.Stdout, "Cancelled.")
|
|
return nil
|
|
}
|
|
|
|
if client.useGRPC() {
|
|
_, err := client.policy.DeletePolicyRule(context.Background(), &mcrv1.DeletePolicyRuleRequest{Id: id})
|
|
if err != nil {
|
|
return fmt.Errorf("delete policy rule: %w", err)
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Policy rule %d deleted.\n", id)
|
|
return nil
|
|
}
|
|
|
|
_, err = client.restDo("DELETE", "/v1/policy/rules/"+strconv.FormatInt(id, 10), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("delete policy rule: %w", err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stdout, "Policy rule %d deleted.\n", id)
|
|
return nil
|
|
}
|
|
|
|
// ---------- audit ----------
|
|
|
|
func auditCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "audit",
|
|
Short: "Audit log management",
|
|
}
|
|
|
|
cmd.AddCommand(auditTailCmd())
|
|
return cmd
|
|
}
|
|
|
|
func auditTailCmd() *cobra.Command {
|
|
var n int
|
|
var eventType string
|
|
cmd := &cobra.Command{
|
|
Use: "tail",
|
|
Short: "Print recent audit events",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
return runAuditTail(n, eventType)
|
|
},
|
|
}
|
|
cmd.Flags().IntVarP(&n, "n", "n", 50, "number of events to retrieve")
|
|
cmd.Flags().StringVar(&eventType, "event-type", "", "filter by event type")
|
|
return cmd
|
|
}
|
|
|
|
func runAuditTail(n int, eventType string) error {
|
|
type auditRow struct {
|
|
ID int64 `json:"id"`
|
|
EventTime string `json:"event_time"`
|
|
EventType string `json:"event_type"`
|
|
ActorID string `json:"actor_id"`
|
|
Repository string `json:"repository"`
|
|
Digest string `json:"digest"`
|
|
IPAddress string `json:"ip_address"`
|
|
Details map[string]string `json:"details"`
|
|
}
|
|
|
|
if client.useGRPC() {
|
|
req := &mcrv1.ListAuditEventsRequest{
|
|
Pagination: &mcrv1.PaginationRequest{Limit: int32(n)}, //nolint:gosec // n is user-provided flag with small values
|
|
}
|
|
if eventType != "" {
|
|
req.EventType = eventType
|
|
}
|
|
resp, err := client.audit.ListAuditEvents(context.Background(), req)
|
|
if err != nil {
|
|
return fmt.Errorf("list audit events: %w", err)
|
|
}
|
|
events := resp.GetEvents()
|
|
if flagJSON {
|
|
return printJSON(events)
|
|
}
|
|
rows := make([][]string, 0, len(events))
|
|
for _, e := range events {
|
|
rows = append(rows, []string{
|
|
strconv.FormatInt(e.GetId(), 10),
|
|
e.GetEventTime(),
|
|
e.GetEventType(),
|
|
e.GetActorId(),
|
|
e.GetRepository(),
|
|
})
|
|
}
|
|
printTable(os.Stdout, []string{"ID", "TIME", "TYPE", "ACTOR", "REPOSITORY"}, rows)
|
|
return nil
|
|
}
|
|
|
|
path := fmt.Sprintf("/v1/audit?n=%d", n)
|
|
if eventType != "" {
|
|
path += "&event_type=" + eventType
|
|
}
|
|
data, err := client.restDo("GET", path, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("list audit events: %w", err)
|
|
}
|
|
|
|
if flagJSON {
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
return printJSON(v)
|
|
}
|
|
|
|
var events []auditRow
|
|
if err := json.Unmarshal(data, &events); err != nil {
|
|
return err
|
|
}
|
|
|
|
rows := make([][]string, 0, len(events))
|
|
for _, e := range events {
|
|
rows = append(rows, []string{
|
|
strconv.FormatInt(e.ID, 10),
|
|
e.EventTime,
|
|
e.EventType,
|
|
e.ActorID,
|
|
e.Repository,
|
|
})
|
|
}
|
|
printTable(os.Stdout, []string{"ID", "TIME", "TYPE", "ACTOR", "REPOSITORY"}, rows)
|
|
return nil
|
|
}
|
|
|
|
// ---------- gc ----------
|
|
|
|
func gcCmd() *cobra.Command {
|
|
var reconcile bool
|
|
cmd := &cobra.Command{
|
|
Use: "gc",
|
|
Short: "Trigger garbage collection",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
return runGC(reconcile)
|
|
},
|
|
}
|
|
cmd.Flags().BoolVar(&reconcile, "reconcile", false, "run reconciliation instead of normal GC")
|
|
|
|
cmd.AddCommand(gcStatusCmd())
|
|
return cmd
|
|
}
|
|
|
|
func runGC(reconcile bool) error {
|
|
if client.useGRPC() {
|
|
resp, err := client.registry.GarbageCollect(context.Background(), &mcrv1.GarbageCollectRequest{})
|
|
if err != nil {
|
|
return fmt.Errorf("trigger gc: %w", err)
|
|
}
|
|
if flagJSON {
|
|
return printJSON(resp)
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Garbage collection started (ID: %s)\n", resp.GetId())
|
|
return nil
|
|
}
|
|
|
|
path := "/v1/gc"
|
|
if reconcile {
|
|
path += "?reconcile=true"
|
|
}
|
|
data, err := client.restDo("POST", path, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("trigger gc: %w", err)
|
|
}
|
|
|
|
if flagJSON {
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
return printJSON(v)
|
|
}
|
|
|
|
var resp struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(data, &resp); err != nil {
|
|
return err
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Garbage collection started (ID: %s)\n", resp.ID)
|
|
return nil
|
|
}
|
|
|
|
func gcStatusCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "status",
|
|
Short: "Check GC status",
|
|
RunE: runGCStatus,
|
|
}
|
|
}
|
|
|
|
func runGCStatus(_ *cobra.Command, _ []string) error {
|
|
if client.useGRPC() {
|
|
resp, err := client.registry.GetGCStatus(context.Background(), &mcrv1.GetGCStatusRequest{})
|
|
if err != nil {
|
|
return fmt.Errorf("gc status: %w", err)
|
|
}
|
|
if flagJSON {
|
|
return printJSON(resp)
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stdout, "Running: %v\n", resp.GetRunning())
|
|
if lr := resp.GetLastRun(); lr != nil {
|
|
_, _ = fmt.Fprintf(os.Stdout, "Last run: %s - %s\n", lr.GetStartedAt(), lr.GetCompletedAt())
|
|
_, _ = fmt.Fprintf(os.Stdout, " Blobs removed: %d\n", lr.GetBlobsRemoved())
|
|
_, _ = fmt.Fprintf(os.Stdout, " Bytes freed: %s\n", formatSize(lr.GetBytesFreed()))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
data, err := client.restDo("GET", "/v1/gc/status", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("gc status: %w", err)
|
|
}
|
|
|
|
if flagJSON {
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return err
|
|
}
|
|
return printJSON(v)
|
|
}
|
|
|
|
var resp struct {
|
|
Running bool `json:"running"`
|
|
LastRun *struct {
|
|
StartedAt string `json:"started_at"`
|
|
CompletedAt string `json:"completed_at"`
|
|
BlobsRemoved int `json:"blobs_removed"`
|
|
BytesFreed int64 `json:"bytes_freed"`
|
|
} `json:"last_run"`
|
|
}
|
|
if err := json.Unmarshal(data, &resp); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stdout, "Running: %v\n", resp.Running)
|
|
if resp.LastRun != nil {
|
|
_, _ = fmt.Fprintf(os.Stdout, "Last run: %s - %s\n", resp.LastRun.StartedAt, resp.LastRun.CompletedAt)
|
|
_, _ = fmt.Fprintf(os.Stdout, " Blobs removed: %d\n", resp.LastRun.BlobsRemoved)
|
|
_, _ = fmt.Fprintf(os.Stdout, " Bytes freed: %s\n", formatSize(resp.LastRun.BytesFreed))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---------- snapshot ----------
|
|
|
|
func snapshotCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "snapshot",
|
|
Short: "Trigger database backup via VACUUM INTO",
|
|
RunE: runSnapshot,
|
|
}
|
|
}
|
|
|
|
func runSnapshot(_ *cobra.Command, _ []string) error {
|
|
data, err := client.restDo("POST", "/v1/snapshot", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("trigger snapshot: %w", err)
|
|
}
|
|
|
|
if flagJSON {
|
|
var v any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
// Snapshot may return 204 with no body.
|
|
return nil
|
|
}
|
|
return printJSON(v)
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(os.Stdout, "Snapshot triggered.")
|
|
_ = data // may be empty
|
|
return nil
|
|
}
|
|
|
|
// ---------- helpers ----------
|
|
|
|
// confirmPrompt displays msg and waits for y/n from stdin.
|
|
func confirmPrompt(msg string) bool {
|
|
fmt.Fprintf(os.Stderr, "%s [y/N] ", msg)
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
if scanner.Scan() {
|
|
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
|
|
return answer == "y" || answer == "yes"
|
|
}
|
|
return false
|
|
}
|