mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
chore: create workspaces and templates for multiple orgs (#13866)
* chore: creating workspaces and templates to work with orgs * handle wrong org selected * create org member in coderdtest helper
This commit is contained in:
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -29,6 +30,8 @@ func (r *RootCmd) create() *serpent.Command {
|
|||||||
parameterFlags workspaceParameterFlags
|
parameterFlags workspaceParameterFlags
|
||||||
autoUpdates string
|
autoUpdates string
|
||||||
copyParametersFrom string
|
copyParametersFrom string
|
||||||
|
// Organization context is only required if more than 1 template
|
||||||
|
// shares the same name across multiple organizations.
|
||||||
orgContext = NewOrganizationContext()
|
orgContext = NewOrganizationContext()
|
||||||
)
|
)
|
||||||
client := new(codersdk.Client)
|
client := new(codersdk.Client)
|
||||||
@ -44,11 +47,7 @@ func (r *RootCmd) create() *serpent.Command {
|
|||||||
),
|
),
|
||||||
Middleware: serpent.Chain(r.InitClient(client)),
|
Middleware: serpent.Chain(r.InitClient(client)),
|
||||||
Handler: func(inv *serpent.Invocation) error {
|
Handler: func(inv *serpent.Invocation) error {
|
||||||
organization, err := orgContext.Selected(inv, client)
|
var err error
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceOwner := codersdk.Me
|
workspaceOwner := codersdk.Me
|
||||||
if len(inv.Args) >= 1 {
|
if len(inv.Args) >= 1 {
|
||||||
workspaceOwner, workspaceName, err = splitNamedWorkspace(inv.Args[0])
|
workspaceOwner, workspaceName, err = splitNamedWorkspace(inv.Args[0])
|
||||||
@ -99,7 +98,7 @@ func (r *RootCmd) create() *serpent.Command {
|
|||||||
if templateName == "" {
|
if templateName == "" {
|
||||||
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a template below to preview the provisioned infrastructure:"))
|
_, _ = 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -111,13 +110,28 @@ func (r *RootCmd) create() *serpent.Command {
|
|||||||
templateNames := make([]string, 0, len(templates))
|
templateNames := make([]string, 0, len(templates))
|
||||||
templateByName := make(map[string]codersdk.Template, 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
|
||||||
|
}
|
||||||
|
|
||||||
for _, template := range templates {
|
for _, template := range templates {
|
||||||
templateName := template.Name
|
templateName := template.Name
|
||||||
|
if len(uniqueOrganizations) > 1 {
|
||||||
|
templateName += cliui.Placeholder(
|
||||||
|
fmt.Sprintf(
|
||||||
|
" (%s)",
|
||||||
|
template.OrganizationName,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if template.ActiveUserCount > 0 {
|
if template.ActiveUserCount > 0 {
|
||||||
templateName += cliui.Placeholder(
|
templateName += cliui.Placeholder(
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
" (used by %s)",
|
" used by %s",
|
||||||
formatActiveDevelopers(template.ActiveUserCount),
|
formatActiveDevelopers(template.ActiveUserCount),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -145,13 +159,65 @@ func (r *RootCmd) create() *serpent.Command {
|
|||||||
}
|
}
|
||||||
templateVersionID = sourceWorkspace.LatestBuild.TemplateVersionID
|
templateVersionID = sourceWorkspace.LatestBuild.TemplateVersionID
|
||||||
} else {
|
} else {
|
||||||
template, err = client.TemplateByName(inv.Context(), organization.ID, templateName)
|
templates, err := client.Templates(inv.Context(), codersdk.TemplateFilter{
|
||||||
|
ExactName: templateName,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("get template by name: %w", err)
|
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 {
|
||||||
|
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=<organization_name>` 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. Templates by that name exist in organizations: %s. Use --org=<organization_name> to select one.", templateName, selectedOrg.Name, strings.Join(templateOrgs, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// remake the list with the only template selected
|
||||||
|
templates = []codersdk.Template{templates[index]}
|
||||||
|
}
|
||||||
|
|
||||||
|
template = templates[0]
|
||||||
templateVersionID = template.ActiveVersionID
|
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
|
var schedSpec *string
|
||||||
if startAt != "" {
|
if startAt != "" {
|
||||||
sched, err := parseCLISchedule(startAt)
|
sched, err := parseCLISchedule(startAt)
|
||||||
@ -207,7 +273,7 @@ func (r *RootCmd) create() *serpent.Command {
|
|||||||
ttlMillis = ptr.Ref(stopAfter.Milliseconds())
|
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,
|
TemplateVersionID: templateVersionID,
|
||||||
Name: workspaceName,
|
Name: workspaceName,
|
||||||
AutostartSchedule: schedSpec,
|
AutostartSchedule: schedSpec,
|
||||||
|
11
cli/root.go
11
cli/root.go
@ -641,9 +641,10 @@ func NewOrganizationContext() *OrganizationContext {
|
|||||||
return &OrganizationContext{}
|
return &OrganizationContext{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*OrganizationContext) optionName() string { return "Organization" }
|
||||||
func (o *OrganizationContext) AttachOptions(cmd *serpent.Command) {
|
func (o *OrganizationContext) AttachOptions(cmd *serpent.Command) {
|
||||||
cmd.Options = append(cmd.Options, serpent.Option{
|
cmd.Options = append(cmd.Options, serpent.Option{
|
||||||
Name: "Organization",
|
Name: o.optionName(),
|
||||||
Description: "Select which organization (uuid or name) to use.",
|
Description: "Select which organization (uuid or name) to use.",
|
||||||
// Only required if the user is a part of more than 1 organization.
|
// Only required if the user is a part of more than 1 organization.
|
||||||
// Otherwise, we can assume a default value.
|
// 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) {
|
func (o *OrganizationContext) Selected(inv *serpent.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
|
||||||
// Fetch the set of organizations the user is a member of.
|
// Fetch the set of organizations the user is a member of.
|
||||||
orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me)
|
orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me)
|
||||||
|
@ -160,7 +160,7 @@ func (r *RootCmd) templateCreate() *serpent.Command {
|
|||||||
RequireActiveVersion: requireActiveVersion,
|
RequireActiveVersion: requireActiveVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = client.CreateTemplate(inv.Context(), organization.ID, createReq)
|
template, err := client.CreateTemplate(inv.Context(), organization.ID, createReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -171,7 +171,7 @@ func (r *RootCmd) templateCreate() *serpent.Command {
|
|||||||
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))+"! "+
|
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))+"! "+
|
||||||
"Developers can provision a workspace with this template using:")+"\n")
|
"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)
|
_, _ = fmt.Fprintln(inv.Stdout)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -244,6 +244,7 @@ func (r *RootCmd) templateCreate() *serpent.Command {
|
|||||||
|
|
||||||
cliui.SkipPromptOption(),
|
cliui.SkipPromptOption(),
|
||||||
}
|
}
|
||||||
|
orgContext.AttachOptions(cmd)
|
||||||
cmd.Options = append(cmd.Options, uploadFlags.options()...)
|
cmd.Options = append(cmd.Options, uploadFlags.options()...)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,9 @@ USAGE:
|
|||||||
flag
|
flag
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
|
-O, --org string, $CODER_ORGANIZATION
|
||||||
|
Select which organization (uuid or name) to use.
|
||||||
|
|
||||||
--default-ttl duration (default: 24h)
|
--default-ttl duration (default: 24h)
|
||||||
Specify a default TTL for workspaces created from this template. It is
|
Specify a default TTL for workspaces created from this template. It is
|
||||||
the default time before shutdown - workspaces created from this
|
the default time before shutdown - workspaces created from this
|
||||||
|
@ -199,8 +199,7 @@ func Templates(ctx context.Context, db database.Store, query string) (database.G
|
|||||||
parser := httpapi.NewQueryParamParser()
|
parser := httpapi.NewQueryParamParser()
|
||||||
filter := database.GetTemplatesWithFilterParams{
|
filter := database.GetTemplatesWithFilterParams{
|
||||||
Deleted: parser.Boolean(values, false, "deleted"),
|
Deleted: parser.Boolean(values, false, "deleted"),
|
||||||
// TODO: Should name be a fuzzy search?
|
ExactName: parser.String(values, "", "exact_name"),
|
||||||
ExactName: parser.String(values, "", "name"),
|
|
||||||
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
|
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
|
||||||
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
|
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
|
||||||
}
|
}
|
||||||
|
@ -365,6 +365,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui
|
|||||||
|
|
||||||
type TemplateFilter struct {
|
type TemplateFilter struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
|
ExactName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// asRequestOption returns a function that can be used in (*Client).Request.
|
// 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()))
|
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 := r.URL.Query()
|
||||||
q.Set("q", strings.Join(params, " "))
|
q.Set("q", strings.Join(params, " "))
|
||||||
r.URL.RawQuery = q.Encode()
|
r.URL.RawQuery = q.Encode()
|
||||||
|
9
docs/cli/templates_create.md
generated
9
docs/cli/templates_create.md
generated
@ -105,6 +105,15 @@ Requires workspace builds to use the active template version. This setting does
|
|||||||
|
|
||||||
Bypass prompts.
|
Bypass prompts.
|
||||||
|
|
||||||
|
### -O, --org
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ----------- | -------------------------------- |
|
||||||
|
| Type | <code>string</code> |
|
||||||
|
| Environment | <code>$CODER_ORGANIZATION</code> |
|
||||||
|
|
||||||
|
Select which organization (uuid or name) to use.
|
||||||
|
|
||||||
### -d, --directory
|
### -d, --directory
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
203
enterprise/cli/create_test.go
Normal file
203
enterprise/cli/create_test.go
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"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 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: true,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test creating a workspace in the second organization with a template
|
||||||
|
// name.
|
||||||
|
t.Run("CreateMultipleOrganization", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const templateName = "secondtemplate"
|
||||||
|
setup := setupMultipleOrganizations(t, setupArgs{
|
||||||
|
secondTemplates: []string{templateName},
|
||||||
|
})
|
||||||
|
member := setup.member
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"create",
|
||||||
|
"my-workspace",
|
||||||
|
"-y",
|
||||||
|
"--template", templateName,
|
||||||
|
}
|
||||||
|
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.Name, "workspace in second organization")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
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.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)
|
||||||
|
// The error message should indicate the flag to fix the issue.
|
||||||
|
require.ErrorContains(t, err, fmt.Sprintf("--org=%q", "first-organization"))
|
||||||
|
})
|
||||||
|
}
|
1
site/src/api/typesGenerated.ts
generated
1
site/src/api/typesGenerated.ts
generated
@ -1204,6 +1204,7 @@ export interface TemplateExample {
|
|||||||
// From codersdk/organizations.go
|
// From codersdk/organizations.go
|
||||||
export interface TemplateFilter {
|
export interface TemplateFilter {
|
||||||
readonly OrganizationID: string;
|
readonly OrganizationID: string;
|
||||||
|
readonly ExactName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/templates.go
|
// From codersdk/templates.go
|
||||||
|
Reference in New Issue
Block a user