Skip to content

Commit 4ea68bc

Browse files
committed
Merge branch 'main' into dean/listening-ports
2 parents 3962635 + bbe2baf commit 4ea68bc

File tree

253 files changed

+5572
-3736
lines changed

Some content is hidden

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

253 files changed

+5572
-3736
lines changed

.github/workflows/coder.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ jobs:
489489
go mod download
490490
491491
version="$(./scripts/version.sh)"
492+
make gen/mark-fresh
492493
make -j \
493494
build/coder_"$version"_windows_amd64.zip \
494495
build/coder_"$version"_linux_amd64.{tar.gz,deb}

.github/workflows/dogfood.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
steps:
2020
- name: Get branch name
2121
id: branch-name
22-
uses: tj-actions/branch-names@v5.4
22+
uses: tj-actions/branch-names@v6.1
2323

2424
- name: "Branch name to Docker tag name"
2525
id: docker-tag-name

.github/workflows/stale.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
steps:
1414
# v5.1.0 has a weird bug that makes stalebot add then remove its own label
1515
# https://github.com/actions/stale/pull/775
16-
- uses: actions/stale@v5.0.0
16+
- uses: actions/stale@v6.0.0
1717
with:
1818
stale-issue-label: stale
1919
stale-pr-label: stale

.gitignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ site/**/*.typegen.ts
3131
site/build-storybook.log
3232

3333
# Build
34-
build/
35-
dist/
34+
/build/
35+
/dist/
3636
site/out/
3737

3838
*.tfstate

cli/agent_test.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ func TestWorkspaceAgent(t *testing.T) {
6060
ctx := context.WithValue(ctx, "azure-client", metadataClient)
6161
errC <- cmd.ExecuteContext(ctx)
6262
}()
63-
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
64-
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
63+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
64+
workspace, err := client.Workspace(ctx, workspace.ID)
6565
require.NoError(t, err)
66-
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
66+
resources := workspace.LatestBuild.Resources
67+
if assert.NotEmpty(t, workspace.LatestBuild.Resources) && assert.NotEmpty(t, resources[0].Agents) {
6768
assert.NotEmpty(t, resources[0].Agents[0].Version)
6869
}
6970
dialer, err := client.DialWorkspaceAgentTailnet(ctx, slog.Logger{}, resources[0].Agents[0].ID)
@@ -120,9 +121,10 @@ func TestWorkspaceAgent(t *testing.T) {
120121
ctx := context.WithValue(ctx, "aws-client", metadataClient)
121122
errC <- cmd.ExecuteContext(ctx)
122123
}()
123-
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
124-
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
124+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
125+
workspace, err := client.Workspace(ctx, workspace.ID)
125126
require.NoError(t, err)
127+
resources := workspace.LatestBuild.Resources
126128
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
127129
assert.NotEmpty(t, resources[0].Agents[0].Version)
128130
}
@@ -180,9 +182,10 @@ func TestWorkspaceAgent(t *testing.T) {
180182
ctx := context.WithValue(ctx, "gcp-client", metadata)
181183
errC <- cmd.ExecuteContext(ctx)
182184
}()
183-
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
184-
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
185+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
186+
workspace, err := client.Workspace(ctx, workspace.ID)
185187
require.NoError(t, err)
188+
resources := workspace.LatestBuild.Resources
186189
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
187190
assert.NotEmpty(t, resources[0].Agents[0].Version)
188191
}

cli/cliui/table.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
6767
}
6868

6969
// Get the list of table column headers.
70-
headersRaw, err := TypeToTableHeaders(v.Type().Elem())
70+
headersRaw, err := typeToTableHeaders(v.Type().Elem())
7171
if err != nil {
7272
return "", xerrors.Errorf("get table headers recursively for type %q: %w", v.Type().Elem().String(), err)
7373
}
@@ -207,10 +207,10 @@ func isStructOrStructPointer(t reflect.Type) bool {
207207
return t.Kind() == reflect.Struct || (t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Struct)
208208
}
209209

