Skip to content

Commit 9c9a5e2

Browse files
committed
Merge remote-tracking branch 'origin/main' into stevenmasley/workspace-template-search
2 parents b5f5705 + 02d2aea commit 9c9a5e2

File tree

66 files changed

+1756
-515
lines changed

Some content is hidden

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

66 files changed

+1756
-515
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"sdktrace",
6464
"Signup",
6565
"sourcemapped",
66+
"Srcs",
6667
"stretchr",
6768
"TCGETS",
6869
"tcpip",

cli/server.go

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"golang.org/x/mod/semver"
3434
"golang.org/x/oauth2"
3535
xgithub "golang.org/x/oauth2/github"
36+
"golang.org/x/sync/errgroup"
3637
"golang.org/x/xerrors"
3738
"google.golang.org/api/idtoken"
3839
"google.golang.org/api/option"
@@ -169,8 +170,9 @@ func server() *cobra.Command {
169170
}
170171

171172
var (
172-
tunnelErrChan <-chan error
173173
ctxTunnel, closeTunnel = context.WithCancel(cmd.Context())
174+
devTunnel = (*devtunnel.Tunnel)(nil)
175+
devTunnelErrChan = make(<-chan error, 1)
174176
)
175177
defer closeTunnel()
176178

@@ -195,14 +197,37 @@ func server() *cobra.Command {
195197
}
196198
}
197199
if err == nil {
198-
accessURL, tunnelErrChan, err = devtunnel.New(ctxTunnel, localURL)
200+
devTunnel, devTunnelErrChan, err = devtunnel.New(ctxTunnel, logger.Named("devtunnel"))
199201
if err != nil {
200202
return xerrors.Errorf("create tunnel: %w", err)
201203
}
204+
accessURL = devTunnel.URL
202205
}
203206
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
204207
}
205208

209+
// Warn the user if the access URL appears to be a loopback address.
210+
isLocal, err := isLocalURL(cmd.Context(), accessURL)
211+
if isLocal || err != nil {
212+
var reason string
213+
if isLocal {
214+
reason = "appears to be a loopback address"
215+
} else {
216+
reason = "could not be resolved"
217+
}
218+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(
219+
cliui.Styles.Warn.Render("Warning:")+" The current access URL:")+"\n\n")
220+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), " "+cliui.Styles.Field.Render(accessURL)+"\n\n")
221+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(
222+
reason+". Provisioned workspaces are unlikely to be able to "+
223+
"connect to Coder. Please consider changing your "+
224+
"access URL using the --access-url option, or directly "+
225+
"specifying access URLs on templates.",
226+
)+"\n\n")
227+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "For more information, see "+
228+
"https://github.com/coder/coder/issues/1528\n\n")
229+
}
230+
206231
validator, err := idtoken.NewValidator(cmd.Context(), option.WithoutAuthentication())
207232
if err != nil {
208233
return err
@@ -327,7 +352,27 @@ func server() *cobra.Command {
327352
return shutdownConnsCtx
328353
},
329354
}
330-
errCh <- server.Serve(listener)
355+
356+
wg := errgroup.Group{}
357+
wg.Go(func() error {
358+
// Make sure to close the tunnel listener if we exit so the
359+
// errgroup doesn't wait forever!
360+
if dev && tunnel {
361+
defer devTunnel.Listener.Close()
362+
}
363+
364+
return server.Serve(listener)
365+
})
366+
367+
if dev && tunnel {
368+
wg.Go(func() error {
369+
defer listener.Close()
370+
371+
return server.Serve(devTunnel.Listener)
372+
})
373+
}
374+
375+
errCh <- wg.Wait()
331376
}()
332377

