diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 49388aa3537a5..d4e473a17a610 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -600,6 +600,18 @@ func NewTaggedProvisionerDaemon(t testing.TB, coderAPI *coderd.API, name string, } func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer { + t.Helper() + + // Without this check, the provisioner will silently fail. + entitlements, err := client.Entitlements(context.Background()) + if err == nil { + feature := entitlements.Features[codersdk.FeatureExternalProvisionerDaemons] + if !feature.Enabled || feature.Entitlement != codersdk.EntitlementEntitled { + require.NoError(t, xerrors.Errorf("external provisioner daemons require an entitled license")) + return nil + } + } + echoClient, echoServer := drpc.MemTransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) serveDone := make(chan struct{}) @@ -638,6 +650,7 @@ func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui t.Cleanup(func() { _ = closer.Close() }) + return closer } @@ -790,6 +803,37 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI return other, user } +type CreateOrganizationOptions struct { + // IncludeProvisionerDaemon will spin up an external provisioner for the organization. + // This requires enterprise and the feature 'codersdk.FeatureExternalProvisionerDaemons' + IncludeProvisionerDaemon bool +} + +func CreateOrganization(t *testing.T, client *codersdk.Client, opts CreateOrganizationOptions, mutators ...func(*codersdk.CreateOrganizationRequest)) codersdk.Organization { + ctx := testutil.Context(t, testutil.WaitMedium) + req := codersdk.CreateOrganizationRequest{ + Name: strings.ReplaceAll(strings.ToLower(namesgenerator.GetRandomName(0)), "_", "-"), + DisplayName: namesgenerator.GetRandomName(1), + Description: namesgenerator.GetRandomName(1), + Icon: "", + } + for _, mutator := range mutators { + mutator(&req) + } + + org, err := client.CreateOrganization(ctx, req) + require.NoError(t, err) + + if opts.IncludeProvisionerDaemon { + closer := NewExternalProvisionerDaemon(t, client, org.ID, map[string]string{}) + t.Cleanup(func() { + _ = closer.Close() + }) + } + + return org +} + // CreateTemplateVersion creates a template import provisioner job // with the responses provided. It uses the "echo" provisioner for compatibility // with testing. diff --git a/enterprise/cli/templatecreate_test.go b/enterprise/cli/templatecreate_test.go index 987cac0b93058..3f089a62622a6 100644 --- a/enterprise/cli/templatecreate_test.go +++ b/enterprise/cli/templatecreate_test.go @@ -134,4 +134,71 @@ func TestTemplateCreate(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "your license is not entitled to use enterprise access control, so you cannot set --require-active-version") }) + + // Create a template in a second organization via custom role + t.Run("SecondOrganization", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} + ownerClient, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + // This only affects the first org. + IncludeProvisionerDaemon: false, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAccessControl: 1, + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + + // Create the second organization + secondOrg := coderdtest.CreateOrganization(t, ownerClient, coderdtest.CreateOrganizationOptions{ + IncludeProvisionerDaemon: true, + }) + + ctx := testutil.Context(t, testutil.WaitMedium) + + //nolint:gocritic // owner required to make custom roles + orgTemplateAdminRole, err := ownerClient.PatchOrganizationRole(ctx, secondOrg.ID, codersdk.Role{ + Name: "org-template-admin", + OrganizationID: secondOrg.ID.String(), + OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + codersdk.ResourceTemplate: codersdk.RBACResourceActions[codersdk.ResourceTemplate], + }), + }) + require.NoError(t, err, "create admin role") + + orgTemplateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, secondOrg.ID, rbac.RoleIdentifier{ + Name: orgTemplateAdminRole.Name, + OrganizationID: secondOrg.ID, + }) + + source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + }) + + const templateName = "new-template" + inv, conf := newCLI(t, "templates", + "push", templateName, + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "-y", + ) + + clitest.SetupConfig(t, orgTemplateAdmin, conf) + + err = inv.Run() + require.NoError(t, err) + + ctx = testutil.Context(t, testutil.WaitMedium) + template, err := orgTemplateAdmin.TemplateByName(ctx, secondOrg.ID, templateName) + require.NoError(t, err) + require.Equal(t, template.OrganizationID, secondOrg.ID) + }) } diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 7440ee743dca2..80000f2eb22b4 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -717,6 +717,57 @@ func TestTemplates(t *testing.T) { _, err = owner.Template(ctx, template.ID) require.NoError(t, err) }) + + // Create a template in a second organization via custom role + t.Run("SecondOrganization", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} + ownerClient, _ := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + IncludeProvisionerDaemon: false, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAccessControl: 1, + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitMedium) + secondOrg := coderdtest.CreateOrganization(t, ownerClient, coderdtest.CreateOrganizationOptions{ + IncludeProvisionerDaemon: true, + }) + + //nolint:gocritic // owner required to make custom roles + orgTemplateAdminRole, err := ownerClient.PatchOrganizationRole(ctx, secondOrg.ID, codersdk.Role{ + Name: "org-template-admin", + OrganizationID: secondOrg.ID.String(), + OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + codersdk.ResourceTemplate: codersdk.RBACResourceActions[codersdk.ResourceTemplate], + }), + }) + require.NoError(t, err, "create admin role") + + orgTemplateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, secondOrg.ID, rbac.RoleIdentifier{ + Name: orgTemplateAdminRole.Name, + OrganizationID: secondOrg.ID, + }) + + version := coderdtest.CreateTemplateVersion(t, orgTemplateAdmin, secondOrg.ID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + ProvisionPlan: echo.PlanComplete, + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, orgTemplateAdmin, version.ID) + + template := coderdtest.CreateTemplate(t, orgTemplateAdmin, secondOrg.ID, version.ID) + require.Equal(t, template.OrganizationID, secondOrg.ID) + }) } func TestTemplateACL(t *testing.T) {