Skip to content

Commit 7dda3a2

Browse files
committed
Add command to start a provisioner daemon
1 parent cf1221b commit 7dda3a2

11 files changed

+191
-14
lines changed

cli/deployment/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func newConfig() *codersdk.DeploymentConfig {
143143
Name: "Cache Directory",
144144
Usage: "The directory to cache temporary files. If unspecified and $CACHE_DIRECTORY is set, it will be used for compatibility with systemd.",
145145
Flag: "cache-dir",
146-
Default: defaultCacheDir(),
146+
Default: DefaultCacheDir(),
147147
},
148148
InMemoryDatabase: &codersdk.DeploymentConfigField[bool]{
149149
Name: "In Memory Database",
@@ -632,7 +632,7 @@ func formatEnv(key string) string {
632632
return "CODER_" + strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(key))
633633
}
634634

635-
func defaultCacheDir() string {
635+
func DefaultCacheDir() string {
636636
defaultCacheDir, err := os.UserCacheDir()
637637
if err != nil {
638638
defaultCacheDir = os.TempDir()

cli/gitaskpass.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func gitAskpass() *cobra.Command {
2626
RunE: func(cmd *cobra.Command, args []string) error {
2727
ctx := cmd.Context()
2828

29-
ctx, stop := signal.NotifyContext(ctx, interruptSignals...)
29+
ctx, stop := signal.NotifyContext(ctx, InterruptSignals...)
3030
defer stop()
3131

3232
user, host, err := gitauth.ParseAskpass(args[0])

cli/gitssh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func gitssh() *cobra.Command {
2929

3030
// Catch interrupt signals to ensure the temporary private
3131
// key file is cleaned up on most cases.
32-
ctx, stop := signal.NotifyContext(ctx, interruptSignals...)
32+
ctx, stop := signal.NotifyContext(ctx, InterruptSignals...)
3333
defer stop()
3434

3535
// Early check so errors are reported immediately.

cli/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
104104
//
105105
// To get out of a graceful shutdown, the user can send
106106
// SIGQUIT with ctrl+\ or SIGKILL with `kill -9`.
107-
notifyCtx, notifyStop := signal.NotifyContext(ctx, interruptSignals...)
107+
notifyCtx, notifyStop := signal.NotifyContext(ctx, InterruptSignals...)
108108
defer notifyStop()
109109

110110
// Clean up idle connections at the end, e.g.

cli/signal_unix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"syscall"
88
)
99

10-
var interruptSignals = []os.Signal{
10+
var InterruptSignals = []os.Signal{
1111
os.Interrupt,
1212
syscall.SIGTERM,
1313
syscall.SIGHUP,

cli/signal_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ import (
66
"os"
77
)
88

9-
var interruptSignals = []os.Signal{os.Interrupt}
9+
var InterruptSignals = []os.Signal{os.Interrupt}

enterprise/cli/provisionerdaemons.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,158 @@
11
package cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
"strings"
9+
"time"
10+
11+
"cdr.dev/slog"
12+
"cdr.dev/slog/sloggers/sloghuman"
13+
agpl "github.com/coder/coder/cli"
14+
"github.com/coder/coder/cli/cliflag"
15+
"github.com/coder/coder/cli/cliui"
16+
"github.com/coder/coder/cli/deployment"
17+
"github.com/coder/coder/coderd/database"
18+
"github.com/coder/coder/codersdk"
19+
"github.com/coder/coder/provisioner/terraform"
20+
"github.com/coder/coder/provisionerd"
21+
provisionerdproto "github.com/coder/coder/provisionerd/proto"
22+
"github.com/coder/coder/provisionersdk"
23+
"github.com/coder/coder/provisionersdk/proto"
24+
25+
"github.com/spf13/cobra"
26+
"golang.org/x/xerrors"
27+
)
28+
29+
func provisionerDaemons() *cobra.Command {
30+
cmd := &cobra.Command{
31+
Use: "provisionerd",
32+
Short: "Manage provisioner daemons",
33+
}
34+
cmd.AddCommand(provisionerDaemonStart())
35+
36+
return cmd
37+
}
38+
39+
func provisionerDaemonStart() *cobra.Command {
40+
var (
41+
cacheDir string
42+
rawTags []string
43+
)
44+
cmd := &cobra.Command{
45+
Use: "start",
46+
Short: "Run a provisioner daemon",
47+
RunE: func(cmd *cobra.Command, args []string) error {
48+
ctx, cancel := context.WithCancel(cmd.Context())
49+
defer cancel()
50+
51+
notifyCtx, notifyStop := signal.NotifyContext(ctx, agpl.InterruptSignals...)
52+
defer notifyStop()
53+
54+
client, err := agpl.CreateClient(cmd)
55+
if err != nil {
56+
return xerrors.Errorf("create client: %w", err)
57+
}
58+
org, err := agpl.CurrentOrganization(cmd, client)
59+
if err != nil {
60+
return xerrors.Errorf("get current organization: %w", err)
61+
}
62+
63+
tags := map[string]string{}
64+
for _, rawTag := range rawTags {
65+
parts := strings.SplitN(rawTag, "=", 2)
66+
if len(parts) < 2 {
67+
return xerrors.Errorf("invalid tag format for %q. must be key=value", rawTag)
68+
}
69+
tags[parts[0]] = parts[1]
70+
}
71+
72+
err = os.MkdirAll(cacheDir, 0o700)
73+
if err != nil {
74+
return xerrors.Errorf("mkdir %q: %w", cacheDir, err)
75+
}
76+
77+
terraformClient, terraformServer := provisionersdk.TransportPipe()
78+
go func() {
79+
<-ctx.Done()
80+
_ = terraformClient.Close()
81+
_ = terraformServer.Close()
82+
}()
83+
84+
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
85+
errCh := make(chan error, 1)
86+
go func() {
87+
defer cancel()
88+
89+
err := terraform.Serve(ctx, &terraform.ServeOptions{
90+
ServeOptions: &provisionersdk.ServeOptions{
91+
Listener: terraformServer,
92+
},
93+
CachePath: cacheDir,
94+
Logger: logger.Named("terraform"),
95+
})
96+
if err != nil && !xerrors.Is(err, context.Canceled) {
97+
select {
98+
case errCh <- err:
99+
default:
100+
}
101+
}
102+
}()
103+
104+
tempDir, err := os.MkdirTemp("", "provisionerd")
105+
if err != nil {
106+
return err
107+
}
108+
109+
provisioners := provisionerd.Provisioners{
110+
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(provisionersdk.Conn(terraformClient)),
111+
}
112+
srv := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
113+
return client.ServeProvisionerDaemon(ctx, org.ID, []codersdk.ProvisionerType{
114+
codersdk.ProvisionerTypeTerraform,
115+
}, tags)
116+
}, &provisionerd.Options{
117+
Logger: logger,
118+
PollInterval: 500 * time.Millisecond,
119+
UpdateInterval: 500 * time.Millisecond,
120+
Provisioners: provisioners,
121+
WorkDirectory: tempDir,
122+
})
123+
124+
var exitErr error
125+
select {
126+
case <-notifyCtx.Done():
127+
exitErr = notifyCtx.Err()
128+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Bold.Render(
129+
"Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit",
130+
))
131+
case exitErr = <-errCh:
132+
}
133+
if exitErr != nil && !xerrors.Is(exitErr, context.Canceled) {
134+
cmd.Printf("Unexpected error, shutting down server: %s\n", exitErr)
135+
}
136+
137+
shutdown, shutdownCancel := context.WithTimeout(ctx, time.Minute)
138+
defer shutdownCancel()
139+
err = srv.Shutdown(shutdown)
140+
if err != nil {
141+
return xerrors.Errorf("shutdown: %w", err)
142+
}
143+
144+
cancel()
145+
if xerrors.Is(exitErr, context.Canceled) {
146+
return nil
147+
}
148+
return exitErr
149+
},
150+
}
151+
152+
cliflag.StringVarP(cmd.Flags(), &cacheDir, "cache-dir", "c", "CODER_CACHE_DIRECTORY", deployment.DefaultCacheDir(),
153+
"Specify a directory to cache provisioner job files.")
154+
cliflag.StringArrayVarP(cmd.Flags(), &rawTags, "tag", "t", "CODER_PROVISIONERD_TAGS", []string{},
155+
"Specify a list of tags to target provisioner jobs.")
156+
157+
return cmd
158+
}

enterprise/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ func enterpriseOnly() []*cobra.Command {
1212
features(),
1313
licenses(),
1414
groups(),
15+
provisionerDaemons(),
1516
}
1617
}
1718

enterprise/coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func New(ctx context.Context, options *Options) (*API, error) {
9292
})
9393
r.Route("/organizations/{organization}/provisionerdaemons", func(r chi.Router) {
9494
r.Use(
95+
api.provisionerDaemonsEnabledMW,
9596
apiKeyMiddleware,
9697
httpmw.ExtractOrganizationParam(api.Database),
9798
)

enterprise/coderd/provisionerdaemons.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ func (api *API) provisionerDaemonsEnabledMW(next http.Handler) http.Handler {
3636
api.entitlementsMu.RUnlock()
3737

3838
if !epd {
39-
httpapi.Write(r.Context(), rw, http.Status)
39+
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
40+
Message: "External provisioner daemons is an Enterprise feature. Contact sales!",
41+
})
4042
return
4143
}
4244

@@ -146,8 +148,6 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
146148
return
147149
}
148150

149-
fmt.Printf("TAGS %+v\n", daemon.Tags)
150-
151151
rawTags, err := json.Marshal(daemon.Tags)
152152
if err != nil {
153153
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{

enterprise/coderd/provisionerdaemons_test.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,26 @@ import (
1818

1919
func TestProvisionerDaemonServe(t *testing.T) {
2020
t.Parallel()
21+
t.Run("NoLicense", func(t *testing.T) {
22+
t.Parallel()
23+
client := coderdenttest.New(t, nil)
24+
user := coderdtest.CreateFirstUser(t, client)
25+
_, err := client.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{
26+
codersdk.ProvisionerTypeEcho,
27+
}, map[string]string{})
28+
require.Error(t, err)
29+
var apiError *codersdk.Error
30+
require.ErrorAs(t, err, &apiError)
31+
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
32+
})
33+
2134
t.Run("Organization", func(t *testing.T) {
2235
t.Parallel()
2336
client := coderdenttest.New(t, nil)
2437
user := coderdtest.CreateFirstUser(t, client)
38+
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
39+
ExternalProvisionerDaemons: true,
40+
})
2541
srv, err := client.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{
2642
codersdk.ProvisionerTypeEcho,
2743
}, map[string]string{})
@@ -33,6 +49,9 @@ func TestProvisionerDaemonServe(t *testing.T) {
3349
t.Parallel()
3450
client := coderdenttest.New(t, nil)
3551
user := coderdtest.CreateFirstUser(t, client)
52+
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
53+
ExternalProvisionerDaemons: true,
54+
})
3655
another := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
3756
_, err := another.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{
3857
codersdk.ProvisionerTypeEcho,
@@ -49,6 +68,9 @@ func TestProvisionerDaemonServe(t *testing.T) {
4968
t.Parallel()
5069
client := coderdenttest.New(t, nil)
5170
user := coderdtest.CreateFirstUser(t, client)
71+
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
72+
ExternalProvisionerDaemons: true,
73+
})
5274
closer := coderdtest.NewExternalProvisionerDaemon(t, client, user.OrganizationID, map[string]string{
5375
provisionerdserver.TagScope: provisionerdserver.ScopeUser,
5476
})
@@ -115,7 +137,3 @@ func TestProvisionerDaemonServe(t *testing.T) {
115137
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
116138
})
117139
}
118-
119-
func TestPostProvisionerDaemon(t *testing.T) {
120-
t.Parallel()
121-
}

0 commit comments

Comments
 (0)