Skip to content

Commit e52858f

Browse files
committed
feat(coderd): insert provisioner daemons
1 parent df7ed18 commit e52858f

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"
@@ -1161,8 +1163,19 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, name string
11611163
}
11621164
}()
11631165

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

11681181
mux := drpcmux.New()
@@ -1173,10 +1186,8 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, name string
11731186
api.AccessURL,
11741187
uuid.New(),
11751188
logger,
1176-
[]database.ProvisionerType{
1177-
database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform,
1178-
},
1179-
tags,
1189+
daemon.Provisioners,
1190+
provisionerdserver.Tags(daemon.Tags),
11801191
api.Database,
11811192
api.Pubsub,
11821193
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
@@ -162,9 +162,6 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization,
162162
}
163163

164164
// ProvisionerDaemons returns provisioner daemons available.
165-
//
166-
// Deprecated: We no longer track provisioner daemons as they connect. This function may return historical data
167-
// but new provisioner daemons will not appear.
168165
func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
169166
res, err := c.Request(ctx, http.MethodGet,
170167
// 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"
@@ -49,6 +50,10 @@ func TestProvisionerDaemonServe(t *testing.T) {
4950
})
5051
require.NoError(t, err)
5152
srv.DRPCConn().Close()
53+
54+
daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion.
55+
require.NoError(t, err)
56+
require.Len(t, daemons, 1)
5257
})
5358

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

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

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

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

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

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)