diff --git a/codersdk/organizations.go b/codersdk/organizations.go
index 0841bdba85..041087b267 100644
--- a/codersdk/organizations.go
+++ b/codersdk/organizations.go
@@ -290,6 +290,24 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, e
return daemons, json.NewDecoder(res.Body).Decode(&daemons)
}
+func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerDaemon, error) {
+ res, err := c.Request(ctx, http.MethodGet,
+ fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organizationID.String()),
+ nil,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("execute request: %w", err)
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode != http.StatusOK {
+ return nil, ReadBodyAsError(res)
+ }
+
+ var daemons []ProvisionerDaemon
+ return daemons, json.NewDecoder(res.Body).Decode(&daemons)
+}
+
// CreateTemplateVersion processes source-code and optionally associates the version with a template.
// Executing without a template is useful for validating source-code.
func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid.UUID, req CreateTemplateVersionRequest) (TemplateVersion, error) {
diff --git a/docs/cli/provisionerd_start.md b/docs/cli/provisionerd_start.md
index b781a4b5fe..c3ccccbd0e 100644
--- a/docs/cli/provisionerd_start.md
+++ b/docs/cli/provisionerd_start.md
@@ -135,3 +135,12 @@ Serve prometheus metrics on the address defined by prometheus address.
| Default | 127.0.0.1:2112 |
The bind address to serve prometheus metrics.
+
+### -O, --org
+
+| | |
+| ----------- | -------------------------------- |
+| Type | string |
+| Environment | $CODER_ORGANIZATION |
+
+Select which organization (uuid or name) to use.
diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go
index 2ea8983adc..1f843da3cb 100644
--- a/enterprise/cli/provisionerdaemons.go
+++ b/enterprise/cli/provisionerdaemons.go
@@ -4,7 +4,9 @@ package cli
import (
"context"
+ "errors"
"fmt"
+ "net/http"
"os"
"regexp"
"time"
@@ -76,6 +78,7 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
prometheusEnable bool
prometheusAddress string
)
+ orgContext := agpl.NewOrganizationContext()
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "start",
@@ -95,6 +98,35 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
interruptCtx, interruptCancel := inv.SignalNotifyContext(ctx, agpl.InterruptSignals...)
defer interruptCancel()
+ // This can fail to get the current organization
+ // if the client is not authenticated as a user,
+ // like when only PSK is provided.
+ // This will be cleaner once PSK is replaced
+ // with org scoped authentication tokens.
+ org, err := orgContext.Selected(inv, client)
+ if err != nil {
+ var cErr *codersdk.Error
+ if !errors.As(err, &cErr) || cErr.StatusCode() != http.StatusUnauthorized {
+ return xerrors.Errorf("current organization: %w", err)
+ }
+
+ if preSharedKey == "" {
+ return xerrors.New("must provide a pre-shared key when not authenticated as a user")
+ }
+
+ org = codersdk.Organization{ID: uuid.Nil}
+ if orgContext.FlagSelect != "" {
+ // If we are using PSK, we can't fetch the organization
+ // to validate org name so we need the user to provide
+ // a valid organization ID.
+ orgID, err := uuid.Parse(orgContext.FlagSelect)
+ if err != nil {
+ return xerrors.New("must provide an org ID when not authenticated as a user and organization is specified")
+ }
+ org = codersdk.Organization{ID: orgID}
+ }
+ }
+
tags, err := agpl.ParseProvisionerTags(rawTags)
if err != nil {
return err
@@ -198,16 +230,16 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
connector := provisionerd.LocalProvisioners{
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(terraformClient),
}
- id := uuid.New()
srv := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
- ID: id,
+ ID: uuid.New(),
Name: name,
Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeTerraform,
},
Tags: tags,
PreSharedKey: preSharedKey,
+ Organization: org.ID,
})
}, &provisionerd.Options{
Logger: logger,
@@ -348,6 +380,7 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
Default: "127.0.0.1:2112",
},
}
+ orgContext.AttachOptions(cmd)
return cmd
}
diff --git a/enterprise/cli/provisionerdaemons_test.go b/enterprise/cli/provisionerdaemons_test.go
index 67054938d9..3fdc31de06 100644
--- a/enterprise/cli/provisionerdaemons_test.go
+++ b/enterprise/cli/provisionerdaemons_test.go
@@ -27,36 +27,134 @@ import (
func TestProvisionerDaemon_PSK(t *testing.T) {
t.Parallel()
- client, _ := coderdenttest.New(t, &coderdenttest.Options{
- ProvisionerDaemonPSK: "provisionersftw",
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{
- codersdk.FeatureExternalProvisionerDaemons: 1,
- },
- },
- })
- inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw", "--name=matt-daemon")
- err := conf.URL().Write(client.URL.String())
- require.NoError(t, err)
- pty := ptytest.New(t).Attach(inv)
- ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
- defer cancel()
- clitest.Start(t, inv)
- pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon")
- pty.ExpectMatchContext(ctx, "matt-daemon")
+ t.Run("OK", func(t *testing.T) {
+ t.Parallel()
- var daemons []codersdk.ProvisionerDaemon
- require.Eventually(t, func() bool {
- daemons, err = client.ProvisionerDaemons(ctx)
- 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)
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ ProvisionerDaemonPSK: "provisionersftw",
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ })
+ inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw", "--name=matt-daemon")
+ err := conf.URL().Write(client.URL.String())
+ require.NoError(t, err)
+ pty := ptytest.New(t).Attach(inv)
+ ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
+ defer cancel()
+ 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.ProvisionerDaemons(ctx)
+ 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("AnotherOrg", func(t *testing.T) {
+ t.Parallel()
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ ProvisionerDaemonPSK: "provisionersftw",
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ })
+ anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
+ inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw", "--name", "org-daemon", "--org", anotherOrg.ID.String())
+ err := conf.URL().Write(client.URL.String())
+ require.NoError(t, err)
+ pty := ptytest.New(t).Attach(inv)
+ ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
+ defer cancel()
+ clitest.Start(t, inv)
+ pty.ExpectMatchContext(ctx, "starting provisioner 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)
+ assert.Equal(t, "org-daemon", daemons[0].Name)
+ assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
+ assert.Equal(t, buildinfo.Version(), daemons[0].Version)
+ assert.Equal(t, proto.CurrentVersion.String(), daemons[0].APIVersion)
+ })
+
+ t.Run("AnotherOrgByNameWithUser", func(t *testing.T) {
+ t.Parallel()
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ ProvisionerDaemonPSK: "provisionersftw",
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ })
+ anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
+ anotherClient, _ := coderdtest.CreateAnotherUser(t, client, anotherOrg.ID, rbac.RoleTemplateAdmin())
+ inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw", "--name", "org-daemon", "--org", anotherOrg.Name)
+ clitest.SetupConfig(t, anotherClient, conf)
+ pty := ptytest.New(t).Attach(inv)
+ ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
+ defer cancel()
+ clitest.Start(t, inv)
+ pty.ExpectMatchContext(ctx, "starting provisioner daemon")
+ })
+
+ t.Run("AnotherOrgByNameNoUser", func(t *testing.T) {
+ t.Parallel()
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ ProvisionerDaemonPSK: "provisionersftw",
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ })
+ anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
+ inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw", "--name", "org-daemon", "--org", anotherOrg.Name)
+ err := conf.URL().Write(client.URL.String())
+ require.NoError(t, err)
+ ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
+ defer cancel()
+ err = inv.WithContext(ctx).Run()
+ require.ErrorContains(t, err, "must provide an org ID when not authenticated as a user and organization is specified")
+ })
+
+ t.Run("NoUserNoPSK", func(t *testing.T) {
+ t.Parallel()
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ ProvisionerDaemonPSK: "provisionersftw",
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ })
+ inv, conf := newCLI(t, "provisionerd", "start", "--name", "org-daemon")
+ err := conf.URL().Write(client.URL.String())
+ require.NoError(t, err)
+ ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
+ defer cancel()
+ err = inv.WithContext(ctx).Run()
+ require.ErrorContains(t, err, "must provide a pre-shared key when not authenticated as a user")
+ })
}
func TestProvisionerDaemon_SessionToken(t *testing.T) {
@@ -166,6 +264,42 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
assert.Equal(t, proto.CurrentVersion.String(), daemons[0].APIVersion)
})
+ t.Run("ScopeUserAnotherOrg", func(t *testing.T) {
+ t.Parallel()
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ ProvisionerDaemonPSK: "provisionersftw",
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ })
+ anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
+ anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, anotherOrg.ID, rbac.RoleTemplateAdmin())
+ inv, conf := newCLI(t, "provisionerd", "start", "--tag", "scope=user", "--name", "org-daemon", "--org", anotherOrg.ID.String())
+ clitest.SetupConfig(t, anotherClient, conf)
+ pty := ptytest.New(t).Attach(inv)
+ ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
+ defer cancel()
+ clitest.Start(t, inv)
+ pty.ExpectMatchContext(ctx, "starting provisioner daemon")
+
+ var daemons []codersdk.ProvisionerDaemon
+ var err error
+ 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)
+ assert.Equal(t, "org-daemon", daemons[0].Name)
+ assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope])
+ assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner])
+ assert.Equal(t, buildinfo.Version(), daemons[0].Version)
+ assert.Equal(t, proto.CurrentVersion.String(), daemons[0].APIVersion)
+ })
+
t.Run("PrometheusEnabled", func(t *testing.T) {
t.Parallel()
diff --git a/enterprise/cli/testdata/coder_provisionerd_start_--help.golden b/enterprise/cli/testdata/coder_provisionerd_start_--help.golden
index 90694af40f..3f20d2d04e 100644
--- a/enterprise/cli/testdata/coder_provisionerd_start_--help.golden
+++ b/enterprise/cli/testdata/coder_provisionerd_start_--help.golden
@@ -6,6 +6,9 @@ USAGE:
Run a provisioner daemon
OPTIONS:
+ -O, --org string, $CODER_ORGANIZATION
+ Select which organization (uuid or name) to use.
+
-c, --cache-dir string, $CODER_CACHE_DIRECTORY (default: [cache dir])
Directory to store cached data.
diff --git a/scripts/develop.sh b/scripts/develop.sh
index 3eb9c00600..51f6ded4b9 100755
--- a/scripts/develop.sh
+++ b/scripts/develop.sh
@@ -18,8 +18,9 @@ debug=0
DEFAULT_PASSWORD="SomeSecurePassword!"
password="${CODER_DEV_ADMIN_PASSWORD:-${DEFAULT_PASSWORD}}"
use_proxy=0
+multi_org=0
-args="$(getopt -o "" -l access-url:,use-proxy,agpl,debug,password: -- "$@")"
+args="$(getopt -o "" -l access-url:,use-proxy,agpl,debug,password:,multi-organization -- "$@")"
eval set -- "$args"
while true; do
case "$1" in
@@ -39,6 +40,10 @@ while true; do
use_proxy=1
shift
;;
+ --multi-organization)
+ multi_org=1
+ shift
+ ;;
--debug)
debug=1
shift
@@ -57,6 +62,10 @@ if [ "${CODER_BUILD_AGPL:-0}" -gt "0" ] && [ "${use_proxy}" -gt "0" ]; then
echo '== ERROR: cannot use both external proxies and APGL build.' && exit 1
fi
+if [ "${CODER_BUILD_AGPL:-0}" -gt "0" ] && [ "${multi_org}" -gt "0" ]; then
+ echo '== ERROR: cannot use both multi-organizations and APGL build.' && exit 1
+fi
+
# Preflight checks: ensure we have our required dependencies, and make sure nothing is listening on port 3000 or 8080
dependencies curl git go make pnpm
curl --fail http://127.0.0.1:3000 >/dev/null 2>&1 && echo '== ERROR: something is listening on port 3000. Kill it and re-run this script.' && exit 1
@@ -168,21 +177,51 @@ fatal() {
echo 'Failed to create regular user. To troubleshoot, try running this command manually.'
fi
+ # Create a new organization and add the member user to it.
+ if [ "${multi_org}" -gt "0" ]; then
+ another_org="second-organization"
+ if ! "${CODER_DEV_SHIM}" organizations show selected --org "${another_org}" >/dev/null 2>&1; then
+ echo "Creating organization '${another_org}'..."
+ (
+ "${CODER_DEV_SHIM}" organizations create -y "${another_org}"
+ ) || echo "Failed to create organization '${another_org}'"
+ fi
+
+ if ! "${CODER_DEV_SHIM}" org members list --org ${another_org} | grep "^member" >/dev/null 2>&1; then
+ echo "Adding member user to organization '${another_org}'..."
+ (
+ "${CODER_DEV_SHIM}" organizations members add member --org "${another_org}"
+ ) || echo "Failed to add member user to organization '${another_org}'"
+ fi
+
+ echo "Starting external provisioner for '${another_org}'..."
+ (
+ start_cmd EXT_PROVISIONER "" "${CODER_DEV_SHIM}" provisionerd start --tag "scope=organization" --name second-org-daemon --org "${another_org}"
+ ) || echo "Failed to start external provisioner. No external provisioner started."
+ fi
+
# If we have docker available and the "docker" template doesn't already
# exist, then let's try to create a template!
template_name="docker"
if docker info >/dev/null 2>&1 && ! "${CODER_DEV_SHIM}" templates versions list "${template_name}" >/dev/null 2>&1; then
# sometimes terraform isn't installed yet when we go to create the
# template
+ echo "Waiting for terraform to be installed..."
sleep 5
+ echo "Initializing docker template..."
temp_template_dir="$(mktemp -d)"
"${CODER_DEV_SHIM}" templates init --id "${template_name}" "${temp_template_dir}"
DOCKER_HOST="$(docker context inspect --format '{{ .Endpoints.docker.Host }}')"
printf 'docker_arch: "%s"\ndocker_host: "%s"\n' "${GOARCH}" "${DOCKER_HOST}" >"${temp_template_dir}/params.yaml"
(
- "${CODER_DEV_SHIM}" templates push "${template_name}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes
+ echo "Pushing docker template to 'first-organization'..."
+ "${CODER_DEV_SHIM}" templates push "${template_name}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes --org first-organization
+ if [ "${multi_org}" -gt "0" ]; then
+ echo "Pushing docker template to '${another_org}'..."
+ "${CODER_DEV_SHIM}" templates push "${template_name}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes --org "${another_org}"
+ fi
rm -rfv "${temp_template_dir}" # Only delete template dir if template creation succeeds
) || echo "Failed to create a template. The template files are in ${temp_template_dir}"
fi