From 6bfe27664112d3630290647e2e2a4a5fe1eebd5a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 10 Jul 2024 10:46:51 -0500 Subject: [PATCH 01/11] chore: creating workspaces and templates to work with orgs --- cli/create.go | 40 ++++++++++++++++++++++++++++-------- cli/templatecreate.go | 5 +++-- coderd/searchquery/search.go | 5 ++--- codersdk/organizations.go | 5 +++++ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/cli/create.go b/cli/create.go index df60fdd72d345..590b2abb3cae0 100644 --- a/cli/create.go +++ b/cli/create.go @@ -29,7 +29,9 @@ func (r *RootCmd) create() *serpent.Command { parameterFlags workspaceParameterFlags autoUpdates string copyParametersFrom string - orgContext = NewOrganizationContext() + // Organization context is only required if more than 1 template + // shares the same name across multiple organizations. + orgContext = NewOrganizationContext() ) client := new(codersdk.Client) cmd := &serpent.Command{ @@ -44,11 +46,7 @@ func (r *RootCmd) create() *serpent.Command { ), Middleware: serpent.Chain(r.InitClient(client)), Handler: func(inv *serpent.Invocation) error { - organization, err := orgContext.Selected(inv, client) - if err != nil { - return err - } - + var err error workspaceOwner := codersdk.Me if len(inv.Args) >= 1 { workspaceOwner, workspaceName, err = splitNamedWorkspace(inv.Args[0]) @@ -99,7 +97,7 @@ func (r *RootCmd) create() *serpent.Command { if templateName == "" { _, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a template below to preview the provisioned infrastructure:")) - templates, err := client.TemplatesByOrganization(inv.Context(), organization.ID) + templates, err := client.Templates(inv.Context(), codersdk.TemplateFilter{}) if err != nil { return err } @@ -145,10 +143,34 @@ func (r *RootCmd) create() *serpent.Command { } templateVersionID = sourceWorkspace.LatestBuild.TemplateVersionID } else { - template, err = client.TemplateByName(inv.Context(), organization.ID, templateName) + templates, err := client.Templates(inv.Context(), codersdk.TemplateFilter{ + ExactName: templateName, + }) if err != nil { return xerrors.Errorf("get template by name: %w", err) } + if len(templates) == 0 { + return xerrors.Errorf("no template found with the name %q", templateName) + } + + if len(templates) > 1 { + selectedOrg, err := orgContext.Selected(inv, client) + if err != nil { + return xerrors.Errorf("multiple templates found with the name %q, use `--org=` to specify which template by that name to use", templateName) + } + + index := slices.IndexFunc(templates, func(i codersdk.Template) bool { + return i.OrganizationID == selectedOrg.ID + }) + if index == -1 { + return xerrors.Errorf("no templates found with the name %q in the organization %q", templateName, selectedOrg.Name) + } + + // remake the list with the only template selected + templates = []codersdk.Template{templates[index]} + } + + template = templates[0] templateVersionID = template.ActiveVersionID } @@ -207,7 +229,7 @@ func (r *RootCmd) create() *serpent.Command { ttlMillis = ptr.Ref(stopAfter.Milliseconds()) } - workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, workspaceOwner, codersdk.CreateWorkspaceRequest{ + workspace, err := client.CreateWorkspace(inv.Context(), template.OrganizationID, workspaceOwner, codersdk.CreateWorkspaceRequest{ TemplateVersionID: templateVersionID, Name: workspaceName, AutostartSchedule: schedSpec, diff --git a/cli/templatecreate.go b/cli/templatecreate.go index e70a7bba03a8b..c6d81f3f53822 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -160,7 +160,7 @@ func (r *RootCmd) templateCreate() *serpent.Command { RequireActiveVersion: requireActiveVersion, } - _, err = client.CreateTemplate(inv.Context(), organization.ID, createReq) + template, err := client.CreateTemplate(inv.Context(), organization.ID, createReq) if err != nil { return err } @@ -171,7 +171,7 @@ func (r *RootCmd) templateCreate() *serpent.Command { pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))+"! "+ "Developers can provision a workspace with this template using:")+"\n") - _, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("coder create --template=%q [workspace name]", templateName))) + _, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("coder create --template=%q --org=%q [workspace name]", templateName, template.OrganizationName))) _, _ = fmt.Fprintln(inv.Stdout) return nil @@ -244,6 +244,7 @@ func (r *RootCmd) templateCreate() *serpent.Command { cliui.SkipPromptOption(), } + orgContext.AttachOptions(cmd) cmd.Options = append(cmd.Options, uploadFlags.options()...) return cmd } diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 0744ec8482926..2ad2a04f57356 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -198,9 +198,8 @@ func Templates(ctx context.Context, db database.Store, query string) (database.G parser := httpapi.NewQueryParamParser() filter := database.GetTemplatesWithFilterParams{ - Deleted: parser.Boolean(values, false, "deleted"), - // TODO: Should name be a fuzzy search? - ExactName: parser.String(values, "", "name"), + Deleted: parser.Boolean(values, false, "deleted"), + ExactName: parser.String(values, "", "exact_name"), IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"), Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"), } diff --git a/codersdk/organizations.go b/codersdk/organizations.go index ecc87ccb983ce..4cb59482d15e7 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -365,6 +365,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui type TemplateFilter struct { OrganizationID uuid.UUID + ExactName string } // asRequestOption returns a function that can be used in (*Client).Request. @@ -378,6 +379,10 @@ func (f TemplateFilter) asRequestOption() RequestOption { params = append(params, fmt.Sprintf("organization:%q", f.OrganizationID.String())) } + if f.ExactName != "" { + params = append(params, fmt.Sprintf("exact_name:%q", f.ExactName)) + } + q := r.URL.Query() q.Set("q", strings.Join(params, " ")) r.URL.RawQuery = q.Encode() From 8624d5474e9e5be58c05b0592934029cb9029b6e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 10 Jul 2024 11:13:50 -0500 Subject: [PATCH 02/11] handle wrong org selected --- cli/create.go | 33 +++++++++++++++++++++++++++++++-- cli/root.go | 11 ++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/cli/create.go b/cli/create.go index 590b2abb3cae0..9135a5d88ef83 100644 --- a/cli/create.go +++ b/cli/create.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "strings" "time" "github.com/google/uuid" @@ -154,16 +155,21 @@ func (r *RootCmd) create() *serpent.Command { } if len(templates) > 1 { + templateOrgs := []string{} + for _, tpl := range templates { + templateOrgs = append(templateOrgs, tpl.OrganizationName) + } + selectedOrg, err := orgContext.Selected(inv, client) if err != nil { - return xerrors.Errorf("multiple templates found with the name %q, use `--org=` to specify which template by that name to use", templateName) + return xerrors.Errorf("multiple templates found with the name %q, use `--org=` to specify which template by that name to use. Organizations available: %s", templateName, strings.Join(templateOrgs, ", ")) } index := slices.IndexFunc(templates, func(i codersdk.Template) bool { return i.OrganizationID == selectedOrg.ID }) if index == -1 { - return xerrors.Errorf("no templates found with the name %q in the organization %q", templateName, selectedOrg.Name) + return xerrors.Errorf("no templates found with the name %q in the organization %q. Templates by that name exist in organizations: %s. Use --org= to select one.", templateName, selectedOrg.Name, strings.Join(templateOrgs, ", ")) } // remake the list with the only template selected @@ -174,6 +180,29 @@ func (r *RootCmd) create() *serpent.Command { templateVersionID = template.ActiveVersionID } + // If the user specified an organization via a flag or env var, the template **must** + // be in that organization. Otherwise, we should throw an error. + orgValue, orgValueSource := orgContext.ValueSource(inv) + if orgValue != "" && !(orgValueSource == serpent.ValueSourceDefault || orgValueSource == serpent.ValueSourceNone) { + selectedOrg, err := orgContext.Selected(inv, client) + if err != nil { + return err + } + + if template.OrganizationID != selectedOrg.ID { + orgNameFormat := "'--org=%q'" + if orgValueSource == serpent.ValueSourceEnv { + orgNameFormat = "CODER_ORGANIZATION=%q" + } + + return xerrors.Errorf("template is in organization %q, but %s was specified. Use %s to use this template", + template.OrganizationName, + fmt.Sprintf(orgNameFormat, selectedOrg.Name), + fmt.Sprintf(orgNameFormat, template.OrganizationName), + ) + } + } + var schedSpec *string if startAt != "" { sched, err := parseCLISchedule(startAt) diff --git a/cli/root.go b/cli/root.go index 579f3c4c29202..22d153c00f7f1 100644 --- a/cli/root.go +++ b/cli/root.go @@ -641,9 +641,10 @@ func NewOrganizationContext() *OrganizationContext { return &OrganizationContext{} } +func (*OrganizationContext) optionName() string { return "Organization" } func (o *OrganizationContext) AttachOptions(cmd *serpent.Command) { cmd.Options = append(cmd.Options, serpent.Option{ - Name: "Organization", + Name: o.optionName(), Description: "Select which organization (uuid or name) to use.", // Only required if the user is a part of more than 1 organization. // Otherwise, we can assume a default value. @@ -655,6 +656,14 @@ func (o *OrganizationContext) AttachOptions(cmd *serpent.Command) { }) } +func (o *OrganizationContext) ValueSource(inv *serpent.Invocation) (string, serpent.ValueSource) { + opt := inv.Command.Options.ByName(o.optionName()) + if opt == nil { + return o.FlagSelect, serpent.ValueSourceNone + } + return o.FlagSelect, opt.ValueSource +} + func (o *OrganizationContext) Selected(inv *serpent.Invocation, client *codersdk.Client) (codersdk.Organization, error) { // Fetch the set of organizations the user is a member of. orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me) From 4bd97ffc4d2c0fc8fed74d0ae9aa1429c9ef7b89 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 10 Jul 2024 11:29:42 -0500 Subject: [PATCH 03/11] make gen --- docs/cli/templates_create.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md index de15a9fb905f8..c2ab11bd4916f 100644 --- a/docs/cli/templates_create.md +++ b/docs/cli/templates_create.md @@ -105,6 +105,15 @@ Requires workspace builds to use the active template version. This setting does Bypass prompts. +### -O, --org + +| | | +| ----------- | -------------------------------- | +| Type | string | +| Environment | $CODER_ORGANIZATION | + +Select which organization (uuid or name) to use. + ### -d, --directory | | | From 25fd02491135a0f76b4e6aedb9b1395ff888f618 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 10 Jul 2024 11:59:07 -0500 Subject: [PATCH 04/11] create org member in coderdtest helper --- enterprise/cli/create_test.go | 132 ++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 enterprise/cli/create_test.go diff --git a/enterprise/cli/create_test.go b/enterprise/cli/create_test.go new file mode 100644 index 0000000000000..e9bfa8c283acf --- /dev/null +++ b/enterprise/cli/create_test.go @@ -0,0 +1,132 @@ +package cli_test + +import ( + "context" + "sync" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/coder/v2/pty/ptytest" +) + +func TestEnterpriseCreate(t *testing.T) { + t.Parallel() + + type setupData struct { + firstResponse codersdk.CreateFirstUserResponse + second codersdk.Organization + owner *codersdk.Client + member *codersdk.Client + } + + type setupArgs struct { + firstTemplates []string + secondTemplates []string + } + + setupMultipleOrganizations := func(t *testing.T, args setupArgs) setupData { + ownerClient, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + // This only affects the first org. + IncludeProvisionerDaemon: false, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + + second := coderdtest.CreateOrganization(t, ownerClient, coderdtest.CreateOrganizationOptions{ + IncludeProvisionerDaemon: true, + }) + member, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.ScopedRoleOrgMember(second.ID)) + + var wg sync.WaitGroup + + createTemplate := func(tplName string, orgID uuid.UUID) { + version := coderdtest.CreateTemplateVersion(t, ownerClient, orgID, nil) + wg.Add(1) + go func() { + coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version.ID) + wg.Done() + }() + + coderdtest.CreateTemplate(t, ownerClient, orgID, version.ID, func(request *codersdk.CreateTemplateRequest) { + request.Name = tplName + }) + } + + for _, tplName := range args.firstTemplates { + createTemplate(tplName, first.OrganizationID) + } + + for _, tplName := range args.secondTemplates { + createTemplate(tplName, second.ID) + } + + wg.Wait() + + return setupData{ + firstResponse: first, + owner: ownerClient, + second: second, + member: member, + } + } + + t.Run("CreateMultipleOrganization", func(t *testing.T) { + // Creates a workspace in another organization + t.Parallel() + + const templateName = "secondtemplate" + setup := setupMultipleOrganizations(t, setupArgs{ + secondTemplates: []string{templateName}, + }) + member := setup.member + + args := []string{ + "create", + "my-workspace", + "--template", templateName, + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, member, root) + doneChan := make(chan struct{}) + pty := ptytest.New(t).Attach(inv) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + matches := []struct { + match string + write string + }{ + {match: "compute.main"}, + {match: "smith (linux, i386)"}, + {match: "Confirm create", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + if len(m.write) > 0 { + pty.WriteLine(m.write) + } + } + <-doneChan + + ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{}) + if assert.NoError(t, err, "expected workspace to be created") { + assert.Equal(t, ws.TemplateName, templateName) + assert.Equal(t, ws.OrganizationName, setup.second.ID, "workspace in second organization") + } + }) +} From da4d8eed7aa83db15ce121f3702dd08d17bc7e8a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 11 Jul 2024 10:13:58 -0500 Subject: [PATCH 05/11] add more tests --- coderd/coderdtest/coderdtest.go | 6 +- enterprise/cli/create_test.go | 114 +++++++++++++++++++++++++------- 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 97541ea927c98..1a84da7a069f6 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -637,14 +637,18 @@ func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui assert.NoError(t, err) }() + time.Sleep(time.Second) daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { - return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + client, err := client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), Name: t.Name(), Organization: org, Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho}, Tags: tags, }) + assert.NoError(t, err, "provisioner daemon failed to start") + + return client, err }, &provisionerd.Options{ Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug), UpdateInterval: 250 * time.Millisecond, diff --git a/enterprise/cli/create_test.go b/enterprise/cli/create_test.go index e9bfa8c283acf..5c9ef004540a4 100644 --- a/enterprise/cli/create_test.go +++ b/enterprise/cli/create_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" @@ -32,11 +33,13 @@ func TestEnterpriseCreate(t *testing.T) { secondTemplates []string } + // setupMultipleOrganizations creates an extra organization, assigns a member + // both organizations, and optionally creates templates in each organization. setupMultipleOrganizations := func(t *testing.T, args setupArgs) setupData { ownerClient, first := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ // This only affects the first org. - IncludeProvisionerDaemon: false, + IncludeProvisionerDaemon: true, }, LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ @@ -83,8 +86,9 @@ func TestEnterpriseCreate(t *testing.T) { } } + // Test creating a workspace in the second organization with a template + // name. t.Run("CreateMultipleOrganization", func(t *testing.T) { - // Creates a workspace in another organization t.Parallel() const templateName = "secondtemplate" @@ -96,37 +100,101 @@ func TestEnterpriseCreate(t *testing.T) { args := []string{ "create", "my-workspace", + "-y", "--template", templateName, } inv, root := clitest.New(t, args...) clitest.SetupConfig(t, member, root) - doneChan := make(chan struct{}) - pty := ptytest.New(t).Attach(inv) - go func() { - defer close(doneChan) - err := inv.Run() - assert.NoError(t, err) - }() - matches := []struct { - match string - write string - }{ - {match: "compute.main"}, - {match: "smith (linux, i386)"}, - {match: "Confirm create", write: "yes"}, + _ = ptytest.New(t).Attach(inv) + err := inv.Run() + require.NoError(t, err) + + ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{}) + if assert.NoError(t, err, "expected workspace to be created") { + assert.Equal(t, ws.TemplateName, templateName) + assert.Equal(t, ws.OrganizationName, setup.second.Name, "workspace in second organization") } - for _, m := range matches { - pty.ExpectMatch(m.match) - if len(m.write) > 0 { - pty.WriteLine(m.write) - } + }) + + // If a template name exists in two organizations, the workspace create will + // fail. + t.Run("AmbiguousTemplateName", func(t *testing.T) { + t.Parallel() + + const templateName = "ambiguous" + setup := setupMultipleOrganizations(t, setupArgs{ + firstTemplates: []string{templateName}, + secondTemplates: []string{templateName}, + }) + member := setup.member + + args := []string{ + "create", + "my-workspace", + "-y", + "--template", templateName, } - <-doneChan + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, member, root) + _ = ptytest.New(t).Attach(inv) + err := inv.Run() + require.Error(t, err, "expected error due to ambiguous template name") + require.ErrorContains(t, err, "multiple templates found") + }) + + // Ambiguous template names are allowed if the organization is specified. + t.Run("WorkingAmbiguousTemplateName", func(t *testing.T) { + t.Parallel() + + const templateName = "ambiguous" + setup := setupMultipleOrganizations(t, setupArgs{ + firstTemplates: []string{templateName}, + secondTemplates: []string{templateName}, + }) + member := setup.member + + args := []string{ + "create", + "my-workspace", + "-y", + "--template", templateName, + "--org", setup.second.Name, + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, member, root) + _ = ptytest.New(t).Attach(inv) + err := inv.Run() + require.NoError(t, err) ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{}) if assert.NoError(t, err, "expected workspace to be created") { assert.Equal(t, ws.TemplateName, templateName) - assert.Equal(t, ws.OrganizationName, setup.second.ID, "workspace in second organization") + assert.Equal(t, ws.OrganizationName, setup.second.Name, "workspace in second organization") } }) + + // If an organization is specified, but the template is not in that + // organization, an error is thrown. + t.Run("CreateIncorrectOrg", func(t *testing.T) { + t.Parallel() + + const templateName = "secondtemplate" + setup := setupMultipleOrganizations(t, setupArgs{ + firstTemplates: []string{templateName}, + }) + member := setup.member + + args := []string{ + "create", + "my-workspace", + "-y", + "--org", setup.second.Name, + "--template", templateName, + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, member, root) + _ = ptytest.New(t).Attach(inv) + err := inv.Run() + require.Error(t, err) + }) } From 932b502cf807ccec63d8e84bc53458896de4eda6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 11 Jul 2024 11:33:01 -0500 Subject: [PATCH 06/11] fixup --- coderd/coderdtest/coderdtest.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 1a84da7a069f6..97541ea927c98 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -637,18 +637,14 @@ func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui assert.NoError(t, err) }() - time.Sleep(time.Second) daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { - client, err := client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), Name: t.Name(), Organization: org, Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho}, Tags: tags, }) - assert.NoError(t, err, "provisioner daemon failed to start") - - return client, err }, &provisionerd.Options{ Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug), UpdateInterval: 250 * time.Millisecond, From cb01324c0daea343b34c0040516aa6816eeb7d73 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 11 Jul 2024 11:51:47 -0500 Subject: [PATCH 07/11] add extra assert --- enterprise/cli/create_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/enterprise/cli/create_test.go b/enterprise/cli/create_test.go index 5c9ef004540a4..085ecdf93ca4d 100644 --- a/enterprise/cli/create_test.go +++ b/enterprise/cli/create_test.go @@ -2,6 +2,7 @@ package cli_test import ( "context" + "fmt" "sync" "testing" @@ -196,5 +197,7 @@ func TestEnterpriseCreate(t *testing.T) { _ = ptytest.New(t).Attach(inv) err := inv.Run() require.Error(t, err) + // The error message should indicate the flag to fix the issue. + require.ErrorContains(t, err, fmt.Sprintf("--org=%q", "first-organization")) }) } From 3708ff1db7e65ecab5a3f1d88125a2c723a90ad1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 11 Jul 2024 11:59:38 -0500 Subject: [PATCH 08/11] update golden files --- cli/testdata/coder_templates_create_--help.golden | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/testdata/coder_templates_create_--help.golden b/cli/testdata/coder_templates_create_--help.golden index be37480655f04..5bb7bb96b6899 100644 --- a/cli/testdata/coder_templates_create_--help.golden +++ b/cli/testdata/coder_templates_create_--help.golden @@ -7,6 +7,9 @@ USAGE: flag OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + --default-ttl duration (default: 24h) Specify a default TTL for workspaces created from this template. It is the default time before shutdown - workspaces created from this From 632c1d64c31278b72cbb6903bfc9007cfb8908b5 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 11 Jul 2024 12:07:47 -0500 Subject: [PATCH 09/11] make gen --- site/src/api/typesGenerated.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 70318955fd18f..e4f81b703c181 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1203,6 +1203,7 @@ export interface TemplateExample { // From codersdk/organizations.go export interface TemplateFilter { readonly OrganizationID: string; + readonly ExactName: string; } // From codersdk/templates.go From c13ce5376d6b1ff337cc740cd3174c48ff05b509 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 11 Jul 2024 12:26:34 -0500 Subject: [PATCH 10/11] add organization to select prompt --- cli/create.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cli/create.go b/cli/create.go index 9135a5d88ef83..b9f145beab8b1 100644 --- a/cli/create.go +++ b/cli/create.go @@ -109,14 +109,26 @@ func (r *RootCmd) create() *serpent.Command { templateNames := make([]string, 0, len(templates)) templateByName := make(map[string]codersdk.Template, len(templates)) + uniqueOrganizations := make(map[uuid.UUID]bool) + for _, template := range templates { + uniqueOrganizations[template.OrganizationID] = true + } for _, template := range templates { templateName := template.Name + if len(uniqueOrganizations) > 1 { + templateName += cliui.Placeholder( + fmt.Sprintf( + " (%s)", + template.OrganizationName, + ), + ) + } if template.ActiveUserCount > 0 { templateName += cliui.Placeholder( fmt.Sprintf( - " (used by %s)", + " used by %s", formatActiveDevelopers(template.ActiveUserCount), ), ) From 7d8f2d064aff850b0e554c6c6cd9b7ad00590171 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 11 Jul 2024 13:08:43 -0500 Subject: [PATCH 11/11] add comment --- cli/create.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/create.go b/cli/create.go index b9f145beab8b1..bdf805ee26d69 100644 --- a/cli/create.go +++ b/cli/create.go @@ -109,6 +109,9 @@ func (r *RootCmd) create() *serpent.Command { templateNames := make([]string, 0, len(templates)) templateByName := make(map[string]codersdk.Template, len(templates)) + + // If more than 1 organization exists in the list of templates, + // then include the organization name in the select options. uniqueOrganizations := make(map[uuid.UUID]bool) for _, template := range templates { uniqueOrganizations[template.OrganizationID] = true