Skip to content

Commit 03c697d

Browse files
committed
Merge remote-tracking branch 'origin/main' into stevenmasley/unnamed-apps
2 parents a7e5bd6 + 611ca55 commit 03c697d

File tree

24 files changed

+767
-161
lines changed

24 files changed

+767
-161
lines changed

.github/workflows/dogfood.yaml

+9-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ jobs:
2121
id: branch-name
2222
uses: tj-actions/branch-names@v5.4
2323

24+
- name: "Branch name to Docker tag name"
25+
id: docker-tag-name
26+
run: |
27+
tag=${{ steps.branch-name.outputs.current_branch }}
28+
# Replace / with --, e.g. user/feature => user--feature.
29+
tag=${tag//\//--}
30+
echo "::set-output name=tag::${tag}"
31+
2432
- name: Set up QEMU
2533
uses: docker/setup-qemu-action@v2
2634

@@ -38,6 +46,6 @@ jobs:
3846
with:
3947
context: "{{defaultContext}}:dogfood"
4048
push: true
41-
tags: "codercom/oss-dogfood:${{ steps.branch-name.outputs.current_branch }},codercom/oss-dogfood:latest"
49+
tags: "codercom/oss-dogfood:${{ steps.docker-tag-name.outputs.tag }},codercom/oss-dogfood:latest"
4250
cache-from: type=registry,ref=codercom/oss-dogfood:latest
4351
cache-to: type=inline

Makefile

+3-3
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ build: site/out/index.html $(shell find . -not -path './vendor/*' -type f -name
5656
.PHONY: build
5757

5858
# Runs migrations to output a dump of the database.
59-
coderd/database/dump.sql: coderd/database/dump/main.go $(wildcard coderd/database/migrations/*.sql)
60-
go run coderd/database/dump/main.go
59+
coderd/database/dump.sql: coderd/database/gen/dump/main.go $(wildcard coderd/database/migrations/*.sql)
60+
go run coderd/database/gen/dump/main.go
6161

6262
# Generates Go code for querying the database.
63-
coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
63+
coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql) coderd/database/gen/enum/main.go
6464
coderd/database/generate.sh
6565

6666
fmt/prettier:

cli/configssh.go

+2-8
Original file line numberDiff line numberDiff line change
@@ -280,16 +280,10 @@ func configSSH() *cobra.Command {
280280
"\tLogLevel ERROR",
281281
)
282282
if !skipProxyCommand {
283-
// In SSH configs, strings inside "" are interpreted literally and there
284-
// is no need to e.g. escape backslashes (common on Windows platforms).
285-
// We will escape the quotes, though.
286-
escapedBinaryFile := strings.ReplaceAll(binaryFile, "\"", "\\\"")
287283
if !wireguard {
288-
//nolint:gocritic // We don't want to use %q here, see above.
289-
configOptions = append(configOptions, fmt.Sprintf("\tProxyCommand \"%s\" --global-config \"%s\" ssh --stdio %s", escapedBinaryFile, root, hostname))
284+
configOptions = append(configOptions, fmt.Sprintf("\tProxyCommand %q --global-config %q ssh --stdio %s", binaryFile, root, hostname))
290285
} else {
291-
//nolint:gocritic // We don't want to use %q here, see above.
292-
configOptions = append(configOptions, fmt.Sprintf("\tProxyCommand \"%s\" --global-config \"%s\" ssh --wireguard --stdio %s", escapedBinaryFile, root, hostname))
286+
configOptions = append(configOptions, fmt.Sprintf("\tProxyCommand %q --global-config %q ssh --wireguard --stdio %s", binaryFile, root, hostname))
293287
}
294288
}
295289

cli/server.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -659,13 +659,13 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
659659
root.AddCommand(&cobra.Command{
660660
Use: "postgres-builtin-url",
661661
Short: "Output the connection URL for the built-in PostgreSQL deployment.",
662-
RunE: func(cmd *cobra.Command, args []string) error {
662+
RunE: func(cmd *cobra.Command, _ []string) error {
663663
cfg := createConfig(cmd)
664664
url, err := embeddedPostgresURL(cfg)
665665
if err != nil {
666666
return err
667667
}
668-
cmd.Println(cliui.Styles.Code.Render("psql \"" + url + "\""))
668+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "psql %q\n", url)
669669
return nil
670670
},
671671
})

coderd/coderd.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -131,22 +131,25 @@ func New(options *Options) *API {
131131
})
132132
},
133133
httpmw.Prometheus(options.PrometheusRegistry),
134+
api.handleSubdomain,
134135
)
135136

136137
apps := func(r chi.Router) {
137138
r.Use(
138139
httpmw.RateLimitPerMinute(options.APIRateLimit),
140+
tracing.HTTPMW(api.TracerProvider, "coderd.http"),
139141
httpmw.ExtractAPIKey(options.Database, oauthConfigs, true),
140142
httpmw.ExtractUserParam(api.Database),
141-
tracing.HTTPMW(api.TracerProvider, "coderd.http"),
143+
// Extracts the <workspace.agent> from the url
144+
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
142145
)
143146
r.HandleFunc("/*", api.workspaceAppsProxyPath)
144147
}
145148
// %40 is the encoded character of the @ symbol. VS Code Web does
146149
// not handle character encoding properly, so it's safe to assume
147150
// other applications might not as well.
148-
r.Route("/%40{user}/{workspacename}/apps/{workspaceapp}", apps)
149-
r.Route("/@{user}/{workspacename}/apps/{workspaceapp}", apps)
151+
r.Route("/%40{user}/{workspace_and_agent}/apps/{workspaceapp}", apps)
152+
r.Route("/@{user}/{workspace_and_agent}/apps/{workspaceapp}", apps)
150153

151154
r.Route("/api/v2", func(r chi.Router) {
152155
r.NotFound(func(rw http.ResponseWriter, r *http.Request) {

coderd/coderdtest/authtest.go

+42-25
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func NewAuthTester(ctx context.Context, t *testing.T, options *Options) *AuthTes
8585
Name: "some",
8686
Type: "example",
8787
Agents: []*proto.Agent{{
88+
Name: "agent",
8889
Id: "something",
8990
Auth: &proto.Agent_Token{},
9091
Apps: []*proto.App{{
@@ -119,22 +120,23 @@ func NewAuthTester(ctx context.Context, t *testing.T, options *Options) *AuthTes
119120
require.NoError(t, err, "create template param")
120121

121122
urlParameters := map[string]string{
122-
"{organization}": admin.OrganizationID.String(),
123-
"{user}": admin.UserID.String(),
124-
"{organizationname}": organization.Name,
125-
"{workspace}": workspace.ID.String(),
126-
"{workspacebuild}": workspace.LatestBuild.ID.String(),
127-
"{workspacename}": workspace.Name,
128-
"{workspacebuildname}": workspace.LatestBuild.Name,
129-
"{workspaceagent}": workspaceResources[0].Agents[0].ID.String(),
130-
"{buildnumber}": strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10),
131-
"{template}": template.ID.String(),
132-
"{hash}": file.Hash,
133-
"{workspaceresource}": workspaceResources[0].ID.String(),
134-
"{workspaceapp}": workspaceResources[0].Agents[0].Apps[0].Name,
135-
"{templateversion}": version.ID.String(),
136-
"{jobID}": templateVersionDryRun.ID.String(),
137-
"{templatename}": template.Name,
123+
"{organization}": admin.OrganizationID.String(),
124+
"{user}": admin.UserID.String(),
125+
"{organizationname}": organization.Name,
126+
"{workspace}": workspace.ID.String(),
127+
"{workspacebuild}": workspace.LatestBuild.ID.String(),
128+
"{workspacename}": workspace.Name,
129+
"{workspacebuildname}": workspace.LatestBuild.Name,
130+
"{workspaceagent}": workspaceResources[0].Agents[0].ID.String(),
131+
"{buildnumber}": strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10),
132+
"{template}": template.ID.String(),
133+
"{hash}": file.Hash,
134+
"{workspaceresource}": workspaceResources[0].ID.String(),
135+
"{workspaceapp}": workspaceResources[0].Agents[0].Apps[0].Name,
136+
"{templateversion}": version.ID.String(),
137+
"{jobID}": templateVersionDryRun.ID.String(),
138+
"{templatename}": template.Name,
139+
"{workspace_and_agent}": workspace.Name + "." + workspaceResources[0].Agents[0].Name,
138140
// Only checking template scoped params here
139141
"parameters/{scope}/{id}": fmt.Sprintf("parameters/%s/%s",
140142
string(templateParam.Scope), templateParam.ScopeID.String()),
@@ -178,15 +180,6 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
178180
"POST:/api/v2/csp/reports": {NoAuthorize: true},
179181
"GET:/api/v2/entitlements": {NoAuthorize: true},
180182

181-
"GET:/%40{user}/{workspacename}/apps/{workspaceapp}/*": {
182-
AssertAction: rbac.ActionCreate,
183-
AssertObject: workspaceExecObj,
184-
},
185-
"GET:/@{user}/{workspacename}/apps/{workspaceapp}/*": {
186-
AssertAction: rbac.ActionCreate,
187-
AssertObject: workspaceExecObj,
188-
},
189-
190183
// Has it's own auth
191184
"GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true},
192185
"GET:/api/v2/users/oidc/callback": {NoAuthorize: true},
@@ -399,6 +392,29 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
399392
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
400393
"POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
401394
}
395+
396+
// Routes like proxy routes support all HTTP methods. A helper func to expand
397+
// 1 url to all http methods.
398+
assertAllHTTPMethods := func(url string, check RouteCheck) {
399+
methods := []string{http.MethodGet, http.MethodHead, http.MethodPost,
400+
http.MethodPut, http.MethodPatch, http.MethodDelete,
401+
http.MethodConnect, http.MethodOptions, http.MethodTrace}
402+
403+
for _, method := range methods {
404+
route := method + ":" + url
405+
assertRoute[route] = check
406+
}
407+
}
408+
409+
assertAllHTTPMethods("/%40{user}/{workspace_and_agent}/apps/{workspaceapp}/*", RouteCheck{
410+
AssertAction: rbac.ActionCreate,
411+
AssertObject: workspaceExecObj,
412+
})
413+
assertAllHTTPMethods("/@{user}/{workspace_and_agent}/apps/{workspaceapp}/*", RouteCheck{
414+
AssertAction: rbac.ActionCreate,
415+
AssertObject: workspaceExecObj,
416+
})
417+
402418
return skipRoutes, assertRoute
403419
}
404420

@@ -446,6 +462,7 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
446462
a.t.Run(name, func(t *testing.T) {
447463
a.authorizer.reset()
448464
routeKey := strings.TrimRight(name, "/")
465+
449466
routeAssertions, ok := assertRoute[routeKey]
450467
if !ok {
451468
// By default, all omitted routes check for just "authorize" called

coderd/database/databasefake/databasefake.go

+2-8
Original file line numberDiff line numberDiff line change
@@ -592,14 +592,14 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, wo
592592
defer q.mutex.RUnlock()
593593

594594
var row database.WorkspaceBuild
595-
var buildNum int32
595+
var buildNum int32 = -1
596596
for _, workspaceBuild := range q.workspaceBuilds {
597597
if workspaceBuild.WorkspaceID.String() == workspaceID.String() && workspaceBuild.BuildNumber > buildNum {
598598
row = workspaceBuild
599599
buildNum = workspaceBuild.BuildNumber
600600
}
601601
}
602-
if buildNum == 0 {
602+
if buildNum == -1 {
603603
return database.WorkspaceBuild{}, sql.ErrNoRows
604604
}
605605
return row, nil
@@ -1263,9 +1263,6 @@ func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(_ context.Context, resourc
12631263
workspaceAgents = append(workspaceAgents, agent)
12641264
}
12651265
}
1266-
if len(workspaceAgents) == 0 {
1267-
return nil, sql.ErrNoRows
1268-
}
12691266
return workspaceAgents, nil
12701267
}
12711268

@@ -1347,9 +1344,6 @@ func (q *fakeQuerier) GetWorkspaceResourcesByJobID(_ context.Context, jobID uuid
13471344
}
13481345
resources = append(resources, resource)
13491346
}
1350-
if len(resources) == 0 {
1351-
return nil, sql.ErrNoRows
1352-
}
13531347
return resources, nil
13541348
}
13551349

coderd/database/errors.go

-9
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,6 @@ import (
66
"github.com/lib/pq"
77
)
88

9-
// UniqueConstraint represents a named unique constraint on a table.
10-
type UniqueConstraint string
11-
12-
// UniqueConstraint enums.
13-
// TODO(mafredri): Generate these from the database schema.
14-
const (
15-
UniqueWorkspacesOwnerIDLowerIdx UniqueConstraint = "workspaces_owner_id_lower_idx"
16-
)
17-
189
// IsUniqueViolation checks if the error is due to a unique violation.
1910
// If one or more specific unique constraints are given as arguments,
2011
// the error must be caused by one of them. If no constraints are given,

coderd/database/dump/main.go renamed to coderd/database/gen/dump/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func main() {
8888
if !ok {
8989
panic("couldn't get caller path")
9090
}
91-
err = os.WriteFile(filepath.Join(mainPath, "..", "..", "dump.sql"), []byte(dump), 0600)
91+
err = os.WriteFile(filepath.Join(mainPath, "..", "..", "..", "dump.sql"), []byte(dump), 0o600)
9292
if err != nil {
9393
panic(err)
9494
}

coderd/database/gen/enum/main.go

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"strings"
10+
11+
"golang.org/x/xerrors"
12+
)
13+
14+
const header = `// Code generated by gen/enum. DO NOT EDIT.
15+
package database
16+
`
17+
18+
func main() {
19+
if err := run(); err != nil {
20+
panic(err)
21+
}
22+
}
23+
24+
func run() error {
25+
dump, err := os.Open("dump.sql")
26+
if err != nil {
27+
_, _ = fmt.Fprintf(os.Stderr, "error: %s must be run in the database directory with dump.sql present\n", os.Args[0])
28+
return err
29+
}
30+
defer dump.Close()
31+
32+
var uniqueConstraints []string
33+
34+
s := bufio.NewScanner(dump)
35+
query := ""
36+
for s.Scan() {
37+
line := strings.TrimSpace(s.Text())
38+
switch {
39+
case strings.HasPrefix(line, "--"):
40+
case line == "":
41+
case strings.HasSuffix(line, ";"):
42+
query += line
43+
if isUniqueConstraint(query) {
44+
uniqueConstraints = append(uniqueConstraints, query)
45+
}
46+
query = ""
47+
default:
48+
query += line + " "
49+
}
50+
}
51+
if err = s.Err(); err != nil {
52+
return err
53+
}
54+
55+
return writeContents("unique_constraint.go", uniqueConstraints, generateUniqueConstraints)
56+
}
57+
58+
func isUniqueConstraint(query string) bool {
59+
return strings.Contains(query, "UNIQUE")
60+
}
61+
62+
func generateUniqueConstraints(queries []string) ([]byte, error) {
63+
s := &bytes.Buffer{}
64+
65+
_, _ = fmt.Fprint(s, header)
66+
_, _ = fmt.Fprint(s, `
67+
// UniqueConstraint represents a named unique constraint on a table.
68+
type UniqueConstraint string
69+
70+
// UniqueConstraint enums.
71+
const (
72+
`)
73+
for _, query := range queries {
74+
name := ""
75+
switch {
76+
case strings.Contains(query, "ALTER TABLE") && strings.Contains(query, "ADD CONSTRAINT"):
77+
name = strings.Split(query, " ")[6]
78+
case strings.Contains(query, "CREATE UNIQUE INDEX"):
79+
name = strings.Split(query, " ")[3]
80+
default:
81+
return nil, xerrors.Errorf("unknown unique constraint format: %s", query)
82+
}
83+
_, _ = fmt.Fprintf(s, "\tUnique%s UniqueConstraint = %q // %s\n", nameFromSnakeCase(name), name, query)
84+
}
85+
_, _ = fmt.Fprint(s, ")\n")
86+
87+
return s.Bytes(), nil
88+
}
89+
90+
func writeContents[T any](dest string, arg T, fn func(T) ([]byte, error)) error {
91+
b, err := fn(arg)
92+
if err != nil {
93+
return err
94+
}
95+
err = os.WriteFile(dest, b, 0o600)
96+
if err != nil {
97+
return err
98+
}
99+
cmd := exec.Command("goimports", "-w", dest)
100+
return cmd.Run()
101+
}
102+
103+
func nameFromSnakeCase(s string) string {
104+
var ret string
105+
for _, ss := range strings.Split(s, "_") {
106+
switch ss {
107+
case "id":
108+
ret += "ID"
109+
case "ids":
110+
ret += "IDs"
111+
case "jwt":
112+
ret += "JWT"
113+
case "idx":
114+
ret += "Index"
115+
default:
116+
ret += strings.Title(ss)
117+
}
118+
}
119+
return ret
120+
}

0 commit comments

Comments
 (0)