diff --git a/codersdk/organizations.go b/codersdk/organizations.go
index 0841bdba8554f..041087b26709a 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 b781a4b5fe800..c3ccccbd0e1a1 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 079b1891346eb..cf127e2fb96b8 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"
@@ -74,6 +76,7 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
prometheusEnable bool
prometheusAddress string
)
+ orgContext := agpl.NewOrganizationContext()
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "start",
@@ -93,6 +96,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
@@ -196,16 +228,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,
@@ -346,6 +378,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 67054938d9068..3fdc31de062f2 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,
+ t.Run("OK", 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", "--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")
})
- 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)
}
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 90694af40f797..3f20d2d04eb72 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 3eb9c006003de..51f6ded4b96f5 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
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: