Skip to content

Commit 275083e

Browse files
author
Yixin Hao
authored
Merge branch 'coder:main' into immoral-v0.17.0
2 parents abd40af + 4493649 commit 275083e

File tree

135 files changed

+2333
-1168
lines changed

Some content is hidden

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

135 files changed

+2333
-1168
lines changed

.github/workflows/release.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ jobs:
6363
6464
- name: Create release notes
6565
env:
66+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6667
# We always have to set this since there might be commits on
6768
# main that didn't have a PR.
6869
CODER_IGNORE_MISSING_COMMIT_METADATA: "1"
@@ -288,6 +289,7 @@ jobs:
288289
retention-days: 7
289290

290291
- name: Start Packer builds
292+
if: ${{ !inputs.dry_run }}
291293
uses: peter-evans/repository-dispatch@v2
292294
with:
293295
token: ${{ secrets.CDRCI_GITHUB_TOKEN }}

.github/workflows/security.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ jobs:
9292
restore-keys: |
9393
js-${{ runner.os }}-
9494
95+
- name: Install yq
96+
run: go run github.com/mikefarah/yq/v4@v4.30.6
97+
9598
- name: Build Coder linux amd64 Docker image
9699
id: build
97100
run: |
@@ -124,6 +127,7 @@ jobs:
124127
uses: github/codeql-action/upload-sarif@v2
125128
with:
126129
sarif_file: trivy-results.sarif
130+
category: "Trivy"
127131

128132
- name: Upload Trivy scan results as an artifact
129133
uses: actions/upload-artifact@v2

agent/agent.go

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ type Client interface {
7575
ReportStats(ctx context.Context, log slog.Logger, stats func() *agentsdk.Stats) (io.Closer, error)
7676
PostLifecycle(ctx context.Context, state agentsdk.PostLifecycleRequest) error
7777
PostAppHealth(ctx context.Context, req agentsdk.PostAppHealthsRequest) error
78-
PostVersion(ctx context.Context, version string) error
78+
PostStartup(ctx context.Context, req agentsdk.PostStartupRequest) error
7979
}
8080

