feat(coderd): add provisioner build version and api_version on serve (#11369)

* assert provisioner daemon version and api_version in unit tests
* add build info in HTTP header, extract codersdk.BuildVersionHeader
* add api_version to codersdk.ProvisionerDaemon
* testutil.MustString -> testutil.MustRandString
This commit is contained in:
Cian Johnston 2024-01-03 09:01:57 +00:00 committed by GitHub
parent 9031b498ea
commit 1ef96022b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 97 additions and 34 deletions

View File

@ -16,6 +16,7 @@ import (
"github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/pty/ptytest"
) )
@ -58,7 +59,7 @@ func TestLogin(t *testing.T) {
t.Parallel() t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Coder-Build-Version", "something") w.Header().Set(codersdk.BuildVersionHeader, "something")
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Not Found")) w.Write([]byte("Not Found"))
})) }))

3
coderd/apidoc/docs.go generated
View File

@ -10052,6 +10052,9 @@ const docTemplate = `{
"codersdk.ProvisionerDaemon": { "codersdk.ProvisionerDaemon": {
"type": "object", "type": "object",
"properties": { "properties": {
"api_version": {
"type": "string"
},
"created_at": { "created_at": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"

View File

@ -9036,6 +9036,9 @@
"codersdk.ProvisionerDaemon": { "codersdk.ProvisionerDaemon": {
"type": "object", "type": "object",
"properties": { "properties": {
"api_version": {
"type": "string"
},
"created_at": { "created_at": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"

View File

@ -564,7 +564,7 @@ func New(options *Options) *API {
// Build-Version is helpful for debugging. // Build-Version is helpful for debugging.
func(next http.Handler) http.Handler { func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Coder-Build-Version", buildinfo.Version()) w.Header().Add(codersdk.BuildVersionHeader, buildinfo.Version())
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
}, },
@ -1194,7 +1194,7 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, name string
Tags: provisionersdk.MutateTags(uuid.Nil, nil), Tags: provisionersdk.MutateTags(uuid.Nil, nil),
LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true}, LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true},
Version: buildinfo.Version(), Version: buildinfo.Version(),
APIVersion: "1.0", APIVersion: provisionersdk.APIVersionCurrent,
}) })
if err != nil { if err != nil {
return nil, xerrors.Errorf("failed to create in-memory provisioner daemon: %w", err) return nil, xerrors.Errorf("failed to create in-memory provisioner daemon: %w", err)

View File

@ -7279,6 +7279,7 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up
ReplicaID: uuid.NullUUID{}, ReplicaID: uuid.NullUUID{},
LastSeenAt: arg.LastSeenAt, LastSeenAt: arg.LastSeenAt,
Version: arg.Version, Version: arg.Version,
APIVersion: arg.APIVersion,
} }
q.provisionerDaemons = append(q.provisionerDaemons, d) q.provisionerDaemons = append(q.provisionerDaemons, d)
return d, nil return d, nil

View File

@ -218,7 +218,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) {
CreatedAt: now.Add(-14 * 24 * time.Hour), CreatedAt: now.Add(-14 * 24 * time.Hour),
LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-7 * 24 * time.Hour).Add(time.Minute)}, LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-7 * 24 * time.Hour).Add(time.Minute)},
Version: "1.0.0", Version: "1.0.0",
APIVersion: "1.0", APIVersion: provisionersdk.APIVersionCurrent,
}) })
require.NoError(t, err) require.NoError(t, err)
_, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
@ -229,7 +229,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) {
CreatedAt: now.Add(-8 * 24 * time.Hour), CreatedAt: now.Add(-8 * 24 * time.Hour),
LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-8 * 24 * time.Hour).Add(time.Hour)}, LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-8 * 24 * time.Hour).Add(time.Hour)},
Version: "1.0.0", Version: "1.0.0",
APIVersion: "1.0", APIVersion: provisionersdk.APIVersionCurrent,
}) })
require.NoError(t, err) require.NoError(t, err)
_, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
@ -242,7 +242,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) {
}, },
CreatedAt: now.Add(-9 * 24 * time.Hour), CreatedAt: now.Add(-9 * 24 * time.Hour),
Version: "1.0.0", Version: "1.0.0",
APIVersion: "1.0", APIVersion: provisionersdk.APIVersionCurrent,
}) })
require.NoError(t, err) require.NoError(t, err)
_, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
@ -256,7 +256,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) {
CreatedAt: now.Add(-6 * 24 * time.Hour), CreatedAt: now.Add(-6 * 24 * time.Hour),
LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-6 * 24 * time.Hour)}, LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-6 * 24 * time.Hour)},
Version: "1.0.0", Version: "1.0.0",
APIVersion: "1.0", APIVersion: provisionersdk.APIVersionCurrent,
}) })
require.NoError(t, err) require.NoError(t, err)

View File

@ -24,6 +24,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"cdr.dev/slog/sloggers/slogtest" "cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database"
@ -1784,8 +1785,8 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho}, Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: database.StringMap{}, Tags: database.StringMap{},
LastSeenAt: sql.NullTime{}, LastSeenAt: sql.NullTime{},
Version: "", Version: buildinfo.Version(),
APIVersion: "1.0", APIVersion: provisionersdk.APIVersionCurrent,
}) })
require.NoError(t, err) require.NoError(t, err)

View File

@ -78,6 +78,9 @@ const (
// ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon // ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon
ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK" ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK"
// BuildVersionHeader contains build information of Coder.
BuildVersionHeader = "X-Coder-Build-Version"
) )
// loggableMimeTypes is a list of MIME types that are safe to log // loggableMimeTypes is a list of MIME types that are safe to log

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
"nhooyr.io/websocket" "nhooyr.io/websocket"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/codersdk/drpc" "github.com/coder/coder/v2/codersdk/drpc"
"github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/provisionerd/runner" "github.com/coder/coder/v2/provisionerd/runner"
@ -41,6 +42,7 @@ type ProvisionerDaemon struct {
LastSeenAt NullTime `json:"last_seen_at,omitempty" format:"date-time"` LastSeenAt NullTime `json:"last_seen_at,omitempty" format:"date-time"`
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
APIVersion string `json:"api_version"`
Provisioners []ProvisionerType `json:"provisioners"` Provisioners []ProvisionerType `json:"provisioners"`
Tags map[string]string `json:"tags"` Tags map[string]string `json:"tags"`
} }
@ -212,6 +214,7 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
} }
headers := http.Header{} headers := http.Header{}
headers.Set(BuildVersionHeader, buildinfo.Version())
if req.PreSharedKey == "" { if req.PreSharedKey == "" {
// use session token if we don't have a PSK. // use session token if we don't have a PSK.
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)

View File

@ -203,7 +203,7 @@ func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
if res.StatusCode == http.StatusNotFound { if res.StatusCode == http.StatusNotFound {
// ensure we are talking to coder and not // ensure we are talking to coder and not
// some other service that returns 404 // some other service that returns 404
v := res.Header.Get("X-Coder-Build-Version") v := res.Header.Get(BuildVersionHeader)
if v == "" { if v == "" {
return false, xerrors.Errorf("missing build version header, not a coder instance") return false, xerrors.Errorf("missing build version header, not a coder instance")
} }

View File

@ -1051,6 +1051,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi
```json ```json
[ [
{ {
"api_version": "string",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
@ -1078,6 +1079,7 @@ Status Code **200**
| Name | Type | Required | Restrictions | Description | | Name | Type | Required | Restrictions | Description |
| ------------------- | ----------------- | -------- | ------------ | ----------- | | ------------------- | ----------------- | -------- | ------------ | ----------- |
| `[array item]` | array | false | | | | `[array item]` | array | false | | |
| `» api_version` | string | false | | |
| `» created_at` | string(date-time) | false | | | | `» created_at` | string(date-time) | false | | |
| `» id` | string(uuid) | false | | | | `» id` | string(uuid) | false | | |
| `» last_seen_at` | string(date-time) | false | | | | `» last_seen_at` | string(date-time) | false | | |

2
docs/api/schemas.md generated
View File

@ -3902,6 +3902,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json ```json
{ {
"api_version": "string",
"created_at": "2019-08-24T14:15:22Z", "created_at": "2019-08-24T14:15:22Z",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z", "last_seen_at": "2019-08-24T14:15:22Z",
@ -3919,6 +3920,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Name | Type | Required | Restrictions | Description | | Name | Type | Required | Restrictions | Description |
| ------------------ | --------------- | -------- | ------------ | ----------- | | ------------------ | --------------- | -------- | ------------ | ----------- |
| `api_version` | string | false | | |
| `created_at` | string | false | | | | `created_at` | string | false | | |
| `id` | string | false | | | | `id` | string | false | | |
| `last_seen_at` | string | false | | | | `last_seen_at` | string | false | | |

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac"
@ -49,6 +50,8 @@ func TestProvisionerDaemon_PSK(t *testing.T) {
}, testutil.WaitShort, testutil.IntervalSlow) }, testutil.WaitShort, testutil.IntervalSlow)
require.Equal(t, "matt-daemon", daemons[0].Name) require.Equal(t, "matt-daemon", daemons[0].Name)
require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope]) require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
require.Equal(t, buildinfo.Version(), daemons[0].Version)
require.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
} }
func TestProvisionerDaemon_SessionToken(t *testing.T) { func TestProvisionerDaemon_SessionToken(t *testing.T) {
@ -84,6 +87,8 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
assert.Equal(t, "my-daemon", daemons[0].Name) assert.Equal(t, "my-daemon", daemons[0].Name)
assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope]) assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope])
assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner]) assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner])
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
}) })
t.Run("ScopeAnotherUser", func(t *testing.T) { t.Run("ScopeAnotherUser", func(t *testing.T) {
@ -118,6 +123,8 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope]) assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope])
// This should get clobbered to the user who started the daemon. // This should get clobbered to the user who started the daemon.
assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner]) assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner])
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
}) })
t.Run("ScopeOrg", func(t *testing.T) { t.Run("ScopeOrg", func(t *testing.T) {
@ -150,5 +157,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
}, testutil.WaitShort, testutil.IntervalSlow) }, testutil.WaitShort, testutil.IntervalSlow)
assert.Equal(t, "org-daemon", daemons[0].Name) assert.Equal(t, "org-daemon", daemons[0].Name)
assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope]) assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
}) })
} }

