chore(cli): replace clibase with external `coder/serpent` (#12252)

This commit is contained in:
Ammar Bandukwala 2024-03-15 11:24:38 -05:00 committed by GitHub
parent bed2545636
commit 496232446d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
185 changed files with 3770 additions and 7221 deletions

View File

@ -29,12 +29,12 @@ import (
"github.com/coder/coder/v2/agent/agentproc"
"github.com/coder/coder/v2/agent/reaper"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/serpent"
)
func (r *RootCmd) workspaceAgent() *clibase.Cmd {
func (r *RootCmd) workspaceAgent() *serpent.Cmd {
var (
auth string
logDir string
@ -49,12 +49,12 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
slogJSONPath string
slogStackdriverPath string
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "agent",
Short: `Starts the Coder workspace agent.`,
// This command isn't useful to manually execute.
Hidden: true,
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
@ -325,33 +325,33 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "auth",
Default: "token",
Description: "Specify the authentication type to use for the agent.",
Env: "CODER_AGENT_AUTH",
Value: clibase.StringOf(&auth),
Value: serpent.StringOf(&auth),
},
{
Flag: "log-dir",
Default: os.TempDir(),
Description: "Specify the location for the agent log files.",
Env: "CODER_AGENT_LOG_DIR",
Value: clibase.StringOf(&logDir),
Value: serpent.StringOf(&logDir),
},
{
Flag: "script-data-dir",
Default: os.TempDir(),
Description: "Specify the location for storing script data.",
Env: "CODER_AGENT_SCRIPT_DATA_DIR",
Value: clibase.StringOf(&scriptDataDir),
Value: serpent.StringOf(&scriptDataDir),
},
{
Flag: "pprof-address",
Default: "127.0.0.1:6060",
Env: "CODER_AGENT_PPROF_ADDRESS",
Value: clibase.StringOf(&pprofAddress),
Value: serpent.StringOf(&pprofAddress),
Description: "The address to serve pprof.",
},
{
@ -359,7 +359,7 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
Env: "",
Description: "Do not start a process reaper.",
Value: clibase.BoolOf(&noReap),
Value: serpent.BoolOf(&noReap),
},
{
Flag: "ssh-max-timeout",
@ -367,27 +367,27 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
Default: "72h",
Env: "CODER_AGENT_SSH_MAX_TIMEOUT",
Description: "Specify the max timeout for a SSH connection, it is advisable to set it to a minimum of 60s, but no more than 72h.",
Value: clibase.DurationOf(&sshMaxTimeout),
Value: serpent.DurationOf(&sshMaxTimeout),
},
{
Flag: "tailnet-listen-port",
Default: "0",
Env: "CODER_AGENT_TAILNET_LISTEN_PORT",
Description: "Specify a static port for Tailscale to use for listening.",
Value: clibase.Int64Of(&tailnetListenPort),
Value: serpent.Int64Of(&tailnetListenPort),
},
{
Flag: "prometheus-address",
Default: "127.0.0.1:2112",
Env: "CODER_AGENT_PROMETHEUS_ADDRESS",
Value: clibase.StringOf(&prometheusAddress),
Value: serpent.StringOf(&prometheusAddress),
Description: "The bind address to serve Prometheus metrics.",
},
{
Flag: "debug-address",
Default: "127.0.0.1:2113",
Env: "CODER_AGENT_DEBUG_ADDRESS",
Value: clibase.StringOf(&debugAddress),
Value: serpent.StringOf(&debugAddress),
Description: "The bind address to serve a debug HTTP server.",
},
{
@ -396,7 +396,7 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
Flag: "log-human",
Env: "CODER_AGENT_LOGGING_HUMAN",
Default: "/dev/stderr",
Value: clibase.StringOf(&slogHumanPath),
Value: serpent.StringOf(&slogHumanPath),
},
{
Name: "JSON Log Location",
@ -404,7 +404,7 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
Flag: "log-json",
Env: "CODER_AGENT_LOGGING_JSON",
Default: "",
Value: clibase.StringOf(&slogJSONPath),
Value: serpent.StringOf(&slogJSONPath),
},
{
Name: "Stackdriver Log Location",
@ -412,7 +412,7 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
Flag: "log-stackdriver",
Env: "CODER_AGENT_LOGGING_STACKDRIVER",
Default: "",
Value: clibase.StringOf(&slogStackdriverPath),
Value: serpent.StringOf(&slogStackdriverPath),
},
}

View File

@ -6,22 +6,22 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) autoupdate() *clibase.Cmd {
func (r *RootCmd) autoupdate() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "autoupdate <workspace> <always|never>",
Short: "Toggle auto-update policy for a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(2),
Middleware: serpent.Chain(
serpent.RequireNArgs(2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
policy := strings.ToLower(inv.Args[1])
err := validateAutoUpdatePolicy(policy)
if err != nil {

View File

@ -1,80 +0,0 @@
// Package clibase offers an all-in-one solution for a highly configurable CLI
// application. Within Coder, we use it for all of our subcommands, which
// demands more functionality than cobra/viber offers.
//
// The Command interface is loosely based on the chi middleware pattern and
// http.Handler/HandlerFunc.
package clibase
import (
"strings"
"golang.org/x/exp/maps"
)
// Group describes a hierarchy of groups that an option or command belongs to.
type Group struct {
Parent *Group `json:"parent,omitempty"`
Name string `json:"name,omitempty"`
YAML string `json:"yaml,omitempty"`
Description string `json:"description,omitempty"`
}
// Ancestry returns the group and all of its parents, in order.
func (g *Group) Ancestry() []Group {
if g == nil {
return nil
}
groups := []Group{*g}
for p := g.Parent; p != nil; p = p.Parent {
// Prepend to the slice so that the order is correct.
groups = append([]Group{*p}, groups...)
}
return groups
}
func (g *Group) FullName() string {
var names []string
for _, g := range g.Ancestry() {
names = append(names, g.Name)
}
return strings.Join(names, " / ")
}
// Annotations is an arbitrary key-mapping used to extend the Option and Command types.
// Its methods won't panic if the map is nil.
type Annotations map[string]string
// Mark sets a value on the annotations map, creating one
// if it doesn't exist. Mark does not mutate the original and
// returns a copy. It is suitable for chaining.
func (a Annotations) Mark(key string, value string) Annotations {
var aa Annotations
if a != nil {
aa = maps.Clone(a)
} else {
aa = make(Annotations)
}
aa[key] = value
return aa
}
// IsSet returns true if the key is set in the annotations map.
func (a Annotations) IsSet(key string) bool {
if a == nil {
return false
}
_, ok := a[key]
return ok
}
// Get retrieves a key from the map, returning false if the key is not found
// or the map is nil.
func (a Annotations) Get(key string) (string, bool) {
if a == nil {
return "", false
}
v, ok := a[key]
return v, ok
}

View File

@ -1,633 +0,0 @@
package clibase
import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"os/signal"
"strings"
"testing"
"unicode"
"cdr.dev/slog"
"github.com/spf13/pflag"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
"github.com/coder/coder/v2/coderd/util/slice"
)
// Cmd describes an executable command.
type Cmd struct {
// Parent is the direct parent of the command.
Parent *Cmd
// Children is a list of direct descendants.
Children []*Cmd
// Use is provided in form "command [flags] [args...]".
Use string
// Aliases is a list of alternative names for the command.
Aliases []string
// Short is a one-line description of the command.
Short string
// Hidden determines whether the command should be hidden from help.
Hidden bool
// RawArgs determines whether the command should receive unparsed arguments.
// No flags are parsed when set, and the command is responsible for parsing
// its own flags.
RawArgs bool
// Long is a detailed description of the command,
// presented on its help page. It may contain examples.
Long string
Options OptionSet
Annotations Annotations
// Middleware is called before the Handler.
// Use Chain() to combine multiple middlewares.
Middleware MiddlewareFunc
Handler HandlerFunc
HelpHandler HandlerFunc
}
// AddSubcommands adds the given subcommands, setting their
// Parent field automatically.
func (c *Cmd) AddSubcommands(cmds ...*Cmd) {
for _, cmd := range cmds {
cmd.Parent = c
c.Children = append(c.Children, cmd)
}
}
// Walk calls fn for the command and all its children.
func (c *Cmd) Walk(fn func(*Cmd)) {
fn(c)
for _, child := range c.Children {
child.Parent = c
child.Walk(fn)
}
}
// PrepareAll performs initialization and linting on the command and all its children.
func (c *Cmd) PrepareAll() error {
if c.Use == "" {
return xerrors.New("command must have a Use field so that it has a name")
}
var merr error
for i := range c.Options {
opt := &c.Options[i]
if opt.Name == "" {
switch {
case opt.Flag != "":
opt.Name = opt.Flag
case opt.Env != "":
opt.Name = opt.Env
case opt.YAML != "":
opt.Name = opt.YAML
default:
merr = errors.Join(merr, xerrors.Errorf("option must have a Name, Flag, Env or YAML field"))
}
}
if opt.Description != "" {
// Enforce that description uses sentence form.
if unicode.IsLower(rune(opt.Description[0])) {
merr = errors.Join(merr, xerrors.Errorf("option %q description should start with a capital letter", opt.Name))
}
if !strings.HasSuffix(opt.Description, ".") {
merr = errors.Join(merr, xerrors.Errorf("option %q description should end with a period", opt.Name))
}
}
}
slices.SortFunc(c.Options, func(a, b Option) int {
return slice.Ascending(a.Name, b.Name)
})
slices.SortFunc(c.Children, func(a, b *Cmd) int {
return slice.Ascending(a.Name(), b.Name())
})
for _, child := range c.Children {
child.Parent = c
err := child.PrepareAll()
if err != nil {
merr = errors.Join(merr, xerrors.Errorf("command %v: %w", child.Name(), err))
}
}
return merr
}
// Name returns the first word in the Use string.
func (c *Cmd) Name() string {
return strings.Split(c.Use, " ")[0]
}
// FullName returns the full invocation name of the command,
// as seen on the command line.
func (c *Cmd) FullName() string {
var names []string
if c.Parent != nil {
names = append(names, c.Parent.FullName())
}
names = append(names, c.Name())
return strings.Join(names, " ")
}
// FullName returns usage of the command, preceded
// by the usage of its parents.
func (c *Cmd) FullUsage() string {
var uses []string
if c.Parent != nil {
uses = append(uses, c.Parent.FullName())
}
uses = append(uses, c.Use)
return strings.Join(uses, " ")
}
// FullOptions returns the options of the command and its parents.
func (c *Cmd) FullOptions() OptionSet {
var opts OptionSet
if c.Parent != nil {
opts = append(opts, c.Parent.FullOptions()...)
}
opts = append(opts, c.Options...)
return opts
}
// Invoke creates a new invocation of the command, with
// stdio discarded.
//
// The returned invocation is not live until Run() is called.
func (c *Cmd) Invoke(args ...string) *Invocation {
return &Invocation{
Command: c,
Args: args,
Stdout: io.Discard,
Stderr: io.Discard,
Stdin: strings.NewReader(""),
Logger: slog.Make(),
}
}
// Invocation represents an instance of a command being executed.
type Invocation struct {
ctx context.Context
Command *Cmd
parsedFlags *pflag.FlagSet
Args []string
// Environ is a list of environment variables. Use EnvsWithPrefix to parse
// os.Environ.
Environ Environ
Stdout io.Writer
Stderr io.Writer
Stdin io.Reader
Logger slog.Logger
Net Net
// testing
signalNotifyContext func(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
}
// WithOS returns the invocation as a main package, filling in the invocation's unset
// fields with OS defaults.
func (inv *Invocation) WithOS() *Invocation {
return inv.with(func(i *Invocation) {
i.Stdout = os.Stdout
i.Stderr = os.Stderr
i.Stdin = os.Stdin
i.Args = os.Args[1:]
i.Environ = ParseEnviron(os.Environ(), "")
i.Net = osNet{}
})
}
// WithTestSignalNotifyContext allows overriding the default implementation of SignalNotifyContext.
// This should only be used in testing.
func (inv *Invocation) WithTestSignalNotifyContext(
_ testing.TB, // ensure we only call this from tests
f func(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc),
) *Invocation {
return inv.with(func(i *Invocation) {
i.signalNotifyContext = f
})
}
// SignalNotifyContext is equivalent to signal.NotifyContext, but supports being overridden in
// tests.
func (inv *Invocation) SignalNotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
if inv.signalNotifyContext == nil {
return signal.NotifyContext(parent, signals...)
}
return inv.signalNotifyContext(parent, signals...)
}
func (inv *Invocation) WithTestParsedFlags(
_ testing.TB, // ensure we only call this from tests
parsedFlags *pflag.FlagSet,
) *Invocation {
return inv.with(func(i *Invocation) {
i.parsedFlags = parsedFlags
})
}
func (inv *Invocation) Context() context.Context {
if inv.ctx == nil {
return context.Background()
}
return inv.ctx
}
func (inv *Invocation) ParsedFlags() *pflag.FlagSet {
if inv.parsedFlags == nil {
panic("flags not parsed, has Run() been called?")
}
return inv.parsedFlags
}
type runState struct {
allArgs []string
commandDepth int
flagParseErr error
}
func copyFlagSetWithout(fs *pflag.FlagSet, without string) *pflag.FlagSet {
fs2 := pflag.NewFlagSet("", pflag.ContinueOnError)
fs2.Usage = func() {}
fs.VisitAll(func(f *pflag.Flag) {
if f.Name == without {
return
}
fs2.AddFlag(f)
})
return fs2
}
// run recursively executes the command and its children.
// allArgs is wired through the stack so that global flags can be accepted
// anywhere in the command invocation.
func (inv *Invocation) run(state *runState) error {
err := inv.Command.Options.ParseEnv(inv.Environ)
if err != nil {
return xerrors.Errorf("parsing env: %w", err)
}
// Now the fun part, argument parsing!
children := make(map[string]*Cmd)
for _, child := range inv.Command.Children {
child.Parent = inv.Command
for _, name := range append(child.Aliases, child.Name()) {
if _, ok := children[name]; ok {
return xerrors.Errorf("duplicate command name: %s", name)
}
children[name] = child
}
}
if inv.parsedFlags == nil {
inv.parsedFlags = pflag.NewFlagSet(inv.Command.Name(), pflag.ContinueOnError)
// We handle Usage ourselves.
inv.parsedFlags.Usage = func() {}
}
// If we find a duplicate flag, we want the deeper command's flag to override
// the shallow one. Unfortunately, pflag has no way to remove a flag, so we
// have to create a copy of the flagset without a value.
inv.Command.Options.FlagSet().VisitAll(func(f *pflag.Flag) {
if inv.parsedFlags.Lookup(f.Name) != nil {
inv.parsedFlags = copyFlagSetWithout(inv.parsedFlags, f.Name)
}
inv.parsedFlags.AddFlag(f)
})
var parsedArgs []string
if !inv.Command.RawArgs {
// Flag parsing will fail on intermediate commands in the command tree,
// so we check the error after looking for a child command.
state.flagParseErr = inv.parsedFlags.Parse(state.allArgs)
parsedArgs = inv.parsedFlags.Args()
}
// Set value sources for flags.
for i, opt := range inv.Command.Options {
if fl := inv.parsedFlags.Lookup(opt.Flag); fl != nil && fl.Changed {
inv.Command.Options[i].ValueSource = ValueSourceFlag
}
}
// Read YAML configs, if any.
for _, opt := range inv.Command.Options {
path, ok := opt.Value.(*YAMLConfigPath)
if !ok || path.String() == "" {
continue
}
byt, err := os.ReadFile(path.String())
if err != nil {
return xerrors.Errorf("reading yaml: %w", err)
}
var n yaml.Node
err = yaml.Unmarshal(byt, &n)
if err != nil {
return xerrors.Errorf("decoding yaml: %w", err)
}
err = inv.Command.Options.UnmarshalYAML(&n)
if err != nil {
return xerrors.Errorf("applying yaml: %w", err)
}
}
err = inv.Command.Options.SetDefaults()
if err != nil {
return xerrors.Errorf("setting defaults: %w", err)
}
// Run child command if found (next child only)
// We must do subcommand detection after flag parsing so we don't mistake flag
// values for subcommand names.
if len(parsedArgs) > state.commandDepth {
nextArg := parsedArgs[state.commandDepth]
if child, ok := children[nextArg]; ok {
child.Parent = inv.Command
inv.Command = child
state.commandDepth++
return inv.run(state)
}
}
// Flag parse errors are irrelevant for raw args commands.
if !inv.Command.RawArgs && state.flagParseErr != nil && !errors.Is(state.flagParseErr, pflag.ErrHelp) {
return xerrors.Errorf(
"parsing flags (%v) for %q: %w",
state.allArgs,
inv.Command.FullName(), state.flagParseErr,
)
}
// All options should be set. Check all required options have sources,
// meaning they were set by the user in some way (env, flag, etc).
var missing []string
for _, opt := range inv.Command.Options {
if opt.Required && opt.ValueSource == ValueSourceNone {
missing = append(missing, opt.Flag)
}
}
// Don't error for missing flags if `--help` was supplied.
if len(missing) > 0 && !errors.Is(state.flagParseErr, pflag.ErrHelp) {
return xerrors.Errorf("Missing values for the required flags: %s", strings.Join(missing, ", "))
}
if inv.Command.RawArgs {
// If we're at the root command, then the name is omitted
// from the arguments, so we can just use the entire slice.
if state.commandDepth == 0 {
inv.Args = state.allArgs
} else {
argPos, err := findArg(inv.Command.Name(), state.allArgs, inv.parsedFlags)
if err != nil {
panic(err)
}
inv.Args = state.allArgs[argPos+1:]
}
} else {
// In non-raw-arg mode, we want to skip over flags.
inv.Args = parsedArgs[state.commandDepth:]
}
mw := inv.Command.Middleware
if mw == nil {
mw = Chain()
}
ctx := inv.ctx
if ctx == nil {
ctx = context.Background()
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
inv = inv.WithContext(ctx)
if inv.Command.Handler == nil || errors.Is(state.flagParseErr, pflag.ErrHelp) {
if inv.Command.HelpHandler == nil {
return xerrors.Errorf("no handler or help for command %s", inv.Command.FullName())
}
return inv.Command.HelpHandler(inv)
}
err = mw(inv.Command.Handler)(inv)
if err != nil {
return &RunCommandError{
Cmd: inv.Command,
Err: err,
}
}
return nil
}
type RunCommandError struct {
Cmd *Cmd
Err error
}
func (e *RunCommandError) Unwrap() error {
return e.Err
}
func (e *RunCommandError) Error() string {
return fmt.Sprintf("running command %q: %+v", e.Cmd.FullName(), e.Err)
}
// findArg returns the index of the first occurrence of arg in args, skipping
// over all flags.
func findArg(want string, args []string, fs *pflag.FlagSet) (int, error) {
for i := 0; i < len(args); i++ {
arg := args[i]
if !strings.HasPrefix(arg, "-") {
if arg == want {
return i, nil
}
continue
}
// This is a flag!
if strings.Contains(arg, "=") {
// The flag contains the value in the same arg, just skip.
continue
}
// We need to check if NoOptValue is set, then we should not wait
// for the next arg to be the value.
f := fs.Lookup(strings.TrimLeft(arg, "-"))
if f == nil {
return -1, xerrors.Errorf("unknown flag: %s", arg)
}
if f.NoOptDefVal != "" {
continue
}
if i == len(args)-1 {
return -1, xerrors.Errorf("flag %s requires a value", arg)
}
// Skip the value.
i++
}
return -1, xerrors.Errorf("arg %s not found", want)
}
// Run executes the command.
// If two command share a flag name, the first command wins.
//
//nolint:revive
func (inv *Invocation) Run() (err error) {
defer func() {
// Pflag is panicky, so additional context is helpful in tests.
if flag.Lookup("test.v") == nil {
return
}
if r := recover(); r != nil {
err = xerrors.Errorf("panic recovered for %s: %v", inv.Command.FullName(), r)
panic(err)
}
}()
// We close Stdin to prevent deadlocks, e.g. when the command
// has ended but an io.Copy is still reading from Stdin.
defer func() {
if inv.Stdin == nil {
return
}
rc, ok := inv.Stdin.(io.ReadCloser)
if !ok {
return
}
e := rc.Close()
err = errors.Join(err, e)
}()
err = inv.run(&runState{
allArgs: inv.Args,
})
return err
}
// WithContext returns a copy of the Invocation with the given context.
func (inv *Invocation) WithContext(ctx context.Context) *Invocation {
return inv.with(func(i *Invocation) {
i.ctx = ctx
})
}
// with returns a copy of the Invocation with the given function applied.
func (inv *Invocation) with(fn func(*Invocation)) *Invocation {
i2 := *inv
fn(&i2)
return &i2
}
// MiddlewareFunc returns the next handler in the chain,
// or nil if there are no more.
type MiddlewareFunc func(next HandlerFunc) HandlerFunc
func chain(ms ...MiddlewareFunc) MiddlewareFunc {
return MiddlewareFunc(func(next HandlerFunc) HandlerFunc {
if len(ms) > 0 {
return chain(ms[1:]...)(ms[0](next))
}
return next
})
}
// Chain returns a Handler that first calls middleware in order.
//
//nolint:revive
func Chain(ms ...MiddlewareFunc) MiddlewareFunc {
// We need to reverse the array to provide top-to-bottom execution
// order when defining a command.
reversed := make([]MiddlewareFunc, len(ms))
for i := range ms {
reversed[len(ms)-1-i] = ms[i]
}
return chain(reversed...)
}
func ShowUsageOnError(next HandlerFunc) HandlerFunc {
return func(i *Invocation) error {
err := next(i)
if err != nil {
return xerrors.Errorf("Usage: %s\nError: %w", i.Command.FullUsage(), err)
}
return nil
}
}
func RequireNArgs(want int) MiddlewareFunc {
return RequireRangeArgs(want, want)
}
// RequireRangeArgs returns a Middleware that requires the number of arguments
// to be between start and end (inclusive). If end is -1, then the number of
// arguments must be at least start.
func RequireRangeArgs(start, end int) MiddlewareFunc {
if start < 0 {
panic("start must be >= 0")
}
return func(next HandlerFunc) HandlerFunc {
// ShowUsageOnError will add the command usage before the error message.
return ShowUsageOnError(func(i *Invocation) error {
got := len(i.Args)
switch {
case start == end && got != start:
switch start {
case 0:
if len(i.Command.Children) > 0 {
return xerrors.Errorf("unrecognized subcommand %q", i.Args[0])
}
return xerrors.Errorf("wanted no args but got %v %v", got, i.Args)
default:
return xerrors.Errorf(
"wanted %v args but got %v %v",
start,
got,
i.Args,
)
}
case start > 0 && end == -1:
switch {
case got < start:
return xerrors.Errorf(
"wanted at least %v args but got %v",
start,
got,
)
default:
return next(i)
}
case start > end:
panic("start must be <= end")
case got < start || got > end:
return xerrors.Errorf(
"wanted between %v and %v args but got %v",
start, end,
got,
)
default:
return next(i)
}
})
}
}
// HandlerFunc handles an Invocation of a command.
type HandlerFunc func(i *Invocation) error

View File

@ -1,735 +0,0 @@
package clibase_test
import (
"bytes"
"context"
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
)
// ioBufs is the standard input, output, and error for a command.
type ioBufs struct {
Stdin bytes.Buffer
Stdout bytes.Buffer
Stderr bytes.Buffer
}
// fakeIO sets Stdin, Stdout, and Stderr to buffers.
func fakeIO(i *clibase.Invocation) *ioBufs {
var b ioBufs
i.Stdout = &b.Stdout
i.Stderr = &b.Stderr
i.Stdin = &b.Stdin
return &b
}
func TestCommand(t *testing.T) {
t.Parallel()
cmd := func() *clibase.Cmd {
var (
verbose bool
lower bool
prefix string
reqBool bool
reqStr string
)
return &clibase.Cmd{
Use: "root [subcommand]",
Options: clibase.OptionSet{
clibase.Option{
Name: "verbose",
Flag: "verbose",
Value: clibase.BoolOf(&verbose),
},
clibase.Option{
Name: "prefix",
Flag: "prefix",
Value: clibase.StringOf(&prefix),
},
},
Children: []*clibase.Cmd{
{
Use: "required-flag --req-bool=true --req-string=foo",
Short: "Example with required flags",
Options: clibase.OptionSet{
clibase.Option{
Name: "req-bool",
Flag: "req-bool",
Value: clibase.BoolOf(&reqBool),
Required: true,
},
clibase.Option{
Name: "req-string",
Flag: "req-string",
Value: clibase.Validate(clibase.StringOf(&reqStr), func(value *clibase.String) error {
ok := strings.Contains(value.String(), " ")
if !ok {
return xerrors.Errorf("string must contain a space")
}
return nil
}),
Required: true,
},
},
HelpHandler: func(i *clibase.Invocation) error {
_, _ = i.Stdout.Write([]byte("help text.png"))
return nil
},
Handler: func(i *clibase.Invocation) error {
_, _ = i.Stdout.Write([]byte(fmt.Sprintf("%s-%t", reqStr, reqBool)))
return nil
},
},
{
Use: "toupper [word]",
Short: "Converts a word to upper case",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
),
Aliases: []string{"up"},
Options: clibase.OptionSet{
clibase.Option{
Name: "lower",
Flag: "lower",
Value: clibase.BoolOf(&lower),
},
},
Handler: func(i *clibase.Invocation) error {
_, _ = i.Stdout.Write([]byte(prefix))
w := i.Args[0]
if lower {
w = strings.ToLower(w)
} else {
w = strings.ToUpper(w)
}
_, _ = i.Stdout.Write(
[]byte(
w,
),
)
if verbose {
i.Stdout.Write([]byte("!!!"))
}
return nil
},
},
},
}
}
t.Run("SimpleOK", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke("toupper", "hello")
io := fakeIO(i)
i.Run()
require.Equal(t, "HELLO", io.Stdout.String())
})
t.Run("Alias", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"up", "hello",
)
io := fakeIO(i)
i.Run()
require.Equal(t, "HELLO", io.Stdout.String())
})
t.Run("NoSubcommand", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"na",
)
io := fakeIO(i)
err := i.Run()
require.Empty(t, io.Stdout.String())
require.Error(t, err)
})
t.Run("BadArgs", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"toupper",
)
io := fakeIO(i)
err := i.Run()
require.Empty(t, io.Stdout.String())
require.Error(t, err)
})
t.Run("UnknownFlags", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"toupper", "--unknown",
)
io := fakeIO(i)
err := i.Run()
require.Empty(t, io.Stdout.String())
require.Error(t, err)
})
t.Run("Verbose", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"--verbose", "toupper", "hello",
)
io := fakeIO(i)
require.NoError(t, i.Run())
require.Equal(t, "HELLO!!!", io.Stdout.String())
})
t.Run("Verbose=", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"--verbose=true", "toupper", "hello",
)
io := fakeIO(i)
require.NoError(t, i.Run())
require.Equal(t, "HELLO!!!", io.Stdout.String())
})
t.Run("PrefixSpace", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"--prefix", "conv: ", "toupper", "hello",
)
io := fakeIO(i)
require.NoError(t, i.Run())
require.Equal(t, "conv: HELLO", io.Stdout.String())
})
t.Run("GlobalFlagsAnywhere", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"toupper", "--prefix", "conv: ", "hello", "--verbose",
)
io := fakeIO(i)
require.NoError(t, i.Run())
require.Equal(t, "conv: HELLO!!!", io.Stdout.String())
})
t.Run("LowerVerbose", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"toupper", "--verbose", "hello", "--lower",
)
io := fakeIO(i)
require.NoError(t, i.Run())
require.Equal(t, "hello!!!", io.Stdout.String())
})
t.Run("ParsedFlags", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"toupper", "--verbose", "hello", "--lower",
)
_ = fakeIO(i)
require.NoError(t, i.Run())
require.Equal(t,
"true",
i.ParsedFlags().Lookup("verbose").Value.String(),
)
})
t.Run("NoDeepChild", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"root", "level", "level", "toupper", "--verbose", "hello", "--lower",
)
fio := fakeIO(i)
require.Error(t, i.Run(), fio.Stdout.String())
})
t.Run("RequiredFlagsMissing", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag",
)
fio := fakeIO(i)
err := i.Run()
require.Error(t, err, fio.Stdout.String())
require.ErrorContains(t, err, "Missing values")
})
t.Run("RequiredFlagsMissingWithHelp", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag",
"--help",
)
fio := fakeIO(i)
err := i.Run()
require.NoError(t, err)
require.Contains(t, fio.Stdout.String(), "help text.png")
})
t.Run("RequiredFlagsMissingBool", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag", "--req-string", "foo bar",
)
fio := fakeIO(i)
err := i.Run()
require.Error(t, err, fio.Stdout.String())
require.ErrorContains(t, err, "Missing values for the required flags: req-bool")
})
t.Run("RequiredFlagsMissingString", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag", "--req-bool", "true",
)
fio := fakeIO(i)
err := i.Run()
require.Error(t, err, fio.Stdout.String())
require.ErrorContains(t, err, "Missing values for the required flags: req-string")
})
t.Run("RequiredFlagsInvalid", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag", "--req-string", "nospace",
)
fio := fakeIO(i)
err := i.Run()
require.Error(t, err, fio.Stdout.String())
require.ErrorContains(t, err, "string must contain a space")
})
t.Run("RequiredFlagsOK", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag", "--req-bool", "true", "--req-string", "foo bar",
)
fio := fakeIO(i)
err := i.Run()
require.NoError(t, err, fio.Stdout.String())
})
}
func TestCommand_DeepNest(t *testing.T) {
t.Parallel()
cmd := &clibase.Cmd{
Use: "1",
Children: []*clibase.Cmd{
{
Use: "2",
Children: []*clibase.Cmd{
{
Use: "3",
Handler: func(i *clibase.Invocation) error {
i.Stdout.Write([]byte("3"))
return nil
},
},
},
},
},
}
inv := cmd.Invoke("2", "3")
stdio := fakeIO(inv)
err := inv.Run()
require.NoError(t, err)
require.Equal(t, "3", stdio.Stdout.String())
}
func TestCommand_FlagOverride(t *testing.T) {
t.Parallel()
var flag string
cmd := &clibase.Cmd{
Use: "1",
Options: clibase.OptionSet{
{
Name: "flag",
Flag: "f",
Value: clibase.DiscardValue,
},
},
Children: []*clibase.Cmd{
{
Use: "2",
Options: clibase.OptionSet{
{
Name: "flag",
Flag: "f",
Value: clibase.StringOf(&flag),
},
},
Handler: func(i *clibase.Invocation) error {
return nil
},
},
},
}
err := cmd.Invoke("2", "--f", "mhmm").Run()
require.NoError(t, err)
require.Equal(t, "mhmm", flag)
}
func TestCommand_MiddlewareOrder(t *testing.T) {
t.Parallel()
mw := func(letter string) clibase.MiddlewareFunc {
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return (func(i *clibase.Invocation) error {
_, _ = i.Stdout.Write([]byte(letter))
return next(i)
})
}
}
cmd := &clibase.Cmd{
Use: "toupper [word]",
Short: "Converts a word to upper case",
Middleware: clibase.Chain(
mw("A"),
mw("B"),
mw("C"),
),
Handler: (func(i *clibase.Invocation) error {
return nil
}),
}
i := cmd.Invoke(
"hello", "world",
)
io := fakeIO(i)
require.NoError(t, i.Run())
require.Equal(t, "ABC", io.Stdout.String())
}
func TestCommand_RawArgs(t *testing.T) {
t.Parallel()
cmd := func() *clibase.Cmd {
return &clibase.Cmd{
Use: "root",
Options: clibase.OptionSet{
{
Name: "password",
Flag: "password",
Value: clibase.StringOf(new(string)),
},
},
Children: []*clibase.Cmd{
{
Use: "sushi <args...>",
Short: "Throws back raw output",
RawArgs: true,
Handler: (func(i *clibase.Invocation) error {
if v := i.ParsedFlags().Lookup("password").Value.String(); v != "codershack" {
return xerrors.Errorf("password %q is wrong!", v)
}
i.Stdout.Write([]byte(strings.Join(i.Args, " ")))
return nil
}),
},
},
}
}
t.Run("OK", func(t *testing.T) {
// Flag parsed before the raw arg command should still work.
t.Parallel()
i := cmd().Invoke(
"--password", "codershack", "sushi", "hello", "--verbose", "world",
)
io := fakeIO(i)
require.NoError(t, i.Run())
require.Equal(t, "hello --verbose world", io.Stdout.String())
})
t.Run("BadFlag", func(t *testing.T) {
// Verbose before the raw arg command should fail.
t.Parallel()
i := cmd().Invoke(
"--password", "codershack", "--verbose", "sushi", "hello", "world",
)
io := fakeIO(i)
require.Error(t, i.Run())
require.Empty(t, io.Stdout.String())
})
t.Run("NoPassword", func(t *testing.T) {
// Flag parsed before the raw arg command should still work.
t.Parallel()
i := cmd().Invoke(
"sushi", "hello", "--verbose", "world",
)
_ = fakeIO(i)
require.Error(t, i.Run())
})
}
func TestCommand_RootRaw(t *testing.T) {
t.Parallel()
cmd := &clibase.Cmd{
RawArgs: true,
Handler: func(i *clibase.Invocation) error {
i.Stdout.Write([]byte(strings.Join(i.Args, " ")))
return nil
},
}
inv := cmd.Invoke("hello", "--verbose", "--friendly")
stdio := fakeIO(inv)
err := inv.Run()
require.NoError(t, err)
require.Equal(t, "hello --verbose --friendly", stdio.Stdout.String())
}
func TestCommand_HyphenHyphen(t *testing.T) {
t.Parallel()
cmd := &clibase.Cmd{
Handler: (func(i *clibase.Invocation) error {
i.Stdout.Write([]byte(strings.Join(i.Args, " ")))
return nil
}),
}
inv := cmd.Invoke("--", "--verbose", "--friendly")
stdio := fakeIO(inv)
err := inv.Run()
require.NoError(t, err)
require.Equal(t, "--verbose --friendly", stdio.Stdout.String())
}
func TestCommand_ContextCancels(t *testing.T) {
t.Parallel()
var gotCtx context.Context
cmd := &clibase.Cmd{
Handler: (func(i *clibase.Invocation) error {
gotCtx = i.Context()
if err := gotCtx.Err(); err != nil {
return xerrors.Errorf("unexpected context error: %w", i.Context().Err())
}
return nil
}),
}
err := cmd.Invoke().Run()
require.NoError(t, err)
require.Error(t, gotCtx.Err())
}
func TestCommand_Help(t *testing.T) {
t.Parallel()
cmd := func() *clibase.Cmd {
return &clibase.Cmd{
Use: "root",
HelpHandler: (func(i *clibase.Invocation) error {
i.Stdout.Write([]byte("abdracadabra"))
return nil
}),
Handler: (func(i *clibase.Invocation) error {
return xerrors.New("should not be called")
}),
}
}
t.Run("NoHandler", func(t *testing.T) {
t.Parallel()
c := cmd()
c.HelpHandler = nil
err := c.Invoke("--help").Run()
require.Error(t, err)
})
t.Run("Long", func(t *testing.T) {
t.Parallel()
inv := cmd().Invoke("--help")
stdio := fakeIO(inv)
err := inv.Run()
require.NoError(t, err)
require.Contains(t, stdio.Stdout.String(), "abdracadabra")
})
t.Run("Short", func(t *testing.T) {
t.Parallel()
inv := cmd().Invoke("-h")
stdio := fakeIO(inv)
err := inv.Run()
require.NoError(t, err)
require.Contains(t, stdio.Stdout.String(), "abdracadabra")
})
}
func TestCommand_SliceFlags(t *testing.T) {
t.Parallel()
cmd := func(want ...string) *clibase.Cmd {
var got []string
return &clibase.Cmd{
Use: "root",
Options: clibase.OptionSet{
{
Name: "arr",
Flag: "arr",
Default: "bad,bad,bad",
Value: clibase.StringArrayOf(&got),
},
},
Handler: (func(i *clibase.Invocation) error {
require.Equal(t, want, got)
return nil
}),
}
}
err := cmd("good", "good", "good").Invoke("--arr", "good", "--arr", "good", "--arr", "good").Run()
require.NoError(t, err)
err = cmd("bad", "bad", "bad").Invoke().Run()
require.NoError(t, err)
}
func TestCommand_EmptySlice(t *testing.T) {
t.Parallel()
cmd := func(want ...string) *clibase.Cmd {
var got []string
return &clibase.Cmd{
Use: "root",
Options: clibase.OptionSet{
{
Name: "arr",
Flag: "arr",
Default: "def,def,def",
Env: "ARR",
Value: clibase.StringArrayOf(&got),
},
},
Handler: (func(i *clibase.Invocation) error {
require.Equal(t, want, got)
return nil
}),
}
}
// Base-case, uses default.
err := cmd("def", "def", "def").Invoke().Run()
require.NoError(t, err)
// Empty-env uses default, too.
inv := cmd("def", "def", "def").Invoke()
inv.Environ.Set("ARR", "")
require.NoError(t, err)
// Reset to nothing at all via flag.
inv = cmd().Invoke("--arr", "")
inv.Environ.Set("ARR", "cant see")
err = inv.Run()
require.NoError(t, err)
// Reset to a specific value with flag.
inv = cmd("great").Invoke("--arr", "great")
inv.Environ.Set("ARR", "")
err = inv.Run()
require.NoError(t, err)
}
func TestCommand_DefaultsOverride(t *testing.T) {
t.Parallel()
test := func(name string, want string, fn func(t *testing.T, inv *clibase.Invocation)) {
t.Run(name, func(t *testing.T) {
t.Parallel()
var (
got string
config clibase.YAMLConfigPath
)
cmd := &clibase.Cmd{
Options: clibase.OptionSet{
{
Name: "url",
Flag: "url",
Default: "def.com",
Env: "URL",
Value: clibase.StringOf(&got),
YAML: "url",
},
{
Name: "config",
Flag: "config",
Default: "",
Value: &config,
},
},
Handler: (func(i *clibase.Invocation) error {
_, _ = fmt.Fprintf(i.Stdout, "%s", got)
return nil
}),
}
inv := cmd.Invoke()
stdio := fakeIO(inv)
fn(t, inv)
err := inv.Run()
require.NoError(t, err)
require.Equal(t, want, stdio.Stdout.String())
})
}
test("DefaultOverNothing", "def.com", func(t *testing.T, inv *clibase.Invocation) {})
test("FlagOverDefault", "good.com", func(t *testing.T, inv *clibase.Invocation) {
inv.Args = []string{"--url", "good.com"}
})
test("EnvOverDefault", "good.com", func(t *testing.T, inv *clibase.Invocation) {
inv.Environ.Set("URL", "good.com")
})
test("FlagOverEnv", "good.com", func(t *testing.T, inv *clibase.Invocation) {
inv.Environ.Set("URL", "bad.com")
inv.Args = []string{"--url", "good.com"}
})
test("FlagOverYAML", "good.com", func(t *testing.T, inv *clibase.Invocation) {
fi, err := os.CreateTemp(t.TempDir(), "config.yaml")
require.NoError(t, err)
defer fi.Close()
_, err = fi.WriteString("url: bad.com")
require.NoError(t, err)
inv.Args = []string{"--config", fi.Name(), "--url", "good.com"}
})
test("YAMLOverDefault", "good.com", func(t *testing.T, inv *clibase.Invocation) {
fi, err := os.CreateTemp(t.TempDir(), "config.yaml")
require.NoError(t, err)
defer fi.Close()
_, err = fi.WriteString("url: good.com")
require.NoError(t, err)
inv.Args = []string{"--config", fi.Name()}
})
}

