Skip to content

Commit e1d30f9

Browse files
committed
feat(coderd): insert provisioner daemons
1 parent 1e49190 commit e1d30f9

File tree

8 files changed

+133
-14
lines changed

8 files changed

+133
-14
lines changed

coderd/coderd.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/tls"
66
"crypto/x509"
7+
"database/sql"
78
"flag"
89
"fmt"
910
"io"
@@ -49,6 +50,7 @@ import (
4950
"github.com/coder/coder/v2/coderd/batchstats"
5051
"github.com/coder/coder/v2/coderd/database"
5152
"github.com/coder/coder/v2/coderd/database/dbauthz"
53+
"github.com/coder/coder/v2/coderd/database/dbtime"
5254
"github.com/coder/coder/v2/coderd/database/pubsub"
5355
"github.com/coder/coder/v2/coderd/gitsshkey"
5456
"github.com/coder/coder/v2/coderd/healthcheck"
@@ -1168,8 +1170,19 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, name string
11681170
}
11691171
}()
11701172

1171-
tags := provisionerdserver.Tags{
1172-
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
1173+
//nolint:gocritic // in-memory provisioners are owned by system
1174+
daemon, err := api.Database.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(ctx), database.UpsertProvisionerDaemonParams{
1175+
Name: name,
1176+
CreatedAt: dbtime.Now(),
1177+
Provisioners: []database.ProvisionerType{
1178+
database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform,
1179+
},
1180+
Tags: database.StringMap{provisionersdk.TagScope: provisionersdk.ScopeOrganization},
1181+
LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true},
1182+
Version: buildinfo.Version(),
1183+
})
1184+
if err != nil {
1185+
return nil, xerrors.Errorf("failed to create in-memory provisioner daemon: %w", err)
11731186
}
11741187

11751188
mux := drpcmux.New()
@@ -1180,10 +1193,8 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, name string
11801193
api.AccessURL,
11811194
uuid.New(),
11821195
logger,
1183-
[]database.ProvisionerType{
1184-
database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform,
1185-
},
1186-
tags,
1196+
daemon.Provisioners,
1197+
provisionerdserver.Tags(daemon.Tags),
11871198
api.Database,
11881199
api.Pubsub,
11891200
api.Acquirer,

coderd/database/dbauthz/dbauthz.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ var (
228228
rbac.ResourceOrganization.Type: {rbac.ActionCreate},
229229
rbac.ResourceOrganizationMember.Type: {rbac.ActionCreate},
230230
rbac.ResourceOrgRoleAssignment.Type: {rbac.ActionCreate},
231+
rbac.ResourceProvisionerDaemon.Type: {rbac.ActionCreate},
231232
rbac.ResourceUser.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
232233
rbac.ResourceUserData.Type: {rbac.ActionCreate, rbac.ActionUpdate},
233234
rbac.ResourceWorkspace.Type: {rbac.ActionUpdate},

coderd/rbac/roles.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
155155
// Users cannot do create/update/delete on themselves, but they
156156
// can read their own details.
157157
ResourceUser.Type: {ActionRead},
158+
// Users can create provisioner daemons scoped to themselves.
159+
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate},
158160
})...,
159161
),
160162
}.withCachedRegoValue()

codersdk/organizations.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,6 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization,
164164
}
165165

