From 6f20a64f9d4ee297f2bd636b7a48b45a9b395780 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 18 Jul 2024 15:43:07 -0400 Subject: [PATCH] chore: add multi-org flag to develop.sh (#13923) --- codersdk/organizations.go | 18 ++ docs/cli/provisionerd_start.md | 9 + enterprise/cli/provisionerdaemons.go | 37 +++- enterprise/cli/provisionerdaemons_test.go | 192 +++++++++++++++--- .../coder_provisionerd_start_--help.golden | 3 + scripts/develop.sh | 43 +++- 6 files changed, 269 insertions(+), 33 deletions(-) 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