From 5cb09b9a49b3681ccaebc2b5b62789487813a596 Mon Sep 17 00:00:00 2001 From: Karl Kirch Date: Tue, 15 Feb 2022 10:51:28 -0600 Subject: [PATCH 1/4] Add registry provider client and integration tests --- helper_test.go | 79 ++++- registry_provider.go | 248 +++++++++++++++ registry_provider_integration_test.go | 442 ++++++++++++++++++++++++++ registry_provider_platform.go | 16 + registry_provider_version.go | 16 + tfe.go | 2 + 6 files changed, 798 insertions(+), 5 deletions(-) create mode 100644 registry_provider.go create mode 100644 registry_provider_integration_test.go create mode 100644 registry_provider_platform.go create mode 100644 registry_provider_version.go diff --git a/helper_test.go b/helper_test.go index 4aef68cf1..3ac4cd9fb 100644 --- a/helper_test.go +++ b/helper_test.go @@ -720,23 +720,24 @@ func createRegistryModuleWithVersion(t *testing.T, client *Client, org *Organiza } func createRunTask(t *testing.T, client *Client, org *Organization) (*RunTask, func()) { + runTaskURL := os.Getenv("TFC_RUN_TASK_URL") + if runTaskURL == "" { + t.Error("Cannot create a run task with an empty URL. You must set TFC_RUN_TASK_URL for run task related tests.") + } + var orgCleanup func() if org == nil { org, orgCleanup = createOrganization(t, client) } - runTaskURL := os.Getenv("TFC_RUN_TASK_URL") - if runTaskURL == "" { - t.Error("Cannot create a run task with an empty URL. You must set TFC_RUN_TASK_URL for run task related tests.") - } - ctx := context.Background() r, err := client.RunTasks.Create(ctx, org.Name, RunTaskCreateOptions{ Name: "tst-" + randomString(t), URL: runTaskURL, Category: "task", }) + if err != nil { t.Fatal(err) } @@ -754,6 +755,74 @@ func createRunTask(t *testing.T, client *Client, org *Organization) (*RunTask, f } } +func createPrivateRegistryProvider(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) { + var orgCleanup func() + + if org == nil { + org, orgCleanup = createOrganization(t, client) + } + + ctx := context.Background() + + privateName := PrivateRegistry + + options := RegistryProviderCreateOptions{ + Name: String("tst-name-" + randomString(t)), + Namespace: &org.Name, + RegistryName: &privateName, + } + prv, err := client.RegistryProviders.Create(ctx, org.Name, options) + if err != nil { + t.Fatal(err) + } + + return prv, func() { + if err := client.RegistryProviders.Delete(ctx, org.Name, prv.RegistryName, prv.Namespace, prv.Name); err != nil { + t.Errorf("Error destroying registry provider! WARNING: Dangling resources\n"+ + "may exist! The full error is shown below.\n\n"+ + "Registry Provider: %s/%s\nError: %s", prv.Namespace, prv.Name, err) + } + + if orgCleanup != nil { + orgCleanup() + } + } +} + +func createPublicRegistryProvider(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) { + var orgCleanup func() + + if org == nil { + org, orgCleanup = createOrganization(t, client) + } + + ctx := context.Background() + + publicName := PublicRegistry + + options := RegistryProviderCreateOptions{ + Name: String("tst-name-" + randomString(t)), + Namespace: String("tst-namespace-" + randomString(t)), + RegistryName: &publicName, + } + prv, err := client.RegistryProviders.Create(ctx, org.Name, options) + if err != nil { + t.Fatal(err) + } + + return prv, func() { + if err := client.RegistryProviders.Delete(ctx, org.Name, prv.RegistryName, prv.Namespace, prv.Name); err != nil { + t.Errorf("Error destroying registry provider! WARNING: Dangling resources\n"+ + "may exist! The full error is shown below.\n\n"+ + "Registry Provider: %s/%s\nError: %s", prv.Namespace, prv.Name, err) + } + + if orgCleanup != nil { + orgCleanup() + } + } +} + func createSSHKey(t *testing.T, client *Client, org *Organization) (*SSHKey, func()) { var orgCleanup func() diff --git a/registry_provider.go b/registry_provider.go new file mode 100644 index 000000000..e02e94956 --- /dev/null +++ b/registry_provider.go @@ -0,0 +1,248 @@ +package tfe + +import ( + "context" + "errors" + "fmt" + "net/url" +) + +// Compile-time proof of interface implementation. +var _ RegistryProviders = (*registryProviders)(nil) + +// RegistryProviders describes all the registry provider related methods that the Terraform +// Enterprise API supports. +// +// TFE API docs: https://www.terraform.io/docs/cloud/api/providers.html +type RegistryProviders interface { + // List all the providers within an organization. + List(ctx context.Context, organization string, options *RegistryProviderListOptions) (*RegistryProviderList, error) + + // Create a registry provider + Create(ctx context.Context, organization string, options RegistryProviderCreateOptions) (*RegistryProvider, error) + + // Read a registry provider + Read(ctx context.Context, organization string, registryName RegistryName, namespace string, name string, options *RegistryProviderReadOptions) (*RegistryProvider, error) + + // Delete a registry provider + Delete(ctx context.Context, organization string, registryName RegistryName, namespace string, name string) error +} + +// registryProviders implements RegistryProviders. +type registryProviders struct { + client *Client +} + +// RegistryName represents which registry is being targeted +type RegistryName string + +// List of available registry names +const ( + PrivateRegistry RegistryName = "private" + PublicRegistry RegistryName = "public" +) + +// RegistryProvider represents a registry provider +type RegistryProvider struct { + ID string `jsonapi:"primary,registry-providers"` + Namespace string `jsonapi:"attr,namespace"` + Name string `jsonapi:"attr,name"` + RegistryName RegistryName `jsonapi:"attr,registry-name"` + Permissions *RegistryProviderPermissions `jsonapi:"attr,permissions"` + CreatedAt string `jsonapi:"attr,created-at"` + UpdatedAt string `jsonapi:"attr,updated-at"` + + // Relations + Organization *Organization `jsonapi:"relation,organization"` + RegistryProviderVersions []RegistryProviderVersion `jsonapi:"relation,registry-provider-version"` +} + +type RegistryProviderPermissions struct { + CanDelete bool `jsonapi:"attr,can-delete"` +} + +type RegistryProviderListOptions struct { + ListOptions + // A query string to filter by registry_name + RegistryName *RegistryName `url:"filter[registry_name],omitempty"` + // A query string to filter by organization + OrganizationName *string `url:"filter[organization_name],omitempty"` + // A query string to do a fuzzy search + Search *string `url:"q,omitempty"` +} + +type RegistryProviderList struct { + *Pagination + Items []*RegistryProvider +} + +func (o RegistryProviderListOptions) valid() error { + return nil +} + +func (r *registryProviders) List(ctx context.Context, organization string, options *RegistryProviderListOptions) (*RegistryProviderList, error) { + + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + if options != nil { + if err := options.valid(); err != nil { + return nil, err + } + } + + u := fmt.Sprintf("organizations/%s/registry-providers", url.QueryEscape(organization)) + req, err := r.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + pl := &RegistryProviderList{} + err = r.client.do(ctx, req, pl) + if err != nil { + return nil, err + } + + return pl, nil +} + +// RegistryProviderCreateOptions is used when creating a registry provider +type RegistryProviderCreateOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,registry-providers"` + + Namespace *string `jsonapi:"attr,namespace"` + Name *string `jsonapi:"attr,name"` + RegistryName *RegistryName `jsonapi:"attr,registry-name"` +} + +func (o RegistryProviderCreateOptions) valid() error { + if !validString(o.Name) { + return ErrRequiredName + } + if !validStringID(o.Name) { + return ErrInvalidName + } + if !validString(o.Namespace) { + return errors.New("namespace is required") + } + if !validStringID(o.Namespace) { + return errors.New("invalid value for namespace") + } + if !validString((*string)(o.RegistryName)) { + return errors.New("registry-name is required") + } + return nil +} + +func (r *registryProviders) Create(ctx context.Context, organization string, options RegistryProviderCreateOptions) (*RegistryProvider, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + if err := options.valid(); err != nil { + return nil, err + } + // Private providers must match their namespace and organization name + // This is enforced by the API as well + if *options.RegistryName == PrivateRegistry && organization != *options.Namespace { + return nil, errors.New("namespace must match organization name for private providers") + } + + u := fmt.Sprintf( + "organizations/%s/registry-providers", + url.QueryEscape(organization), + ) + req, err := r.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + prv := &RegistryProvider{} + err = r.client.do(ctx, req, prv) + if err != nil { + return nil, err + } + + return prv, nil +} + +type RegistryProviderReadOptions struct { +} + +func (r *registryProviders) Read(ctx context.Context, organization string, registryName RegistryName, namespace string, name string, options *RegistryProviderReadOptions) (*RegistryProvider, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + if !validString(&name) { + return nil, ErrRequiredName + } + if !validStringID(&name) { + return nil, ErrInvalidName + } + if !validString(&namespace) { + return nil, errors.New("namespace is required") + } + if !validStringID(&namespace) { + return nil, errors.New("invalid value for namespace") + } + if !validString((*string)(®istryName)) { + return nil, errors.New("registry-name is required") + } + + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s", + url.QueryEscape(organization), + url.QueryEscape(string(registryName)), + url.QueryEscape(namespace), + url.QueryEscape(name), + ) + req, err := r.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + prv := &RegistryProvider{} + err = r.client.do(ctx, req, prv) + if err != nil { + return nil, err + } + + return prv, nil +} + +func (r *registryProviders) Delete(ctx context.Context, organization string, registryName RegistryName, namespace string, name string) error { + if !validStringID(&organization) { + return ErrInvalidOrg + } + if !validString(&name) { + return ErrRequiredName + } + if !validStringID(&name) { + return ErrInvalidName + } + if !validString(&namespace) { + return errors.New("namespace is required") + } + if !validStringID(&namespace) { + return errors.New("invalid value for namespace") + } + if !validString((*string)(®istryName)) { + return errors.New("registry-name is required") + } + + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s", + url.QueryEscape(organization), + url.QueryEscape(string(registryName)), + url.QueryEscape(namespace), + url.QueryEscape(name), + ) + req, err := r.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return r.client.do(ctx, req, nil) +} diff --git a/registry_provider_integration_test.go b/registry_provider_integration_test.go new file mode 100644 index 000000000..9f26bda28 --- /dev/null +++ b/registry_provider_integration_test.go @@ -0,0 +1,442 @@ +package tfe + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRegistryProvidersList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + t.Run("with providers", func(t *testing.T) { + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + createN := 10 + providers := make([]*RegistryProvider, 0) + // these providers will be destroyed when the org is cleaned up + for i := 0; i < createN; i++ { + providerTest, _ := createPublicRegistryProvider(t, client, orgTest) + providers = append(providers, providerTest) + } + for i := 0; i < createN; i++ { + providerTest, _ := createPrivateRegistryProvider(t, client, orgTest) + providers = append(providers, providerTest) + } + providerN := len(providers) + publicProviderN := createN + + t.Run("returns all providers", func(t *testing.T) { + returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{ + ListOptions: ListOptions{ + PageNumber: 0, + PageSize: providerN, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, returnedProviders.Items) + assert.Equal(t, providerN, returnedProviders.TotalCount) + assert.Equal(t, 1, returnedProviders.TotalPages) + for _, rp := range returnedProviders.Items { + foundProvider := false + for _, p := range providers { + if rp.ID == p.ID { + foundProvider = true + break + } + } + assert.True(t, foundProvider, "Expected to find provider %s but did not:\nexpected:\n%v\nreturned\n%v", rp.ID, providers, returnedProviders) + } + }) + + t.Run("returns pages", func(t *testing.T) { + pageN := 2 + pageSize := providerN / pageN + + for page := 0; page < pageN; page++ { + testName := fmt.Sprintf("returns page %d of providers", page) + t.Run(testName, func(t *testing.T) { + returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{ + ListOptions: ListOptions{ + PageNumber: page, + PageSize: pageSize, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, returnedProviders.Items) + assert.Equal(t, providerN, returnedProviders.TotalCount) + assert.Equal(t, pageN, returnedProviders.TotalPages) + assert.Equal(t, pageSize, len(returnedProviders.Items)) + for _, rp := range returnedProviders.Items { + foundProvider := false + for _, p := range providers { + if rp.ID == p.ID { + foundProvider = true + break + } + } + assert.True(t, foundProvider, "Expected to find provider %s but did not:\nexpected:\n%v\nreturned\n%v", rp.ID, providers, returnedProviders) + } + }) + } + }) + + t.Run("filters on registry name", func(t *testing.T) { + publicName := PublicRegistry + returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{ + RegistryName: &publicName, + ListOptions: ListOptions{ + PageNumber: 0, + PageSize: providerN, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, returnedProviders.Items) + assert.Equal(t, publicProviderN, returnedProviders.TotalCount) + assert.Equal(t, 1, returnedProviders.TotalPages) + for _, rp := range returnedProviders.Items { + foundProvider := false + for _, p := range providers { + if rp.ID == p.ID { + foundProvider = true + break + } + } + assert.Equal(t, publicName, rp.RegistryName) + assert.True(t, foundProvider, "Expected to find provider %s but did not:\nexpected:\n%v\nreturned\n%v", rp.ID, providers, returnedProviders) + } + }) + + t.Run("searches", func(t *testing.T) { + expectedProvider := providers[0] + returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{ + Search: &expectedProvider.Name, + ListOptions: ListOptions{ + PageNumber: 0, + PageSize: providerN, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, returnedProviders.Items) + assert.Equal(t, 1, returnedProviders.TotalCount) + assert.Equal(t, 1, returnedProviders.TotalPages) + foundProvider := returnedProviders.Items[0] + + assert.Equal(t, foundProvider.ID, expectedProvider.ID, "Expected to find provider %s but did not:\nexpected:\n%v\nreturned\n%v", expectedProvider.ID, providers, returnedProviders) + }) + }) + + t.Run("without providers", func(t *testing.T) { + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + providers, err := client.RegistryProviders.List(ctx, orgTest.Name, nil) + require.NoError(t, err) + assert.Empty(t, providers.Items) + assert.Equal(t, 0, providers.TotalCount) + assert.Equal(t, 0, providers.TotalPages) + }) +} + +func TestRegistryProvidersCreate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + publicName := PublicRegistry + privateName := PrivateRegistry + + t.Run("with valid options", func(t *testing.T) { + + publicProviderOptions := RegistryProviderCreateOptions{ + Name: String("provider_name"), + Namespace: String("public_namespace"), + RegistryName: &publicName, + } + privateProviderOptions := RegistryProviderCreateOptions{ + Name: String("provider_name"), + Namespace: &orgTest.Name, + RegistryName: &privateName, + } + + registryOptions := []RegistryProviderCreateOptions{publicProviderOptions, privateProviderOptions} + + for _, options := range registryOptions { + testName := fmt.Sprintf("with %s provider", *options.RegistryName) + t.Run(testName, func(t *testing.T) { + prv, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + require.NoError(t, err) + assert.NotEmpty(t, prv.ID) + assert.Equal(t, *options.Name, prv.Name) + assert.Equal(t, *options.Namespace, prv.Namespace) + assert.Equal(t, *options.RegistryName, prv.RegistryName) + + t.Run("permissions are properly decoded", func(t *testing.T) { + assert.True(t, prv.Permissions.CanDelete) + }) + + t.Run("relationships are properly decoded", func(t *testing.T) { + assert.Equal(t, orgTest.Name, prv.Organization.Name) + }) + + t.Run("timestamps are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, prv.CreatedAt) + assert.NotEmpty(t, prv.UpdatedAt) + }) + }) + } + }) + + t.Run("with invalid options", func(t *testing.T) { + t.Run("without a name", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Namespace: String("namespace"), + RegistryName: &publicName, + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrRequiredName.Error()) + }) + + t.Run("with an invalid name", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: String("invalid name"), + Namespace: String("namespace"), + RegistryName: &publicName, + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidName.Error()) + }) + + t.Run("without a namespace", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: String("name"), + RegistryName: &publicName, + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, "namespace is required") + }) + + t.Run("with an invalid namespace", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: String("name"), + Namespace: String("invalid namespace"), + RegistryName: &publicName, + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, "invalid value for namespace") + }) + + t.Run("without a registry-name", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: String("name"), + Namespace: String("namespace"), + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, "registry-name is required") + }) + }) + + t.Run("without a valid organization", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: String("name"), + Namespace: String("namespace"), + RegistryName: &publicName, + } + rm, err := client.RegistryProviders.Create(ctx, badIdentifier, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) + + t.Run("without a matching namespace organization.name for private registry", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: String("name"), + Namespace: String("namespace"), + RegistryName: &privateName, + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, "namespace must match organization name for private providers") + }) +} + +func TestRegistryProvidersRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + type ProviderContext struct { + ProviderCreator func(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) + RegistryName RegistryName + } + + providerContexts := []ProviderContext{ + { + ProviderCreator: createPublicRegistryProvider, + RegistryName: PublicRegistry, + }, + { + ProviderCreator: createPrivateRegistryProvider, + RegistryName: PrivateRegistry, + }, + } + + for _, prvCtx := range providerContexts { + testName := fmt.Sprintf("with %s provider", prvCtx.RegistryName) + t.Run(testName, func(t *testing.T) { + t.Run("with valid provider", func(t *testing.T) { + registryProviderTest, providerTestCleanup := prvCtx.ProviderCreator(t, client, orgTest) + defer providerTestCleanup() + + prv, err := client.RegistryProviders.Read(ctx, orgTest.Name, registryProviderTest.RegistryName, registryProviderTest.Namespace, registryProviderTest.Name, nil) + assert.NoError(t, err) + assert.NotEmpty(t, prv.ID) + assert.Equal(t, registryProviderTest.Name, prv.Name) + assert.Equal(t, registryProviderTest.Namespace, prv.Namespace) + assert.Equal(t, registryProviderTest.RegistryName, prv.RegistryName) + + t.Run("permissions are properly decoded", func(t *testing.T) { + assert.True(t, prv.Permissions.CanDelete) + }) + + t.Run("relationships are properly decoded", func(t *testing.T) { + assert.Equal(t, orgTest.Name, prv.Organization.Name) + }) + + t.Run("timestamps are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, prv.CreatedAt) + assert.NotEmpty(t, prv.UpdatedAt) + }) + }) + + t.Run("when the registry provider does not exist", func(t *testing.T) { + _, err := client.RegistryProviders.Read(ctx, orgTest.Name, prvCtx.RegistryName, "nonexistent", "nonexistent", nil) + assert.Error(t, err) + // Local TFC/E will return a forbidden here when TFC/E is in development mode + // In non development mode this returns a 404 + assert.Equal(t, ErrResourceNotFound, err) + }) + + t.Run("without a name", func(t *testing.T) { + _, err := client.RegistryProviders.Read(ctx, orgTest.Name, prvCtx.RegistryName, "namespace", "", nil) + assert.EqualError(t, err, ErrRequiredName.Error()) + }) + + t.Run("with an invalid name", func(t *testing.T) { + _, err := client.RegistryProviders.Read(ctx, orgTest.Name, prvCtx.RegistryName, "namespace", badIdentifier, nil) + assert.EqualError(t, err, ErrInvalidName.Error()) + }) + + t.Run("without a namespace", func(t *testing.T) { + _, err := client.RegistryProviders.Read(ctx, orgTest.Name, prvCtx.RegistryName, "", "name", nil) + assert.EqualError(t, err, "namespace is required") + }) + + t.Run("with an invalid namespace", func(t *testing.T) { + _, err := client.RegistryProviders.Read(ctx, orgTest.Name, prvCtx.RegistryName, badIdentifier, "name", nil) + assert.EqualError(t, err, "invalid value for namespace") + }) + + t.Run("without a registry-name", func(t *testing.T) { + _, err := client.RegistryProviders.Read(ctx, orgTest.Name, "", "namespace", "name", nil) + assert.EqualError(t, err, "registry-name is required") + }) + + t.Run("without a valid organization", func(t *testing.T) { + _, err := client.RegistryProviders.Read(ctx, badIdentifier, prvCtx.RegistryName, "namespace", "name", nil) + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) + }) + } +} + +func TestRegistryProvidersDelete(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + type ProviderContext struct { + ProviderCreator func(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) + RegistryName RegistryName + } + + providerContexts := []ProviderContext{ + { + ProviderCreator: createPublicRegistryProvider, + RegistryName: PublicRegistry, + }, + { + ProviderCreator: createPrivateRegistryProvider, + RegistryName: PrivateRegistry, + }, + } + + for _, prvCtx := range providerContexts { + testName := fmt.Sprintf("with %s provider", prvCtx.RegistryName) + t.Run(testName, func(t *testing.T) { + t.Run("with valid provider", func(t *testing.T) { + registryProviderTest, _ := prvCtx.ProviderCreator(t, client, orgTest) + + err := client.RegistryProviders.Delete(ctx, orgTest.Name, registryProviderTest.RegistryName, registryProviderTest.Namespace, registryProviderTest.Name) + require.NoError(t, err) + + prv, err := client.RegistryProviders.Read(ctx, orgTest.Name, registryProviderTest.RegistryName, registryProviderTest.Namespace, registryProviderTest.Name, nil) + assert.Nil(t, prv) + assert.Error(t, err) + }) + + t.Run("when the registry provider does not exist", func(t *testing.T) { + err := client.RegistryProviders.Delete(ctx, orgTest.Name, prvCtx.RegistryName, "nonexistent", "nonexistent") + assert.Error(t, err) + // Local TFC/E will return a forbidden here when TFC/E is in development mode + // In non development mode this returns a 404 + assert.Equal(t, ErrResourceNotFound, err) + }) + + t.Run("without a name", func(t *testing.T) { + err := client.RegistryProviders.Delete(ctx, orgTest.Name, prvCtx.RegistryName, "namespace", "") + assert.EqualError(t, err, ErrRequiredName.Error()) + }) + + t.Run("with an invalid name", func(t *testing.T) { + err := client.RegistryProviders.Delete(ctx, orgTest.Name, prvCtx.RegistryName, "namespace", badIdentifier) + assert.EqualError(t, err, ErrInvalidName.Error()) + }) + + t.Run("without a namespace", func(t *testing.T) { + err := client.RegistryProviders.Delete(ctx, orgTest.Name, prvCtx.RegistryName, "", "name") + assert.EqualError(t, err, "namespace is required") + }) + + t.Run("with an invalid namespace", func(t *testing.T) { + err := client.RegistryProviders.Delete(ctx, orgTest.Name, prvCtx.RegistryName, badIdentifier, "name") + assert.EqualError(t, err, "invalid value for namespace") + }) + + t.Run("without a registry-name", func(t *testing.T) { + err := client.RegistryProviders.Delete(ctx, orgTest.Name, "", "namespace", "name") + assert.EqualError(t, err, "registry-name is required") + }) + + t.Run("without a valid organization", func(t *testing.T) { + err := client.RegistryProviders.Delete(ctx, badIdentifier, prvCtx.RegistryName, "namespace", "name") + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) + }) + } +} diff --git a/registry_provider_platform.go b/registry_provider_platform.go new file mode 100644 index 000000000..4f6db5f6f --- /dev/null +++ b/registry_provider_platform.go @@ -0,0 +1,16 @@ +package tfe + +// RegistryProviderPlatform represents a registry provider platform +type RegistryProviderPlatform struct { + ID string `jsonapi:"primary,registry-provider-platforms"` + Os string `jsonapi:"attr,os"` + Arch string `jsonapi:"attr,arch"` + Filename string `jsonapi:"attr,filename"` + SHASUM string `jsonapi:"attr,shasum"` + + // Relations + RegistryProviderVersion *RegistryProviderVersion `jsonapi:"relation,registry-provider-version"` + + // Links + Links map[string]interface{} `jsonapi:"links,omitempty"` +} diff --git a/registry_provider_version.go b/registry_provider_version.go new file mode 100644 index 000000000..a7cf8311f --- /dev/null +++ b/registry_provider_version.go @@ -0,0 +1,16 @@ +package tfe + +// RegistryProviderVersion represents a registry provider version +type RegistryProviderVersion struct { + ID string `jsonapi:"primary,registry-provider-versions"` + Version string `jsonapi:"attr,version"` + KeyID string `jsonapi:"attr,key-id"` + Protocols []string `jsonapi:"attr,protocols,omitempty"` + + // Relations + RegistryProvider *RegistryProvider `jsonapi:"relation,registry-provider"` + RegistryProviderPlatforms []RegistryProviderPlatform `jsonapi:"relation,registry-provider-platform"` + + // Links + Links map[string]interface{} `jsonapi:"links,omitempty"` +} diff --git a/tfe.go b/tfe.go index 22512c0cf..d8c5e72c3 100644 --- a/tfe.go +++ b/tfe.go @@ -128,6 +128,7 @@ type Client struct { PolicySetVersions PolicySetVersions PolicySets PolicySets RegistryModules RegistryModules + RegistryProviders RegistryProviders Runs Runs RunTasks RunTasks RunTriggers RunTriggers @@ -272,6 +273,7 @@ func NewClient(cfg *Config) (*Client, error) { client.PolicySetVersions = &policySetVersions{client: client} client.PolicySets = &policySets{client: client} client.RegistryModules = ®istryModules{client: client} + client.RegistryProviders = ®istryProviders{client: client} client.Runs = &runs{client: client} client.RunTasks = &runTasks{client: client} client.RunTriggers = &runTriggers{client: client} From ece36e42db3c244200645ce878164b95d2895e39 Mon Sep 17 00:00:00 2001 From: Anna Winkler <3526523+annawinkler@users.noreply.github.com> Date: Mon, 23 May 2022 14:00:53 -0600 Subject: [PATCH 2/4] Refactor test method into one --- helper_test.go | 43 +++++---------------------- registry_provider_integration_test.go | 14 +++------ 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/helper_test.go b/helper_test.go index 3ac4cd9fb..cf73c9ab0 100644 --- a/helper_test.go +++ b/helper_test.go @@ -755,7 +755,7 @@ func createRunTask(t *testing.T, client *Client, org *Organization) (*RunTask, f } } -func createPrivateRegistryProvider(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) { +func createRegistryProvider(t *testing.T, client *Client, org *Organization, registryName RegistryName) (*RegistryProvider, func()) { var orgCleanup func() if org == nil { @@ -764,46 +764,19 @@ func createPrivateRegistryProvider(t *testing.T, client *Client, org *Organizati ctx := context.Background() - privateName := PrivateRegistry - - options := RegistryProviderCreateOptions{ - Name: String("tst-name-" + randomString(t)), - Namespace: &org.Name, - RegistryName: &privateName, - } - prv, err := client.RegistryProviders.Create(ctx, org.Name, options) - if err != nil { - t.Fatal(err) + if registryName != PrivateRegistry && registryName != PublicRegistry { + t.Fatal("Registry name must be either public or private") } - return prv, func() { - if err := client.RegistryProviders.Delete(ctx, org.Name, prv.RegistryName, prv.Namespace, prv.Name); err != nil { - t.Errorf("Error destroying registry provider! WARNING: Dangling resources\n"+ - "may exist! The full error is shown below.\n\n"+ - "Registry Provider: %s/%s\nError: %s", prv.Namespace, prv.Name, err) - } - - if orgCleanup != nil { - orgCleanup() - } + namespaceName := String("tst-namespace-" + randomString(t)) + if registryName == PrivateRegistry { + namespaceName = &org.Name } -} - -func createPublicRegistryProvider(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) { - var orgCleanup func() - - if org == nil { - org, orgCleanup = createOrganization(t, client) - } - - ctx := context.Background() - - publicName := PublicRegistry options := RegistryProviderCreateOptions{ Name: String("tst-name-" + randomString(t)), - Namespace: String("tst-namespace-" + randomString(t)), - RegistryName: &publicName, + Namespace: namespaceName, + RegistryName: ®istryName, } prv, err := client.RegistryProviders.Create(ctx, org.Name, options) if err != nil { diff --git a/registry_provider_integration_test.go b/registry_provider_integration_test.go index 9f26bda28..bec988657 100644 --- a/registry_provider_integration_test.go +++ b/registry_provider_integration_test.go @@ -21,11 +21,11 @@ func TestRegistryProvidersList(t *testing.T) { providers := make([]*RegistryProvider, 0) // these providers will be destroyed when the org is cleaned up for i := 0; i < createN; i++ { - providerTest, _ := createPublicRegistryProvider(t, client, orgTest) + providerTest, _ := createRegistryProvider(t, client, orgTest, PublicRegistry) providers = append(providers, providerTest) } for i := 0; i < createN; i++ { - providerTest, _ := createPrivateRegistryProvider(t, client, orgTest) + providerTest, _ := createRegistryProvider(t, client, orgTest, PrivateRegistry) providers = append(providers, providerTest) } providerN := len(providers) @@ -279,17 +279,14 @@ func TestRegistryProvidersRead(t *testing.T) { defer orgTestCleanup() type ProviderContext struct { - ProviderCreator func(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) RegistryName RegistryName } providerContexts := []ProviderContext{ { - ProviderCreator: createPublicRegistryProvider, RegistryName: PublicRegistry, }, { - ProviderCreator: createPrivateRegistryProvider, RegistryName: PrivateRegistry, }, } @@ -298,7 +295,7 @@ func TestRegistryProvidersRead(t *testing.T) { testName := fmt.Sprintf("with %s provider", prvCtx.RegistryName) t.Run(testName, func(t *testing.T) { t.Run("with valid provider", func(t *testing.T) { - registryProviderTest, providerTestCleanup := prvCtx.ProviderCreator(t, client, orgTest) + registryProviderTest, providerTestCleanup := createRegistryProvider(t, client, orgTest, prvCtx.RegistryName) defer providerTestCleanup() prv, err := client.RegistryProviders.Read(ctx, orgTest.Name, registryProviderTest.RegistryName, registryProviderTest.Namespace, registryProviderTest.Name, nil) @@ -371,17 +368,14 @@ func TestRegistryProvidersDelete(t *testing.T) { defer orgTestCleanup() type ProviderContext struct { - ProviderCreator func(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) RegistryName RegistryName } providerContexts := []ProviderContext{ { - ProviderCreator: createPublicRegistryProvider, RegistryName: PublicRegistry, }, { - ProviderCreator: createPrivateRegistryProvider, RegistryName: PrivateRegistry, }, } @@ -390,7 +384,7 @@ func TestRegistryProvidersDelete(t *testing.T) { testName := fmt.Sprintf("with %s provider", prvCtx.RegistryName) t.Run(testName, func(t *testing.T) { t.Run("with valid provider", func(t *testing.T) { - registryProviderTest, _ := prvCtx.ProviderCreator(t, client, orgTest) + registryProviderTest, _ := createRegistryProvider(t, client, orgTest, prvCtx.RegistryName) err := client.RegistryProviders.Delete(ctx, orgTest.Name, registryProviderTest.RegistryName, registryProviderTest.Namespace, registryProviderTest.Name) require.NoError(t, err) From 88b4b9460f2da3d078b40bd16b47e13d8b0e9879 Mon Sep 17 00:00:00 2001 From: Anna Winkler <3526523+annawinkler@users.noreply.github.com> Date: Mon, 23 May 2022 14:35:16 -0600 Subject: [PATCH 3/4] RegistryProviderPermissions does not need to be a pointer --- registry_provider.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/registry_provider.go b/registry_provider.go index e02e94956..07b376c0a 100644 --- a/registry_provider.go +++ b/registry_provider.go @@ -44,13 +44,13 @@ const ( // RegistryProvider represents a registry provider type RegistryProvider struct { - ID string `jsonapi:"primary,registry-providers"` - Namespace string `jsonapi:"attr,namespace"` - Name string `jsonapi:"attr,name"` - RegistryName RegistryName `jsonapi:"attr,registry-name"` - Permissions *RegistryProviderPermissions `jsonapi:"attr,permissions"` - CreatedAt string `jsonapi:"attr,created-at"` - UpdatedAt string `jsonapi:"attr,updated-at"` + ID string `jsonapi:"primary,registry-providers"` + Namespace string `jsonapi:"attr,namespace"` + Name string `jsonapi:"attr,name"` + RegistryName RegistryName `jsonapi:"attr,registry-name"` + Permissions RegistryProviderPermissions `jsonapi:"attr,permissions"` + CreatedAt string `jsonapi:"attr,created-at"` + UpdatedAt string `jsonapi:"attr,updated-at"` // Relations Organization *Organization `jsonapi:"relation,organization"` From 8c726838530ad92a422d467a63acca533f04979b Mon Sep 17 00:00:00 2001 From: Anna Winkler <3526523+annawinkler@users.noreply.github.com> Date: Mon, 23 May 2022 14:37:29 -0600 Subject: [PATCH 4/4] Remove error that the API handles --- registry_provider.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/registry_provider.go b/registry_provider.go index 07b376c0a..9fa56cd2d 100644 --- a/registry_provider.go +++ b/registry_provider.go @@ -145,11 +145,6 @@ func (r *registryProviders) Create(ctx context.Context, organization string, opt if err := options.valid(); err != nil { return nil, err } - // Private providers must match their namespace and organization name - // This is enforced by the API as well - if *options.RegistryName == PrivateRegistry && organization != *options.Namespace { - return nil, errors.New("namespace must match organization name for private providers") - } u := fmt.Sprintf( "organizations/%s/registry-providers",