210-
// TypeToTableHeaders converts a type to a slice of column names. If the given
210+
// typeToTableHeaders converts a type to a slice of column names. If the given
211211
// type is invalid (not a struct or a pointer to a struct, has invalid table
212212
// tags, etc.), an error is returned.
213-
func TypeToTableHeaders(t reflect.Type) ([]string, error) {
213+
func typeToTableHeaders(t reflect.Type) ([]string, error) {
214214
if !isStructOrStructPointer(t) {
215215
return nil, xerrors.Errorf("typeToTableHeaders called with a non-struct or a non-pointer-to-a-struct type")
216216
}
@@ -235,7 +235,7 @@ func TypeToTableHeaders(t reflect.Type) ([]string, error) {
235235
return nil, xerrors.Errorf("field %q in type %q is marked as recursive but does not contain a struct or a pointer to a struct", field.Name, t.String())
236236
}
237237

238-
childNames, err := TypeToTableHeaders(fieldType)
238+
childNames, err := typeToTableHeaders(fieldType)
239239
if err != nil {
240240
return nil, xerrors.Errorf("get child field header names for field %q in type %q: %w", field.Name, fieldType.String(), err)
241241
}
@@ -305,3 +305,18 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
305305

306306
return row, nil
307307
}
308+
309+
// TableHeaders returns the table header names of all
310+
// fields in tSlice. tSlice must be a slice of some type.
311+
func TableHeaders(tSlice any) ([]string, error) {
312+
v := reflect.Indirect(reflect.ValueOf(tSlice))
313+
rawHeaders, err := typeToTableHeaders(v.Type().Elem())
314+
if err != nil {
315+
return nil, xerrors.Errorf("type to table headers: %w", err)
316+
}
317+
out := make([]string, 0, len(rawHeaders))
318+
for _, hdr := range rawHeaders {
319+
out = append(out, strings.Replace(hdr, " ", "_", -1))
320+
}
321+
return out, nil
322+
}

cli/cliui/table_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,28 @@ baz baz1 baz3 Aug 2 15:49:10
327327
})
328328
}
329329

330+
func Test_TableHeaders(t *testing.T) {
331+
t.Parallel()
332+
s := []tableTest1{}
333+
expectedFields := []string{
334+
"name",
335+
"age",
336+
"roles",
337+
"sub_1_name",
338+
"sub_1_age",
339+
"sub_2_name",
340+
"sub_2_age",
341+
"sub_3_inner_name",
342+
"sub_3_inner_age",
343+
"sub_4",
344+
"time",
345+
"time_ptr",
346+
}
347+
headers, err := cliui.TableHeaders(s)
348+
require.NoError(t, err)
349+
require.EqualValues(t, expectedFields, headers)
350+
}
351+
330352
// compareTables normalizes the incoming table lines
331353
func compareTables(t *testing.T, expected, out string) {
332354
t.Helper()

cli/configssh_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func TestConfigSSH(t *testing.T) {
114114
defer func() {
115115
_ = agentCloser.Close()
116116
}()
117-
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
117+
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
118118
agentConn, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, resources[0].Agents[0].ID)
119119
require.NoError(t, err)
120120
defer agentConn.Close()

cli/gitssh_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func prepareTestGitSSH(ctx context.Context, t *testing.T) (*codersdk.Client, str
8181
errC <- cmd.ExecuteContext(ctx)
8282
}()
8383
t.Cleanup(func() { require.NoError(t, <-errC) })
84-
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
84+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
8585
return agentClient, agentToken, pubkey
8686
}
8787

cli/list.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cli
22

33
import (
44
"fmt"
5-
"reflect"
65
"strings"
76
"time"
87

@@ -128,14 +127,10 @@ func list() *cobra.Command {
128127
},
129128
}
130129

131-
v := reflect.Indirect(reflect.ValueOf(displayWorkspaces))
132-
availColumns, err := cliui.TypeToTableHeaders(v.Type().Elem())
130+
availColumns, err := cliui.TableHeaders(displayWorkspaces)
133131
if err != nil {
134132
panic(err)
135133
}
136-
for i, s := range availColumns {
137-
availColumns[i] = strings.Replace(s, " ", "_", -1)
138-
}
139134
columnString := strings.Join(availColumns[:], ", ")
140135

141136
cmd.Flags().BoolVarP(&all, "all", "a", false,

cli/portforward.go

+92-31
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
"cdr.dev/slog"
2020
"github.com/coder/coder/agent"
21+
"github.com/coder/coder/cli/cliflag"
2122
"github.com/coder/coder/cli/cliui"
2223
"github.com/coder/coder/codersdk"
2324
)
@@ -45,6 +46,10 @@ func portForward() *cobra.Command {
4546
Description: "Port forward multiple TCP ports and a UDP port",
4647
Command: "coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53",
4748
},
49+
example{
50+
Description: "Port forward multiple ports (TCP or UDP) in condensed syntax",
51+
Command: "coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012",
52+
},
4853
),
4954
RunE: func(cmd *cobra.Command, args []string) error {
5055
ctx, cancel := context.WithCancel(cmd.Context())
@@ -164,8 +169,8 @@ func portForward() *cobra.Command {
164169
},
165170
}
166171

167-
cmd.Flags().StringArrayVarP(&tcpForwards, "tcp", "p", []string{}, "Forward a TCP port from the workspace to the local machine")
168-
cmd.Flags().StringArrayVar(&udpForwards, "udp", []string{}, "Forward a UDP port from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols")
172+
cliflag.StringArrayVarP(cmd.Flags(), &tcpForwards, "tcp", "p", "CODER_PORT_FORWARD_TCP", nil, "Forward TCP port(s) from the workspace to the local machine")
173+
cliflag.StringArrayVarP(cmd.Flags(), &udpForwards, "udp", "", "CODER_PORT_FORWARD_UDP", nil, "Forward UDP port(s) from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols")
169174
return cmd
170175
}
171176

