From 8538be2eebaf59a1e9e3f79ac9ab9edb1c50d680 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 24 Mar 2022 00:50:46 +0000 Subject: [PATCH 1/6] feat: Add systemd service and production deployment This modifies CI to use a dpkg produced from release to update and run Coder on a tiny VM in GCP. It's intentionally kept simple, because customers should be able to get this same easy install experience. --- .github/workflows/coder.yaml | 55 ++++--- .goreleaser.yml | 9 +- Makefile | 15 +- cli/cliui/job.go | 13 +- cli/start.go | 271 +++++++++++++++++++++++++++------- cli/start_test.go | 40 ++++- cli/workspacecreate_test.go | 3 +- coder.env | 3 + coder.service | 29 ++++ coderd/tunnel/tunnel.go | 13 +- database/postgres/postgres.go | 3 +- go.mod | 6 +- go.sum | 4 +- images/coder/Dockerfile | 13 -- images/coder/run.sh | 30 ---- site/e2e/constants.ts | 6 +- site/e2e/globalSetup.ts | 18 +-- site/e2e/playwright.config.ts | 2 +- 18 files changed, 371 insertions(+), 162 deletions(-) create mode 100644 coder.env create mode 100644 coder.service delete mode 100644 images/coder/Dockerfile delete mode 100755 images/coder/run.sh diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml index 380f5b0a32fdf..fb4dd122e3736 100644 --- a/.github/workflows/coder.yaml +++ b/.github/workflows/coder.yaml @@ -281,7 +281,7 @@ jobs: deploy: name: "deploy" runs-on: ubuntu-latest - if: github.event_name != 'pull_request' + # if: github.ref == 'refs/heads/main' permissions: contents: read id-token: write @@ -291,36 +291,55 @@ jobs: - name: Authenticate to Google Cloud uses: google-github-actions/auth@v0 with: - workload_identity_provider: projects/477254869654/locations/global/workloadIdentityPools/github/providers/github - service_account: github-coder@coder-ci.iam.gserviceaccount.com + workload_identity_provider: projects/573722524737/locations/global/workloadIdentityPools/github/providers/github + service_account: coder-ci@coder-dogfood.iam.gserviceaccount.com - name: Set up Google Cloud SDK uses: google-github-actions/setup-gcloud@v0 - - name: Configure Docker for Google Artifact Registry - run: gcloud auth configure-docker us-docker.pkg.dev - - - uses: actions/setup-node@v3 + - uses: actions/setup-go@v2 with: - node-version: "14" + go-version: "^1.17" - - name: Install node_modules - run: ./scripts/yarn_install.sh + - name: Echo Go Cache Paths + id: go-cache-paths + run: | + echo "::set-output name=go-build::$(go env GOCACHE)" + echo "::set-output name=go-mod::$(go env GOMODCACHE)" - - uses: actions/setup-go@v2 + - name: Go Build Cache + uses: actions/cache@v3 with: - go-version: "^1.17" + path: ${{ steps.go-cache-paths.outputs.go-build }} + key: ${{ runner.os }}-release-go-build-${{ hashFiles('**/go.sum') }} + + - name: Go Mod Cache + uses: actions/cache@v3 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-release-go-mod-${{ hashFiles('**/go.sum') }} - uses: goreleaser/goreleaser-action@v2 with: install-only: true - - run: make docker/image/coder + - name: Build Release + run: make release + + - uses: actions/upload-artifact@v3 + with: + name: coder_linux_amd64.deb + path: ./dist/coder_*_linux_amd64.deb - - run: docker push us-docker.pkg.dev/coder-blacktriangle-dev/ci/coder:latest + - name: Install Release + run: | + gcloud config set project coder-dogfood + gcloud config set compute/zone us-central1-a + gcloud compute scp ./dist/coder_*_linux_amd64.deb coder:/tmp/coder.deb + gcloud compute ssh coder -- sudo dpkg -i /tmp/coder.deb - - name: Update coder service - run: gcloud run services update coder --image us-docker.pkg.dev/coder-blacktriangle-dev/ci/coder:latest --project coder-blacktriangle-dev --tag "git-$(git rev-parse --short HEAD)" --region us-central1 + - name: Start + run: gcloud compute ssh coder -- sudo service coder restart test-js: name: "test/js" @@ -439,7 +458,9 @@ jobs: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - - run: make build + - name: Build + run: | + make site/out - run: yarn playwright:install working-directory: site diff --git a/.goreleaser.yml b/.goreleaser.yml index 4c61371a8bb78..3d888656b7851 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -16,7 +16,7 @@ builds: ldflags: ["-s -w"] env: [CGO_ENABLED=0] goos: [darwin, linux, windows] - goarch: [amd64, arm64] + goarch: [amd64] hooks: # The "trimprefix" appends ".exe" on Windows. post: | @@ -44,6 +44,13 @@ nfpms: - postgresql builds: - coder + bindir: /usr/bin + contents: + - src: coder.env + dst: /etc/coder.d/coder.env + type: "config|noreplace" + - src: coder.service + dst: /usr/lib/systemd/system/coder.service release: ids: [coder] diff --git a/Makefile b/Makefile index 008141e584814..e2e9ffb1c78fe 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ GOOS=$(shell go env GOOS) GOARCH=$(shell go env GOARCH) bin: - goreleaser build --single-target --snapshot --rm-dist + goreleaser build --snapshot --rm-dist .PHONY: bin build: site/out bin @@ -20,11 +20,6 @@ database/generate: fmt/sql database/dump.sql database/query.sql cd database && gofmt -w -r 'Queries -> sqlQuerier' *.go .PHONY: database/generate -docker/image/coder: build - cp ./images/coder/run.sh ./dist/coder_$(GOOS)_$(GOARCH) - docker build --network=host -t us-docker.pkg.dev/coder-blacktriangle-dev/ci/coder:latest -f images/coder/Dockerfile ./dist/coder_$(GOOS)_$(GOARCH) -.PHONY: docker/build - fmt/prettier: @echo "--- prettier" # Avoid writing files in CI to reduce file write activity @@ -55,10 +50,6 @@ install: bin @echo "-- CLI available at $(shell ls $(INSTALL_DIR)/coder*)" .PHONY: install -package: - goreleaser release --snapshot --rm-dist -.PHONY: package - peerbroker/proto: peerbroker/proto/peerbroker.proto protoc \ --go_out=. \ @@ -86,6 +77,10 @@ provisionersdk/proto: provisionersdk/proto/provisioner.proto ./provisionersdk/proto/provisioner.proto .PHONY: provisionersdk/proto +release: + goreleaser release --snapshot --rm-dist +.PHONY: release + site/out: ./scripts/yarn_install.sh cd site && yarn typegen diff --git a/cli/cliui/job.go b/cli/cliui/job.go index 79d29faccf648..ed8d04f1c9a64 100644 --- a/cli/cliui/job.go +++ b/cli/cliui/job.go @@ -16,6 +16,7 @@ import ( type JobOptions struct { Title string + Output bool Fetch func() (codersdk.ProvisionerJob, error) Cancel func() error Logs func() (<-chan codersdk.ProvisionerJobLog, error) @@ -40,7 +41,7 @@ func Job(cmd *cobra.Command, opts JobOptions) (codersdk.ProvisionerJob, error) { var err error job, err = opts.Fetch() if err != nil { - // If a single fetch fails, it could be a one-off. + _, _ = fmt.Fprintln(cmd.OutOrStdout(), defaultStyles.Error.Render(err.Error())) return } @@ -94,12 +95,15 @@ func Job(cmd *cobra.Command, opts JobOptions) (codersdk.ProvisionerJob, error) { return } } + signal.Stop(stopChan) spin.Stop() _, _ = fmt.Fprintf(cmd.OutOrStdout(), Styles.FocusedPrompt.String()+"Gracefully canceling... wait for exit or data loss may occur!\n") spin.Start() err := opts.Cancel() if err != nil { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Failed to cancel %s...\n", err) + spin.Stop() + _, _ = fmt.Fprintln(cmd.OutOrStdout(), defaultStyles.Error.Render(err.Error())) + return } refresh() }() @@ -123,12 +127,15 @@ func Job(cmd *cobra.Command, opts JobOptions) (codersdk.ProvisionerJob, error) { case log, ok := <-logs: if !ok { refresh() - continue + return job, nil } if !firstLog { refresh() firstLog = true } + if !opts.Output { + continue + } spin.Stop() var style lipgloss.Style switch log.Level { diff --git a/cli/start.go b/cli/start.go index 7c4e915f72d35..9a5bf0a7109c6 100644 --- a/cli/start.go +++ b/cli/start.go @@ -2,16 +2,20 @@ package cli import ( "context" + "database/sql" "fmt" - "io" "io/ioutil" "net" "net/http" "net/url" "os" + "os/signal" "time" + "github.com/briandowns/spinner" + "github.com/coreos/go-systemd/daemon" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/xerrors" "google.golang.org/api/idtoken" "google.golang.org/api/option" @@ -19,6 +23,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/cli/config" "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/tunnel" "github.com/coder/coder/codersdk" @@ -32,14 +37,28 @@ import ( func start() *cobra.Command { var ( - address string - dev bool - useTunnel bool + address string + postgresURL string + provisionerDaemonCount uint8 + dev bool + useTunnel bool ) root := &cobra.Command{ Use: "start", RunE: func(cmd *cobra.Command, args []string) error { - logger := slog.Make(sloghuman.Sink(os.Stderr)) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), ` ▄█▀ ▀█▄ + ▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█ + ▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀ +█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █ + ██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀ + +`) + + if postgresURL == "" { + // Default to the environment variable! + postgresURL = os.Getenv("CODER_PG_CONNECTION_URL") + } + listener, err := net.Listen("tcp", address) if err != nil { return xerrors.Errorf("listen %q: %w", address, err) @@ -49,6 +68,7 @@ func start() *cobra.Command { if !valid { return xerrors.New("must be listening on tcp") } + // If just a port is specified, assume localhost. if tcpAddr.IP.IsUnspecified() { tcpAddr.IP = net.IPv4(127, 0, 0, 1) } @@ -59,8 +79,16 @@ func start() *cobra.Command { } accessURL := localURL var tunnelErr <-chan error - if dev { - if useTunnel { + // If we're attempting to tunnel in dev-mode, the access URL + // needs to be changed to use the tunnel. + if dev && useTunnel { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Coder requires a network endpoint that can be accessed by provisioned workspaces. In dev mode, a free tunnel can be created for you. This will expose your Coder deployment to the internet.")+"\n") + + _, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: "Would you like Coder to start a tunnel for simple setup?", + IsConfirm: true, + }) + if err == nil { var accessURLRaw string accessURLRaw, tunnelErr, err = tunnel.New(cmd.Context(), localURL.String()) if err != nil { @@ -70,40 +98,60 @@ func start() *cobra.Command { if err != nil { return xerrors.Errorf("parse: %w", err) } - } - - _, _ = fmt.Fprintf(cmd.OutOrStdout(), ` ▄█▀ ▀█▄ - ▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█ - ▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀ -█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █ - ██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀ -`+cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Started in `+ - cliui.Styles.Field.Render("dev")+` mode. All data is in-memory! Learn how to setup and manage a production Coder deployment here: `+cliui.Styles.Prompt.Render("https://coder.com/docs/TODO")))+ - ` -`+ - cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.FocusedPrompt.String()+`Run `+cliui.Styles.Code.Render("coder projects init")+" in a new terminal to get started.\n"))+` -`) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Tunnel started. Your deployment is accessible at:`))+"\n "+cliui.Styles.Field.Render(accessURL.String())) + } } - validator, err := idtoken.NewValidator(cmd.Context(), option.WithoutAuthentication()) if err != nil { return err } - handler, closeCoderd := coderd.New(&coderd.Options{ + + logger := slog.Make(sloghuman.Sink(os.Stderr)) + options := &coderd.Options{ AccessURL: accessURL, - Logger: logger, + Logger: logger.Named("coderd"), Database: databasefake.New(), Pubsub: database.NewPubsubInMemory(), GoogleTokenValidator: validator, - }) + } + + if !dev { + sqlDB, err := sql.Open("postgres", postgresURL) + if err != nil { + return xerrors.Errorf("dial postgres: %w", err) + } + err = sqlDB.Ping() + if err != nil { + return xerrors.Errorf("ping postgres: %w", err) + } + err = database.MigrateUp(sqlDB) + if err != nil { + return xerrors.Errorf("migrate up: %w", err) + } + options.Database = database.New(sqlDB) + options.Pubsub, err = database.NewPubsub(cmd.Context(), sqlDB, postgresURL) + if err != nil { + return xerrors.Errorf("create pubsub: %w", err) + } + } + + handler, closeCoderd := coderd.New(options) client := codersdk.New(localURL) - daemonClose, err := newProvisionerDaemon(cmd.Context(), client, logger) - if err != nil { - return xerrors.Errorf("create provisioner daemon: %w", err) + provisionerDaemons := make([]*provisionerd.Server, 0) + for i := uint8(0); i < provisionerDaemonCount; i++ { + daemonClose, err := newProvisionerDaemon(cmd.Context(), client, logger) + if err != nil { + return xerrors.Errorf("create provisioner daemon: %w", err) + } + provisionerDaemons = append(provisionerDaemons, daemonClose) } - defer daemonClose.Close() + defer func() { + for _, provisionerDaemon := range provisionerDaemons { + _ = provisionerDaemon.Close() + } + }() errCh := make(chan error) go func() { @@ -111,57 +159,174 @@ func start() *cobra.Command { errCh <- http.Serve(listener, handler) }() + config := createConfig(cmd) + if dev { - config := createConfig(cmd) - _, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{ - Email: "dev@coder.com", - Username: "developer", - Password: "password", - Organization: "coder", - }) - if err != nil { - return xerrors.Errorf("create first user: %w\n", err) - } - token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{ - Email: "dev@coder.com", - Password: "password", - }) + err = createFirstUser(cmd, client, config) if err != nil { - return xerrors.Errorf("login with first user: %w", err) + return xerrors.Errorf("create first user: %w", err) } - err = config.URL().Write(localURL.String()) + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Started in `+ + cliui.Styles.Field.Render("dev")+` mode. All data is in-memory! Do not use in production. Press `+cliui.Styles.Field.Render("ctrl+c")+` to clean up provisioned infrastructure.`))+ + ` +`+ + cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Run `+cliui.Styles.Code.Render("coder projects init")+" in a new terminal to get started.\n"))+` +`) + } else { + // This is helpful for tests, but can be silently ignored. + // Coder may be ran as users that don't have permission to write in the homedir, + // such as via the systemd service. + _ = config.URL().Write(client.URL.String()) + + hasFirstUser, err := client.HasFirstUser(cmd.Context()) if err != nil { - return xerrors.Errorf("write local url: %w", err) + return xerrors.Errorf("check for first user: %w", err) } - err = config.Session().Write(token.SessionToken) - if err != nil { - return xerrors.Errorf("write session token: %w", err) + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Started in `+ + cliui.Styles.Field.Render("production")+` mode. All data is stored in the PostgreSQL provided! Press `+cliui.Styles.Field.Render("ctrl+c")+` to gracefully shutdown.`))+"\n") + + if !hasFirstUser { + _, _ = fmt.Fprint(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.FocusedPrompt.String()+`Run `+cliui.Styles.Code.Render("coder login "+client.URL.String())+" in a new terminal to get started.\n"))) } } - closeCoderd() + // Updates the systemd status from activating to activated. + _, err = daemon.SdNotify(false, daemon.SdNotifyReady) + if err != nil { + return xerrors.Errorf("notify systemd: %w", err) + } + + stopChan := make(chan os.Signal, 1) + defer signal.Stop(stopChan) + signal.Notify(stopChan, os.Interrupt) select { case <-cmd.Context().Done(): + closeCoderd() return cmd.Context().Err() case err := <-tunnelErr: return err case err := <-errCh: + closeCoderd() return err + case <-stopChan: } + signal.Stop(stopChan) + _, err = daemon.SdNotify(false, daemon.SdNotifyStopping) + if err != nil { + return xerrors.Errorf("notify systemd: %w", err) + } + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "\n\n"+cliui.Styles.Bold.Render("Interrupt caught. Gracefully exiting...")) + + if dev { + workspaces, err := client.WorkspacesByUser(cmd.Context(), "") + if err != nil { + return xerrors.Errorf("get workspaces: %w", err) + } + for _, workspace := range workspaces { + before := time.Now() + build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: database.WorkspaceTransitionDelete, + }) + if err != nil { + return xerrors.Errorf("delete workspace: %w", err) + } + + _, err = cliui.Job(cmd, cliui.JobOptions{ + Title: fmt.Sprintf("Deleting workspace %s...", cliui.Styles.Keyword.Render(workspace.Name)), + Fetch: func() (codersdk.ProvisionerJob, error) { + build, err := client.WorkspaceBuild(cmd.Context(), build.ID) + return build.Job, err + }, + Cancel: func() error { + return client.CancelWorkspaceBuild(cmd.Context(), build.ID) + }, + Logs: func() (<-chan codersdk.ProvisionerJobLog, error) { + return client.WorkspaceBuildLogsAfter(cmd.Context(), build.ID, before) + }, + }) + if err != nil { + return err + } + } + } + + for _, provisionerDaemon := range provisionerDaemons { + spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) + spin.Writer = cmd.OutOrStdout() + spin.Suffix = cliui.Styles.Keyword.Render(" Shutting down provisioner daemon...") + spin.Start() + err = provisionerDaemon.Shutdown(cmd.Context()) + if err != nil { + spin.FinalMSG = cliui.Styles.Prompt.String() + "Failed to shutdown provisioner daemon: " + err.Error() + spin.Stop() + } + err = provisionerDaemon.Close() + if err != nil { + spin.Stop() + return xerrors.Errorf("close provisioner daemon: %w", err) + } + spin.FinalMSG = cliui.Styles.Prompt.String() + "Gracefully shut down provisioner daemon!\n" + spin.Stop() + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for WebSocket connections to close...\n") + closeCoderd() + return nil }, } - defaultAddress, ok := os.LookupEnv("ADDRESS") - if !ok { + defaultAddress := os.Getenv("CODER_ADDRESS") + if defaultAddress == "" { defaultAddress = "127.0.0.1:3000" } root.Flags().StringVarP(&address, "address", "a", defaultAddress, "The address to serve the API and dashboard.") root.Flags().BoolVarP(&dev, "dev", "", false, "Serve Coder in dev mode for tinkering.") - root.Flags().BoolVarP(&useTunnel, "tunnel", "", true, `Serve "dev" mode through a Cloudflare Tunnel for easy setup.`) + root.Flags().StringVarP(&postgresURL, "postgres-url", "", "", "URL of a PostgreSQL database to connect to (defaults to $CODER_PG_CONNECTION_URL).") + root.Flags().Uint8VarP(&provisionerDaemonCount, "provisioner-daemons", "", 1, "The amount of provisioner daemons to create on start.") + root.Flags().BoolVarP(&useTunnel, "tunnel", "", true, "Serve dev mode through a Cloudflare Tunnel for easy setup.") + _ = root.Flags().MarkHidden("tunnel") + + if os.Getenv("DUMP") != "" { + root.Flags().VisitAll(func(flag *pflag.Flag) { + fmt.Printf("%s %s\n", flag.Name, flag.Usage) + }) + } return root } -func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger slog.Logger) (io.Closer, error) { +func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root) error { + _, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{ + Email: "admin@coder.com", + Username: "developer", + Password: "password", + Organization: "acme-corp", + }) + if err != nil { + return xerrors.Errorf("create first user: %w\n", err) + } + token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{ + Email: "admin@coder.com", + Password: "password", + }) + if err != nil { + return xerrors.Errorf("login with first user: %w", err) + } + client.SessionToken = token.SessionToken + + err = cfg.URL().Write(client.URL.String()) + if err != nil { + return xerrors.Errorf("write local url: %w", err) + } + err = cfg.Session().Write(token.SessionToken) + if err != nil { + return xerrors.Errorf("write session token: %w", err) + } + return nil +} + +func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger slog.Logger) (*provisionerd.Server, error) { terraformClient, terraformServer := provisionersdk.TransportPipe() go func() { err := terraform.Serve(ctx, &terraform.ServeOptions{ diff --git a/cli/start_test.go b/cli/start_test.go index 4b2df538b1238..465d392647f3a 100644 --- a/cli/start_test.go +++ b/cli/start_test.go @@ -3,6 +3,7 @@ package cli_test import ( "context" "net/url" + "runtime" "testing" "time" @@ -10,17 +11,48 @@ import ( "github.com/coder/coder/cli/clitest" "github.com/coder/coder/codersdk" + "github.com/coder/coder/database/postgres" ) func TestStart(t *testing.T) { t.Parallel() t.Run("Production", func(t *testing.T) { t.Parallel() + if runtime.GOOS != "linux" || testing.Short() { + // Skip on non-Linux because it spawns a PostgreSQL instance. + t.SkipNow() + } + connectionURL, closeFunc, err := postgres.Open() + require.NoError(t, err) + defer closeFunc() ctx, cancelFunc := context.WithCancel(context.Background()) - go cancelFunc() - root, _ := clitest.New(t, "start", "--address", ":0") - err := root.ExecuteContext(ctx) - require.ErrorIs(t, err, context.Canceled) + done := make(chan struct{}) + root, cfg := clitest.New(t, "start", "--address", ":0", "--postgres-url", connectionURL) + go func() { + defer close(done) + err = root.ExecuteContext(ctx) + require.ErrorIs(t, err, context.Canceled) + }() + var client *codersdk.Client + require.Eventually(t, func() bool { + rawURL, err := cfg.URL().Read() + if err != nil { + return false + } + accessURL, err := url.Parse(rawURL) + require.NoError(t, err) + client = codersdk.New(accessURL) + return true + }, 15*time.Second, 25*time.Millisecond) + _, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{ + Email: "some@one.com", + Username: "example", + Password: "password", + Organization: "example", + }) + require.NoError(t, err) + cancelFunc() + <-done }) t.Run("Development", func(t *testing.T) { t.Parallel() diff --git a/cli/workspacecreate_test.go b/cli/workspacecreate_test.go index 5dd85e0833c93..d076de8d8d6c6 100644 --- a/cli/workspacecreate_test.go +++ b/cli/workspacecreate_test.go @@ -3,10 +3,11 @@ package cli_test import ( "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/pty/ptytest" - "github.com/stretchr/testify/require" ) func TestWorkspaceCreate(t *testing.T) { diff --git a/coder.env b/coder.env new file mode 100644 index 0000000000000..e26c7bad88029 --- /dev/null +++ b/coder.env @@ -0,0 +1,3 @@ +# Runtime variables for "coder start". +CODER_ADDRESS= +CODER_PG_CONNECTION_URL= diff --git a/coder.service b/coder.service new file mode 100644 index 0000000000000..f777842385fcc --- /dev/null +++ b/coder.service @@ -0,0 +1,29 @@ +[Unit] +Description="Coder - Self-hosted developer workspaces on your infra" +Documentation=https://coder.com/docs/ +Requires=network-online.target +After=network-online.target +ConditionFileNotEmpty=/etc/coder.d/coder.env +StartLimitIntervalSec=60 +StartLimitBurst=3 + +[Service] +Type=notify +EnvironmentFile=/etc/coder.d/coder.env +User=coder +Group=coder +ProtectSystem=full +ProtectHome=read-only +PrivateTmp=yes +PrivateDevices=yes +SecureBits=keep-caps +AmbientCapabilities=CAP_IPC_LOCK +CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK +NoNewPrivileges=yes +ExecStart=/usr/bin/coder start +Restart=on-failure +RestartSec=5 +TimeoutStopSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/coderd/tunnel/tunnel.go b/coderd/tunnel/tunnel.go index d3a1adbc3ce38..7d02eeb296826 100644 --- a/coderd/tunnel/tunnel.go +++ b/coderd/tunnel/tunnel.go @@ -16,6 +16,8 @@ import ( "github.com/rs/zerolog" "github.com/urfave/cli/v2" "golang.org/x/xerrors" + + "github.com/coder/retry" ) // New creates a new tunnel pointing at the URL provided. @@ -73,7 +75,7 @@ func New(ctx context.Context, url string) (string, <-chan error, error) { set := flag.NewFlagSet("", 0) set.String("protocol", "", "") set.String("url", "", "") - set.Int("retries", 5, "") + // set.Int("retries", 5, "") appCtx := cli.NewContext(&cli.App{}, set, nil) appCtx.Context = ctx _ = appCtx.Set("url", url) @@ -81,8 +83,13 @@ func New(ctx context.Context, url string) (string, <-chan error, error) { logger := zerolog.New(os.Stdout).Level(zerolog.Disabled) errCh := make(chan error, 1) go func() { - err := tunnel.StartServer(appCtx, &cliutil.BuildInfo{}, namedTunnel, &logger, false) - errCh <- err + for retry.New(250*time.Millisecond, 5*time.Second).Wait(ctx) { + err := tunnel.StartServer(appCtx, &cliutil.BuildInfo{}, namedTunnel, &logger, false) + if err != nil && strings.Contains(err.Error(), "Failed to get tunnel") { + continue + } + errCh <- err + } }() if !strings.HasPrefix(data.Result.Hostname, "https://") { data.Result.Hostname = "https://" + data.Result.Hostname diff --git a/database/postgres/postgres.go b/database/postgres/postgres.go index 563fd4c63b2b1..89fcf0000ddea 100644 --- a/database/postgres/postgres.go +++ b/database/postgres/postgres.go @@ -9,10 +9,11 @@ import ( "sync" "time" - "github.com/coder/coder/cryptorand" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" "golang.org/x/xerrors" + + "github.com/coder/coder/cryptorand" ) // Required to prevent port collision during container creation. diff --git a/go.mod b/go.mod index bc2d01088fa5f..6e636536e07cc 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.2022030 // These are to allow embedding the cloudflared quick-tunnel CLI. // Required until https://github.com/cloudflare/cloudflared/pull/597 is merged. -replace github.com/cloudflare/cloudflared => github.com/kylecarbs/cloudflared v0.0.0-20220311054120-ea109c6bf7be +replace github.com/cloudflare/cloudflared => github.com/kylecarbs/cloudflared v0.0.0-20220323202451-083379ce31c3 replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d @@ -34,6 +34,7 @@ require ( github.com/charmbracelet/lipgloss v0.5.0 github.com/cloudflare/cloudflared v0.0.0-20220308214351-5352b3cf0489 github.com/coder/retry v1.3.0 + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/creack/pty v1.1.17 github.com/fatih/color v1.13.0 github.com/gliderlabs/ssh v0.3.3 @@ -67,6 +68,7 @@ require ( github.com/quasilyte/go-ruleguard/dsl v0.3.19 github.com/rs/zerolog v1.26.1 github.com/spf13/cobra v1.4.0 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.1 github.com/tabbed/pqtype v0.1.1 github.com/unrolled/secure v1.10.0 @@ -110,7 +112,6 @@ require ( github.com/containerd/continuity v0.2.2 // indirect github.com/coredns/caddy v1.1.1 // indirect github.com/coredns/coredns v1.9.0 // indirect - github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dhui/dktest v0.3.9 // indirect @@ -206,7 +207,6 @@ require ( github.com/spf13/afero v1.8.1 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index 11339d865bb38..5b74f7d3590cf 100644 --- a/go.sum +++ b/go.sum @@ -1125,8 +1125,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= -github.com/kylecarbs/cloudflared v0.0.0-20220311054120-ea109c6bf7be h1:kl9byH/iaZJ99iJbSAFXjJ8jBpg6TLk6L2/73uSV8wU= -github.com/kylecarbs/cloudflared v0.0.0-20220311054120-ea109c6bf7be/go.mod h1:4chGYq3uDzeHSpht2LFNZc/8ulHhMW9MvHPvzT5aZx8= +github.com/kylecarbs/cloudflared v0.0.0-20220323202451-083379ce31c3 h1:JopBWZaVmN4tqWlOb/cEv5oGcL/TUE5gdI4g0yCOyh0= +github.com/kylecarbs/cloudflared v0.0.0-20220323202451-083379ce31c3/go.mod h1:4chGYq3uDzeHSpht2LFNZc/8ulHhMW9MvHPvzT5aZx8= github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA= github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= github.com/kylecarbs/promptui v0.8.1-0.20201231190244-d8f2159af2b2 h1:MUREBTh4kybLY1KyuBfSx+QPfTB8XiUHs6ZxUhOPTnU= diff --git a/images/coder/Dockerfile b/images/coder/Dockerfile deleted file mode 100644 index 3b828cda1fc77..0000000000000 --- a/images/coder/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM registry.access.redhat.com/ubi8/ubi:latest - -RUN yum install -y yum-utils -RUN yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo -RUN yum install -y terraform - -COPY coder /coder -RUN chmod +x /coder - -COPY run.sh /run.sh -RUN chmod +x /run.sh - -ENTRYPOINT ["/run.sh"] diff --git a/images/coder/run.sh b/images/coder/run.sh deleted file mode 100755 index 3eac591270060..0000000000000 --- a/images/coder/run.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -EMAIL=${EMAIL:-admin@coder.com} -USERNAME=${USERNAME:-admin} -ORGANIZATION=${ORGANIZATION:-ACME-Corp} -PASSWORD=${PASSWORD:-password} -PORT=${PORT:-8000} - -# Helper to create an initial user -function create_initial_user() { - # TODO: We need to wait for `coderd` to spin up - - # need to replace with a deterministic strategy - sleep 5s - - curl -X POST \ - -d '{"email": "'"$EMAIL"'", "username": "'"$USERNAME"'", "organization": "'"$ORGANIZATION"'", "password": "'"$PASSWORD"'"}' \ - -H 'Content-Type:application/json' \ - "http://localhost:$PORT/api/v2/users/first" -} - -# This is a way to run multiple processes in parallel, and have Ctrl-C work correctly -# to kill both at the same time. For more details, see: -# https://stackoverflow.com/questions/3004811/how-do-you-run-multiple-programs-in-parallel-from-a-bash-script -( - trap 'kill 0' SIGINT - create_initial_user & - /coder start --address=":$PORT" -) diff --git a/site/e2e/constants.ts b/site/e2e/constants.ts index 503b30ab44a0b..a8617434bf78b 100644 --- a/site/e2e/constants.ts +++ b/site/e2e/constants.ts @@ -1,5 +1,5 @@ -// Default credentials and user for running tests -export const username = "admin" +// Credentials for the default user when running in dev mode. +export const username = "developer" export const password = "password" -export const organization = "acme-crop" +export const organization = "acme-corp" export const email = "admin@coder.com" diff --git a/site/e2e/globalSetup.ts b/site/e2e/globalSetup.ts index 7a63071bc6415..dbc133fd864b1 100644 --- a/site/e2e/globalSetup.ts +++ b/site/e2e/globalSetup.ts @@ -2,23 +2,7 @@ import { FullConfig, request } from "@playwright/test" import { email, username, password, organization } from "./constants" const globalSetup = async (config: FullConfig): Promise => { - // Grab the 'baseURL' from the webserver (`coderd`) - const { baseURL } = config.projects[0].use - - // Create a context that will issue http requests. - const context = await request.newContext({ - baseURL, - }) - - // Create initial user - await context.post("/api/v2/users/first", { - data: { - email, - username, - password, - organization, - }, - }) + // Nothing yet! } export default globalSetup diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index 80045a9f1eb37..c9080c110801a 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -17,7 +17,7 @@ const config: PlaywrightTestConfig = { // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests webServer: { // Run the coder daemon directly. - command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} start`, + command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} start --dev --tunnel=false`, port: 3000, timeout: 120 * 10000, reuseExistingServer: false, From b249fea6ce09107b8449b3c49f2934e0b74f3685 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 23 Mar 2022 21:31:22 -0500 Subject: [PATCH 2/6] Update globalSetup.ts --- site/e2e/globalSetup.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/e2e/globalSetup.ts b/site/e2e/globalSetup.ts index dbc133fd864b1..1dc2474d133a6 100644 --- a/site/e2e/globalSetup.ts +++ b/site/e2e/globalSetup.ts @@ -1,5 +1,4 @@ -import { FullConfig, request } from "@playwright/test" -import { email, username, password, organization } from "./constants" +import { FullConfig } from "@playwright/test" const globalSetup = async (config: FullConfig): Promise => { // Nothing yet! From ba0cd906f6054c789cfa5cea10be4bd3d0e5e2bc Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 23 Mar 2022 21:34:18 -0500 Subject: [PATCH 3/6] Update globalSetup.ts --- site/e2e/globalSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/e2e/globalSetup.ts b/site/e2e/globalSetup.ts index 1dc2474d133a6..b39e6d75261c8 100644 --- a/site/e2e/globalSetup.ts +++ b/site/e2e/globalSetup.ts @@ -1,6 +1,6 @@ import { FullConfig } from "@playwright/test" -const globalSetup = async (config: FullConfig): Promise => { +const globalSetup = async (): Promise => { // Nothing yet! } From 4c91719cce8f39eed451133402142da83ad80af2 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 23 Mar 2022 21:38:56 -0500 Subject: [PATCH 4/6] Update globalSetup.ts --- site/e2e/globalSetup.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/site/e2e/globalSetup.ts b/site/e2e/globalSetup.ts index b39e6d75261c8..7ea3c62e2a216 100644 --- a/site/e2e/globalSetup.ts +++ b/site/e2e/globalSetup.ts @@ -1,5 +1,3 @@ -import { FullConfig } from "@playwright/test" - const globalSetup = async (): Promise => { // Nothing yet! } From 5dce6989f339a61e262b2bc789af69ec1a40e0d1 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 23 Mar 2022 22:13:10 -0500 Subject: [PATCH 5/6] Update coder.yaml --- .github/workflows/coder.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml index fb4dd122e3736..97afa17d2bd90 100644 --- a/.github/workflows/coder.yaml +++ b/.github/workflows/coder.yaml @@ -281,7 +281,7 @@ jobs: deploy: name: "deploy" runs-on: ubuntu-latest - # if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' permissions: contents: read id-token: write From 4e047871ccc52097ed6c213d7695c7409762115b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 24 Mar 2022 14:27:42 +0000 Subject: [PATCH 6/6] Use pinned version of Go --- .github/workflows/coder.yaml | 14 +++++++------- cli/start.go | 11 ++--------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml index 97afa17d2bd90..3c4424451897e 100644 --- a/.github/workflows/coder.yaml +++ b/.github/workflows/coder.yaml @@ -40,7 +40,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v2 with: - go-version: "^1.17" + go-version: "~1.17" - name: golangci-lint uses: golangci/golangci-lint-action@v3.1.0 with: @@ -82,7 +82,7 @@ jobs: version: "3.19.4" - uses: actions/setup-go@v2 with: - go-version: "^1.17" + go-version: "~1.17" - run: curl -sSL https://github.com/kyleconroy/sqlc/releases/download/v1.11.0/sqlc_1.11.0_linux_amd64.tar.gz | sudo tar -C /usr/bin -xz sqlc @@ -133,7 +133,7 @@ jobs: - uses: actions/setup-go@v2 with: - go-version: "^1.17" + go-version: "~1.17" - name: Echo Go Cache Paths id: go-cache-paths @@ -201,7 +201,7 @@ jobs: - uses: actions/setup-go@v2 with: - go-version: "^1.17" + go-version: "~1.17" - name: Echo Go Cache Paths id: go-cache-paths @@ -299,7 +299,7 @@ jobs: - uses: actions/setup-go@v2 with: - go-version: "^1.17" + go-version: "~1.17" - name: Echo Go Cache Paths id: go-cache-paths @@ -361,7 +361,7 @@ jobs: # Go is required for uploading the test results to datadog - uses: actions/setup-go@v2 with: - go-version: "^1.17" + go-version: "~1.17" - uses: actions/setup-node@v3 with: @@ -425,7 +425,7 @@ jobs: # Go is required for uploading the test results to datadog - uses: actions/setup-go@v2 with: - go-version: "^1.17" + go-version: "~1.17" - uses: hashicorp/setup-terraform@v1 with: diff --git a/cli/start.go b/cli/start.go index 9a5bf0a7109c6..dc0205e71c733 100644 --- a/cli/start.go +++ b/cli/start.go @@ -15,7 +15,6 @@ import ( "github.com/briandowns/spinner" "github.com/coreos/go-systemd/daemon" "github.com/spf13/cobra" - "github.com/spf13/pflag" "golang.org/x/xerrors" "google.golang.org/api/idtoken" "google.golang.org/api/option" @@ -247,7 +246,7 @@ func start() *cobra.Command { }, }) if err != nil { - return err + return xerrors.Errorf("delete workspace %s: %w", workspace.Name, err) } } } @@ -287,12 +286,6 @@ func start() *cobra.Command { root.Flags().BoolVarP(&useTunnel, "tunnel", "", true, "Serve dev mode through a Cloudflare Tunnel for easy setup.") _ = root.Flags().MarkHidden("tunnel") - if os.Getenv("DUMP") != "" { - root.Flags().VisitAll(func(flag *pflag.Flag) { - fmt.Printf("%s %s\n", flag.Name, flag.Usage) - }) - } - return root } @@ -304,7 +297,7 @@ func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Roo Organization: "acme-corp", }) if err != nil { - return xerrors.Errorf("create first user: %w\n", err) + return xerrors.Errorf("create first user: %w", err) } token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{ Email: "admin@coder.com",