View File

@ -15,9 +15,9 @@ import (
"github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/postgres" "github.com/coder/coder/v2/coderd/database/postgres"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/enterprise/dbcrypt" "github.com/coder/coder/v2/enterprise/dbcrypt"
"github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
) )
// TestServerDBCrypt tests end-to-end encryption, decryption, and deletion // TestServerDBCrypt tests end-to-end encryption, decryption, and deletion
@ -50,7 +50,7 @@ func TestServerDBCrypt(t *testing.T) {
users := genData(t, db) users := genData(t, db)
// Setup an initial cipher A // Setup an initial cipher A
keyA := mustString(t, 32) keyA := testutil.MustRandString(t, 32)
cipherA, err := dbcrypt.NewCiphers([]byte(keyA)) cipherA, err := dbcrypt.NewCiphers([]byte(keyA))
require.NoError(t, err) require.NoError(t, err)
@ -87,7 +87,7 @@ func TestServerDBCrypt(t *testing.T) {
} }
// Re-encrypt all existing data with a new cipher. // Re-encrypt all existing data with a new cipher.
keyB := mustString(t, 32) keyB := testutil.MustRandString(t, 32)
cipherBA, err := dbcrypt.NewCiphers([]byte(keyB), []byte(keyA)) cipherBA, err := dbcrypt.NewCiphers([]byte(keyB), []byte(keyA))
require.NoError(t, err) require.NoError(t, err)
@ -160,7 +160,7 @@ func TestServerDBCrypt(t *testing.T) {
} }
// Re-encrypt all existing data with a new cipher. // Re-encrypt all existing data with a new cipher.
keyC := mustString(t, 32) keyC := testutil.MustRandString(t, 32)
cipherC, err := dbcrypt.NewCiphers([]byte(keyC)) cipherC, err := dbcrypt.NewCiphers([]byte(keyC))
require.NoError(t, err) require.NoError(t, err)
@ -222,7 +222,7 @@ func genData(t *testing.T, db database.Store) []database.User {
for _, status := range database.AllUserStatusValues() { for _, status := range database.AllUserStatusValues() {
for _, loginType := range database.AllLoginTypeValues() { for _, loginType := range database.AllLoginTypeValues() {
for _, deleted := range []bool{false, true} { for _, deleted := range []bool{false, true} {
randName := mustString(t, 32) randName := testutil.MustRandString(t, 32)
usr := dbgen.User(t, db, database.User{ usr := dbgen.User(t, db, database.User{
Username: randName, Username: randName,
Email: randName + "@notcoder.com", Email: randName + "@notcoder.com",
@ -252,13 +252,6 @@ func genData(t *testing.T, db database.Store) []database.User {
return users return users
} }
func mustString(t *testing.T, n int) string {
t.Helper()
s, err := cryptorand.String(n)
require.NoError(t, err)
return s
}
func requireEncryptedEquals(t *testing.T, c dbcrypt.Cipher, expected, actual string) { func requireEncryptedEquals(t *testing.T, c dbcrypt.Cipher, expected, actual string) {
t.Helper() t.Helper()
var decodedVal []byte var decodedVal []byte

View File

@ -233,6 +233,8 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
authCtx = dbauthz.AsSystemRestricted(ctx) authCtx = dbauthz.AsSystemRestricted(ctx)
} }
versionHdrVal := r.Header.Get(codersdk.BuildVersionHeader)
// Create the daemon in the database. // Create the daemon in the database.
now := dbtime.Now() now := dbtime.Now()
daemon, err := api.Database.UpsertProvisionerDaemon(authCtx, database.UpsertProvisionerDaemonParams{ daemon, err := api.Database.UpsertProvisionerDaemon(authCtx, database.UpsertProvisionerDaemonParams{
@ -241,8 +243,8 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
Tags: tags, Tags: tags,
CreatedAt: now, CreatedAt: now,
LastSeenAt: sql.NullTime{Time: now, Valid: true}, LastSeenAt: sql.NullTime{Time: now, Valid: true},
Version: "", // TODO: provisionerd needs to send version Version: versionHdrVal,
APIVersion: "1.0", APIVersion: provisionersdk.APIVersionCurrent,
}) })
if err != nil { if err != nil {
if !xerrors.Is(err, context.Canceled) { if !xerrors.Is(err, context.Canceled) {
@ -361,6 +363,7 @@ func convertProvisionerDaemon(daemon database.ProvisionerDaemon) codersdk.Provis
Name: daemon.Name, Name: daemon.Name,
Tags: daemon.Tags, Tags: daemon.Tags,
Version: daemon.Version, Version: daemon.Version,
APIVersion: daemon.APIVersion,
} }
for _, provisionerType := range daemon.Provisioners { for _, provisionerType := range daemon.Provisioners {
result.Provisioners = append(result.Provisioners, codersdk.ProvisionerType(provisionerType)) result.Provisioners = append(result.Provisioners, codersdk.ProvisionerType(provisionerType))

View File

@ -12,6 +12,7 @@ import (
"cdr.dev/slog" "cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest" "cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac"
@ -40,9 +41,10 @@ func TestProvisionerDaemonServe(t *testing.T) {
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin())
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel() defer cancel()
daemonName := testutil.MustRandString(t, 63)
srv, err := templateAdminClient.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ srv, err := templateAdminClient.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
ID: uuid.New(), ID: uuid.New(),
Name: t.Name(), Name: daemonName,
Organization: user.OrganizationID, Organization: user.OrganizationID,
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,
@ -54,7 +56,11 @@ func TestProvisionerDaemonServe(t *testing.T) {
daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion. daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion.
require.NoError(t, err) require.NoError(t, err)
require.Len(t, daemons, 1) if assert.Len(t, daemons, 1) {
assert.Equal(t, daemonName, daemons[0].Name)
assert.Equal(t, buildinfo.Version(), daemons[0].Version)
assert.Equal(t, provisionersdk.APIVersionCurrent, daemons[0].APIVersion)
}
}) })
t.Run("NoLicense", func(t *testing.T) { t.Run("NoLicense", func(t *testing.T) {
@ -63,9 +69,10 @@ func TestProvisionerDaemonServe(t *testing.T) {
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin())
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel() defer cancel()
daemonName := testutil.MustRandString(t, 63)
_, err := templateAdminClient.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ _, err := templateAdminClient.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
ID: uuid.New(), ID: uuid.New(),
Name: t.Name(), Name: daemonName,
Organization: user.OrganizationID, Organization: user.OrganizationID,
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,
@ -90,7 +97,7 @@ func TestProvisionerDaemonServe(t *testing.T) {
defer cancel() defer cancel()
_, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
ID: uuid.New(), ID: uuid.New(),
Name: t.Name(), Name: testutil.MustRandString(t, 63),
Organization: user.OrganizationID, Organization: user.OrganizationID,
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,
@ -117,7 +124,7 @@ func TestProvisionerDaemonServe(t *testing.T) {
defer cancel() defer cancel()
_, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
ID: uuid.New(), ID: uuid.New(),
Name: t.Name(), Name: testutil.MustRandString(t, 63),
Organization: user.OrganizationID, Organization: user.OrganizationID,
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,
@ -212,7 +219,9 @@ func TestProvisionerDaemonServe(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel() defer cancel()
another := codersdk.New(client.URL) another := codersdk.New(client.URL)
daemonName := testutil.MustRandString(t, 63)
srv, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ srv, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
Name: daemonName,
Organization: user.OrganizationID, Organization: user.OrganizationID,
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,
@ -229,6 +238,7 @@ func TestProvisionerDaemonServe(t *testing.T) {
daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion. daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion.
require.NoError(t, err) require.NoError(t, err)
if assert.Len(t, daemons, 1) { if assert.Len(t, daemons, 1) {
assert.Equal(t, daemonName, daemons[0].Name)
assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope]) assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
} }
}) })
@ -274,7 +284,7 @@ func TestProvisionerDaemonServe(t *testing.T) {
pd := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { pd := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
return another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ return another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
ID: uuid.New(), ID: uuid.New(),
Name: t.Name(), Name: testutil.MustRandString(t, 63),
Organization: user.OrganizationID, Organization: user.OrganizationID,
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,
@ -352,7 +362,7 @@ func TestProvisionerDaemonServe(t *testing.T) {
defer cancel() defer cancel()
_, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
ID: uuid.New(), ID: uuid.New(),
Name: t.Name(), Name: testutil.MustRandString(t, 32),
Organization: user.OrganizationID, Organization: user.OrganizationID,
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,
@ -387,7 +397,7 @@ func TestProvisionerDaemonServe(t *testing.T) {
another := codersdk.New(client.URL) another := codersdk.New(client.URL)
_, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
ID: uuid.New(), ID: uuid.New(),
Name: t.Name(), Name: testutil.MustRandString(t, 63),
Organization: user.OrganizationID, Organization: user.OrganizationID,
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,
@ -420,7 +430,7 @@ func TestProvisionerDaemonServe(t *testing.T) {
another := codersdk.New(client.URL) another := codersdk.New(client.URL)
_, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
ID: uuid.New(), ID: uuid.New(),
Name: t.Name(), Name: testutil.MustRandString(t, 63),
Organization: user.OrganizationID, Organization: user.OrganizationID,
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,

View File

@ -323,7 +323,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
// Build-Version is helpful for debugging. // Build-Version is helpful for debugging.
func(next http.Handler) http.Handler { func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Coder-Build-Version", buildinfo.Version()) w.Header().Add(codersdk.BuildVersionHeader, buildinfo.Version())
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
}, },

View File

@ -20,6 +20,13 @@ import (
"github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/provisionersdk/proto"
) )
const (
// APIVersionCurrent is the current provisionerd API version.
// Breaking changes to the provisionerd API **MUST** increment
// the major version below.
APIVersionCurrent = "1.0"
)
// ServeOptions are configurations to serve a provisioner. // ServeOptions are configurations to serve a provisioner.
type ServeOptions struct { type ServeOptions struct {
// Listener serves multiple connections. Cannot be combined with Conn. // Listener serves multiple connections. Cannot be combined with Conn.

View File

@ -79,6 +79,7 @@ export const provisioners: TypesGen.ProvisionerDaemon[] = [
provisioners: [], provisioners: [],
tags: {}, tags: {},
version: "v2.34.5", version: "v2.34.5",
api_version: "1.0",
}, },
{ {
id: "cdr-basic", id: "cdr-basic",
@ -87,6 +88,7 @@ export const provisioners: TypesGen.ProvisionerDaemon[] = [
provisioners: [], provisioners: [],
tags: {}, tags: {},
version: "v2.34.5", version: "v2.34.5",
api_version: "1.0",
}, },
]; ];

View File

@ -809,6 +809,7 @@ export interface ProvisionerDaemon {
readonly last_seen_at?: string; readonly last_seen_at?: string;
readonly name: string; readonly name: string;
readonly version: string; readonly version: string;
readonly api_version: string;
readonly provisioners: ProvisionerType[]; readonly provisioners: ProvisionerType[];
readonly tags: Record<string, string>; readonly tags: Record<string, string>;
} }

View File

@ -336,6 +336,7 @@ export const MockProvisioner: TypesGen.ProvisionerDaemon = {
provisioners: ["echo"], provisioners: ["echo"],
tags: { scope: "organization" }, tags: { scope: "organization" },
version: "v2.34.5", version: "v2.34.5",
api_version: "1.0",
}; };
export const MockUserProvisioner: TypesGen.ProvisionerDaemon = { export const MockUserProvisioner: TypesGen.ProvisionerDaemon = {
@ -345,6 +346,7 @@ export const MockUserProvisioner: TypesGen.ProvisionerDaemon = {
provisioners: ["echo"], provisioners: ["echo"],
tags: { scope: "user", owner: "12345678-abcd-1234-abcd-1234567890abcd" }, tags: { scope: "user", owner: "12345678-abcd-1234-abcd-1234567890abcd" },
version: "v2.34.5", version: "v2.34.5",
api_version: "1.0",
}; };
export const MockProvisionerJob: TypesGen.ProvisionerJob = { export const MockProvisionerJob: TypesGen.ProvisionerJob = {

17
testutil/rand.go Normal file
View File

@ -0,0 +1,17 @@
package testutil
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cryptorand"
)
// MustRandString returns a random string of length n.
func MustRandString(t *testing.T, n int) string {
t.Helper()
s, err := cryptorand.String(n)
require.NoError(t, err)
return s
}