@@ -242,32 +247,40 @@ type portForwardSpec struct {
242247
func parsePortForwards(tcpSpecs, udpSpecs []string) ([]portForwardSpec, error) {
243248
specs := []portForwardSpec{}
244249

245-
for _, spec := range tcpSpecs {
246-
local, remote, err := parsePortPort(spec)
247-
if err != nil {
248-
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
249-
}
250+
for _, specEntry := range tcpSpecs {
251+
for _, spec := range strings.Split(specEntry, ",") {
252+
ports, err := parseSrcDestPorts(spec)
253+
if err != nil {
254+
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
255+
}
250256

251-
specs = append(specs, portForwardSpec{
252-
listenNetwork: "tcp",
253-
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
254-
dialNetwork: "tcp",
255-
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
256-
})
257+
for _, port := range ports {
258+
specs = append(specs, portForwardSpec{
259+
listenNetwork: "tcp",
260+
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
261+
dialNetwork: "tcp",
262+
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
263+
})
264+
}
265+
}
257266
}
258267

259-
for _, spec := range udpSpecs {
260-
local, remote, err := parsePortPort(spec)
261-
if err != nil {
262-
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
263-
}
268+
for _, specEntry := range udpSpecs {
269+
for _, spec := range strings.Split(specEntry, ",") {
270+
ports, err := parseSrcDestPorts(spec)
271+
if err != nil {
272+
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
273+
}
264274

265-
specs = append(specs, portForwardSpec{
266-
listenNetwork: "udp",
267-
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
268-
dialNetwork: "udp",
269-
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
270-
})
275+
for _, port := range ports {
276+
specs = append(specs, portForwardSpec{
277+
listenNetwork: "udp",
278+
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
279+
dialNetwork: "udp",
280+
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
281+
})
282+
}
283+
}
271284
}
272285

273286
// Check for duplicate entries.
@@ -295,24 +308,72 @@ func parsePort(in string) (uint16, error) {
295308
return uint16(port), nil
296309
}
297310

298-
func parsePortPort(in string) (local uint16, remote uint16, err error) {
311+
type parsedSrcDestPort struct {
312+
local, remote uint16
313+
}
314+
315+
func parseSrcDestPorts(in string) ([]parsedSrcDestPort, error) {
299316
parts := strings.Split(in, ":")
300317
if len(parts) > 2 {
301-
return 0, 0, xerrors.Errorf("invalid port specification %q", in)
318+
return nil, xerrors.Errorf("invalid port specification %q", in)
302319
}
303320
if len(parts) == 1 {
304321
// Duplicate the single part
305322
parts = append(parts, parts[0])
306323
}
324+
if !strings.Contains(parts[0], "-") {
325+
local, err := parsePort(parts[0])
326+
if err != nil {
327+
return nil, xerrors.Errorf("parse local port from %q: %w", in, err)
328+
}
329+
remote, err := parsePort(parts[1])
330+
if err != nil {
331+
return nil, xerrors.Errorf("parse remote port from %q: %w", in, err)
332+
}
307333

308-
local, err = parsePort(parts[0])
334+
return []parsedSrcDestPort{{local: local, remote: remote}}, nil
335+
}
336+
337+
local, err := parsePortRange(parts[0])
309338
if err != nil {
310-
return 0, 0, xerrors.Errorf("parse local port from %q: %w", in, err)
339+
return nil, xerrors.Errorf("parse local port range from %q: %w", in, err)
311340
}
312-
remote, err = parsePort(parts[1])
341+
remote, err := parsePortRange(parts[1])
313342
if err != nil {
314-
return 0, 0, xerrors.Errorf("parse remote port from %q: %w", in, err)
343+
return nil, xerrors.Errorf("parse remote port range from %q: %w", in, err)
344+
}
345+
if len(local) != len(remote) {
346+
return nil, xerrors.Errorf("port ranges must be the same length, got %d ports forwarded to %d ports", len(local), len(remote))
347+
}
348+
var out []parsedSrcDestPort
349+
for i := range local {
350+
out = append(out, parsedSrcDestPort{
351+
local: local[i],
352+
remote: remote[i],
353+
})
315354
}
355+
return out, nil
356+
}
316357

317-
return local, remote, nil
358+
func parsePortRange(in string) ([]uint16, error) {
359+
parts := strings.Split(in, "-")
360+
if len(parts) != 2 {
361+
return nil, xerrors.Errorf("invalid port range specification %q", in)
362+
}
363+
start, err := parsePort(parts[0])
364+
if err != nil {
365+
return nil, xerrors.Errorf("parse range start port from %q: %w", in, err)
366+
}
367+
end, err := parsePort(parts[1])
368+
if err != nil {
369+
return nil, xerrors.Errorf("parse range end port from %q: %w", in, err)
370+
}
371+
if end < start {
372+
return nil, xerrors.Errorf("range end port %v is less than start port %v", end, start)
373+
}
374+
var ports []uint16
375+
for i := start; i <= end; i++ {
376+
ports = append(ports, i)
377+
}
378+
return ports, nil
318379
}

0 commit comments

Comments
 (0)