333378
config := createConfig(cmd)
@@ -393,7 +438,7 @@ func server() *cobra.Command {
393438
case <-cmd.Context().Done():
394439
coderAPI.Close()
395440
return cmd.Context().Err()
396-
case err := <-tunnelErrChan:
441+
case err := <-devTunnelErrChan:
397442
if err != nil {
398443
return err
399444
}
@@ -456,7 +501,7 @@ func server() *cobra.Command {
456501
if dev && tunnel {
457502
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for dev tunnel to close...\n")
458503
closeTunnel()
459-
<-tunnelErrChan
504+
<-devTunnelErrChan
460505
}
461506

462507
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for WebSocket connections to close...\n")
@@ -803,3 +848,24 @@ func serveHandler(ctx context.Context, logger slog.Logger, handler http.Handler,
803848

804849
return func() { _ = srv.Close() }
805850
}
851+
852+
// isLocalURL returns true if the hostname of the provided URL appears to
853+
// resolve to a loopback address.
854+
func isLocalURL(ctx context.Context, urlString string) (bool, error) {
855+
parsedURL, err := url.Parse(urlString)
856+
if err != nil {
857+
return false, err
858+
}
859+
resolver := &net.Resolver{}
860+
ips, err := resolver.LookupIPAddr(ctx, parsedURL.Hostname())
861+
if err != nil {
862+
return false, err
863+
}
864+
865+
for _, ip := range ips {
866+
if ip.IP.IsLoopback() {
867+
return true, nil
868+
}
869+
}
870+
return false, nil
871+
}

cli/server_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ func TestServer(t *testing.T) {
118118
} else {
119119
t.Error("expected password line output; got no match")
120120
}
121+
122+
// Verify that we warned the user about the default access URL possibly not being what they want.
123+
assert.Contains(t, buf.String(), "coder/coder/issues/1528")
121124
})
122125

123126
// Duplicated test from "Development" above to test setting email/password via env.
@@ -163,6 +166,32 @@ func TestServer(t *testing.T) {
163166
assert.Contains(t, buf.String(), fmt.Sprintf("password: %s", wantPassword), "expected output %q; got no match", wantPassword)
164167
})
165168

169+
t.Run("NoWarningWithRemoteAccessURL", func(t *testing.T) {
170+
t.Parallel()
171+
ctx, cancelFunc := context.WithCancel(context.Background())
172+
defer cancelFunc()
173+
174+
root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", "--access-url", "http://1.2.3.4:3000/")
175+
var buf strings.Builder
176+
errC := make(chan error)
177+
root.SetOutput(&buf)
178+
go func() {
179+
errC <- root.ExecuteContext(ctx)
180+
}()
181+
182+
// Just wait for startup
183+
require.Eventually(t, func() bool {
184+
var err error
185+
_, err = cfg.URL().Read()
186+
return err == nil
187+
}, 15*time.Second, 25*time.Millisecond)
188+
189+
cancelFunc()
190+
require.ErrorIs(t, <-errC, context.Canceled)
191+
192+
assert.NotContains(t, buf.String(), "coder/coder/issues/1528")
193+
})
194+
166195
t.Run("TLSBadVersion", func(t *testing.T) {
167196
t.Parallel()
168197
ctx, cancelFunc := context.WithCancel(context.Background())

cli/templateinit.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ func templateInit() *cobra.Command {
2424
exampleNames := []string{}
2525
exampleByName := map[string]examples.Example{}
2626
for _, example := range exampleList {
27-
exampleNames = append(exampleNames, example.Name)
28-
exampleByName[example.Name] = example
27+
name := fmt.Sprintf(
28+
"%s\n%s\n",
29+
cliui.Styles.Bold.Render(example.Name),
30+
cliui.Styles.Wrap.Copy().PaddingLeft(6).Render(example.Description),
31+
)
32+
exampleNames = append(exampleNames, name)
33+
exampleByName[name] = example
2934
}
3035

3136
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Wrap.Render(

cmd/coder/main.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,13 @@ import (
44
"errors"
55
"fmt"
66
"os"
7-
"os/exec"
8-
"path/filepath"
9-
"strings"
107
_ "time/tzdata"
118

129
"github.com/coder/coder/cli"
1310
"github.com/coder/coder/cli/cliui"
1411
)
1512

1613
func main() {
17-
dadjoke()
1814
cmd, err := cli.Root().ExecuteC()
1915
if err != nil {
2016
if errors.Is(err, cliui.Canceled) {
@@ -25,23 +21,3 @@ func main() {
2521
os.Exit(1)
2622
}
2723
}
28-
29-
//nolint
30-
func dadjoke() {
31-
if os.Getenv("EEOFF") != "" || filepath.Base(os.Args[0]) != "gitpod" {
32-
return
33-
}
34-
35-
args := strings.Fields(`run -it --rm git --image=index.docker.io/bitnami/git --command --restart=Never -- git`)
36-
args = append(args, os.Args[1:]...)
37-
cmd := exec.Command("kubectl", args...)
38-
cmd.Stdin = os.Stdin
39-
cmd.Stdout = os.Stdout
40-
cmd.Stderr = os.Stderr
41-
_ = cmd.Start()
42-
err := cmd.Wait()
43-
if exitErr, ok := err.(*exec.ExitError); ok {
44-
os.Exit(exitErr.ExitCode())
45-
}
46-
os.Exit(0)
47-
}

coderd/audit/diff_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func TestDiff(t *testing.T) {
8888
ActiveVersionID: uuid.UUID{3},
8989
MaxTtl: int64(time.Hour),
9090
MinAutostartInterval: int64(time.Minute),
91+
CreatedBy: uuid.NullUUID{UUID: uuid.UUID{4}, Valid: true},
9192
},
9293
exp: audit.Map{
9394
"id": uuid.UUID{1}.String(),
@@ -97,6 +98,7 @@ func TestDiff(t *testing.T) {
9798
"active_version_id": uuid.UUID{3}.String(),
9899
"max_ttl": int64(3600000000000),
99100
"min_autostart_interval": int64(60000000000),
101+
"created_by": uuid.UUID{4}.String(),
100102
},
101103
},
102104
})

coderd/audit/table.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
7272
"description": ActionTrack,
7373
"max_ttl": ActionTrack,
7474
"min_autostart_interval": ActionTrack,
75+
"created_by": ActionTrack,
7576
},
7677
&database.TemplateVersion{}: {
7778
"id": ActionTrack,

coderd/coderd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,10 @@ func New(options *Options) *API {
270270
r.Get("/", api.organizationsByUser)
271271
r.Get("/{organizationname}", api.organizationByUserAndName)
272272
})
273-
r.Get("/workspace/{workspacename}", api.workspaceByOwnerAndName)
273+
r.Route("/workspace/{workspacename}", func(r chi.Router) {
274+
r.Get("/", api.workspaceByOwnerAndName)
275+
r.Get("/builds/{buildnumber}", api.workspaceBuildByBuildNumber)
276+
})
274277
r.Get("/gitsshkey", api.gitSSHKey)
275278
r.Put("/gitsshkey", api.regenerateGitSSHKey)
276279
})

coderd/coderd_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"io"
66
"net/http"
7+
"strconv"
78
"strings"
89
"testing"
910
"time"
@@ -163,6 +164,10 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
163164
AssertObject: rbac.ResourceWorkspace,
164165
AssertAction: rbac.ActionRead,
165166
},
167+
"GET:/api/v2/users/me/workspace/{workspacename}/builds/{buildnumber}": {
168+
AssertObject: rbac.ResourceWorkspace,
169+
AssertAction: rbac.ActionRead,
170+
},
166171
"GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {
167172
AssertAction: rbac.ActionRead,
168173
AssertObject: workspaceRBACObj,
@@ -388,6 +393,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
388393
route = strings.ReplaceAll(route, "{workspacename}", workspace.Name)
389394
route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name)
390395
route = strings.ReplaceAll(route, "{workspaceagent}", workspaceResources[0].Agents[0].ID.String())
396+
route = strings.ReplaceAll(route, "{buildnumber}", strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10))
391397
route = strings.ReplaceAll(route, "{template}", template.ID.String())
392398
route = strings.ReplaceAll(route, "{hash}", file.Hash)
393399
route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String())