View File

@ -1,76 +0,0 @@
package clibase
import "strings"
// name returns the name of the environment variable.
func envName(line string) string {
return strings.ToUpper(
strings.SplitN(line, "=", 2)[0],
)
}
// value returns the value of the environment variable.
func envValue(line string) string {
tokens := strings.SplitN(line, "=", 2)
if len(tokens) < 2 {
return ""
}
return tokens[1]
}
// Var represents a single environment variable of form
// NAME=VALUE.
type EnvVar struct {
Name string
Value string
}
type Environ []EnvVar
func (e Environ) ToOS() []string {
var env []string
for _, v := range e {
env = append(env, v.Name+"="+v.Value)
}
return env
}
func (e Environ) Lookup(name string) (string, bool) {
for _, v := range e {
if v.Name == name {
return v.Value, true
}
}
return "", false
}
func (e Environ) Get(name string) string {
v, _ := e.Lookup(name)
return v
}
func (e *Environ) Set(name, value string) {
for i, v := range *e {
if v.Name == name {
(*e)[i].Value = value
return
}
}
*e = append(*e, EnvVar{Name: name, Value: value})
}
// ParseEnviron returns all environment variables starting with
// prefix without said prefix.
func ParseEnviron(environ []string, prefix string) Environ {
var filtered []EnvVar
for _, line := range environ {
name := envName(line)
if strings.HasPrefix(name, prefix) {
filtered = append(filtered, EnvVar{
Name: strings.TrimPrefix(name, prefix),
Value: envValue(line),
})
}
}
return filtered
}

View File

