Skip to content

Commit eb5ec5c

Browse files
committed
Merge branch 'main' of https://github.com/coder/coder into dk/provision-detailed-apply
2 parents ebbaf31 + cf8be4e commit eb5ec5c

File tree

155 files changed

+3745
-2174
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

155 files changed

+3745
-2174
lines changed

.github/workflows/contrib.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
steps:
3535
- name: cla
3636
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
37-
uses: contributor-assistant/github-action@v2.4.0
37+
uses: contributor-assistant/github-action@v2.5.1
3838
env:
3939
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4040
# the below token should have repo scope and must be manually added by you in the repository's secret

cli/cliui/output.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ func (f *OutputFormatter) AttachOptions(opts *serpent.OptionSet) {
6565
Flag: "output",
6666
FlagShorthand: "o",
6767
Default: f.formats[0].ID(),
68-
Value: serpent.StringOf(&f.formatID),
69-
Description: "Output format. Available formats: " + strings.Join(formatNames, ", ") + ".",
68+
Value: serpent.EnumOf(&f.formatID, formatNames...),
69+
Description: "Output format.",
7070
},
7171
)
7272
}
@@ -136,8 +136,8 @@ func (f *tableFormat) AttachOptions(opts *serpent.OptionSet) {
136136
Flag: "column",
137137
FlagShorthand: "c",
138138
Default: strings.Join(f.defaultColumns, ","),
139-
Value: serpent.StringArrayOf(&f.columns),
140-
Description: "Columns to display in table output. Available columns: " + strings.Join(f.allColumns, ", ") + ".",
139+
Value: serpent.EnumArrayOf(&f.columns, f.allColumns...),
140+
Description: "Columns to display in table output.",
141141
},
142142
)
143143
}

cli/cliui/output_test.go

+8-9
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ func Test_OutputFormatter(t *testing.T) {
106106

107107
fs := cmd.Options.FlagSet()
108108

109-
selected, err := fs.GetString("output")
110-
require.NoError(t, err)
111-
require.Equal(t, "json", selected)
109+
selected := cmd.Options.ByFlag("output")
110+
require.NotNil(t, selected)
111+
require.Equal(t, "json", selected.Value.String())
112112
usage := fs.FlagUsages()
113-
require.Contains(t, usage, "Available formats: json, foo")
113+
require.Contains(t, usage, "Output format.")
114114
require.Contains(t, usage, "foo flag 1234")
115115

116116
ctx := context.Background()
@@ -129,11 +129,10 @@ func Test_OutputFormatter(t *testing.T) {
129129
require.Equal(t, "foo", out)
130130
require.EqualValues(t, 1, atomic.LoadInt64(&called))
131131

132-
require.NoError(t, fs.Set("output", "bar"))
132+
require.Error(t, fs.Set("output", "bar"))
133133
out, err = f.Format(ctx, data)
134-
require.Error(t, err)
135-
require.ErrorContains(t, err, "bar")
136-
require.Equal(t, "", out)
137-
require.EqualValues(t, 1, atomic.LoadInt64(&called))
134+
require.NoError(t, err)
135+
require.Equal(t, "foo", out)
136+
require.EqualValues(t, 2, atomic.LoadInt64(&called))
138137
})
139138
}

