feat: add --key flag to provisionerd start (#14002)

This commit is contained in:
Garrett Delfosse
2024-07-25 15:26:26 -04:00
committed by GitHub
parent c082868f55
commit 2279441517
4 changed files with 228 additions and 39 deletions

View File

@ -24,6 +24,7 @@ import (
"github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil" "github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/provisionerkey"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/drpc" "github.com/coder/coder/v2/codersdk/drpc"
"github.com/coder/coder/v2/provisioner/terraform" "github.com/coder/coder/v2/provisioner/terraform"
@ -46,6 +47,7 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
pollInterval time.Duration pollInterval time.Duration
pollJitter time.Duration pollJitter time.Duration
preSharedKey string preSharedKey string
provisionerKey string
verbose bool verbose bool
prometheusEnable bool prometheusEnable bool
@ -83,8 +85,8 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
return xerrors.Errorf("current organization: %w", err) return xerrors.Errorf("current organization: %w", err)
} }
if preSharedKey == "" { if preSharedKey == "" && provisionerKey == "" {
return xerrors.New("must provide a pre-shared key when not authenticated as a user") return xerrors.New("must provide a pre-shared key or provisioner key when not authenticated as a user")
} }
org = codersdk.Organization{MinimalOrganization: codersdk.MinimalOrganization{ID: uuid.Nil}} org = codersdk.Organization{MinimalOrganization: codersdk.MinimalOrganization{ID: uuid.Nil}}
@ -113,6 +115,19 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
return err return err
} }
if provisionerKey != "" {
if preSharedKey != "" {
return xerrors.New("cannot provide both provisioner key --key and pre-shared key --psk")
}
if len(rawTags) > 0 {
return xerrors.New("cannot provide tags when using provisioner key")
}
_, _, err := provisionerkey.Parse(provisionerKey)
if err != nil {
return xerrors.Errorf("parse provisioner key: %w", err)
}
}
logOpts := []clilog.Option{ logOpts := []clilog.Option{
clilog.WithFilter(logFilter...), clilog.WithFilter(logFilter...),
clilog.WithHuman(logHuman), clilog.WithHuman(logHuman),
@ -136,12 +151,17 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
logger.Info(ctx, "note: untagged provisioners can only pick up jobs from untagged templates") logger.Info(ctx, "note: untagged provisioners can only pick up jobs from untagged templates")
} }
// When authorizing with a PSK, we automatically scope the provisionerd // When authorizing with a PSK / provisioner key, we automatically scope the provisionerd
// to organization. Scoping to user with PSK auth is not a valid configuration. // to organization. Scoping to user with PSK / provisioner key auth is not a valid configuration.
if preSharedKey != "" { if preSharedKey != "" {
logger.Info(ctx, "psk auth automatically sets tag "+provisionersdk.TagScope+"="+provisionersdk.ScopeOrganization) logger.Info(ctx, "psk automatically sets tag "+provisionersdk.TagScope+"="+provisionersdk.ScopeOrganization)
tags[provisionersdk.TagScope] = provisionersdk.ScopeOrganization tags[provisionersdk.TagScope] = provisionersdk.ScopeOrganization
} }
if provisionerKey != "" {
logger.Info(ctx, "provisioner key auth automatically sets tag "+provisionersdk.TagScope+" empty")
// no scope tag will default to org scope
delete(tags, provisionersdk.TagScope)
}
err = os.MkdirAll(cacheDir, 0o700) err = os.MkdirAll(cacheDir, 0o700)
if err != nil { if err != nil {
@ -210,9 +230,10 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeTerraform, codersdk.ProvisionerTypeTerraform,
}, },
Tags: tags, Tags: tags,
PreSharedKey: preSharedKey, PreSharedKey: preSharedKey,
Organization: org.ID, Organization: org.ID,
ProvisionerKey: provisionerKey,
}) })
}, &provisionerd.Options{ }, &provisionerd.Options{
Logger: logger, Logger: logger,
@ -296,6 +317,13 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
Description: "Pre-shared key to authenticate with Coder server.", Description: "Pre-shared key to authenticate with Coder server.",
Value: serpent.StringOf(&preSharedKey), Value: serpent.StringOf(&preSharedKey),
}, },
{
Flag: "key",
Env: "CODER_PROVISIONER_DAEMON_KEY",
Description: "Provisioner key to authenticate with Coder server.",
Value: serpent.StringOf(&provisionerKey),
Hidden: true,
},
{ {
Flag: "name", Flag: "name",
Env: "CODER_PROVISIONER_DAEMON_NAME", Env: "CODER_PROVISIONER_DAEMON_NAME",

View File

@ -153,7 +153,7 @@ func TestProvisionerDaemon_PSK(t *testing.T) {
ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong) ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
defer cancel() defer cancel()
err = inv.WithContext(ctx).Run() err = inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "must provide a pre-shared key when not authenticated as a user") require.ErrorContains(t, err, "must provide a pre-shared key or provisioner key when not authenticated as a user")
}) })
} }
@ -301,6 +301,165 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
}) })
} }
func TestProvisionerDaemon_ProvisionerKey(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments.Append(string(codersdk.ExperimentMultiOrganization))
client, user := coderdenttest.New(t, &coderdenttest.Options{
ProvisionerDaemonPSK: "provisionersftw",
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
codersdk.FeatureMultipleOrganizations: 1,
},
},
Options: &coderdtest.Options{
DeploymentValues: dv,
},
})
// nolint:gocritic // test
res, err := client.CreateProvisionerKey(ctx, user.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: "dont-TEST-me",
})
require.NoError(t, err)
inv, conf := newCLI(t, "provisionerd", "start", "--key", res.Key, "--name=matt-daemon")
err = conf.URL().Write(client.URL.String())
require.NoError(t, err)
pty := ptytest.New(t).Attach(inv)
clitest.Start(t, inv)
pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon")
pty.ExpectMatchContext(ctx, "matt-daemon")
var daemons []codersdk.ProvisionerDaemon
require.Eventually(t, func() bool {
daemons, err = client.OrganizationProvisionerDaemons(ctx, user.OrganizationID)
if err != nil {
return false
}
return len(daemons) == 1
}, testutil.WaitShort, testutil.IntervalSlow)
require.Equal(t, "matt-daemon", daemons[0].Name)
require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
require.Equal(t, buildinfo.Version(), daemons[0].Version)
require.Equal(t, proto.CurrentVersion.String(), daemons[0].APIVersion)
})
t.Run("NoPSK", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments.Append(string(codersdk.ExperimentMultiOrganization))
client, user := coderdenttest.New(t, &coderdenttest.Options{
ProvisionerDaemonPSK: "provisionersftw",
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
codersdk.FeatureMultipleOrganizations: 1,
},
},
Options: &coderdtest.Options{
DeploymentValues: dv,
},
})
// nolint:gocritic // test
res, err := client.CreateProvisionerKey(ctx, user.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: "dont-TEST-me",
})
require.NoError(t, err)
inv, conf := newCLI(t, "provisionerd", "start", "--psk", "provisionersftw", "--key", res.Key, "--name=matt-daemon")
err = conf.URL().Write(client.URL.String())
require.NoError(t, err)
err = inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "cannot provide both provisioner key --key and pre-shared key --psk")
})
t.Run("NoTags", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments.Append(string(codersdk.ExperimentMultiOrganization))
client, user := coderdenttest.New(t, &coderdenttest.Options{
ProvisionerDaemonPSK: "provisionersftw",
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
codersdk.FeatureMultipleOrganizations: 1,
},
},
Options: &coderdtest.Options{
DeploymentValues: dv,
},
})
// nolint:gocritic // test
res, err := client.CreateProvisionerKey(ctx, user.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: "dont-TEST-me",
})
require.NoError(t, err)
inv, conf := newCLI(t, "provisionerd", "start", "--tag", "mykey=yourvalue", "--key", res.Key, "--name=matt-daemon")
err = conf.URL().Write(client.URL.String())
require.NoError(t, err)
err = inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "cannot provide tags when using provisioner key")
})
t.Run("AnotherOrg", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments.Append(string(codersdk.ExperimentMultiOrganization))
client, _ := coderdenttest.New(t, &coderdenttest.Options{
ProvisionerDaemonPSK: "provisionersftw",
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
codersdk.FeatureMultipleOrganizations: 1,
},
},
Options: &coderdtest.Options{
DeploymentValues: dv,
},
})
anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
// nolint:gocritic // test
res, err := client.CreateProvisionerKey(ctx, anotherOrg.ID, codersdk.CreateProvisionerKeyRequest{
Name: "dont-TEST-me",
})
require.NoError(t, err)
inv, conf := newCLI(t, "provisionerd", "start", "--org", anotherOrg.ID.String(), "--key", res.Key, "--name=matt-daemon")
err = conf.URL().Write(client.URL.String())
require.NoError(t, err)
pty := ptytest.New(t).Attach(inv)
clitest.Start(t, inv)
pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon")
pty.ExpectMatchContext(ctx, "matt-daemon")
var daemons []codersdk.ProvisionerDaemon
require.Eventually(t, func() bool {
daemons, err = client.OrganizationProvisionerDaemons(ctx, anotherOrg.ID)
if err != nil {
return false
}
return len(daemons) == 1
}, testutil.WaitShort, testutil.IntervalSlow)
require.Equal(t, "matt-daemon", daemons[0].Name)
require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
require.Equal(t, buildinfo.Version(), daemons[0].Version)
require.Equal(t, proto.CurrentVersion.String(), daemons[0].APIVersion)
})
}
//nolint:paralleltest,tparallel // Test uses a static port. //nolint:paralleltest,tparallel // Test uses a static port.
func TestProvisionerDaemon_PrometheusEnabled(t *testing.T) { func TestProvisionerDaemon_PrometheusEnabled(t *testing.T) {
// Ephemeral ports have a tendency to conflict and fail with `bind: address already in use` error. // Ephemeral ports have a tendency to conflict and fail with `bind: address already in use` error.

View File

@ -13,6 +13,7 @@ import (
"github.com/hashicorp/yamux" "github.com/hashicorp/yamux"
"github.com/moby/moby/pkg/namesgenerator" "github.com/moby/moby/pkg/namesgenerator"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"golang.org/x/exp/maps"
"golang.org/x/xerrors" "golang.org/x/xerrors"
"nhooyr.io/websocket" "nhooyr.io/websocket"
"storj.io/drpc/drpcmux" "storj.io/drpc/drpcmux"
@ -97,39 +98,43 @@ func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags
return nil, xerrors.New("Both API key and provisioner key authentication provided. Only one is allowed.") return nil, xerrors.New("Both API key and provisioner key authentication provided. Only one is allowed.")
} }
if apiKeyOK { // Provisioner Key Auth
tags = provisionersdk.MutateTags(apiKey.UserID, tags)
if tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {
// Any authenticated user can create provisioner daemons scoped
// for jobs that they own,
return tags, nil
}
ua := httpmw.UserAuthorization(r)
err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID))
if err != nil {
if !provAuth {
return nil, xerrors.New("user unauthorized")
}
// Allow fallback to PSK auth if the user is not allowed to create provisioner daemons.
// This is to preserve backwards compatibility with existing user provisioner daemons.
// If using PSK auth, the daemon is, by definition, scoped to the organization.
tags = provisionersdk.MutateTags(uuid.Nil, tags)
return tags, nil
}
// User is allowed to create provisioner daemons
return tags, nil
}
if pkOK { if pkOK {
if pk.OrganizationID != orgID { if pk.OrganizationID != orgID {
return nil, xerrors.New("provisioner key unauthorized") return nil, xerrors.New("provisioner key unauthorized")
} }
if tags != nil && !maps.Equal(tags, map[string]string{}) {
return nil, xerrors.New("tags are not allowed when using a provisioner key")
}
// If using provisioner key / PSK auth, the daemon is, by definition, scoped to the organization.
// Use the provisioner key tags here.
tags = provisionersdk.MutateTags(uuid.Nil, pk.Tags)
return tags, nil
} }
// If using provisioner key / PSK auth, the daemon is, by definition, scoped to the organization. // User Auth
tags = provisionersdk.MutateTags(uuid.Nil, tags) tags = provisionersdk.MutateTags(apiKey.UserID, tags)
if tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {
// Any authenticated user can create provisioner daemons scoped
// for jobs that they own,
return tags, nil
}
ua := httpmw.UserAuthorization(r)
err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID))
if err != nil {
if !provAuth {
return nil, xerrors.New("user unauthorized")
}
// Allow fallback to PSK auth if the user is not allowed to create provisioner daemons.
// This is to preserve backwards compatibility with existing user provisioner daemons.
// If using PSK auth, the daemon is, by definition, scoped to the organization.
tags = provisionersdk.MutateTags(uuid.Nil, tags)
return tags, nil
}
// User is allowed to create provisioner daemons
return tags, nil return tags, nil
} }

View File

@ -703,9 +703,6 @@ func TestProvisionerDaemonServe(t *testing.T) {
Provisioners: []codersdk.ProvisionerType{ Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeEcho,
}, },
Tags: map[string]string{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
},
PreSharedKey: tc.requestPSK, PreSharedKey: tc.requestPSK,
ProvisionerKey: tc.requestProvisionerKey, ProvisionerKey: tc.requestProvisionerKey,
}) })