@ -1,44 +0,0 @@
package clibase_test
import (
"reflect"
"testing"
"github.com/coder/coder/v2/cli/clibase"
)
func TestFilterNamePrefix(t *testing.T) {
t.Parallel()
type args struct {
environ []string
prefix string
}
tests := []struct {
name string
args args
want clibase.Environ
}{
{"empty", args{[]string{}, "SHIRE"}, nil},
{
"ONE",
args{
[]string{
"SHIRE_BRANDYBUCK=hmm",
},
"SHIRE_",
},
[]clibase.EnvVar{
{Name: "BRANDYBUCK", Value: "hmm"},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := clibase.ParseEnviron(tt.args.environ, tt.args.prefix); !reflect.DeepEqual(got, tt.want) {
t.Errorf("FilterNamePrefix() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,50 +0,0 @@
package clibase
import (
"net"
"strconv"
"github.com/pion/udp"
"golang.org/x/xerrors"
)
// Net abstracts CLI commands interacting with the operating system networking.
//
// At present, it covers opening local listening sockets, since doing this
// in testing is a challenge without flakes, since it's hard to pick a port we
// know a priori will be free.
type Net interface {
// Listen has the same semantics as `net.Listen` but also supports `udp`
Listen(network, address string) (net.Listener, error)
}
// osNet is an implementation that call the real OS for networking.
type osNet struct{}
func (osNet) Listen(network, address string) (net.Listener, error) {
switch network {
case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
return net.Listen(network, address)
case "udp":
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, xerrors.Errorf("split %q: %w", address, err)
}
var portInt int
portInt, err = strconv.Atoi(port)
if err != nil {
return nil, xerrors.Errorf("parse port %v from %q as int: %w", port, address, err)
}
// Use pion here so that we get a stream-style net.Conn listener, instead
// of a packet-oriented connection that can read and write to multiple
// addresses.
return udp.Listen(network, &net.UDPAddr{
IP: net.ParseIP(host),
Port: portInt,
})
default:
return nil, xerrors.Errorf("unknown listen network %q", network)
}
}

View File

@ -1,346 +0,0 @@
package clibase
import (
"bytes"
"encoding/json"
"os"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
type ValueSource string
const (
ValueSourceNone ValueSource = ""
ValueSourceFlag ValueSource = "flag"
ValueSourceEnv ValueSource = "env"
ValueSourceYAML ValueSource = "yaml"
ValueSourceDefault ValueSource = "default"
)
// Option is a configuration option for a CLI application.
type Option struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
// Required means this value must be set by some means. It requires
// `ValueSource != ValueSourceNone`
// If `Default` is set, then `Required` is ignored.
Required bool `json:"required,omitempty"`
// Flag is the long name of the flag used to configure this option. If unset,
// flag configuring is disabled.
Flag string `json:"flag,omitempty"`
// FlagShorthand is the one-character shorthand for the flag. If unset, no
// shorthand is used.
FlagShorthand string `json:"flag_shorthand,omitempty"`
// Env is the environment variable used to configure this option. If unset,
// environment configuring is disabled.
Env string `json:"env,omitempty"`
// YAML is the YAML key used to configure this option. If unset, YAML
// configuring is disabled.
YAML string `json:"yaml,omitempty"`
// Default is parsed into Value if set.
Default string `json:"default,omitempty"`
// Value includes the types listed in values.go.
Value pflag.Value `json:"value,omitempty"`
// Annotations enable extensions to clibase higher up in the stack. It's useful for
// help formatting and documentation generation.
Annotations Annotations `json:"annotations,omitempty"`
// Group is a group hierarchy that helps organize this option in help, configs
// and other documentation.
Group *Group `json:"group,omitempty"`
// UseInstead is a list of options that should be used instead of this one.
// The field is used to generate a deprecation warning.
UseInstead []Option `json:"use_instead,omitempty"`
Hidden bool `json:"hidden,omitempty"`
ValueSource ValueSource `json:"value_source,omitempty"`
}
// optionNoMethods is just a wrapper around Option so we can defer to the
// default json.Unmarshaler behavior.
type optionNoMethods Option
func (o *Option) UnmarshalJSON(data []byte) error {
// If an option has no values, we have no idea how to unmarshal it.
// So just discard the json data.
if o.Value == nil {
o.Value = &DiscardValue
}
return json.Unmarshal(data, (*optionNoMethods)(o))
}
func (o Option) YAMLPath() string {
if o.YAML == "" {
return ""
}
var gs []string
for _, g := range o.Group.Ancestry() {
gs = append(gs, g.YAML)
}
return strings.Join(append(gs, o.YAML), ".")
}
// OptionSet is a group of options that can be applied to a command.
type OptionSet []Option
// UnmarshalJSON implements json.Unmarshaler for OptionSets. Options have an
// interface Value type that cannot handle unmarshalling because the types cannot
// be inferred. Since it is a slice, instantiating the Options first does not
// help.
//
// However, we typically do instantiate the slice to have the correct types.
// So this unmarshaller will attempt to find the named option in the existing
// set, if it cannot, the value is discarded. If the option exists, the value
// is unmarshalled into the existing option, and replaces the existing option.
//
// The value is discarded if it's type cannot be inferred. This behavior just
// feels "safer", although it should never happen if the correct option set
// is passed in. The situation where this could occur is if a client and server
// are on different versions with different options.
func (optSet *OptionSet) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewBuffer(data))
// Should be a json array, so consume the starting open bracket.
t, err := dec.Token()
if err != nil {
return xerrors.Errorf("read array open bracket: %w", err)
}
if t != json.Delim('[') {
return xerrors.Errorf("expected array open bracket, got %q", t)
}
// As long as json elements exist, consume them. The counter is used for
// better errors.
var i int
OptionSetDecodeLoop:
for dec.More() {
var opt Option
// jValue is a placeholder value that allows us to capture the
// raw json for the value to attempt to unmarshal later.
var jValue jsonValue
opt.Value = &jValue
err := dec.Decode(&opt)
if err != nil {
return xerrors.Errorf("decode %d option: %w", i, err)
}
// This counter is used to contextualize errors to show which element of
// the array we failed to decode. It is only used in the error above, as
// if the above works, we can instead use the Option.Name which is more
// descriptive and useful. So increment here for the next decode.
i++
// Try to see if the option already exists in the option set.
// If it does, just update the existing option.
for optIndex, have := range *optSet {
if have.Name == opt.Name {
if jValue != nil {
err := json.Unmarshal(jValue, &(*optSet)[optIndex].Value)
if err != nil {
return xerrors.Errorf("decode option %q value: %w", have.Name, err)
}
// Set the opt's value
opt.Value = (*optSet)[optIndex].Value
} else {
// Hopefully the user passed empty values in the option set. There is no easy way
// to tell, and if we do not do this, it breaks json.Marshal if we do it again on
// this new option set.
opt.Value = (*optSet)[optIndex].Value
}
// Override the existing.
(*optSet)[optIndex] = opt
// Go to the next option to decode.
continue OptionSetDecodeLoop
}
}
// If the option doesn't exist, the value will be discarded.
// We do this because we cannot infer the type of the value.
opt.Value = DiscardValue
*optSet = append(*optSet, opt)
}
t, err = dec.Token()
if err != nil {
return xerrors.Errorf("read array close bracket: %w", err)
}
if t != json.Delim(']') {
return xerrors.Errorf("expected array close bracket, got %q", t)
}
return nil
}
// Add adds the given Options to the OptionSet.
func (optSet *OptionSet) Add(opts ...Option) {
*optSet = append(*optSet, opts...)
}
// Filter will only return options that match the given filter. (return true)
func (optSet OptionSet) Filter(filter func(opt Option) bool) OptionSet {
cpy := make(OptionSet, 0)
for _, opt := range optSet {
if filter(opt) {
cpy = append(cpy, opt)
}
}
return cpy
}
// FlagSet returns a pflag.FlagSet for the OptionSet.
func (optSet *OptionSet) FlagSet() *pflag.FlagSet {
if optSet == nil {
return &pflag.FlagSet{}
}
fs := pflag.NewFlagSet("", pflag.ContinueOnError)
for _, opt := range *optSet {
if opt.Flag == "" {
continue
}
var noOptDefValue string
{
no, ok := opt.Value.(NoOptDefValuer)
if ok {
noOptDefValue = no.NoOptDefValue()
}
}
val := opt.Value
if val == nil {
val = DiscardValue
}
fs.AddFlag(&pflag.Flag{
Name: opt.Flag,
Shorthand: opt.FlagShorthand,
Usage: opt.Description,
Value: val,
DefValue: "",
Changed: false,
Deprecated: "",
NoOptDefVal: noOptDefValue,
Hidden: opt.Hidden,
})
}
fs.Usage = func() {
_, _ = os.Stderr.WriteString("Override (*FlagSet).Usage() to print help text.\n")
}
return fs
}
// ParseEnv parses the given environment variables into the OptionSet.
// Use EnvsWithPrefix to filter out prefixes.
func (optSet *OptionSet) ParseEnv(vs []EnvVar) error {
if optSet == nil {
return nil
}
var merr *multierror.Error
// We parse environment variables first instead of using a nested loop to
// avoid N*M complexity when there are a lot of options and environment
// variables.
envs := make(map[string]string)
for _, v := range vs {
envs[v.Name] = v.Value
}
for i, opt := range *optSet {
if opt.Env == "" {
continue
}
envVal, ok := envs[opt.Env]
if !ok {
// Homebrew strips all environment variables that do not start with `HOMEBREW_`.
// This prevented using brew to invoke the Coder agent, because the environment
// variables to not get passed down.
//
// A customer wanted to use their custom tap inside a workspace, which was failing
// because the agent lacked the environment variables to authenticate with Git.
envVal, ok = envs[`HOMEBREW_`+opt.Env]
}
// Currently, empty values are treated as if the environment variable is
// unset. This behavior is technically not correct as there is now no
// way for a user to change a Default value to an empty string from
// the environment. Unfortunately, we have old configuration files
// that rely on the faulty behavior.
//
// TODO: We should remove this hack in May 2023, when deployments
// have had months to migrate to the new behavior.
if !ok || envVal == "" {
continue
}
(*optSet)[i].ValueSource = ValueSourceEnv
if err := opt.Value.Set(envVal); err != nil {
merr = multierror.Append(
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
)
}
}
return merr.ErrorOrNil()
}
// SetDefaults sets the default values for each Option, skipping values
// that already have a value source.
func (optSet *OptionSet) SetDefaults() error {
if optSet == nil {
return nil
}
var merr *multierror.Error
for i, opt := range *optSet {
// Skip values that may have already been set by the user.
if opt.ValueSource != ValueSourceNone {
continue
}
if opt.Default == "" {
continue
}
if opt.Value == nil {
merr = multierror.Append(
merr,
xerrors.Errorf(
"parse %q: no Value field set\nFull opt: %+v",
opt.Name, opt,
),
)
continue
}
(*optSet)[i].ValueSource = ValueSourceDefault
if err := opt.Value.Set(opt.Default); err != nil {
merr = multierror.Append(
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
)
}
}
return merr.ErrorOrNil()
}
// ByName returns the Option with the given name, or nil if no such option
// exists.
func (optSet *OptionSet) ByName(name string) *Option {
for i := range *optSet {
opt := &(*optSet)[i]
if opt.Name == name {
return opt
}
}
return nil
}

View File

@ -1,391 +0,0 @@
package clibase_test
import (
"bytes"
"encoding/json"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
)
func TestOptionSet_ParseFlags(t *testing.T) {
t.Parallel()
t.Run("SimpleString", func(t *testing.T) {
t.Parallel()
var workspaceName clibase.String
os := clibase.OptionSet{
clibase.Option{
Name: "Workspace Name",
Value: &workspaceName,
Flag: "workspace-name",
FlagShorthand: "n",
},
}
var err error
err = os.FlagSet().Parse([]string{"--workspace-name", "foo"})
require.NoError(t, err)
require.EqualValues(t, "foo", workspaceName)
err = os.FlagSet().Parse([]string{"-n", "f"})
require.NoError(t, err)
require.EqualValues(t, "f", workspaceName)
})
t.Run("StringArray", func(t *testing.T) {
t.Parallel()
var names clibase.StringArray
os := clibase.OptionSet{
clibase.Option{
Name: "name",
Value: &names,
Flag: "name",
FlagShorthand: "n",
},
}
err := os.SetDefaults()
require.NoError(t, err)
err = os.FlagSet().Parse([]string{"--name", "foo", "--name", "bar"})
require.NoError(t, err)
require.EqualValues(t, []string{"foo", "bar"}, names)
})
t.Run("ExtraFlags", func(t *testing.T) {
t.Parallel()
var workspaceName clibase.String
os := clibase.OptionSet{
clibase.Option{
Name: "Workspace Name",
Value: &workspaceName,
},
}
err := os.FlagSet().Parse([]string{"--some-unknown", "foo"})
require.Error(t, err)
})
t.Run("RegexValid", func(t *testing.T) {
t.Parallel()
var regexpString clibase.Regexp
os := clibase.OptionSet{
clibase.Option{
Name: "RegexpString",
Value: &regexpString,
Flag: "regexp-string",
},
}
err := os.FlagSet().Parse([]string{"--regexp-string", "$test^"})
require.NoError(t, err)
})
t.Run("RegexInvalid", func(t *testing.T) {
t.Parallel()
var regexpString clibase.Regexp
os := clibase.OptionSet{
clibase.Option{
Name: "RegexpString",
Value: &regexpString,
Flag: "regexp-string",
},
}
err := os.FlagSet().Parse([]string{"--regexp-string", "(("})
require.Error(t, err)
})
}
func TestOptionSet_ParseEnv(t *testing.T) {
t.Parallel()
t.Run("SimpleString", func(t *testing.T) {
t.Parallel()
var workspaceName clibase.String
os := clibase.OptionSet{
clibase.Option{
Name: "Workspace Name",
Value: &workspaceName,
Env: "WORKSPACE_NAME",
},
}
err := os.ParseEnv([]clibase.EnvVar{
{Name: "WORKSPACE_NAME", Value: "foo"},
})
require.NoError(t, err)
require.EqualValues(t, "foo", workspaceName)
})
t.Run("EmptyValue", func(t *testing.T) {
t.Parallel()
var workspaceName clibase.String
os := clibase.OptionSet{
clibase.Option{
Name: "Workspace Name",
Value: &workspaceName,
Default: "defname",
Env: "WORKSPACE_NAME",
},
}
err := os.SetDefaults()
require.NoError(t, err)
err = os.ParseEnv(clibase.ParseEnviron([]string{"CODER_WORKSPACE_NAME="}, "CODER_"))
require.NoError(t, err)
require.EqualValues(t, "defname", workspaceName)
})
t.Run("StringSlice", func(t *testing.T) {
t.Parallel()
var actual clibase.StringArray
expected := []string{"foo", "bar", "baz"}
os := clibase.OptionSet{
clibase.Option{
Name: "name",
Value: &actual,
Env: "NAMES",
},
}
err := os.SetDefaults()
require.NoError(t, err)
err = os.ParseEnv([]clibase.EnvVar{
{Name: "NAMES", Value: "foo,bar,baz"},
})
require.NoError(t, err)
require.EqualValues(t, expected, actual)
})
t.Run("StructMapStringString", func(t *testing.T) {
t.Parallel()
var actual clibase.Struct[map[string]string]
expected := map[string]string{"foo": "bar", "baz": "zap"}
os := clibase.OptionSet{
clibase.Option{
Name: "labels",
Value: &actual,
Env: "LABELS",
},
}
err := os.SetDefaults()
require.NoError(t, err)
err = os.ParseEnv([]clibase.EnvVar{
{Name: "LABELS", Value: `{"foo":"bar","baz":"zap"}`},
})
require.NoError(t, err)
require.EqualValues(t, expected, actual.Value)
})
t.Run("Homebrew", func(t *testing.T) {
t.Parallel()
var agentToken clibase.String
os := clibase.OptionSet{
clibase.Option{
Name: "Agent Token",
Value: &agentToken,
Env: "AGENT_TOKEN",
},
}
err := os.ParseEnv([]clibase.EnvVar{
{Name: "HOMEBREW_AGENT_TOKEN", Value: "foo"},
})
require.NoError(t, err)
require.EqualValues(t, "foo", agentToken)
})
}
func TestOptionSet_JsonMarshal(t *testing.T) {
t.Parallel()
// This unit test ensures if the source optionset is missing the option
// and cannot determine the type, it will not panic. The unmarshal will
// succeed with a best effort.
t.Run("MissingSrcOption", func(t *testing.T) {
t.Parallel()
var str clibase.String = "something"
var arr clibase.StringArray = []string{"foo", "bar"}
opts := clibase.OptionSet{
clibase.Option{
Name: "StringOpt",
Value: &str,
},
clibase.Option{
Name: "ArrayOpt",
Value: &arr,
},
}
data, err := json.Marshal(opts)
require.NoError(t, err, "marshal option set")
tgt := clibase.OptionSet{}
err = json.Unmarshal(data, &tgt)
require.NoError(t, err, "unmarshal option set")
for i := range opts {
compareOptionsExceptValues(t, opts[i], tgt[i])
require.Empty(t, tgt[i].Value.String(), "unknown value types are empty")
}
})
t.Run("RegexCase", func(t *testing.T) {
t.Parallel()
val := clibase.Regexp(*regexp.MustCompile(".*"))
opts := clibase.OptionSet{
clibase.Option{
Name: "Regex",
Value: &val,
Default: ".*",
},
}
data, err := json.Marshal(opts)
require.NoError(t, err, "marshal option set")
var foundVal clibase.Regexp
newOpts := clibase.OptionSet{
clibase.Option{
Name: "Regex",
Value: &foundVal,
},
}
err = json.Unmarshal(data, &newOpts)
require.NoError(t, err, "unmarshal option set")
require.EqualValues(t, opts[0].Value.String(), newOpts[0].Value.String())
})
t.Run("AllValues", func(t *testing.T) {
t.Parallel()
vals := coderdtest.DeploymentValues(t)
opts := vals.Options()
sources := []clibase.ValueSource{
clibase.ValueSourceNone,
clibase.ValueSourceFlag,
clibase.ValueSourceEnv,
clibase.ValueSourceYAML,
clibase.ValueSourceDefault,
}
for i := range opts {
opts[i].ValueSource = sources[i%len(sources)]
}
data, err := json.Marshal(opts)
require.NoError(t, err, "marshal option set")
newOpts := (&codersdk.DeploymentValues{}).Options()
err = json.Unmarshal(data, &newOpts)
require.NoError(t, err, "unmarshal option set")
for i := range opts {
exp := opts[i]
found := newOpts[i]
compareOptionsExceptValues(t, exp, found)
compareValues(t, exp, found)
}
thirdOpts := (&codersdk.DeploymentValues{}).Options()
data, err = json.Marshal(newOpts)
require.NoError(t, err, "marshal option set")
err = json.Unmarshal(data, &thirdOpts)
require.NoError(t, err, "unmarshal option set")
// Compare to the original opts again
for i := range opts {
exp := opts[i]
found := thirdOpts[i]
compareOptionsExceptValues(t, exp, found)
compareValues(t, exp, found)
}
})
}
func compareOptionsExceptValues(t *testing.T, exp, found clibase.Option) {
t.Helper()
require.Equalf(t, exp.Name, found.Name, "option name %q", exp.Name)
require.Equalf(t, exp.Description, found.Description, "option description %q", exp.Name)
require.Equalf(t, exp.Required, found.Required, "option required %q", exp.Name)
require.Equalf(t, exp.Flag, found.Flag, "option flag %q", exp.Name)
require.Equalf(t, exp.FlagShorthand, found.FlagShorthand, "option flag shorthand %q", exp.Name)
require.Equalf(t, exp.Env, found.Env, "option env %q", exp.Name)
require.Equalf(t, exp.YAML, found.YAML, "option yaml %q", exp.Name)
require.Equalf(t, exp.Default, found.Default, "option default %q", exp.Name)
require.Equalf(t, exp.ValueSource, found.ValueSource, "option value source %q", exp.Name)
require.Equalf(t, exp.Hidden, found.Hidden, "option hidden %q", exp.Name)
require.Equalf(t, exp.Annotations, found.Annotations, "option annotations %q", exp.Name)
require.Equalf(t, exp.Group, found.Group, "option group %q", exp.Name)
// UseInstead is the same comparison problem, just check the length
require.Equalf(t, len(exp.UseInstead), len(found.UseInstead), "option use instead %q", exp.Name)
}
func compareValues(t *testing.T, exp, found clibase.Option) {
t.Helper()
if (exp.Value == nil || found.Value == nil) || (exp.Value.String() != found.Value.String() && found.Value.String() == "") {
// If the string values are different, this can be a "nil" issue.
// So only run this case if the found string is the empty string.
// We use MarshalYAML for struct strings, and it will return an
// empty string '""' for nil slices/maps/etc.
// So use json to compare.
expJSON, err := json.Marshal(exp.Value)
require.NoError(t, err, "marshal")
foundJSON, err := json.Marshal(found.Value)
require.NoError(t, err, "marshal")
expJSON = normalizeJSON(expJSON)
foundJSON = normalizeJSON(foundJSON)
assert.Equalf(t, string(expJSON), string(foundJSON), "option value %q", exp.Name)
} else {
assert.Equal(t,
exp.Value.String(),
found.Value.String(),
"option value %q", exp.Name)
}
}
// normalizeJSON handles the fact that an empty map/slice is not the same
// as a nil empty/slice. For our purposes, they are the same.
func normalizeJSON(data []byte) []byte {
if bytes.Equal(data, []byte("[]")) || bytes.Equal(data, []byte("{}")) {
return []byte("null")
}
return data
}

View File

@ -1,593 +0,0 @@
package clibase
import (
"encoding/csv"
"encoding/json"
"fmt"
"net"
"net/url"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
// NoOptDefValuer describes behavior when no
// option is passed into the flag.
//
// This is useful for boolean or otherwise binary flags.
type NoOptDefValuer interface {
NoOptDefValue() string
}
// Validator is a wrapper around a pflag.Value that allows for validation
// of the value after or before it has been set.
type Validator[T pflag.Value] struct {
Value T
// validate is called after the value is set.
validate func(T) error
}
func Validate[T pflag.Value](opt T, validate func(value T) error) *Validator[T] {
return &Validator[T]{Value: opt, validate: validate}
}
func (i *Validator[T]) String() string {
return i.Value.String()
}
func (i *Validator[T]) Set(input string) error {
err := i.Value.Set(input)
if err != nil {
return err
}
if i.validate != nil {
err = i.validate(i.Value)
if err != nil {
return err
}
}
return nil
}
func (i *Validator[T]) Type() string {
return i.Value.Type()
}
func (i *Validator[T]) MarshalYAML() (interface{}, error) {
m, ok := any(i.Value).(yaml.Marshaler)
if !ok {
return i.Value, nil
}
return m.MarshalYAML()
}
func (i *Validator[T]) UnmarshalYAML(n *yaml.Node) error {
return n.Decode(i.Value)
}
func (i *Validator[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(i.Value)
}
func (i *Validator[T]) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, i.Value)
}
func (i *Validator[T]) Underlying() pflag.Value { return i.Value }
// values.go contains a standard set of value types that can be used as
// Option Values.
type Int64 int64
func Int64Of(i *int64) *Int64 {
return (*Int64)(i)
}
func (i *Int64) Set(s string) error {
ii, err := strconv.ParseInt(s, 10, 64)
*i = Int64(ii)
return err
}
func (i Int64) Value() int64 {
return int64(i)
}
func (i Int64) String() string {
return strconv.Itoa(int(i))
}
func (Int64) Type() string {
return "int"
}
type Bool bool
func BoolOf(b *bool) *Bool {
return (*Bool)(b)
}
func (b *Bool) Set(s string) error {
if s == "" {
*b = Bool(false)
return nil
}
bb, err := strconv.ParseBool(s)
*b = Bool(bb)
return err
}
func (*Bool) NoOptDefValue() string {
return "true"
}
func (b Bool) String() string {
return strconv.FormatBool(bool(b))
}
func (b Bool) Value() bool {
return bool(b)
}
func (Bool) Type() string {
return "bool"
}
type String string
func StringOf(s *string) *String {
return (*String)(s)
}
func (*String) NoOptDefValue() string {
return ""
}
func (s *String) Set(v string) error {
*s = String(v)
return nil
}
func (s String) String() string {
return string(s)
}
func (s String) Value() string {
return string(s)
}
func (String) Type() string {
return "string"
}
var _ pflag.SliceValue = &StringArray{}
// StringArray is a slice of strings that implements pflag.Value and pflag.SliceValue.
type StringArray []string
func StringArrayOf(ss *[]string) *StringArray {
return (*StringArray)(ss)
}
func (s *StringArray) Append(v string) error {
*s = append(*s, v)
return nil
}
func (s *StringArray) Replace(vals []string) error {
*s = vals
return nil
}
func (s *StringArray) GetSlice() []string {
return *s
}
func readAsCSV(v string) ([]string, error) {
return csv.NewReader(strings.NewReader(v)).Read()
}
func writeAsCSV(vals []string) string {
var sb strings.Builder
err := csv.NewWriter(&sb).Write(vals)
if err != nil {
return fmt.Sprintf("error: %s", err)
}
return sb.String()
}
func (s *StringArray) Set(v string) error {
if v == "" {
*s = nil
return nil
}
ss, err := readAsCSV(v)
if err != nil {
return err
}
*s = append(*s, ss...)
return nil
}
func (s StringArray) String() string {
return writeAsCSV([]string(s))
}
func (s StringArray) Value() []string {
return []string(s)
}
func (StringArray) Type() string {
return "string-array"
}
type Duration time.Duration
func DurationOf(d *time.Duration) *Duration {
return (*Duration)(d)
}
func (d *Duration) Set(v string) error {
dd, err := time.ParseDuration(v)
*d = Duration(dd)
return err
}
func (d *Duration) Value() time.Duration {
return time.Duration(*d)
}
func (d *Duration) String() string {
return time.Duration(*d).String()
}
func (Duration) Type() string {
return "duration"
}
func (d *Duration) MarshalYAML() (interface{}, error) {
return yaml.Node{
Kind: yaml.ScalarNode,
Value: d.String(),
}, nil
}
func (d *Duration) UnmarshalYAML(n *yaml.Node) error {
return d.Set(n.Value)
}
type URL url.URL
func URLOf(u *url.URL) *URL {
return (*URL)(u)
}
func (u *URL) Set(v string) error {
uu, err := url.Parse(v)
if err != nil {
return err
}
*u = URL(*uu)
return nil
}
func (u *URL) String() string {
uu := url.URL(*u)
return uu.String()
}
func (u *URL) MarshalYAML() (interface{}, error) {
return yaml.Node{
Kind: yaml.ScalarNode,
Value: u.String(),
}, nil
}
func (u *URL) UnmarshalYAML(n *yaml.Node) error {
return u.Set(n.Value)
}
func (u *URL) MarshalJSON() ([]byte, error) {
return json.Marshal(u.String())
}
func (u *URL) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
return u.Set(s)
}
func (*URL) Type() string {
return "url"
}
func (u *URL) Value() *url.URL {
return (*url.URL)(u)
}
// HostPort is a host:port pair.
type HostPort struct {
Host string
Port string
}
func (hp *HostPort) Set(v string) error {
if v == "" {
return xerrors.Errorf("must not be empty")
}
var err error
hp.Host, hp.Port, err = net.SplitHostPort(v)
return err
}
func (hp *HostPort) String() string {
if hp.Host == "" && hp.Port == "" {
return ""
}
// Warning: net.JoinHostPort must be used over concatenation to support
// IPv6 addresses.
return net.JoinHostPort(hp.Host, hp.Port)
}
func (hp *HostPort) MarshalJSON() ([]byte, error) {
return json.Marshal(hp.String())
}
func (hp *HostPort) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
if s == "" {
hp.Host = ""
hp.Port = ""
return nil
}
return hp.Set(s)
}
func (hp *HostPort) MarshalYAML() (interface{}, error) {
return yaml.Node{
Kind: yaml.ScalarNode,
Value: hp.String(),
}, nil
}
func (hp *HostPort) UnmarshalYAML(n *yaml.Node) error {
return hp.Set(n.Value)
}
func (*HostPort) Type() string {
return "host:port"
}
var (
_ yaml.Marshaler = new(Struct[struct{}])
_ yaml.Unmarshaler = new(Struct[struct{}])
)
// Struct is a special value type that encodes an arbitrary struct.
// It implements the flag.Value interface, but in general these values should
// only be accepted via config for ergonomics.
//
// The string encoding type is YAML.
type Struct[T any] struct {
Value T
}
//nolint:revive
func (s *Struct[T]) Set(v string) error {
return yaml.Unmarshal([]byte(v), &s.Value)
}
//nolint:revive
func (s *Struct[T]) String() string {
byt, err := yaml.Marshal(s.Value)
if err != nil {
return "decode failed: " + err.Error()
}
return string(byt)
}
// nolint:revive
func (s *Struct[T]) MarshalYAML() (interface{}, error) {
var n yaml.Node
err := n.Encode(s.Value)
if err != nil {
return nil, err
}
return n, nil
}
// nolint:revive
func (s *Struct[T]) UnmarshalYAML(n *yaml.Node) error {
// HACK: for compatibility with flags, we use nil slices instead of empty
// slices. In most cases, nil slices and empty slices are treated
// the same, so this behavior may be removed at some point.
if typ := reflect.TypeOf(s.Value); typ.Kind() == reflect.Slice && len(n.Content) == 0 {
reflect.ValueOf(&s.Value).Elem().Set(reflect.Zero(typ))
return nil
}
return n.Decode(&s.Value)
}
//nolint:revive
func (s *Struct[T]) Type() string {
return fmt.Sprintf("struct[%T]", s.Value)
}
// nolint:revive
func (s *Struct[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Value)
}
// nolint:revive
func (s *Struct[T]) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, &s.Value)
}
// DiscardValue does nothing but implements the pflag.Value interface.
// It's useful in cases where you want to accept an option, but access the
// underlying value directly instead of through the Option methods.
var DiscardValue discardValue
type discardValue struct{}
func (discardValue) Set(string) error {
return nil
}
func (discardValue) String() string {
return ""
}
func (discardValue) Type() string {
return "discard"
}
func (discardValue) UnmarshalJSON([]byte) error {
return nil
}
// jsonValue is intentionally not exported. It is just used to store the raw JSON
// data for a value to defer it's unmarshal. It implements the pflag.Value to be
// usable in an Option.
type jsonValue json.RawMessage
func (jsonValue) Set(string) error {
return xerrors.Errorf("json value is read-only")
}
func (jsonValue) String() string {
return ""
}
func (jsonValue) Type() string {
return "json"
}
func (j *jsonValue) UnmarshalJSON(data []byte) error {
if j == nil {
return xerrors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*j = append((*j)[0:0], data...)
return nil
}
var _ pflag.Value = (*Enum)(nil)
type Enum struct {
Choices []string
Value *string
}
func EnumOf(v *string, choices ...string) *Enum {
return &Enum{
Choices: choices,
Value: v,
}
}
func (e *Enum) Set(v string) error {
for _, c := range e.Choices {
if v == c {
*e.Value = v
return nil
}
}
return xerrors.Errorf("invalid choice: %s, should be one of %v", v, e.Choices)
}
func (e *Enum) Type() string {
return fmt.Sprintf("enum[%v]", strings.Join(e.Choices, "\\|"))
}
func (e *Enum) String() string {
return *e.Value
}
type Regexp regexp.Regexp
func (r *Regexp) MarshalJSON() ([]byte, error) {
return json.Marshal(r.String())
}
func (r *Regexp) UnmarshalJSON(data []byte) error {
var source string
err := json.Unmarshal(data, &source)
if err != nil {
return err
}
exp, err := regexp.Compile(source)
if err != nil {
return xerrors.Errorf("invalid regex expression: %w", err)
}
*r = Regexp(*exp)
return nil
}
func (r *Regexp) MarshalYAML() (interface{}, error) {
return yaml.Node{
Kind: yaml.ScalarNode,
Value: r.String(),
}, nil
}
func (r *Regexp) UnmarshalYAML(n *yaml.Node) error {
return r.Set(n.Value)
}
func (r *Regexp) Set(v string) error {
exp, err := regexp.Compile(v)
if err != nil {
return xerrors.Errorf("invalid regex expression: %w", err)
}
*r = Regexp(*exp)
return nil
}
func (r Regexp) String() string {
return r.Value().String()
}
func (r *Regexp) Value() *regexp.Regexp {
if r == nil {
return nil
}
return (*regexp.Regexp)(r)
}
func (Regexp) Type() string {
return "regexp"
}
var _ pflag.Value = (*YAMLConfigPath)(nil)
// YAMLConfigPath is a special value type that encodes a path to a YAML
// configuration file where options are read from.
type YAMLConfigPath string
func (p *YAMLConfigPath) Set(v string) error {
*p = YAMLConfigPath(v)
return nil
}
func (p *YAMLConfigPath) String() string {
return string(*p)
}
func (*YAMLConfigPath) Type() string {
return "yaml-config-path"
}

View File

@ -1,299 +0,0 @@
package clibase
import (
"errors"
"fmt"
"strings"
"github.com/mitchellh/go-wordwrap"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
var (
_ yaml.Marshaler = new(OptionSet)
_ yaml.Unmarshaler = new(OptionSet)
)
// deepMapNode returns the mapping node at the given path,
// creating it if it doesn't exist.
func deepMapNode(n *yaml.Node, path []string, headComment string) *yaml.Node {
if len(path) == 0 {
return n
}
// Name is every two nodes.
for i := 0; i < len(n.Content)-1; i += 2 {
if n.Content[i].Value == path[0] {
// Found matching name, recurse.
return deepMapNode(n.Content[i+1], path[1:], headComment)
}
}
// Not found, create it.
nameNode := yaml.Node{
Kind: yaml.ScalarNode,
Value: path[0],
HeadComment: headComment,
}
valueNode := yaml.Node{
Kind: yaml.MappingNode,
}
n.Content = append(n.Content, &nameNode)
n.Content = append(n.Content, &valueNode)
return deepMapNode(&valueNode, path[1:], headComment)
}
// MarshalYAML converts the option set to a YAML node, that can be
// converted into bytes via yaml.Marshal.
//
// The node is returned to enable post-processing higher up in
// the stack.
//
// It is isomorphic with FromYAML.
func (optSet *OptionSet) MarshalYAML() (any, error) {
root := yaml.Node{
Kind: yaml.MappingNode,
}
for _, opt := range *optSet {
if opt.YAML == "" {
continue
}
defValue := opt.Default
if defValue == "" {
defValue = "<unset>"
}
comment := wordwrap.WrapString(
fmt.Sprintf("%s\n(default: %s, type: %s)", opt.Description, defValue, opt.Value.Type()),
80,
)
nameNode := yaml.Node{
Kind: yaml.ScalarNode,
Value: opt.YAML,
HeadComment: comment,
}
_, isValidator := opt.Value.(interface{ Underlying() pflag.Value })
var valueNode yaml.Node
if opt.Value == nil {
valueNode = yaml.Node{
Kind: yaml.ScalarNode,
Value: "null",
}
} else if m, ok := opt.Value.(yaml.Marshaler); ok && !isValidator {
// Validators do a wrap, and should be handled by the else statement.
v, err := m.MarshalYAML()
if err != nil {
return nil, xerrors.Errorf(
"marshal %q: %w", opt.Name, err,
)
}
valueNode, ok = v.(yaml.Node)
if !ok {
return nil, xerrors.Errorf(
"marshal %q: unexpected underlying type %T",
opt.Name, v,
)
}
} else {
// The all-other types case.
//
// A bit of a hack, we marshal and then unmarshal to get
// the underlying node.
byt, err := yaml.Marshal(opt.Value)
if err != nil {
return nil, xerrors.Errorf(
"marshal %q: %w", opt.Name, err,
)
}
var docNode yaml.Node
err = yaml.Unmarshal(byt, &docNode)
if err != nil {
return nil, xerrors.Errorf(
"unmarshal %q: %w", opt.Name, err,
)
}
if len(docNode.Content) != 1 {
return nil, xerrors.Errorf(
"unmarshal %q: expected one node, got %d",
opt.Name, len(docNode.Content),
)
}
valueNode = *docNode.Content[0]
}
var group []string
for _, g := range opt.Group.Ancestry() {
if g.YAML == "" {
return nil, xerrors.Errorf(
"group yaml name is empty for %q, groups: %+v",
opt.Name,
opt.Group,
)
}
group = append(group, g.YAML)
}
var groupDesc string
if opt.Group != nil {
groupDesc = wordwrap.WrapString(opt.Group.Description, 80)
}
parentValueNode := deepMapNode(
&root, group,
groupDesc,
)
parentValueNode.Content = append(
parentValueNode.Content,
&nameNode,
&valueNode,
)
}
return &root, nil
}
// mapYAMLNodes converts parent into a map with keys of form "group.subgroup.option"
// and values as the corresponding YAML nodes.
func mapYAMLNodes(parent *yaml.Node) (map[string]*yaml.Node, error) {
if parent.Kind != yaml.MappingNode {
return nil, xerrors.Errorf("expected mapping node, got type %v", parent.Kind)
}
if len(parent.Content)%2 != 0 {
return nil, xerrors.Errorf("expected an even number of k/v pairs, got %d", len(parent.Content))
}
var (
key string
m = make(map[string]*yaml.Node, len(parent.Content)/2)
merr error
)
for i, child := range parent.Content {
if i%2 == 0 {
if child.Kind != yaml.ScalarNode {
// We immediately because the rest of the code is bound to fail
// if we don't know to expect a key or a value.
return nil, xerrors.Errorf("expected scalar node for key, got type %v", child.Kind)
}
key = child.Value
continue
}
// We don't know if this is a grouped simple option or complex option,
// so we store both "key" and "group.key". Since we're storing pointers,
// the additional memory is of little concern.
m[key] = child
if child.Kind != yaml.MappingNode {
continue
}
sub, err := mapYAMLNodes(child)
if err != nil {
merr = errors.Join(merr, xerrors.Errorf("mapping node %q: %w", key, err))
continue
}
for k, v := range sub {
m[key+"."+k] = v
}
}
return m, nil
}
func (o *Option) setFromYAMLNode(n *yaml.Node) error {
o.ValueSource = ValueSourceYAML
if um, ok := o.Value.(yaml.Unmarshaler); ok {
return um.UnmarshalYAML(n)
}
switch n.Kind {
case yaml.ScalarNode:
return o.Value.Set(n.Value)
case yaml.SequenceNode:
// We treat empty values as nil for consistency with other option
// mechanisms.
if len(n.Content) == 0 {
o.Value = nil
return nil
}
return n.Decode(o.Value)
case yaml.MappingNode:
return xerrors.Errorf("mapping nodes must implement yaml.Unmarshaler")
default:
return xerrors.Errorf("unexpected node kind %v", n.Kind)
}
}
// UnmarshalYAML converts the given YAML node into the option set.
// It is isomorphic with ToYAML.
func (optSet *OptionSet) UnmarshalYAML(rootNode *yaml.Node) error {
// The rootNode will be a DocumentNode if it's read from a file. We do
// not support multiple documents in a single file.
if rootNode.Kind == yaml.DocumentNode {
if len(rootNode.Content) != 1 {
return xerrors.Errorf("expected one node in document, got %d", len(rootNode.Content))
}
rootNode = rootNode.Content[0]
}
yamlNodes, err := mapYAMLNodes(rootNode)
if err != nil {
return xerrors.Errorf("mapping nodes: %w", err)
}
matchedNodes := make(map[string]*yaml.Node, len(yamlNodes))
var merr error
for i := range *optSet {
opt := &(*optSet)[i]
if opt.YAML == "" {
continue
}
var group []string
for _, g := range opt.Group.Ancestry() {
if g.YAML == "" {
return xerrors.Errorf(
"group yaml name is empty for %q, groups: %+v",
opt.Name,
opt.Group,
)
}
group = append(group, g.YAML)
delete(yamlNodes, strings.Join(group, "."))
}
key := strings.Join(append(group, opt.YAML), ".")
node, ok := yamlNodes[key]
if !ok {
continue
}
matchedNodes[key] = node
if opt.ValueSource != ValueSourceNone {
continue
}
if err := opt.setFromYAMLNode(node); err != nil {
merr = errors.Join(merr, xerrors.Errorf("setting %q: %w", opt.YAML, err))
}
}
// Remove all matched nodes and their descendants from yamlNodes so we
// can accurately report unknown options.
for k := range yamlNodes {
var key string
for _, part := range strings.Split(k, ".") {
if key != "" {
key += "."
}
key += part
if _, ok := matchedNodes[key]; ok {
delete(yamlNodes, k)
}
}
}
for k := range yamlNodes {
merr = errors.Join(merr, xerrors.Errorf("unknown option %q", k))
}
return merr
}

View File

@ -1,202 +0,0 @@
package clibase_test
import (
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
"github.com/coder/coder/v2/cli/clibase"
)
func TestOptionSet_YAML(t *testing.T) {
t.Parallel()
t.Run("RequireKey", func(t *testing.T) {
t.Parallel()
var workspaceName clibase.String
os := clibase.OptionSet{
clibase.Option{
Name: "Workspace Name",
Value: &workspaceName,
Default: "billie",
},
}
node, err := os.MarshalYAML()
require.NoError(t, err)
require.Len(t, node.(*yaml.Node).Content, 0)
})
t.Run("SimpleString", func(t *testing.T) {
t.Parallel()
var workspaceName clibase.String
os := clibase.OptionSet{
clibase.Option{
Name: "Workspace Name",
Value: &workspaceName,
Default: "billie",
Description: "The workspace's name.",
Group: &clibase.Group{YAML: "names"},
YAML: "workspaceName",
},
}
err := os.SetDefaults()
require.NoError(t, err)
n, err := os.MarshalYAML()
require.NoError(t, err)
// Visually inspect for now.
byt, err := yaml.Marshal(n)
require.NoError(t, err)
t.Logf("Raw YAML:\n%s", string(byt))
})
}
func TestOptionSet_YAMLUnknownOptions(t *testing.T) {
t.Parallel()
os := clibase.OptionSet{
{
Name: "Workspace Name",
Default: "billie",
Description: "The workspace's name.",
YAML: "workspaceName",
Value: new(clibase.String),
},
}
const yamlDoc = `something: else`
err := yaml.Unmarshal([]byte(yamlDoc), &os)
require.Error(t, err)
require.Empty(t, os[0].Value.String())
os[0].YAML = "something"
err = yaml.Unmarshal([]byte(yamlDoc), &os)
require.NoError(t, err)
require.Equal(t, "else", os[0].Value.String())
}
// TestOptionSet_YAMLIsomorphism tests that the YAML representations of an
// OptionSet converts to the same OptionSet when read back in.
func TestOptionSet_YAMLIsomorphism(t *testing.T) {
t.Parallel()
// This is used to form a generic.
//nolint:unused
type kid struct {
Name string `yaml:"name"`
Age int `yaml:"age"`
}
for _, tc := range []struct {
name string
os clibase.OptionSet
zeroValue func() pflag.Value
}{
{
name: "SimpleString",
os: clibase.OptionSet{
{
Name: "Workspace Name",
Default: "billie",
Description: "The workspace's name.",
Group: &clibase.Group{YAML: "names"},
YAML: "workspaceName",
},
},
zeroValue: func() pflag.Value {
return clibase.StringOf(new(string))
},
},
{
name: "Array",
os: clibase.OptionSet{
{
YAML: "names",
Default: "jill,jack,joan",
},
},
zeroValue: func() pflag.Value {
return clibase.StringArrayOf(&[]string{})
},
},
{
name: "ComplexObject",
os: clibase.OptionSet{
{
YAML: "kids",
Default: `- name: jill
age: 12
- name: jack
age: 13`,
},
},
zeroValue: func() pflag.Value {
return &clibase.Struct[[]kid]{}
},
},
{
name: "DeepGroup",
os: clibase.OptionSet{
{
YAML: "names",
Default: "jill,jack,joan",
Group: &clibase.Group{YAML: "kids", Parent: &clibase.Group{YAML: "family"}},
},
},
zeroValue: func() pflag.Value {
return clibase.StringArrayOf(&[]string{})
},
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Set initial values.
for i := range tc.os {
tc.os[i].Value = tc.zeroValue()
}
err := tc.os.SetDefaults()
require.NoError(t, err)
y, err := tc.os.MarshalYAML()
require.NoError(t, err)
toByt, err := yaml.Marshal(y)
require.NoError(t, err)
t.Logf("Raw YAML:\n%s", string(toByt))
var y2 yaml.Node
err = yaml.Unmarshal(toByt, &y2)
require.NoError(t, err)
os2 := slices.Clone(tc.os)
for i := range os2 {
os2[i].Value = tc.zeroValue()
os2[i].ValueSource = clibase.ValueSourceNone
}
// os2 values should be zeroed whereas tc.os should be
// set to defaults.
// This check makes sure we aren't mixing pointers.
require.NotEqual(t, tc.os, os2)
err = os2.UnmarshalYAML(&y2)
require.NoError(t, err)
want := tc.os
for i := range want {
want[i].ValueSource = clibase.ValueSourceYAML
}
require.Equal(t, tc.os, os2)
})
}
}

View File

@ -14,9 +14,9 @@ import (
"cdr.dev/slog/sloggers/sloghuman"
"cdr.dev/slog/sloggers/slogjson"
"cdr.dev/slog/sloggers/slogstackdriver"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
type (
@ -86,7 +86,7 @@ func FromDeploymentValues(vals *codersdk.DeploymentValues) Option {
}
}
func (b *Builder) Build(inv *clibase.Invocation) (log slog.Logger, closeLog func(), err error) {
func (b *Builder) Build(inv *serpent.Invocation) (log slog.Logger, closeLog func(), err error) {
var (
sinks = []slog.Sink{}
closers = []func() error{}

View File

@ -8,10 +8,10 @@ import (
"strings"
"testing"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/clilog"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -23,7 +23,7 @@ func TestBuilder(t *testing.T) {
t.Run("NoConfiguration", func(t *testing.T) {
t.Parallel()
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "test",
Handler: testHandler(t),
}
@ -35,7 +35,7 @@ func TestBuilder(t *testing.T) {
t.Parallel()
tempFile := filepath.Join(t.TempDir(), "test.log")
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "test",
Handler: testHandler(t,
clilog.WithHuman(tempFile),
@ -51,7 +51,7 @@ func TestBuilder(t *testing.T) {
t.Parallel()
tempFile := filepath.Join(t.TempDir(), "test.log")
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "test",
Handler: testHandler(t,
clilog.WithHuman(tempFile),
@ -68,7 +68,7 @@ func TestBuilder(t *testing.T) {
t.Parallel()
tempFile := filepath.Join(t.TempDir(), "test.log")
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "test",
Handler: testHandler(t, clilog.WithHuman(tempFile)),
}
@ -81,7 +81,7 @@ func TestBuilder(t *testing.T) {
t.Parallel()
tempFile := filepath.Join(t.TempDir(), "test.log")
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "test",
Handler: testHandler(t, clilog.WithJSON(tempFile), clilog.WithVerbose()),
}
@ -107,7 +107,7 @@ func TestBuilder(t *testing.T) {
// Use the default deployment values.
dv := coderdtest.DeploymentValues(t)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "test",
Handler: testHandler(t, clilog.FromDeploymentValues(dv)),
}
@ -127,15 +127,15 @@ func TestBuilder(t *testing.T) {
dv := &codersdk.DeploymentValues{
Logging: codersdk.LoggingConfig{
Filter: []string{"foo", "baz"},
Human: clibase.String(tempFile),
JSON: clibase.String(tempJSON),
Human: serpent.String(tempFile),
JSON: serpent.String(tempJSON),
},
Verbose: true,
Trace: codersdk.TraceConfig{
Enable: true,
},
}
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "test",
Handler: testHandler(t, clilog.FromDeploymentValues(dv)),
}
@ -150,9 +150,9 @@ func TestBuilder(t *testing.T) {
t.Parallel()
tempFile := filepath.Join(t.TempDir(), "doesnotexist", "test.log")
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "test",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
logger, closeLog, err := clilog.New(
clilog.WithFilter("foo", "baz"),
clilog.WithHuman(tempFile),
@ -181,10 +181,10 @@ var (
filterLog = "this is an important debug message you want to see"
)
func testHandler(t testing.TB, opts ...clilog.Option) clibase.HandlerFunc {
func testHandler(t testing.TB, opts ...clilog.Option) serpent.HandlerFunc {
t.Helper()
return func(inv *clibase.Invocation) error {
return func(inv *serpent.Invocation) error {
logger, closeLog, err := clilog.New(opts...).Build(inv)
if err != nil {
return err

View File

@ -20,16 +20,16 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
// New creates a CLI instance with a configuration pointed to a
// temporary testing directory.
func New(t testing.TB, args ...string) (*clibase.Invocation, config.Root) {
func New(t testing.TB, args ...string) (*serpent.Invocation, config.Root) {
var root cli.RootCmd
cmd, err := root.Command(root.AGPL())
@ -56,15 +56,15 @@ func (l *logWriter) Write(p []byte) (n int, err error) {
}
func NewWithCommand(
t testing.TB, cmd *clibase.Cmd, args ...string,
) (*clibase.Invocation, config.Root) {
t testing.TB, cmd *serpent.Cmd, args ...string,
) (*serpent.Invocation, config.Root) {
configDir := config.Root(t.TempDir())
// I really would like to fail test on error logs, but realistically, turning on by default
// in all our CLI tests is going to create a lot of flaky noise.
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).
Leveled(slog.LevelDebug).
Named("cli")
i := &clibase.Invocation{
i := &serpent.Invocation{
Command: cmd,
Args: append([]string{"--global-config", string(configDir)}, args...),
Stdin: io.LimitReader(nil, 0),
@ -140,11 +140,11 @@ func extractTar(t *testing.T, data []byte, directory string) {
// Start runs the command in a goroutine and cleans it up when the test
// completed.
func Start(t *testing.T, inv *clibase.Invocation) {
func Start(t *testing.T, inv *serpent.Invocation) {
StartWithAssert(t, inv, nil)
}
func StartWithAssert(t *testing.T, inv *clibase.Invocation, assertCallback func(t *testing.T, err error)) { //nolint:revive
func StartWithAssert(t *testing.T, inv *serpent.Invocation, assertCallback func(t *testing.T, err error)) { //nolint:revive
t.Helper()
closeCh := make(chan struct{})
@ -175,7 +175,7 @@ func StartWithAssert(t *testing.T, inv *clibase.Invocation, assertCallback func(
}
// Run runs the command and asserts that there is no error.
func Run(t *testing.T, inv *clibase.Invocation) {
func Run(t *testing.T, inv *serpent.Invocation) {
t.Helper()
err := inv.Run()
@ -228,7 +228,7 @@ func (w *ErrorWaiter) RequireAs(want interface{}) {
// StartWithWaiter runs the command in a goroutine but returns the error instead
// of asserting it. This is useful for testing error cases.
func StartWithWaiter(t *testing.T, inv *clibase.Invocation) *ErrorWaiter {
func StartWithWaiter(t *testing.T, inv *serpent.Invocation) *ErrorWaiter {
t.Helper()
var (

View File

@ -13,12 +13,12 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
// UpdateGoldenFiles indicates golden files should be updated.
@ -48,7 +48,7 @@ func DefaultCases() []CommandHelpCase {
// TestCommandHelp will test the help output of the given commands
// using golden files.
func TestCommandHelp(t *testing.T, getRoot func(t *testing.T) *clibase.Cmd, cases []CommandHelpCase) {
func TestCommandHelp(t *testing.T, getRoot func(t *testing.T) *serpent.Cmd, cases []CommandHelpCase) {
t.Parallel()
rootClient, replacements := prepareTestData(t)
@ -148,7 +148,7 @@ func NormalizeGoldenFile(t *testing.T, byt []byte) []byte {
return byt
}
func extractVisibleCommandPaths(cmdPath []string, cmds []*clibase.Cmd) [][]string {
func extractVisibleCommandPaths(cmdPath []string, cmds []*serpent.Cmd) [][]string {
var cmdPaths [][]string
for _, c := range cmds {
if c.Hidden {

View File

@ -3,7 +3,7 @@ package clitest
import (
"testing"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/serpent"
)
// HandlersOK asserts that all commands have a handler.
@ -11,11 +11,11 @@ import (
// non-root commands (like 'groups' or 'users'), a handler is required.
// These handlers are likely just the 'help' handler, but this must be
// explicitly set.
func HandlersOK(t *testing.T, cmd *clibase.Cmd) {
cmd.Walk(func(cmd *clibase.Cmd) {
func HandlersOK(t *testing.T, cmd *serpent.Cmd) {
cmd.Walk(func(cmd *serpent.Cmd) {
if cmd.Handler == nil {
// If you see this error, make the Handler a helper invoker.
// Handler: func(inv *clibase.Invocation) error {
// Handler: func(inv *serpent.Invocation) error {
// return inv.Command.HelpHandler(inv)
// },
t.Errorf("command %q has no handler, change to a helper invoker using: 'inv.Command.HelpHandler(inv)'", cmd.Name())

View File

@ -18,13 +18,13 @@ import (
"golang.org/x/xerrors"
"tailscale.com/tailcfg"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
func TestAgent(t *testing.T) {
@ -382,8 +382,8 @@ func TestAgent(t *testing.T) {
output := make(chan string, 100) // Buffered to avoid blocking, overflow is discarded.
logs := make(chan []codersdk.WorkspaceAgentLog, 1)
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cmd := &serpent.Cmd{
Handler: func(inv *serpent.Invocation) error {
tc.opts.Fetch = func(_ context.Context, _ uuid.UUID) (codersdk.WorkspaceAgent, error) {
t.Log("iter", len(tc.iter))
var err error
@ -450,8 +450,8 @@ func TestAgent(t *testing.T) {
t.Parallel()
var fetchCalled uint64
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cmd := &serpent.Cmd{
Handler: func(inv *serpent.Invocation) error {
buf := bytes.Buffer{}
err := cliui.Agent(inv.Context(), &buf, uuid.Nil, cliui.AgentOptions{
FetchInterval: 10 * time.Millisecond,

View File

@ -3,13 +3,13 @@ package cliui
import (
"fmt"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func DeprecationWarning(message string) clibase.MiddlewareFunc {
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(i *clibase.Invocation) error {
func DeprecationWarning(message string) serpent.MiddlewareFunc {
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(i *serpent.Invocation) error {
_, _ = fmt.Fprintln(i.Stdout, "\n"+pretty.Sprint(DefaultStyles.Wrap,
pretty.Sprint(
DefaultStyles.Warn,

View File

@ -8,11 +8,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
func TestExternalAuth(t *testing.T) {
@ -22,8 +22,8 @@ func TestExternalAuth(t *testing.T) {
defer cancel()
ptty := ptytest.New(t)
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cmd := &serpent.Cmd{
Handler: func(inv *serpent.Invocation) error {
var fetched atomic.Bool
return cliui.ExternalAuth(inv.Context(), inv.Stdout, cliui.ExternalAuthOptions{
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) {

View File

@ -1,8 +1,8 @@
package cliui
import (
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
var defaultQuery = "owner:me"
@ -11,12 +11,12 @@ var defaultQuery = "owner:me"
// and allows easy integration to a CLI command.
// Example usage:
//
// func (r *RootCmd) MyCmd() *clibase.Cmd {
// func (r *RootCmd) MyCmd() *serpent.Cmd {
// var (
// filter cliui.WorkspaceFilter
// ...
// )
// cmd := &clibase.Cmd{
// cmd := &serpent.Cmd{
// ...
// }
// filter.AttachOptions(&cmd.Options)
@ -44,20 +44,20 @@ func (w *WorkspaceFilter) Filter() codersdk.WorkspaceFilter {
return f
}
func (w *WorkspaceFilter) AttachOptions(opts *clibase.OptionSet) {
func (w *WorkspaceFilter) AttachOptions(opts *serpent.OptionSet) {
*opts = append(*opts,
clibase.Option{
serpent.Option{
Flag: "all",
FlagShorthand: "a",
Description: "Specifies whether all workspaces will be listed or not.",
Value: clibase.BoolOf(&w.all),
Value: serpent.BoolOf(&w.all),
},
clibase.Option{
serpent.Option{
Flag: "search",
Description: "Search for a workspace with a query.",
Default: defaultQuery,
Value: clibase.StringOf(&w.searchQuery),
Value: serpent.StringOf(&w.searchQuery),
},
)
}

View File

@ -9,12 +9,12 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/serpent"
)
type OutputFormat interface {
ID() string
AttachOptions(opts *clibase.OptionSet)
AttachOptions(opts *serpent.OptionSet)
Format(ctx context.Context, data any) (string, error)
}
@ -49,7 +49,7 @@ func NewOutputFormatter(formats ...OutputFormat) *OutputFormatter {
// AttachOptions attaches the --output flag to the given command, and any
// additional flags required by the output formatters.
func (f *OutputFormatter) AttachOptions(opts *clibase.OptionSet) {
func (f *OutputFormatter) AttachOptions(opts *serpent.OptionSet) {
for _, format := range f.formats {
format.AttachOptions(opts)
}
@ -60,11 +60,11 @@ func (f *OutputFormatter) AttachOptions(opts *clibase.OptionSet) {
}
*opts = append(*opts,
clibase.Option{
serpent.Option{
Flag: "output",
FlagShorthand: "o",
Default: f.formats[0].ID(),
Value: clibase.StringOf(&f.formatID),
Value: serpent.StringOf(&f.formatID),
Description: "Output format. Available formats: " + strings.Join(formatNames, ", ") + ".",
},
)
@ -129,13 +129,13 @@ func (*tableFormat) ID() string {
}
// AttachOptions implements OutputFormat.
func (f *tableFormat) AttachOptions(opts *clibase.OptionSet) {
func (f *tableFormat) AttachOptions(opts *serpent.OptionSet) {
*opts = append(*opts,
clibase.Option{
serpent.Option{
Flag: "column",
FlagShorthand: "c",
Default: strings.Join(f.defaultColumns, ","),
Value: clibase.StringArrayOf(&f.columns),
Value: serpent.StringArrayOf(&f.columns),
Description: "Columns to display in table output. Available columns: " + strings.Join(f.allColumns, ", ") + ".",
},
)
@ -161,7 +161,7 @@ func (jsonFormat) ID() string {
}
// AttachOptions implements OutputFormat.
func (jsonFormat) AttachOptions(_ *clibase.OptionSet) {}
func (jsonFormat) AttachOptions(_ *serpent.OptionSet) {}
// Format implements OutputFormat.
func (jsonFormat) Format(_ context.Context, data any) (string, error) {
@ -187,7 +187,7 @@ func (textFormat) ID() string {
return "text"
}
func (textFormat) AttachOptions(_ *clibase.OptionSet) {}
func (textFormat) AttachOptions(_ *serpent.OptionSet) {}
func (textFormat) Format(_ context.Context, data any) (string, error) {
return fmt.Sprintf("%s", data), nil
@ -213,7 +213,7 @@ func (d *DataChangeFormat) ID() string {
return d.format.ID()
}
func (d *DataChangeFormat) AttachOptions(opts *clibase.OptionSet) {
func (d *DataChangeFormat) AttachOptions(opts *serpent.OptionSet) {
d.format.AttachOptions(opts)
}

View File

@ -8,13 +8,13 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/serpent"
)
type format struct {
id string
attachOptionsFn func(opts *clibase.OptionSet)
attachOptionsFn func(opts *serpent.OptionSet)
formatFn func(ctx context.Context, data any) (string, error)
}
@ -24,7 +24,7 @@ func (f *format) ID() string {
return f.id
}
func (f *format) AttachOptions(opts *clibase.OptionSet) {
func (f *format) AttachOptions(opts *serpent.OptionSet) {
if f.attachOptionsFn != nil {
f.attachOptionsFn(opts)
}
@ -85,12 +85,12 @@ func Test_OutputFormatter(t *testing.T) {
cliui.JSONFormat(),
&format{
id: "foo",
attachOptionsFn: func(opts *clibase.OptionSet) {
opts.Add(clibase.Option{
attachOptionsFn: func(opts *serpent.OptionSet) {
opts.Add(serpent.Option{
Name: "foo",
Flag: "foo",
FlagShorthand: "f",
Value: clibase.DiscardValue,
Value: serpent.DiscardValue,
Description: "foo flag 1234",
})
},
@ -101,7 +101,7 @@ func Test_OutputFormatter(t *testing.T) {
},
)
cmd := &clibase.Cmd{}
cmd := &serpent.Cmd{}
f.AttachOptions(&cmd.Options)
fs := cmd.Options.FlagSet()

View File

@ -5,12 +5,12 @@ import (
"fmt"
"strings"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
label := templateVersionParameter.Name
if templateVersionParameter.DisplayName != "" {
label = templateVersionParameter.DisplayName

View File

@ -13,8 +13,8 @@ import (
"github.com/mattn/go-isatty"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
// PromptOptions supply a set of options to the prompt.
@ -30,13 +30,13 @@ const skipPromptFlag = "yes"
// SkipPromptOption adds a "--yes/-y" flag to the cmd that can be used to skip
// prompts.
func SkipPromptOption() clibase.Option {
return clibase.Option{
func SkipPromptOption() serpent.Option {
return serpent.Option{
Flag: skipPromptFlag,
FlagShorthand: "y",
Description: "Bypass prompts.",
// Discard
Value: clibase.BoolOf(new(bool)),
Value: serpent.BoolOf(new(bool)),
}
}
@ -46,7 +46,7 @@ const (
)
// Prompt asks the user for input.
func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
func Prompt(inv *serpent.Invocation, opts PromptOptions) (string, error) {
// If the cmd has a "yes" flag for skipping confirm prompts, honor it.
// If it's not a "Confirm" prompt, then don't skip. As the default value of
// "yes" makes no sense.

View File

@ -11,11 +11,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/pty"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
func TestPrompt(t *testing.T) {
@ -77,7 +77,7 @@ func TestPrompt(t *testing.T) {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "ShouldNotSeeThis",
IsConfirm: true,
}, func(inv *clibase.Invocation) {
}, func(inv *serpent.Invocation) {
inv.Command.Options = append(inv.Command.Options, cliui.SkipPromptOption())
inv.Args = []string{"-y"}
})
@ -145,10 +145,10 @@ func TestPrompt(t *testing.T) {
})
}
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions, invOpt func(inv *clibase.Invocation)) (string, error) {
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions, invOpt func(inv *serpent.Invocation)) (string, error) {
value := ""
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cmd := &serpent.Cmd{
Handler: func(inv *serpent.Invocation) error {
var err error
value, err = cliui.Prompt(inv, opts)
return err
@ -210,8 +210,8 @@ func TestPasswordTerminalState(t *testing.T) {
// nolint:unused
func passwordHelper() {
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cmd := &serpent.Cmd{
Handler: func(inv *serpent.Invocation) error {
cliui.Prompt(inv, cliui.PromptOptions{
Text: "Password:",
Secret: true,

View File

@ -11,11 +11,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/serpent"
)
// This cannot be ran in parallel because it uses a signal.
@ -127,8 +127,8 @@ func newProvisionerJob(t *testing.T) provisionerJobTest {
}
jobLock := sync.Mutex{}
logs := make(chan codersdk.ProvisionerJobLog, 1)
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cmd := &serpent.Cmd{
Handler: func(inv *serpent.Invocation) error {
return cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
FetchInterval: time.Millisecond,
Fetch: func() (codersdk.ProvisionerJob, error) {

View File

@ -10,8 +10,8 @@ import (
"github.com/AlecAivazis/survey/v2/terminal"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func init() {
@ -68,7 +68,7 @@ type RichSelectOptions struct {
}
// RichSelect displays a list of user options including name and description.
func RichSelect(inv *clibase.Invocation, richOptions RichSelectOptions) (*codersdk.TemplateVersionParameterOption, error) {
func RichSelect(inv *serpent.Invocation, richOptions RichSelectOptions) (*codersdk.TemplateVersionParameterOption, error) {
opts := make([]string, len(richOptions.Options))
var defaultOpt string
for i, option := range richOptions.Options {
@ -102,7 +102,7 @@ func RichSelect(inv *clibase.Invocation, richOptions RichSelectOptions) (*coders
}
// Select displays a list of user options.
func Select(inv *clibase.Invocation, opts SelectOptions) (string, error) {
func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
// The survey library used *always* fails when testing on Windows,
// as it requires a live TTY (can't be a conpty). We should fork
// this library to add a dummy fallback, that simply reads/writes
@ -138,7 +138,7 @@ func Select(inv *clibase.Invocation, opts SelectOptions) (string, error) {
return value, err
}
func MultiSelect(inv *clibase.Invocation, items []string) ([]string, error) {
func MultiSelect(inv *serpent.Invocation, items []string) ([]string, error) {
// Similar hack is applied to Select()
if flag.Lookup("test.v") != nil {
return items, nil

View File

@ -6,10 +6,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/serpent"
)
func TestSelect(t *testing.T) {
@ -31,8 +31,8 @@ func TestSelect(t *testing.T) {
func newSelect(ptty *ptytest.PTY, opts cliui.SelectOptions) (string, error) {
value := ""
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cmd := &serpent.Cmd{
Handler: func(inv *serpent.Invocation) error {
var err error
value, err = cliui.Select(inv, opts)
return err
@ -72,8 +72,8 @@ func TestRichSelect(t *testing.T) {
func newRichSelect(ptty *ptytest.PTY, opts cliui.RichSelectOptions) (string, error) {
value := ""
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cmd := &serpent.Cmd{
Handler: func(inv *serpent.Invocation) error {
richOption, err := cliui.RichSelect(inv, opts)
if err == nil {
value = richOption.Value
@ -105,8 +105,8 @@ func TestMultiSelect(t *testing.T) {
func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {
var values []string
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cmd := &serpent.Cmd{
Handler: func(inv *serpent.Invocation) error {
selectedItems, err := cliui.MultiSelect(inv, items)
if err == nil {
values = selectedItems

View File

@ -24,10 +24,10 @@ import (
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
const (
@ -215,7 +215,7 @@ func sshPrepareWorkspaceConfigs(ctx context.Context, client *codersdk.Client) (r
}
}
func (r *RootCmd) configSSH() *clibase.Cmd {
func (r *RootCmd) configSSH() *serpent.Cmd {
var (
sshConfigFile string
sshConfigOpts sshConfigOptions
@ -226,7 +226,7 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
coderCliPath string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "config-ssh",
Short: "Add an SSH Host entry for your workspaces \"ssh coder.workspace\"",
@ -240,11 +240,11 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
Command: "coder config-ssh --dry-run",
},
),
Middleware: clibase.Chain(
clibase.RequireNArgs(0),
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
if sshConfigOpts.waitEnum != "auto" && skipProxyCommand {
// The wait option is applied to the ProxyCommand. If the user
// specifies skip-proxy-command, then wait cannot be applied.
@ -538,13 +538,13 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "ssh-config-file",
Env: "CODER_SSH_CONFIG_FILE",
Default: sshDefaultConfigFileName,
Description: "Specifies the path to an SSH config.",
Value: clibase.StringOf(&sshConfigFile),
Value: serpent.StringOf(&sshConfigFile),
},
{
Flag: "coder-binary-path",
@ -552,7 +552,7 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
Default: "",
Description: "Optionally specify the absolute path to the coder binary used in ProxyCommand. " +
"By default, the binary invoking this command ('config ssh') is used.",
Value: clibase.Validate(clibase.StringOf(&coderCliPath), func(value *clibase.String) error {
Value: serpent.Validate(serpent.StringOf(&coderCliPath), func(value *serpent.String) error {
if runtime.GOOS == goosWindows {
// For some reason filepath.IsAbs() does not work on windows.
return nil
@ -569,46 +569,46 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
FlagShorthand: "o",
Env: "CODER_SSH_CONFIG_OPTS",
Description: "Specifies additional SSH options to embed in each host stanza.",
Value: clibase.StringArrayOf(&sshConfigOpts.sshOptions),
Value: serpent.StringArrayOf(&sshConfigOpts.sshOptions),
},
{
Flag: "dry-run",
FlagShorthand: "n",
Env: "CODER_SSH_DRY_RUN",
Description: "Perform a trial run with no changes made, showing a diff at the end.",
Value: clibase.BoolOf(&dryRun),
Value: serpent.BoolOf(&dryRun),
},
{
Flag: "skip-proxy-command",
Env: "CODER_SSH_SKIP_PROXY_COMMAND",
Description: "Specifies whether the ProxyCommand option should be skipped. Useful for testing.",
Value: clibase.BoolOf(&skipProxyCommand),
Value: serpent.BoolOf(&skipProxyCommand),
Hidden: true,
},
{
Flag: "use-previous-options",
Env: "CODER_SSH_USE_PREVIOUS_OPTIONS",
Description: "Specifies whether or not to keep options from previous run of config-ssh.",
Value: clibase.BoolOf(&usePreviousOpts),
Value: serpent.BoolOf(&usePreviousOpts),
},
{
Flag: "ssh-host-prefix",
Env: "CODER_CONFIGSSH_SSH_HOST_PREFIX",
Description: "Override the default host prefix.",
Value: clibase.StringOf(&sshConfigOpts.userHostPrefix),
Value: serpent.StringOf(&sshConfigOpts.userHostPrefix),
},
{
Flag: "wait",
Env: "CODER_CONFIGSSH_WAIT", // Not to be mixed with CODER_SSH_WAIT.
Description: "Specifies whether or not to wait for the startup script to finish executing. Auto means that the agent startup script behavior configured in the workspace template is used.",
Default: "auto",
Value: clibase.EnumOf(&sshConfigOpts.waitEnum, "yes", "no", "auto"),
Value: serpent.EnumOf(&sshConfigOpts.waitEnum, "yes", "no", "auto"),
},
{
Flag: "disable-autostart",
Description: "Disable starting the workspace automatically when connecting via SSH.",
Env: "CODER_CONFIGSSH_DISABLE_AUTOSTART",
Value: clibase.BoolOf(&sshConfigOpts.disableAutostart),
Value: serpent.BoolOf(&sshConfigOpts.disableAutostart),
Default: "false",
},
{
@ -617,7 +617,7 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
Description: "By default, 'config-ssh' uses the os path separator when writing the ssh config. " +
"This might be an issue in Windows machine that use a unix-like shell. " +
"This flag forces the use of unix file paths (the forward slash '/').",
Value: clibase.BoolOf(&forceUnixSeparators),
Value: serpent.BoolOf(&forceUnixSeparators),
// On non-windows showing this command is useless because it is a noop.
// Hide vs disable it though so if a command is copied from a Windows
// machine to a unix machine it will still work and not throw an

View File

@ -12,14 +12,14 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) create() *clibase.Cmd {
func (r *RootCmd) create() *serpent.Cmd {
var (
templateName string
startAt string
@ -31,7 +31,7 @@ func (r *RootCmd) create() *clibase.Cmd {
copyParametersFrom string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "create [name]",
Short: "Create a workspace",
@ -41,8 +41,8 @@ func (r *RootCmd) create() *clibase.Cmd {
Command: "coder create <username>/<workspace_name>",
},
),
Middleware: clibase.Chain(r.InitClient(client)),
Handler: func(inv *clibase.Invocation) error {
Middleware: serpent.Chain(r.InitClient(client)),
Handler: func(inv *serpent.Invocation) error {
organization, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
@ -227,37 +227,37 @@ func (r *RootCmd) create() *clibase.Cmd {
},
}
cmd.Options = append(cmd.Options,
clibase.Option{
serpent.Option{
Flag: "template",
FlagShorthand: "t",
Env: "CODER_TEMPLATE_NAME",
Description: "Specify a template name.",
Value: clibase.StringOf(&templateName),
Value: serpent.StringOf(&templateName),
},
clibase.Option{
serpent.Option{
Flag: "start-at",
Env: "CODER_WORKSPACE_START_AT",
Description: "Specify the workspace autostart schedule. Check coder schedule start --help for the syntax.",
Value: clibase.StringOf(&startAt),
Value: serpent.StringOf(&startAt),
},
clibase.Option{
serpent.Option{
Flag: "stop-after",
Env: "CODER_WORKSPACE_STOP_AFTER",
Description: "Specify a duration after which the workspace should shut down (e.g. 8h).",
Value: clibase.DurationOf(&stopAfter),
Value: serpent.DurationOf(&stopAfter),
},
clibase.Option{
serpent.Option{
Flag: "automatic-updates",
Env: "CODER_WORKSPACE_AUTOMATIC_UPDATES",
Description: "Specify automatic updates setting for the workspace (accepts 'always' or 'never').",
Default: string(codersdk.AutomaticUpdatesNever),
Value: clibase.StringOf(&autoUpdates),
Value: serpent.StringOf(&autoUpdates),
},
clibase.Option{
serpent.Option{
Flag: "copy-parameters-from",
Env: "CODER_WORKSPACE_COPY_PARAMETERS_FROM",
Description: "Specify the source workspace name to copy parameters from.",
Value: clibase.StringOf(&copyParametersFrom),
Value: serpent.StringOf(&copyParametersFrom),
},
cliui.SkipPromptOption(),
)
@ -283,7 +283,7 @@ type prepWorkspaceBuildArgs struct {
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
// Any missing params will be prompted to the user. It supports rich parameters.
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
ctx := inv.Context()
templateVersion, err := client.TemplateVersion(ctx, args.TemplateVersionID)

View File

@ -4,24 +4,24 @@ import (
"fmt"
"time"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
// nolint
func (r *RootCmd) deleteWorkspace() *clibase.Cmd {
func (r *RootCmd) deleteWorkspace() *serpent.Cmd {
var orphan bool
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "delete <workspace>",
Short: "Delete a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return err
@ -62,12 +62,12 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd {
return nil
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "orphan",
Description: "Delete a workspace without deleting its resources. This can delete a workspace in a broken state, but may also lead to unaccounted cloud resources.",
Value: clibase.BoolOf(&orphan),
Value: serpent.BoolOf(&orphan),
},
cliui.SkipPromptOption(),
}

View File

@ -15,18 +15,18 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/serpent"
)
func (r *RootCmd) dotfiles() *clibase.Cmd {
func (r *RootCmd) dotfiles() *serpent.Cmd {
var symlinkDir string
var gitbranch string
var dotfilesRepoDir string
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "dotfiles <git_repo_url>",
Middleware: clibase.RequireNArgs(1),
Middleware: serpent.RequireNArgs(1),
Short: "Personalize your workspace by applying a canonical dotfiles repository",
Long: formatExamples(
example{
@ -34,7 +34,7 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
Command: "coder dotfiles --yes git@github.com:example/dotfiles.git",
},
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var (
gitRepo = inv.Args[0]
cfg = r.createConfig()
@ -276,26 +276,26 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
return nil
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "symlink-dir",
Env: "CODER_SYMLINK_DIR",
Description: "Specifies the directory for the dotfiles symlink destinations. If empty, will use $HOME.",
Value: clibase.StringOf(&symlinkDir),
Value: serpent.StringOf(&symlinkDir),
},
{
Flag: "branch",
FlagShorthand: "b",
Description: "Specifies which branch to clone. " +
"If empty, will default to cloning the default branch or using the existing branch in the cloned repo on disk.",
Value: clibase.StringOf(&gitbranch),
Value: serpent.StringOf(&gitbranch),
},
{
Flag: "repo-dir",
Default: "dotfiles",
Env: "CODER_DOTFILES_REPO_DIR",
Description: "Specifies the directory for the dotfiles repository, relative to global config directory.",
Value: clibase.StringOf(&dotfilesRepoDir),
Value: serpent.StringOf(&dotfilesRepoDir),
},
cliui.SkipPromptOption(),
}
@ -308,7 +308,7 @@ type ensureCorrectGitBranchParams struct {
gitBranch string
}
func ensureCorrectGitBranch(baseInv *clibase.Invocation, params ensureCorrectGitBranchParams) error {
func ensureCorrectGitBranch(baseInv *serpent.Invocation, params ensureCorrectGitBranchParams) error {
dotfileCmd := func(cmd string, args ...string) *exec.Cmd {
c := exec.CommandContext(baseInv.Context(), cmd, args...)
c.Dir = params.repoDir

View File

@ -9,15 +9,15 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (RootCmd) errorExample() *clibase.Cmd {
errorCmd := func(use string, err error) *clibase.Cmd {
return &clibase.Cmd{
func (RootCmd) errorExample() *serpent.Cmd {
errorCmd := func(use string, err error) *serpent.Cmd {
return &serpent.Cmd{
Use: use,
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return err
},
}
@ -50,18 +50,18 @@ func (RootCmd) errorExample() *clibase.Cmd {
apiErrorNoHelper.Helper = ""
// Some flags
var magicWord clibase.String
var magicWord serpent.String
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "example-error",
Short: "Shows what different error messages look like",
Long: "This command is pretty pointless, but without it testing errors is" +
"difficult to visually inspect. Error message formatting is inherently" +
"visual, so we need a way to quickly see what they look like.",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
// Typical codersdk api error
errorCmd("api", apiError),
@ -71,7 +71,7 @@ func (RootCmd) errorExample() *clibase.Cmd {
// A multi-error
{
Use: "multi-error",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return xerrors.Errorf("wrapped: %w", errors.Join(
xerrors.Errorf("first error: %w", errorWithStackTrace()),
xerrors.Errorf("second error: %w", errorWithStackTrace()),
@ -82,7 +82,7 @@ func (RootCmd) errorExample() *clibase.Cmd {
{
Use: "multi-multi-error",
Short: "This is a multi error inside a multi error",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
// Closing the stdin file descriptor will cause the next close
// to fail. This is joined to the returned Command error.
if f, ok := inv.Stdin.(*os.File); ok {
@ -97,29 +97,29 @@ func (RootCmd) errorExample() *clibase.Cmd {
},
{
Use: "validation",
Options: clibase.OptionSet{
clibase.Option{
Options: serpent.OptionSet{
serpent.Option{
Name: "magic-word",
Description: "Take a good guess.",
Required: true,
Flag: "magic-word",
Default: "",
Value: clibase.Validate(&magicWord, func(value *clibase.String) error {
Value: serpent.Validate(&magicWord, func(value *serpent.String) error {
return xerrors.Errorf("magic word is incorrect")
}),
},
},
Handler: func(i *clibase.Invocation) error {
Handler: func(i *serpent.Invocation) error {
_, _ = fmt.Fprint(i.Stdout, "Try setting the --magic-word flag\n")
return nil
},
},
{
Use: "arg-required <required>",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
Handler: func(i *clibase.Invocation) error {
Handler: func(i *serpent.Invocation) error {
_, _ = fmt.Fprint(i.Stdout, "Try running this without an argument\n")
return nil
},

View File

@ -1,16 +1,16 @@
package cli
import "github.com/coder/coder/v2/cli/clibase"
import "github.com/coder/serpent"
func (r *RootCmd) expCmd() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) expCmd() *serpent.Cmd {
cmd := &serpent.Cmd{
Use: "exp",
Short: "Internal commands for testing and experimentation. These are prone to breaking changes with no notice.",
Handler: func(i *clibase.Invocation) error {
Handler: func(i *serpent.Invocation) error {
return i.Command.HelpHandler(i)
},
Hidden: true,
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.scaletestCmd(),
r.errorExample(),
},

View File

@ -27,7 +27,6 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/tracing"
@ -40,18 +39,19 @@ import (
"github.com/coder/coder/v2/scaletest/reconnectingpty"
"github.com/coder/coder/v2/scaletest/workspacebuild"
"github.com/coder/coder/v2/scaletest/workspacetraffic"
"github.com/coder/serpent"
)
const scaletestTracerName = "coder_scaletest"
func (r *RootCmd) scaletestCmd() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) scaletestCmd() *serpent.Cmd {
cmd := &serpent.Cmd{
Use: "scaletest",
Short: "Run a scale test against the Coder API",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.scaletestCleanup(),
r.scaletestDashboard(),
r.scaletestCreateWorkspaces(),
@ -69,32 +69,32 @@ type scaletestTracingFlags struct {
tracePropagate bool
}
func (s *scaletestTracingFlags) attach(opts *clibase.OptionSet) {
func (s *scaletestTracingFlags) attach(opts *serpent.OptionSet) {
*opts = append(
*opts,
clibase.Option{
serpent.Option{
Flag: "trace",
Env: "CODER_SCALETEST_TRACE",
Description: "Whether application tracing data is collected. It exports to a backend configured by environment variables. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md.",
Value: clibase.BoolOf(&s.traceEnable),
Value: serpent.BoolOf(&s.traceEnable),
},
clibase.Option{
serpent.Option{
Flag: "trace-coder",
Env: "CODER_SCALETEST_TRACE_CODER",
Description: "Whether opentelemetry traces are sent to Coder. We recommend keeping this disabled unless we advise you to enable it.",
Value: clibase.BoolOf(&s.traceCoder),
Value: serpent.BoolOf(&s.traceCoder),
},
clibase.Option{
serpent.Option{
Flag: "trace-honeycomb-api-key",
Env: "CODER_SCALETEST_TRACE_HONEYCOMB_API_KEY",
Description: "Enables trace exporting to Honeycomb.io using the provided API key.",
Value: clibase.StringOf(&s.traceHoneycombAPIKey),
Value: serpent.StringOf(&s.traceHoneycombAPIKey),
},
clibase.Option{
serpent.Option{
Flag: "trace-propagate",
Env: "CODER_SCALETEST_TRACE_PROPAGATE",
Description: "Enables trace propagation to the Coder backend, which will be used to correlate server-side spans with client-side spans. Only enable this if the server is configured with the exact same tracing configuration as the client.",
Value: clibase.BoolOf(&s.tracePropagate),
Value: serpent.BoolOf(&s.tracePropagate),
},
)
}
@ -137,7 +137,7 @@ type scaletestStrategyFlags struct {
timeoutPerJob time.Duration
}
func (s *scaletestStrategyFlags) attach(opts *clibase.OptionSet) {
func (s *scaletestStrategyFlags) attach(opts *serpent.OptionSet) {
concurrencyLong, concurrencyEnv, concurrencyDescription := "concurrency", "CODER_SCALETEST_CONCURRENCY", "Number of concurrent jobs to run. 0 means unlimited."
timeoutLong, timeoutEnv, timeoutDescription := "timeout", "CODER_SCALETEST_TIMEOUT", "Timeout for the entire test run. 0 means unlimited."
jobTimeoutLong, jobTimeoutEnv, jobTimeoutDescription := "job-timeout", "CODER_SCALETEST_JOB_TIMEOUT", "Timeout per job. Jobs may take longer to complete under higher concurrency limits."
@ -149,26 +149,26 @@ func (s *scaletestStrategyFlags) attach(opts *clibase.OptionSet) {
*opts = append(
*opts,
clibase.Option{
serpent.Option{
Flag: concurrencyLong,
Env: concurrencyEnv,
Description: concurrencyDescription,
Default: "1",
Value: clibase.Int64Of(&s.concurrency),
Value: serpent.Int64Of(&s.concurrency),
},
clibase.Option{
serpent.Option{
Flag: timeoutLong,
Env: timeoutEnv,
Description: timeoutDescription,
Default: "30m",
Value: clibase.DurationOf(&s.timeout),
Value: serpent.DurationOf(&s.timeout),
},
clibase.Option{
serpent.Option{
Flag: jobTimeoutLong,
Env: jobTimeoutEnv,
Description: jobTimeoutDescription,
Default: "5m",
Value: clibase.DurationOf(&s.timeoutPerJob),
Value: serpent.DurationOf(&s.timeoutPerJob),
},
)
}
@ -268,13 +268,13 @@ type scaletestOutputFlags struct {
outputSpecs []string
}
func (s *scaletestOutputFlags) attach(opts *clibase.OptionSet) {
*opts = append(*opts, clibase.Option{
func (s *scaletestOutputFlags) attach(opts *serpent.OptionSet) {
*opts = append(*opts, serpent.Option{
Flag: "output",
Env: "CODER_SCALETEST_OUTPUTS",
Description: `Output format specs in the format "<format>[:<path>]". Not specifying a path will default to stdout. Available formats: text, json.`,
Default: "text",
Value: clibase.StringArrayOf(&s.outputSpecs),
Value: serpent.StringArrayOf(&s.outputSpecs),
})
}
@ -331,21 +331,21 @@ type scaletestPrometheusFlags struct {
Wait time.Duration
}
func (s *scaletestPrometheusFlags) attach(opts *clibase.OptionSet) {
func (s *scaletestPrometheusFlags) attach(opts *serpent.OptionSet) {
*opts = append(*opts,
clibase.Option{
serpent.Option{
Flag: "scaletest-prometheus-address",
Env: "CODER_SCALETEST_PROMETHEUS_ADDRESS",
Default: "0.0.0.0:21112",
Description: "Address on which to expose scaletest Prometheus metrics.",
Value: clibase.StringOf(&s.Address),
Value: serpent.StringOf(&s.Address),
},
clibase.Option{
serpent.Option{
Flag: "scaletest-prometheus-wait",
Env: "CODER_SCALETEST_PROMETHEUS_WAIT",
Default: "15s",
Description: "How long to wait before exiting in order to allow Prometheus metrics to be scraped.",
Value: clibase.DurationOf(&s.Wait),
Value: serpent.DurationOf(&s.Wait),
},
)
}
@ -398,20 +398,20 @@ func (r *userCleanupRunner) Run(ctx context.Context, _ string, _ io.Writer) erro
return nil
}
func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
func (r *RootCmd) scaletestCleanup() *serpent.Cmd {
var template string
cleanupStrategy := &scaletestStrategyFlags{cleanup: true}
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "cleanup",
Short: "Cleanup scaletest workspaces, then cleanup scaletest users.",
Long: "The strategy flags will apply to each stage of the cleanup process.",
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
me, err := requireAdmin(ctx, client)
@ -508,12 +508,12 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "template",
Env: "CODER_SCALETEST_CLEANUP_TEMPLATE",
Description: "Name or ID of the template. Only delete workspaces created from the given template.",
Value: clibase.StringOf(&template),
Value: serpent.StringOf(&template),
},
}
@ -521,7 +521,7 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
return cmd
}
func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
func (r *RootCmd) scaletestCreateWorkspaces() *serpent.Cmd {
var (
count int64
retry int64
@ -558,12 +558,12 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "create-workspaces",
Short: "Creates many users, then creates a workspace for each user and waits for them finish building and fully come online. Optionally runs a command inside each workspace, and connects to the workspace over WireGuard.",
Long: `It is recommended that all rate limits are disabled on the server before running this scaletest. This test generates many login events which will be rate limited against the (most likely single) IP.`,
Middleware: r.InitClient(client),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
me, err := requireAdmin(ctx, client)
@ -746,98 +746,98 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "count",
FlagShorthand: "c",
Env: "CODER_SCALETEST_COUNT",
Default: "1",
Description: "Required: Number of workspaces to create.",
Value: clibase.Int64Of(&count),
Value: serpent.Int64Of(&count),
},
{
Flag: "retry",
Env: "CODER_SCALETEST_RETRY",
Default: "0",
Description: "Number of tries to create and bring up the workspace.",
Value: clibase.Int64Of(&retry),
Value: serpent.Int64Of(&retry),
},
{
Flag: "template",
FlagShorthand: "t",
Env: "CODER_SCALETEST_TEMPLATE",
Description: "Required: Name or ID of the template to use for workspaces.",
Value: clibase.StringOf(&template),
Value: serpent.StringOf(&template),
},
{
Flag: "no-cleanup",
Env: "CODER_SCALETEST_NO_CLEANUP",
Description: "Do not clean up resources after the test completes. You can cleanup manually using coder scaletest cleanup.",
Value: clibase.BoolOf(&noCleanup),
Value: serpent.BoolOf(&noCleanup),
},
{
Flag: "no-wait-for-agents",
Env: "CODER_SCALETEST_NO_WAIT_FOR_AGENTS",
Description: `Do not wait for agents to start before marking the test as succeeded. This can be useful if you are running the test against a template that does not start the agent quickly.`,
Value: clibase.BoolOf(&noWaitForAgents),
Value: serpent.BoolOf(&noWaitForAgents),
},
{
Flag: "run-command",
Env: "CODER_SCALETEST_RUN_COMMAND",
Description: "Command to run inside each workspace using reconnecting-pty (i.e. web terminal protocol). " + "If not specified, no command will be run.",
Value: clibase.StringOf(&runCommand),
Value: serpent.StringOf(&runCommand),
},
{
Flag: "run-timeout",
Env: "CODER_SCALETEST_RUN_TIMEOUT",
Default: "5s",
Description: "Timeout for the command to complete.",
Value: clibase.DurationOf(&runTimeout),
Value: serpent.DurationOf(&runTimeout),
},
{
Flag: "run-expect-timeout",
Env: "CODER_SCALETEST_RUN_EXPECT_TIMEOUT",
Description: "Expect the command to timeout." + " If the command does not finish within the given --run-timeout, it will be marked as succeeded." + " If the command finishes before the timeout, it will be marked as failed.",
Value: clibase.BoolOf(&runExpectTimeout),
Value: serpent.BoolOf(&runExpectTimeout),
},
{
Flag: "run-expect-output",
Env: "CODER_SCALETEST_RUN_EXPECT_OUTPUT",
Description: "Expect the command to output the given string (on a single line). " + "If the command does not output the given string, it will be marked as failed.",
Value: clibase.StringOf(&runExpectOutput),
Value: serpent.StringOf(&runExpectOutput),
},
{
Flag: "run-log-output",
Env: "CODER_SCALETEST_RUN_LOG_OUTPUT",
Description: "Log the output of the command to the test logs. " + "This should be left off unless you expect small amounts of output. " + "Large amounts of output will cause high memory usage.",
Value: clibase.BoolOf(&runLogOutput),
Value: serpent.BoolOf(&runLogOutput),
},
{
Flag: "connect-url",
Env: "CODER_SCALETEST_CONNECT_URL",
Description: "URL to connect to inside the the workspace over WireGuard. " + "If not specified, no connections will be made over WireGuard.",
Value: clibase.StringOf(&connectURL),
Value: serpent.StringOf(&connectURL),
},
{
Flag: "connect-mode",
Env: "CODER_SCALETEST_CONNECT_MODE",
Default: "derp",
Description: "Mode to use for connecting to the workspace.",
Value: clibase.EnumOf(&connectMode, "derp", "direct"),
Value: serpent.EnumOf(&connectMode, "derp", "direct"),
},
{
Flag: "connect-hold",
Env: "CODER_SCALETEST_CONNECT_HOLD",
Default: "30s",
Description: "How long to hold the WireGuard connection open for.",
Value: clibase.DurationOf(&connectHold),
Value: serpent.DurationOf(&connectHold),
},
{
Flag: "connect-interval",
Env: "CODER_SCALETEST_CONNECT_INTERVAL",
Default: "1s",
Value: clibase.DurationOf(&connectInterval),
Value: serpent.DurationOf(&connectInterval),
Description: "How long to wait between making requests to the --connect-url once the connection is established.",
},
{
@ -845,14 +845,14 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
Env: "CODER_SCALETEST_CONNECT_TIMEOUT",
Default: "5s",
Description: "Timeout for each request to the --connect-url.",
Value: clibase.DurationOf(&connectTimeout),
Value: serpent.DurationOf(&connectTimeout),
},
{
Flag: "use-host-login",
Env: "CODER_SCALETEST_USE_HOST_LOGIN",
Default: "false",
Description: "Use the user logged in on the host machine, instead of creating users.",
Value: clibase.BoolOf(&useHostUser),
Value: serpent.BoolOf(&useHostUser),
},
}
@ -864,7 +864,7 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
return cmd
}
func (r *RootCmd) scaletestWorkspaceTraffic() *clibase.Cmd {
func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Cmd {
var (
tickInterval time.Duration
bytesPerTick int64
@ -881,13 +881,13 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *clibase.Cmd {
prometheusFlags = &scaletestPrometheusFlags{}
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "workspace-traffic",
Short: "Generate traffic to scaletest workspaces through coderd",
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) (err error) {
Handler: func(inv *serpent.Invocation) (err error) {
ctx := inv.Context()
notifyCtx, stop := signal.NotifyContext(ctx, StopSignals...) // Checked later.
@ -1056,47 +1056,47 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *clibase.Cmd {
},
}
cmd.Options = []clibase.Option{
cmd.Options = []serpent.Option{
{
Flag: "template",
FlagShorthand: "t",
Env: "CODER_SCALETEST_TEMPLATE",
Description: "Name or ID of the template. Traffic generation will be limited to workspaces created from this template.",
Value: clibase.StringOf(&template),
Value: serpent.StringOf(&template),
},
{
Flag: "target-workspaces",
Env: "CODER_SCALETEST_TARGET_WORKSPACES",
Description: "Target a specific range of workspaces in the format [START]:[END] (exclusive). Example: 0:10 will target the 10 first alphabetically sorted workspaces (0-9).",
Value: clibase.StringOf(&targetWorkspaces),
Value: serpent.StringOf(&targetWorkspaces),
},
{
Flag: "bytes-per-tick",
Env: "CODER_SCALETEST_WORKSPACE_TRAFFIC_BYTES_PER_TICK",
Default: "1024",
Description: "How much traffic to generate per tick.",
Value: clibase.Int64Of(&bytesPerTick),
Value: serpent.Int64Of(&bytesPerTick),
},
{
Flag: "tick-interval",
Env: "CODER_SCALETEST_WORKSPACE_TRAFFIC_TICK_INTERVAL",
Default: "100ms",
Description: "How often to send traffic.",
Value: clibase.DurationOf(&tickInterval),
Value: serpent.DurationOf(&tickInterval),
},
{
Flag: "ssh",
Env: "CODER_SCALETEST_WORKSPACE_TRAFFIC_SSH",
Default: "",
Description: "Send traffic over SSH, cannot be used with --app.",
Value: clibase.BoolOf(&ssh),
Value: serpent.BoolOf(&ssh),
},
{
Flag: "app",
Env: "CODER_SCALETEST_WORKSPACE_TRAFFIC_APP",
Default: "",
Description: "Send WebSocket traffic to a workspace app (proxied via coderd), cannot be used with --ssh.",
Value: clibase.StringOf(&app),
Value: serpent.StringOf(&app),
},
}
@ -1109,7 +1109,7 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *clibase.Cmd {
return cmd
}
func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
func (r *RootCmd) scaletestDashboard() *serpent.Cmd {
var (
interval time.Duration
jitter time.Duration
@ -1125,13 +1125,13 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
prometheusFlags = &scaletestPrometheusFlags{}
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "dashboard",
Short: "Generate traffic to the HTTP API to simulate use of the dashboard.",
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
if !(interval > 0) {
return xerrors.Errorf("--interval must be greater than zero")
}
@ -1261,40 +1261,40 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
},
}
cmd.Options = []clibase.Option{
cmd.Options = []serpent.Option{
{
Flag: "target-users",
Env: "CODER_SCALETEST_DASHBOARD_TARGET_USERS",
Description: "Target a specific range of users in the format [START]:[END] (exclusive). Example: 0:10 will target the 10 first alphabetically sorted users (0-9).",
Value: clibase.StringOf(&targetUsers),
Value: serpent.StringOf(&targetUsers),
},
{
Flag: "interval",
Env: "CODER_SCALETEST_DASHBOARD_INTERVAL",
Default: "10s",
Description: "Interval between actions.",
Value: clibase.DurationOf(&interval),
Value: serpent.DurationOf(&interval),
},
{
Flag: "jitter",
Env: "CODER_SCALETEST_DASHBOARD_JITTER",
Default: "5s",
Description: "Jitter between actions.",
Value: clibase.DurationOf(&jitter),
Value: serpent.DurationOf(&jitter),
},
{
Flag: "headless",
Env: "CODER_SCALETEST_DASHBOARD_HEADLESS",
Default: "true",
Description: "Controls headless mode. Setting to false is useful for debugging.",
Value: clibase.BoolOf(&headless),
Value: serpent.BoolOf(&headless),
},
{
Flag: "rand-seed",
Env: "CODER_SCALETEST_DASHBOARD_RAND_SEED",
Default: "0",
Description: "Seed for the random number generator.",
Value: clibase.Int64Of(&randSeed),
Value: serpent.Int64Of(&randSeed),
},
}

View File

@ -2,13 +2,13 @@
package cli
import "github.com/coder/coder/v2/cli/clibase"
import "github.com/coder/serpent"
func (r *RootCmd) scaletestCmd() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) scaletestCmd() *serpent.Cmd {
cmd := &serpent.Cmd{
Use: "scaletest",
Short: "Run a scale test against the Coder API",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
SlimUnsupported(inv.Stderr, "exp scaletest")
return nil
},

View File

@ -7,28 +7,28 @@ import (
"github.com/tidwall/gjson"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/serpent"
)
func (r *RootCmd) externalAuth() *clibase.Cmd {
return &clibase.Cmd{
func (r *RootCmd) externalAuth() *serpent.Cmd {
return &serpent.Cmd{
Use: "external-auth",
Short: "Manage external authentication",
Long: "Authenticate with external services inside of a workspace.",
Handler: func(i *clibase.Invocation) error {
Handler: func(i *serpent.Invocation) error {
return i.Command.HelpHandler(i)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.externalAuthAccessToken(),
},
}
}
func (r *RootCmd) externalAuthAccessToken() *clibase.Cmd {
func (r *RootCmd) externalAuthAccessToken() *serpent.Cmd {
var extra string
return &clibase.Cmd{
return &serpent.Cmd{
Use: "access-token <provider>",
Short: "Print auth for an external provider",
Long: "Print an access-token for an external auth provider. " +
@ -52,17 +52,17 @@ fi
Command: "coder external-auth access-token slack --extra \"authed_user.id\"",
},
),
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
Options: clibase.OptionSet{{
Options: serpent.OptionSet{{
Name: "Extra",
Flag: "extra",
Description: "Extract a field from the \"extra\" properties of the OAuth token.",
Value: clibase.StringOf(&extra),
Value: serpent.StringOf(&extra),
}},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
ctx, stop := inv.SignalNotifyContext(ctx, StopSignals...)

View File

@ -5,22 +5,22 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) favorite() *clibase.Cmd {
func (r *RootCmd) favorite() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Aliases: []string{"fav", "favou" + "rite"},
Annotations: workspaceCommand,
Use: "favorite <workspace>",
Short: "Add a workspace to your favorites",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)
@ -36,18 +36,18 @@ func (r *RootCmd) favorite() *clibase.Cmd {
return cmd
}
func (r *RootCmd) unfavorite() *clibase.Cmd {
func (r *RootCmd) unfavorite() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Aliases: []string{"unfav", "unfavou" + "rite"},
Annotations: workspaceCommand,
Use: "unfavorite <workspace>",
Short: "Remove a workspace from your favorites",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)

View File

@ -8,21 +8,21 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/gitauth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/retry"
"github.com/coder/serpent"
)
// gitAskpass is used by the Coder agent to automatically authenticate
// with Git providers based on a hostname.
func (r *RootCmd) gitAskpass() *clibase.Cmd {
return &clibase.Cmd{
func (r *RootCmd) gitAskpass() *serpent.Cmd {
return &serpent.Cmd{
Use: "gitaskpass",
Hidden: true,
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
ctx, stop := inv.SignalNotifyContext(ctx, StopSignals...)

View File

@ -13,17 +13,17 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) gitssh() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) gitssh() *serpent.Cmd {
cmd := &serpent.Cmd{
Use: "gitssh",
Hidden: true,
Short: `Wraps the "ssh" command and uses the coder gitssh key for authentication`,
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
env := os.Environ()

View File

@ -15,9 +15,9 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
//go:embed help.tpl
@ -26,7 +26,7 @@ var helpTemplateRaw string
type optionGroup struct {
Name string
Description string
Options clibase.OptionSet
Options serpent.OptionSet
}
func ttyWidth() int {
@ -75,9 +75,9 @@ var usageTemplate = func() *template.Template {
headerFg.Format(txt)
return txt.String()
},
"typeHelper": func(opt *clibase.Option) string {
"typeHelper": func(opt *serpent.Option) string {
switch v := opt.Value.(type) {
case *clibase.Enum:
case *serpent.Enum:
return strings.Join(v.Choices, "|")
default:
return v.Type()
@ -107,7 +107,7 @@ var usageTemplate = func() *template.Template {
}
return sb.String()
},
"formatSubcommand": func(cmd *clibase.Cmd) string {
"formatSubcommand": func(cmd *serpent.Cmd) string {
// Minimize padding by finding the longest neighboring name.
maxNameLength := len(cmd.Name())
if parent := cmd.Parent; parent != nil {
@ -142,23 +142,23 @@ var usageTemplate = func() *template.Template {
return sb.String()
},
"envName": func(opt clibase.Option) string {
"envName": func(opt serpent.Option) string {
if opt.Env == "" {
return ""
}
return opt.Env
},
"flagName": func(opt clibase.Option) string {
"flagName": func(opt serpent.Option) string {
return opt.Flag
},
"isEnterprise": func(opt clibase.Option) bool {
"isEnterprise": func(opt serpent.Option) bool {
return opt.Annotations.IsSet("enterprise")
},
"isDeprecated": func(opt clibase.Option) bool {
"isDeprecated": func(opt serpent.Option) bool {
return len(opt.UseInstead) > 0
},
"useInstead": func(opt clibase.Option) string {
"useInstead": func(opt serpent.Option) string {
var sb strings.Builder
for i, s := range opt.UseInstead {
if i > 0 {
@ -189,12 +189,12 @@ var usageTemplate = func() *template.Template {
s = wrapTTY(s)
return s
},
"visibleChildren": func(cmd *clibase.Cmd) []*clibase.Cmd {
return filterSlice(cmd.Children, func(c *clibase.Cmd) bool {
"visibleChildren": func(cmd *serpent.Cmd) []*serpent.Cmd {
return filterSlice(cmd.Children, func(c *serpent.Cmd) bool {
return !c.Hidden
})
},
"optionGroups": func(cmd *clibase.Cmd) []optionGroup {
"optionGroups": func(cmd *serpent.Cmd) []optionGroup {
groups := []optionGroup{{
// Default group.
Name: "",
@ -240,7 +240,7 @@ var usageTemplate = func() *template.Template {
groups = append(groups, optionGroup{
Name: groupName,
Description: opt.Group.Description,
Options: clibase.OptionSet{opt},
Options: serpent.OptionSet{opt},
})
}
sort.Slice(groups, func(i, j int) bool {
@ -318,8 +318,8 @@ var usageWantsArgRe = regexp.MustCompile(`<.*>`)
// helpFn returns a function that generates usage (help)
// output for a given command.
func helpFn() clibase.HandlerFunc {
return func(inv *clibase.Invocation) error {
func helpFn() serpent.HandlerFunc {
return func(inv *serpent.Invocation) error {
// We use stdout for help and not stderr since there's no straightforward
// way to distinguish between a user error and a help request.
//

View File

@ -8,10 +8,10 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
// workspaceListRow is the type provided to the OutputFormatter. This is a bit
@ -70,7 +70,7 @@ func workspaceListRowFromWorkspace(now time.Time, workspace codersdk.Workspace)
}
}
func (r *RootCmd) list() *clibase.Cmd {
func (r *RootCmd) list() *serpent.Cmd {
var (
filter cliui.WorkspaceFilter
formatter = cliui.NewOutputFormatter(
@ -92,16 +92,16 @@ func (r *RootCmd) list() *clibase.Cmd {
)
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "list",
Short: "List workspaces",
Aliases: []string{"ls"},
Middleware: clibase.Chain(
clibase.RequireNArgs(0),
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
res, err := queryConvertWorkspaces(inv.Context(), client, filter.Filter(), workspaceListRowFromWorkspace)
if err != nil {
return err

View File

@ -19,10 +19,10 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/userpassword"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
const (
@ -40,7 +40,7 @@ func init() {
browser.Stdout = io.Discard
}
func promptFirstUsername(inv *clibase.Invocation) (string, error) {
func promptFirstUsername(inv *serpent.Invocation) (string, error) {
currentUser, err := user.Current()
if err != nil {
return "", xerrors.Errorf("get current user: %w", err)
@ -59,7 +59,7 @@ func promptFirstUsername(inv *clibase.Invocation) (string, error) {
return username, nil
}
func promptFirstPassword(inv *clibase.Invocation) (string, error) {
func promptFirstPassword(inv *serpent.Invocation) (string, error) {
retry:
password, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password") + ":",
@ -89,7 +89,7 @@ retry:
}
func (r *RootCmd) loginWithPassword(
inv *clibase.Invocation,
inv *serpent.Invocation,
client *codersdk.Client,
email, password string,
) error {
@ -125,7 +125,7 @@ func (r *RootCmd) loginWithPassword(
return nil
}
func (r *RootCmd) login() *clibase.Cmd {
func (r *RootCmd) login() *serpent.Cmd {
const firstUserTrialEnv = "CODER_FIRST_USER_TRIAL"
var (
@ -135,11 +135,11 @@ func (r *RootCmd) login() *clibase.Cmd {
trial bool
useTokenForSession bool
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "login [<url>]",
Short: "Authenticate with Coder deployment",
Middleware: clibase.RequireRangeArgs(0, 1),
Handler: func(inv *clibase.Invocation) error {
Middleware: serpent.RequireRangeArgs(0, 1),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
rawURL := ""
var urlSource string
@ -350,35 +350,35 @@ func (r *RootCmd) login() *clibase.Cmd {
return nil
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "first-user-email",
Env: "CODER_FIRST_USER_EMAIL",
Description: "Specifies an email address to use if creating the first user for the deployment.",
Value: clibase.StringOf(&email),
Value: serpent.StringOf(&email),
},
{
Flag: "first-user-username",
Env: "CODER_FIRST_USER_USERNAME",
Description: "Specifies a username to use if creating the first user for the deployment.",
Value: clibase.StringOf(&username),
Value: serpent.StringOf(&username),
},
{
Flag: "first-user-password",
Env: "CODER_FIRST_USER_PASSWORD",
Description: "Specifies a password to use if creating the first user for the deployment.",
Value: clibase.StringOf(&password),
Value: serpent.StringOf(&password),
},
{
Flag: "first-user-trial",
Env: firstUserTrialEnv,
Description: "Specifies whether a trial license should be provisioned for the Coder deployment or not.",
Value: clibase.BoolOf(&trial),
Value: serpent.BoolOf(&trial),
},
{
Flag: "use-token-as-session",
Description: "By default, the CLI will generate a new session token when logging in. This flag will instead use the provided token as the session token.",
Value: clibase.BoolOf(&useTokenForSession),
Value: serpent.BoolOf(&useTokenForSession),
},
}
return cmd
@ -397,7 +397,7 @@ func isWSL() (bool, error) {
}
// openURL opens the provided URL via user's default browser
func openURL(inv *clibase.Invocation, urlToOpen string) error {
func openURL(inv *serpent.Invocation, urlToOpen string) error {
noOpen, err := inv.ParsedFlags().GetBool(varNoOpen)
if err != nil {
panic(err)

View File

@ -7,20 +7,20 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) logout() *clibase.Cmd {
func (r *RootCmd) logout() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "logout",
Short: "Unauthenticate your local session",
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var errors []error
config := r.createConfig()

View File

@ -8,21 +8,21 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) netcheck() *clibase.Cmd {
func (r *RootCmd) netcheck() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "netcheck",
Short: "Print network debug information for DERP and STUN",
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithTimeout(inv.Context(), 30*time.Second)
defer cancel()
@ -56,6 +56,6 @@ func (r *RootCmd) netcheck() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{}
cmd.Options = serpent.OptionSet{}
return cmd
}

View File

@ -12,19 +12,19 @@ import (
"github.com/skratchdot/open-golang/open"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) open() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) open() *serpent.Cmd {
cmd := &serpent.Cmd{
Use: "open",
Short: "Open a workspace",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.openVSCode(),
},
}
@ -33,22 +33,22 @@ func (r *RootCmd) open() *clibase.Cmd {
const vscodeDesktopName = "VS Code Desktop"
func (r *RootCmd) openVSCode() *clibase.Cmd {
func (r *RootCmd) openVSCode() *serpent.Cmd {
var (
generateToken bool
testOpenError bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "vscode <workspace> [<directory in workspace>]",
Short: fmt.Sprintf("Open a workspace in %s", vscodeDesktopName),
Middleware: clibase.Chain(
clibase.RequireRangeArgs(1, 2),
Middleware: serpent.Chain(
serpent.RequireRangeArgs(1, 2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
@ -186,7 +186,7 @@ func (r *RootCmd) openVSCode() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "generate-token",
Env: "CODER_OPEN_VSCODE_GENERATE_TOKEN",
@ -195,12 +195,12 @@ func (r *RootCmd) openVSCode() *clibase.Cmd {
"This flag does not need to be specified when running this command on a local machine unless automatic open fails.",
vscodeDesktopName,
),
Value: clibase.BoolOf(&generateToken),
Value: serpent.BoolOf(&generateToken),
},
{
Flag: "test.open-error",
Description: "Don't run the open command.",
Value: clibase.BoolOf(&testOpenError),
Value: serpent.BoolOf(&testOpenError),
Hidden: true, // This is for testing!
},
}

View File

@ -9,38 +9,38 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) organizations() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) organizations() *serpent.Cmd {
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "organizations [subcommand]",
Short: "Organization related commands",
Aliases: []string{"organization", "org", "orgs"},
Hidden: true, // Hidden until these commands are complete.
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.currentOrganization(),
r.switchOrganization(),
r.createOrganization(),
},
}
cmd.Options = clibase.OptionSet{}
cmd.Options = serpent.OptionSet{}
return cmd
}
func (r *RootCmd) switchOrganization() *clibase.Cmd {
func (r *RootCmd) switchOrganization() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "set <organization name | ID>",
Short: "set the organization used by the CLI. Pass an empty string to reset to the default organization.",
Long: "set the organization used by the CLI. Pass an empty string to reset to the default organization.\n" + formatExamples(
@ -53,12 +53,12 @@ func (r *RootCmd) switchOrganization() *clibase.Cmd {
Command: "coder organizations set my-org",
},
),
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
clibase.RequireRangeArgs(0, 1),
serpent.RequireRangeArgs(0, 1),
),
Options: clibase.OptionSet{},
Handler: func(inv *clibase.Invocation) error {
Options: serpent.OptionSet{},
Handler: func(inv *serpent.Invocation) error {
conf := r.createConfig()
orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me)
if err != nil {
@ -138,7 +138,7 @@ func (r *RootCmd) switchOrganization() *clibase.Cmd {
// promptUserSelectOrg will prompt the user to select an organization from a list
// of their organizations.
func promptUserSelectOrg(inv *clibase.Invocation, conf config.Root, orgs []codersdk.Organization) (string, error) {
func promptUserSelectOrg(inv *serpent.Invocation, conf config.Root, orgs []codersdk.Organization) (string, error) {
// Default choice
var defaultOrg string
// Comes from config file
@ -206,7 +206,7 @@ func orgNames(orgs []codersdk.Organization) []string {
return names
}
func (r *RootCmd) currentOrganization() *clibase.Cmd {
func (r *RootCmd) currentOrganization() *serpent.Cmd {
var (
stringFormat func(orgs []codersdk.Organization) (string, error)
client = new(codersdk.Client)
@ -224,23 +224,23 @@ func (r *RootCmd) currentOrganization() *clibase.Cmd {
)
onlyID = false
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "show [current|me|uuid]",
Short: "Show the organization, if no argument is given, the organization currently in use will be shown.",
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
clibase.RequireRangeArgs(0, 1),
serpent.RequireRangeArgs(0, 1),
),
Options: clibase.OptionSet{
Options: serpent.OptionSet{
{
Name: "only-id",
Description: "Only print the organization ID.",
Required: false,
Flag: "only-id",
Value: clibase.BoolOf(&onlyID),
Value: serpent.BoolOf(&onlyID),
},
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
orgArg := "current"
if len(inv.Args) >= 1 {
orgArg = inv.Args[0]

View File

@ -6,28 +6,28 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) createOrganization() *clibase.Cmd {
func (r *RootCmd) createOrganization() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "create <organization name>",
Short: "Create a new organization.",
// This action is currently irreversible, so it's hidden until we have a way to delete organizations.
Hidden: true,
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
clibase.RequireNArgs(1),
serpent.RequireNArgs(1),
),
Options: clibase.OptionSet{
Options: serpent.OptionSet{
cliui.SkipPromptOption(),
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
orgName := inv.Args[0]
// This check is not perfect since not all users can read all organizations.

View File

@ -9,8 +9,8 @@ import (
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
// workspaceParameterFlags are used by commands processing rich parameters and/or build options.
@ -24,49 +24,49 @@ type workspaceParameterFlags struct {
promptRichParameters bool
}
func (wpf *workspaceParameterFlags) allOptions() []clibase.Option {
func (wpf *workspaceParameterFlags) allOptions() []serpent.Option {
options := append(wpf.cliBuildOptions(), wpf.cliParameters()...)
return append(options, wpf.alwaysPrompt())
}
func (wpf *workspaceParameterFlags) cliBuildOptions() []clibase.Option {
return clibase.OptionSet{
func (wpf *workspaceParameterFlags) cliBuildOptions() []serpent.Option {
return serpent.OptionSet{
{
Flag: "build-option",
Env: "CODER_BUILD_OPTION",
Description: `Build option value in the format "name=value".`,
Value: clibase.StringArrayOf(&wpf.buildOptions),
Value: serpent.StringArrayOf(&wpf.buildOptions),
},
{
Flag: "build-options",
Description: "Prompt for one-time build options defined with ephemeral parameters.",
Value: clibase.BoolOf(&wpf.promptBuildOptions),
Value: serpent.BoolOf(&wpf.promptBuildOptions),
},
}
}
func (wpf *workspaceParameterFlags) cliParameters() []clibase.Option {
return clibase.OptionSet{
clibase.Option{
func (wpf *workspaceParameterFlags) cliParameters() []serpent.Option {
return serpent.OptionSet{
serpent.Option{
Flag: "parameter",
Env: "CODER_RICH_PARAMETER",
Description: `Rich parameter value in the format "name=value".`,
Value: clibase.StringArrayOf(&wpf.richParameters),
Value: serpent.StringArrayOf(&wpf.richParameters),
},
clibase.Option{
serpent.Option{
Flag: "rich-parameter-file",
Env: "CODER_RICH_PARAMETER_FILE",
Description: "Specify a file path with values for rich parameters defined in the template.",
Value: clibase.StringOf(&wpf.richParameterFile),
Value: serpent.StringOf(&wpf.richParameterFile),
},
}
}
func (wpf *workspaceParameterFlags) alwaysPrompt() clibase.Option {
return clibase.Option{
func (wpf *workspaceParameterFlags) alwaysPrompt() serpent.Option {
return serpent.Option{
Flag: "always-prompt",
Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.",
Value: clibase.BoolOf(&wpf.promptRichParameters),
Value: serpent.BoolOf(&wpf.promptRichParameters),
}
}

View File

@ -6,11 +6,11 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil/levenshtein"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
type WorkspaceCLIAction int
@ -69,7 +69,7 @@ func (pr *ParameterResolver) WithPromptBuildOptions(promptBuildOptions bool) *Pa
return pr
}
func (pr *ParameterResolver) Resolve(inv *clibase.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) {
func (pr *ParameterResolver) Resolve(inv *serpent.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) {
var staged []codersdk.WorkspaceBuildParameter
var err error
@ -209,7 +209,7 @@ func (pr *ParameterResolver) verifyConstraints(resolved []codersdk.WorkspaceBuil
return nil
}
func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuildParameter, inv *clibase.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) {
func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuildParameter, inv *serpent.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) {
for _, tvp := range templateVersionParameters {
p := findWorkspaceBuildParameter(tvp.Name, resolved)
if p != nil {

View File

@ -12,12 +12,12 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) ping() *clibase.Cmd {
func (r *RootCmd) ping() *serpent.Cmd {
var (
pingNum int64
pingTimeout time.Duration
@ -25,15 +25,15 @@ func (r *RootCmd) ping() *clibase.Cmd {
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "ping <workspace>",
Short: "Ping a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
@ -143,26 +143,26 @@ func (r *RootCmd) ping() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "wait",
Description: "Specifies how long to wait between pings.",
Default: "1s",
Value: clibase.DurationOf(&pingWait),
Value: serpent.DurationOf(&pingWait),
},
{
Flag: "timeout",
FlagShorthand: "t",
Default: "5s",
Description: "Specifies how long to wait for a ping to complete.",
Value: clibase.DurationOf(&pingTimeout),
Value: serpent.DurationOf(&pingTimeout),
},
{
Flag: "num",
FlagShorthand: "n",
Default: "10",
Description: "Specifies the number of pings to perform.",
Value: clibase.Int64Of(&pingNum),
Value: serpent.Int64Of(&pingNum),
},
}
return cmd

View File

@ -18,19 +18,19 @@ import (
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) portForward() *clibase.Cmd {
func (r *RootCmd) portForward() *serpent.Cmd {
var (
tcpForwards []string // <port>:<port>
udpForwards []string // <port>:<port>
disableAutostart bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "port-forward <workspace>",
Short: `Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R".`,
Aliases: []string{"tunnel"},
@ -56,11 +56,11 @@ func (r *RootCmd) portForward() *clibase.Cmd {
Command: "coder port-forward <workspace> --tcp 1.2.3.4:8080:8080",
},
),
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
@ -171,21 +171,21 @@ func (r *RootCmd) portForward() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "tcp",
FlagShorthand: "p",
Env: "CODER_PORT_FORWARD_TCP",
Description: "Forward TCP port(s) from the workspace to the local machine.",
Value: clibase.StringArrayOf(&tcpForwards),
Value: serpent.StringArrayOf(&tcpForwards),
},
{
Flag: "udp",
Env: "CODER_PORT_FORWARD_UDP",
Description: "Forward UDP port(s) from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols.",
Value: clibase.StringArrayOf(&udpForwards),
Value: serpent.StringArrayOf(&udpForwards),
},
sshDisableAutostartOption(clibase.BoolOf(&disableAutostart)),
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)),
}
return cmd
@ -193,7 +193,7 @@ func (r *RootCmd) portForward() *clibase.Cmd {
func listenAndPortForward(
ctx context.Context,
inv *clibase.Invocation,
inv *serpent.Invocation,
conn *codersdk.WorkspaceAgentConn,
wg *sync.WaitGroup,
spec portForwardSpec,

View File

@ -6,21 +6,21 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/serpent"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) publickey() *clibase.Cmd {
func (r *RootCmd) publickey() *serpent.Cmd {
var reset bool
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "publickey",
Aliases: []string{"pubkey"},
Short: "Output your Coder public key used for Git operations",
Middleware: r.InitClient(client),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
if reset {
// Confirm prompt if using --reset. We don't want to accidentally
// reset our public key.
@ -58,11 +58,11 @@ func (r *RootCmd) publickey() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "reset",
Description: "Regenerate your public key. This will require updating the key on any services it's registered with.",
Value: clibase.BoolOf(&reset),
Value: serpent.BoolOf(&reset),
},
cliui.SkipPromptOption(),
}

View File

@ -6,23 +6,23 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/serpent"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) rename() *clibase.Cmd {
func (r *RootCmd) rename() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "rename <workspace> <new name>",
Short: "Rename a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(2),
Middleware: serpent.Chain(
serpent.RequireNArgs(2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)

View File

@ -9,22 +9,22 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/serpent"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/migrations"
"github.com/coder/coder/v2/coderd/userpassword"
)
func (*RootCmd) resetPassword() *clibase.Cmd {
func (*RootCmd) resetPassword() *serpent.Cmd {
var postgresURL string
root := &clibase.Cmd{
root := &serpent.Cmd{
Use: "reset-password <username>",
Short: "Directly connect to the database to reset a user's password",
Middleware: clibase.RequireNArgs(1),
Handler: func(inv *clibase.Invocation) error {
Middleware: serpent.RequireNArgs(1),
Handler: func(inv *serpent.Invocation) error {
username := inv.Args[0]
sqlDB, err := sql.Open("postgres", postgresURL)
@ -90,12 +90,12 @@ func (*RootCmd) resetPassword() *clibase.Cmd {
},
}
root.Options = clibase.OptionSet{
root.Options = serpent.OptionSet{
{
Flag: "postgres-url",
Description: "URL of a PostgreSQL database to connect to.",
Env: "CODER_PG_CONNECTION_URL",
Value: clibase.StringOf(&postgresURL),
Value: serpent.StringOf(&postgresURL),
},
}

View File

@ -2,18 +2,16 @@
package cli
import (
"github.com/coder/coder/v2/cli/clibase"
)
import "github.com/coder/serpent"
func (*RootCmd) resetPassword() *clibase.Cmd {
root := &clibase.Cmd{
func (*RootCmd) resetPassword() *serpent.Cmd {
root := &serpent.Cmd{
Use: "reset-password <username>",
Short: "Directly connect to the database to reset a user's password",
// We accept RawArgs so all commands and flags are accepted.
RawArgs: true,
Hidden: true,
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
SlimUnsupported(inv.Stderr, "reset-password")
return nil
},

View File

@ -7,26 +7,26 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) restart() *clibase.Cmd {
func (r *RootCmd) restart() *serpent.Cmd {
var parameterFlags workspaceParameterFlags
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "restart <workspace>",
Short: "Restart a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Options: clibase.OptionSet{cliui.SkipPromptOption()},
Handler: func(inv *clibase.Invocation) error {
Options: serpent.OptionSet{cliui.SkipPromptOption()},
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
out := inv.Stdout

View File

@ -31,13 +31,13 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/cli/gitauth"
"github.com/coder/coder/v2/cli/telemetry"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/serpent"
)
var (
@ -84,9 +84,9 @@ var (
errUnauthenticatedURLSaved = xerrors.New(notLoggedInURLSavedMessage)
)
func (r *RootCmd) Core() []*clibase.Cmd {
func (r *RootCmd) Core() []*serpent.Cmd {
// Please re-sort this list alphabetically if you change it!
return []*clibase.Cmd{
return []*serpent.Cmd{
r.dotfiles(),
r.externalAuth(),
r.login(),
@ -132,13 +132,13 @@ func (r *RootCmd) Core() []*clibase.Cmd {
}
}
func (r *RootCmd) AGPL() []*clibase.Cmd {
func (r *RootCmd) AGPL() []*serpent.Cmd {
all := append(r.Core(), r.Server( /* Do not import coderd here. */ nil))
return all
}
// Main is the entrypoint for the Coder CLI.
func (r *RootCmd) RunMain(subcommands []*clibase.Cmd) {
func (r *RootCmd) RunMain(subcommands []*serpent.Cmd) {
rand.Seed(time.Now().UnixMicro())
cmd, err := r.Command(subcommands)
@ -166,10 +166,10 @@ func (r *RootCmd) RunMain(subcommands []*clibase.Cmd) {
}
}
func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
func (r *RootCmd) Command(subcommands []*serpent.Cmd) (*serpent.Cmd, error) {
fmtLong := `Coder %s A tool for provisioning self-hosted development environments with Terraform.
`
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "coder [global-flags] <subcommand>",
Long: fmt.Sprintf(fmtLong, buildinfo.Version()) + formatExamples(
example{
@ -181,7 +181,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
Command: "coder templates init",
},
),
Handler: func(i *clibase.Invocation) error {
Handler: func(i *serpent.Invocation) error {
if r.versionFlag {
return r.version(defaultVersionInfo).Handler(i)
}
@ -200,7 +200,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
cmd.AddSubcommands(subcommands...)
// Set default help handler for all commands.
cmd.Walk(func(c *clibase.Cmd) {
cmd.Walk(func(c *serpent.Cmd) {
if c.HelpHandler == nil {
c.HelpHandler = helpFn()
}
@ -208,7 +208,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
var merr error
// Add [flags] to usage when appropriate.
cmd.Walk(func(cmd *clibase.Cmd) {
cmd.Walk(func(cmd *serpent.Cmd) {
const flags = "[flags]"
if strings.Contains(cmd.Use, flags) {
merr = errors.Join(
@ -244,7 +244,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
})
// Add alises when appropriate.
cmd.Walk(func(cmd *clibase.Cmd) {
cmd.Walk(func(cmd *serpent.Cmd) {
// TODO: we should really be consistent about naming.
if cmd.Name() == "delete" || cmd.Name() == "remove" {
if slices.Contains(cmd.Aliases, "rm") {
@ -259,7 +259,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
})
// Sanity-check command options.
cmd.Walk(func(cmd *clibase.Cmd) {
cmd.Walk(func(cmd *serpent.Cmd) {
for _, opt := range cmd.Options {
// Verify that every option is configurable.
if opt.Flag == "" && opt.Env == "" {
@ -282,7 +282,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
var debugOptions bool
// Add a wrapper to every command to enable debugging options.
cmd.Walk(func(cmd *clibase.Cmd) {
cmd.Walk(func(cmd *serpent.Cmd) {
h := cmd.Handler
if h == nil {
// We should never have a nil handler, but if we do, do not
@ -291,12 +291,12 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
// is required for a command such as command grouping (e.g. `users'
// and 'groups'), then the handler should be set to the helper
// function.
// func(inv *clibase.Invocation) error {
// func(inv *serpent.Invocation) error {
// return inv.Command.HelpHandler(inv)
// }
return
}
cmd.Handler = func(i *clibase.Invocation) error {
cmd.Handler = func(i *serpent.Invocation) error {
if !debugOptions {
return h(i)
}
@ -318,36 +318,36 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
r.clientURL = new(url.URL)
}
globalGroup := &clibase.Group{
globalGroup := &serpent.Group{
Name: "Global",
Description: `Global options are applied to all commands. They can be set using environment variables or flags.`,
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: varURL,
Env: envURL,
Description: "URL to a deployment.",
Value: clibase.URLOf(r.clientURL),
Value: serpent.URLOf(r.clientURL),
Group: globalGroup,
},
{
Flag: "debug-options",
Description: "Print all options, how they're set, then exit.",
Value: clibase.BoolOf(&debugOptions),
Value: serpent.BoolOf(&debugOptions),
Group: globalGroup,
},
{
Flag: varToken,
Env: envSessionToken,
Description: fmt.Sprintf("Specify an authentication token. For security reasons setting %s is preferred.", envSessionToken),
Value: clibase.StringOf(&r.token),
Value: serpent.StringOf(&r.token),
Group: globalGroup,
},
{
Flag: varAgentToken,
Env: envAgentToken,
Description: "An agent authentication token.",
Value: clibase.StringOf(&r.agentToken),
Value: serpent.StringOf(&r.agentToken),
Hidden: true,
Group: globalGroup,
},
@ -355,7 +355,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
Flag: varAgentTokenFile,
Env: envAgentTokenFile,
Description: "A file containing an agent authentication token.",
Value: clibase.StringOf(&r.agentTokenFile),
Value: serpent.StringOf(&r.agentTokenFile),
Hidden: true,
Group: globalGroup,
},
@ -363,7 +363,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
Flag: varAgentURL,
Env: "CODER_AGENT_URL",
Description: "URL for an agent to access your deployment.",
Value: clibase.URLOf(r.agentURL),
Value: serpent.URLOf(r.agentURL),
Hidden: true,
Group: globalGroup,
},
@ -371,35 +371,35 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
Flag: varNoVersionCheck,
Env: envNoVersionCheck,
Description: "Suppress warning when client and server versions do not match.",
Value: clibase.BoolOf(&r.noVersionCheck),
Value: serpent.BoolOf(&r.noVersionCheck),
Group: globalGroup,
},
{
Flag: varNoFeatureWarning,
Env: envNoFeatureWarning,
Description: "Suppress warnings about unlicensed features.",
Value: clibase.BoolOf(&r.noFeatureWarning),
Value: serpent.BoolOf(&r.noFeatureWarning),
Group: globalGroup,
},
{
Flag: varHeader,
Env: "CODER_HEADER",
Description: "Additional HTTP headers added to all requests. Provide as " + `key=value` + ". Can be specified multiple times.",
Value: clibase.StringArrayOf(&r.header),
Value: serpent.StringArrayOf(&r.header),
Group: globalGroup,
},
{
Flag: varHeaderCommand,
Env: "CODER_HEADER_COMMAND",
Description: "An external command that outputs additional HTTP headers added to all requests. The command must output each header as `key=value` on its own line.",
Value: clibase.StringOf(&r.headerCommand),
Value: serpent.StringOf(&r.headerCommand),
Group: globalGroup,
},
{
Flag: varNoOpen,
Env: "CODER_NO_OPEN",
Description: "Suppress opening the browser after logging in.",
Value: clibase.BoolOf(&r.noOpen),
Value: serpent.BoolOf(&r.noOpen),
Hidden: true,
Group: globalGroup,
},
@ -408,7 +408,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
Env: "CODER_FORCE_TTY",
Hidden: true,
Description: "Force the use of a TTY.",
Value: clibase.BoolOf(&r.forceTTY),
Value: serpent.BoolOf(&r.forceTTY),
Group: globalGroup,
},
{
@ -416,20 +416,20 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
FlagShorthand: "v",
Env: "CODER_VERBOSE",
Description: "Enable verbose output.",
Value: clibase.BoolOf(&r.verbose),
Value: serpent.BoolOf(&r.verbose),
Group: globalGroup,
},
{
Flag: varDisableDirect,
Env: "CODER_DISABLE_DIRECT_CONNECTIONS",
Description: "Disable direct (P2P) connections to workspaces.",
Value: clibase.BoolOf(&r.disableDirect),
Value: serpent.BoolOf(&r.disableDirect),
Group: globalGroup,
},
{
Flag: "debug-http",
Description: "Debug codersdk HTTP requests.",
Value: clibase.BoolOf(&r.debugHTTP),
Value: serpent.BoolOf(&r.debugHTTP),
Group: globalGroup,
Hidden: true,
},
@ -438,7 +438,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
Env: "CODER_CONFIG_DIR",
Description: "Path to the global `coder` config directory.",
Default: config.DefaultDir(),
Value: clibase.StringOf(&r.globalConfig),
Value: serpent.StringOf(&r.globalConfig),
Group: globalGroup,
},
{
@ -446,7 +446,7 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
FlagShorthand: "z",
Env: "CODER_ORGANIZATION",
Description: "Select which organization (uuid or name) to use This overrides what is present in the config file.",
Value: clibase.StringOf(&r.organizationSelect),
Value: serpent.StringOf(&r.organizationSelect),
Group: globalGroup,
},
{
@ -455,16 +455,11 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
// They have two Coder CLIs, and want to tell the difference by running
// the same base command.
Description: "Run the version command. Useful for v1 customers migrating to v2.",
Value: clibase.BoolOf(&r.versionFlag),
Value: serpent.BoolOf(&r.versionFlag),
Hidden: true,
},
}
err := cmd.PrepareAll()
if err != nil {
return nil, err
}
return cmd, nil
}
@ -490,7 +485,7 @@ type RootCmd struct {
noFeatureWarning bool
}
func addTelemetryHeader(client *codersdk.Client, inv *clibase.Invocation) {
func addTelemetryHeader(client *codersdk.Client, inv *serpent.Invocation) {
transport, ok := client.HTTPClient.Transport.(*codersdk.HeaderTransport)
if !ok {
transport = &codersdk.HeaderTransport{
@ -502,7 +497,7 @@ func addTelemetryHeader(client *codersdk.Client, inv *clibase.Invocation) {
var topts []telemetry.Option
for _, opt := range inv.Command.FullOptions() {
if opt.ValueSource == clibase.ValueSourceNone || opt.ValueSource == clibase.ValueSourceDefault {
if opt.ValueSource == serpent.ValueSourceNone || opt.ValueSource == serpent.ValueSourceDefault {
continue
}
topts = append(topts, telemetry.Option{
@ -534,28 +529,28 @@ func addTelemetryHeader(client *codersdk.Client, inv *clibase.Invocation) {
// InitClient sets client to a new client.
// It reads from global configuration files if flags are not set.
func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc {
return clibase.Chain(
func (r *RootCmd) InitClient(client *codersdk.Client) serpent.MiddlewareFunc {
return serpent.Chain(
r.initClientInternal(client, false),
// By default, we should print warnings in addition to initializing the client
r.PrintWarnings(client),
)
}
func (r *RootCmd) InitClientMissingTokenOK(client *codersdk.Client) clibase.MiddlewareFunc {
func (r *RootCmd) InitClientMissingTokenOK(client *codersdk.Client) serpent.MiddlewareFunc {
return r.initClientInternal(client, true)
}
// nolint: revive
func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing bool) clibase.MiddlewareFunc {
func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing bool) serpent.MiddlewareFunc {
if client == nil {
panic("client is nil")
}
if r == nil {
panic("root is nil")
}
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(inv *clibase.Invocation) error {
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(inv *serpent.Invocation) error {
conf := r.createConfig()
var err error
if r.clientURL == nil || r.clientURL.String() == "" {
@ -604,15 +599,15 @@ func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing
}
}
func (r *RootCmd) PrintWarnings(client *codersdk.Client) clibase.MiddlewareFunc {
func (r *RootCmd) PrintWarnings(client *codersdk.Client) serpent.MiddlewareFunc {
if client == nil {
panic("client is nil")
}
if r == nil {
panic("root is nil")
}
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(inv *clibase.Invocation) error {
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(inv *serpent.Invocation) error {
// We send these requests in parallel to minimize latency.
var (
versionErr = make(chan error)
@ -715,7 +710,7 @@ func (r *RootCmd) createAgentClient() (*agentsdk.Client, error) {
}
// CurrentOrganization returns the currently active organization for the authenticated user.
func CurrentOrganization(r *RootCmd, inv *clibase.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
func CurrentOrganization(r *RootCmd, inv *serpent.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
conf := r.createConfig()
selected := r.organizationSelect
if selected == "" && conf.Organization().Exists() {
@ -795,7 +790,7 @@ func (r *RootCmd) createConfig() config.Root {
}
// isTTY returns whether the passed reader is a TTY or not.
func isTTY(inv *clibase.Invocation) bool {
func isTTY(inv *serpent.Invocation) bool {
// If the `--force-tty` command is available, and set,
// assume we're in a tty. This is primarily for cases on Windows
// where we may not be able to reliably detect this automatically (ie, tests)
@ -811,16 +806,16 @@ func isTTY(inv *clibase.Invocation) bool {
}
// isTTYOut returns whether the passed reader is a TTY or not.
func isTTYOut(inv *clibase.Invocation) bool {
func isTTYOut(inv *serpent.Invocation) bool {
return isTTYWriter(inv, inv.Stdout)
}
// isTTYErr returns whether the passed reader is a TTY or not.
func isTTYErr(inv *clibase.Invocation) bool {
func isTTYErr(inv *serpent.Invocation) bool {
return isTTYWriter(inv, inv.Stderr)
}
func isTTYWriter(inv *clibase.Invocation, writer io.Writer) bool {
func isTTYWriter(inv *serpent.Invocation, writer io.Writer) bool {
// If the `--force-tty` command is available, and set,
// assume we're in a tty. This is primarily for cases on Windows
// where we may not be able to reliably detect this automatically (ie, tests)
@ -871,7 +866,7 @@ func formatExamples(examples ...example) string {
// is detected. forceCheck is a test flag and should always be false in production.
//
//nolint:revive
func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client, clientVersion string) error {
func (r *RootCmd) checkVersions(i *serpent.Invocation, client *codersdk.Client, clientVersion string) error {
if r.noVersionCheck {
return nil
}
@ -905,7 +900,7 @@ func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client,
return nil
}
func (r *RootCmd) checkWarnings(i *clibase.Invocation, client *codersdk.Client) error {
func (r *RootCmd) checkWarnings(i *serpent.Invocation, client *codersdk.Client) error {
if r.noFeatureWarning {
return nil
}
@ -931,7 +926,7 @@ func (r *RootCmd) checkWarnings(i *clibase.Invocation, client *codersdk.Client)
}
// Verbosef logs a message if verbose mode is enabled.
func (r *RootCmd) Verbosef(inv *clibase.Invocation, fmtStr string, args ...interface{}) {
func (r *RootCmd) Verbosef(inv *serpent.Invocation, fmtStr string, args ...interface{}) {
if r.verbose {
cliui.Infof(inv.Stdout, fmtStr, args...)
}
@ -1127,7 +1122,7 @@ func cliHumanFormatError(from string, err error, opts *formatOpts) (string, bool
return formatCoderSDKError(from, sdkError, opts), true
}
if cmdErr, ok := err.(*clibase.RunCommandError); ok {
if cmdErr, ok := err.(*serpent.RunCommandError); ok {
// no need to pass the "from" context to this since it is always
// top level. We care about what is below this.
return formatRunCommandError(cmdErr, opts), true
@ -1199,7 +1194,7 @@ func formatMultiError(from string, multi []error, opts *formatOpts) string {
// broad, as it contains all errors that occur when running a command.
// If you know the error is something else, like a codersdk.Error, make a new
// formatter and add it to cliHumanFormatError function.
func formatRunCommandError(err *clibase.RunCommandError, opts *formatOpts) string {
func formatRunCommandError(err *serpent.RunCommandError, opts *formatOpts) string {
var str strings.Builder
_, _ = str.WriteString(pretty.Sprint(headLineStyle(), fmt.Sprintf("Encountered an error running %q", err.Cmd.FullName())))

View File

@ -10,12 +10,12 @@ import (
"sync/atomic"
"testing"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -28,7 +28,7 @@ import (
//nolint:tparallel,paralleltest
func TestCommandHelp(t *testing.T) {
// Test with AGPL commands
getCmds := func(t *testing.T) *clibase.Cmd {
getCmds := func(t *testing.T) *serpent.Cmd {
// Must return a fresh instance of cmds each time.
t.Helper()

View File

@ -8,12 +8,12 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/schedule/cron"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/tz"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
const (
@ -53,15 +53,15 @@ When enabling scheduled stop, enter a duration in one of the following formats:
`
)
func (r *RootCmd) schedules() *clibase.Cmd {
scheduleCmd := &clibase.Cmd{
func (r *RootCmd) schedules() *serpent.Cmd {
scheduleCmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "schedule { show | start | stop | override } <workspace>",
Short: "Schedule automated start and stop times for workspaces",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.scheduleShow(),
r.scheduleStart(),
r.scheduleStop(),
@ -73,7 +73,7 @@ func (r *RootCmd) schedules() *clibase.Cmd {
}
// scheduleShow() is just a wrapper for list() with some different defaults.
func (r *RootCmd) scheduleShow() *clibase.Cmd {
func (r *RootCmd) scheduleShow() *serpent.Cmd {
var (
filter cliui.WorkspaceFilter
formatter = cliui.NewOutputFormatter(
@ -91,15 +91,15 @@ func (r *RootCmd) scheduleShow() *clibase.Cmd {
)
)
client := new(codersdk.Client)
showCmd := &clibase.Cmd{
showCmd := &serpent.Cmd{
Use: "show <workspace | --search <query> | --all>",
Short: "Show workspace schedules",
Long: scheduleShowDescriptionLong,
Middleware: clibase.Chain(
clibase.RequireRangeArgs(0, 1),
Middleware: serpent.Chain(
serpent.RequireRangeArgs(0, 1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
// To preserve existing behavior, if an argument is passed we will
// only show the schedule for that workspace.
// This will clobber the search query if one is passed.
@ -136,9 +136,9 @@ func (r *RootCmd) scheduleShow() *clibase.Cmd {
return showCmd
}
func (r *RootCmd) scheduleStart() *clibase.Cmd {
func (r *RootCmd) scheduleStart() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "start <workspace-name> { <start-time> [day-of-week] [location] | manual }",
Long: scheduleStartDescriptionLong + "\n" + formatExamples(
example{
@ -147,11 +147,11 @@ func (r *RootCmd) scheduleStart() *clibase.Cmd {
},
),
Short: "Edit workspace start schedule",
Middleware: clibase.Chain(
clibase.RequireRangeArgs(2, 4),
Middleware: serpent.Chain(
serpent.RequireRangeArgs(2, 4),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return err
@ -185,9 +185,9 @@ func (r *RootCmd) scheduleStart() *clibase.Cmd {
return cmd
}
func (r *RootCmd) scheduleStop() *clibase.Cmd {
func (r *RootCmd) scheduleStop() *serpent.Cmd {
client := new(codersdk.Client)
return &clibase.Cmd{
return &serpent.Cmd{
Use: "stop <workspace-name> { <duration> | manual }",
Long: scheduleStopDescriptionLong + "\n" + formatExamples(
example{
@ -195,11 +195,11 @@ func (r *RootCmd) scheduleStop() *clibase.Cmd {
},
),
Short: "Edit workspace stop schedule",
Middleware: clibase.Chain(
clibase.RequireNArgs(2),
Middleware: serpent.Chain(
serpent.RequireNArgs(2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return err
@ -229,9 +229,9 @@ func (r *RootCmd) scheduleStop() *clibase.Cmd {
}
}
func (r *RootCmd) scheduleOverride() *clibase.Cmd {
func (r *RootCmd) scheduleOverride() *serpent.Cmd {
client := new(codersdk.Client)
overrideCmd := &clibase.Cmd{
overrideCmd := &serpent.Cmd{
Use: "override-stop <workspace-name> <duration from now>",
Short: "Override the stop time of a currently running workspace instance.",
Long: scheduleOverrideDescriptionLong + "\n" + formatExamples(
@ -239,11 +239,11 @@ func (r *RootCmd) scheduleOverride() *clibase.Cmd {
Command: "coder schedule override-stop my-workspace 90m",
},
),
Middleware: clibase.Chain(
clibase.RequireNArgs(2),
Middleware: serpent.Chain(
serpent.RequireNArgs(2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
overrideDuration, err := parseDuration(inv.Args[1])
if err != nil {
return err

View File

@ -56,7 +56,6 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/clilog"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
@ -99,6 +98,7 @@ import (
"github.com/coder/coder/v2/tailnet"
"github.com/coder/pretty"
"github.com/coder/retry"
"github.com/coder/serpent"
"github.com/coder/wgtunnel/tunnelsdk"
)
@ -258,7 +258,7 @@ func enablePrometheus(
), nil
}
func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *clibase.Cmd {
func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *serpent.Cmd {
if newAPI == nil {
newAPI = func(_ context.Context, o *coderd.Options) (*coderd.API, io.Closer, error) {
api := coderd.New(o)
@ -270,16 +270,16 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
vals = new(codersdk.DeploymentValues)
opts = vals.Options()
)
serverCmd := &clibase.Cmd{
serverCmd := &serpent.Cmd{
Use: "server",
Short: "Start a Coder server",
Options: opts,
Middleware: clibase.Chain(
Middleware: serpent.Chain(
WriteConfigMW(vals),
PrintDeprecatedOptions(),
clibase.RequireNArgs(0),
serpent.RequireNArgs(0),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
// Main command context for managing cancellation of running
// services.
ctx, cancel := context.WithCancel(inv.Context())
@ -432,7 +432,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}
defer tunnel.Close()
tunnelDone = tunnel.Wait()
vals.AccessURL = clibase.URL(*tunnel.URL)
vals.AccessURL = serpent.URL(*tunnel.URL)
if vals.WildcardAccessURL.String() == "" {
// Suffixed wildcard access URL.
@ -1148,10 +1148,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
var pgRawURL bool
postgresBuiltinURLCmd := &clibase.Cmd{
postgresBuiltinURLCmd := &serpent.Cmd{
Use: "postgres-builtin-url",
Short: "Output the connection URL for the built-in PostgreSQL deployment.",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
url, err := embeddedPostgresURL(r.createConfig())
if err != nil {
return err
@ -1165,10 +1165,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
},
}
postgresBuiltinServeCmd := &clibase.Cmd{
postgresBuiltinServeCmd := &serpent.Cmd{
Use: "postgres-builtin-serve",
Short: "Run the built-in PostgreSQL deployment.",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
cfg := r.createConfig()
@ -1199,10 +1199,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
createAdminUserCmd := r.newCreateAdminUserCommand()
rawURLOpt := clibase.Option{
rawURLOpt := serpent.Option{
Flag: "raw-url",
Value: clibase.BoolOf(&pgRawURL),
Value: serpent.BoolOf(&pgRawURL),
Description: "Output the raw connection URL instead of a psql command.",
}
createAdminUserCmd.Options.Add(rawURLOpt)
@ -1219,9 +1219,9 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// printDeprecatedOptions loops through all command options, and prints
// a warning for usage of deprecated options.
func PrintDeprecatedOptions() clibase.MiddlewareFunc {
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(inv *clibase.Invocation) error {
func PrintDeprecatedOptions() serpent.MiddlewareFunc {
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(inv *serpent.Invocation) error {
opts := inv.Command.Options
// Print deprecation warnings.
for _, opt := range opts {
@ -1229,7 +1229,7 @@ func PrintDeprecatedOptions() clibase.MiddlewareFunc {
continue
}
if opt.ValueSource == clibase.ValueSourceNone || opt.ValueSource == clibase.ValueSourceDefault {
if opt.ValueSource == serpent.ValueSourceNone || opt.ValueSource == serpent.ValueSourceDefault {
continue
}
@ -1255,9 +1255,9 @@ func PrintDeprecatedOptions() clibase.MiddlewareFunc {
// writeConfigMW will prevent the main command from running if the write-config
// flag is set. Instead, it will marshal the command options to YAML and write
// them to stdout.
func WriteConfigMW(cfg *codersdk.DeploymentValues) clibase.MiddlewareFunc {
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(inv *clibase.Invocation) error {
func WriteConfigMW(cfg *codersdk.DeploymentValues) serpent.MiddlewareFunc {
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(inv *serpent.Invocation) error {
if !cfg.WriteConfig {
return next(inv)
}
@ -1427,7 +1427,7 @@ func newProvisionerDaemon(
}
// nolint: revive
func PrintLogo(inv *clibase.Invocation, daemonTitle string) {
func PrintLogo(inv *serpent.Invocation, daemonTitle string) {
// Only print the logo in TTYs.
if !isTTYOut(inv) {
return
@ -2242,7 +2242,7 @@ func ConfigureTraceProvider(
return tracerProvider, sqlDriver, closeTracing
}
func ConfigureHTTPServers(logger slog.Logger, inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (_ *HTTPServers, err error) {
func ConfigureHTTPServers(logger slog.Logger, inv *serpent.Invocation, cfg *codersdk.DeploymentValues) (_ *HTTPServers, err error) {
ctx := inv.Context()
httpServers := &HTTPServers{}
defer func() {
@ -2375,7 +2375,7 @@ func ConfigureHTTPServers(logger slog.Logger, inv *clibase.Invocation, cfg *code
// Also, for a while we have been accepting the environment variable (but not the
// corresponding flag!) "CODER_TLS_REDIRECT_HTTP", and it appeared in a configuration
// example, so we keep accepting it to not break backward compat.
func redirectHTTPToHTTPSDeprecation(ctx context.Context, logger slog.Logger, inv *clibase.Invocation, cfg *codersdk.DeploymentValues) {
func redirectHTTPToHTTPSDeprecation(ctx context.Context, logger slog.Logger, inv *serpent.Invocation, cfg *codersdk.DeploymentValues) {
truthy := func(s string) bool {
b, err := strconv.ParseBool(s)
if err != nil {
@ -2414,7 +2414,7 @@ func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]coder
sort.Strings(environ)
var providers []codersdk.ExternalAuthConfig
for _, v := range clibase.ParseEnviron(environ, prefix) {
for _, v := range serpent.ParseEnviron(environ, prefix) {
tokens := strings.SplitN(v.Name, "_", 2)
if len(tokens) != 2 {
return nil, xerrors.Errorf("invalid env var: %s", v.Name)
@ -2529,7 +2529,7 @@ func escapePostgresURLUserInfo(v string) (string, error) {
return v, nil
}
func signalNotifyContext(ctx context.Context, inv *clibase.Invocation, sig ...os.Signal) (context.Context, context.CancelFunc) {
func signalNotifyContext(ctx context.Context, inv *serpent.Invocation, sig ...os.Signal) (context.Context, context.CancelFunc) {
// On Windows, some of our signal functions lack support.
// If we pass in no signals, we should just return the context as-is.
if len(sig) == 0 {

View File

@ -11,7 +11,6 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
@ -20,9 +19,10 @@ import (
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/userpassword"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) newCreateAdminUserCommand() *clibase.Cmd {
func (r *RootCmd) newCreateAdminUserCommand() *serpent.Cmd {
var (
newUserDBURL string
newUserSSHKeygenAlgorithm string
@ -30,10 +30,10 @@ func (r *RootCmd) newCreateAdminUserCommand() *clibase.Cmd {
newUserEmail string
newUserPassword string
)
createAdminUserCommand := &clibase.Cmd{
createAdminUserCommand := &serpent.Cmd{
Use: "create-admin-user",
Short: "Create a new admin user with the given username, email and password and adds it to every organization.",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(newUserSSHKeygenAlgorithm)
@ -237,36 +237,36 @@ func (r *RootCmd) newCreateAdminUserCommand() *clibase.Cmd {
}
createAdminUserCommand.Options.Add(
clibase.Option{
serpent.Option{
Env: "CODER_PG_CONNECTION_URL",
Flag: "postgres-url",
Description: "URL of a PostgreSQL database. If empty, the built-in PostgreSQL deployment will be used (Coder must not be already running in this case).",
Value: clibase.StringOf(&newUserDBURL),
Value: serpent.StringOf(&newUserDBURL),
},
clibase.Option{
serpent.Option{
Env: "CODER_SSH_KEYGEN_ALGORITHM",
Flag: "ssh-keygen-algorithm",
Description: "The algorithm to use for generating ssh keys. Accepted values are \"ed25519\", \"ecdsa\", or \"rsa4096\".",
Default: "ed25519",
Value: clibase.StringOf(&newUserSSHKeygenAlgorithm),
Value: serpent.StringOf(&newUserSSHKeygenAlgorithm),
},
clibase.Option{
serpent.Option{
Env: "CODER_USERNAME",
Flag: "username",
Description: "The username of the new user. If not specified, you will be prompted via stdin.",
Value: clibase.StringOf(&newUserUsername),
Value: serpent.StringOf(&newUserUsername),
},
clibase.Option{
serpent.Option{
Env: "CODER_EMAIL",
Flag: "email",
Description: "The email of the new user. If not specified, you will be prompted via stdin.",
Value: clibase.StringOf(&newUserEmail),
Value: serpent.StringOf(&newUserEmail),
},
clibase.Option{
serpent.Option{
Env: "CODER_PASSWORD",
Flag: "password",
Description: "The password of the new user. If not specified, you will be prompted via stdin.",
Value: clibase.StringOf(&newUserPassword),
Value: serpent.StringOf(&newUserPassword),
},
)

View File

@ -15,9 +15,9 @@ import (
"cdr.dev/slog/sloggers/sloghuman"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
func Test_configureCipherSuites(t *testing.T) {
@ -182,43 +182,43 @@ func TestRedirectHTTPToHTTPSDeprecation(t *testing.T) {
testcases := []struct {
name string
environ clibase.Environ
environ serpent.Environ
flags []string
expected bool
}{
{
name: "AllUnset",
environ: clibase.Environ{},
environ: serpent.Environ{},
flags: []string{},
expected: false,
},
{
name: "CODER_TLS_REDIRECT_HTTP=true",
environ: clibase.Environ{{Name: "CODER_TLS_REDIRECT_HTTP", Value: "true"}},
environ: serpent.Environ{{Name: "CODER_TLS_REDIRECT_HTTP", Value: "true"}},
flags: []string{},
expected: true,
},
{
name: "CODER_TLS_REDIRECT_HTTP_TO_HTTPS=true",
environ: clibase.Environ{{Name: "CODER_TLS_REDIRECT_HTTP_TO_HTTPS", Value: "true"}},
environ: serpent.Environ{{Name: "CODER_TLS_REDIRECT_HTTP_TO_HTTPS", Value: "true"}},
flags: []string{},
expected: true,
},
{
name: "CODER_TLS_REDIRECT_HTTP=false",
environ: clibase.Environ{{Name: "CODER_TLS_REDIRECT_HTTP", Value: "false"}},
environ: serpent.Environ{{Name: "CODER_TLS_REDIRECT_HTTP", Value: "false"}},
flags: []string{},
expected: false,
},
{
name: "CODER_TLS_REDIRECT_HTTP_TO_HTTPS=false",
environ: clibase.Environ{{Name: "CODER_TLS_REDIRECT_HTTP_TO_HTTPS", Value: "false"}},
environ: serpent.Environ{{Name: "CODER_TLS_REDIRECT_HTTP_TO_HTTPS", Value: "false"}},
flags: []string{},
expected: false,
},
{
name: "--tls-redirect-http-to-https",
environ: clibase.Environ{},
environ: serpent.Environ{},
flags: []string{"--tls-redirect-http-to-https"},
expected: true,
},
@ -234,7 +234,7 @@ func TestRedirectHTTPToHTTPSDeprecation(t *testing.T) {
_ = flags.Bool("tls-redirect-http-to-https", true, "")
err := flags.Parse(tc.flags)
require.NoError(t, err)
inv := (&clibase.Invocation{Environ: tc.environ}).WithTestParsedFlags(t, flags)
inv := (&serpent.Invocation{Environ: tc.environ}).WithTestParsedFlags(t, flags)
cfg := &codersdk.DeploymentValues{}
opts := cfg.Options()
err = opts.SetDefaults()

View File

@ -2,18 +2,16 @@
package cli
import (
"github.com/coder/coder/v2/cli/clibase"
)
import "github.com/coder/serpent"
func (r *RootCmd) Server(_ func()) *clibase.Cmd {
root := &clibase.Cmd{
func (r *RootCmd) Server(_ func()) *serpent.Cmd {
root := &serpent.Cmd{
Use: "server",
Short: "Start a Coder server",
// We accept RawArgs so all commands and flags are accepted.
RawArgs: true,
Hidden: true,
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
SlimUnsupported(inv.Stderr, "server")
return nil
},

View File

@ -3,21 +3,21 @@ package cli
import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) show() *clibase.Cmd {
func (r *RootCmd) show() *serpent.Cmd {
client := new(codersdk.Client)
return &clibase.Cmd{
return &serpent.Cmd{
Use: "show <workspace>",
Short: "Display details of a workspace's resources and agents",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
buildInfo, err := client.BuildInfo(inv.Context())
if err != nil {
return xerrors.Errorf("get server version: %w", err)

View File

@ -13,12 +13,12 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) speedtest() *clibase.Cmd {
func (r *RootCmd) speedtest() *serpent.Cmd {
var (
direct bool
duration time.Duration
@ -26,15 +26,15 @@ func (r *RootCmd) speedtest() *clibase.Cmd {
pcapFile string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "speedtest <workspace>",
Short: "Run upload and download tests from your machine to a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
@ -142,32 +142,32 @@ func (r *RootCmd) speedtest() *clibase.Cmd {
return err
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Description: "Specifies whether to wait for a direct connection before testing speed.",
Flag: "direct",
FlagShorthand: "d",
Value: clibase.BoolOf(&direct),
Value: serpent.BoolOf(&direct),
},
{
Description: "Specifies whether to run in reverse mode where the client receives and the server sends.",
Flag: "direction",
Default: "down",
Value: clibase.EnumOf(&direction, "up", "down"),
Value: serpent.EnumOf(&direction, "up", "down"),
},
{
Description: "Specifies the duration to monitor traffic.",
Flag: "time",
FlagShorthand: "t",
Default: tsspeedtest.DefaultDuration.String(),
Value: clibase.DurationOf(&duration),
Value: serpent.DurationOf(&duration),
},
{
Description: "Specifies a file to write a network capture to.",
Flag: "pcap-file",
Default: "",
Value: clibase.StringOf(&pcapFile),
Value: serpent.StringOf(&pcapFile),
},
}
return cmd

View File

@ -26,11 +26,11 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"github.com/coder/retry"
"github.com/coder/serpent"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/coderd/autobuild/notify"
@ -44,7 +44,7 @@ var (
autostopNotifyCountdown = []time.Duration{30 * time.Minute}
)
func (r *RootCmd) ssh() *clibase.Cmd {
func (r *RootCmd) ssh() *serpent.Cmd {
var (
stdio bool
forwardAgent bool
@ -58,15 +58,15 @@ func (r *RootCmd) ssh() *clibase.Cmd {
disableAutostart bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "ssh <workspace>",
Short: "Start a shell into a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) (retErr error) {
Handler: func(inv *serpent.Invocation) (retErr error) {
// Before dialing the SSH server over TCP, capture Interrupt signals
// so that if we are interrupted, we have a chance to tear down the
// TCP session cleanly before exiting. If we don't, then the TCP
@ -412,70 +412,70 @@ func (r *RootCmd) ssh() *clibase.Cmd {
return nil
},
}
waitOption := clibase.Option{
waitOption := serpent.Option{
Flag: "wait",
Env: "CODER_SSH_WAIT",
Description: "Specifies whether or not to wait for the startup script to finish executing. Auto means that the agent startup script behavior configured in the workspace template is used.",
Default: "auto",
Value: clibase.EnumOf(&waitEnum, "yes", "no", "auto"),
Value: serpent.EnumOf(&waitEnum, "yes", "no", "auto"),
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "stdio",
Env: "CODER_SSH_STDIO",
Description: "Specifies whether to emit SSH output over stdin/stdout.",
Value: clibase.BoolOf(&stdio),
Value: serpent.BoolOf(&stdio),
},
{
Flag: "forward-agent",
FlagShorthand: "A",
Env: "CODER_SSH_FORWARD_AGENT",
Description: "Specifies whether to forward the SSH agent specified in $SSH_AUTH_SOCK.",
Value: clibase.BoolOf(&forwardAgent),
Value: serpent.BoolOf(&forwardAgent),
},
{
Flag: "forward-gpg",
FlagShorthand: "G",
Env: "CODER_SSH_FORWARD_GPG",
Description: "Specifies whether to forward the GPG agent. Unsupported on Windows workspaces, but supports all clients. Requires gnupg (gpg, gpgconf) on both the client and workspace. The GPG agent must already be running locally and will not be started for you. If a GPG agent is already running in the workspace, it will be attempted to be killed.",
Value: clibase.BoolOf(&forwardGPG),
Value: serpent.BoolOf(&forwardGPG),
},
{
Flag: "identity-agent",
Env: "CODER_SSH_IDENTITY_AGENT",
Description: "Specifies which identity agent to use (overrides $SSH_AUTH_SOCK), forward agent must also be enabled.",
Value: clibase.StringOf(&identityAgent),
Value: serpent.StringOf(&identityAgent),
},
{
Flag: "workspace-poll-interval",
Env: "CODER_WORKSPACE_POLL_INTERVAL",
Description: "Specifies how often to poll for workspace automated shutdown.",
Default: "1m",
Value: clibase.DurationOf(&wsPollInterval),
Value: serpent.DurationOf(&wsPollInterval),
},
waitOption,
{
Flag: "no-wait",
Env: "CODER_SSH_NO_WAIT",
Description: "Enter workspace immediately after the agent has connected. This is the default if the template has configured the agent startup script behavior as non-blocking.",
Value: clibase.BoolOf(&noWait),
UseInstead: []clibase.Option{waitOption},
Value: serpent.BoolOf(&noWait),
UseInstead: []serpent.Option{waitOption},
},
{
Flag: "log-dir",
Description: "Specify the directory containing SSH diagnostic log files.",
Env: "CODER_SSH_LOG_DIR",
FlagShorthand: "l",
Value: clibase.StringOf(&logDirPath),
Value: serpent.StringOf(&logDirPath),
},
{
Flag: "remote-forward",
Description: "Enable remote port forwarding (remote_port:local_address:local_port).",
Env: "CODER_SSH_REMOTE_FORWARD",
FlagShorthand: "R",
Value: clibase.StringArrayOf(&remoteForwards),
Value: serpent.StringArrayOf(&remoteForwards),
},
sshDisableAutostartOption(clibase.BoolOf(&disableAutostart)),
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)),
}
return cmd
}
@ -549,7 +549,7 @@ startWatchLoop:
// getWorkspaceAgent returns the workspace and agent selected using either the
// `<workspace>[.<agent>]` syntax via `in`.
// If autoStart is true, the workspace will be started if it is not already running.
func getWorkspaceAndAgent(ctx context.Context, inv *clibase.Invocation, client *codersdk.Client, autostart bool, userID string, in string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, userID string, in string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
var (
workspace codersdk.Workspace
workspaceParts = strings.Split(in, ".")
@ -990,8 +990,8 @@ func (c *rawSSHCopier) Close() error {
return err
}
func sshDisableAutostartOption(src *clibase.Bool) clibase.Option {
return clibase.Option{
func sshDisableAutostartOption(src *serpent.Bool) serpent.Option {
return serpent.Option{
Flag: "disable-autostart",
Description: "Disable starting the workspace automatically when connecting via SSH.",
Env: "CODER_SSH_DISABLE_AUTOSTART",

View File

@ -7,25 +7,25 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) start() *clibase.Cmd {
func (r *RootCmd) start() *serpent.Cmd {
var parameterFlags workspaceParameterFlags
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "start <workspace>",
Short: "Start a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Options: clibase.OptionSet{cliui.SkipPromptOption()},
Handler: func(inv *clibase.Invocation) error {
Options: serpent.OptionSet{cliui.SkipPromptOption()},
Handler: func(inv *serpent.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return err
@ -77,7 +77,7 @@ func (r *RootCmd) start() *clibase.Cmd {
return cmd
}
func buildWorkspaceStartRequest(inv *clibase.Invocation, client *codersdk.Client, workspace codersdk.Workspace, parameterFlags workspaceParameterFlags, action WorkspaceCLIAction) (codersdk.CreateWorkspaceBuildRequest, error) {
func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client, workspace codersdk.Workspace, parameterFlags workspaceParameterFlags, action WorkspaceCLIAction) (codersdk.CreateWorkspaceBuildRequest, error) {
version := workspace.LatestBuild.TemplateVersionID
if workspace.AutomaticUpdates == codersdk.AutomaticUpdatesAlways || action == WorkspaceUpdate {
@ -125,7 +125,7 @@ func buildWorkspaceStartRequest(inv *clibase.Invocation, client *codersdk.Client
}, nil
}
func startWorkspace(inv *clibase.Invocation, client *codersdk.Client, workspace codersdk.Workspace, parameterFlags workspaceParameterFlags, action WorkspaceCLIAction) (codersdk.WorkspaceBuild, error) {
func startWorkspace(inv *serpent.Invocation, client *codersdk.Client, workspace codersdk.Workspace, parameterFlags workspaceParameterFlags, action WorkspaceCLIAction) (codersdk.WorkspaceBuild, error) {
if workspace.DormantAt != nil {
_, _ = fmt.Fprintln(inv.Stdout, "Activating dormant workspace...")
err := client.UpdateWorkspaceDormancy(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceDormancy{

View File

@ -7,14 +7,14 @@ import (
"github.com/spf13/afero"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/clistat"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/serpent"
)
func initStatterMW(tgt **clistat.Statter, fs afero.Fs) clibase.MiddlewareFunc {
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(i *clibase.Invocation) error {
func initStatterMW(tgt **clistat.Statter, fs afero.Fs) serpent.MiddlewareFunc {
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(i *serpent.Invocation) error {
var err error
stat, err := clistat.New(clistat.WithFS(fs))
if err != nil {
@ -26,7 +26,7 @@ func initStatterMW(tgt **clistat.Statter, fs afero.Fs) clibase.MiddlewareFunc {
}
}
func (r *RootCmd) stat() *clibase.Cmd {
func (r *RootCmd) stat() *serpent.Cmd {
var (
st *clistat.Statter
fs = afero.NewReadOnlyFs(afero.NewOsFs())
@ -41,16 +41,16 @@ func (r *RootCmd) stat() *clibase.Cmd {
cliui.JSONFormat(),
)
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "stat",
Short: "Show resource usage for the current workspace.",
Middleware: initStatterMW(&st, fs),
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.statCPU(fs),
r.statMem(fs),
r.statDisk(fs),
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var sr statsRow
// Get CPU measurements first.
@ -130,24 +130,24 @@ func (r *RootCmd) stat() *clibase.Cmd {
return cmd
}
func (*RootCmd) statCPU(fs afero.Fs) *clibase.Cmd {
func (*RootCmd) statCPU(fs afero.Fs) *serpent.Cmd {
var (
hostArg bool
st *clistat.Statter
formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "cpu",
Short: "Show CPU usage, in cores.",
Middleware: initStatterMW(&st, fs),
Options: clibase.OptionSet{
Options: serpent.OptionSet{
{
Flag: "host",
Value: clibase.BoolOf(&hostArg),
Value: serpent.BoolOf(&hostArg),
Description: "Force host CPU measurement.",
},
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var cs *clistat.Result
var err error
if ok, _ := clistat.IsContainerized(fs); ok && !hostArg {
@ -171,28 +171,28 @@ func (*RootCmd) statCPU(fs afero.Fs) *clibase.Cmd {
return cmd
}
func (*RootCmd) statMem(fs afero.Fs) *clibase.Cmd {
func (*RootCmd) statMem(fs afero.Fs) *serpent.Cmd {
var (
hostArg bool
prefixArg string
st *clistat.Statter
formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "mem",
Short: "Show memory usage, in gigabytes.",
Middleware: initStatterMW(&st, fs),
Options: clibase.OptionSet{
Options: serpent.OptionSet{
{
Flag: "host",
Value: clibase.BoolOf(&hostArg),
Value: serpent.BoolOf(&hostArg),
Description: "Force host memory measurement.",
},
{
Description: "SI Prefix for memory measurement.",
Default: clistat.PrefixHumanGibi,
Flag: "prefix",
Value: clibase.EnumOf(&prefixArg,
Value: serpent.EnumOf(&prefixArg,
clistat.PrefixHumanKibi,
clistat.PrefixHumanMebi,
clistat.PrefixHumanGibi,
@ -200,7 +200,7 @@ func (*RootCmd) statMem(fs afero.Fs) *clibase.Cmd {
),
},
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
pfx := clistat.ParsePrefix(prefixArg)
var ms *clistat.Result
var err error
@ -225,21 +225,21 @@ func (*RootCmd) statMem(fs afero.Fs) *clibase.Cmd {
return cmd
}
func (*RootCmd) statDisk(fs afero.Fs) *clibase.Cmd {
func (*RootCmd) statDisk(fs afero.Fs) *serpent.Cmd {
var (
pathArg string
prefixArg string
st *clistat.Statter
formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "disk",
Short: "Show disk usage, in gigabytes.",
Middleware: initStatterMW(&st, fs),
Options: clibase.OptionSet{
Options: serpent.OptionSet{
{
Flag: "path",
Value: clibase.StringOf(&pathArg),
Value: serpent.StringOf(&pathArg),
Description: "Path for which to check disk usage.",
Default: "/",
},
@ -247,7 +247,7 @@ func (*RootCmd) statDisk(fs afero.Fs) *clibase.Cmd {
Flag: "prefix",
Default: clistat.PrefixHumanGibi,
Description: "SI Prefix for disk measurement.",
Value: clibase.EnumOf(&prefixArg,
Value: serpent.EnumOf(&prefixArg,
clistat.PrefixHumanKibi,
clistat.PrefixHumanMebi,
clistat.PrefixHumanGibi,
@ -255,7 +255,7 @@ func (*RootCmd) statDisk(fs afero.Fs) *clibase.Cmd {
),
},
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
pfx := clistat.ParsePrefix(prefixArg)
// Users may also call `coder stat disk <path>`.
if len(inv.Args) > 0 {

View File

@ -6,19 +6,19 @@ import (
"os"
"strconv"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) state() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) state() *serpent.Cmd {
cmd := &serpent.Cmd{
Use: "state",
Short: "Manually manage Terraform state to fix broken workspaces",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.statePull(),
r.statePush(),
},
@ -26,17 +26,17 @@ func (r *RootCmd) state() *clibase.Cmd {
return cmd
}
func (r *RootCmd) statePull() *clibase.Cmd {
func (r *RootCmd) statePull() *serpent.Cmd {
var buildNumber int64
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "pull <workspace> [file]",
Short: "Pull a Terraform state file from a workspace.",
Middleware: clibase.Chain(
clibase.RequireRangeArgs(1, 2),
Middleware: serpent.Chain(
serpent.RequireRangeArgs(1, 2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var err error
var build codersdk.WorkspaceBuild
if buildNumber == 0 {
@ -69,32 +69,32 @@ func (r *RootCmd) statePull() *clibase.Cmd {
return os.WriteFile(inv.Args[1], state, 0o600)
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
buildNumberOption(&buildNumber),
}
return cmd
}
func buildNumberOption(n *int64) clibase.Option {
return clibase.Option{
func buildNumberOption(n *int64) serpent.Option {
return serpent.Option{
Flag: "build",
FlagShorthand: "b",
Description: "Specify a workspace build to target by name. Defaults to latest.",
Value: clibase.Int64Of(n),
Value: serpent.Int64Of(n),
}
}
func (r *RootCmd) statePush() *clibase.Cmd {
func (r *RootCmd) statePush() *serpent.Cmd {
var buildNumber int64
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "push <workspace> <file>",
Short: "Push a Terraform state file to a workspace.",
Middleware: clibase.Chain(
clibase.RequireNArgs(2),
Middleware: serpent.Chain(
serpent.RequireNArgs(2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return err
@ -134,7 +134,7 @@ func (r *RootCmd) statePush() *clibase.Cmd {
return cliui.WorkspaceBuild(inv.Context(), inv.Stderr, client, build.ID)
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
buildNumberOption(&buildNumber),
}
return cmd

View File

@ -4,25 +4,25 @@ import (
"fmt"
"time"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) stop() *clibase.Cmd {
func (r *RootCmd) stop() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "stop <workspace>",
Short: "Stop a workspace",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Options: clibase.OptionSet{
Options: serpent.OptionSet{
cliui.SkipPromptOption(),
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
_, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Confirm stop workspace?",
IsConfirm: true,

View File

@ -16,38 +16,38 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/support"
"github.com/coder/serpent"
)
func (r *RootCmd) support() *clibase.Cmd {
supportCmd := &clibase.Cmd{
func (r *RootCmd) support() *serpent.Cmd {
supportCmd := &serpent.Cmd{
Use: "support",
Short: "Commands for troubleshooting issues with a Coder deployment.",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Hidden: true, // TODO: un-hide once the must-haves from #12160 are completed.
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.supportBundle(),
},
}
return supportCmd
}
func (r *RootCmd) supportBundle() *clibase.Cmd {
func (r *RootCmd) supportBundle() *serpent.Cmd {
var outputPath string
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "bundle <workspace> [<agent>]",
Short: "Generate a support bundle to troubleshoot issues connecting to a workspace.",
Long: `This command generates a file containing detailed troubleshooting information about the Coder deployment and workspace connections. You must specify a single workspace (and optionally an agent name).`,
Middleware: clibase.Chain(
clibase.RequireRangeArgs(0, 2),
Middleware: serpent.Chain(
serpent.RequireRangeArgs(0, 2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var (
log = slog.Make(sloghuman.Sink(inv.Stderr)).
Leveled(slog.LevelDebug)
@ -108,13 +108,13 @@ func (r *RootCmd) supportBundle() *clibase.Cmd {
return nil
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "output",
FlagShorthand: "o",
Env: "CODER_SUPPORT_BUNDLE_OUTPUT",
Description: "File path for writing the generated support bundle. Defaults to coder-support-$(date +%s).zip.",
Value: clibase.StringOf(&outputPath),
Value: serpent.StringOf(&outputPath),
},
}

View File

@ -9,14 +9,14 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/serpent"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) templateCreate() *clibase.Cmd {
func (r *RootCmd) templateCreate() *serpent.Cmd {
var (
provisioner string
provisionerTags []string
@ -34,18 +34,18 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
uploadFlags templateUploadFlags
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "create [name]",
Short: "DEPRECATED: Create a template from the current directory or as specified by flag",
Middleware: clibase.Chain(
clibase.RequireRangeArgs(0, 1),
Middleware: serpent.Chain(
serpent.RequireRangeArgs(0, 1),
cliui.DeprecationWarning(
"Use `coder templates push` command for creating and updating templates. \n"+
"Use `coder templates edit` command for editing template settings. ",
),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
isTemplateSchedulingOptionsSet := failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 || maxTTL != 0
if isTemplateSchedulingOptionsSet || requireActiveVersion {
@ -178,74 +178,74 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
return nil
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "private",
Description: "Disable the default behavior of granting template access to the 'everyone' group. " +
"The template permissions must be updated to allow non-admin users to use this template.",
Value: clibase.BoolOf(&disableEveryone),
Value: serpent.BoolOf(&disableEveryone),
},
{
Flag: "variables-file",
Description: "Specify a file path with values for Terraform-managed variables.",
Value: clibase.StringOf(&variablesFile),
Value: serpent.StringOf(&variablesFile),
},
{
Flag: "variable",
Description: "Specify a set of values for Terraform-managed variables.",
Value: clibase.StringArrayOf(&commandLineVariables),
Value: serpent.StringArrayOf(&commandLineVariables),
},
{
Flag: "var",
Description: "Alias of --variable.",
Value: clibase.StringArrayOf(&commandLineVariables),
Value: serpent.StringArrayOf(&commandLineVariables),
},
{
Flag: "provisioner-tag",
Description: "Specify a set of tags to target provisioner daemons.",
Value: clibase.StringArrayOf(&provisionerTags),
Value: serpent.StringArrayOf(&provisionerTags),
},
{
Flag: "default-ttl",
Description: "Specify a default TTL for workspaces created from this template. It is the default time before shutdown - workspaces created from this template default to this value. Maps to \"Default autostop\" in the UI.",
Default: "24h",
Value: clibase.DurationOf(&defaultTTL),
Value: serpent.DurationOf(&defaultTTL),
},
{
Flag: "failure-ttl",
Description: "Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed \"start\" build before coder automatically schedules a \"stop\" build to cleanup.This licensed feature's default is 0h (off). Maps to \"Failure cleanup\"in the UI.",
Default: "0h",
Value: clibase.DurationOf(&failureTTL),
Value: serpent.DurationOf(&failureTTL),
},
{
Flag: "dormancy-threshold",
Description: "Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.",
Default: "0h",
Value: clibase.DurationOf(&dormancyThreshold),
Value: serpent.DurationOf(&dormancyThreshold),
},
{
Flag: "dormancy-auto-deletion",
Description: "Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to \"Dormancy Auto-Deletion\" in the UI.",
Default: "0h",
Value: clibase.DurationOf(&dormancyAutoDeletion),
Value: serpent.DurationOf(&dormancyAutoDeletion),
},
{
Flag: "max-ttl",
Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.",
Value: clibase.DurationOf(&maxTTL),
Value: serpent.DurationOf(&maxTTL),
},
{
Flag: "test.provisioner",
Description: "Customize the provisioner backend.",
Default: "terraform",
Value: clibase.StringOf(&provisioner),
Value: serpent.StringOf(&provisioner),
Hidden: true,
},
{
Flag: "require-active-version",
Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature.",
Value: clibase.BoolOf(&requireActiveVersion),
Value: serpent.BoolOf(&requireActiveVersion),
Default: "false",
},

View File

@ -9,23 +9,23 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) templateDelete() *clibase.Cmd {
func (r *RootCmd) templateDelete() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "delete [name...]",
Short: "Delete templates",
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
),
Options: clibase.OptionSet{
Options: serpent.OptionSet{
cliui.SkipPromptOption(),
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var (
ctx = inv.Context()
templateNames = []string{}

View File

@ -9,13 +9,13 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/serpent"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) templateEdit() *clibase.Cmd {
func (r *RootCmd) templateEdit() *serpent.Cmd {
const deprecatedFlagName = "deprecated"
var (
name string
@ -40,14 +40,14 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "edit <template>",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Short: "Edit the metadata of a template by name.",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
unsetAutostopRequirementDaysOfWeek := len(autostopRequirementDaysOfWeek) == 1 && autostopRequirementDaysOfWeek[0] == "none"
requiresScheduling := (len(autostopRequirementDaysOfWeek) > 0 && !unsetAutostopRequirementDaysOfWeek) ||
autostopRequirementWeeks > 0 ||
@ -207,53 +207,53 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "name",
Description: "Edit the template name.",
Value: clibase.StringOf(&name),
Value: serpent.StringOf(&name),
},
{
Flag: "display-name",
Description: "Edit the template display name.",
Value: clibase.StringOf(&displayName),
Value: serpent.StringOf(&displayName),
},
{
Flag: "description",
Description: "Edit the template description.",
Value: clibase.StringOf(&description),
Value: serpent.StringOf(&description),
},
{
Name: deprecatedFlagName,
Flag: "deprecated",
Description: "Sets the template as deprecated. Must be a message explaining why the template is deprecated.",
Value: clibase.StringOf(&deprecationMessage),
Value: serpent.StringOf(&deprecationMessage),
},
{
Flag: "icon",
Description: "Edit the template icon path.",
Value: clibase.StringOf(&icon),
Value: serpent.StringOf(&icon),
},
{
Flag: "default-ttl",
Description: "Edit the template default time before shutdown - workspaces created from this template default to this value. Maps to \"Default autostop\" in the UI.",
Value: clibase.DurationOf(&defaultTTL),
Value: serpent.DurationOf(&defaultTTL),
},
{
Flag: "activity-bump",
Description: "Edit the template activity bump - workspaces created from this template will have their shutdown time bumped by this value when activity is detected. Maps to \"Activity bump\" in the UI.",
Value: clibase.DurationOf(&activityBump),
Value: serpent.DurationOf(&activityBump),
},
{
Flag: "max-ttl",
Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting, regardless of user activity. This is an enterprise-only feature. Maps to \"Max lifetime\" in the UI.",
Value: clibase.DurationOf(&maxTTL),
Value: serpent.DurationOf(&maxTTL),
},
{
Flag: "autostart-requirement-weekdays",
// workspaces created from this template must be restarted on the given weekdays. To unset this value for the template (and disable the autostop requirement for the template), pass 'none'.
Description: "Edit the template autostart requirement weekdays - workspaces created from this template can only autostart on the given weekdays. To unset this value for the template (and allow autostart on all days), pass 'all'.",
Value: clibase.Validate(clibase.StringArrayOf(&autostartRequirementDaysOfWeek), func(value *clibase.StringArray) error {
Value: serpent.Validate(serpent.StringArrayOf(&autostartRequirementDaysOfWeek), func(value *serpent.StringArray) error {
v := value.GetSlice()
if len(v) == 1 && v[0] == "all" {
return nil
@ -270,7 +270,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
Description: "Edit the template autostop requirement weekdays - workspaces created from this template must be restarted on the given weekdays. To unset this value for the template (and disable the autostop requirement for the template), pass 'none'.",
// TODO(@dean): unhide when we delete max_ttl
Hidden: true,
Value: clibase.Validate(clibase.StringArrayOf(&autostopRequirementDaysOfWeek), func(value *clibase.StringArray) error {
Value: serpent.Validate(serpent.StringArrayOf(&autostopRequirementDaysOfWeek), func(value *serpent.StringArray) error {
v := value.GetSlice()
if len(v) == 1 && v[0] == "none" {
return nil
@ -287,55 +287,55 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
Description: "Edit the template autostop requirement weeks - workspaces created from this template must be restarted on an n-weekly basis.",
// TODO(@dean): unhide when we delete max_ttl
Hidden: true,
Value: clibase.Int64Of(&autostopRequirementWeeks),
Value: serpent.Int64Of(&autostopRequirementWeeks),
},
{
Flag: "failure-ttl",
Description: "Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed \"start\" build before coder automatically schedules a \"stop\" build to cleanup.This licensed feature's default is 0h (off). Maps to \"Failure cleanup\" in the UI.",
Default: "0h",
Value: clibase.DurationOf(&failureTTL),
Value: serpent.DurationOf(&failureTTL),
},
{
Flag: "dormancy-threshold",
Description: "Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.",
Default: "0h",
Value: clibase.DurationOf(&dormancyThreshold),
Value: serpent.DurationOf(&dormancyThreshold),
},
{
Flag: "dormancy-auto-deletion",
Description: "Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to \"Dormancy Auto-Deletion\" in the UI.",
Default: "0h",
Value: clibase.DurationOf(&dormancyAutoDeletion),
Value: serpent.DurationOf(&dormancyAutoDeletion),
},
{
Flag: "allow-user-cancel-workspace-jobs",
Description: "Allow users to cancel in-progress workspace jobs.",
Default: "true",
Value: clibase.BoolOf(&allowUserCancelWorkspaceJobs),
Value: serpent.BoolOf(&allowUserCancelWorkspaceJobs),
},
{
Flag: "allow-user-autostart",
Description: "Allow users to configure autostart for workspaces on this template. This can only be disabled in enterprise.",
Default: "true",
Value: clibase.BoolOf(&allowUserAutostart),
Value: serpent.BoolOf(&allowUserAutostart),
},
{
Flag: "allow-user-autostop",
Description: "Allow users to customize the autostop TTL for workspaces on this template. This can only be disabled in enterprise.",
Default: "true",
Value: clibase.BoolOf(&allowUserAutostop),
Value: serpent.BoolOf(&allowUserAutostop),
},
{
Flag: "require-active-version",
Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature.",
Value: clibase.BoolOf(&requireActiveVersion),
Value: serpent.BoolOf(&requireActiveVersion),
Default: "false",
},
{
Flag: "private",
Description: "Disable the default behavior of granting template access to the 'everyone' group. " +
"The template permissions must be updated to allow non-admin users to use this template.",
Value: clibase.BoolOf(&disableEveryone),
Value: serpent.BoolOf(&disableEveryone),
Default: "false",
},
cliui.SkipPromptOption(),

View File

@ -12,15 +12,15 @@ import (
"golang.org/x/exp/maps"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/examples"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (*RootCmd) templateInit() *clibase.Cmd {
func (*RootCmd) templateInit() *serpent.Cmd {
var templateID string
exampleList, err := examples.List()
if err != nil {
@ -32,11 +32,11 @@ func (*RootCmd) templateInit() *clibase.Cmd {
templateIDs = append(templateIDs, ex.ID)
}
sort.Strings(templateIDs)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "init [directory]",
Short: "Get started with a templated template.",
Middleware: clibase.RequireRangeArgs(0, 1),
Handler: func(inv *clibase.Invocation) error {
Middleware: serpent.RequireRangeArgs(0, 1),
Handler: func(inv *serpent.Invocation) error {
// If the user didn't specify any template, prompt them to select one.
if templateID == "" {
optsToID := map[string]string{}
@ -76,7 +76,7 @@ func (*RootCmd) templateInit() *clibase.Cmd {
selectedTemplate, ok := templateByID(templateID, exampleList)
if !ok {
// clibase.EnumOf would normally handle this.
// serpent.EnumOf would normally handle this.
return xerrors.Errorf("template not found: %q", templateID)
}
archive, err := examples.Archive(selectedTemplate.ID)
@ -120,11 +120,11 @@ func (*RootCmd) templateInit() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "id",
Description: "Specify a given example template by ID.",
Value: clibase.EnumOf(&templateID, templateIDs...),
Value: serpent.EnumOf(&templateID, templateIDs...),
},
}

View File

@ -5,26 +5,26 @@ import (
"github.com/fatih/color"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) templateList() *clibase.Cmd {
func (r *RootCmd) templateList() *serpent.Cmd {
formatter := cliui.NewOutputFormatter(
cliui.TableFormat([]templateTableRow{}, []string{"name", "last updated", "used by"}),
cliui.JSONFormat(),
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "list",
Short: "List all the templates available for the organization",
Aliases: []string{"ls"},
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
organization, err := CurrentOrganization(r, inv, client)
if err != nil {
return err

View File

@ -10,12 +10,12 @@ import (
"github.com/codeclysm/extract/v3"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) templatePull() *clibase.Cmd {
func (r *RootCmd) templatePull() *serpent.Cmd {
var (
tarMode bool
zipMode bool
@ -23,14 +23,14 @@ func (r *RootCmd) templatePull() *clibase.Cmd {
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "pull <name> [destination]",
Short: "Download the active, latest, or specified version of a template to a path.",
Middleware: clibase.Chain(
clibase.RequireRangeArgs(1, 2),
Middleware: serpent.Chain(
serpent.RequireRangeArgs(1, 2),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var (
ctx = inv.Context()
templateName = inv.Args[0]
@ -166,24 +166,24 @@ func (r *RootCmd) templatePull() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Description: "Output the template as a tar archive to stdout.",
Flag: "tar",
Value: clibase.BoolOf(&tarMode),
Value: serpent.BoolOf(&tarMode),
},
{
Description: "Output the template as a zip archive to stdout.",
Flag: "zip",
Value: clibase.BoolOf(&zipMode),
Value: serpent.BoolOf(&zipMode),
},
{
Description: "The name of the template version to pull. Use 'active' to pull the active version, 'latest' to pull the latest version, or the name of the template version to pull.",
Flag: "version",
Value: clibase.StringOf(&versionName),
Value: serpent.StringOf(&versionName),
},
cliui.SkipPromptOption(),
}

View File

@ -16,14 +16,14 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) templatePush() *clibase.Cmd {
func (r *RootCmd) templatePush() *serpent.Cmd {
var (
versionName string
provisioner string
@ -36,14 +36,14 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
activate bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "push [template]",
Short: "Create or update a template from the current directory or as specified by flag",
Middleware: clibase.Chain(
clibase.RequireRangeArgs(0, 1),
Middleware: serpent.Chain(
serpent.RequireRangeArgs(0, 1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
uploadFlags.setWorkdir(workdir)
organization, err := CurrentOrganization(r, inv, client)
@ -160,12 +160,12 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "test.provisioner",
Description: "Customize the provisioner backend.",
Default: "terraform",
Value: clibase.StringOf(&provisioner),
Value: serpent.StringOf(&provisioner),
// This is for testing!
Hidden: true,
},
@ -173,45 +173,45 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
Flag: "test.workdir",
Description: "Customize the working directory.",
Default: "",
Value: clibase.StringOf(&workdir),
Value: serpent.StringOf(&workdir),
// This is for testing!
Hidden: true,
},
{
Flag: "variables-file",
Description: "Specify a file path with values for Terraform-managed variables.",
Value: clibase.StringOf(&variablesFile),
Value: serpent.StringOf(&variablesFile),
},
{
Flag: "variable",
Description: "Specify a set of values for Terraform-managed variables.",
Value: clibase.StringArrayOf(&commandLineVariables),
Value: serpent.StringArrayOf(&commandLineVariables),
},
{
Flag: "var",
Description: "Alias of --variable.",
Value: clibase.StringArrayOf(&commandLineVariables),
Value: serpent.StringArrayOf(&commandLineVariables),
},
{
Flag: "provisioner-tag",
Description: "Specify a set of tags to target provisioner daemons.",
Value: clibase.StringArrayOf(&provisionerTags),
Value: serpent.StringArrayOf(&provisionerTags),
},
{
Flag: "name",
Description: "Specify a name for the new template version. It will be automatically generated if not provided.",
Value: clibase.StringOf(&versionName),
Value: serpent.StringOf(&versionName),
},
{
Flag: "always-prompt",
Description: "Always prompt all parameters. Does not pull parameter values from active template version.",
Value: clibase.BoolOf(&alwaysPrompt),
Value: serpent.BoolOf(&alwaysPrompt),
},
{
Flag: "activate",
Description: "Whether the new template will be marked active.",
Default: "true",
Value: clibase.BoolOf(&activate),
Value: serpent.BoolOf(&activate),
},
cliui.SkipPromptOption(),
}
@ -225,23 +225,23 @@ type templateUploadFlags struct {
message string
}
func (pf *templateUploadFlags) options() []clibase.Option {
return []clibase.Option{{
func (pf *templateUploadFlags) options() []serpent.Option {
return []serpent.Option{{
Flag: "directory",
FlagShorthand: "d",
Description: "Specify the directory to create from, use '-' to read tar from stdin.",
Default: ".",
Value: clibase.StringOf(&pf.directory),
Value: serpent.StringOf(&pf.directory),
}, {
Flag: "ignore-lockfile",
Description: "Ignore warnings about not having a .terraform.lock.hcl file present in the template.",
Default: "false",
Value: clibase.BoolOf(&pf.ignoreLockfile),
Value: serpent.BoolOf(&pf.ignoreLockfile),
}, {
Flag: "message",
FlagShorthand: "m",
Description: "Specify a message describing the changes in this version of the template. Messages longer than 72 characters will be displayed as truncated.",
Value: clibase.StringOf(&pf.message),
Value: serpent.StringOf(&pf.message),
}}
}
@ -260,7 +260,7 @@ func (pf *templateUploadFlags) stdin() bool {
return pf.directory == "-"
}
func (pf *templateUploadFlags) upload(inv *clibase.Invocation, client *codersdk.Client) (*codersdk.UploadResponse, error) {
func (pf *templateUploadFlags) upload(inv *serpent.Invocation, client *codersdk.Client) (*codersdk.UploadResponse, error) {
var content io.Reader
if pf.stdin() {
content = inv.Stdin
@ -297,7 +297,7 @@ func (pf *templateUploadFlags) upload(inv *clibase.Invocation, client *codersdk.
return &resp, nil
}
func (pf *templateUploadFlags) checkForLockfile(inv *clibase.Invocation) error {
func (pf *templateUploadFlags) checkForLockfile(inv *serpent.Invocation) error {
if pf.stdin() || pf.ignoreLockfile {
// Just assume there's a lockfile if reading from stdin.
return nil
@ -317,7 +317,7 @@ func (pf *templateUploadFlags) checkForLockfile(inv *clibase.Invocation) error {
return nil
}
func (pf *templateUploadFlags) templateMessage(inv *clibase.Invocation) string {
func (pf *templateUploadFlags) templateMessage(inv *serpent.Invocation) string {
title := strings.SplitN(pf.message, "\n", 2)[0]
if len(title) > 72 {
cliui.Warn(inv.Stdout, "Template message is longer than 72 characters, it will be displayed as truncated.")
@ -370,7 +370,7 @@ type createValidTemplateVersionArgs struct {
UserVariableValues []codersdk.VariableValue
}
func createValidTemplateVersion(inv *clibase.Invocation, args createValidTemplateVersionArgs) (*codersdk.TemplateVersion, error) {
func createValidTemplateVersion(inv *serpent.Invocation, args createValidTemplateVersionArgs) (*codersdk.TemplateVersion, error) {
client := args.Client
req := codersdk.CreateTemplateVersionRequest{

View File

@ -6,14 +6,14 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) templates() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) templates() *serpent.Cmd {
cmd := &serpent.Cmd{
Use: "templates",
Short: "Manage templates",
Long: "Templates are written in standard Terraform and describe the infrastructure for workspaces\n" + formatExamples(
@ -27,10 +27,10 @@ func (r *RootCmd) templates() *clibase.Cmd {
},
),
Aliases: []string{"template"},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.templateCreate(),
r.templateEdit(),
r.templateInit(),
@ -46,7 +46,7 @@ func (r *RootCmd) templates() *clibase.Cmd {
return cmd
}
func selectTemplate(inv *clibase.Invocation, client *codersdk.Client, organization codersdk.Organization) (codersdk.Template, error) {
func selectTemplate(inv *serpent.Invocation, client *codersdk.Client, organization codersdk.Organization) (codersdk.Template, error) {
var empty codersdk.Template
ctx := inv.Context()
allTemplates, err := client.TemplatesByOrganization(ctx, organization.ID)

View File

@ -8,22 +8,22 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) unarchiveTemplateVersion() *clibase.Cmd {
func (r *RootCmd) unarchiveTemplateVersion() *serpent.Cmd {
return r.setArchiveTemplateVersion(false)
}
func (r *RootCmd) archiveTemplateVersion() *clibase.Cmd {
func (r *RootCmd) archiveTemplateVersion() *serpent.Cmd {
return r.setArchiveTemplateVersion(true)
}
//nolint:revive
func (r *RootCmd) setArchiveTemplateVersion(archive bool) *clibase.Cmd {
func (r *RootCmd) setArchiveTemplateVersion(archive bool) *serpent.Cmd {
presentVerb := "archive"
pastVerb := "archived"
if !archive {
@ -32,16 +32,16 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *clibase.Cmd {
}
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: presentVerb + " <template-name> [template-version-names...] ",
Short: strings.ToUpper(string(presentVerb[0])) + presentVerb[1:] + " a template version(s).",
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
),
Options: clibase.OptionSet{
Options: serpent.OptionSet{
cliui.SkipPromptOption(),
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var (
ctx = inv.Context()
versions []codersdk.TemplateVersion
@ -96,25 +96,25 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *clibase.Cmd {
return cmd
}
func (r *RootCmd) archiveTemplateVersions() *clibase.Cmd {
var all clibase.Bool
func (r *RootCmd) archiveTemplateVersions() *serpent.Cmd {
var all serpent.Bool
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "archive [template-name...] ",
Short: "Archive unused or failed template versions from a given template(s)",
Middleware: clibase.Chain(
Middleware: serpent.Chain(
r.InitClient(client),
),
Options: clibase.OptionSet{
Options: serpent.OptionSet{
cliui.SkipPromptOption(),
clibase.Option{
serpent.Option{
Name: "all",
Description: "Include all unused template versions. By default, only failed template versions are archived.",
Flag: "all",
Value: &all,
},
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var (
ctx = inv.Context()
templateNames = []string{}

View File

@ -8,14 +8,14 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) templateVersions() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) templateVersions() *serpent.Cmd {
cmd := &serpent.Cmd{
Use: "versions",
Short: "Manage different versions of the specified template",
Aliases: []string{"version"},
@ -25,10 +25,10 @@ func (r *RootCmd) templateVersions() *clibase.Cmd {
Command: "coder templates versions list my-template",
},
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.templateVersionsList(),
r.archiveTemplateVersion(),
r.unarchiveTemplateVersion(),
@ -38,7 +38,7 @@ func (r *RootCmd) templateVersions() *clibase.Cmd {
return cmd
}
func (r *RootCmd) templateVersionsList() *clibase.Cmd {
func (r *RootCmd) templateVersionsList() *serpent.Cmd {
defaultColumns := []string{
"Name",
"Created At",
@ -52,15 +52,15 @@ func (r *RootCmd) templateVersionsList() *clibase.Cmd {
)
client := new(codersdk.Client)
var includeArchived clibase.Bool
var includeArchived serpent.Bool
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "list <template>",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(i *clibase.Invocation) error {
func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(i *serpent.Invocation) error {
// This is the only way to dynamically add the "archived"
// column if '--include-archived' is true.
// It does not make sense to show this column if the
@ -68,8 +68,8 @@ func (r *RootCmd) templateVersionsList() *clibase.Cmd {
if includeArchived {
for _, opt := range i.Command.Options {
if opt.Flag == "column" {
if opt.ValueSource == clibase.ValueSourceDefault {
v, ok := opt.Value.(*clibase.StringArray)
if opt.ValueSource == serpent.ValueSourceDefault {
v, ok := opt.Value.(*serpent.StringArray)
if ok {
// Add the extra new default column.
*v = append(*v, "Archived")
@ -84,7 +84,7 @@ func (r *RootCmd) templateVersionsList() *clibase.Cmd {
},
),
Short: "List all the versions of the specified template",
Options: clibase.OptionSet{
Options: serpent.OptionSet{
{
Name: "include-archived",
Description: "Include archived versions in the result list.",
@ -92,7 +92,7 @@ func (r *RootCmd) templateVersionsList() *clibase.Cmd {
Value: &includeArchived,
},
},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
organization, err := CurrentOrganization(r, inv, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)

View File

@ -8,13 +8,13 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) tokens() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) tokens() *serpent.Cmd {
cmd := &serpent.Cmd{
Use: "tokens",
Short: "Manage personal access tokens",
Long: "Tokens are used to authenticate automated clients to Coder.\n" + formatExamples(
@ -32,10 +32,10 @@ func (r *RootCmd) tokens() *clibase.Cmd {
},
),
Aliases: []string{"token"},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.createToken(),
r.listTokens(),
r.removeToken(),
@ -44,20 +44,20 @@ func (r *RootCmd) tokens() *clibase.Cmd {
return cmd
}
func (r *RootCmd) createToken() *clibase.Cmd {
func (r *RootCmd) createToken() *serpent.Cmd {
var (
tokenLifetime time.Duration
name string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "create",
Short: "Create a token",
Middleware: clibase.Chain(
clibase.RequireNArgs(0),
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
res, err := client.CreateToken(inv.Context(), codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: tokenLifetime,
TokenName: name,
@ -72,20 +72,20 @@ func (r *RootCmd) createToken() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "lifetime",
Env: "CODER_TOKEN_LIFETIME",
Description: "Specify a duration for the lifetime of the token.",
Default: (time.Hour * 24 * 30).String(),
Value: clibase.DurationOf(&tokenLifetime),
Value: serpent.DurationOf(&tokenLifetime),
},
{
Flag: "name",
FlagShorthand: "n",
Env: "CODER_TOKEN_NAME",
Description: "Specify a human-readable name.",
Value: clibase.StringOf(&name),
Value: serpent.StringOf(&name),
},
}
@ -118,7 +118,7 @@ func tokenListRowFromToken(token codersdk.APIKeyWithOwner) tokenListRow {
}
}
func (r *RootCmd) listTokens() *clibase.Cmd {
func (r *RootCmd) listTokens() *serpent.Cmd {
// we only display the 'owner' column if the --all argument is passed in
defaultCols := []string{"id", "name", "last used", "expires at", "created at"}
if slices.Contains(os.Args, "-a") || slices.Contains(os.Args, "--all") {
@ -135,15 +135,15 @@ func (r *RootCmd) listTokens() *clibase.Cmd {
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "list",
Aliases: []string{"ls"},
Short: "List tokens",
Middleware: clibase.Chain(
clibase.RequireNArgs(0),
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
tokens, err := client.Tokens(inv.Context(), codersdk.Me, codersdk.TokensFilter{
IncludeAll: all,
})
@ -174,12 +174,12 @@ func (r *RootCmd) listTokens() *clibase.Cmd {
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "all",
FlagShorthand: "a",
Description: "Specifies whether all users' tokens will be listed or not (must have Owner role to see all tokens).",
Value: clibase.BoolOf(&all),
Value: serpent.BoolOf(&all),
},
}
@ -187,17 +187,17 @@ func (r *RootCmd) listTokens() *clibase.Cmd {
return cmd
}
func (r *RootCmd) removeToken() *clibase.Cmd {
func (r *RootCmd) removeToken() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "remove <name>",
Aliases: []string{"delete"},
Short: "Delete a token",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
token, err := client.APIKeyByName(inv.Context(), codersdk.Me, inv.Args[0])
if err != nil {
return xerrors.Errorf("fetch api key by name %s: %w", inv.Args[0], err)

View File

@ -5,24 +5,24 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) update() *clibase.Cmd {
func (r *RootCmd) update() *serpent.Cmd {
var parameterFlags workspaceParameterFlags
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Annotations: workspaceCommand,
Use: "update <workspace>",
Short: "Will update and start a given workspace if it is out of date",
Long: "Use --always-prompt to change the parameter values of the workspace.",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return err

View File

@ -9,13 +9,13 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/serpent"
)
func (r *RootCmd) userCreate() *clibase.Cmd {
func (r *RootCmd) userCreate() *serpent.Cmd {
var (
email string
username string
@ -24,13 +24,13 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
loginType string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "create",
Middleware: clibase.Chain(
clibase.RequireNArgs(0),
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
organization, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
@ -114,31 +114,31 @@ Create a workspace `+pretty.Sprint(cliui.DefaultStyles.Code, "coder create")+`!
return nil
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "email",
FlagShorthand: "e",
Description: "Specifies an email address for the new user.",
Value: clibase.StringOf(&email),
Value: serpent.StringOf(&email),
},
{
Flag: "username",
FlagShorthand: "u",
Description: "Specifies a username for the new user.",
Value: clibase.StringOf(&username),
Value: serpent.StringOf(&username),
},
{
Flag: "password",
FlagShorthand: "p",
Description: "Specifies a password for the new user.",
Value: clibase.StringOf(&password),
Value: serpent.StringOf(&password),
},
{
Flag: "disable-login",
Hidden: true,
Description: "Deprecated: Use '--login-type=none'. \nDisabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. " +
"Be careful when using this flag as it can lock the user out of their account.",
Value: clibase.BoolOf(&disableLogin),
Value: serpent.BoolOf(&disableLogin),
},
{
Flag: "login-type",
@ -148,7 +148,7 @@ Create a workspace `+pretty.Sprint(cliui.DefaultStyles.Code, "coder create")+`!
string(codersdk.LoginTypePassword), string(codersdk.LoginTypeNone), string(codersdk.LoginTypeGithub), string(codersdk.LoginTypeOIDC),
}, ", ",
)),
Value: clibase.StringOf(&loginType),
Value: serpent.StringOf(&loginType),
},
}
return cmd

View File

@ -5,22 +5,22 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) userDelete() *clibase.Cmd {
func (r *RootCmd) userDelete() *serpent.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "delete <username|user_id>",
Short: "Delete a user by username or user_id.",
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
user, err := client.User(ctx, inv.Args[0])
if err != nil {

View File

@ -8,26 +8,26 @@ import (
"github.com/jedib0t/go-pretty/v6/table"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) userList() *clibase.Cmd {
func (r *RootCmd) userList() *serpent.Cmd {
formatter := cliui.NewOutputFormatter(
cliui.TableFormat([]codersdk.User{}, []string{"username", "email", "created_at", "status"}),
cliui.JSONFormat(),
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "list",
Aliases: []string{"ls"},
Middleware: clibase.Chain(
clibase.RequireNArgs(0),
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
res, err := client.Users(inv.Context(), codersdk.UsersRequest{})
if err != nil {
return err
@ -47,14 +47,14 @@ func (r *RootCmd) userList() *clibase.Cmd {
return cmd
}
func (r *RootCmd) userSingle() *clibase.Cmd {
func (r *RootCmd) userSingle() *serpent.Cmd {
formatter := cliui.NewOutputFormatter(
&userShowFormat{},
cliui.JSONFormat(),
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "show <username|user_id|'me'>",
Short: "Show a single user. Use 'me' to indicate the currently authenticated user.",
Long: formatExamples(
@ -62,11 +62,11 @@ func (r *RootCmd) userSingle() *clibase.Cmd {
Command: "coder users show me",
},
),
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
user, err := client.User(inv.Context(), inv.Args[0])
if err != nil {
return err
@ -114,7 +114,7 @@ func (*userShowFormat) ID() string {
}
// AttachOptions implements OutputFormat.
func (*userShowFormat) AttachOptions(_ *clibase.OptionSet) {}
func (*userShowFormat) AttachOptions(_ *serpent.OptionSet) {}
// Format implements OutputFormat.
func (*userShowFormat) Format(_ context.Context, out interface{}) (string, error) {

View File

@ -1,19 +1,19 @@
package cli
import (
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) users() *clibase.Cmd {
cmd := &clibase.Cmd{
func (r *RootCmd) users() *serpent.Cmd {
cmd := &serpent.Cmd{
Short: "Manage users",
Use: "users [subcommand]",
Aliases: []string{"user"},
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
Children: []*serpent.Cmd{
r.userCreate(),
r.userList(),
r.userSingle(),

View File

@ -8,13 +8,13 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
// createUserStatusCommand sets a user status.
func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *clibase.Cmd {
func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *serpent.Cmd {
var verb string
var pastVerb string
var aliases []string
@ -36,7 +36,7 @@ func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *clibas
client := new(codersdk.Client)
var columns []string
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: fmt.Sprintf("%s <username|user_id>", verb),
Short: short,
Aliases: aliases,
@ -45,11 +45,11 @@ func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *clibas
Command: fmt.Sprintf("coder users %s example_user", verb),
},
),
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
identifier := inv.Args[0]
if identifier == "" {
return xerrors.Errorf("user identifier cannot be an empty string")
@ -94,13 +94,13 @@ func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *clibas
return nil
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "column",
FlagShorthand: "c",
Description: "Specify a column to filter in the table.",
Default: strings.Join([]string{"username", "email", "created_at", "status"}, ","),
Value: clibase.StringArrayOf(&columns),
Value: serpent.StringArrayOf(&columns),
},
}
return cmd

View File

@ -8,9 +8,9 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/coderd/schedule/cron"
"github.com/coder/coder/v2/coderd/util/tz"
"github.com/coder/serpent"
)
var (
@ -23,10 +23,10 @@ var (
// This is helpful if the zero value of a flag is meaningful, and you need
// to distinguish between the user setting the flag to the zero value and
// the user not setting the flag at all.
func userSetOption(inv *clibase.Invocation, flagName string) bool {
func userSetOption(inv *serpent.Invocation, flagName string) bool {
for _, opt := range inv.Command.Options {
if opt.Name == flagName {
return !(opt.ValueSource == clibase.ValueSourceNone || opt.ValueSource == clibase.ValueSourceDefault)
return !(opt.ValueSource == serpent.ValueSourceNone || opt.ValueSource == serpent.ValueSourceDefault)
}
}
return false

View File

@ -6,9 +6,9 @@ import (
"time"
"github.com/coder/pretty"
"github.com/coder/serpent"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
)
@ -61,7 +61,7 @@ func defaultVersionInfo() *versionInfo {
}
// version prints the coder version
func (*RootCmd) version(versionInfo func() *versionInfo) *clibase.Cmd {
func (*RootCmd) version(versionInfo func() *versionInfo) *serpent.Cmd {
var (
formatter = cliui.NewOutputFormatter(
cliui.TextFormat(),
@ -70,11 +70,11 @@ func (*RootCmd) version(versionInfo func() *versionInfo) *clibase.Cmd {
vi = versionInfo()
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
Use: "version",
Short: "Show coder version",
Options: clibase.OptionSet{},
Handler: func(inv *clibase.Invocation) error {
Options: serpent.OptionSet{},
Handler: func(inv *serpent.Invocation) error {
out, err := formatter.Format(inv.Context(), vi)
if err != nil {
return err

View File

@ -20,10 +20,10 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
// vscodeSSH is used by the Coder VS Code extension to establish
@ -32,7 +32,7 @@ import (
// This command needs to remain stable for compatibility with
// various VS Code versions, so it's kept separate from our
// standard SSH command.
func (r *RootCmd) vscodeSSH() *clibase.Cmd {
func (r *RootCmd) vscodeSSH() *serpent.Cmd {
var (
sessionTokenFile string
urlFile string
@ -41,15 +41,15 @@ func (r *RootCmd) vscodeSSH() *clibase.Cmd {
networkInfoInterval time.Duration
waitEnum string
)
cmd := &clibase.Cmd{
cmd := &serpent.Cmd{
// A SSH config entry is added by the VS Code extension that
// passes %h to ProxyCommand. The prefix of `coder-vscode--`
// is a magical string represented in our VS Code extension.
// It's not important here, only the delimiter `--` is.
Use: "vscodessh <coder-vscode--<owner>--<workspace>--<agent?>>",
Hidden: true,
Middleware: clibase.RequireNArgs(1),
Handler: func(inv *clibase.Invocation) error {
Middleware: serpent.RequireNArgs(1),
Handler: func(inv *serpent.Invocation) error {
if networkInfoDir == "" {
return xerrors.New("network-info-dir must be specified")
}
@ -234,38 +234,38 @@ func (r *RootCmd) vscodeSSH() *clibase.Cmd {
}
},
}
cmd.Options = clibase.OptionSet{
cmd.Options = serpent.OptionSet{
{
Flag: "network-info-dir",
Description: "Specifies a directory to write network information periodically.",
Value: clibase.StringOf(&networkInfoDir),
Value: serpent.StringOf(&networkInfoDir),
},
{
Flag: "log-dir",
Description: "Specifies a directory to write logs to.",
Value: clibase.StringOf(&logDir),
Value: serpent.StringOf(&logDir),
},
{
Flag: "session-token-file",
Description: "Specifies a file that contains a session token.",
Value: clibase.StringOf(&sessionTokenFile),
Value: serpent.StringOf(&sessionTokenFile),
},
{
Flag: "url-file",
Description: "Specifies a file that contains the Coder URL.",
Value: clibase.StringOf(&urlFile),
Value: serpent.StringOf(&urlFile),
},
{
Flag: "network-info-interval",
Description: "Specifies the interval to update network information.",
Default: "5s",
Value: clibase.DurationOf(&networkInfoInterval),
Value: serpent.DurationOf(&networkInfoInterval),
},
{
Flag: "wait",
Description: "Specifies whether or not to wait for the startup script to finish executing. Auto means that the agent startup script behavior configured in the workspace template is used.",
Default: "auto",
Value: clibase.EnumOf(&waitEnum, "yes", "no", "auto"),
Value: serpent.EnumOf(&waitEnum, "yes", "no", "auto"),
},
}
return cmd

View File

@ -15,18 +15,18 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func main() {
var root *clibase.Cmd
root = &clibase.Cmd{
var root *serpent.Cmd
root = &serpent.Cmd{
Use: "cliui",
Short: "Used for visually testing UI components for the CLI.",
HelpHandler: func(inv *clibase.Invocation) error {
HelpHandler: func(inv *serpent.Invocation) error {
_, _ = fmt.Fprintln(inv.Stdout, "This command is used for visually testing UI components for the CLI.")
_, _ = fmt.Fprintln(inv.Stdout, "It is not intended to be used by end users.")
_, _ = fmt.Fprintln(inv.Stdout, "Subcommands: ")
@ -37,9 +37,9 @@ func main() {
},
}
root.Children = append(root.Children, &clibase.Cmd{
root.Children = append(root.Children, &serpent.Cmd{
Use: "prompt",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
_, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "What is our " + cliui.Field("company name") + "?",
Default: "acme-corp",
@ -75,9 +75,9 @@ func main() {
},
})
root.Children = append(root.Children, &clibase.Cmd{
root.Children = append(root.Children, &serpent.Cmd{
Use: "select",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
value, err := cliui.Select(inv, cliui.SelectOptions{
Options: []string{"Tomato", "Banana", "Onion", "Grape", "Lemon"},
Size: 3,
@ -87,9 +87,9 @@ func main() {
},
})
root.Children = append(root.Children, &clibase.Cmd{
root.Children = append(root.Children, &serpent.Cmd{
Use: "job",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
job := codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobPending,
CreatedAt: dbtime.Now(),
@ -173,9 +173,9 @@ func main() {
},
})
root.Children = append(root.Children, &clibase.Cmd{
root.Children = append(root.Children, &serpent.Cmd{
Use: "agent",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var agent codersdk.WorkspaceAgent
var logs []codersdk.WorkspaceAgentLog
@ -265,9 +265,9 @@ func main() {
},
})
root.Children = append(root.Children, &clibase.Cmd{
root.Children = append(root.Children, &serpent.Cmd{
Use: "resources",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
disconnected := dbtime.Now().Add(-4 * time.Second)
return cliui.WorkspaceResources(inv.Stdout, []codersdk.WorkspaceResource{{
Transition: codersdk.WorkspaceTransitionStart,
@ -315,9 +315,9 @@ func main() {
},
})
root.Children = append(root.Children, &clibase.Cmd{
root.Children = append(root.Children, &serpent.Cmd{
Use: "git-auth",
Handler: func(inv *clibase.Invocation) error {
Handler: func(inv *serpent.Invocation) error {
var count atomic.Int32
var githubAuthed atomic.Bool
var gitlabAuthed atomic.Bool

418
coderd/apidoc/docs.go generated
View File

@ -8028,201 +8028,6 @@ const docTemplate = `{
}
}
},
"clibase.Annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"clibase.Group": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"parent": {
"$ref": "#/definitions/clibase.Group"
},
"yaml": {
"type": "string"
}
}
},
"clibase.HostPort": {
"type": "object",
"properties": {
"host": {
"type": "string"
},
"port": {
"type": "string"
}
}
},
"clibase.Option": {
"type": "object",
"properties": {
"annotations": {
"description": "Annotations enable extensions to clibase higher up in the stack. It's useful for\nhelp formatting and documentation generation.",
"allOf": [
{
"$ref": "#/definitions/clibase.Annotations"
}
]
},
"default": {
"description": "Default is parsed into Value if set.",
"type": "string"
},
"description": {
"type": "string"
},
"env": {
"description": "Env is the environment variable used to configure this option. If unset,\nenvironment configuring is disabled.",
"type": "string"
},
"flag": {
"description": "Flag is the long name of the flag used to configure this option. If unset,\nflag configuring is disabled.",
"type": "string"
},
"flag_shorthand": {
"description": "FlagShorthand is the one-character shorthand for the flag. If unset, no\nshorthand is used.",
"type": "string"
},
"group": {
"description": "Group is a group hierarchy that helps organize this option in help, configs\nand other documentation.",
"allOf": [
{
"$ref": "#/definitions/clibase.Group"
}
]
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"required": {
"description": "Required means this value must be set by some means. It requires\n` + "`" + `ValueSource != ValueSourceNone` + "`" + `\nIf ` + "`" + `Default` + "`" + ` is set, then ` + "`" + `Required` + "`" + ` is ignored.",
"type": "boolean"
},
"use_instead": {
"description": "UseInstead is a list of options that should be used instead of this one.\nThe field is used to generate a deprecation warning.",
"type": "array",
"items": {
"$ref": "#/definitions/clibase.Option"
}
},
"value": {
"description": "Value includes the types listed in values.go."
},
"value_source": {
"$ref": "#/definitions/clibase.ValueSource"
},
"yaml": {
"description": "YAML is the YAML key used to configure this option. If unset, YAML\nconfiguring is disabled.",
"type": "string"
}
}
},
"clibase.Regexp": {
"type": "object"
},
"clibase.Struct-array_codersdk_ExternalAuthConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ExternalAuthConfig"
}
}
}
},
"clibase.Struct-array_codersdk_LinkConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.LinkConfig"
}
}
}
},
"clibase.URL": {
"type": "object",
"properties": {
"forceQuery": {
"description": "append a query ('?') even if RawQuery is empty",
"type": "boolean"
},
"fragment": {
"description": "fragment for references, without '#'",
"type": "string"
},
"host": {
"description": "host or host:port",
"type": "string"
},
"omitHost": {
"description": "do not emit empty host (authority)",
"type": "boolean"
},
"opaque": {
"description": "encoded opaque data",
"type": "string"
},
"path": {
"description": "path (relative paths may omit leading slash)",
"type": "string"
},
"rawFragment": {
"description": "encoded fragment hint (see EscapedFragment method)",
"type": "string"
},
"rawPath": {
"description": "encoded path hint (see EscapedPath method)",
"type": "string"
},
"rawQuery": {
"description": "encoded query values, without '?'",
"type": "string"
},
"scheme": {
"type": "string"
},
"user": {
"description": "username and password information",
"allOf": [
{
"$ref": "#/definitions/url.Userinfo"
}
]
}
}
},
"clibase.ValueSource": {
"type": "string",
"enum": [
"",
"flag",
"env",
"yaml",
"default"
],
"x-enum-varnames": [
"ValueSourceNone",
"ValueSourceFlag",
"ValueSourceEnv",
"ValueSourceYAML",
"ValueSourceDefault"
]
},
"coderd.SCIMUser": {
"type": "object",
"properties": {
@ -9536,7 +9341,7 @@ const docTemplate = `{
"type": "string"
},
"relay_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"stun_addresses": {
"type": "array",
@ -9625,7 +9430,7 @@ const docTemplate = `{
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/clibase.Option"
"$ref": "#/definitions/serpent.Option"
}
}
}
@ -9660,18 +9465,18 @@ const docTemplate = `{
"type": "object",
"properties": {
"access_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"address": {
"description": "DEPRECATED: Use HTTPAddress or TLS.Address instead.",
"allOf": [
{
"$ref": "#/definitions/clibase.HostPort"
"$ref": "#/definitions/serpent.HostPort"
}
]
},
"agent_fallback_troubleshooting_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"agent_stat_refresh_interval": {
"type": "integer"
@ -9716,7 +9521,7 @@ const docTemplate = `{
"type": "boolean"
},
"docs_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"enable_terraform_debug_mode": {
"type": "boolean"
@ -9728,7 +9533,7 @@ const docTemplate = `{
}
},
"external_auth": {
"$ref": "#/definitions/clibase.Struct-array_codersdk_ExternalAuthConfig"
"$ref": "#/definitions/serpent.Struct-array_codersdk_ExternalAuthConfig"
},
"external_token_encryption_keys": {
"type": "array",
@ -10747,13 +10552,13 @@ const docTemplate = `{
"type": "object"
},
"group_regex_filter": {
"$ref": "#/definitions/clibase.Regexp"
"$ref": "#/definitions/serpent.Regexp"
},
"groups_field": {
"type": "string"
},
"icon_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"ignore_email_verified": {
"type": "boolean"
@ -10939,7 +10744,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/clibase.HostPort"
"$ref": "#/definitions/serpent.HostPort"
},
"enable": {
"type": "boolean"
@ -10950,7 +10755,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/clibase.HostPort"
"$ref": "#/definitions/serpent.HostPort"
},
"aggregate_agent_stats_by": {
"type": "array",
@ -11642,7 +11447,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"links": {
"$ref": "#/definitions/clibase.Struct-array_codersdk_LinkConfig"
"$ref": "#/definitions/serpent.Struct-array_codersdk_LinkConfig"
}
}
},
@ -11658,7 +11463,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/clibase.HostPort"
"$ref": "#/definitions/serpent.HostPort"
},
"allow_insecure_ciphers": {
"type": "boolean"
@ -11714,7 +11519,7 @@ const docTemplate = `{
"type": "boolean"
},
"url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
}
}
},
@ -14240,6 +14045,201 @@ const docTemplate = `{
}
}
},
"serpent.Annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"serpent.Group": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"parent": {
"$ref": "#/definitions/serpent.Group"
},
"yaml": {
"type": "string"
}
}
},
"serpent.HostPort": {
"type": "object",
"properties": {
"host": {
"type": "string"
},
"port": {
"type": "string"
}
}
},
"serpent.Option": {
"type": "object",
"properties": {
"annotations": {
"description": "Annotations enable extensions to serpent higher up in the stack. It's useful for\nhelp formatting and documentation generation.",
"allOf": [
{
"$ref": "#/definitions/serpent.Annotations"
}
]
},
"default": {
"description": "Default is parsed into Value if set.",
"type": "string"
},
"description": {
"type": "string"
},
"env": {
"description": "Env is the environment variable used to configure this option. If unset,\nenvironment configuring is disabled.",
"type": "string"
},
"flag": {
"description": "Flag is the long name of the flag used to configure this option. If unset,\nflag configuring is disabled.",
"type": "string"
},
"flag_shorthand": {
"description": "FlagShorthand is the one-character shorthand for the flag. If unset, no\nshorthand is used.",
"type": "string"
},
"group": {
"description": "Group is a group hierarchy that helps organize this option in help, configs\nand other documentation.",
"allOf": [
{
"$ref": "#/definitions/serpent.Group"
}
]
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"required": {
"description": "Required means this value must be set by some means. It requires\n` + "`" + `ValueSource != ValueSourceNone` + "`" + `\nIf ` + "`" + `Default` + "`" + ` is set, then ` + "`" + `Required` + "`" + ` is ignored.",
"type": "boolean"
},
"use_instead": {
"description": "UseInstead is a list of options that should be used instead of this one.\nThe field is used to generate a deprecation warning.",
"type": "array",
"items": {
"$ref": "#/definitions/serpent.Option"
}
},
"value": {
"description": "Value includes the types listed in values.go."
},
"value_source": {
"$ref": "#/definitions/serpent.ValueSource"
},
"yaml": {
"description": "YAML is the YAML key used to configure this option. If unset, YAML\nconfiguring is disabled.",
"type": "string"
}
}
},
"serpent.Regexp": {
"type": "object"
},
"serpent.Struct-array_codersdk_ExternalAuthConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ExternalAuthConfig"
}
}
}
},
"serpent.Struct-array_codersdk_LinkConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.LinkConfig"
}
}
}
},
"serpent.URL": {
"type": "object",
"properties": {
"forceQuery": {
"description": "append a query ('?') even if RawQuery is empty",
"type": "boolean"
},
"fragment": {
"description": "fragment for references, without '#'",
"type": "string"
},
"host": {
"description": "host or host:port",
"type": "string"
},
"omitHost": {
"description": "do not emit empty host (authority)",
"type": "boolean"
},
"opaque": {
"description": "encoded opaque data",
"type": "string"
},
"path": {
"description": "path (relative paths may omit leading slash)",
"type": "string"
},
"rawFragment": {
"description": "encoded fragment hint (see EscapedFragment method)",
"type": "string"
},
"rawPath": {
"description": "encoded path hint (see EscapedPath method)",
"type": "string"
},
"rawQuery": {
"description": "encoded query values, without '?'",
"type": "string"
},
"scheme": {
"type": "string"
},
"user": {
"description": "username and password information",
"allOf": [
{
"$ref": "#/definitions/url.Userinfo"
}
]
}
}
},
"serpent.ValueSource": {
"type": "string",
"enum": [
"",
"flag",
"env",
"yaml",
"default"
],
"x-enum-varnames": [
"ValueSourceNone",
"ValueSourceFlag",
"ValueSourceEnv",
"ValueSourceYAML",
"ValueSourceDefault"
]
},
"tailcfg.DERPHomeParams": {
"type": "object",
"properties": {

View File

@ -7119,195 +7119,6 @@
}
}
},
"clibase.Annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"clibase.Group": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"parent": {
"$ref": "#/definitions/clibase.Group"
},
"yaml": {
"type": "string"
}
}
},
"clibase.HostPort": {
"type": "object",
"properties": {
"host": {
"type": "string"
},
"port": {
"type": "string"
}
}
},
"clibase.Option": {
"type": "object",
"properties": {
"annotations": {
"description": "Annotations enable extensions to clibase higher up in the stack. It's useful for\nhelp formatting and documentation generation.",
"allOf": [
{
"$ref": "#/definitions/clibase.Annotations"
}
]
},
"default": {
"description": "Default is parsed into Value if set.",
"type": "string"
},
"description": {
"type": "string"
},
"env": {
"description": "Env is the environment variable used to configure this option. If unset,\nenvironment configuring is disabled.",
"type": "string"
},
"flag": {
"description": "Flag is the long name of the flag used to configure this option. If unset,\nflag configuring is disabled.",
"type": "string"
},
"flag_shorthand": {
"description": "FlagShorthand is the one-character shorthand for the flag. If unset, no\nshorthand is used.",
"type": "string"
},
"group": {
"description": "Group is a group hierarchy that helps organize this option in help, configs\nand other documentation.",
"allOf": [
{
"$ref": "#/definitions/clibase.Group"
}
]
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"required": {
"description": "Required means this value must be set by some means. It requires\n`ValueSource != ValueSourceNone`\nIf `Default` is set, then `Required` is ignored.",
"type": "boolean"
},
"use_instead": {
"description": "UseInstead is a list of options that should be used instead of this one.\nThe field is used to generate a deprecation warning.",
"type": "array",
"items": {
"$ref": "#/definitions/clibase.Option"
}
},
"value": {
"description": "Value includes the types listed in values.go."
},
"value_source": {
"$ref": "#/definitions/clibase.ValueSource"
},
"yaml": {
"description": "YAML is the YAML key used to configure this option. If unset, YAML\nconfiguring is disabled.",
"type": "string"
}
}
},
"clibase.Regexp": {
"type": "object"
},
"clibase.Struct-array_codersdk_ExternalAuthConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ExternalAuthConfig"
}
}
}
},
"clibase.Struct-array_codersdk_LinkConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.LinkConfig"
}
}
}
},
"clibase.URL": {
"type": "object",
"properties": {
"forceQuery": {
"description": "append a query ('?') even if RawQuery is empty",
"type": "boolean"
},
"fragment": {
"description": "fragment for references, without '#'",
"type": "string"
},
"host": {
"description": "host or host:port",
"type": "string"
},
"omitHost": {
"description": "do not emit empty host (authority)",
"type": "boolean"
},
"opaque": {
"description": "encoded opaque data",
"type": "string"
},
"path": {
"description": "path (relative paths may omit leading slash)",
"type": "string"
},
"rawFragment": {
"description": "encoded fragment hint (see EscapedFragment method)",
"type": "string"
},
"rawPath": {
"description": "encoded path hint (see EscapedPath method)",
"type": "string"
},
"rawQuery": {
"description": "encoded query values, without '?'",
"type": "string"
},
"scheme": {
"type": "string"
},
"user": {
"description": "username and password information",
"allOf": [
{
"$ref": "#/definitions/url.Userinfo"
}
]
}
}
},
"clibase.ValueSource": {
"type": "string",
"enum": ["", "flag", "env", "yaml", "default"],
"x-enum-varnames": [
"ValueSourceNone",
"ValueSourceFlag",
"ValueSourceEnv",
"ValueSourceYAML",
"ValueSourceDefault"
]
},
"coderd.SCIMUser": {
"type": "object",
"properties": {
@ -8521,7 +8332,7 @@
"type": "string"
},
"relay_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"stun_addresses": {
"type": "array",
@ -8606,7 +8417,7 @@
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/clibase.Option"
"$ref": "#/definitions/serpent.Option"
}
}
}
@ -8641,18 +8452,18 @@
"type": "object",
"properties": {
"access_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"address": {
"description": "DEPRECATED: Use HTTPAddress or TLS.Address instead.",
"allOf": [
{
"$ref": "#/definitions/clibase.HostPort"
"$ref": "#/definitions/serpent.HostPort"
}
]
},
"agent_fallback_troubleshooting_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"agent_stat_refresh_interval": {
"type": "integer"
@ -8697,7 +8508,7 @@
"type": "boolean"
},
"docs_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"enable_terraform_debug_mode": {
"type": "boolean"
@ -8709,7 +8520,7 @@
}
},
"external_auth": {
"$ref": "#/definitions/clibase.Struct-array_codersdk_ExternalAuthConfig"
"$ref": "#/definitions/serpent.Struct-array_codersdk_ExternalAuthConfig"
},
"external_token_encryption_keys": {
"type": "array",
@ -9669,13 +9480,13 @@
"type": "object"
},
"group_regex_filter": {
"$ref": "#/definitions/clibase.Regexp"
"$ref": "#/definitions/serpent.Regexp"
},
"groups_field": {
"type": "string"
},
"icon_url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
},
"ignore_email_verified": {
"type": "boolean"
@ -9847,7 +9658,7 @@
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/clibase.HostPort"
"$ref": "#/definitions/serpent.HostPort"
},
"enable": {
"type": "boolean"
@ -9858,7 +9669,7 @@
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/clibase.HostPort"
"$ref": "#/definitions/serpent.HostPort"
},
"aggregate_agent_stats_by": {
"type": "array",
@ -10516,7 +10327,7 @@
"type": "object",
"properties": {
"links": {
"$ref": "#/definitions/clibase.Struct-array_codersdk_LinkConfig"
"$ref": "#/definitions/serpent.Struct-array_codersdk_LinkConfig"
}
}
},
@ -10532,7 +10343,7 @@
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/clibase.HostPort"
"$ref": "#/definitions/serpent.HostPort"
},
"allow_insecure_ciphers": {
"type": "boolean"
@ -10588,7 +10399,7 @@
"type": "boolean"
},
"url": {
"$ref": "#/definitions/clibase.URL"
"$ref": "#/definitions/serpent.URL"
}
}
},
@ -12972,6 +12783,195 @@
}
}
},
"serpent.Annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"serpent.Group": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"parent": {
"$ref": "#/definitions/serpent.Group"
},
"yaml": {
"type": "string"
}
}
},
"serpent.HostPort": {
"type": "object",
"properties": {
"host": {
"type": "string"
},
"port": {
"type": "string"
}
}
},
"serpent.Option": {
"type": "object",
"properties": {
"annotations": {
"description": "Annotations enable extensions to serpent higher up in the stack. It's useful for\nhelp formatting and documentation generation.",
"allOf": [
{
"$ref": "#/definitions/serpent.Annotations"
}
]
},
"default": {
"description": "Default is parsed into Value if set.",
"type": "string"
},
"description": {
"type": "string"
},
"env": {
"description": "Env is the environment variable used to configure this option. If unset,\nenvironment configuring is disabled.",
"type": "string"
},
"flag": {
"description": "Flag is the long name of the flag used to configure this option. If unset,\nflag configuring is disabled.",
"type": "string"
},
"flag_shorthand": {
"description": "FlagShorthand is the one-character shorthand for the flag. If unset, no\nshorthand is used.",
"type": "string"
},
"group": {
"description": "Group is a group hierarchy that helps organize this option in help, configs\nand other documentation.",
"allOf": [
{
"$ref": "#/definitions/serpent.Group"
}
]
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"required": {
"description": "Required means this value must be set by some means. It requires\n`ValueSource != ValueSourceNone`\nIf `Default` is set, then `Required` is ignored.",
"type": "boolean"
},
"use_instead": {
"description": "UseInstead is a list of options that should be used instead of this one.\nThe field is used to generate a deprecation warning.",
"type": "array",
"items": {
"$ref": "#/definitions/serpent.Option"
}
},
"value": {
"description": "Value includes the types listed in values.go."
},
"value_source": {
"$ref": "#/definitions/serpent.ValueSource"
},
"yaml": {
"description": "YAML is the YAML key used to configure this option. If unset, YAML\nconfiguring is disabled.",
"type": "string"
}
}
},
"serpent.Regexp": {
"type": "object"
},
"serpent.Struct-array_codersdk_ExternalAuthConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.ExternalAuthConfig"
}
}
}
},
"serpent.Struct-array_codersdk_LinkConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.LinkConfig"
}
}
}
},
"serpent.URL": {
"type": "object",
"properties": {
"forceQuery": {
"description": "append a query ('?') even if RawQuery is empty",
"type": "boolean"
},
"fragment": {
"description": "fragment for references, without '#'",
"type": "string"
},
"host": {
"description": "host or host:port",
"type": "string"
},
"omitHost": {
"description": "do not emit empty host (authority)",
"type": "boolean"
},
"opaque": {
"description": "encoded opaque data",
"type": "string"
},
"path": {
"description": "path (relative paths may omit leading slash)",
"type": "string"
},
"rawFragment": {
"description": "encoded fragment hint (see EscapedFragment method)",
"type": "string"
},
"rawPath": {
"description": "encoded path hint (see EscapedPath method)",
"type": "string"
},
"rawQuery": {
"description": "encoded query values, without '?'",
"type": "string"
},
"scheme": {
"type": "string"
},
"user": {
"description": "username and password information",
"allOf": [
{
"$ref": "#/definitions/url.Userinfo"
}
]
}
}
},
"serpent.ValueSource": {
"type": "string",
"enum": ["", "flag", "env", "yaml", "default"],
"x-enum-varnames": [
"ValueSourceNone",
"ValueSourceFlag",
"ValueSourceEnv",
"ValueSourceYAML",
"ValueSourceDefault"
]
},
"tailcfg.DERPHomeParams": {
"type": "object",
"properties": {

View File

@ -10,7 +10,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
@ -18,6 +17,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
func TestTokenCRUD(t *testing.T) {
@ -125,7 +125,7 @@ func TestTokenUserSetMaxLifetime(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dc := coderdtest.DeploymentValues(t)
dc.MaxTokenLifetime = clibase.Duration(time.Hour * 24 * 7)
dc.MaxTokenLifetime = serpent.Duration(time.Hour * 24 * 7)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: dc,
})
@ -165,7 +165,7 @@ func TestSessionExpiry(t *testing.T) {
//
// We don't support updating the deployment config after startup, but for
// this test it works because we don't copy the value (and we use pointers).
dc.SessionDuration = clibase.Duration(time.Second)
dc.SessionDuration = serpent.Duration(time.Second)
userClient, _ := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID)

View File

@ -40,7 +40,6 @@ import (
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
_ "github.com/coder/coder/v2/coderd/apidoc" // Used for swagger docs.
"github.com/coder/coder/v2/coderd/appearance"
"github.com/coder/coder/v2/coderd/audit"
@ -73,6 +72,7 @@ import (
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/site"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/serpent"
)
// We must only ever instantiate one httpSwagger.Handler because of a data race
@ -169,7 +169,7 @@ type Options struct {
// contextual information about how the values were set.
// Do not use DeploymentOptions to retrieve values, use DeploymentValues instead.
// All secrets values are stripped.
DeploymentOptions clibase.OptionSet
DeploymentOptions serpent.OptionSet
UpdateCheckOptions *updatecheck.Options // Set non-nil to enable update checking.
// SSHConfig is the response clients use to configure config-ssh locally.

View File

@ -25,7 +25,6 @@ import (
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
@ -42,6 +41,7 @@ import (
"github.com/coder/coder/v2/provisionersdk"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)
func mockAuditor() *atomic.Pointer[audit.Auditor] {
@ -171,7 +171,7 @@ func TestAcquireJob(t *testing.T) {
// Set the max session token lifetime so we can assert we
// create an API key with an expiration within the bounds of the
// deployment config.
dv := &codersdk.DeploymentValues{MaxTokenLifetime: clibase.Duration(time.Hour)}
dv := &codersdk.DeploymentValues{MaxTokenLifetime: serpent.Duration(time.Hour)}
gitAuthProvider := &sdkproto.ExternalAuthProviderResource{
Id: "github",
}

Some files were not shown because too many files have changed in this diff Show More