273 lines
7.4 KiB
Go
273 lines
7.4 KiB
Go
package clicmd
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/iceBear67/simplepages/internal/cliutil"
|
|
"github.com/iceBear67/simplepages/internal/version"
|
|
)
|
|
|
|
// Root builds the command tree.
|
|
func Root(g *Globals) *cliutil.Command {
|
|
var showVersion bool
|
|
return &cliutil.Command{
|
|
Name: "pages",
|
|
Short: "Deploy static sites atomically",
|
|
Long: "Settings are taken from flags first, then PAGES_* environment\n" +
|
|
"variables, then the config file, then built-in defaults.\n\n" +
|
|
" PAGES_SERVER management API base URL\n" +
|
|
" PAGES_TOKEN API token\n" +
|
|
" PAGES_TOKEN_FILE file holding the API token\n" +
|
|
" PAGES_PROJECT default project\n" +
|
|
" PAGES_OUTPUT table or json\n" +
|
|
" PAGES_TIMEOUT per-request timeout\n" +
|
|
" PAGES_CONFIG config file path",
|
|
Flags: func(fs *flag.FlagSet) {
|
|
fs.BoolVar(&showVersion, "version", false, "print the version and exit")
|
|
},
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
if showVersion {
|
|
fmt.Fprintln(g.Out, version.String())
|
|
return nil
|
|
}
|
|
return cliutil.ErrUsage
|
|
},
|
|
Sub: []*cliutil.Command{
|
|
deployCmd(g),
|
|
projectCmd(g),
|
|
deploymentCmd(g),
|
|
keyCmd(g),
|
|
whoamiCmd(g),
|
|
systemCmd(g),
|
|
configCmd(g),
|
|
versionCmd(g),
|
|
},
|
|
}
|
|
}
|
|
|
|
func versionCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "version",
|
|
Short: "Print the version",
|
|
Long: "Reports the client build only; it does not contact the server.",
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
fmt.Fprintln(g.Out, version.String())
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func whoamiCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "whoami",
|
|
Short: "Show which key is being used",
|
|
Long: "Useful for confirming a CI runner picked up the credential you meant.",
|
|
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
|
|
}
|
|
who, err := c.WhoAmI(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p, err := g.Printer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return p.Print(who, func() *cliutil.Table {
|
|
t := cliutil.NewTable()
|
|
t.Row("key_id", who.KeyID)
|
|
t.Row("scope", who.Scope)
|
|
t.Row("project", cliutil.Str(who.Project))
|
|
t.Row("name", cliutil.Str(who.Name))
|
|
t.Row("expires_at", cliutil.TimePtr(who.ExpiresAt))
|
|
t.Row("server", c.BaseURL())
|
|
return t
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
func systemCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "system",
|
|
Short: "Inspect and maintain the server",
|
|
Sub: []*cliutil.Command{systemInfoCmd(g), systemGCCmd(g), systemFsckCmd(g)},
|
|
}
|
|
}
|
|
|
|
func systemInfoCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "info",
|
|
Short: "Show server version and storage counters",
|
|
Long: "Requires an admin key.",
|
|
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
|
|
}
|
|
info, err := c.SystemInfo(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p, err := g.Printer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return p.Print(info, func() *cliutil.Table {
|
|
t := cliutil.NewTable()
|
|
t.Row("version", info.Version)
|
|
t.Row("uptime", (time.Duration(info.UptimeS) * time.Second).String())
|
|
t.Row("schema_version", strconv.Itoa(info.SchemaVer))
|
|
t.Row("projects", strconv.FormatInt(info.Projects, 10))
|
|
t.Row("deployments", strconv.FormatInt(info.Deployments, 10))
|
|
t.Row("blobs", strconv.FormatInt(info.Blobs, 10))
|
|
t.Row("cas_bytes", cliutil.Bytes(info.CASBytes))
|
|
// How deployment trees are built on this host: hardlink shares
|
|
// inodes with the store, copy does not, and the difference shows
|
|
// up as disk usage.
|
|
t.Row("link_mode", info.LinkMode)
|
|
return t
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------- config
|
|
|
|
func configCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "config",
|
|
Short: "Read and write the CLI config file",
|
|
Long: "The config file is JSON, mode 0600, at $PAGES_CONFIG or\n" +
|
|
"$XDG_CONFIG_HOME/pages/config.json. It is a convenience for a\n" +
|
|
"workstation; CI should use PAGES_* environment variables instead.",
|
|
Sub: []*cliutil.Command{
|
|
configShowCmd(g),
|
|
configSetCmd(g),
|
|
configPathCmd(g),
|
|
},
|
|
}
|
|
}
|
|
|
|
// configView is what "config show" prints: the file's contents with the token
|
|
// reduced to its public half.
|
|
type configView struct {
|
|
Path string `json:"path"`
|
|
Server string `json:"server,omitempty"`
|
|
Token string `json:"token,omitempty"`
|
|
Project string `json:"project,omitempty"`
|
|
Output string `json:"output,omitempty"`
|
|
}
|
|
|
|
func configShowCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "show",
|
|
Short: "Show the config file, with the token redacted",
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
if err := exactArgs(args, 0, "no arguments"); err != nil {
|
|
return err
|
|
}
|
|
if err := g.Resolve(); err != nil {
|
|
return err
|
|
}
|
|
// From the file, not from the resolved settings: this command answers
|
|
// "what is stored here", and printing an environment-supplied token
|
|
// back at the user would be actively misleading.
|
|
view := configView{
|
|
Path: g.Config,
|
|
Server: g.file.Server,
|
|
Token: redactToken(g.file.Token),
|
|
Project: g.file.Project,
|
|
Output: g.file.Output,
|
|
}
|
|
p, err := g.Printer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return p.Print(view, func() *cliutil.Table {
|
|
t := cliutil.NewTable()
|
|
t.Row("path", cliutil.Str(view.Path))
|
|
t.Row("server", cliutil.Str(view.Server))
|
|
t.Row("token", cliutil.Str(view.Token))
|
|
t.Row("project", cliutil.Str(view.Project))
|
|
t.Row("output", cliutil.Str(view.Output))
|
|
return t
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
func configSetCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "set",
|
|
Args: "<server|token|project|output> <value>",
|
|
Short: "Set one config value",
|
|
Long: "An empty value removes the setting. The file is created mode 0600.\n" +
|
|
"Reading a token from a file avoids putting it in your shell history:\n" +
|
|
" pages config set token \"$(cat token.txt)\"",
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
if err := exactArgs(args, 2, "a key and a value"); err != nil {
|
|
return err
|
|
}
|
|
if err := g.Resolve(); err != nil {
|
|
return err
|
|
}
|
|
key, value := args[0], args[1]
|
|
f := g.file
|
|
switch key {
|
|
case "server":
|
|
f.Server = value
|
|
case "token":
|
|
f.Token = value
|
|
case "project":
|
|
f.Project = value
|
|
case "output":
|
|
if value != "" && !cliutil.ValidFormat(value) {
|
|
return fmt.Errorf("unknown output format %q: use table or json", value)
|
|
}
|
|
f.Output = value
|
|
default:
|
|
return cliutil.UsageErrorf("unknown config key %q", key)
|
|
}
|
|
if err := SaveConfig(g.Config, f); err != nil {
|
|
return err
|
|
}
|
|
p, err := g.Printer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.Printf("set %s in %s\n", key, g.Config)
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func configPathCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "path",
|
|
Short: "Print the config file path",
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
if err := exactArgs(args, 0, "no arguments"); err != nil {
|
|
return err
|
|
}
|
|
if err := g.Resolve(); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(g.Out, g.Config)
|
|
return nil
|
|
},
|
|
}
|
|
}
|