166166
// ProvisionerDaemons returns provisioner daemons available.
167-
//
168-
// Deprecated: We no longer track provisioner daemons as they connect. This function may return historical data
169-
// but new provisioner daemons will not appear.
170167
func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
171168
res, err := c.Request(ctx, http.MethodGet,
172169
// TODO: the organization path parameter is currently ignored.

enterprise/cli/provisionerdaemons_test.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import (
44
"context"
55
"testing"
66

7+
"github.com/stretchr/testify/assert"
78
"github.com/stretchr/testify/require"
89

910
"github.com/coder/coder/v2/cli/clitest"
1011
"github.com/coder/coder/v2/coderd/coderdtest"
12+
"github.com/coder/coder/v2/coderd/rbac"
1113
"github.com/coder/coder/v2/codersdk"
1214
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
1315
"github.com/coder/coder/v2/enterprise/coderd/license"
16+
"github.com/coder/coder/v2/provisionersdk"
1417
"github.com/coder/coder/v2/pty/ptytest"
1518
"github.com/coder/coder/v2/testutil"
1619
)
@@ -35,6 +38,17 @@ func TestProvisionerDaemon_PSK(t *testing.T) {
3538
clitest.Start(t, inv)
3639
pty.ExpectMatchContext(ctx, "starting provisioner daemon")
3740
pty.ExpectMatchContext(ctx, "matt-daemon")
41+
42+
var daemons []codersdk.ProvisionerDaemon
43+
require.Eventually(t, func() bool {
44+
daemons, err = client.ProvisionerDaemons(ctx)
45+
if err != nil {
46+
return false
47+
}
48+
return len(daemons) > 0
49+
}, testutil.WaitShort, testutil.IntervalSlow)
50+
require.Equal(t, "matt-daemon", daemons[0].Name)
51+
require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
3852
}
3953

4054
func TestProvisionerDaemon_SessionToken(t *testing.T) {
@@ -49,14 +63,27 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
4963
},
5064
},
5165
})
52-
anotherClient, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
53-
inv, conf := newCLI(t, "provisionerd", "start", "--tag", "scope=user")
66+
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
67+
inv, conf := newCLI(t, "provisionerd", "start", "--tag", "scope=user", "--name", "my-daemon")
5468
clitest.SetupConfig(t, anotherClient, conf)
5569
pty := ptytest.New(t).Attach(inv)
5670
ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
5771
defer cancel()
5872
clitest.Start(t, inv)
5973
pty.ExpectMatchContext(ctx, "starting provisioner daemon")
74+
75+
var daemons []codersdk.ProvisionerDaemon
76+
var err error
77+
require.Eventually(t, func() bool {
78+
daemons, err = client.ProvisionerDaemons(ctx)
79+
if err != nil {
80+
return false
81+
}
82+
return len(daemons) > 0
83+
}, testutil.WaitShort, testutil.IntervalSlow)
84+
assert.Equal(t, "my-daemon", daemons[0].Name)
85+
assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope])
86+
assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner])
6087
})
6188

6289
t.Run("ScopeOrg", func(t *testing.T) {
@@ -69,13 +96,25 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
6996
},
7097
},
7198
})
72-
anotherClient, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
73-
inv, conf := newCLI(t, "provisionerd", "start", "--tag", "scope=organization")
99+
anotherClient, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin())
100+
inv, conf := newCLI(t, "provisionerd", "start", "--tag", "scope=organization", "--name", "org-daemon")
74101
clitest.SetupConfig(t, anotherClient, conf)
75102
pty := ptytest.New(t).Attach(inv)
76103
ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
77104
defer cancel()
78105
clitest.Start(t, inv)
79106
pty.ExpectMatchContext(ctx, "starting provisioner daemon")
107+
108+
var daemons []codersdk.ProvisionerDaemon
109+
var err error
110+
require.Eventually(t, func() bool {
111+
daemons, err = client.ProvisionerDaemons(ctx)
112+
if err != nil {
113+
return false
114+
}
115+
return len(daemons) > 0
116+
}, testutil.WaitShort, testutil.IntervalSlow)
117+
assert.Equal(t, "org-daemon", daemons[0].Name)
118+
assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
80119
})
81120
}

enterprise/coderd/provisionerdaemons.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
"cdr.dev/slog"
2727
"github.com/coder/coder/v2/coderd"
2828
"github.com/coder/coder/v2/coderd/database"
29+
"github.com/coder/coder/v2/coderd/database/dbauthz"
30+
"github.com/coder/coder/v2/coderd/database/dbtime"
2931
"github.com/coder/coder/v2/coderd/httpapi"
3032
"github.com/coder/coder/v2/coderd/httpmw"
3133
"github.com/coder/coder/v2/coderd/provisionerdserver"
@@ -191,7 +193,10 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
191193
if !authorized {
192194
api.Logger.Warn(ctx, "unauthorized provisioner daemon serve request", slog.F("tags", tags))
193195
httpapi.Write(ctx, rw, http.StatusForbidden,
194-
codersdk.Response{Message: "You aren't allowed to create provisioner daemons"})
196+
codersdk.Response{
197+
Message: fmt.Sprintf("You aren't allowed to create provisioner daemons with scope %q", tags[provisionersdk.TagScope]),
198+
},
199+
)
195200
return
196201
}
197202
api.Logger.Debug(ctx, "provisioner authorized", slog.F("tags", tags))
@@ -221,6 +226,31 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
221226
slog.F("tags", tags),
222227
)
223228