coderd/database/databasefake/databasefake.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,22 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, a
631631
return database.WorkspaceBuild{}, sql.ErrNoRows
632632
}
633633

634+
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) {
635+
q.mutex.RLock()
636+
defer q.mutex.RUnlock()
637+
638+
for _, workspaceBuild := range q.workspaceBuilds {
639+
if workspaceBuild.WorkspaceID.String() != arg.WorkspaceID.String() {
640+
continue
641+
}
642+
if workspaceBuild.BuildNumber != arg.BuildNumber {
643+
continue
644+
}
645+
return workspaceBuild, nil
646+
}
647+
return database.WorkspaceBuild{}, sql.ErrNoRows
648+
}
649+
634650
func (q *fakeQuerier) GetOrganizations(_ context.Context) ([]database.Organization, error) {
635651
q.mutex.RLock()
636652
defer q.mutex.RUnlock()
@@ -1311,6 +1327,7 @@ func (q *fakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl
13111327
Description: arg.Description,
13121328
MaxTtl: arg.MaxTtl,
13131329
MinAutostartInterval: arg.MinAutostartInterval,
1330+
CreatedBy: arg.CreatedBy,
13141331
}
13151332
q.templates = append(q.templates, template)
13161333
return template, nil

coderd/database/dump.sql

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE ONLY templates DROP COLUMN IF EXISTS created_by;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE ONLY templates ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES users (id) ON DELETE RESTRICT;

coderd/database/models.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)