feat: refactor deployment config (#6347)

This commit is contained in:
Ammar Bandukwala 2023-03-07 15:10:01 -06:00 committed by GitHub
parent bb0a996fc2
commit 3b73321a6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 5643 additions and 6682 deletions

View File

@ -215,7 +215,6 @@ linters:
- asciicheck
- bidichk
- bodyclose
- deadcode
- dogsled
- errcheck
- errname
@ -259,4 +258,3 @@ linters:
- typecheck
- unconvert
- unused
- varcheck

View File

@ -501,7 +501,8 @@ docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/me
yarn run format:write:only ../docs/admin/prometheus.md
docs/cli.md: scripts/clidocgen/main.go $(GO_SRC_FILES) docs/manifest.json
rm -rf ./docs/cli/*.md
# TODO(@ammario): re-enable server.md once we finish clibase migration.
ls ./docs/cli/*.md | grep -vP "\/coder_server" | xargs rm
BASE_PATH="." go run ./scripts/clidocgen
cd site
yarn run format:write:only ../docs/cli.md ../docs/cli/*.md ../docs/manifest.json

86
cli/clibase/clibase.go Normal file
View File

@ -0,0 +1,86 @@
// Package clibase offers an all-in-one solution for a highly configurable CLI
// application. Within Coder, we use it for our `server` subcommand, which
// demands more functionality than cobra/viper can offer.
//
// We will extend its usage to the rest of our application, completely replacing
// cobra/viper. It's also a candidate to be broken out into its own open-source
// library, so we avoid deep coupling with Coder concepts.
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"`
Children []Group `json:"children,omitempty"`
Description string `json:"description,omitempty"`
}
func (g *Group) AddChild(child Group) {
child.Parent = g
g.Children = append(g.Children, child)
}
// 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
}

48
cli/clibase/cmd.go Normal file
View File

@ -0,0 +1,48 @@
package clibase
import "strings"
// 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
// Short is a one-line description of the command.
Short string
// Long is a detailed description of the command,
// presented on its help page. It may contain examples.
Long string
Options OptionSet
Annotations Annotations
}
// 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.FullUsage())
}
uses = append(uses, c.Use)
return strings.Join(uses, " ")
}

42
cli/clibase/env.go Normal file
View File

@ -0,0 +1,42 @@
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
}
// EnvsWithPrefix returns all environment variables starting with
// prefix without said prefix.
func EnvsWithPrefix(environ []string, prefix string) []EnvVar {
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
}

44
cli/clibase/env_test.go Normal file
View File

@ -0,0 +1,44 @@
package clibase_test
import (
"reflect"
"testing"
"github.com/coder/coder/cli/clibase"
)
func TestFilterNamePrefix(t *testing.T) {
t.Parallel()
type args struct {
environ []string
prefix string
}
tests := []struct {
name string
args args
want []clibase.EnvVar
}{
{"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.EnvsWithPrefix(tt.args.environ, tt.args.prefix); !reflect.DeepEqual(got, tt.want) {
t.Errorf("EnvsWithPrefix() = %v, want %v", got, tt.want)
}
})
}
}

149
cli/clibase/option.go Normal file
View File

@ -0,0 +1,149 @@
package clibase
import (
"os"
"github.com/hashicorp/go-multierror"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
// Option is a configuration option for a CLI application.
type Option struct {
Name string `json:"name,omitempty"`
Description string `json:"description,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"`
}
// OptionSet is a group of options that can be applied to a command.
type OptionSet []Option
// Add adds the given Options to the OptionSet.
func (s *OptionSet) Add(opts ...Option) {
*s = append(*s, opts...)
}
// FlagSet returns a pflag.FlagSet for the OptionSet.
func (s *OptionSet) FlagSet() *pflag.FlagSet {
fs := pflag.NewFlagSet("", pflag.ContinueOnError)
for _, opt := range *s {
if opt.Flag == "" {
continue
}
var noOptDefValue string
{
no, ok := opt.Value.(NoOptDefValuer)
if ok {
noOptDefValue = no.NoOptDefValue()
}
}
fs.AddFlag(&pflag.Flag{
Name: opt.Flag,
Shorthand: opt.FlagShorthand,
Usage: opt.Description,
Value: opt.Value,
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.
func (s *OptionSet) ParseEnv(globalPrefix string, environ []string) error {
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 EnvsWithPrefix(environ, globalPrefix) {
envs[v.Name] = v.Value
}
for _, opt := range *s {
if opt.Env == "" {
continue
}
envVal, ok := envs[opt.Env]
if !ok {
continue
}
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.
// It should be called before all parsing (e.g. ParseFlags, ParseEnv).
func (s *OptionSet) SetDefaults() error {
var merr *multierror.Error
for _, opt := range *s {
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
}
if err := opt.Value.Set(opt.Default); err != nil {
merr = multierror.Append(
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
)
}
}
return merr.ErrorOrNil()
}

View File

@ -0,0 +1,94 @@
package clibase_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clibase"
)
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("Strings", func(t *testing.T) {
t.Parallel()
var names clibase.Strings
os := clibase.OptionSet{
clibase.Option{
Name: "name",
Value: &names,
Flag: "name",
FlagShorthand: "n",
},
}
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)
})
}
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("CODER_", []string{"CODER_WORKSPACE_NAME=foo"})
require.NoError(t, err)
require.EqualValues(t, "foo", workspaceName)
})
}

335
cli/clibase/values.go Normal file
View File

@ -0,0 +1,335 @@
package clibase
import (
"encoding/csv"
"encoding/json"
"fmt"
"net"
"net/url"
"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
}
// values.go contains a standard set of value types that can be used as
// Option Values.
type Int64 int64
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 (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 (*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 = &Strings{}
// Strings is a slice of strings that implements pflag.Value and pflag.SliceValue.
type Strings []string
func (s *Strings) Append(v string) error {
*s = append(*s, v)
return nil
}
func (s *Strings) Replace(vals []string) error {
*s = vals
return nil
}
func (s *Strings) 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 *Strings) Set(v string) error {
ss, err := readAsCSV(v)
if err != nil {
return err
}
*s = append(*s, ss...)
return nil
}
func (s Strings) String() string {
return writeAsCSV([]string(s))
}
func (s Strings) Value() []string {
return []string(s)
}
func (Strings) Type() string {
return "strings"
}
type Duration time.Duration
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 (d *Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
return d.Set(s)
}
func (Duration) Type() string {
return "duration"
}
type URL url.URL
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) 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 (*HostPort) Type() string {
return "bind-address"
}
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
}
func (s *Struct[T]) Set(v string) error {
return yaml.Unmarshal([]byte(v), &s.Value)
}
func (s *Struct[T]) String() string {
byt, err := yaml.Marshal(s.Value)
if err != nil {
return "decode failed: " + err.Error()
}
return string(byt)
}
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
}
func (s *Struct[T]) UnmarshalYAML(n *yaml.Node) error {
return n.Decode(&s.Value)
}
func (s *Struct[T]) Type() string {
return fmt.Sprintf("struct[%T]", s.Value)
}
func (s *Struct[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Value)
}
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.
type DiscardValue struct{}
func (DiscardValue) Set(string) error {
return nil
}
func (DiscardValue) String() string {
return ""
}
func (DiscardValue) Type() string {
return "discard"
}

105
cli/clibase/yaml.go Normal file
View File

@ -0,0 +1,105 @@
package clibase
import (
"github.com/iancoleman/strcase"
"github.com/mitchellh/go-wordwrap"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
// 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)
}
// ToYAML 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.
func (s OptionSet) ToYAML() (*yaml.Node, error) {
root := yaml.Node{
Kind: yaml.MappingNode,
}
for _, opt := range s {
if opt.YAML == "" {
continue
}
nameNode := yaml.Node{
Kind: yaml.ScalarNode,
Value: opt.YAML,
HeadComment: wordwrap.WrapString(opt.Description, 80),
}
var valueNode yaml.Node
if m, ok := opt.Value.(yaml.Marshaler); ok {
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 {
valueNode = yaml.Node{
Kind: yaml.ScalarNode,
Value: opt.Value.String(),
}
}
var group []string
for _, g := range opt.Group.Ancestry() {
if g.Name == "" {
return nil, xerrors.Errorf(
"group name is empty for %q, groups: %+v",
opt.Name,
opt.Group,
)
}
group = append(group, strcase.ToLowerCamel(g.Name))
}
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
}

57
cli/clibase/yaml_test.go Normal file
View File

@ -0,0 +1,57 @@
package clibase_test
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/coder/coder/cli/clibase"
)
func TestOption_ToYAML(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.ToYAML()
require.NoError(t, err)
require.Len(t, 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{Name: "Names"},
YAML: "workspaceName",
},
}
err := os.SetDefaults()
require.NoError(t, err)
n, err := os.ToYAML()
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))
})
}

View File

@ -3,6 +3,7 @@ package clitest
import (
"archive/tar"
"bytes"
"context"
"errors"
"io"
"io/ioutil"
@ -10,14 +11,17 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/testutil"
)
// New creates a CLI instance with a configuration pointed to a
@ -26,6 +30,22 @@ func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
return NewWithSubcommands(t, cli.AGPL(), args...)
}
type logWriter struct {
prefix string
t *testing.T
}
func (l *logWriter) Write(p []byte) (n int, err error) {
trimmed := strings.TrimSpace(string(p))
if trimmed == "" {
return len(p), nil
}
l.t.Log(
l.prefix + ": " + trimmed,
)
return len(p), nil
}
func NewWithSubcommands(
t *testing.T, subcommands []*cobra.Command, args ...string,
) (*cobra.Command, config.Root) {
@ -34,10 +54,9 @@ func NewWithSubcommands(
root := config.Root(dir)
cmd.SetArgs(append([]string{"--global-config", dir}, args...))
// We could consider using writers
// that log via t.Log here instead.
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
// These can be overridden by the test.
cmd.SetOut(&logWriter{prefix: "stdout", t: t})
cmd.SetErr(&logWriter{prefix: "stderr", t: t})
return cmd, root
}
@ -98,3 +117,34 @@ 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(ctx context.Context, t *testing.T, cmd *cobra.Command) {
t.Helper()
closeCh := make(chan struct{})
deadline, hasDeadline := ctx.Deadline()
if !hasDeadline {
// We don't want to wait the full 5 minutes for a test to time out.
deadline = time.Now().Add(testutil.WaitMedium)
}
ctx, cancel := context.WithDeadline(ctx, deadline)
go func() {
defer cancel()
defer close(closeCh)
err := cmd.ExecuteContext(ctx)
if ctx.Err() == nil {
assert.NoError(t, err)
}
}()
// Don't exit test routine until server is done.
t.Cleanup(func() {
cancel()
<-closeCh
})
}

View File

@ -4,6 +4,8 @@ import (
"io"
"os"
"path/filepath"
"github.com/kirsle/configdir"
)
const (
@ -46,10 +48,6 @@ func (r Root) PostgresPort() File {
return File(filepath.Join(r.PostgresPath(), "port"))
}
func (r Root) DeploymentConfigPath() string {
return filepath.Join(string(r), "server.yaml")
}
// File provides convenience methods for interacting with *os.File.
type File string
@ -98,3 +96,7 @@ func read(path string) ([]byte, error) {
defer fi.Close()
return io.ReadAll(fi)
}
func DefaultDir() string {
return configdir.LocalConfig("coderv2")
}

View File

@ -1,896 +0,0 @@
package deployment
import (
"flag"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"golang.org/x/xerrors"
"github.com/coder/coder/buildinfo"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/codersdk"
)
func newConfig() *codersdk.DeploymentConfig {
return &codersdk.DeploymentConfig{
AccessURL: &codersdk.DeploymentConfigField[string]{
Name: "Access URL",
Usage: "External URL to access your deployment. This must be accessible by all provisioned workspaces.",
Flag: "access-url",
},
WildcardAccessURL: &codersdk.DeploymentConfigField[string]{
Name: "Wildcard Access URL",
Usage: "Specifies the wildcard hostname to use for workspace applications in the form \"*.example.com\".",
Flag: "wildcard-access-url",
},
RedirectToAccessURL: &codersdk.DeploymentConfigField[bool]{
Name: "Redirect to Access URL",
Usage: "Specifies whether to redirect requests that do not match the access URL host.",
Flag: "redirect-to-access-url",
},
// DEPRECATED: Use HTTPAddress or TLS.Address instead.
Address: &codersdk.DeploymentConfigField[string]{
Name: "Address",
Usage: "Bind address of the server.",
Flag: "address",
Shorthand: "a",
// Deprecated, so we don't have a default. If set, it will overwrite
// HTTPAddress and TLS.Address and print a warning.
Hidden: true,
Default: "",
},
HTTPAddress: &codersdk.DeploymentConfigField[string]{
Name: "Address",
Usage: "HTTP bind address of the server. Unset to disable the HTTP endpoint.",
Flag: "http-address",
Default: "127.0.0.1:3000",
},
AutobuildPollInterval: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Autobuild Poll Interval",
Usage: "Interval to poll for scheduled workspace builds.",
Flag: "autobuild-poll-interval",
Hidden: true,
Default: time.Minute,
},
DERP: &codersdk.DERP{
Server: &codersdk.DERPServerConfig{
Enable: &codersdk.DeploymentConfigField[bool]{
Name: "DERP Server Enable",
Usage: "Whether to enable or disable the embedded DERP relay server.",
Flag: "derp-server-enable",
Default: true,
},
RegionID: &codersdk.DeploymentConfigField[int]{
Name: "DERP Server Region ID",
Usage: "Region ID to use for the embedded DERP server.",
Flag: "derp-server-region-id",
Default: 999,
},
RegionCode: &codersdk.DeploymentConfigField[string]{
Name: "DERP Server Region Code",
Usage: "Region code to use for the embedded DERP server.",
Flag: "derp-server-region-code",
Default: "coder",
},
RegionName: &codersdk.DeploymentConfigField[string]{
Name: "DERP Server Region Name",
Usage: "Region name that for the embedded DERP server.",
Flag: "derp-server-region-name",
Default: "Coder Embedded Relay",
},
STUNAddresses: &codersdk.DeploymentConfigField[[]string]{
Name: "DERP Server STUN Addresses",
Usage: "Addresses for STUN servers to establish P2P connections. Set empty to disable P2P connections.",
Flag: "derp-server-stun-addresses",
Default: []string{"stun.l.google.com:19302"},
},
RelayURL: &codersdk.DeploymentConfigField[string]{
Name: "DERP Server Relay URL",
Usage: "An HTTP URL that is accessible by other replicas to relay DERP traffic. Required for high availability.",
Flag: "derp-server-relay-url",
Enterprise: true,
},
},
Config: &codersdk.DERPConfig{
URL: &codersdk.DeploymentConfigField[string]{
Name: "DERP Config URL",
Usage: "URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custom-derp-servers/",
Flag: "derp-config-url",
},
Path: &codersdk.DeploymentConfigField[string]{
Name: "DERP Config Path",
Usage: "Path to read a DERP mapping from. See: https://tailscale.com/kb/1118/custom-derp-servers/",
Flag: "derp-config-path",
},
},
},
GitAuth: &codersdk.DeploymentConfigField[[]codersdk.GitAuthConfig]{
Name: "Git Auth",
Usage: "Automatically authenticate Git inside workspaces.",
Flag: "gitauth",
Default: []codersdk.GitAuthConfig{},
},
Prometheus: &codersdk.PrometheusConfig{
Enable: &codersdk.DeploymentConfigField[bool]{
Name: "Prometheus Enable",
Usage: "Serve prometheus metrics on the address defined by prometheus address.",
Flag: "prometheus-enable",
},
Address: &codersdk.DeploymentConfigField[string]{
Name: "Prometheus Address",
Usage: "The bind address to serve prometheus metrics.",
Flag: "prometheus-address",
Default: "127.0.0.1:2112",
},
},
Pprof: &codersdk.PprofConfig{
Enable: &codersdk.DeploymentConfigField[bool]{
Name: "Pprof Enable",
Usage: "Serve pprof metrics on the address defined by pprof address.",
Flag: "pprof-enable",
},
Address: &codersdk.DeploymentConfigField[string]{
Name: "Pprof Address",
Usage: "The bind address to serve pprof.",
Flag: "pprof-address",
Default: "127.0.0.1:6060",
},
},
ProxyTrustedHeaders: &codersdk.DeploymentConfigField[[]string]{
Name: "Proxy Trusted Headers",
Flag: "proxy-trusted-headers",
Usage: "Headers to trust for forwarding IP addresses. e.g. Cf-Connecting-Ip, True-Client-Ip, X-Forwarded-For",
},
ProxyTrustedOrigins: &codersdk.DeploymentConfigField[[]string]{
Name: "Proxy Trusted Origins",
Flag: "proxy-trusted-origins",
Usage: "Origin addresses to respect \"proxy-trusted-headers\". e.g. 192.168.1.0/24",
},
CacheDirectory: &codersdk.DeploymentConfigField[string]{
Name: "Cache Directory",
Usage: "The directory to cache temporary files. If unspecified and $CACHE_DIRECTORY is set, it will be used for compatibility with systemd.",
Flag: "cache-dir",
Default: DefaultCacheDir(),
},
InMemoryDatabase: &codersdk.DeploymentConfigField[bool]{
Name: "In Memory Database",
Usage: "Controls whether data will be stored in an in-memory database.",
Flag: "in-memory",
Hidden: true,
},
PostgresURL: &codersdk.DeploymentConfigField[string]{
Name: "Postgres Connection URL",
Usage: "URL of a PostgreSQL database. If empty, PostgreSQL binaries will be downloaded from Maven (https://repo1.maven.org/maven2) and store all data in the config root. Access the built-in database with \"coder server postgres-builtin-url\".",
Flag: "postgres-url",
Secret: true,
},
OAuth2: &codersdk.OAuth2Config{
Github: &codersdk.OAuth2GithubConfig{
ClientID: &codersdk.DeploymentConfigField[string]{
Name: "OAuth2 GitHub Client ID",
Usage: "Client ID for Login with GitHub.",
Flag: "oauth2-github-client-id",
},
ClientSecret: &codersdk.DeploymentConfigField[string]{
Name: "OAuth2 GitHub Client Secret",
Usage: "Client secret for Login with GitHub.",
Flag: "oauth2-github-client-secret",
Secret: true,
},
AllowedOrgs: &codersdk.DeploymentConfigField[[]string]{
Name: "OAuth2 GitHub Allowed Orgs",
Usage: "Organizations the user must be a member of to Login with GitHub.",
Flag: "oauth2-github-allowed-orgs",
},
AllowedTeams: &codersdk.DeploymentConfigField[[]string]{
Name: "OAuth2 GitHub Allowed Teams",
Usage: "Teams inside organizations the user must be a member of to Login with GitHub. Structured as: <organization-name>/<team-slug>.",
Flag: "oauth2-github-allowed-teams",
},
AllowSignups: &codersdk.DeploymentConfigField[bool]{
Name: "OAuth2 GitHub Allow Signups",
Usage: "Whether new users can sign up with GitHub.",
Flag: "oauth2-github-allow-signups",
},
AllowEveryone: &codersdk.DeploymentConfigField[bool]{
Name: "OAuth2 GitHub Allow Everyone",
Usage: "Allow all logins, setting this option means allowed orgs and teams must be empty.",
Flag: "oauth2-github-allow-everyone",
},
EnterpriseBaseURL: &codersdk.DeploymentConfigField[string]{
Name: "OAuth2 GitHub Enterprise Base URL",
Usage: "Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",
Flag: "oauth2-github-enterprise-base-url",
},
},
},
OIDC: &codersdk.OIDCConfig{
AllowSignups: &codersdk.DeploymentConfigField[bool]{
Name: "OIDC Allow Signups",
Usage: "Whether new users can sign up with OIDC.",
Flag: "oidc-allow-signups",
Default: true,
},
ClientID: &codersdk.DeploymentConfigField[string]{
Name: "OIDC Client ID",
Usage: "Client ID to use for Login with OIDC.",
Flag: "oidc-client-id",
},
ClientSecret: &codersdk.DeploymentConfigField[string]{
Name: "OIDC Client Secret",
Usage: "Client secret to use for Login with OIDC.",
Flag: "oidc-client-secret",
Secret: true,
},
EmailDomain: &codersdk.DeploymentConfigField[[]string]{
Name: "OIDC Email Domain",
Usage: "Email domains that clients logging in with OIDC must match.",
Flag: "oidc-email-domain",
},
IssuerURL: &codersdk.DeploymentConfigField[string]{
Name: "OIDC Issuer URL",
Usage: "Issuer URL to use for Login with OIDC.",
Flag: "oidc-issuer-url",
},
Scopes: &codersdk.DeploymentConfigField[[]string]{
Name: "OIDC Scopes",
Usage: "Scopes to grant when authenticating with OIDC.",
Flag: "oidc-scopes",
Default: []string{oidc.ScopeOpenID, "profile", "email"},
},
IgnoreEmailVerified: &codersdk.DeploymentConfigField[bool]{
Name: "OIDC Ignore Email Verified",
Usage: "Ignore the email_verified claim from the upstream provider.",
Flag: "oidc-ignore-email-verified",
Default: false,
},
UsernameField: &codersdk.DeploymentConfigField[string]{
Name: "OIDC Username Field",
Usage: "OIDC claim field to use as the username.",
Flag: "oidc-username-field",
Default: "preferred_username",
},
SignInText: &codersdk.DeploymentConfigField[string]{
Name: "OpenID Connect sign in text",
Usage: "The text to show on the OpenID Connect sign in button",
Flag: "oidc-sign-in-text",
Default: "OpenID Connect",
},
IconURL: &codersdk.DeploymentConfigField[string]{
Name: "OpenID connect icon URL",
Usage: "URL pointing to the icon to use on the OepnID Connect login button",
Flag: "oidc-icon-url",
},
},
Telemetry: &codersdk.TelemetryConfig{
Enable: &codersdk.DeploymentConfigField[bool]{
Name: "Telemetry Enable",
Usage: "Whether telemetry is enabled or not. Coder collects anonymized usage data to help improve our product.",
Flag: "telemetry",
Default: flag.Lookup("test.v") == nil,
},
Trace: &codersdk.DeploymentConfigField[bool]{
Name: "Telemetry Trace",
Usage: "Whether Opentelemetry traces are sent to Coder. Coder collects anonymized application tracing to help improve our product. Disabling telemetry also disables this option.",
Flag: "telemetry-trace",
Default: flag.Lookup("test.v") == nil,
},
URL: &codersdk.DeploymentConfigField[string]{
Name: "Telemetry URL",
Usage: "URL to send telemetry.",
Flag: "telemetry-url",
Hidden: true,
Default: "https://telemetry.coder.com",
},
},
TLS: &codersdk.TLSConfig{
Enable: &codersdk.DeploymentConfigField[bool]{
Name: "TLS Enable",
Usage: "Whether TLS will be enabled.",
Flag: "tls-enable",
},
Address: &codersdk.DeploymentConfigField[string]{
Name: "TLS Address",
Usage: "HTTPS bind address of the server.",
Flag: "tls-address",
Default: "127.0.0.1:3443",
},
// DEPRECATED: Use RedirectToAccessURL instead.
RedirectHTTP: &codersdk.DeploymentConfigField[bool]{
Name: "Redirect HTTP to HTTPS",
Usage: "Whether HTTP requests will be redirected to the access URL (if it's a https URL and TLS is enabled). Requests to local IP addresses are never redirected regardless of this setting.",
Flag: "tls-redirect-http-to-https",
Default: true,
Hidden: true,
},
CertFiles: &codersdk.DeploymentConfigField[[]string]{
Name: "TLS Certificate Files",
Usage: "Path to each certificate for TLS. It requires a PEM-encoded file. To configure the listener to use a CA certificate, concatenate the primary certificate and the CA certificate together. The primary certificate should appear first in the combined file.",
Flag: "tls-cert-file",
},
ClientCAFile: &codersdk.DeploymentConfigField[string]{
Name: "TLS Client CA Files",
Usage: "PEM-encoded Certificate Authority file used for checking the authenticity of client",
Flag: "tls-client-ca-file",
},
ClientAuth: &codersdk.DeploymentConfigField[string]{
Name: "TLS Client Auth",
Usage: "Policy the server will follow for TLS Client Authentication. Accepted values are \"none\", \"request\", \"require-any\", \"verify-if-given\", or \"require-and-verify\".",
Flag: "tls-client-auth",
Default: "none",
},
KeyFiles: &codersdk.DeploymentConfigField[[]string]{
Name: "TLS Key Files",
Usage: "Paths to the private keys for each of the certificates. It requires a PEM-encoded file.",
Flag: "tls-key-file",
},
MinVersion: &codersdk.DeploymentConfigField[string]{
Name: "TLS Minimum Version",
Usage: "Minimum supported version of TLS. Accepted values are \"tls10\", \"tls11\", \"tls12\" or \"tls13\"",
Flag: "tls-min-version",
Default: "tls12",
},
ClientCertFile: &codersdk.DeploymentConfigField[string]{
Name: "TLS Client Cert File",
Usage: "Path to certificate for client TLS authentication. It requires a PEM-encoded file.",
Flag: "tls-client-cert-file",
},
ClientKeyFile: &codersdk.DeploymentConfigField[string]{
Name: "TLS Client Key File",
Usage: "Path to key for client TLS authentication. It requires a PEM-encoded file.",
Flag: "tls-client-key-file",
},
},
Trace: &codersdk.TraceConfig{
Enable: &codersdk.DeploymentConfigField[bool]{
Name: "Trace Enable",
Usage: "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",
Flag: "trace",
},
HoneycombAPIKey: &codersdk.DeploymentConfigField[string]{
Name: "Trace Honeycomb API Key",
Usage: "Enables trace exporting to Honeycomb.io using the provided API Key.",
Flag: "trace-honeycomb-api-key",
Secret: true,
},
CaptureLogs: &codersdk.DeploymentConfigField[bool]{
Name: "Capture Logs in Traces",
Usage: "Enables capturing of logs as events in traces. This is useful for debugging, but may result in a very large amount of events being sent to the tracing backend which may incur significant costs. If the verbose flag was supplied, debug-level logs will be included.",
Flag: "trace-logs",
},
},
SecureAuthCookie: &codersdk.DeploymentConfigField[bool]{
Name: "Secure Auth Cookie",
Usage: "Controls if the 'Secure' property is set on browser session cookies.",
Flag: "secure-auth-cookie",
},
StrictTransportSecurity: &codersdk.DeploymentConfigField[int]{
Name: "Strict-Transport-Security",
Usage: "Controls if the 'Strict-Transport-Security' header is set on all static file responses. " +
"This header should only be set if the server is accessed via HTTPS. This value is the MaxAge in seconds of " +
"the header.",
Default: 0,
Flag: "strict-transport-security",
},
StrictTransportSecurityOptions: &codersdk.DeploymentConfigField[[]string]{
Name: "Strict-Transport-Security Options",
Usage: "Two optional fields can be set in the Strict-Transport-Security header; 'includeSubDomains' and 'preload'. " +
"The 'strict-transport-security' flag must be set to a non-zero value for these options to be used.",
Flag: "strict-transport-security-options",
},
SSHKeygenAlgorithm: &codersdk.DeploymentConfigField[string]{
Name: "SSH Keygen Algorithm",
Usage: "The algorithm to use for generating ssh keys. Accepted values are \"ed25519\", \"ecdsa\", or \"rsa4096\".",
Flag: "ssh-keygen-algorithm",
Default: "ed25519",
},
MetricsCacheRefreshInterval: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Metrics Cache Refresh Interval",
Usage: "How frequently metrics are refreshed",
Flag: "metrics-cache-refresh-interval",
Hidden: true,
Default: time.Hour,
},
AgentStatRefreshInterval: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Agent Stat Refresh Interval",
Usage: "How frequently agent stats are recorded",
Flag: "agent-stats-refresh-interval",
Hidden: true,
Default: 10 * time.Minute,
},
AgentFallbackTroubleshootingURL: &codersdk.DeploymentConfigField[string]{
Name: "Agent Fallback Troubleshooting URL",
Usage: "URL to use for agent troubleshooting when not set in the template",
Flag: "agent-fallback-troubleshooting-url",
Hidden: true,
Default: "https://coder.com/docs/coder-oss/latest/templates#troubleshooting-templates",
},
AuditLogging: &codersdk.DeploymentConfigField[bool]{
Name: "Audit Logging",
Usage: "Specifies whether audit logging is enabled.",
Flag: "audit-logging",
Default: true,
Enterprise: true,
},
BrowserOnly: &codersdk.DeploymentConfigField[bool]{
Name: "Browser Only",
Usage: "Whether Coder only allows connections to workspaces via the browser.",
Flag: "browser-only",
Enterprise: true,
},
SCIMAPIKey: &codersdk.DeploymentConfigField[string]{
Name: "SCIM API Key",
Usage: "Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication.",
Flag: "scim-auth-header",
Enterprise: true,
Secret: true,
},
Provisioner: &codersdk.ProvisionerConfig{
Daemons: &codersdk.DeploymentConfigField[int]{
Name: "Provisioner Daemons",
Usage: "Number of provisioner daemons to create on start. If builds are stuck in queued state for a long time, consider increasing this.",
Flag: "provisioner-daemons",
Default: 3,
},
DaemonPollInterval: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Poll Interval",
Usage: "Time to wait before polling for a new job.",
Flag: "provisioner-daemon-poll-interval",
Default: time.Second,
},
DaemonPollJitter: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Poll Jitter",
Usage: "Random jitter added to the poll interval.",
Flag: "provisioner-daemon-poll-jitter",
Default: 100 * time.Millisecond,
},
ForceCancelInterval: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Force Cancel Interval",
Usage: "Time to force cancel provisioning tasks that are stuck.",
Flag: "provisioner-force-cancel-interval",
Default: 10 * time.Minute,
},
},
RateLimit: &codersdk.RateLimitConfig{
DisableAll: &codersdk.DeploymentConfigField[bool]{
Name: "Disable All Rate Limits",
Usage: "Disables all rate limits. This is not recommended in production.",
Flag: "dangerous-disable-rate-limits",
Default: false,
},
API: &codersdk.DeploymentConfigField[int]{
Name: "API Rate Limit",
Usage: "Maximum number of requests per minute allowed to the API per user, or per IP address for unauthenticated users. Negative values mean no rate limit. Some API endpoints have separate strict rate limits regardless of this value to prevent denial-of-service or brute force attacks.",
// Change the env from the auto-generated CODER_RATE_LIMIT_API to the
// old value to avoid breaking existing deployments.
EnvOverride: "CODER_API_RATE_LIMIT",
Flag: "api-rate-limit",
Default: 512,
},
},
// DEPRECATED: use Experiments instead.
Experimental: &codersdk.DeploymentConfigField[bool]{
Name: "Experimental",
Usage: "Enable experimental features. Experimental features are not ready for production.",
Flag: "experimental",
Default: false,
Hidden: true,
},
Experiments: &codersdk.DeploymentConfigField[[]string]{
Name: "Experiments",
Usage: "Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.",
Flag: "experiments",
Default: []string{},
},
UpdateCheck: &codersdk.DeploymentConfigField[bool]{
Name: "Update Check",
Usage: "Periodically check for new releases of Coder and inform the owner. The check is performed once per day.",
Flag: "update-check",
Default: flag.Lookup("test.v") == nil && !buildinfo.IsDev(),
},
MaxTokenLifetime: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Max Token Lifetime",
Usage: "The maximum lifetime duration users can specify when creating an API token.",
Flag: "max-token-lifetime",
// max time.Duration is 290 years
Default: 290 * 365 * 24 * time.Hour,
},
Swagger: &codersdk.SwaggerConfig{
Enable: &codersdk.DeploymentConfigField[bool]{
Name: "Enable swagger endpoint",
Usage: "Expose the swagger endpoint via /swagger.",
Flag: "swagger-enable",
Default: false,
},
},
Logging: &codersdk.LoggingConfig{
Human: &codersdk.DeploymentConfigField[string]{
Name: "Human Log Location",
Usage: "Output human-readable logs to a given file.",
Flag: "log-human",
Default: "/dev/stderr",
},
JSON: &codersdk.DeploymentConfigField[string]{
Name: "JSON Log Location",
Usage: "Output JSON logs to a given file.",
Flag: "log-json",
Default: "",
},
Stackdriver: &codersdk.DeploymentConfigField[string]{
Name: "Stackdriver Log Location",
Usage: "Output Stackdriver compatible logs to a given file.",
Flag: "log-stackdriver",
Default: "",
},
},
Dangerous: &codersdk.DangerousConfig{
AllowPathAppSharing: &codersdk.DeploymentConfigField[bool]{
Name: "DANGEROUS: Allow Path App Sharing",
Usage: "Allow workspace apps that are not served from subdomains to be shared. Path-based app sharing is DISABLED by default for security purposes. Path-based apps can make requests to the Coder API and pose a security risk when the workspace serves malicious JavaScript. Path-based apps can be disabled entirely with --disable-path-apps for further security.",
Flag: "dangerous-allow-path-app-sharing",
Default: false,
},
AllowPathAppSiteOwnerAccess: &codersdk.DeploymentConfigField[bool]{
Name: "DANGEROUS: Allow Site Owners to Access Path Apps",
Usage: "Allow site-owners to access workspace apps from workspaces they do not own. Owners cannot access path-based apps they do not own by default. Path-based apps can make requests to the Coder API and pose a security risk when the workspace serves malicious JavaScript. Path-based apps can be disabled entirely with --disable-path-apps for further security.",
Flag: "dangerous-allow-path-app-site-owner-access",
Default: false,
},
},
DisablePathApps: &codersdk.DeploymentConfigField[bool]{
Name: "Disable Path Apps",
Usage: "Disable workspace apps that are not served from subdomains. Path-based apps can make requests to the Coder API and pose a security risk when the workspace serves malicious JavaScript. This is recommended for security purposes if a --wildcard-access-url is configured.",
Flag: "disable-path-apps",
Default: false,
},
SessionDuration: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Session Duration",
Usage: "The token expiry duration for browser sessions. Sessions may last longer if they are actively making requests, but this functionality can be disabled via --disable-session-expiry-refresh.",
Flag: "session-duration",
Default: 24 * time.Hour,
},
DisableSessionExpiryRefresh: &codersdk.DeploymentConfigField[bool]{
Name: "Disable Session Expiry Refresh",
Usage: "Disable automatic session expiry bumping due to activity. This forces all sessions to become invalid after the session expiry duration has been reached.",
Flag: "disable-session-expiry-refresh",
Default: false,
},
DisablePasswordAuth: &codersdk.DeploymentConfigField[bool]{
Name: "Disable Password Authentication",
Usage: "Disable password authentication. This is recommended for security purposes in production deployments that rely on an identity provider. Any user with the owner role will be able to sign in with their password regardless of this setting to avoid potential lock out. If you are locked out of your account, you can use the `coder server create-admin` command to create a new admin user directly in the database.",
Flag: "disable-password-auth",
Default: false,
},
Support: &codersdk.SupportConfig{
Links: &codersdk.DeploymentConfigField[[]codersdk.LinkConfig]{
Name: "Support links",
Usage: "Use custom support links",
Flag: "support-links",
Default: []codersdk.LinkConfig{},
Enterprise: true,
},
},
}
}
//nolint:revive
func Config(flagset *pflag.FlagSet, vip *viper.Viper) (*codersdk.DeploymentConfig, error) {
dc := newConfig()
flg, err := flagset.GetString(config.FlagName)
if err != nil {
return nil, xerrors.Errorf("get global config from flag: %w", err)
}
vip.SetEnvPrefix("coder")
if flg != "" {
vip.SetConfigFile(flg + "/server.yaml")
err = vip.ReadInConfig()
if err != nil && !xerrors.Is(err, os.ErrNotExist) {
return dc, xerrors.Errorf("reading deployment config: %w", err)
}
}
setConfig("", vip, &dc)
return dc, nil
}
func setConfig(prefix string, vip *viper.Viper, target interface{}) {
val := reflect.Indirect(reflect.ValueOf(target))
typ := val.Type()
if typ.Kind() != reflect.Struct {
val = val.Elem()
typ = val.Type()
}
// Ensure that we only bind env variables to proper fields,
// otherwise Viper will get confused if the parent struct is
// assigned a value.
if strings.HasPrefix(typ.Name(), "DeploymentConfigField[") {
value := val.FieldByName("Value").Interface()
env, ok := val.FieldByName("EnvOverride").Interface().(string)
if !ok {
panic("DeploymentConfigField[].EnvOverride must be a string")
}
if env == "" {
env = formatEnv(prefix)
}
switch value.(type) {
case string:
vip.MustBindEnv(prefix, env)
val.FieldByName("Value").SetString(vip.GetString(prefix))
case bool:
vip.MustBindEnv(prefix, env)
val.FieldByName("Value").SetBool(vip.GetBool(prefix))
case int:
vip.MustBindEnv(prefix, env)
val.FieldByName("Value").SetInt(int64(vip.GetInt(prefix)))
case time.Duration:
vip.MustBindEnv(prefix, env)
val.FieldByName("Value").SetInt(int64(vip.GetDuration(prefix)))
case []string:
vip.MustBindEnv(prefix, env)
// As of October 21st, 2022 we supported delimiting a string
// with a comma, but Viper only supports with a space. This
// is a small hack around it!
rawSlice := reflect.ValueOf(vip.GetStringSlice(prefix)).Interface()
stringSlice, ok := rawSlice.([]string)
if !ok {
panic(fmt.Sprintf("string slice is of type %T", rawSlice))
}
value := make([]string, 0, len(stringSlice))
for _, entry := range stringSlice {
value = append(value, strings.Split(entry, ",")...)
}
val.FieldByName("Value").Set(reflect.ValueOf(value))
case []codersdk.GitAuthConfig:
// Do not bind to CODER_GITAUTH, instead bind to CODER_GITAUTH_0_*, etc.
values := readSliceFromViper[codersdk.GitAuthConfig](vip, prefix, value)
val.FieldByName("Value").Set(reflect.ValueOf(values))
case []codersdk.LinkConfig:
// Do not bind to CODER_SUPPORT_LINKS, instead bind to CODER_SUPPORT_LINKS_0_*, etc.
values := readSliceFromViper[codersdk.LinkConfig](vip, prefix, value)
val.FieldByName("Value").Set(reflect.ValueOf(values))
default:
panic(fmt.Sprintf("unsupported type %T", value))
}
return
}
for i := 0; i < typ.NumField(); i++ {
fv := val.Field(i)
ft := fv.Type()
tag := typ.Field(i).Tag.Get("json")
var key string
if prefix == "" {
key = tag
} else {
key = fmt.Sprintf("%s.%s", prefix, tag)
}
switch ft.Kind() {
case reflect.Ptr:
setConfig(key, vip, fv.Interface())
case reflect.Slice:
for j := 0; j < fv.Len(); j++ {
key := fmt.Sprintf("%s.%d", key, j)
setConfig(key, vip, fv.Index(j).Interface())
}
default:
panic(fmt.Sprintf("unsupported type %T", ft))
}
}
}
// readSliceFromViper reads a typed mapping from the key provided.
// This enables environment variables like CODER_GITAUTH_<index>_CLIENT_ID.
func readSliceFromViper[T any](vip *viper.Viper, key string, value any) []T {
elementType := reflect.TypeOf(value).Elem()
returnValues := make([]T, 0)
for entry := 0; true; entry++ {
// Only create an instance when the entry exists in viper...
// otherwise we risk
var instance *reflect.Value
for i := 0; i < elementType.NumField(); i++ {
fve := elementType.Field(i)
prop := fve.Tag.Get("json")
// For fields that are omitted in JSON, we use a YAML tag.
if prop == "-" {
prop = fve.Tag.Get("yaml")
}
configKey := fmt.Sprintf("%s.%d.%s", key, entry, prop)
// Ensure the env entry for this key is registered
// before checking value.
//
// We don't support DeploymentConfigField[].EnvOverride for array flags so
// this is fine to just use `formatEnv` here.
vip.MustBindEnv(configKey, formatEnv(configKey))
value := vip.Get(configKey)
if value == nil {
continue
}
if instance == nil {
newType := reflect.Indirect(reflect.New(elementType))
instance = &newType
}
switch v := instance.Field(i).Type().String(); v {
case "[]string":
value = vip.GetStringSlice(configKey)
case "bool":
value = vip.GetBool(configKey)
default:
}
instance.Field(i).Set(reflect.ValueOf(value))
}
if instance == nil {
break
}
value, ok := instance.Interface().(T)
if !ok {
continue
}
returnValues = append(returnValues, value)
}
return returnValues
}
func NewViper() *viper.Viper {
dc := newConfig()
vip := viper.New()
vip.SetEnvPrefix("coder")
vip.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
setViperDefaults("", vip, dc)
return vip
}
func setViperDefaults(prefix string, vip *viper.Viper, target interface{}) {
val := reflect.ValueOf(target).Elem()
val = reflect.Indirect(val)
typ := val.Type()
if strings.HasPrefix(typ.Name(), "DeploymentConfigField[") {
value := val.FieldByName("Default").Interface()
vip.SetDefault(prefix, value)
return
}
for i := 0; i < typ.NumField(); i++ {
fv := val.Field(i)
ft := fv.Type()
tag := typ.Field(i).Tag.Get("json")
var key string
if prefix == "" {
key = tag
} else {
key = fmt.Sprintf("%s.%s", prefix, tag)
}
switch ft.Kind() {
case reflect.Ptr:
setViperDefaults(key, vip, fv.Interface())
case reflect.Slice:
// we currently don't support default values on structured slices
continue
default:
panic(fmt.Sprintf("unsupported type %T", ft))
}
}
}
//nolint:revive
func AttachFlags(flagset *pflag.FlagSet, vip *viper.Viper, enterprise bool) {
setFlags("", flagset, vip, newConfig(), enterprise)
}
//nolint:revive
func setFlags(prefix string, flagset *pflag.FlagSet, vip *viper.Viper, target interface{}, enterprise bool) {
val := reflect.Indirect(reflect.ValueOf(target))
typ := val.Type()
if strings.HasPrefix(typ.Name(), "DeploymentConfigField[") {
isEnt := val.FieldByName("Enterprise").Bool()
if enterprise != isEnt {
return
}
flg := val.FieldByName("Flag").String()
if flg == "" {
return
}
env, ok := val.FieldByName("EnvOverride").Interface().(string)
if !ok {
panic("DeploymentConfigField[].EnvOverride must be a string")
}
if env == "" {
env = formatEnv(prefix)
}
usage := val.FieldByName("Usage").String()
usage = fmt.Sprintf("%s\n%s", usage, cliui.Styles.Placeholder.Render("Consumes $"+env))
shorthand := val.FieldByName("Shorthand").String()
hidden := val.FieldByName("Hidden").Bool()
value := val.FieldByName("Default").Interface()
// Allow currently set environment variables
// to override default values in help output.
vip.MustBindEnv(prefix, env)
switch value.(type) {
case string:
_ = flagset.StringP(flg, shorthand, vip.GetString(prefix), usage)
case bool:
_ = flagset.BoolP(flg, shorthand, vip.GetBool(prefix), usage)
case int:
_ = flagset.IntP(flg, shorthand, vip.GetInt(prefix), usage)
case time.Duration:
_ = flagset.DurationP(flg, shorthand, vip.GetDuration(prefix), usage)
case []string:
_ = flagset.StringSliceP(flg, shorthand, vip.GetStringSlice(prefix), usage)
case []codersdk.LinkConfig:
// Ignore this one!
case []codersdk.GitAuthConfig:
// Ignore this one!
default:
panic(fmt.Sprintf("unsupported type %T", typ))
}
_ = vip.BindPFlag(prefix, flagset.Lookup(flg))
if hidden {
_ = flagset.MarkHidden(flg)
}
return
}
for i := 0; i < typ.NumField(); i++ {
fv := val.Field(i)
ft := fv.Type()
tag := typ.Field(i).Tag.Get("json")
var key string
if prefix == "" {
key = tag
} else {
key = fmt.Sprintf("%s.%s", prefix, tag)
}
switch ft.Kind() {
case reflect.Ptr:
setFlags(key, flagset, vip, fv.Interface(), enterprise)
case reflect.Slice:
for j := 0; j < fv.Len(); j++ {
key := fmt.Sprintf("%s.%d", key, j)
setFlags(key, flagset, vip, fv.Index(j).Interface(), enterprise)
}
default:
panic(fmt.Sprintf("unsupported type %T", ft))
}
}
}
func formatEnv(key string) string {
return "CODER_" + strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(key))
}
func DefaultCacheDir() string {
defaultCacheDir, err := os.UserCacheDir()
if err != nil {
defaultCacheDir = os.TempDir()
}
if dir := os.Getenv("CACHE_DIRECTORY"); dir != "" {
// For compatibility with systemd.
defaultCacheDir = dir
}
return filepath.Join(defaultCacheDir, "coder")
}

View File

@ -1,287 +0,0 @@
package deployment_test
import (
"testing"
"time"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/codersdk"
)
// nolint:paralleltest
func TestConfig(t *testing.T) {
viper := deployment.NewViper()
flagSet := pflag.NewFlagSet("", pflag.ContinueOnError)
flagSet.String(config.FlagName, "", "")
deployment.AttachFlags(flagSet, viper, true)
for _, tc := range []struct {
Name string
Env map[string]string
Valid func(config *codersdk.DeploymentConfig)
}{{
Name: "Deployment",
Env: map[string]string{
"CODER_ADDRESS": "0.0.0.0:8443",
"CODER_ACCESS_URL": "https://dev.coder.com",
"CODER_PG_CONNECTION_URL": "some-url",
"CODER_PPROF_ADDRESS": "something",
"CODER_PPROF_ENABLE": "true",
"CODER_PROMETHEUS_ADDRESS": "hello-world",
"CODER_PROMETHEUS_ENABLE": "true",
"CODER_PROVISIONER_DAEMONS": "5",
"CODER_PROVISIONER_DAEMON_POLL_INTERVAL": "5s",
"CODER_PROVISIONER_DAEMON_POLL_JITTER": "1s",
"CODER_SECURE_AUTH_COOKIE": "true",
"CODER_SSH_KEYGEN_ALGORITHM": "potato",
"CODER_TELEMETRY": "false",
"CODER_TELEMETRY_TRACE": "false",
"CODER_WILDCARD_ACCESS_URL": "something-wildcard.com",
"CODER_UPDATE_CHECK": "false",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Equal(t, config.Address.Value, "0.0.0.0:8443")
require.Equal(t, config.AccessURL.Value, "https://dev.coder.com")
require.Equal(t, config.PostgresURL.Value, "some-url")
require.Equal(t, config.Pprof.Address.Value, "something")
require.Equal(t, config.Pprof.Enable.Value, true)
require.Equal(t, config.Prometheus.Address.Value, "hello-world")
require.Equal(t, config.Prometheus.Enable.Value, true)
require.Equal(t, config.Provisioner.Daemons.Value, 5)
require.Equal(t, config.Provisioner.DaemonPollInterval.Value, 5*time.Second)
require.Equal(t, config.Provisioner.DaemonPollJitter.Value, 1*time.Second)
require.Equal(t, config.SecureAuthCookie.Value, true)
require.Equal(t, config.SSHKeygenAlgorithm.Value, "potato")
require.Equal(t, config.Telemetry.Enable.Value, false)
require.Equal(t, config.Telemetry.Trace.Value, false)
require.Equal(t, config.WildcardAccessURL.Value, "something-wildcard.com")
require.Equal(t, config.UpdateCheck.Value, false)
},
}, {
Name: "DERP",
Env: map[string]string{
"CODER_DERP_CONFIG_PATH": "/example/path",
"CODER_DERP_CONFIG_URL": "https://google.com",
"CODER_DERP_SERVER_ENABLE": "false",
"CODER_DERP_SERVER_REGION_CODE": "something",
"CODER_DERP_SERVER_REGION_ID": "123",
"CODER_DERP_SERVER_REGION_NAME": "Code-Land",
"CODER_DERP_SERVER_RELAY_URL": "1.1.1.1",
"CODER_DERP_SERVER_STUN_ADDRESSES": "google.org",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Equal(t, config.DERP.Config.Path.Value, "/example/path")
require.Equal(t, config.DERP.Config.URL.Value, "https://google.com")
require.Equal(t, config.DERP.Server.Enable.Value, false)
require.Equal(t, config.DERP.Server.RegionCode.Value, "something")
require.Equal(t, config.DERP.Server.RegionID.Value, 123)
require.Equal(t, config.DERP.Server.RegionName.Value, "Code-Land")
require.Equal(t, config.DERP.Server.RelayURL.Value, "1.1.1.1")
require.Equal(t, config.DERP.Server.STUNAddresses.Value, []string{"google.org"})
},
}, {
Name: "Enterprise",
Env: map[string]string{
"CODER_AUDIT_LOGGING": "false",
"CODER_BROWSER_ONLY": "true",
"CODER_SCIM_API_KEY": "some-key",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Equal(t, config.AuditLogging.Value, false)
require.Equal(t, config.BrowserOnly.Value, true)
require.Equal(t, config.SCIMAPIKey.Value, "some-key")
},
}, {
Name: "TLS",
Env: map[string]string{
"CODER_TLS_CERT_FILE": "/etc/acme-sh/dev.coder.com,/etc/acme-sh/*.dev.coder.com",
"CODER_TLS_KEY_FILE": "/etc/acme-sh/dev.coder.com,/etc/acme-sh/*.dev.coder.com",
"CODER_TLS_CLIENT_AUTH": "/some/path",
"CODER_TLS_CLIENT_CA_FILE": "/some/path",
"CODER_TLS_ENABLE": "true",
"CODER_TLS_MIN_VERSION": "tls10",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Len(t, config.TLS.CertFiles.Value, 2)
require.Equal(t, config.TLS.CertFiles.Value[0], "/etc/acme-sh/dev.coder.com")
require.Equal(t, config.TLS.CertFiles.Value[1], "/etc/acme-sh/*.dev.coder.com")
require.Len(t, config.TLS.KeyFiles.Value, 2)
require.Equal(t, config.TLS.KeyFiles.Value[0], "/etc/acme-sh/dev.coder.com")
require.Equal(t, config.TLS.KeyFiles.Value[1], "/etc/acme-sh/*.dev.coder.com")
require.Equal(t, config.TLS.ClientAuth.Value, "/some/path")
require.Equal(t, config.TLS.ClientCAFile.Value, "/some/path")
require.Equal(t, config.TLS.Enable.Value, true)
require.Equal(t, config.TLS.MinVersion.Value, "tls10")
},
}, {
Name: "Trace",
Env: map[string]string{
"CODER_TRACE_ENABLE": "true",
"CODER_TRACE_HONEYCOMB_API_KEY": "my-honeycomb-key",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Equal(t, config.Trace.Enable.Value, true)
require.Equal(t, config.Trace.HoneycombAPIKey.Value, "my-honeycomb-key")
},
}, {
Name: "OIDC_Defaults",
Env: map[string]string{},
Valid: func(config *codersdk.DeploymentConfig) {
require.Empty(t, config.OIDC.IssuerURL.Value)
require.Empty(t, config.OIDC.EmailDomain.Value)
require.Empty(t, config.OIDC.ClientID.Value)
require.Empty(t, config.OIDC.ClientSecret.Value)
require.True(t, config.OIDC.AllowSignups.Value)
require.ElementsMatch(t, config.OIDC.Scopes.Value, []string{"openid", "email", "profile"})
require.False(t, config.OIDC.IgnoreEmailVerified.Value)
},
}, {
Name: "OIDC",
Env: map[string]string{
"CODER_OIDC_ISSUER_URL": "https://accounts.google.com",
"CODER_OIDC_EMAIL_DOMAIN": "coder.com",
"CODER_OIDC_CLIENT_ID": "client",
"CODER_OIDC_CLIENT_SECRET": "secret",
"CODER_OIDC_ALLOW_SIGNUPS": "false",
"CODER_OIDC_SCOPES": "something,here",
"CODER_OIDC_IGNORE_EMAIL_VERIFIED": "true",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Equal(t, config.OIDC.IssuerURL.Value, "https://accounts.google.com")
require.Equal(t, config.OIDC.EmailDomain.Value, []string{"coder.com"})
require.Equal(t, config.OIDC.ClientID.Value, "client")
require.Equal(t, config.OIDC.ClientSecret.Value, "secret")
require.False(t, config.OIDC.AllowSignups.Value)
require.Equal(t, config.OIDC.Scopes.Value, []string{"something", "here"})
require.True(t, config.OIDC.IgnoreEmailVerified.Value)
},
}, {
Name: "GitHub",
Env: map[string]string{
"CODER_OAUTH2_GITHUB_CLIENT_ID": "client",
"CODER_OAUTH2_GITHUB_CLIENT_SECRET": "secret",
"CODER_OAUTH2_GITHUB_ALLOWED_ORGS": "coder",
"CODER_OAUTH2_GITHUB_ALLOWED_TEAMS": "coder",
"CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS": "true",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Equal(t, config.OAuth2.Github.ClientID.Value, "client")
require.Equal(t, config.OAuth2.Github.ClientSecret.Value, "secret")
require.Equal(t, []string{"coder"}, config.OAuth2.Github.AllowedOrgs.Value)
require.Equal(t, []string{"coder"}, config.OAuth2.Github.AllowedTeams.Value)
require.Equal(t, config.OAuth2.Github.AllowSignups.Value, true)
},
}, {
Name: "GitAuth",
Env: map[string]string{
"CODER_GITAUTH_0_ID": "hello",
"CODER_GITAUTH_0_TYPE": "github",
"CODER_GITAUTH_0_CLIENT_ID": "client",
"CODER_GITAUTH_0_CLIENT_SECRET": "secret",
"CODER_GITAUTH_0_AUTH_URL": "https://auth.com",
"CODER_GITAUTH_0_TOKEN_URL": "https://token.com",
"CODER_GITAUTH_0_VALIDATE_URL": "https://validate.com",
"CODER_GITAUTH_0_REGEX": "github.com",
"CODER_GITAUTH_0_SCOPES": "read write",
"CODER_GITAUTH_0_NO_REFRESH": "true",
"CODER_GITAUTH_1_ID": "another",
"CODER_GITAUTH_1_TYPE": "gitlab",
"CODER_GITAUTH_1_CLIENT_ID": "client-2",
"CODER_GITAUTH_1_CLIENT_SECRET": "secret-2",
"CODER_GITAUTH_1_AUTH_URL": "https://auth-2.com",
"CODER_GITAUTH_1_TOKEN_URL": "https://token-2.com",
"CODER_GITAUTH_1_REGEX": "gitlab.com",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Len(t, config.GitAuth.Value, 2)
require.Equal(t, []codersdk.GitAuthConfig{{
ID: "hello",
Type: "github",
ClientID: "client",
ClientSecret: "secret",
AuthURL: "https://auth.com",
TokenURL: "https://token.com",
ValidateURL: "https://validate.com",
Regex: "github.com",
Scopes: []string{"read", "write"},
NoRefresh: true,
}, {
ID: "another",
Type: "gitlab",
ClientID: "client-2",
ClientSecret: "secret-2",
AuthURL: "https://auth-2.com",
TokenURL: "https://token-2.com",
Regex: "gitlab.com",
}}, config.GitAuth.Value)
},
}, {
Name: "Support links",
Env: map[string]string{
"CODER_SUPPORT_LINKS_0_NAME": "First link",
"CODER_SUPPORT_LINKS_0_TARGET": "http://target-link-1",
"CODER_SUPPORT_LINKS_0_ICON": "bug",
"CODER_SUPPORT_LINKS_1_NAME": "Second link",
"CODER_SUPPORT_LINKS_1_TARGET": "http://target-link-2",
"CODER_SUPPORT_LINKS_1_ICON": "chat",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Len(t, config.Support.Links.Value, 2)
require.Equal(t, []codersdk.LinkConfig{{
Name: "First link",
Target: "http://target-link-1",
Icon: "bug",
}, {
Name: "Second link",
Target: "http://target-link-2",
Icon: "chat",
}}, config.Support.Links.Value)
},
}, {
Name: "Wrong env must not break default values",
Env: map[string]string{
"CODER_PROMETHEUS_ENABLE": "true",
"CODER_PROMETHEUS": "true", // Wrong env name, must not break prom addr.
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Equal(t, config.Prometheus.Enable.Value, true)
require.Equal(t, config.Prometheus.Address.Value, config.Prometheus.Address.Default)
},
}, {
Name: "Experiments - no features",
Env: map[string]string{
"CODER_EXPERIMENTS": "",
},
Valid: func(config *codersdk.DeploymentConfig) {
require.Empty(t, config.Experiments.Value)
},
}, {
Name: "Experiments - multiple features",
Env: map[string]string{
"CODER_EXPERIMENTS": "foo,bar",
},
Valid: func(config *codersdk.DeploymentConfig) {
expected := []string{"foo", "bar"}
require.ElementsMatch(t, expected, config.Experiments.Value)
},
}} {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Helper()
for key, value := range tc.Env {
t.Setenv(key, value)
}
config, err := deployment.Config(flagSet, viper)
require.NoError(t, err)
tc.Valid(config)
})
}
}

View File

@ -32,7 +32,6 @@ func TestResetPassword(t *testing.T) {
const newPassword = "MyNewPassword!"
// start postgres and coder server processes
connectionURL, closeFunc, err := postgres.Open()
require.NoError(t, err)
defer closeFunc()

View File

@ -22,7 +22,6 @@ import (
"cdr.dev/slog"
"github.com/charmbracelet/lipgloss"
"github.com/kirsle/configdir"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
@ -30,7 +29,6 @@ import (
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/gitauth"
"github.com/coder/coder/codersdk"
@ -110,7 +108,7 @@ func Core() []*cobra.Command {
}
func AGPL() []*cobra.Command {
all := append(Core(), Server(deployment.NewViper(), func(_ context.Context, o *coderd.Options) (*coderd.API, io.Closer, error) {
all := append(Core(), Server(func(_ context.Context, o *coderd.Options) (*coderd.API, io.Closer, error) {
api := coderd.New(o)
return api, api, nil
}))
@ -160,7 +158,7 @@ func Root(subcommands []*cobra.Command) *cobra.Command {
cmd.AddCommand(subcommands...)
fixUnknownSubcommandError(cmd.Commands())
cmd.SetUsageTemplate(usageTemplate())
cmd.SetUsageTemplate(usageTemplateCobra())
cliflag.String(cmd.PersistentFlags(), varURL, "", envURL, "", "URL to a deployment.")
cliflag.Bool(cmd.PersistentFlags(), varNoVersionCheck, "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.")
@ -170,7 +168,7 @@ func Root(subcommands []*cobra.Command) *cobra.Command {
_ = cmd.PersistentFlags().MarkHidden(varAgentToken)
cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "URL for an agent to access your deployment.")
_ = cmd.PersistentFlags().MarkHidden(varAgentURL)
cliflag.String(cmd.PersistentFlags(), config.FlagName, "", "CODER_CONFIG_DIR", configdir.LocalConfig("coderv2"), "Path to the global `coder` config directory.")
cliflag.String(cmd.PersistentFlags(), config.FlagName, "", "CODER_CONFIG_DIR", config.DefaultDir(), "Path to the global `coder` config directory.")
cliflag.StringArray(cmd.PersistentFlags(), varHeader, "", "CODER_HEADER", []string{}, "HTTP headers added to all requests. Provide as \"Key=Value\"")
cmd.PersistentFlags().Bool(varForceTty, false, "Force the `coder` command to run as if connected to a TTY.")
_ = cmd.PersistentFlags().MarkHidden(varForceTty)
@ -481,7 +479,10 @@ func isWorkspaceCommand(cmd *cobra.Command) bool {
return ws
}
func usageTemplate() string {
// We will eventually replace this with the clibase template describedc
// in usage.go. We don't want to continue working around
// Cobra's feature-set.
func usageTemplateCobra() string {
// usageHeader is defined in init().
return `{{usageHeader "Usage:"}}
{{- if .Runnable}}

View File

@ -53,13 +53,14 @@ func TestCommandHelp(t *testing.T) {
name: "coder --help",
cmd: []string{"--help"},
},
{
name: "coder server --help",
cmd: []string{"server", "--help"},
env: map[string]string{
"CODER_CACHE_DIRECTORY": "~/.cache/coder",
},
},
// Re-enable after clibase migrations.
// {
// name: "coder server --help",
// cmd: []string{"server", "--help"},
// env: map[string]string{
// "CODER_CACHE_DIRECTORY": "~/.cache/coder",
// },
// },
{
name: "coder agent --help",
cmd: []string{"agent", "--help"},
@ -177,6 +178,10 @@ func extractVisibleCommandPaths(cmdPath []string, cmds []*cobra.Command) [][]str
if c.Hidden {
continue
}
// TODO: re-enable after clibase migration.
if c.Name() == "server" {
continue
}
cmdPath := append(cmdPath, c.Name())
cmdPaths = append(cmdPaths, cmdPath)
cmdPaths = append(cmdPaths, extractVisibleCommandPaths(cmdPath, c.Commands())...)

View File

@ -12,6 +12,7 @@ import (
"database/sql"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"log"
@ -25,6 +26,7 @@ import (
"os/user"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
@ -40,7 +42,6 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.opentelemetry.io/otel/trace"
"golang.org/x/mod/semver"
"golang.org/x/oauth2"
@ -49,6 +50,7 @@ import (
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
"gopkg.in/yaml.v3"
"tailscale.com/tailcfg"
"cdr.dev/slog"
@ -56,9 +58,9 @@ import (
"cdr.dev/slog/sloggers/slogjson"
"cdr.dev/slog/sloggers/slogstackdriver"
"github.com/coder/coder/buildinfo"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/autobuild/executor"
"github.com/coder/coder/coderd/database"
@ -84,48 +86,221 @@ import (
"github.com/coder/coder/tailnet"
)
// ReadGitAuthProvidersFromEnv is provided for compatibility purposes with the
// viper CLI.
// DEPRECATED
func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.GitAuthConfig, error) {
// The index numbers must be in-order.
sort.Strings(environ)
var providers []codersdk.GitAuthConfig
for _, v := range clibase.EnvsWithPrefix(environ, envPrefix+"GITAUTH_") {
tokens := strings.SplitN(v.Name, "_", 2)
if len(tokens) != 2 {
return nil, xerrors.Errorf("invalid env var: %s", v.Name)
}
providerNum, err := strconv.Atoi(tokens[0])
if err != nil {
return nil, xerrors.Errorf("parse number: %s", v.Name)
}
var provider codersdk.GitAuthConfig
switch {
case len(providers) < providerNum:
return nil, xerrors.Errorf(
"provider num %v skipped: %s",
len(providers),
v.Name,
)
case len(providers) == providerNum:
// At the next next provider.
providers = append(providers, provider)
case len(providers) == providerNum+1:
// At the current provider.
provider = providers[providerNum]
}
key := tokens[1]
switch key {
case "ID":
provider.ID = v.Value
case "TYPE":
provider.Type = v.Value
case "CLIENT_ID":
provider.ClientID = v.Value
case "CLIENT_SECRET":
provider.ClientSecret = v.Value
case "AUTH_URL":
provider.AuthURL = v.Value
case "TOKEN_URL":
provider.TokenURL = v.Value
case "VALIDATE_URL":
provider.ValidateURL = v.Value
case "REGEX":
provider.Regex = v.Value
case "NO_REFRESH":
b, err := strconv.ParseBool(key)
if err != nil {
return nil, xerrors.Errorf("parse bool: %s", v.Value)
}
provider.NoRefresh = b
case "SCOPES":
provider.Scopes = strings.Split(v.Value, " ")
}
providers[providerNum] = provider
}
return providers, nil
}
// nolint:gocyclo
func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *cobra.Command {
func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *cobra.Command {
root := &cobra.Command{
Use: "server",
Short: "Start a Coder server",
Use: "server",
Short: "Start a Coder server",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
// Main command context for managing cancellation of running
// services.
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
cfg := &codersdk.DeploymentValues{}
cliOpts := cfg.Options()
var configDir clibase.String
// This is a hack to get around the fact that the Cobra-defined
// flags are not available.
cliOpts.Add(clibase.Option{
Name: "Global Config",
Flag: config.FlagName,
Description: "Global Config is ignored in server mode.",
Hidden: true,
Default: config.DefaultDir(),
Value: &configDir,
})
err := cliOpts.SetDefaults()
if err != nil {
return xerrors.Errorf("set defaults: %w", err)
}
err = cliOpts.ParseEnv(envPrefix, os.Environ())
if err != nil {
return xerrors.Errorf("parse env: %w", err)
}
flagSet := cliOpts.FlagSet()
// These parents and children will be moved once we convert the
// rest of the `cli` package to clibase.
flagSet.Usage = usageFn(cmd.ErrOrStderr(), &clibase.Cmd{
Parent: &clibase.Cmd{
Use: "coder",
},
Children: []*clibase.Cmd{
{
Use: "postgres-builtin-url",
Short: "Output the connection URL for the built-in PostgreSQL deployment.",
},
{
Use: "postgres-builtin-serve",
Short: "Run the built-in PostgreSQL deployment.",
},
},
Use: "server [flags]",
Short: "Start a Coder server",
Long: `
The server provides the Coder dashboard, API, and provisioners.
If no options are provided, the server will start with a built-in postgres
and an access URL provided by Coder's cloud service.
Use the following command to print the built-in postgres URL:
$ coder server postgres-builtin-url
Use the following command to manually run the built-in postgres:
$ coder server postgres-builtin-serve
Options may be provided via environment variables prefixed with "CODER_",
flags, and YAML configuration. The precedence is as follows:
1. Defaults
2. YAML configuration
3. Environment variables
4. Flags
`,
Options: cliOpts,
})
err = flagSet.Parse(args)
if err != nil {
return xerrors.Errorf("parse flags: %w", err)
}
if cfg.WriteConfig {
// TODO: this should output to a file.
n, err := cliOpts.ToYAML()
if err != nil {
return xerrors.Errorf("generate yaml: %w", err)
}
enc := yaml.NewEncoder(cmd.ErrOrStderr())
err = enc.Encode(n)
if err != nil {
return xerrors.Errorf("encode yaml: %w", err)
}
err = enc.Close()
if err != nil {
return xerrors.Errorf("close yaml encoder: %w", err)
}
return nil
}
// Print deprecation warnings.
for _, opt := range cliOpts {
if opt.UseInstead == nil {
continue
}
warnStr := opt.Name + " is deprecated, please use "
for i, use := range opt.UseInstead {
warnStr += use.Name + " "
if i != len(opt.UseInstead)-1 {
warnStr += "and "
}
}
warnStr += "instead.\n"
cmd.PrintErr(
cliui.Styles.Warn.Render("WARN: ") + warnStr,
)
}
go dumpHandler(ctx)
cfg, err := deployment.Config(cmd.Flags(), vip)
if err != nil {
return xerrors.Errorf("getting deployment config: %w", err)
}
// Validate bind addresses.
if cfg.Address.Value != "" {
cmd.PrintErr(cliui.Styles.Warn.Render("WARN:") + " --address and -a are deprecated, please use --http-address and --tls-address instead")
if cfg.TLS.Enable.Value {
cfg.HTTPAddress.Value = ""
cfg.TLS.Address.Value = cfg.Address.Value
if cfg.Address.String() != "" {
if cfg.TLS.Enable {
cfg.HTTPAddress = ""
cfg.TLS.Address = cfg.Address
} else {
cfg.HTTPAddress.Value = cfg.Address.Value
cfg.TLS.Address.Value = ""
_ = cfg.HTTPAddress.Set(cfg.Address.String())
cfg.TLS.Address.Host = ""
cfg.TLS.Address.Port = ""
}
}
if cfg.TLS.Enable.Value && cfg.TLS.Address.Value == "" {
if cfg.TLS.Enable && cfg.TLS.Address.String() == "" {
return xerrors.Errorf("TLS address must be set if TLS is enabled")
}
if !cfg.TLS.Enable.Value && cfg.HTTPAddress.Value == "" {
if !cfg.TLS.Enable && cfg.HTTPAddress.String() == "" {
return xerrors.Errorf("TLS is disabled. Enable with --tls-enable or specify a HTTP address")
}
if cfg.AccessURL.String() != "" && cfg.AccessURL.Scheme == "" {
return xerrors.Errorf("access-url must include a scheme (e.g. 'http://' or 'https://)")
}
// Disable rate limits if the `--dangerous-disable-rate-limits` flag
// was specified.
loginRateLimit := 60
filesRateLimit := 12
if cfg.RateLimit.DisableAll.Value {
cfg.RateLimit.API.Value = -1
if cfg.RateLimit.DisableAll {
cfg.RateLimit.API = -1
loginRateLimit = -1
filesRateLimit = -1
}
@ -137,6 +312,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
defer logCloser()
// This line is helpful in tests.
logger.Debug(ctx, "started debug logging")
logger.Sync()
// Register signals early on so that graceful shutdown can't
// be interrupted by additional signals. Note that we avoid
// shadowing cancel() (from above) here because notifyStop()
@ -151,7 +330,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
defer notifyStop()
// Ensure we have a unique cache directory for this process.
cacheDir := filepath.Join(cfg.CacheDirectory.Value, uuid.NewString())
cacheDir := filepath.Join(cfg.CacheDir.String(), uuid.NewString())
err = os.MkdirAll(cacheDir, 0o700)
if err != nil {
return xerrors.Errorf("create cache directory: %w", err)
@ -170,18 +349,18 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// Coder tracing should be disabled if telemetry is disabled unless
// --telemetry-trace was explicitly provided.
shouldCoderTrace := cfg.Telemetry.Enable.Value && !isTest()
shouldCoderTrace := cfg.Telemetry.Enable.Value() && !isTest()
// Only override if telemetryTraceEnable was specifically set.
// By default we want it to be controlled by telemetryEnable.
if cmd.Flags().Changed("telemetry-trace") {
shouldCoderTrace = cfg.Telemetry.Trace.Value
shouldCoderTrace = cfg.Telemetry.Trace.Value()
}
if cfg.Trace.Enable.Value || shouldCoderTrace || cfg.Trace.HoneycombAPIKey.Value != "" {
if cfg.Trace.Enable.Value() || shouldCoderTrace || cfg.Trace.HoneycombAPIKey != "" {
sdkTracerProvider, closeTracing, err := tracing.TracerProvider(ctx, "coderd", tracing.TracerOpts{
Default: cfg.Trace.Enable.Value,
Default: cfg.Trace.Enable.Value(),
Coder: shouldCoderTrace,
Honeycomb: cfg.Trace.HoneycombAPIKey.Value,
Honeycomb: cfg.Trace.HoneycombAPIKey.String(),
})
if err != nil {
logger.Warn(ctx, "start telemetry exporter", slog.Error(err))
@ -202,13 +381,18 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
}
config := createConfig(cmd)
config := config.Root(configDir)
builtinPostgres := false
// Only use built-in if PostgreSQL URL isn't specified!
if !cfg.InMemoryDatabase.Value && cfg.PostgresURL.Value == "" {
if !cfg.InMemoryDatabase && cfg.PostgresURL == "" {
var closeFunc func() error
cmd.Printf("Using built-in PostgreSQL (%s)\n", config.PostgresPath())
cfg.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, config, logger)
pgURL, closeFunc, err := startBuiltinPostgres(ctx, config, logger)
if err != nil {
return err
}
err = cfg.PostgresURL.Set(pgURL)
if err != nil {
return err
}
@ -228,10 +412,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
httpListener net.Listener
httpURL *url.URL
)
if cfg.HTTPAddress.Value != "" {
httpListener, err = net.Listen("tcp", cfg.HTTPAddress.Value)
if cfg.HTTPAddress.String() != "" {
httpListener, err = net.Listen("tcp", cfg.HTTPAddress.String())
if err != nil {
return xerrors.Errorf("listen %q: %w", cfg.HTTPAddress.Value, err)
return xerrors.Errorf("listen %q: %w", cfg.HTTPAddress.String(), err)
}
defer httpListener.Close()
@ -240,7 +424,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// httpListener.Addr().String() likes to return it as an ipv6
// address (i.e. [::]:x). If the input ip is 0.0.0.0, try to
// coerce the output back to ipv4 to make it less confusing.
if strings.Contains(cfg.HTTPAddress.Value, "0.0.0.0") {
if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") {
listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0")
}
@ -267,8 +451,8 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
httpsListener net.Listener
httpsURL *url.URL
)
if cfg.TLS.Enable.Value {
if cfg.TLS.Address.Value == "" {
if cfg.TLS.Enable {
if cfg.TLS.Address.String() == "" {
return xerrors.New("tls address must be set if tls is enabled")
}
@ -276,22 +460,22 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// It made more sense to have the redirect be opt-in.
if os.Getenv("CODER_TLS_REDIRECT_HTTP") == "true" || cmd.Flags().Changed("tls-redirect-http-to-https") {
cmd.PrintErr(cliui.Styles.Warn.Render("WARN:") + " --tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead\n")
cfg.RedirectToAccessURL.Value = cfg.TLS.RedirectHTTP.Value
cfg.RedirectToAccessURL = cfg.TLS.RedirectHTTP
}
tlsConfig, err = configureTLS(
cfg.TLS.MinVersion.Value,
cfg.TLS.ClientAuth.Value,
cfg.TLS.CertFiles.Value,
cfg.TLS.KeyFiles.Value,
cfg.TLS.ClientCAFile.Value,
cfg.TLS.MinVersion.String(),
cfg.TLS.ClientAuth.String(),
cfg.TLS.CertFiles,
cfg.TLS.KeyFiles,
cfg.TLS.ClientCAFile.String(),
)
if err != nil {
return xerrors.Errorf("configure tls: %w", err)
}
httpsListenerInner, err := net.Listen("tcp", cfg.TLS.Address.Value)
httpsListenerInner, err := net.Listen("tcp", cfg.TLS.Address.String())
if err != nil {
return xerrors.Errorf("listen %q: %w", cfg.TLS.Address.Value, err)
return xerrors.Errorf("listen %q: %w", cfg.TLS.Address.String(), err)
}
defer httpsListenerInner.Close()
@ -304,7 +488,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// an ipv6 address (i.e. [::]:x). If the input ip is 0.0.0.0,
// try to coerce the output back to ipv4 to make it less
// confusing.
if strings.Contains(cfg.HTTPAddress.Value, "0.0.0.0") {
if strings.Contains(cfg.HTTPAddress.String(), "0.0.0.0") {
listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0")
}
@ -340,9 +524,9 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
ctx, httpClient, err := configureHTTPClient(
ctx,
cfg.TLS.ClientCertFile.Value,
cfg.TLS.ClientKeyFile.Value,
cfg.TLS.ClientCAFile.Value,
cfg.TLS.ClientCertFile.String(),
cfg.TLS.ClientKeyFile.String(),
cfg.TLS.ClientCAFile.String(),
)
if err != nil {
return xerrors.Errorf("configure http client: %w", err)
@ -357,53 +541,60 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// If the access URL is empty, we attempt to run a reverse-proxy
// tunnel to make the initial setup really simple.
if cfg.AccessURL.Value == "" {
if cfg.AccessURL.String() == "" {
cmd.Printf("Opening tunnel so workspaces can connect to your deployment. For production scenarios, specify an external access URL\n")
tunnel, tunnelErr, err = devtunnel.New(ctxTunnel, logger.Named("devtunnel"))
if err != nil {
return xerrors.Errorf("create tunnel: %w", err)
}
cfg.AccessURL.Value = tunnel.URL
err = cfg.AccessURL.Set(tunnel.URL)
if err != nil {
return xerrors.Errorf("set access url: %w", err)
}
if cfg.WildcardAccessURL.Value == "" {
if cfg.WildcardAccessURL.String() == "" {
u, err := parseURL(tunnel.URL)
if err != nil {
return xerrors.Errorf("parse tunnel url: %w", err)
}
// Suffixed wildcard access URL.
cfg.WildcardAccessURL.Value = fmt.Sprintf("*--%s", u.Hostname())
u, err = url.Parse(fmt.Sprintf("*--%s", u.Hostname()))
if err != nil {
return xerrors.Errorf("parse wildcard url: %w", err)
}
cfg.WildcardAccessURL = clibase.URL(*u)
}
}
accessURLParsed, err := parseURL(cfg.AccessURL.Value)
if err != nil {
return xerrors.Errorf("parse URL: %w", err)
}
accessURLPortRaw := accessURLParsed.Port()
_, accessURLPortRaw, _ := net.SplitHostPort(cfg.AccessURL.Host)
if accessURLPortRaw == "" {
accessURLPortRaw = "80"
if accessURLParsed.Scheme == "https" {
if cfg.AccessURL.Scheme == "https" {
accessURLPortRaw = "443"
}
}
accessURLPort, err := strconv.Atoi(accessURLPortRaw)
if err != nil {
return xerrors.Errorf("parse access URL port: %w", err)
}
// Warn the user if the access URL appears to be a loopback address.
isLocal, err := isLocalURL(ctx, accessURLParsed)
isLocal, err := isLocalURL(ctx, cfg.AccessURL.Value())
if isLocal || err != nil {
reason := "could not be resolved"
if isLocal {
reason = "isn't externally reachable"
}
cmd.Printf("%s The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n", cliui.Styles.Warn.Render("Warning:"), cliui.Styles.Field.Render(accessURLParsed.String()), reason)
cmd.Printf(
"%s The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n",
cliui.Styles.Warn.Render("Warning:"), cliui.Styles.Field.Render(cfg.AccessURL.String()), reason,
)
}
// A newline is added before for visibility in terminal output.
cmd.Printf("\nView the Web UI: %s\n", accessURLParsed.String())
cmd.Printf("\nView the Web UI: %s\n", cfg.AccessURL.String())
// Used for zero-trust instance identity with Google Cloud.
googleTokenValidator, err := idtoken.NewValidator(ctx, option.WithoutAuthentication())
@ -411,34 +602,37 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return err
}
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(cfg.SSHKeygenAlgorithm.Value)
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(cfg.SSHKeygenAlgorithm.String())
if err != nil {
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", cfg.SSHKeygenAlgorithm.Value, err)
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", cfg.SSHKeygenAlgorithm, err)
}
defaultRegion := &tailcfg.DERPRegion{
EmbeddedRelay: true,
RegionID: cfg.DERP.Server.RegionID.Value,
RegionCode: cfg.DERP.Server.RegionCode.Value,
RegionName: cfg.DERP.Server.RegionName.Value,
RegionID: int(cfg.DERP.Server.RegionID.Value()),
RegionCode: cfg.DERP.Server.RegionCode.String(),
RegionName: cfg.DERP.Server.RegionName.String(),
Nodes: []*tailcfg.DERPNode{{
Name: fmt.Sprintf("%db", cfg.DERP.Server.RegionID.Value),
RegionID: cfg.DERP.Server.RegionID.Value,
HostName: accessURLParsed.Hostname(),
Name: fmt.Sprintf("%db", cfg.DERP.Server.RegionID),
RegionID: int(cfg.DERP.Server.RegionID.Value()),
HostName: cfg.AccessURL.Host,
DERPPort: accessURLPort,
STUNPort: -1,
ForceHTTP: accessURLParsed.Scheme == "http",
ForceHTTP: cfg.AccessURL.Scheme == "http",
}},
}
if !cfg.DERP.Server.Enable.Value {
if !cfg.DERP.Server.Enable {
defaultRegion = nil
}
derpMap, err := tailnet.NewDERPMap(ctx, defaultRegion, cfg.DERP.Server.STUNAddresses.Value, cfg.DERP.Config.URL.Value, cfg.DERP.Config.Path.Value)
derpMap, err := tailnet.NewDERPMap(
ctx, defaultRegion, cfg.DERP.Server.STUNAddresses,
cfg.DERP.Config.URL.String(), cfg.DERP.Config.Path.String(),
)
if err != nil {
return xerrors.Errorf("create derp map: %w", err)
}
appHostname := strings.TrimSpace(cfg.WildcardAccessURL.Value)
appHostname := cfg.WildcardAccessURL.String()
var appHostnameRegex *regexp.Regexp
if appHostname != "" {
appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname)
@ -447,18 +641,32 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
}
gitAuthConfigs, err := gitauth.ConvertConfig(cfg.GitAuth.Value, accessURLParsed)
gitAuthEnv, err := ReadGitAuthProvidersFromEnv(os.Environ())
if err != nil {
return xerrors.Errorf("parse git auth config: %w", err)
return xerrors.Errorf("read git auth providers from env: %w", err)
}
realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders.Value, cfg.ProxyTrustedOrigins.Value)
gitAuthConfigs, err := gitauth.ConvertConfig(
append(cfg.GitAuthProviders.Value, gitAuthEnv...),
cfg.AccessURL.Value(),
)
if err != nil {
return xerrors.Errorf("convert git auth config: %w", err)
}
for _, c := range gitAuthConfigs {
logger.Debug(
ctx, "loaded git auth config",
slog.F("id", c.ID),
)
}
realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders, cfg.ProxyTrustedOrigins)
if err != nil {
return xerrors.Errorf("parse real ip config: %w", err)
}
options := &coderd.Options{
AccessURL: accessURLParsed,
AccessURL: cfg.AccessURL.Value(),
AppHostname: appHostname,
AppHostnameRegex: appHostnameRegex,
Logger: logger.Named("coderd"),
@ -469,15 +677,15 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
GoogleTokenValidator: googleTokenValidator,
GitAuthConfigs: gitAuthConfigs,
RealIPConfig: realIPConfig,
SecureAuthCookie: cfg.SecureAuthCookie.Value,
SecureAuthCookie: cfg.SecureAuthCookie.Value(),
SSHKeygenAlgorithm: sshKeygenAlgorithm,
TracerProvider: tracerProvider,
Telemetry: telemetry.NewNoop(),
MetricsCacheRefreshInterval: cfg.MetricsCacheRefreshInterval.Value,
AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value,
DeploymentConfig: cfg,
MetricsCacheRefreshInterval: cfg.MetricsCacheRefreshInterval.Value(),
AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value(),
DeploymentValues: cfg,
PrometheusRegistry: prometheus.NewRegistry(),
APIRateLimit: cfg.RateLimit.API.Value,
APIRateLimit: int(cfg.RateLimit.API.Value()),
LoginRateLimit: loginRateLimit,
FilesRateLimit: filesRateLimit,
HTTPClient: httpClient,
@ -486,14 +694,16 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
options.TLSCertificates = tlsConfig.Certificates
}
if cfg.StrictTransportSecurity.Value > 0 {
options.StrictTransportSecurityCfg, err = httpmw.HSTSConfigOptions(cfg.StrictTransportSecurity.Value, cfg.StrictTransportSecurityOptions.Value)
if cfg.StrictTransportSecurity > 0 {
options.StrictTransportSecurityCfg, err = httpmw.HSTSConfigOptions(
int(cfg.StrictTransportSecurity.Value()), cfg.StrictTransportSecurityOptions,
)
if err != nil {
return xerrors.Errorf("coderd: setting hsts header failed (options: %v): %w", cfg.StrictTransportSecurityOptions.Value, err)
return xerrors.Errorf("coderd: setting hsts header failed (options: %v): %w", cfg.StrictTransportSecurityOptions, err)
}
}
if cfg.UpdateCheck.Value {
if cfg.UpdateCheck {
options.UpdateCheckOptions = &updatecheck.Options{
// Avoid spamming GitHub API checking for updates.
Interval: 24 * time.Hour,
@ -512,67 +722,69 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
}
if cfg.OAuth2.Github.ClientSecret.Value != "" {
options.GithubOAuth2Config, err = configureGithubOAuth2(accessURLParsed,
cfg.OAuth2.Github.ClientID.Value,
cfg.OAuth2.Github.ClientSecret.Value,
cfg.OAuth2.Github.AllowSignups.Value,
cfg.OAuth2.Github.AllowEveryone.Value,
cfg.OAuth2.Github.AllowedOrgs.Value,
cfg.OAuth2.Github.AllowedTeams.Value,
cfg.OAuth2.Github.EnterpriseBaseURL.Value,
if cfg.OAuth2.Github.ClientSecret != "" {
options.GithubOAuth2Config, err = configureGithubOAuth2(cfg.AccessURL.Value(),
cfg.OAuth2.Github.ClientID.String(),
cfg.OAuth2.Github.ClientSecret.String(),
cfg.OAuth2.Github.AllowSignups.Value(),
cfg.OAuth2.Github.AllowEveryone.Value(),
cfg.OAuth2.Github.AllowedOrgs,
cfg.OAuth2.Github.AllowedTeams,
cfg.OAuth2.Github.EnterpriseBaseURL.String(),
)
if err != nil {
return xerrors.Errorf("configure github oauth2: %w", err)
}
}
if cfg.OIDC.ClientSecret.Value != "" {
if cfg.OIDC.ClientID.Value == "" {
if cfg.OIDC.ClientSecret != "" {
if cfg.OIDC.ClientID == "" {
return xerrors.Errorf("OIDC client ID be set!")
}
if cfg.OIDC.IssuerURL.Value == "" {
if cfg.OIDC.IssuerURL == "" {
return xerrors.Errorf("OIDC issuer URL must be set!")
}
if cfg.OIDC.IgnoreEmailVerified.Value {
if cfg.OIDC.IgnoreEmailVerified {
logger.Warn(ctx, "coder will not check email_verified for OIDC logins")
}
oidcProvider, err := oidc.NewProvider(ctx, cfg.OIDC.IssuerURL.Value)
oidcProvider, err := oidc.NewProvider(
ctx, cfg.OIDC.IssuerURL.String(),
)
if err != nil {
return xerrors.Errorf("configure oidc provider: %w", err)
}
redirectURL, err := accessURLParsed.Parse("/api/v2/users/oidc/callback")
redirectURL, err := cfg.AccessURL.Value().Parse("/api/v2/users/oidc/callback")
if err != nil {
return xerrors.Errorf("parse oidc oauth callback url: %w", err)
}
options.OIDCConfig = &coderd.OIDCConfig{
OAuth2Config: &oauth2.Config{
ClientID: cfg.OIDC.ClientID.Value,
ClientSecret: cfg.OIDC.ClientSecret.Value,
ClientID: cfg.OIDC.ClientID.String(),
ClientSecret: cfg.OIDC.ClientSecret.String(),
RedirectURL: redirectURL.String(),
Endpoint: oidcProvider.Endpoint(),
Scopes: cfg.OIDC.Scopes.Value,
Scopes: cfg.OIDC.Scopes,
},
Provider: oidcProvider,
Verifier: oidcProvider.Verifier(&oidc.Config{
ClientID: cfg.OIDC.ClientID.Value,
ClientID: cfg.OIDC.ClientID.String(),
}),
EmailDomain: cfg.OIDC.EmailDomain.Value,
AllowSignups: cfg.OIDC.AllowSignups.Value,
UsernameField: cfg.OIDC.UsernameField.Value,
SignInText: cfg.OIDC.SignInText.Value,
IconURL: cfg.OIDC.IconURL.Value,
IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value,
EmailDomain: cfg.OIDC.EmailDomain,
AllowSignups: cfg.OIDC.AllowSignups.Value(),
UsernameField: cfg.OIDC.UsernameField.String(),
SignInText: cfg.OIDC.SignInText.String(),
IconURL: cfg.OIDC.IconURL.String(),
IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value(),
}
}
if cfg.InMemoryDatabase.Value {
if cfg.InMemoryDatabase {
options.Database = dbfake.New()
options.Pubsub = database.NewPubsubInMemory()
} else {
sqlDB, err := connectToPostgres(ctx, logger, sqlDriver, cfg.PostgresURL.Value)
sqlDB, err := connectToPostgres(ctx, logger, sqlDriver, cfg.PostgresURL.String())
if err != nil {
return xerrors.Errorf("connect to postgres: %w", err)
}
@ -581,7 +793,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}()
options.Database = database.New(sqlDB)
options.Pubsub, err = database.NewPubsub(ctx, sqlDB, cfg.PostgresURL.Value)
options.Pubsub, err = database.NewPubsub(ctx, sqlDB, cfg.PostgresURL.String())
if err != nil {
return xerrors.Errorf("create pubsub: %w", err)
}
@ -646,21 +858,13 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return err
}
// Disable telemetry if the in-memory database is used unless explicitly defined!
if cfg.InMemoryDatabase.Value && !cmd.Flags().Changed(cfg.Telemetry.Enable.Flag) {
cfg.Telemetry.Enable.Value = false
}
if cfg.Telemetry.Enable.Value {
// Parse the raw telemetry URL!
telemetryURL, err := parseURL(cfg.Telemetry.URL.Value)
if err != nil {
return xerrors.Errorf("parse telemetry url: %w", err)
}
if cfg.Telemetry.Enable {
gitAuth := make([]telemetry.GitAuth, 0)
// TODO:
var gitAuthConfigs []codersdk.GitAuthConfig
for _, cfg := range gitAuthConfigs {
gitAuth = append(gitAuth, telemetry.GitAuth{
Type: string(cfg.Type),
Type: cfg.Type,
})
}
@ -669,15 +873,15 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
DeploymentID: deploymentID,
Database: options.Database,
Logger: logger.Named("telemetry"),
URL: telemetryURL,
Wildcard: cfg.WildcardAccessURL.Value != "",
DERPServerRelayURL: cfg.DERP.Server.RelayURL.Value,
URL: cfg.Telemetry.URL.Value(),
Wildcard: cfg.WildcardAccessURL.String() != "",
DERPServerRelayURL: cfg.DERP.Server.RelayURL.String(),
GitAuth: gitAuth,
GitHubOAuth: cfg.OAuth2.Github.ClientID.Value != "",
OIDCAuth: cfg.OIDC.ClientID.Value != "",
OIDCIssuerURL: cfg.OIDC.IssuerURL.Value,
Prometheus: cfg.Prometheus.Enable.Value,
STUN: len(cfg.DERP.Server.STUNAddresses.Value) != 0,
GitHubOAuth: cfg.OAuth2.Github.ClientID != "",
OIDCAuth: cfg.OIDC.ClientID != "",
OIDCIssuerURL: cfg.OIDC.IssuerURL.String(),
Prometheus: cfg.Prometheus.Enable.Value(),
STUN: len(cfg.DERP.Server.STUNAddresses) != 0,
Tunnel: tunnel != nil,
})
if err != nil {
@ -688,11 +892,11 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// This prevents the pprof import from being accidentally deleted.
_ = pprof.Handler
if cfg.Pprof.Enable.Value {
if cfg.Pprof.Enable {
//nolint:revive
defer serveHandler(ctx, logger, nil, cfg.Pprof.Address.Value, "pprof")()
defer serveHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")()
}
if cfg.Prometheus.Enable.Value {
if cfg.Prometheus.Enable {
options.PrometheusRegistry.MustRegister(collectors.NewGoCollector())
options.PrometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
@ -711,11 +915,11 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
//nolint:revive
defer serveHandler(ctx, logger, promhttp.InstrumentMetricHandler(
options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}),
), cfg.Prometheus.Address.Value, "prometheus")()
), cfg.Prometheus.Address.String(), "prometheus")()
}
if cfg.Swagger.Enable.Value {
options.SwaggerEndpoint = cfg.Swagger.Enable.Value
if cfg.Swagger.Enable {
options.SwaggerEndpoint = cfg.Swagger.Enable.Value()
}
// We use a separate coderAPICloser so the Enterprise API
@ -742,7 +946,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config.URL().Write(client.URL.String())
err = config.URL().Write(client.URL.String())
if err != nil && flag.Lookup("test.v") != nil {
return xerrors.Errorf("write config url: %w", err)
}
// Since errCh only has one buffered slot, all routines
// sending on it must be wrapped in a select/default to
@ -759,7 +966,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
}()
provisionerdMetrics := provisionerd.NewMetrics(options.PrometheusRegistry)
for i := 0; i < cfg.Provisioner.Daemons.Value; i++ {
for i := int64(0); i < cfg.Provisioner.Daemons.Value(); i++ {
daemonCacheDir := filepath.Join(cacheDir, fmt.Sprintf("provisioner-%d", i))
daemon, err := newProvisionerDaemon(ctx, coderAPI, provisionerdMetrics, logger, cfg, daemonCacheDir, errCh, false)
if err != nil {
@ -774,8 +981,8 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// Wrap the server in middleware that redirects to the access URL if
// the request is not to a local IP.
var handler http.Handler = coderAPI.RootHandler
if cfg.RedirectToAccessURL.Value {
handler = redirectToAccessURL(handler, accessURLParsed, tunnel != nil, appHostnameRegex)
if cfg.RedirectToAccessURL {
handler = redirectToAccessURL(handler, cfg.AccessURL.Value(), tunnel != nil, appHostnameRegex)
}
// ReadHeaderTimeout is purposefully not enabled. It caused some
@ -842,7 +1049,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
cmd.Println("\nFailed to check for the first user: " + err.Error())
} else if !hasFirstUser {
cmd.Println("\nGet started by creating the first user (in a new terminal):")
cmd.Println(cliui.Styles.Code.Render("coder login " + accessURLParsed.String()))
cmd.Println(cliui.Styles.Code.Render("coder login " + cfg.AccessURL.String()))
}
cmd.Println("\n==> Logs will stream in below (press ctrl+c to gracefully exit):")
@ -853,7 +1060,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return xerrors.Errorf("notify systemd: %w", err)
}
autobuildPoller := time.NewTicker(cfg.AutobuildPollInterval.Value)
autobuildPoller := time.NewTicker(cfg.AutobuildPollInterval.Value())
defer autobuildPoller.Stop()
autobuildExecutor := executor.New(ctx, options.Database, logger, autobuildPoller.C)
autobuildExecutor.Run()
@ -1011,10 +1218,11 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
postgresBuiltinServeCmd.Flags().BoolVar(&pgRawURL, "raw-url", false, "Output the raw connection URL instead of a psql command.")
createAdminUserCommand := newCreateAdminUserCommand()
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
// Help is handled by clibase in command body.
})
root.AddCommand(postgresBuiltinURLCmd, postgresBuiltinServeCmd, createAdminUserCommand)
deployment.AttachFlags(root.Flags(), vip, false)
return root
}
@ -1063,7 +1271,7 @@ func newProvisionerDaemon(
coderAPI *coderd.API,
metrics provisionerd.Metrics,
logger slog.Logger,
cfg *codersdk.DeploymentConfig,
cfg *codersdk.DeploymentValues,
cacheDir string,
errCh chan error,
dev bool,
@ -1140,11 +1348,11 @@ func newProvisionerDaemon(
return coderAPI.CreateInMemoryProvisionerDaemon(ctx, debounce)
}, &provisionerd.Options{
Logger: logger,
JobPollInterval: cfg.Provisioner.DaemonPollInterval.Value,
JobPollJitter: cfg.Provisioner.DaemonPollJitter.Value,
JobPollInterval: cfg.Provisioner.DaemonPollInterval.Value(),
JobPollJitter: cfg.Provisioner.DaemonPollJitter.Value(),
JobPollDebounce: debounce,
UpdateInterval: 500 * time.Millisecond,
ForceCancelInterval: cfg.Provisioner.ForceCancelInterval.Value,
ForceCancelInterval: cfg.Provisioner.ForceCancelInterval.Value(),
Provisioners: provisioners,
WorkDirectory: tempDir,
TracerProvider: coderAPI.TracerProvider,
@ -1172,7 +1380,11 @@ func loadCertificates(tlsCertFiles, tlsKeyFiles []string) ([]tls.Certificate, er
certFile, keyFile := tlsCertFiles[i], tlsKeyFiles[i]
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, xerrors.Errorf("load TLS key pair %d (%q, %q): %w", i, certFile, keyFile, err)
return nil, xerrors.Errorf(
"load TLS key pair %d (%q, %q): %w\ncertFiles: %+v\nkeyFiles: %+v",
i, certFile, keyFile, err,
tlsCertFiles, tlsKeyFiles,
)
}
certs[i] = cert
@ -1554,7 +1766,7 @@ func isLocalhost(host string) bool {
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}
func buildLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logger, func(), error) {
func buildLogger(cmd *cobra.Command, cfg *codersdk.DeploymentValues) (slog.Logger, func(), error) {
var (
sinks = []slog.Sink{}
closers = []func() error{}
@ -1575,32 +1787,31 @@ func buildLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logge
if err != nil {
return xerrors.Errorf("open log file %q: %w", loc, err)
}
closers = append(closers, fi.Close)
sinks = append(sinks, sinkFn(fi))
}
return nil
}
err := addSinkIfProvided(sloghuman.Sink, cfg.Logging.Human.Value)
err := addSinkIfProvided(sloghuman.Sink, cfg.Logging.Human.String())
if err != nil {
return slog.Logger{}, nil, xerrors.Errorf("add human sink: %w", err)
}
err = addSinkIfProvided(slogjson.Sink, cfg.Logging.JSON.Value)
err = addSinkIfProvided(slogjson.Sink, cfg.Logging.JSON.String())
if err != nil {
return slog.Logger{}, nil, xerrors.Errorf("add json sink: %w", err)
}
err = addSinkIfProvided(slogstackdriver.Sink, cfg.Logging.Stackdriver.Value)
err = addSinkIfProvided(slogstackdriver.Sink, cfg.Logging.Stackdriver.String())
if err != nil {
return slog.Logger{}, nil, xerrors.Errorf("add stackdriver sink: %w", err)
}
if cfg.Trace.CaptureLogs.Value {
if cfg.Trace.CaptureLogs {
sinks = append(sinks, tracing.SlogSink{})
}
level := slog.LevelInfo
if ok, _ := cmd.Flags().GetBool(varVerbose); ok {
if cfg.Verbose {
level = slog.LevelDebug
}

View File

@ -9,14 +9,12 @@ import (
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/coderd"
)
func Server(vip *viper.Viper, _ func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *cobra.Command {
func Server(_ func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *cobra.Command {
root := &cobra.Command{
Use: "server",
Short: "Start a Coder server",
@ -76,8 +74,6 @@ func Server(vip *viper.Viper, _ func(context.Context, *coderd.Options) (*coderd.
root.AddCommand(postgresBuiltinURLCmd, postgresBuiltinServeCmd, createAdminUserCommand)
deployment.AttachFlags(root.Flags(), vip, false)
return root
}

View File

@ -30,6 +30,7 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"github.com/coder/coder/cli"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/coderd/coderdtest"
@ -40,6 +41,62 @@ import (
"github.com/coder/coder/testutil"
)
func TestReadGitAuthProvidersFromEnv(t *testing.T) {
t.Parallel()
t.Run("Empty", func(t *testing.T) {
t.Parallel()
providers, err := cli.ReadGitAuthProvidersFromEnv([]string{
"HOME=/home/frodo",
})
require.NoError(t, err)
require.Empty(t, providers)
})
t.Run("InvalidKey", func(t *testing.T) {
t.Parallel()
providers, err := cli.ReadGitAuthProvidersFromEnv([]string{
"CODER_GITAUTH_XXX=invalid",
})
require.Error(t, err, "providers: %+v", providers)
require.Empty(t, providers)
})
t.Run("SkipKey", func(t *testing.T) {
t.Parallel()
providers, err := cli.ReadGitAuthProvidersFromEnv([]string{
"CODER_GITAUTH_0_ID=invalid",
"CODER_GITAUTH_2_ID=invalid",
})
require.Error(t, err, "%+v", providers)
require.Empty(t, providers)
})
t.Run("Valid", func(t *testing.T) {
t.Parallel()
providers, err := cli.ReadGitAuthProvidersFromEnv([]string{
"CODER_GITAUTH_0_ID=1",
"CODER_GITAUTH_0_TYPE=gitlab",
"CODER_GITAUTH_1_ID=2",
"CODER_GITAUTH_1_CLIENT_ID=sid",
"CODER_GITAUTH_1_CLIENT_SECRET=hunter12",
"CODER_GITAUTH_1_TOKEN_URL=google.com",
"CODER_GITAUTH_1_VALIDATE_URL=bing.com",
"CODER_GITAUTH_1_SCOPES=repo:read repo:write",
})
require.NoError(t, err)
require.Len(t, providers, 2)
// Validate the first provider.
assert.Equal(t, "1", providers[0].ID)
assert.Equal(t, "gitlab", providers[0].Type)
// Validate the second provider.
assert.Equal(t, "2", providers[1].ID)
assert.Equal(t, "sid", providers[1].ClientID)
assert.Equal(t, "hunter12", providers[1].ClientSecret)
assert.Equal(t, "google.com", providers[1].TokenURL)
assert.Equal(t, "bing.com", providers[1].ValidateURL)
assert.Equal(t, []string{"repo:read", "repo:write"}, providers[1].Scopes)
})
}
// This cannot be ran in parallel because it uses a signal.
// nolint:tparallel,paralleltest
func TestServer(t *testing.T) {
@ -327,6 +384,7 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t, args...)
err := root.ExecuteContext(ctx)
require.Error(t, err)
t.Logf("args: %v", args)
require.ErrorContains(t, err, c.errContains)
})
}
@ -341,17 +399,14 @@ func TestServer(t *testing.T) {
"server",
"--in-memory",
"--http-address", "",
"--access-url", "http://example.com",
"--access-url", "https://example.com",
"--tls-enable",
"--tls-address", ":0",
"--tls-cert-file", certPath,
"--tls-key-file", keyPath,
"--cache-dir", t.TempDir(),
)
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
clitest.Start(ctx, t, root)
// Verify HTTPS
accessURL := waitAccessURL(t, cfg)
@ -368,9 +423,6 @@ func TestServer(t *testing.T) {
defer client.HTTPClient.CloseIdleConnections()
_, err := client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("TLSValidMultiple", func(t *testing.T) {
t.Parallel()
@ -383,7 +435,7 @@ func TestServer(t *testing.T) {
"server",
"--in-memory",
"--http-address", "",
"--access-url", "http://example.com",
"--access-url", "https://example.com",
"--tls-enable",
"--tls-address", ":0",
"--tls-cert-file", cert1Path,
@ -392,10 +444,10 @@ func TestServer(t *testing.T) {
"--tls-key-file", key2Path,
"--cache-dir", t.TempDir(),
)
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
pty := ptytest.New(t)
root.SetOut(pty.Output())
clitest.Start(ctx, t, root)
accessURL := waitAccessURL(t, cfg)
require.Equal(t, "https", accessURL.Scheme)
originalHost := accessURL.Host
@ -451,9 +503,6 @@ func TestServer(t *testing.T) {
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
require.EqualValues(t, 2, atomic.LoadInt64(&dials))
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("TLSAndHTTP", func(t *testing.T) {
@ -715,15 +764,19 @@ func TestServer(t *testing.T) {
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
serverStop := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
err := root.ExecuteContext(ctx)
if err != nil {
t.Error(err)
}
close(serverStop)
}()
pty.ExpectMatch("Started HTTP listener at http://0.0.0.0:")
cancelFunc()
require.NoError(t, <-errC)
<-serverStop
})
t.Run("CanListenUnspecifiedv6", func(t *testing.T) {
@ -741,15 +794,19 @@ func TestServer(t *testing.T) {
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
serverClose := make(chan struct{}, 1)
go func() {
errC <- root.ExecuteContext(ctx)
err := root.ExecuteContext(ctx)
if err != nil {
t.Error(err)
}
close(serverClose)
}()
pty.ExpectMatch("Started HTTP listener at http://[::]:")
cancelFunc()
require.NoError(t, <-errC)
<-serverClose
})
t.Run("NoAddress", func(t *testing.T) {
@ -760,13 +817,13 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t,
"server",
"--in-memory",
"--http-address", "",
"--http-address", ":80",
"--tls-enable=false",
"--tls-address", "",
)
err := root.ExecuteContext(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "TLS is disabled. Enable with --tls-enable or specify a HTTP address")
require.ErrorContains(t, err, "tls-address")
})
t.Run("NoTLSAddress", func(t *testing.T) {
@ -782,7 +839,7 @@ func TestServer(t *testing.T) {
)
err := root.ExecuteContext(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "TLS address must be set if TLS is enabled")
require.ErrorContains(t, err, "must not be empty")
})
// DeprecatedAddress is a test for the deprecated --address flag. If
@ -807,21 +864,15 @@ func TestServer(t *testing.T) {
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
clitest.Start(ctx, t, root)
pty.ExpectMatch("--address and -a are deprecated")
pty.ExpectMatch("is deprecated")
accessURL := waitAccessURL(t, cfg)
require.Equal(t, "http", accessURL.Scheme)
client := codersdk.New(accessURL)
_, err := client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("TLS", func(t *testing.T) {
@ -834,7 +885,7 @@ func TestServer(t *testing.T) {
"server",
"--in-memory",
"--address", ":0",
"--access-url", "http://example.com",
"--access-url", "https://example.com",
"--tls-enable",
"--tls-cert-file", certPath,
"--tls-key-file", keyPath,
@ -843,12 +894,9 @@ func TestServer(t *testing.T) {
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
clitest.Start(ctx, t, root)
pty.ExpectMatch("--address and -a are deprecated")
pty.ExpectMatch("is deprecated")
accessURL := waitAccessURL(t, cfg)
require.Equal(t, "https", accessURL.Scheme)
@ -864,9 +912,6 @@ func TestServer(t *testing.T) {
defer client.HTTPClient.CloseIdleConnections()
_, err := client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
})
})
@ -1150,14 +1195,31 @@ func TestServer(t *testing.T) {
})
})
waitFile := func(t *testing.T, fiName string, dur time.Duration) {
var lastStat os.FileInfo
require.Eventually(t, func() bool {
var err error
lastStat, err = os.Stat(fiName)
if err != nil {
if !os.IsNotExist(err) {
t.Fatalf("unexpected error: %v", err)
}
return false
}
return lastStat.Size() > 0
},
testutil.WaitShort,
testutil.IntervalFast,
"file at %s should exist, last stat: %+v",
fiName, lastStat,
)
}
t.Run("Logging", func(t *testing.T) {
t.Parallel()
t.Run("CreatesFile", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
fiName := testutil.TempFile(t, "", "coder-logging-test-*")
root, _ := clitest.New(t,
@ -1168,24 +1230,13 @@ func TestServer(t *testing.T) {
"--access-url", "http://example.com",
"--log-human", fiName,
)
serverErr := make(chan error, 1)
go func() {
serverErr <- root.ExecuteContext(ctx)
}()
clitest.Start(context.Background(), t, root)
assert.Eventually(t, func() bool {
stat, err := os.Stat(fiName)
return err == nil && stat.Size() > 0
}, testutil.WaitShort, testutil.IntervalFast)
cancelFunc()
<-serverErr
waitFile(t, fiName, testutil.WaitShort)
})
t.Run("Human", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
fi := testutil.TempFile(t, "", "coder-logging-test-*")
root, _ := clitest.New(t,
@ -1196,24 +1247,13 @@ func TestServer(t *testing.T) {
"--access-url", "http://example.com",
"--log-human", fi,
)
serverErr := make(chan error, 1)
go func() {
serverErr <- root.ExecuteContext(ctx)
}()
clitest.Start(context.Background(), t, root)
assert.Eventually(t, func() bool {
stat, err := os.Stat(fi)
return err == nil && stat.Size() > 0
}, testutil.WaitShort, testutil.IntervalFast)
cancelFunc()
<-serverErr
waitFile(t, fi, testutil.WaitShort)
})
t.Run("JSON", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
fi := testutil.TempFile(t, "", "coder-logging-test-*")
root, _ := clitest.New(t,
@ -1224,17 +1264,9 @@ func TestServer(t *testing.T) {
"--access-url", "http://example.com",
"--log-json", fi,
)
serverErr := make(chan error, 1)
go func() {
serverErr <- root.ExecuteContext(ctx)
}()
clitest.Start(context.Background(), t, root)
assert.Eventually(t, func() bool {
stat, err := os.Stat(fi)
return err == nil && stat.Size() > 0
}, testutil.WaitShort, testutil.IntervalFast)
cancelFunc()
<-serverErr
waitFile(t, fi, testutil.WaitShort)
})
t.Run("Stackdriver", func(t *testing.T) {
@ -1271,10 +1303,7 @@ func TestServer(t *testing.T) {
// starting point for expecting logs.
_ = pty.ExpectMatchContext(ctx, "Started HTTP listener at ")
require.Eventually(t, func() bool {
stat, err := os.Stat(fi)
return err == nil && stat.Size() > 0
}, testutil.WaitLong, testutil.IntervalMedium)
waitFile(t, fi, testutil.WaitSuperLong)
})
t.Run("Multiple", func(t *testing.T) {
@ -1306,31 +1335,15 @@ func TestServer(t *testing.T) {
root.SetOut(pty.Output())
root.SetErr(pty.Output())
serverErr := make(chan error, 1)
go func() {
serverErr <- root.ExecuteContext(ctx)
}()
defer func() {
cancelFunc()
<-serverErr
}()
clitest.Start(ctx, t, root)
// Wait for server to listen on HTTP, this is a good
// starting point for expecting logs.
_ = pty.ExpectMatchContext(ctx, "Started HTTP listener at ")
require.Eventually(t, func() bool {
stat, err := os.Stat(fi1)
return err == nil && stat.Size() > 0
}, testutil.WaitShort, testutil.IntervalMedium, "log human size > 0")
require.Eventually(t, func() bool {
stat, err := os.Stat(fi2)
return err == nil && stat.Size() > 0
}, testutil.WaitShort, testutil.IntervalMedium, "log json size > 0")
require.Eventually(t, func() bool {
stat, err := os.Stat(fi3)
return err == nil && stat.Size() > 0
}, testutil.WaitShort, testutil.IntervalMedium, "log stackdriver size > 0")
waitFile(t, fi1, testutil.WaitSuperLong)
waitFile(t, fi2, testutil.WaitSuperLong)
waitFile(t, fi3, testutil.WaitSuperLong)
})
})
}

View File

@ -301,7 +301,6 @@ func TestTemplateEdit(t *testing.T) {
HasLicense: true,
Trial: true,
RequireTelemetry: false,
Experimental: false,
}
for _, feature := range codersdk.FeatureNames {
res.Features[feature] = codersdk.Feature{
@ -374,7 +373,6 @@ func TestTemplateEdit(t *testing.T) {
HasLicense: true,
Trial: true,
RequireTelemetry: false,
Experimental: false,
}
for _, feature := range codersdk.FeatureNames {
var one int64 = 1

View File

@ -1,400 +0,0 @@
Start a Coder server
Usage:
coder server [flags]
coder server [command]
Commands:
create-admin-user Create a new admin user with the given username, email and password and adds it to every organization.
postgres-builtin-serve Run the built-in PostgreSQL deployment.
postgres-builtin-url Output the connection URL for the built-in PostgreSQL deployment.
Flags:
--access-url string External URL to access your
deployment. This must be accessible
by all provisioned workspaces.
Consumes $CODER_ACCESS_URL
--api-rate-limit int Maximum number of requests per
minute allowed to the API per user,
or per IP address for
unauthenticated users. Negative
values mean no rate limit. Some API
endpoints have separate strict rate
limits regardless of this value to
prevent denial-of-service or brute
force attacks.
Consumes $CODER_API_RATE_LIMIT
(default 512)
--cache-dir string The directory to cache temporary
files. If unspecified and
$CACHE_DIRECTORY is set, it will be
used for compatibility with systemd.
Consumes $CODER_CACHE_DIRECTORY
(default "~/.cache/coder")
--dangerous-allow-path-app-sharing Allow workspace apps that are not
served from subdomains to be shared.
Path-based app sharing is DISABLED
by default for security purposes.
Path-based apps can make requests to
the Coder API and pose a security
risk when the workspace serves
malicious JavaScript. Path-based
apps can be disabled entirely with
--disable-path-apps for further
security.
Consumes
$CODER_DANGEROUS_ALLOW_PATH_APP_SHARING
--dangerous-allow-path-app-site-owner-access Allow site-owners to access
workspace apps from workspaces they
do not own. Owners cannot access
path-based apps they do not own by
default. Path-based apps can make
requests to the Coder API and pose a
security risk when the workspace
serves malicious JavaScript.
Path-based apps can be disabled
entirely with --disable-path-apps
for further security.
Consumes
$CODER_DANGEROUS_ALLOW_PATH_APP_SITE_OWNER_ACCESS
--dangerous-disable-rate-limits Disables all rate limits. This is
not recommended in production.
Consumes $CODER_RATE_LIMIT_DISABLE_ALL
--derp-config-path string Path to read a DERP mapping from.
See:
https://tailscale.com/kb/1118/custom-derp-servers/
Consumes $CODER_DERP_CONFIG_PATH
--derp-config-url string URL to fetch a DERP mapping on
startup. See:
https://tailscale.com/kb/1118/custom-derp-servers/
Consumes $CODER_DERP_CONFIG_URL
--derp-server-enable Whether to enable or disable the
embedded DERP relay server.
Consumes $CODER_DERP_SERVER_ENABLE
(default true)
--derp-server-region-code string Region code to use for the embedded
DERP server.
Consumes
$CODER_DERP_SERVER_REGION_CODE
(default "coder")
--derp-server-region-id int Region ID to use for the embedded
DERP server.
Consumes
$CODER_DERP_SERVER_REGION_ID
(default 999)
--derp-server-region-name string Region name that for the embedded
DERP server.
Consumes
$CODER_DERP_SERVER_REGION_NAME
(default "Coder Embedded Relay")
--derp-server-stun-addresses strings Addresses for STUN servers to
establish P2P connections. Set empty
to disable P2P connections.
Consumes
$CODER_DERP_SERVER_STUN_ADDRESSES
(default [stun.l.google.com:19302])
--disable-password-auth coder server create-admin Disable password authentication.
This is recommended for security
purposes in production deployments
that rely on an identity provider.
Any user with the owner role will be
able to sign in with their password
regardless of this setting to avoid
potential lock out. If you are
locked out of your account, you can
use the coder server create-admin
command to create a new admin user
directly in the database.
Consumes $CODER_DISABLE_PASSWORD_AUTH
--disable-path-apps Disable workspace apps that are not
served from subdomains. Path-based
apps can make requests to the Coder
API and pose a security risk when
the workspace serves malicious
JavaScript. This is recommended for
security purposes if a
--wildcard-access-url is configured.
Consumes $CODER_DISABLE_PATH_APPS
--disable-session-expiry-refresh Disable automatic session expiry
bumping due to activity. This forces
all sessions to become invalid after
the session expiry duration has been
reached.
Consumes
$CODER_DISABLE_SESSION_EXPIRY_REFRESH
--experiments strings Enable one or more experiments.
These are not ready for production.
Separate multiple experiments with
commas, or enter '*' to opt-in to
all available experiments.
Consumes $CODER_EXPERIMENTS
-h, --help help for server
--http-address string HTTP bind address of the server.
Unset to disable the HTTP endpoint.
Consumes $CODER_HTTP_ADDRESS
(default "127.0.0.1:3000")
--log-human string Output human-readable logs to a
given file.
Consumes $CODER_LOGGING_HUMAN
(default "/dev/stderr")
--log-json string Output JSON logs to a given file.
Consumes $CODER_LOGGING_JSON
--log-stackdriver string Output Stackdriver compatible logs
to a given file.
Consumes $CODER_LOGGING_STACKDRIVER
--max-token-lifetime duration The maximum lifetime duration users
can specify when creating an API
token.
Consumes $CODER_MAX_TOKEN_LIFETIME
(default 2540400h0m0s)
--oauth2-github-allow-everyone Allow all logins, setting this
option means allowed orgs and teams
must be empty.
Consumes
$CODER_OAUTH2_GITHUB_ALLOW_EVERYONE
--oauth2-github-allow-signups Whether new users can sign up with
GitHub.
Consumes
$CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS
--oauth2-github-allowed-orgs strings Organizations the user must be a
member of to Login with GitHub.
Consumes
$CODER_OAUTH2_GITHUB_ALLOWED_ORGS
--oauth2-github-allowed-teams strings Teams inside organizations the user
must be a member of to Login with
GitHub. Structured as:
<organization-name>/<team-slug>.
Consumes
$CODER_OAUTH2_GITHUB_ALLOWED_TEAMS
--oauth2-github-client-id string Client ID for Login with GitHub.
Consumes $CODER_OAUTH2_GITHUB_CLIENT_ID
--oauth2-github-client-secret string Client secret for Login with GitHub.
Consumes
$CODER_OAUTH2_GITHUB_CLIENT_SECRET
--oauth2-github-enterprise-base-url string Base URL of a GitHub Enterprise
deployment to use for Login with
GitHub.
Consumes
$CODER_OAUTH2_GITHUB_ENTERPRISE_BASE_URL
--oidc-allow-signups Whether new users can sign up with
OIDC.
Consumes $CODER_OIDC_ALLOW_SIGNUPS
(default true)
--oidc-client-id string Client ID to use for Login with
OIDC.
Consumes $CODER_OIDC_CLIENT_ID
--oidc-client-secret string Client secret to use for Login with
OIDC.
Consumes $CODER_OIDC_CLIENT_SECRET
--oidc-email-domain strings Email domains that clients logging
in with OIDC must match.
Consumes $CODER_OIDC_EMAIL_DOMAIN
--oidc-icon-url string URL pointing to the icon to use on
the OepnID Connect login button
Consumes $CODER_OIDC_ICON_URL
--oidc-ignore-email-verified Ignore the email_verified claim from
the upstream provider.
Consumes
$CODER_OIDC_IGNORE_EMAIL_VERIFIED
--oidc-issuer-url string Issuer URL to use for Login with
OIDC.
Consumes $CODER_OIDC_ISSUER_URL
--oidc-scopes strings Scopes to grant when authenticating
with OIDC.
Consumes $CODER_OIDC_SCOPES (default
[openid,profile,email])
--oidc-sign-in-text string The text to show on the OpenID
Connect sign in button
Consumes $CODER_OIDC_SIGN_IN_TEXT
(default "OpenID Connect")
--oidc-username-field string OIDC claim field to use as the
username.
Consumes $CODER_OIDC_USERNAME_FIELD
(default "preferred_username")
--postgres-url string URL of a PostgreSQL database. If
empty, PostgreSQL binaries will be
downloaded from Maven
(https://repo1.maven.org/maven2) and
store all data in the config root.
Access the built-in database with
"coder server postgres-builtin-url".
Consumes $CODER_PG_CONNECTION_URL
--pprof-address string The bind address to serve pprof.
Consumes $CODER_PPROF_ADDRESS
(default "127.0.0.1:6060")
--pprof-enable Serve pprof metrics on the address
defined by pprof address.
Consumes $CODER_PPROF_ENABLE
--prometheus-address string The bind address to serve prometheus
metrics.
Consumes $CODER_PROMETHEUS_ADDRESS
(default "127.0.0.1:2112")
--prometheus-enable Serve prometheus metrics on the
address defined by prometheus
address.
Consumes $CODER_PROMETHEUS_ENABLE
--provisioner-daemon-poll-interval duration Time to wait before polling for a
new job.
Consumes
$CODER_PROVISIONER_DAEMON_POLL_INTERVAL (default 1s)
--provisioner-daemon-poll-jitter duration Random jitter added to the poll
interval.
Consumes
$CODER_PROVISIONER_DAEMON_POLL_JITTER (default 100ms)
--provisioner-daemons int Number of provisioner daemons to
create on start. If builds are stuck
in queued state for a long time,
consider increasing this.
Consumes $CODER_PROVISIONER_DAEMONS
(default 3)
--provisioner-force-cancel-interval duration Time to force cancel provisioning
tasks that are stuck.
Consumes
$CODER_PROVISIONER_FORCE_CANCEL_INTERVAL (default 10m0s)
--proxy-trusted-headers strings Headers to trust for forwarding IP
addresses. e.g. Cf-Connecting-Ip,
True-Client-Ip, X-Forwarded-For
Consumes $CODER_PROXY_TRUSTED_HEADERS
--proxy-trusted-origins strings Origin addresses to respect
"proxy-trusted-headers". e.g.
192.168.1.0/24
Consumes $CODER_PROXY_TRUSTED_ORIGINS
--redirect-to-access-url Specifies whether to redirect
requests that do not match the
access URL host.
Consumes $CODER_REDIRECT_TO_ACCESS_URL
--secure-auth-cookie Controls if the 'Secure' property is
set on browser session cookies.
Consumes $CODER_SECURE_AUTH_COOKIE
--session-duration duration The token expiry duration for
browser sessions. Sessions may last
longer if they are actively making
requests, but this functionality can
be disabled via
--disable-session-expiry-refresh.
Consumes $CODER_MAX_SESSION_EXPIRY
(default 24h0m0s)
--ssh-keygen-algorithm string The algorithm to use for generating
ssh keys. Accepted values are
"ed25519", "ecdsa", or "rsa4096".
Consumes $CODER_SSH_KEYGEN_ALGORITHM
(default "ed25519")
--strict-transport-security int Controls if the
'Strict-Transport-Security' header
is set on all static file responses.
This header should only be set if
the server is accessed via HTTPS.
This value is the MaxAge in seconds
of the header.
Consumes $CODER_STRICT_TRANSPORT_SECURITY
--strict-transport-security-options strings Two optional fields can be set in
the Strict-Transport-Security
header; 'includeSubDomains' and
'preload'. The
'strict-transport-security' flag
must be set to a non-zero value for
these options to be used.
Consumes
$CODER_STRICT_TRANSPORT_SECURITY_OPTIONS
--swagger-enable Expose the swagger endpoint via
/swagger.
Consumes $CODER_SWAGGER_ENABLE
--telemetry Whether telemetry is enabled or not.
Coder collects anonymized usage data
to help improve our product.
Consumes $CODER_TELEMETRY_ENABLE
--telemetry-trace Whether Opentelemetry traces are
sent to Coder. Coder collects
anonymized application tracing to
help improve our product. Disabling
telemetry also disables this option.
Consumes $CODER_TELEMETRY_TRACE
--tls-address string HTTPS bind address of the server.
Consumes $CODER_TLS_ADDRESS (default
"127.0.0.1:3443")
--tls-cert-file strings Path to each certificate for TLS. It
requires a PEM-encoded file. To
configure the listener to use a CA
certificate, concatenate the primary
certificate and the CA certificate
together. The primary certificate
should appear first in the combined
file.
Consumes $CODER_TLS_CERT_FILE
--tls-client-auth string Policy the server will follow for
TLS Client Authentication. Accepted
values are "none", "request",
"require-any", "verify-if-given", or
"require-and-verify".
Consumes $CODER_TLS_CLIENT_AUTH
(default "none")
--tls-client-ca-file string PEM-encoded Certificate Authority
file used for checking the
authenticity of client
Consumes $CODER_TLS_CLIENT_CA_FILE
--tls-client-cert-file string Path to certificate for client TLS
authentication. It requires a
PEM-encoded file.
Consumes $CODER_TLS_CLIENT_CERT_FILE
--tls-client-key-file string Path to key for client TLS
authentication. It requires a
PEM-encoded file.
Consumes $CODER_TLS_CLIENT_KEY_FILE
--tls-enable Whether TLS will be enabled.
Consumes $CODER_TLS_ENABLE
--tls-key-file strings Paths to the private keys for each
of the certificates. It requires a
PEM-encoded file.
Consumes $CODER_TLS_KEY_FILE
--tls-min-version string Minimum supported version of TLS.
Accepted values are "tls10",
"tls11", "tls12" or "tls13"
Consumes $CODER_TLS_MIN_VERSION
(default "tls12")
--trace 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
Consumes $CODER_TRACE_ENABLE
--trace-honeycomb-api-key string Enables trace exporting to
Honeycomb.io using the provided API
Key.
Consumes $CODER_TRACE_HONEYCOMB_API_KEY
--trace-logs Enables capturing of logs as events
in traces. This is useful for
debugging, but may result in a very
large amount of events being sent to
the tracing backend which may incur
significant costs. If the verbose
flag was supplied, debug-level logs
will be included.
Consumes $CODER_TRACE_CAPTURE_LOGS
--update-check Periodically check for new releases
of Coder and inform the owner. The
check is performed once per day.
Consumes $CODER_UPDATE_CHECK
--wildcard-access-url string Specifies the wildcard hostname to
use for workspace applications in
the form "*.example.com".
Consumes $CODER_WILDCARD_ACCESS_URL
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "~/.config/coderv2")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
Use "coder server [command] --help" for more information about a command.

View File

@ -1,36 +0,0 @@
Create a new admin user with the given username, email and password and adds it to every organization.
Usage:
coder server create-admin-user [flags]
Flags:
--email string The email of the new user. If not specified, you will be
prompted via stdin. Consumes $CODER_EMAIL.
-h, --help help for create-admin-user
--password string The password of the new user. If not specified, you will
be prompted via stdin. Consumes $CODER_PASSWORD.
--postgres-url string URL of a PostgreSQL database. If empty, the built-in
PostgreSQL deployment will be used (Coder must not be
already running in this case). Consumes $CODER_POSTGRES_URL.
--ssh-keygen-algorithm string The algorithm to use for generating ssh keys. Accepted
values are "ed25519", "ecdsa", or "rsa4096". Consumes
$CODER_SSH_KEYGEN_ALGORITHM. (default "ed25519")
--username string The username of the new user. If not specified, you will
be prompted via stdin. Consumes $CODER_USERNAME.
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "~/.config/coderv2")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE

View File

@ -1,25 +0,0 @@
Run the built-in PostgreSQL deployment.
Usage:
coder server postgres-builtin-serve [flags]
Flags:
-h, --help help for postgres-builtin-serve
--raw-url Output the raw connection URL instead of a psql command.
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "~/.config/coderv2")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE

View File

@ -1,25 +0,0 @@
Output the connection URL for the built-in PostgreSQL deployment.
Usage:
coder server postgres-builtin-url [flags]
Flags:
-h, --help help for postgres-builtin-url
--raw-url Output the raw connection URL instead of a psql command.
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "~/.config/coderv2")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE

140
cli/usage.go Normal file
View File

@ -0,0 +1,140 @@
package cli
import (
_ "embed"
"fmt"
"io"
"sort"
"strings"
"text/template"
"github.com/mitchellh/go-wordwrap"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
)
//go:embed usage.tpl
var usageTemplateRaw string
type optionGroup struct {
Name string
Description string
Options clibase.OptionSet
}
const envPrefix = "CODER_"
var usageTemplate = template.Must(
template.New("usage").Funcs(
template.FuncMap{
"wordWrap": func(s string, width uint) string {
return wordwrap.WrapString(s, width)
},
"trimNewline": func(s string) string {
return strings.TrimSuffix(s, "\n")
},
"indent": func(s string, tabs int) string {
var sb strings.Builder
for _, line := range strings.Split(s, "\n") {
// Remove existing indent, if any.
_, _ = sb.WriteString(strings.Repeat("\t", tabs))
_, _ = sb.WriteString(line)
_, _ = sb.WriteString("\n")
}
return sb.String()
},
"envName": func(opt clibase.Option) string {
if opt.Env == "" {
return ""
}
return envPrefix + opt.Env
},
"flagName": func(opt clibase.Option) string {
return opt.Flag
},
"prettyHeader": func(s string) string {
return cliui.Styles.Bold.Render(s)
},
"isEnterprise": func(opt clibase.Option) bool {
return opt.Annotations.IsSet("enterprise")
},
"isDeprecated": func(opt clibase.Option) bool {
return len(opt.UseInstead) > 0
},
"formatGroupDescription": func(s string) string {
s = strings.ReplaceAll(s, "\n", "")
s = "\n" + s + "\n"
s = wordwrap.WrapString(s, 60)
return s
},
"optionGroups": func(cmd *clibase.Cmd) []optionGroup {
groups := []optionGroup{{
// Default group.
Name: "",
Description: "",
}}
enterpriseGroup := optionGroup{
Name: "Enterprise",
Description: `These options are only available in the Enterprise Edition.`,
}
// Sort options lexicographically.
sort.Slice(cmd.Options, func(i, j int) bool {
return cmd.Options[i].Name < cmd.Options[j].Name
})
optionLoop:
for _, opt := range cmd.Options {
if opt.Hidden {
continue
}
// Enterprise options are always grouped separately.
if opt.Annotations.IsSet("enterprise") {
enterpriseGroup.Options = append(enterpriseGroup.Options, opt)
continue
}
if len(opt.Group.Ancestry()) == 0 {
// Just add option to default group.
groups[0].Options = append(groups[0].Options, opt)
continue
}
groupName := opt.Group.FullName()
for i, foundGroup := range groups {
if foundGroup.Name != groupName {
continue
}
groups[i].Options = append(groups[i].Options, opt)
continue optionLoop
}
groups = append(groups, optionGroup{
Name: groupName,
Description: opt.Group.Description,
Options: clibase.OptionSet{opt},
})
}
sort.Slice(groups, func(i, j int) bool {
// Sort groups lexicographically.
return groups[i].Name < groups[j].Name
})
// Always show enterprise group last.
return append(groups, enterpriseGroup)
},
},
).Parse(usageTemplateRaw),
)
// usageFn returns a function that generates usage (help)
// output for a given command.
func usageFn(output io.Writer, cmd *clibase.Cmd) func() {
return func() {
err := usageTemplate.Execute(output, cmd)
if err != nil {
_, _ = fmt.Fprintf(output, "execute template: %v", err)
}
}
}

31
cli/usage.tpl Normal file
View File

@ -0,0 +1,31 @@
{{- /* Heavily inspired by the Go toolchain formatting. */ -}}
usage: {{.FullUsage}}
{{.Short}}
{{ with .Long}} {{.}} {{ end }}
{{- range $index, $group := optionGroups . }}
{{ with $group.Name }} {{- print $group.Name " Options" | prettyHeader }} {{ else -}} {{ prettyHeader "Options"}}{{- end -}}
{{- with $group.Description }}
{{ formatGroupDescription . }}
{{- else }}
{{ " " }}
{{- end }}
{{- range $index, $option := $group.Options }}
{{- with flagName $option }}
--{{- . -}} {{ end }} {{- with $option.FlagShorthand }}, -{{- . -}} {{ end }}
{{- with envName $option }}, ${{ . }} {{ end }}
{{- with $option.Default }} (default: {{.}}) {{ end }}
{{- with $option.Description }}
{{- $desc := wordWrap $option.Description 60 }}
{{ indent $desc 2}}
{{- if isDeprecated $option }} DEPRECATED {{ end }}
{{- end -}}
{{- end }}
{{- end }}
{{- range $index, $child := .Children }}
{{- if eq $index 0 }}
{{ prettyHeader "Subcommands"}}
{{- end }}
{{ indent $child.Use 1 | trimNewline }}{{ indent $child.Short 1 | trimNewline }}
{{- end }}