229+
authCtx := ctx
230+
if r.Header.Get(codersdk.ProvisionerDaemonPSK) != "" {
231+
//nolint:gocritic // PSK auth means no actor in request,
232+
// so use system restricted.
233+
authCtx = dbauthz.AsSystemRestricted(ctx)
234+
}
235+
236+
// Create the daemon in the database.
237+
_, err := api.Database.UpsertProvisionerDaemon(authCtx, database.UpsertProvisionerDaemonParams{
238+
Name: name,
239+
Provisioners: provisioners,
240+
Tags: tags,
241+
CreatedAt: dbtime.Now(),
242+
LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true},
243+
Version: "", // TODO: provisionerd needs to send version
244+
})
245+
if err != nil {
246+
log.Error(ctx, "create provisioner daemon", slog.Error(err))
247+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
248+
Message: "Internal error creating provisioner daemon.",
249+
Detail: err.Error(),
250+
})
251+
return
252+
}
253+
224254
api.AGPL.WebsocketWaitMutex.Lock()
225255
api.AGPL.WebsocketWaitGroup.Add(1)
226256
api.AGPL.WebsocketWaitMutex.Unlock()

enterprise/coderd/provisionerdaemons_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/google/uuid"
10+
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
1112

1213
"cdr.dev/slog"
@@ -50,6 +51,10 @@ func TestProvisionerDaemonServe(t *testing.T) {
5051
})
5152
require.NoError(t, err)
5253
srv.DRPCConn().Close()
54+
55+
daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion.
56+
require.NoError(t, err)
57+
require.Len(t, daemons, 1)
5358
})
5459

5560
t.Run("NoLicense", func(t *testing.T) {
@@ -163,6 +168,15 @@ func TestProvisionerDaemonServe(t *testing.T) {
163168
file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, bytes.NewReader(data))
164169
require.NoError(t, err)
165170

171+
require.Eventually(t, func() bool {
172+
daemons, err := client.ProvisionerDaemons(context.Background())
173+
assert.NoError(t, err, "failed to get provisioner daemons")
174+
return len(daemons) > 0 &&
175+
assert.Equal(t, t.Name(), daemons[0].Name) &&
176+
assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope]) &&
177+
assert.Equal(t, user.UserID.String(), daemons[0].Tags[provisionersdk.TagOwner])
178+
}, testutil.WaitShort, testutil.IntervalMedium)
179+
166180
version, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{
167181
Name: "example",
168182
StorageMethod: codersdk.ProvisionerStorageMethodFile,
@@ -211,6 +225,12 @@ func TestProvisionerDaemonServe(t *testing.T) {
211225
require.NoError(t, err)
212226
err = srv.DRPCConn().Close()
213227
require.NoError(t, err)
228+
229+
daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion.
230+
require.NoError(t, err)
231+
if assert.Len(t, daemons, 1) {
232+
assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
233+
}
214234
})
215235

216236
t.Run("PSK_daily_cost", func(t *testing.T) {
@@ -346,6 +366,10 @@ func TestProvisionerDaemonServe(t *testing.T) {
346366
var apiError *codersdk.Error
347367
require.ErrorAs(t, err, &apiError)
348368
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
369+
370+
daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion.
371+
require.NoError(t, err)
372+
require.Len(t, daemons, 0)
349373
})
350374

351375
t.Run("NoAuth", func(t *testing.T) {
@@ -376,6 +400,10 @@ func TestProvisionerDaemonServe(t *testing.T) {
376400
var apiError *codersdk.Error
377401
require.ErrorAs(t, err, &apiError)
378402
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
403+
404+
daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion.
405+
require.NoError(t, err)
406+
require.Len(t, daemons, 0)
379407
})
380408

381409
t.Run("NoPSK", func(t *testing.T) {
@@ -406,5 +434,9 @@ func TestProvisionerDaemonServe(t *testing.T) {
406434
var apiError *codersdk.Error
407435
require.ErrorAs(t, err, &apiError)
408436
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
437+
438+
daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion.
439+
require.NoError(t, err)
440+
require.Len(t, daemons, 0)
409441
})
410442
}

provisionerd/provisionerd.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"net/http"
89
"reflect"
910
"sync"
1011
"time"
@@ -20,6 +21,7 @@ import (
2021

2122
"cdr.dev/slog"
2223
"github.com/coder/coder/v2/coderd/tracing"
24+
"github.com/coder/coder/v2/codersdk"
2325
"github.com/coder/coder/v2/provisionerd/proto"
2426
"github.com/coder/coder/v2/provisionerd/runner"
2527
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
@@ -199,6 +201,11 @@ connectLoop:
199201
if errors.Is(err, context.Canceled) {
200202
return
201203
}
204+
var sdkErr *codersdk.Error
205+
if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusForbidden {
206+
p.opts.Logger.Error(p.closeContext, "failed to dial coderd", slog.Error(err))
207+
return
208+
}
202209
if p.isClosed() {
203210
return
204211
}

0 commit comments

Comments
 (0)