cli/completion.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/coder/coder/v2/cli/cliui"
7+
"github.com/coder/serpent"
8+
"github.com/coder/serpent/completion"
9+
)
10+
11+
func (*RootCmd) completion() *serpent.Command {
12+
var shellName string
13+
var printOutput bool
14+
shellOptions := completion.ShellOptions(&shellName)
15+
return &serpent.Command{
16+
Use: "completion",
17+
Short: "Install or update shell completion scripts for the detected or chosen shell.",
18+
Options: []serpent.Option{
19+
{
20+
Flag: "shell",
21+
FlagShorthand: "s",
22+
Description: "The shell to install completion for.",
23+
Value: shellOptions,
24+
},
25+
{
26+
Flag: "print",
27+
Description: "Print the completion script instead of installing it.",
28+
FlagShorthand: "p",
29+
30+
Value: serpent.BoolOf(&printOutput),
31+
},
32+
},
33+
Handler: func(inv *serpent.Invocation) error {
34+
if shellName != "" {
35+
shell, err := completion.ShellByName(shellName, inv.Command.Parent.Name())
36+
if err != nil {
37+
return err
38+
}
39+
if printOutput {
40+
return shell.WriteCompletion(inv.Stdout)
41+
}
42+
return installCompletion(inv, shell)
43+
}
44+
shell, err := completion.DetectUserShell(inv.Command.Parent.Name())
45+
if err == nil {
46+
return installCompletion(inv, shell)
47+
}
48+
// Silently continue to the shell selection if detecting failed.
49+
choice, err := cliui.Select(inv, cliui.SelectOptions{
50+
Message: "Select a shell to install completion for:",
51+
Options: shellOptions.Choices,
52+
})
53+
if err != nil {
54+
return err
55+
}
56+
shellChoice, err := completion.ShellByName(choice, inv.Command.Parent.Name())
57+
if err != nil {
58+
return err
59+
}
60+
if printOutput {
61+
return shellChoice.WriteCompletion(inv.Stdout)
62+
}
63+
return installCompletion(inv, shellChoice)
64+
},
65+
}
66+
}
67+
68+
func installCompletion(inv *serpent.Invocation, shell completion.Shell) error {
69+
path, err := shell.InstallPath()
70+
if err != nil {
71+
cliui.Error(inv.Stderr, fmt.Sprintf("Failed to determine completion path %v", err))
72+
return shell.WriteCompletion(inv.Stdout)
73+
}
74+
choice, err := cliui.Select(inv, cliui.SelectOptions{
75+
Options: []string{
76+
"Confirm",
77+
"Print to terminal",
78+
},
79+
Message: fmt.Sprintf("Install completion for %s at %s?", shell.Name(), path),
80+
HideSearch: true,
81+
})
82+
if err != nil {
83+
return err
84+
}
85+
if choice == "Print to terminal" {
86+
return shell.WriteCompletion(inv.Stdout)
87+
}
88+
return completion.InstallShellCompletion(shell)
89+
}

cli/configssh.go

+2-45
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"strings"
1818

1919
"github.com/cli/safeexec"
20+
"github.com/natefinch/atomic"
2021
"github.com/pkg/diff"
2122
"github.com/pkg/diff/write"
2223
"golang.org/x/exp/constraints"
@@ -524,7 +525,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
524525
}
525526