679
coderd/apidoc/docs.go generated
View File

@ -5307,6 +5307,177 @@ const docTemplate = `{
}
}
},
"clibase.Annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"clibase.Group": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/clibase.Group"
}
},
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"parent": {
"$ref": "#/definitions/clibase.Group"
}
}
},
"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"
},
"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."
},
"yaml": {
"description": "YAML is the YAML key used to configure this option. If unset, YAML\nconfiguring is disabled.",
"type": "string"
}
}
},
"clibase.Struct-array_codersdk_GitAuthConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.GitAuthConfig"
}
}
}
},
"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"
}
]
}
}
},
"coderd.SCIMUser": {
"type": "object",
"properties": {
@ -6203,10 +6374,10 @@ const docTemplate = `{
"type": "object",
"properties": {
"path": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -6225,22 +6396,25 @@ const docTemplate = `{
"type": "object",
"properties": {
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"region_code": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"region_id": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-int"
"type": "integer"
},
"region_name": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"relay_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
},
"stun_addresses": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
}
}
},
@ -6248,44 +6422,72 @@ const docTemplate = `{
"type": "object",
"properties": {
"allow_path_app_sharing": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"allow_path_app_site_owner_access": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
"codersdk.DeploymentConfig": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/codersdk.DeploymentValues"
},
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/clibase.Option"
}
}
}
},
"codersdk.DeploymentDAUsResponse": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.DAUEntry"
}
}
}
},
"codersdk.DeploymentValues": {
"type": "object",
"properties": {
"access_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
},
"address": {
"description": "DEPRECATED: Use HTTPAddress or TLS.Address instead.",
"allOf": [
{
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.HostPort"
}
]
},
"agent_fallback_troubleshooting_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
},
"agent_stat_refresh_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"audit_logging": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"autobuild_poll_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"browser_only": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"cache_directory": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"config": {
"type": "string"
},
"dangerous": {
"$ref": "#/definitions/codersdk.DangerousConfig"
@ -6294,45 +6496,41 @@ const docTemplate = `{
"$ref": "#/definitions/codersdk.DERP"
},
"disable_password_auth": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"disable_path_apps": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"disable_session_expiry_refresh": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
},
"experimental": {
"description": "DEPRECATED: Use Experiments instead.",
"allOf": [
{
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
}
]
"type": "boolean"
},
"experiments": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"gitauth": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig"
"git_auth": {
"$ref": "#/definitions/clibase.Struct-array_codersdk_GitAuthConfig"
},
"http_address": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"description": "HTTPAddress is a string because it may be set to zero to disable.",
"type": "string"
},
"in_memory_database": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"logging": {
"$ref": "#/definitions/codersdk.LoggingConfig"
},
"max_session_expiry": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"max_token_lifetime": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"metrics_cache_refresh_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"oauth2": {
"$ref": "#/definitions/codersdk.OAuth2Config"
@ -6341,7 +6539,7 @@ const docTemplate = `{
"$ref": "#/definitions/codersdk.OIDCConfig"
},
"pg_connection_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"pprof": {
"$ref": "#/definitions/codersdk.PprofConfig"
@ -6353,31 +6551,40 @@ const docTemplate = `{
"$ref": "#/definitions/codersdk.ProvisionerConfig"
},
"proxy_trusted_headers": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"proxy_trusted_origins": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"rate_limit": {
"$ref": "#/definitions/codersdk.RateLimitConfig"
},
"redirect_to_access_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"scim_api_key": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"secure_auth_cookie": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"ssh_keygen_algorithm": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"strict_transport_security": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-int"
"type": "integer"
},
"strict_transport_security_options": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"support": {
"$ref": "#/definitions/codersdk.SupportConfig"
@ -6395,263 +6602,16 @@ const docTemplate = `{
"$ref": "#/definitions/codersdk.TraceConfig"
},
"update_check": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"verbose": {
"type": "boolean"
},
"wildcard_access_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
}
}
},
"codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig": {
"type": "object",
"properties": {
"default": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.GitAuthConfig"
}
"$ref": "#/definitions/clibase.URL"
},
"enterprise": {
"write_config": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.GitAuthConfig"
}
}
}
},
"codersdk.DeploymentConfigField-array_codersdk_LinkConfig": {
"type": "object",
"properties": {
"default": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.LinkConfig"
}
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.LinkConfig"
}
}
}
},
"codersdk.DeploymentConfigField-array_string": {
"type": "object",
"properties": {
"default": {
"type": "array",
"items": {
"type": "string"
}
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"codersdk.DeploymentConfigField-bool": {
"type": "object",
"properties": {
"default": {
"type": "boolean"
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "boolean"
}
}
},
"codersdk.DeploymentConfigField-int": {
"type": "object",
"properties": {
"default": {
"type": "integer"
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "integer"
}
}
},
"codersdk.DeploymentConfigField-string": {
"type": "object",
"properties": {
"default": {
"type": "string"
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"codersdk.DeploymentConfigField-time_Duration": {
"type": "object",
"properties": {
"default": {
"type": "integer"
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "integer"
}
}
},
"codersdk.DeploymentDAUsResponse": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.DAUEntry"
}
}
}
},
@ -6677,10 +6637,6 @@ const docTemplate = `{
"type": "string"
}
},
"experimental": {
"description": "DEPRECATED: use Experiments instead.",
"type": "boolean"
},
"features": {
"type": "object",
"additionalProperties": {
@ -6936,13 +6892,13 @@ const docTemplate = `{
"type": "object",
"properties": {
"human": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"json": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"stackdriver": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -7000,25 +6956,31 @@ const docTemplate = `{
"type": "object",
"properties": {
"allow_everyone": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"allow_signups": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"allowed_orgs": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"allowed_teams": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"client_id": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_secret": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"enterprise_base_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -7040,34 +7002,40 @@ const docTemplate = `{
"type": "object",
"properties": {
"allow_signups": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"client_id": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_secret": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"email_domain": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"icon_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
},
"ignore_email_verified": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"issuer_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"scopes": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"sign_in_text": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"username_field": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -7305,10 +7273,10 @@ const docTemplate = `{
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.HostPort"
},
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -7316,10 +7284,10 @@ const docTemplate = `{
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.HostPort"
},
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -7327,16 +7295,16 @@ const docTemplate = `{
"type": "object",
"properties": {
"daemon_poll_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"daemon_poll_jitter": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"daemons": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-int"
"type": "integer"
},
"force_cancel_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
}
}
},
@ -7512,10 +7480,10 @@ const docTemplate = `{
"type": "object",
"properties": {
"api": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-int"
"type": "integer"
},
"disable_all": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -7628,7 +7596,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"links": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_codersdk_LinkConfig"
"$ref": "#/definitions/clibase.Struct-array_codersdk_LinkConfig"
}
}
},
@ -7636,7 +7604,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -7644,34 +7612,40 @@ const docTemplate = `{
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.HostPort"
},
"cert_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"client_auth": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_ca_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_cert_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_key_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"key_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"min_version": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"redirect_http": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -7679,13 +7653,13 @@ const docTemplate = `{
"type": "object",
"properties": {
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"trace": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
}
}
},
@ -8054,13 +8028,13 @@ const docTemplate = `{
"type": "object",
"properties": {
"capture_logs": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"honeycomb_api_key": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -9077,6 +9051,9 @@ const docTemplate = `{
"type": "string"
}
}
},
"url.Userinfo": {
"type": "object"
}
},
"securityDefinitions": {

View File

@ -4696,6 +4696,177 @@
}
}
},
"clibase.Annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"clibase.Group": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/clibase.Group"
}
},
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"parent": {
"$ref": "#/definitions/clibase.Group"
}
}
},
"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"
},
"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."
},
"yaml": {
"description": "YAML is the YAML key used to configure this option. If unset, YAML\nconfiguring is disabled.",
"type": "string"
}
}
},
"clibase.Struct-array_codersdk_GitAuthConfig": {
"type": "object",
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.GitAuthConfig"
}
}
}
},
"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"
}
]
}
}
},
"coderd.SCIMUser": {
"type": "object",
"properties": {
@ -5507,10 +5678,10 @@
"type": "object",
"properties": {
"path": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -5529,22 +5700,25 @@
"type": "object",
"properties": {
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"region_code": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"region_id": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-int"
"type": "integer"
},
"region_name": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"relay_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
},
"stun_addresses": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
}
}
},
@ -5552,44 +5726,72 @@
"type": "object",
"properties": {
"allow_path_app_sharing": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"allow_path_app_site_owner_access": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
"codersdk.DeploymentConfig": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/codersdk.DeploymentValues"
},
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/clibase.Option"
}
}
}
},
"codersdk.DeploymentDAUsResponse": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.DAUEntry"
}
}
}
},
"codersdk.DeploymentValues": {
"type": "object",
"properties": {
"access_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
},
"address": {
"description": "DEPRECATED: Use HTTPAddress or TLS.Address instead.",
"allOf": [
{
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.HostPort"
}
]
},
"agent_fallback_troubleshooting_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
},
"agent_stat_refresh_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"audit_logging": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"autobuild_poll_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"browser_only": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"cache_directory": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"config": {
"type": "string"
},
"dangerous": {
"$ref": "#/definitions/codersdk.DangerousConfig"
@ -5598,45 +5800,41 @@
"$ref": "#/definitions/codersdk.DERP"
},
"disable_password_auth": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"disable_path_apps": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"disable_session_expiry_refresh": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
},
"experimental": {
"description": "DEPRECATED: Use Experiments instead.",
"allOf": [
{
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
}
]
"type": "boolean"
},
"experiments": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"gitauth": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig"
"git_auth": {
"$ref": "#/definitions/clibase.Struct-array_codersdk_GitAuthConfig"
},
"http_address": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"description": "HTTPAddress is a string because it may be set to zero to disable.",
"type": "string"
},
"in_memory_database": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"logging": {
"$ref": "#/definitions/codersdk.LoggingConfig"
},
"max_session_expiry": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"max_token_lifetime": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"metrics_cache_refresh_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"oauth2": {
"$ref": "#/definitions/codersdk.OAuth2Config"
@ -5645,7 +5843,7 @@
"$ref": "#/definitions/codersdk.OIDCConfig"
},
"pg_connection_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"pprof": {
"$ref": "#/definitions/codersdk.PprofConfig"
@ -5657,31 +5855,40 @@
"$ref": "#/definitions/codersdk.ProvisionerConfig"
},
"proxy_trusted_headers": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"proxy_trusted_origins": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"rate_limit": {
"$ref": "#/definitions/codersdk.RateLimitConfig"
},
"redirect_to_access_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"scim_api_key": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"secure_auth_cookie": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"ssh_keygen_algorithm": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"strict_transport_security": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-int"
"type": "integer"
},
"strict_transport_security_options": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"support": {
"$ref": "#/definitions/codersdk.SupportConfig"
@ -5699,263 +5906,16 @@
"$ref": "#/definitions/codersdk.TraceConfig"
},
"update_check": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"verbose": {
"type": "boolean"
},
"wildcard_access_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
}
}
},
"codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig": {
"type": "object",
"properties": {
"default": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.GitAuthConfig"
}
"$ref": "#/definitions/clibase.URL"
},
"enterprise": {
"write_config": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.GitAuthConfig"
}
}
}
},
"codersdk.DeploymentConfigField-array_codersdk_LinkConfig": {
"type": "object",
"properties": {
"default": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.LinkConfig"
}
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.LinkConfig"
}
}
}
},
"codersdk.DeploymentConfigField-array_string": {
"type": "object",
"properties": {
"default": {
"type": "array",
"items": {
"type": "string"
}
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"codersdk.DeploymentConfigField-bool": {
"type": "object",
"properties": {
"default": {
"type": "boolean"
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "boolean"
}
}
},
"codersdk.DeploymentConfigField-int": {
"type": "object",
"properties": {
"default": {
"type": "integer"
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "integer"
}
}
},
"codersdk.DeploymentConfigField-string": {
"type": "object",
"properties": {
"default": {
"type": "string"
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"codersdk.DeploymentConfigField-time_Duration": {
"type": "object",
"properties": {
"default": {
"type": "integer"
},
"enterprise": {
"type": "boolean"
},
"flag": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"name": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"shorthand": {
"type": "string"
},
"usage": {
"type": "string"
},
"value": {
"type": "integer"
}
}
},
"codersdk.DeploymentDAUsResponse": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.DAUEntry"
}
}
}
},
@ -5977,10 +5937,6 @@
"type": "string"
}
},
"experimental": {
"description": "DEPRECATED: use Experiments instead.",
"type": "boolean"
},
"features": {
"type": "object",
"additionalProperties": {
@ -6213,13 +6169,13 @@
"type": "object",
"properties": {
"human": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"json": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"stackdriver": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -6267,25 +6223,31 @@
"type": "object",
"properties": {
"allow_everyone": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"allow_signups": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"allowed_orgs": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"allowed_teams": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"client_id": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_secret": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"enterprise_base_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -6307,34 +6269,40 @@
"type": "object",
"properties": {
"allow_signups": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"client_id": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_secret": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"email_domain": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"icon_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
},
"ignore_email_verified": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"issuer_url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"scopes": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"sign_in_text": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"username_field": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -6538,10 +6506,10 @@
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.HostPort"
},
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -6549,10 +6517,10 @@
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.HostPort"
},
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -6560,16 +6528,16 @@
"type": "object",
"properties": {
"daemon_poll_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"daemon_poll_jitter": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
},
"daemons": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-int"
"type": "integer"
},
"force_cancel_interval": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration"
"type": "integer"
}
}
},
@ -6733,10 +6701,10 @@
"type": "object",
"properties": {
"api": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-int"
"type": "integer"
},
"disable_all": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -6849,7 +6817,7 @@
"type": "object",
"properties": {
"links": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_codersdk_LinkConfig"
"$ref": "#/definitions/clibase.Struct-array_codersdk_LinkConfig"
}
}
},
@ -6857,7 +6825,7 @@
"type": "object",
"properties": {
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -6865,34 +6833,40 @@
"type": "object",
"properties": {
"address": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.HostPort"
},
"cert_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"client_auth": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_ca_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_cert_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"client_key_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"key_file": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
"type": "array",
"items": {
"type": "string"
}
},
"min_version": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
},
"redirect_http": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
}
}
},
@ -6900,13 +6874,13 @@
"type": "object",
"properties": {
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"trace": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"url": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"$ref": "#/definitions/clibase.URL"
}
}
},
@ -7247,13 +7221,13 @@
"type": "object",
"properties": {
"capture_logs": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"enable": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
"type": "boolean"
},
"honeycomb_api_key": {
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
"type": "string"
}
}
},
@ -8199,6 +8173,9 @@
"type": "string"
}
}
},
"url.Userinfo": {
"type": "object"
}
},
"securityDefinitions": {

View File

@ -371,8 +371,11 @@ func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
return xerrors.New("lifetime must be positive number greater than 0")
}
if lifetime > api.DeploymentConfig.MaxTokenLifetime.Value {
return xerrors.Errorf("lifetime must be less than %s", api.DeploymentConfig.MaxTokenLifetime.Value)
if lifetime > api.DeploymentValues.MaxTokenLifetime.Value() {
return xerrors.Errorf(
"lifetime must be less than %v",
api.DeploymentValues.MaxTokenLifetime,
)
}
return nil
@ -391,8 +394,8 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h
if params.LifetimeSeconds != 0 {
params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second)
} else {
params.ExpiresAt = database.Now().Add(api.DeploymentConfig.SessionDuration.Value)
params.LifetimeSeconds = int64(api.DeploymentConfig.SessionDuration.Value.Seconds())
params.ExpiresAt = database.Now().Add(api.DeploymentValues.SessionDuration.Value())
params.LifetimeSeconds = int64(api.DeploymentValues.SessionDuration.Value().Seconds())
}
}
if params.LifetimeSeconds == 0 {

View File

@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbtestutil"
@ -110,10 +111,10 @@ func TestTokenUserSetMaxLifetime(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dc := coderdtest.DeploymentConfig(t)
dc.MaxTokenLifetime.Value = time.Hour * 24 * 7
dc := coderdtest.DeploymentValues(t)
dc.MaxTokenLifetime = clibase.Duration(time.Hour * 24 * 7)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: dc,
DeploymentValues: dc,
})
_ = coderdtest.CreateFirstUser(t, client)
@ -130,41 +131,16 @@ func TestTokenUserSetMaxLifetime(t *testing.T) {
require.ErrorContains(t, err, "lifetime must be less")
}
func TestTokenDefaultMaxLifetime(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dc := coderdtest.DeploymentConfig(t)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: dc,
})
_ = coderdtest.CreateFirstUser(t, client)
// success
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 365,
})
require.NoError(t, err)
// fail - default --max-token-lifetime is the maximum value of time.Duration
// which is 24 * 365 * 290.
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 366 * 290,
})
require.ErrorContains(t, err, "lifetime must be less")
}
func TestSessionExpiry(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dc := coderdtest.DeploymentConfig(t)
dc := coderdtest.DeploymentValues(t)
db, pubsub := dbtestutil.NewDB(t)
adminClient := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: dc,
DeploymentValues: dc,
Database: db,
Pubsub: pubsub,
})
@ -176,7 +152,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.Value = time.Second
dc.SessionDuration = clibase.Duration(time.Second)
userClient, _ := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID)
@ -185,8 +161,8 @@ func TestSessionExpiry(t *testing.T) {
apiKey, err := db.GetAPIKeyByID(ctx, strings.Split(token, "-")[0])
require.NoError(t, err)
require.EqualValues(t, dc.SessionDuration.Value.Seconds(), apiKey.LifetimeSeconds)
require.WithinDuration(t, apiKey.CreatedAt.Add(dc.SessionDuration.Value), apiKey.ExpiresAt, 2*time.Second)
require.EqualValues(t, dc.SessionDuration.Value().Seconds(), apiKey.LifetimeSeconds)
require.WithinDuration(t, apiKey.CreatedAt.Add(dc.SessionDuration.Value()), apiKey.ExpiresAt, 2*time.Second)
// Update the session token to be expired so we can test that it is
// rejected for extra points.

View File

@ -60,7 +60,7 @@ func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objec
switch object.RBACObject().Type {
case rbac.ResourceWorkspaceExecution.Type:
// This is not a db resource, always in API layer
case rbac.ResourceDeploymentConfig.Type:
case rbac.ResourceDeploymentValues.Type:
// For metric cache items like DAU, we do not hit the DB.
// Some db actions are in asserted in the authz layer.
case rbac.ResourceReplicas.Type:

View File

@ -135,7 +135,7 @@ type Options struct {
MetricsCacheRefreshInterval time.Duration
AgentStatsRefreshInterval time.Duration
Experimental bool
DeploymentConfig *codersdk.DeploymentConfig
DeploymentValues *codersdk.DeploymentValues
UpdateCheckOptions *updatecheck.Options // Set non-nil to enable update checking.
HTTPClient *http.Client
@ -163,7 +163,9 @@ func New(options *Options) *API {
if options == nil {
options = &Options{}
}
experiments := initExperiments(options.Logger, options.DeploymentConfig.Experiments.Value, options.DeploymentConfig.Experimental.Value)
experiments := initExperiments(
options.Logger, options.DeploymentValues.Experiments.Value(),
)
if options.AppHostname != "" && options.AppHostnameRegex == nil || options.AppHostname == "" && options.AppHostnameRegex != nil {
panic("coderd: both AppHostname and AppHostnameRegex must be set or unset")
}
@ -267,7 +269,7 @@ func New(options *Options) *API {
options.AccessURL,
options.Authorizer,
options.Database,
options.DeploymentConfig,
options.DeploymentValues,
oauthConfigs,
options.AppSigningKey,
),
@ -292,7 +294,7 @@ func New(options *Options) *API {
DB: options.Database,
OAuth2Configs: oauthConfigs,
RedirectToLogin: false,
DisableSessionExpiryRefresh: options.DeploymentConfig.DisableSessionExpiryRefresh.Value,
DisableSessionExpiryRefresh: options.DeploymentValues.DisableSessionExpiryRefresh.Value(),
Optional: false,
})
// Same as above but it redirects to the login page.
@ -300,7 +302,7 @@ func New(options *Options) *API {
DB: options.Database,
OAuth2Configs: oauthConfigs,
RedirectToLogin: true,
DisableSessionExpiryRefresh: options.DeploymentConfig.DisableSessionExpiryRefresh.Value,
DisableSessionExpiryRefresh: options.DeploymentValues.DisableSessionExpiryRefresh.Value(),
Optional: false,
})
@ -397,7 +399,7 @@ func New(options *Options) *API {
r.Get("/updatecheck", api.updateCheck)
r.Route("/config", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Get("/deployment", api.deploymentConfig)
r.Get("/deployment", api.deploymentValues)
})
r.Route("/audit", func(r chi.Router) {
r.Use(
@ -853,7 +855,7 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, debounce ti
}
// nolint:revive
func initExperiments(log slog.Logger, raw []string, legacyAll bool) codersdk.Experiments {
func initExperiments(log slog.Logger, raw []string) codersdk.Experiments {
exps := make([]codersdk.Experiment, 0, len(raw))
for _, v := range raw {
switch v {
@ -867,11 +869,5 @@ func initExperiments(log slog.Logger, raw []string, legacyAll bool) codersdk.Exp
exps = append(exps, ex)
}
}
// --experiments takes precedence over --experimental. It's deprecated.
if legacyAll && len(raw) == 0 {
log.Warn(context.Background(), "--experimental is deprecated, use --experiments='*' instead")
exps = append(exps, codersdk.ExperimentsAll...)
}
return exps
}

View File

@ -792,7 +792,7 @@ func randomRBACType() string {
rbac.ResourceOrganizationMember.Type,
rbac.ResourceWildcard.Type,
rbac.ResourceLicense.Type,
rbac.ResourceDeploymentConfig.Type,
rbac.ResourceDeploymentValues.Type,
rbac.ResourceReplicas.Type,
rbac.ResourceDebugInfo.Type,
}

View File

@ -38,7 +38,6 @@ import (
"github.com/moby/moby/pkg/namesgenerator"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
@ -53,8 +52,6 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/autobuild/executor"
@ -118,7 +115,7 @@ type Options struct {
IncludeProvisionerDaemon bool
MetricsCacheRefreshInterval time.Duration
AgentStatsRefreshInterval time.Duration
DeploymentConfig *codersdk.DeploymentConfig
DeploymentValues *codersdk.DeploymentValues
// Set update check options to enable update check.
UpdateCheckOptions *updatecheck.Options
@ -196,8 +193,8 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
}
options.Database = dbauthz.New(options.Database, options.Authorizer, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
}
if options.DeploymentConfig == nil {
options.DeploymentConfig = DeploymentConfig(t)
if options.DeploymentValues == nil {
options.DeploymentValues = DeploymentValues(t)
}
// If no ratelimits are set, disable all rate limiting for tests.
@ -332,7 +329,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
},
MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval,
AgentStatsRefreshInterval: options.AgentStatsRefreshInterval,
DeploymentConfig: options.DeploymentConfig,
DeploymentValues: options.DeploymentValues,
UpdateCheckOptions: options.UpdateCheckOptions,
SwaggerEndpoint: options.SwaggerEndpoint,
AppSigningKey: AppSigningKey,
@ -1068,12 +1065,10 @@ sz9Di8sGIaUbLZI2rd0CQQCzlVwEtRtoNCyMJTTrkgUuNufLP19RZ5FpyXxBO5/u
QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8
-----END RSA PRIVATE KEY-----`
func DeploymentConfig(t *testing.T) *codersdk.DeploymentConfig {
vip := deployment.NewViper()
fs := pflag.NewFlagSet(randomUsername(), pflag.ContinueOnError)
fs.String(config.FlagName, randomUsername(), randomUsername())
cfg, err := deployment.Config(fs, vip)
func DeploymentValues(t *testing.T) *codersdk.DeploymentValues {
var cfg codersdk.DeploymentValues
opts := cfg.Options()
err := opts.SetDefaults()
require.NoError(t, err)
return cfg
return &cfg
}

View File

@ -287,14 +287,14 @@ func (q *querier) InsertLicense(ctx context.Context, arg database.InsertLicenseP
}
func (q *querier) InsertOrUpdateLogoURL(ctx context.Context, value string) error {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceDeploymentConfig); err != nil {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceDeploymentValues); err != nil {
return err
}
return q.db.InsertOrUpdateLogoURL(ctx, value)
}
func (q *querier) InsertOrUpdateServiceBanner(ctx context.Context, value string) error {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceDeploymentConfig); err != nil {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceDeploymentValues); err != nil {
return err
}
return q.db.InsertOrUpdateServiceBanner(ctx, value)

View File

@ -313,10 +313,10 @@ func (s *MethodTestSuite) TestLicense() {
Asserts(rbac.ResourceLicense, rbac.ActionCreate)
}))
s.Run("InsertOrUpdateLogoURL", s.Subtest(func(db database.Store, check *expects) {
check.Args("value").Asserts(rbac.ResourceDeploymentConfig, rbac.ActionCreate)
check.Args("value").Asserts(rbac.ResourceDeploymentValues, rbac.ActionCreate)
}))
s.Run("InsertOrUpdateServiceBanner", s.Subtest(func(db database.Store, check *expects) {
check.Args("value").Asserts(rbac.ResourceDeploymentConfig, rbac.ActionCreate)
check.Args("value").Asserts(rbac.ResourceDeploymentValues, rbac.ActionCreate)
}))
s.Run("GetLicenseByID", s.Subtest(func(db database.Store, check *expects) {
l, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{

View File

@ -271,7 +271,7 @@ func splitResp(t *testing.T, values []reflect.Value) ([]reflect.Value, error) {
return outputs, err
}
outputs = append(outputs, r)
} //nolint: unreachable
}
t.Fatal("no expected error value found in responses (error can be nil)")
return nil, nil // unreachable, required to compile
}

View File

@ -5,6 +5,7 @@ import (
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
)
// @Summary Get deployment config
@ -14,11 +15,23 @@ import (
// @Tags General
// @Success 200 {object} codersdk.DeploymentConfig
// @Router /config/deployment [get]
func (api *API) deploymentConfig(rw http.ResponseWriter, r *http.Request) {
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentConfig) {
func (api *API) deploymentValues(rw http.ResponseWriter, r *http.Request) {
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentValues) {
httpapi.Forbidden(rw)
return
}
httpapi.Write(r.Context(), rw, http.StatusOK, api.DeploymentConfig)
values, err := api.DeploymentValues.WithoutSecrets()
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(
r.Context(), rw, http.StatusOK,
codersdk.DeploymentConfig{
Values: values,
Options: values.Options(),
},
)
}

View File

@ -10,31 +10,31 @@ import (
"github.com/coder/coder/testutil"
)
func TestDeploymentConfig(t *testing.T) {
func TestDeploymentValues(t *testing.T) {
t.Parallel()
hi := "hi"
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
cfg := coderdtest.DeploymentConfig(t)
cfg := coderdtest.DeploymentValues(t)
// values should be returned
cfg.AccessURL.Value = hi
cfg.BrowserOnly = true
// values should not be returned
cfg.OAuth2.Github.ClientSecret.Value = hi
cfg.OIDC.ClientSecret.Value = hi
cfg.PostgresURL.Value = hi
cfg.SCIMAPIKey.Value = hi
cfg.OAuth2.Github.ClientSecret.Set(hi)
cfg.OIDC.ClientSecret.Set(hi)
cfg.PostgresURL.Set(hi)
cfg.SCIMAPIKey.Set(hi)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: cfg,
DeploymentValues: cfg,
})
_ = coderdtest.CreateFirstUser(t, client)
scrubbed, err := client.DeploymentConfig(ctx)
scrubbed, err := client.DeploymentValues(ctx)
require.NoError(t, err)
// ensure normal values pass through
require.EqualValues(t, hi, scrubbed.AccessURL.Value)
require.EqualValues(t, true, scrubbed.Values.BrowserOnly.Value())
// ensure secrets are removed
require.Empty(t, scrubbed.OAuth2.Github.ClientSecret.Value)
require.Empty(t, scrubbed.OIDC.ClientSecret.Value)
require.Empty(t, scrubbed.PostgresURL.Value)
require.Empty(t, scrubbed.SCIMAPIKey.Value)
require.Empty(t, scrubbed.Values.OAuth2.Github.ClientSecret.Value())
require.Empty(t, scrubbed.Values.OIDC.ClientSecret.Value())
require.Empty(t, scrubbed.Values.PostgresURL.Value())
require.Empty(t, scrubbed.Values.SCIMAPIKey.Value())
}

View File

@ -16,9 +16,9 @@ func Test_Experiments(t *testing.T) {
t.Parallel()
t.Run("empty", func(t *testing.T) {
t.Parallel()
cfg := coderdtest.DeploymentConfig(t)
cfg := coderdtest.DeploymentValues(t)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: cfg,
DeploymentValues: cfg,
})
_ = coderdtest.CreateFirstUser(t, client)
@ -34,10 +34,10 @@ func Test_Experiments(t *testing.T) {
t.Run("multiple features", func(t *testing.T) {
t.Parallel()
cfg := coderdtest.DeploymentConfig(t)
cfg.Experiments.Value = []string{"foo", "BAR"}
cfg := coderdtest.DeploymentValues(t)
cfg.Experiments = []string{"foo", "BAR"}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: cfg,
DeploymentValues: cfg,
})
_ = coderdtest.CreateFirstUser(t, client)
@ -56,10 +56,10 @@ func Test_Experiments(t *testing.T) {
t.Run("wildcard", func(t *testing.T) {
t.Parallel()
cfg := coderdtest.DeploymentConfig(t)
cfg.Experiments.Value = []string{"*"}
cfg := coderdtest.DeploymentValues(t)
cfg.Experiments = []string{"*"}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: cfg,
DeploymentValues: cfg,
})
_ = coderdtest.CreateFirstUser(t, client)
@ -78,10 +78,10 @@ func Test_Experiments(t *testing.T) {
t.Run("alternate wildcard with manual opt-in", func(t *testing.T) {
t.Parallel()
cfg := coderdtest.DeploymentConfig(t)
cfg.Experiments.Value = []string{"*", "dAnGeR"}
cfg := coderdtest.DeploymentValues(t)
cfg.Experiments = []string{"*", "dAnGeR"}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: cfg,
DeploymentValues: cfg,
})
_ = coderdtest.CreateFirstUser(t, client)
@ -99,34 +99,12 @@ func Test_Experiments(t *testing.T) {
require.False(t, experiments.Enabled("herebedragons"))
})
t.Run("legacy wildcard", func(t *testing.T) {
t.Parallel()
cfg := coderdtest.DeploymentConfig(t)
cfg.Experimental.Value = true
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: cfg,
})
_ = coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
experiments, err := client.Experiments(ctx)
require.NoError(t, err)
require.NotNil(t, experiments)
require.ElementsMatch(t, codersdk.ExperimentsAll, experiments)
for _, ex := range codersdk.ExperimentsAll {
require.True(t, experiments.Enabled(ex))
}
require.False(t, experiments.Enabled("danger"))
})
t.Run("Unauthorized", func(t *testing.T) {
t.Parallel()
cfg := coderdtest.DeploymentConfig(t)
cfg.Experiments.Value = []string{"*"}
cfg := coderdtest.DeploymentValues(t)
cfg.Experiments = []string{"*"}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: cfg,
DeploymentValues: cfg,
})
// Explicitly omit creating a user so we're unauthorized.
// _ = coderdtest.CreateFirstUser(t, client)

View File

@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"net/http"
"reflect"
@ -114,6 +115,10 @@ func Write(ctx context.Context, rw http.ResponseWriter, status int, response int
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(true)
// Pretty up JSON when testing.
if flag.Lookup("test.v") != nil {
enc.SetIndent("", "\t")
}
err := enc.Encode(response)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)

View File

@ -17,7 +17,7 @@ import (
// @Router /insights/daus [get]
func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentConfig) {
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentValues) {
httpapi.Forbidden(rw)
return
}

View File

@ -265,7 +265,10 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
}
}
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error reading job agent.",

View File

@ -59,7 +59,7 @@ type authorizeCache struct {
calls []cachedAuthCall
}
//nolint:error-return,revive
//nolint:revive
func (c *authorizeCache) Load(subject Subject, action Action, object Object) (error, bool) {
if c == nil {
return nil, false

View File

@ -142,8 +142,8 @@ var (
Type: "license",
}
// ResourceDeploymentConfig
ResourceDeploymentConfig = Object{
// ResourceDeploymentValues
ResourceDeploymentValues = Object{
Type: "deployment_config",
}

View File

@ -89,7 +89,7 @@ func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
// If password authentication is disabled and the user does not have the
// owner role, block the request.
if api.DeploymentConfig.DisablePasswordAuth.Value {
if api.DeploymentValues.DisablePasswordAuth {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: "Password authentication is disabled.",
})
@ -285,7 +285,7 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AuthMethods{
Password: codersdk.AuthMethod{
Enabled: !api.DeploymentConfig.DisablePasswordAuth.Value,
Enabled: !api.DeploymentValues.DisablePasswordAuth.Value(),
},
Github: codersdk.AuthMethod{Enabled: api.GithubOAuth2Config != nil},
OIDC: codersdk.OIDCAuthMethod{

View File

@ -298,7 +298,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
// If password auth is disabled, don't allow new users to be
// created with a password!
if api.DeploymentConfig.DisablePasswordAuth.Value {
if api.DeploymentValues.DisablePasswordAuth {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: "You cannot manually provision new users with password authentication disabled!",
})

View File

@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
@ -186,9 +187,9 @@ func TestPostLogin(t *testing.T) {
t.Run("DisabledPasswordAuth", func(t *testing.T) {
t.Parallel()
dc := coderdtest.DeploymentConfig(t)
dc := coderdtest.DeploymentValues(t)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: dc,
DeploymentValues: dc,
})
first := coderdtest.CreateFirstUser(t, client)
@ -206,7 +207,7 @@ func TestPostLogin(t *testing.T) {
})
require.NoError(t, err)
dc.DisablePasswordAuth.Value = true
dc.DisablePasswordAuth = clibase.Bool(true)
userClient := codersdk.New(client.URL)
_, err = userClient.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{

View File

@ -61,7 +61,10 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
})
return
}
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error reading workspace agent.",
@ -83,7 +86,10 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error reading workspace agent.",
@ -171,7 +177,10 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
func (api *API) postWorkspaceAgentStartup(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error reading workspace agent.",
@ -234,7 +243,11 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
httpapi.ResourceNotFound(rw)
return
}
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error reading workspace agent.",
@ -315,7 +328,10 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
return
}
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error reading workspace agent.",

View File

@ -84,7 +84,7 @@ func (api *API) appHost(rw http.ResponseWriter, r *http.Request) {
// workspaceAppsProxyPath proxies requests to a workspace application
// through a relative URL path.
func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) {
if api.DeploymentConfig.DisablePathApps.Value {
if api.DeploymentValues.DisablePathApps.Value() {
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusUnauthorized,
Title: "Unauthorized",
@ -106,7 +106,7 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
OIDC: api.OIDCConfig,
},
RedirectToLogin: true,
DisableSessionExpiryRefresh: api.DeploymentConfig.DisableSessionExpiryRefresh.Value,
DisableSessionExpiryRefresh: api.DeploymentValues.DisableSessionExpiryRefresh.Value(),
})
if !ok {
return
@ -340,9 +340,9 @@ func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request
// the current session.
exp := apiKey.ExpiresAt
lifetimeSeconds := apiKey.LifetimeSeconds
if exp.IsZero() || time.Until(exp) > api.DeploymentConfig.SessionDuration.Value {
exp = database.Now().Add(api.DeploymentConfig.SessionDuration.Value)
lifetimeSeconds = int64(api.DeploymentConfig.SessionDuration.Value.Seconds())
if exp.IsZero() || time.Until(exp) > api.DeploymentValues.SessionDuration.Value() {
exp = database.Now().Add(api.DeploymentValues.SessionDuration.Value())
lifetimeSeconds = int64(api.DeploymentValues.SessionDuration.Value().Seconds())
}
cookie, _, err := api.createAPIKey(ctx, createAPIKeyParams{
UserID: apiKey.UserID,

View File

@ -91,7 +91,7 @@ func (p *Provider) ResolveRequest(rw http.ResponseWriter, r *http.Request, appRe
DB: p.Database,
OAuth2Configs: p.OAuth2Configs,
RedirectToLogin: false,
DisableSessionExpiryRefresh: p.DeploymentConfig.DisableSessionExpiryRefresh.Value,
DisableSessionExpiryRefresh: p.DeploymentValues.DisableSessionExpiryRefresh.Value(),
// Optional is true to allow for public apps. If an authorization check
// fails and the user is not authenticated, they will be redirected to
// the login page using code below (not the redirect from the
@ -358,7 +358,7 @@ func (p *Provider) authorizeWorkspaceApp(ctx context.Context, roles *httpmw.Auth
//
// Site owners are blocked from accessing path-based apps unless the
// Dangerous.AllowPathAppSiteOwnerAccess flag is enabled in the check below.
if isPathApp && !p.DeploymentConfig.Dangerous.AllowPathAppSharing.Value {
if isPathApp && !p.DeploymentValues.Dangerous.AllowPathAppSharing.Value() {
sharingLevel = database.AppSharingLevelOwner
}
@ -379,7 +379,7 @@ func (p *Provider) authorizeWorkspaceApp(ctx context.Context, roles *httpmw.Auth
if isPathApp &&
sharingLevel == database.AppSharingLevelOwner &&
workspace.OwnerID.String() != roles.Actor.ID &&
!p.DeploymentConfig.Dangerous.AllowPathAppSiteOwnerAccess.Value {
!p.DeploymentValues.Dangerous.AllowPathAppSiteOwnerAccess.Value() {
return false, nil
}

View File

@ -43,13 +43,13 @@ func Test_ResolveRequest(t *testing.T) {
)
allApps := []string{appNameOwner, appNameAuthed, appNamePublic}
deploymentConfig := coderdtest.DeploymentConfig(t)
deploymentConfig.DisablePathApps.Value = false
deploymentConfig.Dangerous.AllowPathAppSharing.Value = true
deploymentConfig.Dangerous.AllowPathAppSiteOwnerAccess.Value = true
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.DisablePathApps = false
deploymentValues.Dangerous.AllowPathAppSharing = true
deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = true
client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
DeploymentConfig: deploymentConfig,
DeploymentValues: deploymentValues,
IncludeProvisionerDaemon: true,
AgentStatsRefreshInterval: time.Millisecond * 100,
MetricsCacheRefreshInterval: time.Millisecond * 100,

View File

@ -19,12 +19,12 @@ type Provider struct {
AccessURL *url.URL
Authorizer rbac.Authorizer
Database database.Store
DeploymentConfig *codersdk.DeploymentConfig
DeploymentValues *codersdk.DeploymentValues
OAuth2Configs *httpmw.OAuth2Configs
TicketSigningKey []byte
}
func New(log slog.Logger, accessURL *url.URL, authz rbac.Authorizer, db database.Store, cfg *codersdk.DeploymentConfig, oauth2Cfgs *httpmw.OAuth2Configs, ticketSigningKey []byte) *Provider {
func New(log slog.Logger, accessURL *url.URL, authz rbac.Authorizer, db database.Store, cfg *codersdk.DeploymentValues, oauth2Cfgs *httpmw.OAuth2Configs, ticketSigningKey []byte) *Provider {
if len(ticketSigningKey) != 64 {
panic("ticket signing key must be 64 bytes")
}
@ -34,7 +34,7 @@ func New(log slog.Logger, accessURL *url.URL, authz rbac.Authorizer, db database
AccessURL: accessURL,
Authorizer: authz,
Database: db,
DeploymentConfig: cfg,
DeploymentValues: cfg,
OAuth2Configs: oauth2Cfgs,
TicketSigningKey: ticketSigningKey,
}

View File

@ -23,6 +23,7 @@ import (
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/httpapi"
@ -168,13 +169,13 @@ func setupProxyTest(t *testing.T, opts *setupProxyTestOpts) (*codersdk.Client, c
})
go server.Serve(ln)
deploymentConfig := coderdtest.DeploymentConfig(t)
deploymentConfig.DisablePathApps.Value = opts.DisablePathApps
deploymentConfig.Dangerous.AllowPathAppSharing.Value = opts.DangerousAllowPathAppSharing
deploymentConfig.Dangerous.AllowPathAppSiteOwnerAccess.Value = opts.DangerousAllowPathAppSiteOwnerAccess
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.DisablePathApps = clibase.Bool(opts.DisablePathApps)
deploymentValues.Dangerous.AllowPathAppSharing = clibase.Bool(opts.DangerousAllowPathAppSharing)
deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = clibase.Bool(opts.DangerousAllowPathAppSiteOwnerAccess)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: deploymentConfig,
DeploymentValues: deploymentValues,
AppHostname: opts.AppHost,
IncludeProvisionerDaemon: true,
AgentStatsRefreshInterval: time.Millisecond * 100,
@ -307,11 +308,11 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
t.Run("Disabled", func(t *testing.T) {
t.Parallel()
deploymentConfig := coderdtest.DeploymentConfig(t)
deploymentConfig.DisablePathApps.Value = true
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.DisablePathApps = true
client := coderdtest.New(t, &coderdtest.Options{
DeploymentConfig: deploymentConfig,
DeploymentValues: deploymentValues,
IncludeProvisionerDaemon: true,
AgentStatsRefreshInterval: time.Millisecond * 100,
MetricsCacheRefreshInterval: time.Millisecond * 100,
@ -1435,11 +1436,11 @@ func TestAppSharing(t *testing.T) {
siteOwnerCanAccess := !isPathApp || siteOwnerPathAppAccessEnabled
siteOwnerCanAccessShared := siteOwnerCanAccess || pathAppSharingEnabled
deploymentConfig, err := ownerClient.DeploymentConfig(context.Background())
deploymentValues, err := ownerClient.DeploymentValues(context.Background())
require.NoError(t, err)
assert.Equal(t, pathAppSharingEnabled, deploymentConfig.Dangerous.AllowPathAppSharing.Value)
assert.Equal(t, siteOwnerPathAppAccessEnabled, deploymentConfig.Dangerous.AllowPathAppSiteOwnerAccess.Value)
assert.Equal(t, pathAppSharingEnabled, deploymentValues.Values.Dangerous.AllowPathAppSharing.Value())
assert.Equal(t, siteOwnerPathAppAccessEnabled, deploymentValues.Values.Dangerous.AllowPathAppSiteOwnerAccess.Value())
t.Run("LevelOwner", func(t *testing.T) {
t.Parallel()

View File

@ -1122,7 +1122,10 @@ func (api *API) convertWorkspaceBuild(
apiAgents := make([]codersdk.WorkspaceAgent, 0)
for _, agent := range agents {
apps := appsByAgentID[agent.ID]
apiAgent, err := convertWorkspaceAgent(api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(apps), api.AgentInactiveDisconnectTimeout, api.DeploymentConfig.AgentFallbackTroubleshootingURL.Value)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(apps), api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
return codersdk.WorkspaceBuild{}, xerrors.Errorf("converting workspace agent: %w", err)
}

File diff suppressed because it is too large Load Diff

107
codersdk/deployment_test.go Normal file
View File

@ -0,0 +1,107 @@
package codersdk_test
import (
"testing"
"github.com/coder/coder/codersdk"
)
type exclusion struct {
flag bool
env bool
yaml bool
}
func TestDeploymentValues_HighlyConfigurable(t *testing.T) {
t.Parallel()
// This test ensures that every deployment option has
// a corresponding Flag, Env, and YAML name, unless explicitly excluded.
excludes := map[string]exclusion{
// These are used to configure YAML support itself, so
// they make no sense within the YAML file.
"Config Path": {
yaml: true,
},
"Write Config": {
yaml: true,
},
// Dangerous values? Not sure we should help users
// persistent their configuration.
"DANGEROUS: Allow Path App Sharing": {
yaml: true,
},
"DANGEROUS: Allow Site Owners to Access Path Apps": {
yaml: true,
},
// Secrets
"Trace Honeycomb API Key": {
yaml: true,
},
"OAuth2 GitHub Client Secret": {
yaml: true,
},
"OIDC Client Secret": {
yaml: true,
},
"Postgres Connection URL": {
yaml: true,
},
"SCIM API Key": {
yaml: true,
},
// These complex objects should be configured through YAML.
"Support Links": {
flag: true,
env: true,
},
"Git Auth Providers": {
// Technically Git Auth Providers can be provided through the env,
// but bypassing clibase. See cli.ReadGitAuthProvidersFromEnv.
flag: true,
env: true,
},
}
set := (&codersdk.DeploymentValues{}).Options()
for _, opt := range set {
// These are generally for development, so their configurability is
// not relevant.
if opt.Hidden {
delete(excludes, opt.Name)
continue
}
if codersdk.IsSecretDeploymentOption(opt) && opt.YAML != "" {
// Secrets should not be written to YAML and instead should continue
// to be read from the environment.
//
// Unfortunately, secrets are still accepted through flags for
// legacy purposes. Eventually, we should prevent that.
t.Errorf("Option %q is a secret but has a YAML name", opt.Name)
}
excluded := excludes[opt.Name]
switch {
case opt.YAML == "" && !excluded.yaml:
t.Errorf("Option %q should have a YAML name", opt.Name)
case opt.YAML != "" && excluded.yaml:
t.Errorf("Option %q is excluded but has a YAML name", opt.Name)
case opt.Flag == "" && !excluded.flag:
t.Errorf("Option %q should have a flag name", opt.Name)
case opt.Flag != "" && excluded.flag:
t.Errorf("Option %q is excluded but has a flag name", opt.Name)
case opt.Env == "" && !excluded.env:
t.Errorf("Option %q should have an env name", opt.Name)
case opt.Env != "" && excluded.env:
t.Errorf("Option %q is excluded but has an env name", opt.Name)
}
delete(excludes, opt.Name)
}
for opt := range excludes {
t.Errorf("Excluded option %q is not in the deployment config. Remove it?", opt)
}
}

View File

@ -119,7 +119,6 @@ curl -X GET http://coder-server:8080/api/v2/entitlements \
```json
{
"errors": ["string"],
"experimental": true,
"features": {
"property1": {
"actual": 0,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -62,6 +62,5 @@ func TestFeaturesList(t *testing.T) {
assert.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
assert.False(t, entitlements.HasLicense)
assert.False(t, entitlements.Experimental)
})
}

View File

@ -348,9 +348,8 @@ func (*fakeLicenseAPI) entitlements(rw http.ResponseWriter, r *http.Request) {
}
}
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.Entitlements{
Features: features,
Warnings: []string{testWarning},
HasLicense: true,
Experimental: true,
Features: features,
Warnings: []string{testWarning},
HasLicense: true,
})
}

View File

@ -15,7 +15,6 @@ import (
agpl "github.com/coder/coder/cli"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/terraform"
@ -149,7 +148,7 @@ func provisionerDaemonStart() *cobra.Command {
},
}
cliflag.StringVarP(cmd.Flags(), &cacheDir, "cache-dir", "c", "CODER_CACHE_DIRECTORY", deployment.DefaultCacheDir(),
cliflag.StringVarP(cmd.Flags(), &cacheDir, "cache-dir", "c", "CODER_CACHE_DIRECTORY", codersdk.DefaultCacheDir(),
"Specify a directory to cache provisioner job files.")
cliflag.StringArrayVarP(cmd.Flags(), &rawTags, "tag", "t", "CODER_PROVISIONERD_TAGS", []string{},
"Specify a list of tags to target provisioner jobs.")

View File

@ -14,7 +14,6 @@ import (
"tailscale.com/derp"
"tailscale.com/types/key"
"github.com/coder/coder/cli/deployment"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/enterprise/audit"
"github.com/coder/coder/enterprise/audit/backends"
@ -27,10 +26,9 @@ import (
)
func server() *cobra.Command {
vip := deployment.NewViper()
cmd := agpl.Server(vip, func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, io.Closer, error) {
if options.DeploymentConfig.DERP.Server.RelayURL.Value != "" {
_, err := url.Parse(options.DeploymentConfig.DERP.Server.RelayURL.Value)
cmd := agpl.Server(func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, io.Closer, error) {
if options.DeploymentValues.DERP.Server.RelayURL.String() != "" {
_, err := url.Parse(options.DeploymentValues.DERP.Server.RelayURL.String())
if err != nil {
return nil, nil, xerrors.Errorf("derp-server-relay-address must be a valid HTTP URL: %w", err)
}
@ -53,7 +51,7 @@ func server() *cobra.Command {
}
options.DERPServer.SetMeshKey(meshKey)
if options.DeploymentConfig.AuditLogging.Value {
if options.DeploymentValues.AuditLogging.Value() {
options.Auditor = audit.NewAuditor(audit.DefaultFilter,
backends.NewPostgres(options.Database, true),
backends.NewSlog(options.Logger),
@ -63,14 +61,13 @@ func server() *cobra.Command {
options.TrialGenerator = trialer.New(options.Database, "https://v2-licensor.coder.com/trial", coderd.Keys)
o := &coderd.Options{
AuditLogging: options.DeploymentConfig.AuditLogging.Value,
BrowserOnly: options.DeploymentConfig.BrowserOnly.Value,
SCIMAPIKey: []byte(options.DeploymentConfig.SCIMAPIKey.Value),
AuditLogging: options.DeploymentValues.AuditLogging.Value(),
BrowserOnly: options.DeploymentValues.BrowserOnly.Value(),
SCIMAPIKey: []byte(options.DeploymentValues.SCIMAPIKey.Value()),
RBAC: true,
DERPServerRelayAddress: options.DeploymentConfig.DERP.Server.RelayURL.Value,
DERPServerRegionID: options.DeploymentConfig.DERP.Server.RegionID.Value,
Options: options,
DERPServerRelayAddress: options.DeploymentValues.DERP.Server.RelayURL.String(),
DERPServerRegionID: int(options.DeploymentValues.DERP.Server.RegionID.Value()),
Options: options,
}
api, err := coderd.New(ctx, o)
@ -79,8 +76,5 @@ func server() *cobra.Command {
}
return api.AGPL, api, nil
})
deployment.AttachFlags(cmd.Flags(), vip, true)
return cmd
}

View File

@ -6,22 +6,16 @@ import (
"context"
"io"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/deployment"
agpl "github.com/coder/coder/cli"
agplcoderd "github.com/coder/coder/coderd"
"github.com/spf13/cobra"
)
func server() *cobra.Command {
vip := deployment.NewViper()
cmd := agpl.Server(vip, func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, io.Closer, error) {
cmd := agpl.Server(func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, io.Closer, error) {
return nil, nil, xerrors.Errorf("slim build does not support `coder server`")
})
deployment.AttachFlags(cmd.Flags(), vip, true)
return cmd
}

View File

@ -87,10 +87,10 @@ func (api *API) appearance(rw http.ResponseWriter, r *http.Request) {
}
}
if len(api.DeploymentConfig.Support.Links.Value) == 0 {
if len(api.DeploymentValues.Support.Links.Value) == 0 {
cfg.SupportLinks = DefaultSupportLinks
} else {
cfg.SupportLinks = api.DeploymentConfig.Support.Links.Value
cfg.SupportLinks = api.DeploymentValues.Support.Links.Value
}
httpapi.Write(r.Context(), rw, http.StatusOK, cfg)
@ -119,7 +119,7 @@ func validateHexColor(color string) error {
func (api *API) putAppearance(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceDeploymentConfig) {
if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceDeploymentValues) {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: "Insufficient permissions to update appearance",
})

View File

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd"
@ -89,15 +90,14 @@ func TestCustomSupportLinks(t *testing.T) {
Icon: "bug",
},
}
cfg := coderdtest.DeploymentConfig(t)
cfg.Support = new(codersdk.SupportConfig)
cfg.Support.Links = &codersdk.DeploymentConfigField[[]codersdk.LinkConfig]{
cfg := coderdtest.DeploymentValues(t)
cfg.Support.Links = clibase.Struct[[]codersdk.LinkConfig]{
Value: supportLinks,
}
client := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentConfig: cfg,
DeploymentValues: cfg,
},
})
coderdtest.CreateFirstUser(t, client)

View File

@ -259,7 +259,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
return err
}
if entitlements.RequireTelemetry && !api.DeploymentConfig.Telemetry.Enable.Value {
if entitlements.RequireTelemetry && !api.DeploymentValues.Telemetry.Enable.Value() {
// We can't fail because then the user couldn't remove the offending
// license w/o a restart.
//
@ -272,8 +272,6 @@ func (api *API) updateEntitlements(ctx context.Context) error {
return nil
}
entitlements.Experimental = api.DeploymentConfig.Experimental.Value || len(api.AGPL.Experiments) != 0
featureChanged := func(featureName codersdk.FeatureName) (changed bool, enabled bool) {
if api.entitlements.Features == nil {
return true, entitlements.Features[featureName].Enabled

9
go.mod
View File

@ -55,7 +55,7 @@ replace github.com/gliderlabs/ssh => github.com/coder/ssh v0.0.0-20220811105153-
replace github.com/imulab/go-scim/pkg/v2 => github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136
require (
cdr.dev/slog v1.4.2-0.20220525200111-18dce5c2cd5f
cdr.dev/slog v1.4.2-0.20230228204227-60d22dceaf04
cloud.google.com/go/compute/metadata v0.2.1
github.com/AlecAivazis/survey/v2 v2.3.5
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
@ -134,7 +134,6 @@ require (
github.com/spf13/afero v1.9.3
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.14.0
github.com/stretchr/testify v1.8.1
github.com/swaggo/http-swagger v1.3.3
github.com/swaggo/swag v1.8.6
@ -228,7 +227,6 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/elastic/go-windows v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-gonic/gin v1.7.7 // indirect
@ -265,6 +263,7 @@ require (
github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.20.0 // indirect
github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect
github.com/iancoleman/strcase v0.2.0
github.com/illarion/gonotify v1.0.1 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@ -278,7 +277,6 @@ require (
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
@ -303,7 +301,6 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pion/transport v0.13.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
@ -317,7 +314,6 @@ require (
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d // indirect
@ -355,7 +351,6 @@ require (
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
howett.net/plist v1.0.0 // indirect
inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect

19
go.sum
View File

@ -2,8 +2,8 @@
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM=
bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M=
cdr.dev/slog v1.4.2-0.20220525200111-18dce5c2cd5f h1:3bol05M9G8jeze6ylp+oReemDB0eeahsETzFVND0S3U=
cdr.dev/slog v1.4.2-0.20220525200111-18dce5c2cd5f/go.mod h1:wRFV/Qp1sEyUTLuhv8k/v97zRIpfGVR1mpWUmAOwREk=
cdr.dev/slog v1.4.2-0.20230228204227-60d22dceaf04 h1:d5MQ+iI2zk7t0HrHwBP9p7k2XfRsXnRclSe8Kpp3xOo=
cdr.dev/slog v1.4.2-0.20230228204227-60d22dceaf04/go.mod h1:YPVZsUbRMaLaPgme0RzlPWlC7fI7YmDj/j/kZLuvICs=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@ -179,7 +179,6 @@ github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVb
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alecthomas/chroma v0.9.4/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -627,8 +626,6 @@ github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0X
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
@ -1088,6 +1085,7 @@ github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -1306,8 +1304,6 @@ github.com/mafredri/udp v0.1.2-0.20220805105907-b2872e92e98d/go.mod h1:GUd681aT3
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -1576,8 +1572,6 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@ -1777,8 +1771,6 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4=
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
@ -1805,8 +1797,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggest/assertjson v1.7.0 h1:SKw5Rn0LQs6UvmGrIdaKQbMR1R3ncXm5KNon+QJ7jtw=
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc=
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
@ -2431,7 +2421,6 @@ golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
@ -2844,8 +2833,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

View File

@ -31,6 +31,8 @@ func New(t *testing.T, opts ...pty.Option) *PTY {
return create(t, ptty, "cmd")
}
// Start starts a new process asynchronously and returns a PTY and Process.
// It kills the process upon cleanup.
func Start(t *testing.T, cmd *exec.Cmd, opts ...pty.StartOption) (*PTY, pty.Process) {
t.Helper()

View File

@ -710,6 +710,8 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
return TypescriptType{ValueType: "string"}, nil
case "encoding/json.RawMessage":
return TypescriptType{ValueType: "Record<string, string>"}, nil
case "github.com/coder/coder/cli/clibase.URL":
return TypescriptType{ValueType: "string"}, nil
}
// Then see if the type is defined elsewhere. If it is, we can just

View File

@ -140,14 +140,19 @@ func fmtDocFilename(cmd *cobra.Command) string {
return fmt.Sprintf("%s.md", name)
}
func generateDocsTree(rootCmd *cobra.Command, basePath string) error {
if rootCmd.Hidden {
func generateDocsTree(cmd *cobra.Command, basePath string) error {
if cmd.Hidden {
return nil
}
if cmd.Name() == "server" {
// The server command is now managed by clibase and needs a new generator.
return nil
}
// Write out root.
fi, err := os.OpenFile(
filepath.Join(basePath, fmtDocFilename(rootCmd)),
filepath.Join(basePath, fmtDocFilename(cmd)),
os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644,
)
if err != nil {
@ -155,15 +160,15 @@ func generateDocsTree(rootCmd *cobra.Command, basePath string) error {
}
defer fi.Close()
err = writeCommand(fi, rootCmd)
err = writeCommand(fi, cmd)
if err != nil {
return err
}
flog.Info("Generated docs for %q at %v", fullCommandName(rootCmd), fi.Name())
flog.Info("Generated docs for %q at %v", fullCommandName(cmd), fi.Name())
// Recursively generate docs.
for _, subcommand := range rootCmd.Commands() {
for _, subcommand := range cmd.Commands() {
err = generateDocsTree(subcommand, basePath)
if err != nil {
return err

View File

@ -1,6 +1,7 @@
import axios, { AxiosRequestHeaders } from "axios"
import dayjs from "dayjs"
import * as Types from "./types"
import { DeploymentConfig } from "./types"
import * as TypesGen from "./typesGenerated"
export const hardCodedCSRFCookie = (): string => {
@ -684,7 +685,6 @@ export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
if (axios.isAxiosError(ex) && ex.response?.status === 404) {
return {
errors: [],
experimental: false,
features: withDefaultFeatures({}),
has_license: false,
require_telemetry: false,
@ -806,11 +806,10 @@ export const getAgentListeningPorts = async (
return response.data
}
export const getDeploymentConfig =
async (): Promise<TypesGen.DeploymentConfig> => {
const response = await axios.get(`/api/v2/config/deployment`)
return response.data
}
export const getDeploymentValues = async (): Promise<DeploymentConfig> => {
const response = await axios.get(`/api/v2/config/deployment`)
return response.data
}
export const getReplicas = async (): Promise<TypesGen.Replica[]> => {
const response = await axios.get(`/api/v2/replicas`)

View File

@ -1,3 +1,5 @@
import { DeploymentValues } from "./typesGenerated"
export interface UserAgent {
readonly browser: string
readonly device: string
@ -14,3 +16,25 @@ export interface ReconnectingPTYRequest {
export type WorkspaceBuildTransition = "start" | "stop" | "delete"
export type Message = { message: string }
export interface DeploymentGroup {
readonly name: string
readonly parent?: DeploymentGroup
readonly description: string
readonly children: DeploymentGroup[]
}
export interface DeploymentOption {
readonly name: string
readonly description: string
readonly flag: string
readonly flag_shorthand: string
readonly value: unknown
readonly hidden: boolean
readonly group?: DeploymentGroup
}
export type DeploymentConfig = {
readonly config: DeploymentValues
readonly options: DeploymentOption[]
}

View File

@ -272,8 +272,10 @@ export interface DERP {
// From codersdk/deployment.go
export interface DERPConfig {
readonly url: DeploymentConfigField<string>
readonly path: DeploymentConfigField<string>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly url: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly path: string
}
// From codersdk/workspaceagents.go
@ -284,79 +286,25 @@ export interface DERPRegion {
// From codersdk/deployment.go
export interface DERPServerConfig {
readonly enable: DeploymentConfigField<boolean>
readonly region_id: DeploymentConfigField<number>
readonly region_code: DeploymentConfigField<string>
readonly region_name: DeploymentConfigField<string>
readonly stun_addresses: DeploymentConfigField<string[]>
readonly relay_url: DeploymentConfigField<string>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly enable: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Int64")
readonly region_id: number
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly region_code: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly region_name: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly stun_addresses: string[]
readonly relay_url: string
}
// From codersdk/deployment.go
export interface DangerousConfig {
readonly allow_path_app_sharing: DeploymentConfigField<boolean>
readonly allow_path_app_site_owner_access: DeploymentConfigField<boolean>
}
// From codersdk/deployment.go
export interface DeploymentConfig {
readonly access_url: DeploymentConfigField<string>
readonly wildcard_access_url: DeploymentConfigField<string>
readonly redirect_to_access_url: DeploymentConfigField<boolean>
readonly http_address: DeploymentConfigField<string>
readonly autobuild_poll_interval: DeploymentConfigField<number>
readonly derp: DERP
readonly gitauth: DeploymentConfigField<GitAuthConfig[]>
readonly prometheus: PrometheusConfig
readonly pprof: PprofConfig
readonly proxy_trusted_headers: DeploymentConfigField<string[]>
readonly proxy_trusted_origins: DeploymentConfigField<string[]>
readonly cache_directory: DeploymentConfigField<string>
readonly in_memory_database: DeploymentConfigField<boolean>
readonly pg_connection_url: DeploymentConfigField<string>
readonly oauth2: OAuth2Config
readonly oidc: OIDCConfig
readonly telemetry: TelemetryConfig
readonly tls: TLSConfig
readonly trace: TraceConfig
readonly secure_auth_cookie: DeploymentConfigField<boolean>
readonly strict_transport_security: DeploymentConfigField<number>
readonly strict_transport_security_options: DeploymentConfigField<string[]>
readonly ssh_keygen_algorithm: DeploymentConfigField<string>
readonly metrics_cache_refresh_interval: DeploymentConfigField<number>
readonly agent_stat_refresh_interval: DeploymentConfigField<number>
readonly agent_fallback_troubleshooting_url: DeploymentConfigField<string>
readonly audit_logging: DeploymentConfigField<boolean>
readonly browser_only: DeploymentConfigField<boolean>
readonly scim_api_key: DeploymentConfigField<string>
readonly provisioner: ProvisionerConfig
readonly rate_limit: RateLimitConfig
readonly experiments: DeploymentConfigField<string[]>
readonly update_check: DeploymentConfigField<boolean>
readonly max_token_lifetime: DeploymentConfigField<number>
readonly swagger: SwaggerConfig
readonly logging: LoggingConfig
readonly dangerous: DangerousConfig
readonly disable_path_apps: DeploymentConfigField<boolean>
readonly max_session_expiry: DeploymentConfigField<number>
readonly disable_session_expiry_refresh: DeploymentConfigField<boolean>
readonly disable_password_auth: DeploymentConfigField<boolean>
readonly address: DeploymentConfigField<string>
readonly experimental: DeploymentConfigField<boolean>
readonly support: SupportConfig
}
// From codersdk/deployment.go
export interface DeploymentConfigField<T extends Flaggable> {
readonly name: string
readonly usage: string
readonly flag: string
readonly shorthand: string
readonly enterprise: boolean
readonly hidden: boolean
readonly secret: boolean
readonly default: T
readonly value: T
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly allow_path_app_sharing: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly allow_path_app_site_owner_access: boolean
}
// From codersdk/deployment.go
@ -364,6 +312,87 @@ export interface DeploymentDAUsResponse {
readonly entries: DAUEntry[]
}
// From codersdk/deployment.go
export interface DeploymentValues {
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly verbose?: boolean
readonly access_url?: string
readonly wildcard_access_url?: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly redirect_to_access_url?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly http_address?: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Duration")
readonly autobuild_poll_interval?: number
readonly derp?: DERP
readonly prometheus?: PrometheusConfig
readonly pprof?: PprofConfig
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly proxy_trusted_headers?: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly proxy_trusted_origins?: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly cache_directory?: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly in_memory_database?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly pg_connection_url?: string
readonly oauth2?: OAuth2Config
readonly oidc?: OIDCConfig
readonly telemetry?: TelemetryConfig
readonly tls?: TLSConfig
readonly trace?: TraceConfig
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly secure_auth_cookie?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Int64")
readonly strict_transport_security?: number
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly strict_transport_security_options?: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly ssh_keygen_algorithm?: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Duration")
readonly metrics_cache_refresh_interval?: number
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Duration")
readonly agent_stat_refresh_interval?: number
readonly agent_fallback_troubleshooting_url?: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly audit_logging?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly browser_only?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly scim_api_key?: string
readonly provisioner?: ProvisionerConfig
readonly rate_limit?: RateLimitConfig
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly experiments?: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly update_check?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Duration")
readonly max_token_lifetime?: number
readonly swagger?: SwaggerConfig
readonly logging?: LoggingConfig
readonly dangerous?: DangerousConfig
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly disable_path_apps?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Duration")
readonly max_session_expiry?: number
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly disable_session_expiry_refresh?: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly disable_password_auth?: boolean
readonly support?: SupportConfig
// Named type "github.com/coder/coder/cli/clibase.Struct[[]github.com/coder/coder/codersdk.GitAuthConfig]" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO explain why this is needed
readonly git_auth?: any
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly config?: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly write_config?: boolean
// Named type "github.com/coder/coder/cli/clibase.HostPort" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO explain why this is needed
readonly address?: any
}
// From codersdk/deployment.go
export interface Entitlements {
readonly features: Record<FeatureName, Feature>
@ -372,7 +401,6 @@ export interface Entitlements {
readonly has_license: boolean
readonly trial: boolean
readonly require_telemetry: boolean
readonly experimental: boolean
}
// From codersdk/deployment.go
@ -453,9 +481,12 @@ export interface LinkConfig {
// From codersdk/deployment.go
export interface LoggingConfig {
readonly human: DeploymentConfigField<string>
readonly json: DeploymentConfigField<string>
readonly stackdriver: DeploymentConfigField<string>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly human: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly json: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly stackdriver: string
}
// From codersdk/users.go
@ -476,13 +507,20 @@ export interface OAuth2Config {
// From codersdk/deployment.go
export interface OAuth2GithubConfig {
readonly client_id: DeploymentConfigField<string>
readonly client_secret: DeploymentConfigField<string>
readonly allowed_orgs: DeploymentConfigField<string[]>
readonly allowed_teams: DeploymentConfigField<string[]>
readonly allow_signups: DeploymentConfigField<boolean>
readonly allow_everyone: DeploymentConfigField<boolean>
readonly enterprise_base_url: DeploymentConfigField<string>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly client_id: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly client_secret: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly allowed_orgs: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly allowed_teams: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly allow_signups: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly allow_everyone: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly enterprise_base_url: string
}
// From codersdk/users.go
@ -493,16 +531,25 @@ export interface OIDCAuthMethod extends AuthMethod {
// From codersdk/deployment.go
export interface OIDCConfig {
readonly allow_signups: DeploymentConfigField<boolean>
readonly client_id: DeploymentConfigField<string>
readonly client_secret: DeploymentConfigField<string>
readonly email_domain: DeploymentConfigField<string[]>
readonly issuer_url: DeploymentConfigField<string>
readonly scopes: DeploymentConfigField<string[]>
readonly ignore_email_verified: DeploymentConfigField<boolean>
readonly username_field: DeploymentConfigField<string>
readonly sign_in_text: DeploymentConfigField<string>
readonly icon_url: DeploymentConfigField<string>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly allow_signups: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly client_id: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly client_secret: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly email_domain: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly issuer_url: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly scopes: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly ignore_email_verified: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly username_field: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly sign_in_text: string
readonly icon_url: string
}
// From codersdk/organizations.go
@ -573,22 +620,32 @@ export interface PatchGroupRequest {
// From codersdk/deployment.go
export interface PprofConfig {
readonly enable: DeploymentConfigField<boolean>
readonly address: DeploymentConfigField<string>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly enable: boolean
// Named type "github.com/coder/coder/cli/clibase.HostPort" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO explain why this is needed
readonly address: any
}
// From codersdk/deployment.go
export interface PrometheusConfig {
readonly enable: DeploymentConfigField<boolean>
readonly address: DeploymentConfigField<string>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly enable: boolean
// Named type "github.com/coder/coder/cli/clibase.HostPort" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO explain why this is needed
readonly address: any
}
// From codersdk/deployment.go
export interface ProvisionerConfig {
readonly daemons: DeploymentConfigField<number>
readonly daemon_poll_interval: DeploymentConfigField<number>
readonly daemon_poll_jitter: DeploymentConfigField<number>
readonly force_cancel_interval: DeploymentConfigField<number>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Int64")
readonly daemons: number
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Duration")
readonly daemon_poll_interval: number
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Duration")
readonly daemon_poll_jitter: number
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Duration")
readonly force_cancel_interval: number
}
// From codersdk/provisionerdaemons.go
@ -632,8 +689,10 @@ export interface PutExtendWorkspaceRequest {
// From codersdk/deployment.go
export interface RateLimitConfig {
readonly disable_all: DeploymentConfigField<boolean>
readonly api: DeploymentConfigField<number>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly disable_all: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Int64")
readonly api: number
}
// From codersdk/replicas.go
@ -676,33 +735,49 @@ export interface ServiceBannerConfig {
// From codersdk/deployment.go
export interface SupportConfig {
readonly links: DeploymentConfigField<LinkConfig[]>
// Named type "github.com/coder/coder/cli/clibase.Struct[[]github.com/coder/coder/codersdk.LinkConfig]" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO explain why this is needed
readonly links: any
}
// From codersdk/deployment.go
export interface SwaggerConfig {
readonly enable: DeploymentConfigField<boolean>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly enable: boolean
}
// From codersdk/deployment.go
export interface TLSConfig {
readonly enable: DeploymentConfigField<boolean>
readonly address: DeploymentConfigField<string>
readonly redirect_http: DeploymentConfigField<boolean>
readonly cert_file: DeploymentConfigField<string[]>
readonly client_auth: DeploymentConfigField<string>
readonly client_ca_file: DeploymentConfigField<string>
readonly key_file: DeploymentConfigField<string[]>
readonly min_version: DeploymentConfigField<string>
readonly client_cert_file: DeploymentConfigField<string>
readonly client_key_file: DeploymentConfigField<string>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly enable: boolean
// Named type "github.com/coder/coder/cli/clibase.HostPort" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO explain why this is needed
readonly address: any
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly redirect_http: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly cert_file: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly client_auth: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly client_ca_file: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Strings")
readonly key_file: string[]
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly min_version: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly client_cert_file: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly client_key_file: string
}
// From codersdk/deployment.go
export interface TelemetryConfig {
readonly enable: DeploymentConfigField<boolean>
readonly trace: DeploymentConfigField<boolean>
readonly url: DeploymentConfigField<string>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly enable: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly trace: boolean
readonly url: string
}
// From codersdk/templates.go
@ -834,9 +909,12 @@ export interface TokensFilter {
// From codersdk/deployment.go
export interface TraceConfig {
readonly enable: DeploymentConfigField<boolean>
readonly honeycomb_api_key: DeploymentConfigField<string>
readonly capture_logs: DeploymentConfigField<boolean>
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly enable: boolean
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.String")
readonly honeycomb_api_key: string
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.Bool")
readonly capture_logs: boolean
}
// From codersdk/templates.go
@ -1390,12 +1468,3 @@ export const WorkspaceTransitions: WorkspaceTransition[] = [
"start",
"stop",
]
// From codersdk/deployment.go
export type Flaggable =
| string
| number
| boolean
| string[]
| GitAuthConfig[]
| LinkConfig[]

View File

@ -5,15 +5,16 @@ import { Sidebar } from "./Sidebar"
import { createContext, Suspense, useContext, FC } from "react"
import { useMachine } from "@xstate/react"
import { Loader } from "components/Loader/Loader"
import { DeploymentConfig, DeploymentDAUsResponse } from "api/typesGenerated"
import { DeploymentDAUsResponse } from "api/typesGenerated"
import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine"
import { RequirePermission } from "components/RequirePermission/RequirePermission"
import { usePermissions } from "hooks/usePermissions"
import { Outlet } from "react-router-dom"
import { DeploymentConfig } from "api/types"
type DeploySettingsContextValue = {
deploymentConfig: DeploymentConfig
getDeploymentConfigError: unknown
deploymentValues: DeploymentConfig
getDeploymentValuesError: unknown
deploymentDAUs?: DeploymentDAUsResponse
getDeploymentDAUsError: unknown
}
@ -36,24 +37,24 @@ export const DeploySettingsLayout: FC = () => {
const [state] = useMachine(deploymentConfigMachine)
const styles = useStyles()
const {
deploymentConfig,
deploymentValues,
deploymentDAUs,
getDeploymentConfigError,
getDeploymentValuesError,
getDeploymentDAUsError,
} = state.context
const permissions = usePermissions()
return (
<RequirePermission isFeatureVisible={permissions.viewDeploymentConfig}>
<RequirePermission isFeatureVisible={permissions.viewDeploymentValues}>
<Margins>
<Stack className={styles.wrapper} direction="row" spacing={6}>
<Sidebar />
<main className={styles.content}>
{deploymentConfig ? (
{deploymentValues ? (
<DeploySettingsContext.Provider
value={{
deploymentConfig,
getDeploymentConfigError,
deploymentValues,
getDeploymentValuesError,
deploymentDAUs,
getDeploymentDAUsError,
}}

View File

@ -1,5 +1,5 @@
import { makeStyles } from "@material-ui/core/styles"
import { PropsWithChildren, FC } from "react"
import { PropsWithChildren, FC, ReactNode } from "react"
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
import { DisabledBadge, EnabledBadge } from "./Badges"
@ -19,13 +19,23 @@ const NotSet: FC = () => {
return <span className={styles.optionValue}>Not set</span>
}
export const OptionValue: FC<PropsWithChildren> = ({ children }) => {
export const OptionValue: FC<{ children?: ReactNode | unknown }> = ({
children,
}) => {
const styles = useStyles()
if (typeof children === "boolean") {
return children ? <EnabledBadge /> : <DisabledBadge />
}
if (typeof children === "number") {
return <span className={styles.optionValue}>{children}</span>
}
if (typeof children === "string") {
return <span className={styles.optionValue}>{children}</span>
}
if (Array.isArray(children)) {
if (children.length === 0) {
return <NotSet />
@ -46,7 +56,7 @@ export const OptionValue: FC<PropsWithChildren> = ({ children }) => {
return <NotSet />
}
return <span className={styles.optionValue}>{children}</span>
return <span className={styles.optionValue}>{JSON.stringify(children)}</span>
}
const useStyles = makeStyles((theme) => ({

View File

@ -5,19 +5,24 @@ import TableCell from "@material-ui/core/TableCell"
import TableContainer from "@material-ui/core/TableContainer"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import { DeploymentConfigField, Flaggable } from "api/typesGenerated"
import { DeploymentOption } from "api/types"
import {
OptionDescription,
OptionName,
OptionValue,
} from "components/DeploySettingsLayout/Option"
import { FC } from "react"
import { DisabledBadge } from "./Badges"
const OptionsTable: FC<{
options: Record<string, DeploymentConfigField<Flaggable>>
options: DeploymentOption[]
}> = ({ options }) => {
const styles = useStyles()
if (options.length === 0) {
return <DisabledBadge></DisabledBadge>
}
return (
<TableContainer>
<Table className={styles.table}>
@ -29,15 +34,18 @@ const OptionsTable: FC<{
</TableHead>
<TableBody>
{Object.values(options).map((option) => {
if (option.value === null || option.value === "") {
return null
}
return (
<TableRow key={option.flag}>
<TableCell>
<OptionName>{option.name}</OptionName>
<OptionDescription>{option.usage}</OptionDescription>
<OptionDescription>{option.description}</OptionDescription>
</TableCell>
<TableCell>
<OptionValue>{option.value.toString()}</OptionValue>
<OptionValue>{option.value}</OptionValue>
</TableCell>
</TableRow>
)

View File

@ -14,7 +14,7 @@ export const Navbar: FC = () => {
const featureVisibility = useFeatureVisibility()
const canViewAuditLog =
featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog)
const canViewDeployment = Boolean(permissions.viewDeploymentConfig)
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
const onSignOut = () => authSend("SIGN_OUT")
return (

View File

@ -5,7 +5,7 @@ import { pageTitle } from "util/page"
import { GeneralSettingsPageView } from "./GeneralSettingsPageView"
const GeneralSettingsPage: FC = () => {
const { deploymentConfig, deploymentDAUs, getDeploymentDAUsError } =
const { deploymentValues, deploymentDAUs, getDeploymentDAUsError } =
useDeploySettings()
return (
@ -14,7 +14,7 @@ const GeneralSettingsPage: FC = () => {
<title>{pageTitle("General Settings")}</title>
</Helmet>
<GeneralSettingsPageView
deploymentConfig={deploymentConfig}
deploymentOptions={deploymentValues.options}
deploymentDAUs={deploymentDAUs}
getDeploymentDAUsError={getDeploymentDAUsError}
/>

View File

@ -12,21 +12,21 @@ export default {
title: "pages/GeneralSettingsPageView",
component: GeneralSettingsPageView,
argTypes: {
deploymentConfig: {
defaultValue: {
access_url: {
deploymentOptions: {
defaultValue: [
{
name: "Access URL",
usage:
"External URL to access your deployment. This must be accessible by all provisioned workspaces.",
value: "https://dev.coder.com",
},
wildcard_access_url: {
{
name: "Wildcard Access URL",
usage:
'Specifies the wildcard hostname to use for workspace applications in the form "*.example.com".',
value: "*--apps.dev.coder.com",
},
},
],
},
deploymentDAUs: {
defaultValue: MockDeploymentDAUResponse,

View File

@ -1,17 +1,19 @@
import { DeploymentConfig, DeploymentDAUsResponse } from "api/typesGenerated"
import { DeploymentOption } from "api/types"
import { DeploymentDAUsResponse } from "api/typesGenerated"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { DAUChart } from "components/DAUChart/DAUChart"
import { Header } from "components/DeploySettingsLayout/Header"
import OptionsTable from "components/DeploySettingsLayout/OptionsTable"
import { Stack } from "components/Stack/Stack"
import { useDeploymentOptions } from "util/deployOptions"
export type GeneralSettingsPageViewProps = {
deploymentConfig: Pick<DeploymentConfig, "access_url" | "wildcard_access_url">
deploymentOptions: DeploymentOption[]
deploymentDAUs?: DeploymentDAUsResponse
getDeploymentDAUsError: unknown
}
export const GeneralSettingsPageView = ({
deploymentConfig,
deploymentOptions,
deploymentDAUs,
getDeploymentDAUsError,
}: GeneralSettingsPageViewProps): JSX.Element => {
@ -28,10 +30,11 @@ export const GeneralSettingsPageView = ({
)}
{deploymentDAUs && <DAUChart daus={deploymentDAUs} />}
<OptionsTable
options={{
access_url: deploymentConfig.access_url,
wildcard_access_url: deploymentConfig.wildcard_access_url,
}}
options={useDeploymentOptions(
deploymentOptions,
"Access URL",
"Wildcard Access URL",
)}
/>
</Stack>
</>

View File

@ -5,7 +5,7 @@ import { pageTitle } from "util/page"
import { GitAuthSettingsPageView } from "./GitAuthSettingsPageView"
const GitAuthSettingsPage: FC = () => {
const { deploymentConfig: deploymentConfig } = useDeploySettings()
const { deploymentValues: deploymentValues } = useDeploySettings()
return (
<>
@ -13,7 +13,7 @@ const GitAuthSettingsPage: FC = () => {
<title>{pageTitle("Git Authentication Settings")}</title>
</Helmet>
<GitAuthSettingsPageView deploymentConfig={deploymentConfig} />
<GitAuthSettingsPageView config={deploymentValues.config} />
</>
)
}

View File

@ -8,18 +8,16 @@ export default {
title: "pages/GitAuthSettingsPageView",
component: GitAuthSettingsPageView,
argTypes: {
deploymentConfig: {
config: {
defaultValue: {
gitauth: {
name: "Git Auth",
usage: "Automatically authenticate Git inside workspaces.",
value: [
{
id: "123",
client_id: "575",
},
],
},
git_auth: [
{
id: "0000-1111",
type: "GitHub",
client_id: "client_id",
regex: "regex",
},
],
},
},
},

View File

@ -5,17 +5,17 @@ import TableCell from "@material-ui/core/TableCell"
import TableContainer from "@material-ui/core/TableContainer"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import { DeploymentConfig } from "api/typesGenerated"
import { DeploymentValues, GitAuthConfig } from "api/typesGenerated"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { EnterpriseBadge } from "components/DeploySettingsLayout/Badges"
import { Header } from "components/DeploySettingsLayout/Header"
export type GitAuthSettingsPageViewProps = {
deploymentConfig: Pick<DeploymentConfig, "gitauth">
config: DeploymentValues
}
export const GitAuthSettingsPageView = ({
deploymentConfig,
config,
}: GitAuthSettingsPageViewProps): JSX.Element => {
const styles = useStyles()
@ -57,7 +57,7 @@ export const GitAuthSettingsPageView = ({
</TableRow>
</TableHead>
<TableBody>
{deploymentConfig.gitauth.value.length === 0 && (
{((config.git_auth === null || config.git_auth.length === 0) && (
<TableRow>
<TableCell colSpan={999}>
<div className={styles.empty}>
@ -65,18 +65,17 @@ export const GitAuthSettingsPageView = ({
</div>
</TableCell>
</TableRow>
)}
{deploymentConfig.gitauth.value.map((git) => {
const name = git.id || git.type
return (
<TableRow key={name}>
<TableCell>{name}</TableCell>
<TableCell>{git.client_id}</TableCell>
<TableCell>{git.regex || "Not Set"}</TableCell>
</TableRow>
)
})}
)) ||
config.git_auth.map((git: GitAuthConfig) => {
const name = git.id || git.type
return (
<TableRow key={name}>
<TableCell>{name}</TableCell>
<TableCell>{git.client_id}</TableCell>
<TableCell>{git.regex || "Not Set"}</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>

View File

@ -5,7 +5,7 @@ import { pageTitle } from "util/page"
import { NetworkSettingsPageView } from "./NetworkSettingsPageView"
const NetworkSettingsPage: FC = () => {
const { deploymentConfig: deploymentConfig } = useDeploySettings()
const { deploymentValues: deploymentValues } = useDeploySettings()
return (
<>
@ -13,7 +13,7 @@ const NetworkSettingsPage: FC = () => {
<title>{pageTitle("Network Settings")}</title>
</Helmet>
<NetworkSettingsPageView deploymentConfig={deploymentConfig} />
<NetworkSettingsPageView options={deploymentValues.options} />
</>
)
}

View File

@ -8,41 +8,50 @@ export default {
title: "pages/NetworkSettingsPageView",
component: NetworkSettingsPageView,
argTypes: {
deploymentConfig: {
defaultValue: {
derp: {
server: {
enable: {
name: "DERP Server Enable",
usage:
"Whether to enable or disable the embedded DERP relay server.",
value: true,
},
region_name: {
name: "DERP Server Region Name",
usage: "Region name that for the embedded DERP server.",
value: "aws-east",
},
stun_addresses: {
name: "DERP Server STUN Addresses",
usage:
"Addresses for STUN servers to establish P2P connections. Set empty to disable P2P connections.",
value: ["stun.l.google.com:19302", "stun.l.google.com:19301"],
},
},
config: {
url: {
name: "DERP Config URL",
usage:
"URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custom-derp-servers/",
value: "https://coder.com",
},
options: {
defaultValue: [
{
name: "DERP Server Enable",
usage: "Whether to enable or disable the embedded DERP relay server.",
value: true,
group: {
name: "Networking",
},
},
wildcard_access_url: {
{
name: "DERP Server Region Name",
usage: "Region name that for the embedded DERP server.",
value: "aws-east",
group: {
name: "Networking",
},
},
{
name: "DERP Server STUN Addresses",
usage:
"Addresses for STUN servers to establish P2P connections. Set empty to disable P2P connections.",
value: ["stun.l.google.com:19302", "stun.l.google.com:19301"],
group: {
name: "Networking",
},
},
{
name: "DERP Config URL",
usage:
"URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custom-derp-servers/",
value: "https://coder.com",
group: {
name: "Networking",
},
},
},
{
name: "Wildcard Access URL",
value: "https://coder.com",
group: {
name: "Networking",
},
},
],
},
},
} as ComponentMeta<typeof NetworkSettingsPageView>

View File

@ -1,4 +1,4 @@
import { DeploymentConfig } from "api/typesGenerated"
import { DeploymentOption } from "api/types"
import {
Badges,
EnabledBadge,
@ -7,13 +7,17 @@ import {
import { Header } from "components/DeploySettingsLayout/Header"
import OptionsTable from "components/DeploySettingsLayout/OptionsTable"
import { Stack } from "components/Stack/Stack"
import {
deploymentGroupHasParent,
useDeploymentOptions,
} from "util/deployOptions"
export type NetworkSettingsPageViewProps = {
deploymentConfig: Pick<DeploymentConfig, "derp" | "wildcard_access_url">
options: DeploymentOption[]
}
export const NetworkSettingsPageView = ({
deploymentConfig,
options: options,
}: NetworkSettingsPageViewProps): JSX.Element => (
<Stack direction="column" spacing={6}>
<div>
@ -23,13 +27,9 @@ export const NetworkSettingsPageView = ({
docsHref="https://coder.com/docs/coder-oss/latest/networking"
/>
<OptionsTable
options={{
derp_server_enable: deploymentConfig.derp.server.enable,
derp_server_region_name: deploymentConfig.derp.server.region_name,
derp_server_stun_addresses:
deploymentConfig.derp.server.stun_addresses,
derp_config_url: deploymentConfig.derp.config.url,
}}
options={options.filter((o) =>
deploymentGroupHasParent(o.group, "Networking"),
)}
/>
</div>
@ -42,7 +42,8 @@ export const NetworkSettingsPageView = ({
/>
<Badges>
{deploymentConfig.wildcard_access_url.value !== "" ? (
{useDeploymentOptions(options, "Wildcard Access URL")[0].value !==
"" ? (
<EnabledBadge />
) : (
<DisabledBadge />

View File

@ -6,7 +6,7 @@ import { pageTitle } from "util/page"
import { SecuritySettingsPageView } from "./SecuritySettingsPageView"
const SecuritySettingsPage: FC = () => {
const { deploymentConfig: deploymentConfig } = useDeploySettings()
const { deploymentValues: deploymentValues } = useDeploySettings()
const { entitlements } = useDashboard()
return (
@ -16,7 +16,7 @@ const SecuritySettingsPage: FC = () => {
</Helmet>
<SecuritySettingsPageView
deploymentConfig={deploymentConfig}
options={deploymentValues.options}
featureAuditLogEnabled={entitlements.features["audit_log"].enabled}
featureBrowserOnlyEnabled={
entitlements.features["browser_only"].enabled

View File

@ -1,4 +1,5 @@
import { ComponentMeta, Story } from "@storybook/react"
import { DeploymentOption } from "api/types"
import {
SecuritySettingsPageView,
SecuritySettingsPageViewProps,
@ -8,41 +9,27 @@ export default {
title: "pages/SecuritySettingsPageView",
component: SecuritySettingsPageView,
argTypes: {
deploymentConfig: {
defaultValue: {
ssh_keygen_algorithm: {
name: "key",
options: {
defaultValue: [
{
name: "SSH Keygen Algorithm",
usage: "something",
value: "1234",
},
secure_auth_cookie: {
name: "key",
{
name: "Secure Auth Cookie",
usage: "something",
value: "1234",
},
tls: {
enable: {
name: "yes or no",
usage: "something",
value: true,
},
cert_file: {
name: "yes or no",
usage: "something",
value: ["something"],
},
key_file: {
name: "yes or no",
usage: "something",
value: ["something"],
},
min_version: {
name: "yes or no",
usage: "something",
value: "something",
{
name: "TLS Version",
usage: "something",
value: ["something"],
group: {
name: "TLS",
},
},
},
],
},
featureAuditLogEnabled: {
defaultValue: true,
@ -57,3 +44,17 @@ const Template: Story<SecuritySettingsPageViewProps> = (args) => (
<SecuritySettingsPageView {...args} />
)
export const Page = Template.bind({})
export const NoTLS = Template.bind({})
NoTLS.args = {
options: [
{
name: "SSH Keygen Algorithm",
value: "1234",
} as DeploymentOption,
{
name: "Secure Auth Cookie",
value: "1234",
} as DeploymentOption,
],
}

View File

@ -1,4 +1,4 @@
import { DeploymentConfig } from "api/typesGenerated"
import { DeploymentOption } from "api/types"
import {
Badges,
DisabledBadge,
@ -8,17 +8,18 @@ import {
import { Header } from "components/DeploySettingsLayout/Header"
import OptionsTable from "components/DeploySettingsLayout/OptionsTable"
import { Stack } from "components/Stack/Stack"
import {
deploymentGroupHasParent,
useDeploymentOptions,
} from "util/deployOptions"
export type SecuritySettingsPageViewProps = {
deploymentConfig: Pick<
DeploymentConfig,
"tls" | "ssh_keygen_algorithm" | "secure_auth_cookie"
>
options: DeploymentOption[]
featureAuditLogEnabled: boolean
featureBrowserOnlyEnabled: boolean
}
export const SecuritySettingsPageView = ({
deploymentConfig,
options: options,
featureAuditLogEnabled,
featureBrowserOnlyEnabled,
}: SecuritySettingsPageViewProps): JSX.Element => (
@ -31,10 +32,11 @@ export const SecuritySettingsPageView = ({
/>
<OptionsTable
options={{
ssh_keygen_algorithm: deploymentConfig.ssh_keygen_algorithm,
secure_auth_cookie: deploymentConfig.secure_auth_cookie,
}}
options={useDeploymentOptions(
options,
"SSH Keygen Algorithm",
"Secure Auth Cookie",
)}
/>
</div>
@ -74,12 +76,9 @@ export const SecuritySettingsPageView = ({
/>
<OptionsTable
options={{
tls_enable: deploymentConfig.tls.enable,
tls_cert_files: deploymentConfig.tls.cert_file,
tls_key_files: deploymentConfig.tls.key_file,
tls_min_version: deploymentConfig.tls.min_version,
}}
options={options.filter((o) =>
deploymentGroupHasParent(o.group, "TLS"),
)}
/>
</div>
</Stack>

View File

@ -5,7 +5,7 @@ import { pageTitle } from "util/page"
import { UserAuthSettingsPageView } from "./UserAuthSettingsPageView"
const UserAuthSettingsPage: FC = () => {
const { deploymentConfig: deploymentConfig } = useDeploySettings()
const { deploymentValues: deploymentValues } = useDeploySettings()
return (
<>
@ -13,7 +13,7 @@ const UserAuthSettingsPage: FC = () => {
<title>{pageTitle("User Authentication Settings")}</title>
</Helmet>
<UserAuthSettingsPageView deploymentConfig={deploymentConfig} />
<UserAuthSettingsPageView options={deploymentValues.options} />
</>
)
}

View File

@ -8,69 +8,92 @@ export default {
title: "pages/UserAuthSettingsPageView",
component: UserAuthSettingsPageView,
argTypes: {
deploymentConfig: {
defaultValue: {
oidc: {
client_id: {
name: "OIDC Client ID",
usage: "Client ID to use for Login with OIDC.",
value: "1234",
},
allow_signups: {
name: "OIDC Allow Signups",
usage: "Whether new users can sign up with OIDC.",
value: true,
},
email_domain: {
name: "OIDC Email Domain",
usage:
"Email domains that clients logging in with OIDC must match.",
value: "@coder.com",
},
issuer_url: {
name: "OIDC Issuer URL",
usage: "Issuer URL to use for Login with OIDC.",
value: "https://coder.com",
},
scopes: {
name: "OIDC Scopes",
usage: "Scopes to grant when authenticating with OIDC.",
value: ["idk"],
options: {
defaultValue: [
{
name: "OIDC Client ID",
usage: "Client ID to use for Login with OIDC.",
value: "1234",
group: {
name: "OIDC",
},
},
oauth2: {
github: {
client_id: {
name: "OAuth2 GitHub Client ID",
usage: "Client ID for Login with GitHub.",
value: "1224",
},
allow_signups: {
name: "OAuth2 GitHub Allow Signups",
usage: "Whether new users can sign up with GitHub.",
value: true,
},
enterprise_base_url: {
name: "OAuth2 GitHub Enterprise Base URL",
usage:
"Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",
value: "https://google.com",
},
allowed_orgs: {
name: "OAuth2 GitHub Allowed Orgs",
usage:
"Organizations the user must be a member of to Login with GitHub.",
value: true,
},
allowed_teams: {
name: "OAuth2 GitHub Allowed Teams",
usage:
"Teams inside organizations the user must be a member of to Login with GitHub. Structured as: <organization-name>/<team-slug>.",
value: true,
},
{
name: "OIDC Allow Signups",
usage: "Whether new users can sign up with OIDC.",
value: true,
group: {
name: "OIDC",
},
},
},
{
name: "OIDC Email Domain",
usage: "Email domains that clients logging in with OIDC must match.",
value: "@coder.com",
group: {
name: "OIDC",
},
},
{
name: "OIDC Issuer URL",
usage: "Issuer URL to use for Login with OIDC.",
value: "https://coder.com",
group: {
name: "OIDC",
},
},
{
name: "OIDC Scopes",
usage: "Scopes to grant when authenticating with OIDC.",
value: ["idk"],
group: {
name: "OIDC",
},
},
{
name: "OAuth2 GitHub Client ID",
usage: "Client ID for Login with GitHub.",
value: "1224",
group: {
name: "GitHub",
},
},
{
name: "OAuth2 GitHub Allow Signups",
usage: "Whether new users can sign up with GitHub.",
value: true,
group: {
name: "GitHub",
},
},
{
name: "OAuth2 GitHub Enterprise Base URL",
usage:
"Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",
value: "https://google.com",
group: {
name: "GitHub",
},
},
{
name: "OAuth2 GitHub Allowed Orgs",
usage:
"Organizations the user must be a member of to Login with GitHub.",
value: true,
group: {
name: "GitHub",
},
},
{
name: "OAuth2 GitHub Allowed Teams",
usage:
"Teams inside organizations the user must be a member of to Login with GitHub. Structured as: <organization-name>/<team-slug>.",
value: true,
group: {
name: "GitHub",
},
},
],
},
},
} as ComponentMeta<typeof UserAuthSettingsPageView>

View File

@ -1,4 +1,4 @@
import { DeploymentConfig } from "api/typesGenerated"
import { DeploymentOption } from "api/types"
import {
Badges,
DisabledBadge,
@ -7,13 +7,17 @@ import {
import { Header } from "components/DeploySettingsLayout/Header"
import OptionsTable from "components/DeploySettingsLayout/OptionsTable"
import { Stack } from "components/Stack/Stack"
import {
deploymentGroupHasParent,
useDeploymentOptions,
} from "util/deployOptions"
export type UserAuthSettingsPageViewProps = {
deploymentConfig: Pick<DeploymentConfig, "oidc" | "oauth2">
options: DeploymentOption[]
}
export const UserAuthSettingsPageView = ({
deploymentConfig,
options,
}: UserAuthSettingsPageViewProps): JSX.Element => (
<>
<Stack direction="column" spacing={6}>
@ -28,7 +32,7 @@ export const UserAuthSettingsPageView = ({
/>
<Badges>
{deploymentConfig.oidc.client_id.value ? (
{useDeploymentOptions(options, "OIDC Client ID")[0].value ? (
<EnabledBadge />
) : (
<DisabledBadge />
@ -36,13 +40,9 @@ export const UserAuthSettingsPageView = ({
</Badges>
<OptionsTable
options={{
client_id: deploymentConfig.oidc.client_id,
allow_signups: deploymentConfig.oidc.allow_signups,
email_domain: deploymentConfig.oidc.email_domain,
issuer_url: deploymentConfig.oidc.issuer_url,
scopes: deploymentConfig.oidc.scopes,
}}
options={options.filter((o) =>
deploymentGroupHasParent(o.group, "OIDC"),
)}
/>
</div>
@ -55,7 +55,7 @@ export const UserAuthSettingsPageView = ({
/>
<Badges>
{deploymentConfig.oauth2.github.client_id.value ? (
{useDeploymentOptions(options, "OAuth2 GitHub Client ID")[0].value ? (
<EnabledBadge />
) : (
<DisabledBadge />
@ -63,14 +63,9 @@ export const UserAuthSettingsPageView = ({
</Badges>
<OptionsTable
options={{
client_id: deploymentConfig.oauth2.github.client_id,
allow_signups: deploymentConfig.oauth2.github.allow_signups,
allowed_orgs: deploymentConfig.oauth2.github.allowed_orgs,
allowed_teams: deploymentConfig.oauth2.github.allowed_teams,
enterprise_base_url:
deploymentConfig.oauth2.github.enterprise_base_url,
}}
options={options.filter((o) =>
deploymentGroupHasParent(o.group, "GitHub"),
)}
/>
</div>
</Stack>

View File

@ -1224,7 +1224,6 @@ export const MockEntitlements: TypesGen.Entitlements = {
warnings: [],
has_license: false,
features: withDefaultFeatures({}),
experimental: false,
require_telemetry: false,
trial: false,
}
@ -1233,7 +1232,6 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = {
errors: [],
warnings: ["You are over your active user limit.", "And another thing."],
has_license: true,
experimental: false,
trial: false,
require_telemetry: false,
features: withDefaultFeatures({
@ -1258,7 +1256,6 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = {
errors: [],
warnings: [],
has_license: true,
experimental: false,
require_telemetry: false,
trial: false,
features: withDefaultFeatures({
@ -1446,7 +1443,7 @@ export const MockPermissions: Permissions = {
readAllUsers: true,
updateUsers: true,
viewAuditLog: true,
viewDeploymentConfig: true,
viewDeploymentValues: true,
viewUpdateCheck: true,
}

View File

@ -0,0 +1,41 @@
import { useMemo } from "react"
import { DeploymentGroup, DeploymentOption } from "./../api/types"
const deploymentOptions = (
options: DeploymentOption[],
...names: string[]
): DeploymentOption[] => {
const found: DeploymentOption[] = []
for (const name of names) {
const option = options.find((o) => o.name === name)
if (option) {
found.push(option)
} else {
throw new Error(`Deployment option ${name} not found`)
}
}
return found
}
export const useDeploymentOptions = (
options: DeploymentOption[],
...names: string[]
): DeploymentOption[] => {
return useMemo(() => deploymentOptions(options, ...names), [options, names])
}
export const deploymentGroupHasParent = (
group: DeploymentGroup | undefined,
parent: string,
): boolean => {
if (!group) {
return false
}
if (group.parent) {
return deploymentGroupHasParent(group.parent, parent)
}
if (group.name === parent) {
return true
}
return false
}

View File

@ -14,7 +14,7 @@ export const checks = {
createTemplates: "createTemplates",
deleteTemplates: "deleteTemplates",
viewAuditLog: "viewAuditLog",
viewDeploymentConfig: "viewDeploymentConfig",
viewDeploymentValues: "viewDeploymentValues",
createGroup: "createGroup",
viewUpdateCheck: "viewUpdateCheck",
} as const
@ -56,7 +56,7 @@ export const permissionsToCheck = {
},
action: "read",
},
[checks.viewDeploymentConfig]: {
[checks.viewDeploymentValues]: {
object: {
resource_type: "deployment_flags",
},

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