8181
func New(options Options) io.Closer {
@@ -236,16 +236,29 @@ func (a *agent) run(ctx context.Context) error {
236236
}
237237
a.sessionToken.Store(&sessionToken)
238238

239-
err = a.client.PostVersion(ctx, buildinfo.Version())
240-
if err != nil {
241-
return xerrors.Errorf("update workspace agent version: %w", err)
242-
}
243-
244239
metadata, err := a.client.Metadata(ctx)
245240
if err != nil {
246241
return xerrors.Errorf("fetch metadata: %w", err)
247242
}
248243
a.logger.Info(ctx, "fetched metadata")
244+
245+
// Expand the directory and send it back to coderd so external
246+
// applications that rely on the directory can use it.
247+
//
248+
// An example is VS Code Remote, which must know the directory
249+
// before initializing a connection.
250+
metadata.Directory, err = expandDirectory(metadata.Directory)
251+
if err != nil {
252+
return xerrors.Errorf("expand directory: %w", err)
253+
}
254+
err = a.client.PostStartup(ctx, agentsdk.PostStartupRequest{
255+
Version: buildinfo.Version(),
256+
ExpandedDirectory: metadata.Directory,
257+
})
258+
if err != nil {
259+
return xerrors.Errorf("update workspace agent version: %w", err)
260+
}
261+
249262
oldMetadata := a.metadata.Swap(metadata)
250263

251264
// The startup script should only execute on the first run!
@@ -803,7 +816,11 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
803816

804817
cmd := exec.CommandContext(ctx, shell, args...)
805818
cmd.Dir = metadata.Directory
806-
if cmd.Dir == "" {
819+
820+
// If the metadata directory doesn't exist, we run the command
821+
// in the users home directory.
822+
_, err = os.Stat(cmd.Dir)
823+
if cmd.Dir == "" || err != nil {
807824
// Default to user home if a directory is not set.
808825
homedir, err := userHomeDir()
809826
if err != nil {
@@ -1314,3 +1331,20 @@ func userHomeDir() (string, error) {
13141331
}
13151332
return u.HomeDir, nil
13161333
}
1334+
1335+
// expandDirectory converts a directory path to an absolute path.
1336+
// It primarily resolves the home directory and any environment
1337+
// variables that may be set
1338+
func expandDirectory(dir string) (string, error) {
1339+
if dir == "" {
1340+
return "", nil
1341+
}
1342+
if dir[0] == '~' {
1343+
home, err := userHomeDir()
1344+
if err != nil {
1345+
return "", err
1346+
}
1347+
dir = filepath.Join(home, dir[1:])
1348+
}
1349+
return os.ExpandEnv(dir), nil
1350+
}

agent/agent_test.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,56 @@ func TestAgent_Lifecycle(t *testing.T) {
787787
})
788788
}
789789

790+
func TestAgent_Startup(t *testing.T) {
791+
t.Parallel()
792+
793+
t.Run("EmptyDirectory", func(t *testing.T) {
794+
t.Parallel()
795+
796+
_, client, _, _ := setupAgent(t, agentsdk.Metadata{
797+
StartupScript: "true",
798+
StartupScriptTimeout: 30 * time.Second,
799+
Directory: "",
800+
}, 0)
801+
assert.Eventually(t, func() bool {
802+
return client.getStartup().Version != ""
803+
}, testutil.WaitShort, testutil.IntervalFast)
804+
require.Equal(t, "", client.getStartup().ExpandedDirectory)
805+
})
806+
807+
t.Run("HomeDirectory", func(t *testing.T) {
808+
t.Parallel()
809+
810+
_, client, _, _ := setupAgent(t, agentsdk.Metadata{
811+
StartupScript: "true",
812+
StartupScriptTimeout: 30 * time.Second,
813+
Directory: "~",
814+
}, 0)
815+
assert.Eventually(t, func() bool {
816+
return client.getStartup().Version != ""
817+
}, testutil.WaitShort, testutil.IntervalFast)
818+
homeDir, err := os.UserHomeDir()
819+
require.NoError(t, err)
820+
require.Equal(t, homeDir, client.getStartup().ExpandedDirectory)
821+
})
822+
823+
t.Run("HomeEnvironmentVariable", func(t *testing.T) {
824+
t.Parallel()
825+
826+
_, client, _, _ := setupAgent(t, agentsdk.Metadata{
827+
StartupScript: "true",
828+
StartupScriptTimeout: 30 * time.Second,
829+
Directory: "$HOME",
830+
}, 0)
831+
assert.Eventually(t, func() bool {
832+
return client.getStartup().Version != ""
833+
}, testutil.WaitShort, testutil.IntervalFast)
834+
homeDir, err := os.UserHomeDir()
835+
require.NoError(t, err)
836+
require.Equal(t, homeDir, client.getStartup().ExpandedDirectory)
837+
})
838+
}
839+
790840
func TestAgent_ReconnectingPTY(t *testing.T) {
791841
t.Parallel()
792842
if runtime.GOOS == "windows" {
@@ -1178,6 +1228,7 @@ type client struct {
11781228

11791229
mu sync.Mutex // Protects following.
11801230
lifecycleStates []codersdk.WorkspaceAgentLifecycle
1231+
startup agentsdk.PostStartupRequest
11811232
}
11821233

11831234
func (c *client) Metadata(_ context.Context) (agentsdk.Metadata, error) {
@@ -1250,7 +1301,16 @@ func (*client) PostAppHealth(_ context.Context, _ agentsdk.PostAppHealthsRequest
12501301
return nil
12511302
}
12521303

1253-
func (*client) PostVersion(_ context.Context, _ string) error {
1304+
func (c *client) getStartup() agentsdk.PostStartupRequest {
1305+
c.mu.Lock()
1306+
defer c.mu.Unlock()
1307+
return c.startup
1308+
}
1309+
1310+
func (c *client) PostStartup(_ context.Context, startup agentsdk.PostStartupRequest) error {
1311+
c.mu.Lock()
1312+
defer c.mu.Unlock()
1313+
c.startup = startup
12541314
return nil
12551315
}
12561316

cli/cliui/output.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package cliui
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"reflect"
7+
"strings"
8+
9+
"github.com/spf13/cobra"
10+
"golang.org/x/xerrors"
11+
)
12+
13+
type OutputFormat interface {
14+
ID() string
15+
AttachFlags(cmd *cobra.Command)
16+
Format(ctx context.Context, data any) (string, error)
17+
}
18+
19+
type OutputFormatter struct {
20+
formats []OutputFormat
21+
formatID string
22+
}
23+
24+
// NewOutputFormatter creates a new OutputFormatter with the given formats. The
25+
// first format is the default format. At least two formats must be provided.
26+
func NewOutputFormatter(formats ...OutputFormat) *OutputFormatter {
27+
if len(formats) < 2 {
28+
panic("at least two output formats must be provided")
29+
}
30+
31+
formatIDs := make(map[string]struct{}, len(formats))
32+
for _, format := range formats {
33+
if format.ID() == "" {
34+
panic("output format ID must not be empty")
35+
}
36+
if _, ok := formatIDs[format.ID()]; ok {
37+
panic("duplicate format ID: " + format.ID())
38+
}
39+
formatIDs[format.ID()] = struct{}{}
40+
}
41+
42+
return &OutputFormatter{
43+
formats: formats,
44+
formatID: formats[0].ID(),
45+
}
46+
}
47+
48+
// AttachFlags attaches the --output flag to the given command, and any
49+
// additional flags required by the output formatters.
50+
func (f *OutputFormatter) AttachFlags(cmd *cobra.Command) {
51+
for _, format := range f.formats {
52+
format.AttachFlags(cmd)
53+
}
54+
55+
formatNames := make([]string, 0, len(f.formats))
56+
for _, format := range f.formats {
57+
formatNames = append(formatNames, format.ID())
58+
}
59+
60+
cmd.Flags().StringVarP(&f.formatID, "output", "o", f.formats[0].ID(), "Output format. Available formats: "+strings.Join(formatNames, ", "))
61+
}
62+
63+
// Format formats the given data using the format specified by the --output
64+
// flag. If the flag is not set, the default format is used.
65+
func (f *OutputFormatter) Format(ctx context.Context, data any) (string, error) {
66+
for _, format := range f.formats {
67+
if format.ID() == f.formatID {
68+
return format.Format(ctx, data)
69+
}
70+
}
71+
72+
return "", xerrors.Errorf("unknown output format %q", f.formatID)
73+
}
74+
75+
type tableFormat struct {
76+
defaultColumns []string
77+
allColumns []string
78+
sort string
79+
80+
columns []string
81+
}
82+
83+
var _ OutputFormat = &tableFormat{}
84+
85+
// TableFormat creates a table formatter for the given output type. The output
86+
// type should be specified as an empty slice of the desired type.
87+
//
88+
// E.g.: TableFormat([]MyType{}, []string{"foo", "bar"})
89+
//
90+
// defaultColumns is optional and specifies the default columns to display. If
91+
// not specified, all columns are displayed by default.
92+
func TableFormat(out any, defaultColumns []string) OutputFormat {
93+
v := reflect.Indirect(reflect.ValueOf(out))
94+
if v.Kind() != reflect.Slice {
95+
panic("DisplayTable called with a non-slice type")
96+
}
97+
98+
// Get the list of table column headers.
99+
headers, defaultSort, err := typeToTableHeaders(v.Type().Elem())
100+
if err != nil {
101+
panic("parse table headers: " + err.Error())
102+
}
103+
104+
tf := &tableFormat{
105+
defaultColumns: headers,
106+
allColumns: headers,
107+
sort: defaultSort,
108+
}
109+
if len(defaultColumns) > 0 {
110+
tf.defaultColumns = defaultColumns
111+
}
112+
113+
return tf
114+
}
115+
116+
// ID implements OutputFormat.
117+
func (*tableFormat) ID() string {
118+
return "table"
119+
}
120+
121+
// AttachFlags implements OutputFormat.
122+
func (f *tableFormat) AttachFlags(cmd *cobra.Command) {
123+
cmd.Flags().StringSliceVarP(&f.columns, "column", "c", f.defaultColumns, "Columns to display in table output. Available columns: "+strings.Join(f.allColumns, ", "))
124+
}
125+
126+
// Format implements OutputFormat.
127+
func (f *tableFormat) Format(_ context.Context, data any) (string, error) {
128+
return DisplayTable(data, f.sort, f.columns)
129+
}
130+
131+
type jsonFormat struct{}
132+
133+
var _ OutputFormat = jsonFormat{}
134+
135+
// JSONFormat creates a JSON formatter.
136+
func JSONFormat() OutputFormat {
137+
return jsonFormat{}
138+
}
139+
140+
// ID implements OutputFormat.
141+
func (jsonFormat) ID() string {
142+
return "json"
143+
}
144+
145+
// AttachFlags implements OutputFormat.
146+
func (jsonFormat) AttachFlags(_ *cobra.Command) {}
147+
148+
// Format implements OutputFormat.
149+
func (jsonFormat) Format(_ context.Context, data any) (string, error) {
150+
outBytes, err := json.MarshalIndent(data, "", " ")
151+
if err != nil {
152+
return "", xerrors.Errorf("marshal output to JSON: %w", err)
153+
}
154+
155+
return string(outBytes), nil
156+
}

0 commit comments

Comments
 (0)