526527
if !bytes.Equal(configRaw, configModified) {
527-
err = writeWithTempFileAndMove(sshConfigFile, bytes.NewReader(configModified))
528+
err = atomic.WriteFile(sshConfigFile, bytes.NewReader(configModified))
528529
if err != nil {
529530
return xerrors.Errorf("write ssh config failed: %w", err)
530531
}
@@ -758,50 +759,6 @@ func sshConfigSplitOnCoderSection(data []byte) (before, section []byte, after []
758759
return data, nil, nil, nil
759760
}
760761

761-
// writeWithTempFileAndMove writes to a temporary file in the same
762-
// directory as path and renames the temp file to the file provided in
763-
// path. This ensure we avoid trashing the file we are writing due to
764-
// unforeseen circumstance like filesystem full, command killed, etc.
765-
func writeWithTempFileAndMove(path string, r io.Reader) (err error) {
766-
dir := filepath.Dir(path)
767-
name := filepath.Base(path)
768-
769-
// Ensure that e.g. the ~/.ssh directory exists.
770-
if err = os.MkdirAll(dir, 0o700); err != nil {
771-
return xerrors.Errorf("create directory: %w", err)
772-
}
773-
774-
// Create a tempfile in the same directory for ensuring write
775-
// operation does not fail.
776-
f, err := os.CreateTemp(dir, fmt.Sprintf(".%s.", name))
777-
if err != nil {
778-
return xerrors.Errorf("create temp file failed: %w", err)
779-
}
780-
defer func() {
781-
if err != nil {
782-
_ = os.Remove(f.Name()) // Cleanup in case a step failed.
783-
}
784-
}()
785-
786-
_, err = io.Copy(f, r)
787-
if err != nil {
788-
_ = f.Close()
789-
return xerrors.Errorf("write temp file failed: %w", err)
790-
}
791-
792-
err = f.Close()
793-
if err != nil {
794-
return xerrors.Errorf("close temp file failed: %w", err)
795-
}
796-
797-
err = os.Rename(f.Name(), path)
798-
if err != nil {
799-
return xerrors.Errorf("rename temp file failed: %w", err)
800-
}
801-
802-
return nil
803-
}
804-
805762
// sshConfigExecEscape quotes the string if it contains spaces, as per
806763
// `man 5 ssh_config`. However, OpenSSH uses exec in the users shell to
807764
// run the command, and as such the formatting/escape requirements

cli/help.go

+2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ var usageTemplate = func() *template.Template {
8181
switch v := opt.Value.(type) {
8282
case *serpent.Enum:
8383
return strings.Join(v.Choices, "|")
84+
case *serpent.EnumArray:
85+
return fmt.Sprintf("[%s]", strings.Join(v.Choices, "|"))
8486
default:
8587
return v.Type()
8688
}

cli/organizationmembers.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func (r *RootCmd) assignOrganizationRoles(orgContext *OrganizationContext) *serp
137137

138138
func (r *RootCmd) listOrganizationMembers(orgContext *OrganizationContext) *serpent.Command {
139139
formatter := cliui.NewOutputFormatter(
140-
cliui.TableFormat([]codersdk.OrganizationMemberWithUserData{}, []string{"username", "organization_roles"}),
140+
cliui.TableFormat([]codersdk.OrganizationMemberWithUserData{}, []string{"username", "organization roles"}),
141141
cliui.JSONFormat(),
142142
)
143143

cli/organizationmembers_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func TestListOrganizationMembers(t *testing.T) {
2323
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
2424

2525
ctx := testutil.Context(t, testutil.WaitMedium)
26-
inv, root := clitest.New(t, "organization", "members", "list", "-c", "user_id,username,roles")
26+
inv, root := clitest.New(t, "organization", "members", "list", "-c", "user id,username,organization roles")
2727
clitest.SetupConfig(t, client, root)
2828

2929
buf := new(bytes.Buffer)

cli/organizationroles.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func (r *RootCmd) organizationRoles(orgContext *OrganizationContext) *serpent.Co
3636
func (r *RootCmd) showOrganizationRoles(orgContext *OrganizationContext) *serpent.Command {
3737
formatter := cliui.NewOutputFormatter(
3838
cliui.ChangeFormatterData(
39-
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}),
39+
cliui.TableFormat([]roleTableRow{}, []string{"name", "display name", "site permissions", "organization permissions", "user permissions"}),
4040
func(data any) (any, error) {
4141
inputs, ok := data.([]codersdk.AssignableRoles)
4242
if !ok {
@@ -103,7 +103,7 @@ func (r *RootCmd) showOrganizationRoles(orgContext *OrganizationContext) *serpen
103103
func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent.Command {
104104
formatter := cliui.NewOutputFormatter(
105105
cliui.ChangeFormatterData(
106-
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}),
106+
cliui.TableFormat([]roleTableRow{}, []string{"name", "display name", "site permissions", "organization permissions", "user permissions"}),
107107
func(data any) (any, error) {
108108
typed, _ := data.(codersdk.Role)
109109
return []roleTableRow{roleToTableView(typed)}, nil
@@ -408,10 +408,10 @@ func roleToTableView(role codersdk.Role) roleTableRow {
408408

409409
type roleTableRow struct {
410410
Name string `table:"name,default_sort"`
411-
DisplayName string `table:"display_name"`
412-
OrganizationID string `table:"organization_id"`
413-
SitePermissions string ` table:"site_permissions"`
411+
DisplayName string `table:"display name"`
412+
OrganizationID string `table:"organization id"`
413+
SitePermissions string ` table:"site permissions"`
414414
// map[<org_id>] -> Permissions
415-
OrganizationPermissions string `table:"organization_permissions"`
416-
UserPermissions string `table:"user_permissions"`
415+
OrganizationPermissions string `table:"organization permissions"`
416+
UserPermissions string `table:"user permissions"`
417417
}

cli/root.go

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const (
8282
func (r *RootCmd) CoreSubcommands() []*serpent.Command {
8383
// Please re-sort this list alphabetically if you change it!
8484
return []*serpent.Command{
85+
r.completion(),
8586
r.dotfiles(),
8687
r.externalAuth(),
8788
r.login(),

cli/server.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import (
5656
"cdr.dev/slog"
5757
"cdr.dev/slog/sloggers/sloghuman"
5858
"github.com/coder/pretty"
59+
"github.com/coder/quartz"
5960
"github.com/coder/retry"
6061
"github.com/coder/serpent"
6162
"github.com/coder/wgtunnel/tunnelsdk"
@@ -791,18 +792,26 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
791792
}
792793
}
793794

794-
keyBytes, err := hex.DecodeString(oauthSigningKeyStr)
795+
oauthKeyBytes, err := hex.DecodeString(oauthSigningKeyStr)
795796
if err != nil {
796797
return xerrors.Errorf("decode oauth signing key from database: %w", err)
797798
}
798-
if len(keyBytes) != len(options.OAuthSigningKey) {
799-
return xerrors.Errorf("oauth signing key in database is not the correct length, expect %d got %d", len(options.OAuthSigningKey), len(keyBytes))
799+
if len(oauthKeyBytes) != len(options.OAuthSigningKey) {
800+
return xerrors.Errorf("oauth signing key in database is not the correct length, expect %d got %d", len(options.OAuthSigningKey), len(oauthKeyBytes))
800801
}
801-
copy(options.OAuthSigningKey[:], keyBytes)
802+
copy(options.OAuthSigningKey[:], oauthKeyBytes)
802803
if options.OAuthSigningKey == [32]byte{} {
803804
return xerrors.Errorf("oauth signing key in database is empty")
804805
}
805806

807+
// Read the coordinator resume token signing key from the
808+
// database.
809+
resumeTokenKey, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, tx)
810+
if err != nil {
811+
return xerrors.Errorf("get coordinator resume token key from database: %w", err)
812+
}
813+
options.CoordinatorResumeTokenProvider = tailnet.NewResumeTokenKeyProvider(resumeTokenKey, quartz.NewReal(), tailnet.DefaultResumeTokenExpiry)
814+
806815
return nil
807816
}, nil)
808817
if err != nil {

cli/stat.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ func (r *RootCmd) stat() *serpent.Command {
3232
fs = afero.NewReadOnlyFs(afero.NewOsFs())
3333
formatter = cliui.NewOutputFormatter(
3434
cliui.TableFormat([]statsRow{}, []string{
35-
"host_cpu",
36-
"host_memory",
37-
"home_disk",
38-
"container_cpu",
39-
"container_memory",
35+
"host cpu",
36+
"host memory",
37+
"home disk",
38+
"container cpu",
39+
"container memory",
4040
}),
4141
cliui.JSONFormat(),
4242
)
@@ -284,9 +284,9 @@ func (*RootCmd) statDisk(fs afero.Fs) *serpent.Command {
284284
}
285285

286286
type statsRow struct {
287-
HostCPU *clistat.Result `json:"host_cpu" table:"host_cpu,default_sort"`
288-
HostMemory *clistat.Result `json:"host_memory" table:"host_memory"`
289-
Disk *clistat.Result `json:"home_disk" table:"home_disk"`
290-
ContainerCPU *clistat.Result `json:"container_cpu" table:"container_cpu"`
291-
ContainerMemory *clistat.Result `json:"container_memory" table:"container_memory"`
287+
HostCPU *clistat.Result `json:"host_cpu" table:"host cpu,default_sort"`
288+
HostMemory *clistat.Result `json:"host_memory" table:"host memory"`
289+
Disk *clistat.Result `json:"home_disk" table:"home disk"`
290+
ContainerCPU *clistat.Result `json:"container_cpu" table:"container cpu"`
291+
ContainerMemory *clistat.Result `json:"container_memory" table:"container memory"`
292292
}

0 commit comments

Comments
 (0)