init
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
package clicmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/iceBear67/simplepages/api"
|
||||
"github.com/iceBear67/simplepages/internal/client"
|
||||
"github.com/iceBear67/simplepages/internal/cliutil"
|
||||
)
|
||||
|
||||
func deploymentCmd(g *Globals) *cliutil.Command {
|
||||
return &cliutil.Command{
|
||||
Name: "deployment",
|
||||
Short: "Inspect, roll back and remove deployments",
|
||||
Long: "Every deployment a project has ever finished is kept until retention\n" +
|
||||
"drops it, which is what makes a rollback one command rather than a\n" +
|
||||
"rebuild. A project key may manage only its own project's deployments.",
|
||||
Sub: []*cliutil.Command{
|
||||
deploymentListCmd(g),
|
||||
deploymentShowCmd(g),
|
||||
deploymentActivateCmd(g),
|
||||
deploymentDeleteCmd(g),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func deploymentListCmd(g *Globals) *cliutil.Command {
|
||||
var (
|
||||
state string
|
||||
limit int
|
||||
)
|
||||
return &cliutil.Command{
|
||||
Name: "list",
|
||||
Short: "List a project's deployments",
|
||||
Long: "Newest first. The one marked active is what the site is serving, and\n" +
|
||||
"is the one to roll back from; any other ready deployment can be rolled\n" +
|
||||
"back to with \"pages deployment activate\".",
|
||||
Flags: func(fs *flag.FlagSet) {
|
||||
fs.StringVar(&state, "state", "", "show only deployments in this `state`: pending, uploading, ready, failed or deleting")
|
||||
fs.IntVar(&limit, "limit", 0, "stop after this many deployments; 0 lists them all")
|
||||
},
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if err := exactArgs(args, 0, "no arguments"); err != nil {
|
||||
return err
|
||||
}
|
||||
project, err := g.ProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deps, err := listDeployments(ctx, c, project, state, limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := g.Printer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The API's own shape, so -o json is the same document whether it came
|
||||
// from here or from curl, and an empty listing prints as [] not null.
|
||||
out := api.DeploymentList{Deployments: deps}
|
||||
if out.Deployments == nil {
|
||||
out.Deployments = []api.Deployment{}
|
||||
}
|
||||
return p.Print(out, func() *cliutil.Table {
|
||||
t := cliutil.NewTable("ID", "STATE", "ACTIVE", "FILES", "SIZE", "CREATED", "COMMIT")
|
||||
for _, d := range deps {
|
||||
t.Row(d.ID, d.State, cliutil.Bool(d.Active),
|
||||
strconv.Itoa(d.FileCount), cliutil.Bytes(d.TotalBytes),
|
||||
cliutil.Time(d.CreatedAt),
|
||||
cliutil.Str(cliutil.Truncate(d.Meta["git_sha"], 12)))
|
||||
}
|
||||
return t
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// listDeployments follows the cursor, stopping at limit when one was given.
|
||||
// Paging on the caller's behalf matters here for the same reason it does for
|
||||
// projects: a listing that silently showed the first page would be a lie, and
|
||||
// this one is read to decide which deployment to roll back to.
|
||||
func listDeployments(ctx context.Context, c *client.Client, project, state string, limit int) ([]api.Deployment, error) {
|
||||
opts := client.DeploymentListOptions{State: state}
|
||||
opts.Limit = 500
|
||||
if limit > 0 && limit < opts.Limit {
|
||||
opts.Limit = limit
|
||||
}
|
||||
var all []api.Deployment
|
||||
for {
|
||||
page, err := c.ListDeployments(ctx, project, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all = append(all, page.Deployments...)
|
||||
if limit > 0 && len(all) >= limit {
|
||||
return all[:limit], nil
|
||||
}
|
||||
if page.NextCursor == "" || len(page.Deployments) == 0 {
|
||||
return all, nil
|
||||
}
|
||||
opts.Cursor = page.NextCursor
|
||||
}
|
||||
}
|
||||
|
||||
func deploymentShowCmd(g *Globals) *cliutil.Command {
|
||||
var files bool
|
||||
return &cliutil.Command{
|
||||
Name: "show",
|
||||
Args: "<id>",
|
||||
Short: "Show one deployment",
|
||||
Long: "With --files, also lists the manifest: every path, its size and the\n" +
|
||||
"digest of its content. That is one line per file, so it is a lot of\n" +
|
||||
"output for a large site.",
|
||||
Flags: func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&files, "files", false, "also list the deployment's files")
|
||||
},
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if err := exactArgs(args, 1, "one deployment id"); err != nil {
|
||||
return err
|
||||
}
|
||||
project, err := g.ProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := c.GetDeployment(ctx, project, args[0], files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := g.Printer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Print(d, func() *cliutil.Table {
|
||||
t := cliutil.NewTable()
|
||||
t.Row("id", d.ID)
|
||||
t.Row("project", d.Project)
|
||||
t.Row("state", d.State)
|
||||
t.Row("active", cliutil.Bool(d.Active))
|
||||
t.Row("files", strconv.Itoa(d.FileCount))
|
||||
t.Row("total_bytes", cliutil.Bytes(d.TotalBytes))
|
||||
t.Row("created_at", cliutil.Time(d.CreatedAt))
|
||||
t.Row("finalized_at", cliutil.TimePtr(d.FinalizedAt))
|
||||
t.Row("activated_at", cliutil.TimePtr(d.ActivatedAt))
|
||||
if d.URL != "" {
|
||||
t.Row("url", d.URL)
|
||||
}
|
||||
if d.Error != "" {
|
||||
t.Row("error", d.Error)
|
||||
}
|
||||
for _, k := range sortedKeys(d.Meta) {
|
||||
t.Row("meta."+k, d.Meta[k])
|
||||
}
|
||||
for _, f := range d.Files {
|
||||
t.Row(f.Path, fmt.Sprintf("%s %s",
|
||||
cliutil.Bytes(f.Size), cliutil.Truncate(f.Digest, 12)))
|
||||
}
|
||||
return t
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// sortedKeys gives map-backed output a stable order, so two runs of the same
|
||||
// command produce the same lines and a diff of them means something.
|
||||
func sortedKeys(m map[string]string) []string {
|
||||
return slices.Sorted(maps.Keys(m))
|
||||
}
|
||||
|
||||
func deploymentActivateCmd(g *Globals) *cliutil.Command {
|
||||
return &cliutil.Command{
|
||||
Name: "activate",
|
||||
Args: "<id>",
|
||||
Short: "Switch the site to a deployment",
|
||||
Long: "This is how a rollback is done: name an older deployment and the\n" +
|
||||
"project serves it again. The switch is atomic and costs nothing —\n" +
|
||||
"the content is still on disk — so it takes effect immediately.",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if err := exactArgs(args, 1, "one deployment id"); err != nil {
|
||||
return err
|
||||
}
|
||||
project, err := g.ProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := c.Activate(ctx, project, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := g.Printer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Print(d, func() *cliutil.Table {
|
||||
t := cliutil.NewTable()
|
||||
t.Row("deployment", d.ID)
|
||||
t.Row("project", project)
|
||||
t.Row("state", d.State)
|
||||
t.Row("active", cliutil.Bool(d.Active))
|
||||
t.Row("files", strconv.Itoa(d.FileCount))
|
||||
t.Row("url", cliutil.Str(d.URL))
|
||||
return t
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func deploymentDeleteCmd(g *Globals) *cliutil.Command {
|
||||
var yes bool
|
||||
return &cliutil.Command{
|
||||
Name: "delete",
|
||||
Args: "<id>",
|
||||
Short: "Delete a deployment",
|
||||
Long: "The deployment the project is serving cannot be deleted; activate\n" +
|
||||
"another one first. Content no other deployment references is\n" +
|
||||
"reclaimed by the next garbage collection, not immediately.",
|
||||
Flags: func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&yes, "yes", false, "do not ask for confirmation")
|
||||
},
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if err := exactArgs(args, 1, "one deployment id"); err != nil {
|
||||
return err
|
||||
}
|
||||
id := args[0]
|
||||
project, err := g.ProjectName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !yes {
|
||||
if err := cliutil.Confirm(g.In, g.Err,
|
||||
fmt.Sprintf("Delete deployment %s of project %q?", id, project)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.DeleteDeployment(ctx, project, id); err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := g.Printer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Printf("deleted deployment %s\n", id)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ upkeep
|
||||
//
|
||||
// These two are server-wide rather than per-project, which is why they sit
|
||||
// under "pages system" next to "system info" and not under "deployment".
|
||||
|
||||
func systemGCCmd(g *Globals) *cliutil.Command {
|
||||
var dryRun bool
|
||||
return &cliutil.Command{
|
||||
Name: "gc",
|
||||
Short: "Run a garbage collection pass now",
|
||||
Long: "The server collects on a timer anyway; this is for an operator who\n" +
|
||||
"wants the disk back sooner. --dry-run reports what would be deleted\n" +
|
||||
"without deleting it, though it cannot count the content the listed\n" +
|
||||
"deployments hold — nothing was deleted, so it is all still in use.\n" +
|
||||
"Requires an admin key.",
|
||||
Flags: func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&dryRun, "dry-run", false, "report what would be deleted without deleting it")
|
||||
},
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if err := exactArgs(args, 0, "no arguments"); err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stats, err := c.Collect(ctx, dryRun)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := g.Printer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Print(stats, func() *cliutil.Table {
|
||||
t := cliutil.NewTable()
|
||||
t.Row("dry_run", cliutil.Bool(stats.DryRun))
|
||||
t.Row("deployments_deleted", strconv.Itoa(stats.DeploymentsDeleted))
|
||||
t.Row("blobs_deleted", strconv.Itoa(stats.BlobsDeleted))
|
||||
t.Row("bytes_freed", cliutil.Bytes(stats.BytesFreed))
|
||||
return t
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func systemFsckCmd(g *Globals) *cliutil.Command {
|
||||
var repair bool
|
||||
return &cliutil.Command{
|
||||
Name: "fsck",
|
||||
Short: "Check the stored reference counts against the manifests",
|
||||
Long: "On a healthy server this always reports no drift: the counts are\n" +
|
||||
"maintained by database triggers. It is for the cases outside normal\n" +
|
||||
"operation — a restored backup, a database edited by hand — because a\n" +
|
||||
"count that reads low is content the collector will delete while a\n" +
|
||||
"deployment still needs it. --repair rewrites the counts from the\n" +
|
||||
"manifests. Requires an admin key.",
|
||||
Flags: func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&repair, "repair", false, "correct the counts that disagree")
|
||||
},
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if err := exactArgs(args, 0, "no arguments"); err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rep, err := c.Fsck(ctx, repair)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := g.Printer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Print(rep, func() *cliutil.Table {
|
||||
t := cliutil.NewTable()
|
||||
t.Row("blobs", strconv.FormatInt(rep.Blobs, 10))
|
||||
t.Row("drift", strconv.Itoa(rep.DriftCount))
|
||||
t.Row("repaired", strconv.Itoa(rep.Repaired))
|
||||
for _, d := range rep.Drift {
|
||||
t.Row(cliutil.Truncate(d.Digest, 12),
|
||||
fmt.Sprintf("stored %d, actual %d", d.Stored, d.Actual))
|
||||
}
|
||||
return t
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user