Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8065bcd

Browse files
committed
Merge branch 'main' into colin/update-tailscale
2 parents efbbb25 + 4827d9e commit 8065bcd

File tree

130 files changed

+2290
-1158
lines changed

Some content is hidden

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

130 files changed

+2290
-1158
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/[email protected]
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: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ type Client interface {
7575
ReportStats(ctx context.Context, log slog.Logger, statsChan <-chan *agentsdk.Stats, setInterval func(time.Duration)) (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 {
@@ -238,16 +238,29 @@ func (a *agent) run(ctx context.Context) error {
238238
}
239239
a.sessionToken.Store(&sessionToken)
240240

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

253266
// The startup script should only execute on the first run!
@@ -1328,3 +1341,20 @@ func userHomeDir() (string, error) {
13281341
}
13291342
return u.HomeDir, nil
13301343
}
1344+
1345+
// expandDirectory converts a directory path to an absolute path.
1346+
// It primarily resolves the home directory and any environment
1347+
// variables that may be set
1348+
func expandDirectory(dir string) (string, error) {
1349+
if dir == "" {
1350+
return "", nil
1351+
}
1352+
if dir[0] == '~' {
1353+
home, err := userHomeDir()
1354+
if err != nil {
1355+
return "", err
1356+
}
1357+
dir = filepath.Join(home, dir[1:])
1358+
}
1359+
return os.ExpandEnv(dir), nil
1360+
}

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" {
@@ -1177,6 +1227,7 @@ type client struct {
11771227

11781228
mu sync.Mutex // Protects following.
11791229
lifecycleStates []codersdk.WorkspaceAgentLifecycle
1230+
startup agentsdk.PostStartupRequest
11801231
}
11811232

11821233
func (c *client) Metadata(_ context.Context) (agentsdk.Metadata, error) {
@@ -1248,7 +1299,16 @@ func (*client) PostAppHealth(_ context.Context, _ agentsdk.PostAppHealthsRequest
12481299
return nil
12491300
}
12501301

1251-
func (*client) PostVersion(_ context.Context, _ string) error {
1302+
func (c *client) getStartup() agentsdk.PostStartupRequest {
1303+
c.mu.Lock()
1304+
defer c.mu.Unlock()
1305+
return c.startup
1306+
}
1307+
1308+
func (c *client) PostStartup(_ context.Context, startup agentsdk.PostStartupRequest) error {
1309+
c.mu.Lock()
1310+
defer c.mu.Unlock()
1311+
c.startup = startup
12521312
return nil
12531313
}
12541314

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)