From b8f631d89d0b242493729506f13f46c11bc7265d Mon Sep 17 00:00:00 2001 From: Karl Kirch Date: Tue, 15 Feb 2022 10:51:28 -0600 Subject: [PATCH 1/3] 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 3844ab98c..1940bb8db 100644 --- a/helper_test.go +++ b/helper_test.go @@ -744,23 +744,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) } @@ -778,6 +779,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 065c2d37f..bbc0e7f8d 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 f66ba384fa050a77cef22abc6851a01eea49d433 Mon Sep 17 00:00:00 2001 From: Karl Kirch Date: Tue, 15 Feb 2022 17:17:21 -0600 Subject: [PATCH 2/3] Add registry provider version --- errors.go | 12 + generate_mocks.sh | 2 + helper_test.go | 94 +++- registry_provider.go | 199 +++++---- registry_provider_integration_test.go | 333 +++++++++----- registry_provider_version.go | 252 ++++++++++- registry_provider_version_integration_test.go | 405 ++++++++++++++++++ tfe.go | 2 + 8 files changed, 1097 insertions(+), 202 deletions(-) create mode 100644 registry_provider_version_integration_test.go diff --git a/errors.go b/errors.go index b9744d2fa..2491867b0 100644 --- a/errors.go +++ b/errors.go @@ -155,6 +155,16 @@ var ( ErrInvalidCommentID = errors.New("invalid value for comment ID") ErrInvalidCommentBody = errors.New("invalid value for comment body") + + ErrInvalidNamespace = errors.New("invalid value for namespace") + + ErrInvalidPrivateProviderNamespaceDoesntMatchOrganization = errors.New("invalid namespace must match organization name for private providers") + + ErrInvalidRegistryName = errors.New("invalid value for registry-name") + + ErrInvalidRegistryNameType = errors.New("invalid type for registry-name. Please use 'RegistryName'") + + ErrInvalidKeyID = errors.New("invalid value for key-id") ) // Missing values for required field/option @@ -268,4 +278,6 @@ var ( ErrEmptyTeamName = errors.New("team name can not be empty") ErrInvalidEmail = errors.New("email is invalid") + + ErrRequiredPrivateRegistry = errors.New("only private registry is allowed") ) diff --git a/generate_mocks.sh b/generate_mocks.sh index df36385f3..af9a0b20f 100755 --- a/generate_mocks.sh +++ b/generate_mocks.sh @@ -35,6 +35,8 @@ mockgen -source=policy_set.go -destination=mocks/policy_set_mocks.go -package=mo mockgen -source=policy_set_parameter.go -destination=mocks/policy_set_parameter_mocks.go -package=mocks mockgen -source=policy_set_version.go -destination=mocks/policy_set_version_mocks.go -package=mocks mockgen -source=registry_module.go -destination=mocks/registry_module_mocks.go -package=mocks +mockgen -source=registry_provider.go -destination=mocks/registry_provider_mocks.go -package=mocks +mockgen -source=registry_provider_version.go -destination=mocks/registry_provider_version_mocks.go -package=mocks mockgen -source=run.go -destination=mocks/run_mocks.go -package=mocks mockgen -source=run_task.go -destination=mocks/run_tasks.go -package=mocks mockgen -source=run_trigger.go -destination=mocks/run_trigger_mocks.go -package=mocks diff --git a/helper_test.go b/helper_test.go index 1940bb8db..94ad13f37 100644 --- a/helper_test.go +++ b/helper_test.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io/ioutil" + "math/rand" "net/url" "os" "sync" @@ -788,20 +789,29 @@ 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, + Name: "tst-name-" + randomString(t), + Namespace: org.Name, + RegistryName: PrivateRegistry, } + prv, err := client.RegistryProviders.Create(ctx, org.Name, options) + if err != nil { t.Fatal(err) } + prv.Organization = org + return prv, func() { - if err := client.RegistryProviders.Delete(ctx, org.Name, prv.RegistryName, prv.Namespace, prv.Name); err != nil { + id := RegistryProviderID{ + OrganizationName: org.Name, + RegistryName: prv.RegistryName, + Namespace: prv.Namespace, + Name: prv.Name, + } + + if err := client.RegistryProviders.Delete(ctx, id); 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) @@ -822,20 +832,29 @@ func createPublicRegistryProvider(t *testing.T, client *Client, org *Organizatio ctx := context.Background() - publicName := PublicRegistry - options := RegistryProviderCreateOptions{ - Name: String("tst-name-" + randomString(t)), - Namespace: String("tst-namespace-" + randomString(t)), - RegistryName: &publicName, + Name: "tst-name-" + randomString(t), + Namespace: "tst-namespace-" + randomString(t), + RegistryName: PublicRegistry, } + prv, err := client.RegistryProviders.Create(ctx, org.Name, options) + if err != nil { t.Fatal(err) } + prv.Organization = org + return prv, func() { - if err := client.RegistryProviders.Delete(ctx, org.Name, prv.RegistryName, prv.Namespace, prv.Name); err != nil { + id := RegistryProviderID{ + OrganizationName: org.Name, + RegistryName: prv.RegistryName, + Namespace: prv.Namespace, + Name: prv.Name, + } + + if err := client.RegistryProviders.Delete(ctx, id); 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) @@ -847,6 +866,53 @@ func createPublicRegistryProvider(t *testing.T, client *Client, org *Organizatio } } +func createRegistryProviderVersion(t *testing.T, client *Client, provider *RegistryProvider) (*RegistryProviderVersion, func()) { + var providerCleanup func() + + if provider == nil { + provider, providerCleanup = createPrivateRegistryProvider(t, client, nil) + } + + providerID := RegistryProviderID{ + OrganizationName: provider.Organization.Name, + RegistryName: provider.RegistryName, + Namespace: provider.Namespace, + Name: provider.Name, + } + + ctx := context.Background() + + options := RegistryProviderVersionCreateOptions{ + Version: randomSemver(t), + KeyID: randomString(t), + } + + prvv, err := client.RegistryProviderVersions.Create(ctx, providerID, options) + + if err != nil { + t.Fatal(err) + } + + prvv.RegistryProvider = provider + + return prvv, func() { + id := RegistryProviderVersionID{ + Version: options.Version, + RegistryProviderID: providerID, + } + + if err := client.RegistryProviderVersions.Delete(ctx, id); err != nil { + t.Errorf("Error destroying registry provider version! WARNING: Dangling resources\n"+ + "may exist! The full error is shown below.\n\n"+ + "Registry Provider Version: %s/%s/%s\nError: %s", prvv.RegistryProvider.Namespace, prvv.RegistryProvider.Name, prvv.Version, err) + } + + if providerCleanup != nil { + providerCleanup() + } + } +} + func createSSHKey(t *testing.T, client *Client, org *Organization) (*SSHKey, func()) { var orgCleanup func() @@ -1413,6 +1479,10 @@ func randomString(t *testing.T) string { return v } +func randomSemver(t *testing.T) string { + return fmt.Sprintf("%d.%d.%d", rand.Intn(99)+3, rand.Intn(99)+1, rand.Intn(99)+1) +} + // skips a test if the environment is for Terraform Cloud. func skipIfCloud(t *testing.T) { if !enterpriseEnabled() { diff --git a/registry_provider.go b/registry_provider.go index e02e94956..83d2d8964 100644 --- a/registry_provider.go +++ b/registry_provider.go @@ -2,7 +2,6 @@ package tfe import ( "context" - "errors" "fmt" "net/url" ) @@ -10,7 +9,7 @@ import ( // Compile-time proof of interface implementation. var _ RegistryProviders = (*registryProviders)(nil) -// RegistryProviders describes all the registry provider related methods that the Terraform +// 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 @@ -18,14 +17,14 @@ 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 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) + // Read a registry provider. + Read(ctx context.Context, providerID RegistryProviderID, options *RegistryProviderReadOptions) (*RegistryProvider, error) - // Delete a registry provider - Delete(ctx context.Context, organization string, registryName RegistryName, namespace string, name string) error + // Delete a registry provider. + Delete(ctx context.Context, providerID RegistryProviderID) error } // registryProviders implements RegistryProviders. @@ -42,33 +41,51 @@ const ( PublicRegistry RegistryName = "public" ) +// RegistryProviderIncludeOps represents which jsonapi include can be used with registry providers +type RegistryProviderIncludeOps string + +// List of available includes +const ( + RegistryProviderVersionsInclude RegistryProviderIncludeOps = "registry-provider-versions" +) + // 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"` + Namespace string `jsonapi:"attr,namespace"` CreatedAt string `jsonapi:"attr,created-at"` UpdatedAt string `jsonapi:"attr,updated-at"` + RegistryName RegistryName `jsonapi:"attr,registry-name"` + Permissions *RegistryProviderPermissions `jsonapi:"attr,permissions"` // Relations - Organization *Organization `jsonapi:"relation,organization"` - RegistryProviderVersions []RegistryProviderVersion `jsonapi:"relation,registry-provider-version"` + Organization *Organization `jsonapi:"relation,organization"` + RegistryProviderVersions []*RegistryProviderVersion `jsonapi:"relation,registry-provider-versions"` + + // Links + Links map[string]interface{} `jsonapi:"links,omitempty"` } type RegistryProviderPermissions struct { - CanDelete bool `jsonapi:"attr,can-delete"` + CanDelete bool `jsonapi:"attr,can-delete,omitempty"` + CanUploadAsset bool `jsonapi:"attr,can-upload-asset,omitempty"` } type RegistryProviderListOptions struct { ListOptions + // A query string to filter by registry_name - RegistryName *RegistryName `url:"filter[registry_name],omitempty"` + RegistryName RegistryName `url:"filter[registry_name],omitempty"` + // A query string to filter by organization - OrganizationName *string `url:"filter[organization_name],omitempty"` + OrganizationName string `url:"filter[organization_name],omitempty"` + // A query string to do a fuzzy search - Search *string `url:"q,omitempty"` + Search string `url:"q,omitempty"` + + // Include related jsonapi relationships + Include *[]RegistryProviderIncludeOps `url:"include,omitempty"` } type RegistryProviderList struct { @@ -76,12 +93,20 @@ type RegistryProviderList struct { Items []*RegistryProvider } -func (o RegistryProviderListOptions) valid() error { - return nil +// RegistryProviderID is the multi key ID for addressing a provider +type RegistryProviderID struct { + OrganizationName string `jsonapi:"attr,organization-name"` + RegistryName RegistryName `jsonapi:"attr,registry-name"` + Namespace string `jsonapi:"attr,namespace"` + Name string `jsonapi:"attr,name"` } -func (r *registryProviders) List(ctx context.Context, organization string, options *RegistryProviderListOptions) (*RegistryProviderList, error) { +type RegistryProviderReadOptions struct { + // Include related jsonapi relationships + Include *[]RegistryProviderIncludeOps `url:"include,omitempty"` +} +func (r *registryProviders) List(ctx context.Context, organization string, options *RegistryProviderListOptions) (*RegistryProviderList, error) { if !validStringID(&organization) { return nil, ErrInvalidOrg } @@ -114,41 +139,24 @@ type RegistryProviderCreateOptions struct { // 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 + Name string `jsonapi:"attr,name"` + Namespace string `jsonapi:"attr,namespace"` + RegistryName RegistryName `jsonapi:"attr,registry-name"` } 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 + + // For private providers, the organization name and namespace must be the same. // 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") + if options.RegistryName == PrivateRegistry && organization != options.Namespace { + return nil, ErrInvalidPrivateProviderNamespaceDoesntMatchOrganization } u := fmt.Sprintf( @@ -159,6 +167,7 @@ func (r *registryProviders) Create(ctx context.Context, organization string, opt if err != nil { return nil, err } + prv := &RegistryProvider{} err = r.client.do(ctx, req, prv) if err != nil { @@ -168,35 +177,17 @@ func (r *registryProviders) Create(ctx context.Context, organization string, opt 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") +func (r *registryProviders) Read(ctx context.Context, providerID RegistryProviderID, options *RegistryProviderReadOptions) (*RegistryProvider, error) { + if err := providerID.valid(); err != nil { + return nil, err } u := fmt.Sprintf( "organizations/%s/registry-providers/%s/%s/%s", - url.QueryEscape(organization), - url.QueryEscape(string(registryName)), - url.QueryEscape(namespace), - url.QueryEscape(name), + url.QueryEscape(providerID.OrganizationName), + url.QueryEscape(string(providerID.RegistryName)), + url.QueryEscape(providerID.Namespace), + url.QueryEscape(providerID.Name), ) req, err := r.client.newRequest("GET", u, options) if err != nil { @@ -212,32 +203,17 @@ func (r *registryProviders) Read(ctx context.Context, organization string, regis 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") +func (r *registryProviders) Delete(ctx context.Context, providerID RegistryProviderID) error { + if err := providerID.valid(); err != nil { + return err } u := fmt.Sprintf( "organizations/%s/registry-providers/%s/%s/%s", - url.QueryEscape(organization), - url.QueryEscape(string(registryName)), - url.QueryEscape(namespace), - url.QueryEscape(name), + url.QueryEscape(providerID.OrganizationName), + url.QueryEscape(string(providerID.RegistryName)), + url.QueryEscape(providerID.Namespace), + url.QueryEscape(providerID.Name), ) req, err := r.client.newRequest("DELETE", u, nil) if err != nil { @@ -246,3 +222,44 @@ func (r *registryProviders) Delete(ctx context.Context, organization string, reg return r.client.do(ctx, req, nil) } + +func (rn RegistryName) valid() error { + switch rn { + case PrivateRegistry, PublicRegistry: + return nil + } + return ErrInvalidRegistryName +} + +func (o RegistryProviderCreateOptions) valid() error { + if !validStringID(&o.Name) { + return ErrInvalidName + } + if !validStringID(&o.Namespace) { + return ErrInvalidNamespace + } + if err := o.RegistryName.valid(); err != nil { + return err + } + return nil +} + +func (id RegistryProviderID) valid() error { + if !validStringID(&id.OrganizationName) { + return ErrInvalidOrg + } + if !validStringID(&id.Name) { + return ErrInvalidName + } + if !validStringID(&id.Namespace) { + return ErrInvalidNamespace + } + if err := id.RegistryName.valid(); err != nil { + return err + } + return nil +} + +func (o RegistryProviderListOptions) valid() error { + return nil +} diff --git a/registry_provider_integration_test.go b/registry_provider_integration_test.go index 9f26bda28..6b87b8e44 100644 --- a/registry_provider_integration_test.go +++ b/registry_provider_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + package tfe import ( @@ -21,10 +24,12 @@ 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++ { + // Create public providers providerTest, _ := createPublicRegistryProvider(t, client, orgTest) providers = append(providers, providerTest) } for i := 0; i < createN; i++ { + // Create private providers providerTest, _ := createPrivateRegistryProvider(t, client, orgTest) providers = append(providers, providerTest) } @@ -87,9 +92,8 @@ func TestRegistryProvidersList(t *testing.T) { }) t.Run("filters on registry name", func(t *testing.T) { - publicName := PublicRegistry returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{ - RegistryName: &publicName, + RegistryName: PublicRegistry, ListOptions: ListOptions{ PageNumber: 0, PageSize: providerN, @@ -107,7 +111,7 @@ func TestRegistryProvidersList(t *testing.T) { break } } - assert.Equal(t, publicName, rp.RegistryName) + assert.Equal(t, PublicRegistry, rp.RegistryName) assert.True(t, foundProvider, "Expected to find provider %s but did not:\nexpected:\n%v\nreturned\n%v", rp.ID, providers, returnedProviders) } }) @@ -115,7 +119,7 @@ func TestRegistryProvidersList(t *testing.T) { t.Run("searches", func(t *testing.T) { expectedProvider := providers[0] returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{ - Search: &expectedProvider.Name, + Search: expectedProvider.Name, ListOptions: ListOptions{ PageNumber: 0, PageSize: providerN, @@ -141,6 +145,41 @@ func TestRegistryProvidersList(t *testing.T) { assert.Equal(t, 0, providers.TotalCount) assert.Equal(t, 0, providers.TotalPages) }) + + t.Run("with include provider versions", func(t *testing.T) { + version1, version1Cleanup := createRegistryProviderVersion(t, client, nil) + defer version1Cleanup() + + provider := version1.RegistryProvider + + version2, version2Cleanup := createRegistryProviderVersion(t, client, provider) + defer version2Cleanup() + + versions := []*RegistryProviderVersion{version1, version2} + + options := RegistryProviderListOptions{ + Include: &[]RegistryProviderIncludeOps{ + RegistryProviderVersionsInclude, + }, + } + + providersRead, err := client.RegistryProviders.List(ctx, provider.Organization.Name, &options) + assert.NoError(t, err) + providerRead := providersRead.Items[0] + assert.Equal(t, providerRead.ID, provider.ID) + assert.Equal(t, len(versions), len(providerRead.RegistryProviderVersions)) + foundVersion := &RegistryProviderVersion{} + for _, v := range providerRead.RegistryProviderVersions { + for i := 0; i < len(versions); i++ { + if v.ID == versions[i].ID { + foundVersion = versions[i] + break + } + } + assert.True(t, foundVersion.ID != "", "Expected to find versions: %v but did not", versions) + assert.Equal(t, v.Version, foundVersion.Version) + } + }) } func TestRegistryProvidersCreate(t *testing.T) { @@ -150,33 +189,30 @@ func TestRegistryProvidersCreate(t *testing.T) { 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, + Name: "provider_name", + Namespace: "public_namespace", + RegistryName: PublicRegistry, } privateProviderOptions := RegistryProviderCreateOptions{ - Name: String("provider_name"), - Namespace: &orgTest.Name, - RegistryName: &privateName, + Name: "provider_name", + Namespace: orgTest.Name, + RegistryName: PrivateRegistry, } registryOptions := []RegistryProviderCreateOptions{publicProviderOptions, privateProviderOptions} for _, options := range registryOptions { - testName := fmt.Sprintf("with %s provider", *options.RegistryName) + 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) + 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) @@ -197,19 +233,19 @@ func TestRegistryProvidersCreate(t *testing.T) { 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, + Namespace: "namespace", + RegistryName: PublicRegistry, } rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) assert.Nil(t, rm) - assert.EqualError(t, err, ErrRequiredName.Error()) + assert.EqualError(t, err, ErrInvalidName.Error()) }) t.Run("with an invalid name", func(t *testing.T) { options := RegistryProviderCreateOptions{ - Name: String("invalid name"), - Namespace: String("namespace"), - RegistryName: &publicName, + Name: "invalid name", + Namespace: "namespace", + RegistryName: PublicRegistry, } rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) assert.Nil(t, rm) @@ -218,41 +254,52 @@ func TestRegistryProvidersCreate(t *testing.T) { t.Run("without a namespace", func(t *testing.T) { options := RegistryProviderCreateOptions{ - Name: String("name"), - RegistryName: &publicName, + Name: "name", + RegistryName: PublicRegistry, } rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) assert.Nil(t, rm) - assert.EqualError(t, err, "namespace is required") + assert.EqualError(t, err, ErrInvalidNamespace.Error()) }) t.Run("with an invalid namespace", func(t *testing.T) { options := RegistryProviderCreateOptions{ - Name: String("name"), - Namespace: String("invalid namespace"), - RegistryName: &publicName, + Name: "name", + Namespace: "invalid namespace", + RegistryName: PublicRegistry, } rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) assert.Nil(t, rm) - assert.EqualError(t, err, "invalid value for namespace") + assert.EqualError(t, err, ErrInvalidNamespace.Error()) }) t.Run("without a registry-name", func(t *testing.T) { options := RegistryProviderCreateOptions{ - Name: String("name"), - Namespace: String("namespace"), + Name: "name", + Namespace: "namespace", } rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) assert.Nil(t, rm) - assert.EqualError(t, err, "registry-name is required") + assert.EqualError(t, err, ErrInvalidRegistryName.Error()) + }) + + t.Run("with an invalid registry-name", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: "name", + Namespace: "namespace", + RegistryName: "invalid", + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidRegistryName.Error()) }) }) t.Run("without a valid organization", func(t *testing.T) { options := RegistryProviderCreateOptions{ - Name: String("name"), - Namespace: String("namespace"), - RegistryName: &publicName, + Name: "name", + Namespace: "namespace", + RegistryName: PublicRegistry, } rm, err := client.RegistryProviders.Create(ctx, badIdentifier, options) assert.Nil(t, rm) @@ -261,13 +308,13 @@ func TestRegistryProvidersCreate(t *testing.T) { 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, + Name: "name", + Namespace: "namespace", + RegistryName: PrivateRegistry, } 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") + assert.EqualError(t, err, ErrInvalidPrivateProviderNamespaceDoesntMatchOrganization.Error()) }) } @@ -301,7 +348,14 @@ func TestRegistryProvidersRead(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) + id := RegistryProviderID{ + OrganizationName: orgTest.Name, + RegistryName: registryProviderTest.RegistryName, + Namespace: registryProviderTest.Namespace, + Name: registryProviderTest.Name, + } + + prv, err := client.RegistryProviders.Read(ctx, id, nil) assert.NoError(t, err) assert.NotEmpty(t, prv.ID) assert.Equal(t, registryProviderTest.Name, prv.Name) @@ -323,44 +377,61 @@ func TestRegistryProvidersRead(t *testing.T) { }) 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) + id := RegistryProviderID{ + OrganizationName: orgTest.Name, + RegistryName: prvCtx.RegistryName, + Namespace: "nonexistent", + Name: "nonexistent", + } + _, err := client.RegistryProviders.Read(ctx, id, 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("populates version relationships", func(t *testing.T) { + version1, version1Cleanup := createRegistryProviderVersion(t, client, nil) + defer version1Cleanup() - 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()) - }) + provider := version1.RegistryProvider - 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") - }) + version2, version2Cleanup := createRegistryProviderVersion(t, client, provider) + defer version2Cleanup() - 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") - }) + versions := []*RegistryProviderVersion{version1, version2} - 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") - }) + id := RegistryProviderID{ + OrganizationName: provider.Organization.Name, + RegistryName: provider.RegistryName, + Namespace: provider.Namespace, + Name: provider.Name, + } - 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()) - }) - }) - } + options := RegistryProviderReadOptions{ + Include: &[]RegistryProviderIncludeOps{ + RegistryProviderVersionsInclude, + }, + } + + providerRead, err := client.RegistryProviders.Read(ctx, id, &options) + assert.NoError(t, err) + assert.Equal(t, providerRead.ID, provider.ID) + assert.Equal(t, len(versions), len(providerRead.RegistryProviderVersions)) + foundVersion := &RegistryProviderVersion{} + for _, v := range providerRead.RegistryProviderVersions { + for i := 0; i < len(versions); i++ { + if v.ID == versions[i].ID { + foundVersion = versions[i] + break + } + } + assert.True(t, foundVersion.ID != "", "Expected to find versions: %v but did not", versions) + assert.Equal(t, v.Version, foundVersion.Version) + } + }) } func TestRegistryProvidersDelete(t *testing.T) { @@ -392,51 +463,119 @@ func TestRegistryProvidersDelete(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) + id := RegistryProviderID{ + OrganizationName: orgTest.Name, + RegistryName: registryProviderTest.RegistryName, + Namespace: registryProviderTest.Namespace, + Name: registryProviderTest.Name, + } + + err := client.RegistryProviders.Delete(ctx, id) require.NoError(t, err) - prv, err := client.RegistryProviders.Read(ctx, orgTest.Name, registryProviderTest.RegistryName, registryProviderTest.Namespace, registryProviderTest.Name, nil) + prv, err := client.RegistryProviders.Read(ctx, id, 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") + id := RegistryProviderID{ + OrganizationName: orgTest.Name, + RegistryName: prvCtx.RegistryName, + Namespace: "nonexistent", + Name: "nonexistent", + } + err := client.RegistryProviders.Delete(ctx, id) 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()) - }) +func TestRegistryProvidersIDValidation(t *testing.T) { + orgName := "orgName" + registryName := PublicRegistry - 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("valid", func(t *testing.T) { + id := RegistryProviderID{ + OrganizationName: orgName, + RegistryName: registryName, + Namespace: "namespace", + Name: "name", + } + assert.NoError(t, id.valid()) + }) - 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("without a name", func(t *testing.T) { + id := RegistryProviderID{ + OrganizationName: orgName, + RegistryName: registryName, + Namespace: "namespace", + Name: "", + } + assert.EqualError(t, id.valid(), ErrInvalidName.Error()) + }) - 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("with an invalid name", func(t *testing.T) { + id := RegistryProviderID{ + OrganizationName: orgName, + RegistryName: registryName, + Namespace: "namespace", + Name: badIdentifier, + } + assert.EqualError(t, id.valid(), ErrInvalidName.Error()) + }) - 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 namespace", func(t *testing.T) { + id := RegistryProviderID{ + OrganizationName: orgName, + RegistryName: registryName, + Namespace: "", + Name: "name", + } + assert.EqualError(t, id.valid(), ErrInvalidNamespace.Error()) + }) - 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()) - }) - }) - } + t.Run("with an invalid namespace", func(t *testing.T) { + id := RegistryProviderID{ + OrganizationName: orgName, + RegistryName: registryName, + Namespace: badIdentifier, + Name: "name", + } + assert.EqualError(t, id.valid(), ErrInvalidNamespace.Error()) + }) + + t.Run("without a registry-name", func(t *testing.T) { + id := RegistryProviderID{ + OrganizationName: orgName, + RegistryName: "", + Namespace: "namespace", + Name: "name", + } + assert.EqualError(t, id.valid(), ErrInvalidRegistryName.Error()) + }) + + t.Run("with in invalid registry-name", func(t *testing.T) { + id := RegistryProviderID{ + OrganizationName: orgName, + RegistryName: "invalid registry name", + Namespace: "namespace", + Name: "name", + } + assert.EqualError(t, id.valid(), ErrInvalidRegistryName.Error()) + }) + + t.Run("without a valid organization", func(t *testing.T) { + id := RegistryProviderID{ + OrganizationName: badIdentifier, + RegistryName: registryName, + Namespace: "namespace", + Name: "name", + } + assert.EqualError(t, id.valid(), ErrInvalidOrg.Error()) + }) } diff --git a/registry_provider_version.go b/registry_provider_version.go index a7cf8311f..a11230b24 100644 --- a/registry_provider_version.go +++ b/registry_provider_version.go @@ -1,16 +1,264 @@ package tfe +import ( + "context" + "fmt" + "net/url" +) + +// Compile-time proof of interface implementation. +var _ RegistryProviderVersions = (*registryProviderVersions)(nil) + +// RegistryProviderVersions describes the registry provider version methods that +// the Terraform Enterprise API supports. +// +// TFE API docs: https://www.terraform.io/cloud-docs/api-docs/private-registry/provider-versions-platforms +type RegistryProviderVersions interface { + // List all versions for a single provider. + List(ctx context.Context, providerID RegistryProviderID, options *RegistryProviderVersionListOptions) (*RegistryProviderVersionList, error) + + // Create a registry provider version. + Create(ctx context.Context, providerID RegistryProviderID, options RegistryProviderVersionCreateOptions) (*RegistryProviderVersion, error) + + // Read a registry provider version. + Read(ctx context.Context, versionID RegistryProviderVersionID, options *RegistryProviderVersionReadOptions) (*RegistryProviderVersion, error) + + // Delete a registry provider version. + Delete(ctx context.Context, versionID RegistryProviderVersionID) error +} + +// registryProviders implements RegistryProviders. +type registryProviderVersions struct { + client *Client +} + // RegistryProviderVersion represents a registry provider version type RegistryProviderVersion struct { ID string `jsonapi:"primary,registry-provider-versions"` Version string `jsonapi:"attr,version"` + CreatedAt string `jsonapi:"attr,created-at"` + UpdatedAt string `jsonapi:"attr,updated-at"` KeyID string `jsonapi:"attr,key-id"` Protocols []string `jsonapi:"attr,protocols,omitempty"` + Permissions *RegistryProviderPermissions `jsonapi:"attr,permissions"` + ShasumsUploaded bool `jsonapi:"attr,shasums-uploaded"` + ShasumsSigUploaded bool `jsonapi:"attr,sasums-sig-uploaded"` // Relations - RegistryProvider *RegistryProvider `jsonapi:"relation,registry-provider"` - RegistryProviderPlatforms []RegistryProviderPlatform `jsonapi:"relation,registry-provider-platform"` + RegistryProvider *RegistryProvider `jsonapi:"relation,registry-provider"` + RegistryProviderPlatforms []*RegistryProviderPlatform `jsonapi:"relation,platforms"` // Links Links map[string]interface{} `jsonapi:"links,omitempty"` } + +// RegistryProviderVersionID is the multi key ID for addressing a version provider +type RegistryProviderVersionID struct { + RegistryProviderID + Version string `jsonapi:"attr,version"` +} + +type RegistryProviderVersionList struct { + *Pagination + Items []*RegistryProviderVersion +} + +type RegistryProviderVersionListOptions struct { + ListOptions +} + +type RegistryProviderVersionReadOptions struct{} + +type RegistryProviderVersionCreateOptions struct { + Version string `jsonapi:"attr,version"` + KeyID string `jsonapi:"attr,key-id"` + Protocols []string `jsonapi:"attr,protocols"` +} + +// List registry provider versions +func (r *registryProviderVersions) List(ctx context.Context, providerID RegistryProviderID, options *RegistryProviderVersionListOptions) (*RegistryProviderVersionList, error) { + if err := providerID.valid(); err != nil { + return nil, err + } + if options != nil { + if err := options.valid(); err != nil { + return nil, err + } + } + + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s/versions", + url.QueryEscape(providerID.OrganizationName), + url.QueryEscape(string(providerID.RegistryName)), + url.QueryEscape(providerID.Namespace), + url.QueryEscape(providerID.Name), + ) + req, err := r.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + pvl := &RegistryProviderVersionList{} + err = r.client.do(ctx, req, pvl) + if err != nil { + return nil, err + } + + return pvl, nil +} + +// Create a registry provider version +func (r *registryProviderVersions) Create(ctx context.Context, providerID RegistryProviderID, options RegistryProviderVersionCreateOptions) (*RegistryProviderVersion, error) { + if err := providerID.valid(); err != nil { + return nil, err + } + + if providerID.RegistryName != PrivateRegistry { + return nil, ErrRequiredPrivateRegistry + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s/versions", + url.QueryEscape(providerID.OrganizationName), + url.QueryEscape(string(providerID.RegistryName)), + url.QueryEscape(providerID.Namespace), + url.QueryEscape(providerID.Name), + ) + req, err := r.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + + prvv := &RegistryProviderVersion{} + err = r.client.do(ctx, req, prvv) + if err != nil { + return nil, err + } + + return prvv, nil +} + +// Read a registry provider version +func (r *registryProviderVersions) Read(ctx context.Context, versionID RegistryProviderVersionID, options *RegistryProviderVersionReadOptions) (*RegistryProviderVersion, error) { + if err := versionID.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s/versions/%s", + url.QueryEscape(versionID.OrganizationName), + url.QueryEscape(string(versionID.RegistryName)), + url.QueryEscape(versionID.Namespace), + url.QueryEscape(versionID.Name), + url.QueryEscape(versionID.Version), + ) + req, err := r.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + prvv := &RegistryProviderVersion{} + err = r.client.do(ctx, req, prvv) + if err != nil { + return nil, err + } + + return prvv, nil +} + +// Delete a registry provider version +func (r *registryProviderVersions) Delete(ctx context.Context, versionID RegistryProviderVersionID) error { + if err := versionID.valid(); err != nil { + return err + } + + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s/versions/%s", + url.QueryEscape(versionID.OrganizationName), + url.QueryEscape(string(versionID.RegistryName)), + url.QueryEscape(versionID.Namespace), + url.QueryEscape(versionID.Name), + url.QueryEscape(versionID.Version), + ) + req, err := r.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return r.client.do(ctx, req, nil) +} + +func (v RegistryProviderVersion) ShasumsUploadURL() (string, error) { + uploadURL, ok := v.Links["shasums-upload"].(string) + if !ok { + return uploadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums upload link") + } + if uploadURL == "" { + return uploadURL, fmt.Errorf("the Registry Provider Version shasums upload URL is empty") + } + return uploadURL, nil +} + +func (v RegistryProviderVersion) ShasumsSigUploadURL() (string, error) { + uploadURL, ok := v.Links["shasums-sig-upload"].(string) + if !ok { + return uploadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums sig upload link") + } + if uploadURL == "" { + return uploadURL, fmt.Errorf("the Registry Provider Version shasums sig upload URL is empty") + } + return uploadURL, nil +} + +func (v RegistryProviderVersion) ShasumsDownloadURL() (string, error) { + downloadURL, ok := v.Links["shasums-download"].(string) + if !ok { + return downloadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums download link") + } + if downloadURL == "" { + return downloadURL, fmt.Errorf("the Registry Provider Version shasums download URL is empty") + } + return downloadURL, nil +} + +func (v RegistryProviderVersion) ShasumsSigDownloadURL() (string, error) { + downloadURL, ok := v.Links["shasums-sig-download"].(string) + if !ok { + return downloadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums sig download link") + } + if downloadURL == "" { + return downloadURL, fmt.Errorf("the Registry Provider Version shasums sig download URL is empty") + } + return downloadURL, nil +} + +func (id RegistryProviderVersionID) valid() error { + if !validStringID(&id.Version) { + return ErrInvalidVersion + } + if id.RegistryName != PrivateRegistry { + return ErrRequiredPrivateRegistry + } + if err := id.RegistryProviderID.valid(); err != nil { + return err + } + return nil +} + +func (o RegistryProviderVersionListOptions) valid() error { + return nil +} + +func (o RegistryProviderVersionCreateOptions) valid() error { + if !validStringID(&o.Version) { + return ErrInvalidVersion + } + if !validStringID(&o.KeyID) { + return ErrInvalidKeyID + } + return nil +} diff --git a/registry_provider_version_integration_test.go b/registry_provider_version_integration_test.go new file mode 100644 index 000000000..964a27a40 --- /dev/null +++ b/registry_provider_version_integration_test.go @@ -0,0 +1,405 @@ +//go:build integration +// +build integration + +package tfe + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRegistryProviderVersionsIDValidation(t *testing.T) { + version := "1.0.0" + validRegistryProviderId := RegistryProviderID{ + OrganizationName: "orgName", + RegistryName: PrivateRegistry, + Namespace: "namespace", + Name: "name", + } + invalidRegistryProviderId := RegistryProviderID{ + OrganizationName: badIdentifier, + RegistryName: PrivateRegistry, + Namespace: "namespace", + Name: "name", + } + publicRegistryProviderId := RegistryProviderID{ + OrganizationName: "orgName", + RegistryName: PublicRegistry, + Namespace: "namespace", + Name: "name", + } + + t.Run("valid", func(t *testing.T) { + id := RegistryProviderVersionID{ + Version: version, + RegistryProviderID: validRegistryProviderId, + } + assert.NoError(t, id.valid()) + }) + + t.Run("without a version", func(t *testing.T) { + id := RegistryProviderVersionID{ + Version: "", + RegistryProviderID: validRegistryProviderId, + } + assert.EqualError(t, id.valid(), ErrInvalidVersion.Error()) + }) + + t.Run("without a key-id", func(t *testing.T) { + id := RegistryProviderVersionID{ + Version: "", + RegistryProviderID: validRegistryProviderId, + } + assert.EqualError(t, id.valid(), ErrInvalidVersion.Error()) + }) + + t.Run("invalid version", func(t *testing.T) { + t.Skip("This is skipped as we don't actually validate version is a valid semver - the registry does this validation") + id := RegistryProviderVersionID{ + Version: "foo", + RegistryProviderID: validRegistryProviderId, + } + assert.EqualError(t, id.valid(), ErrInvalidVersion.Error()) + }) + + t.Run("invalid registry for parent provider", func(t *testing.T) { + id := RegistryProviderVersionID{ + Version: version, + RegistryProviderID: publicRegistryProviderId, + } + assert.EqualError(t, id.valid(), ErrRequiredPrivateRegistry.Error()) + }) + + t.Run("without a valid registry provider id", func(t *testing.T) { + // this is a proxy for all permutations of an invalid registry provider id + // it is assumed that validity of the registry provider id is delegated to its own valid method + id := RegistryProviderVersionID{ + Version: version, + RegistryProviderID: invalidRegistryProviderId, + } + assert.EqualError(t, id.valid(), ErrInvalidOrg.Error()) + }) +} + +func TestRegistryProviderVersionsCreate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + providerTest, providerTestCleanup := createPrivateRegistryProvider(t, client, nil) + defer providerTestCleanup() + + providerId := RegistryProviderID{ + OrganizationName: providerTest.Organization.Name, + RegistryName: providerTest.RegistryName, + Namespace: providerTest.Namespace, + Name: providerTest.Name, + } + + t.Run("with valid options", func(t *testing.T) { + options := RegistryProviderVersionCreateOptions{ + Version: "1.0.0", + KeyID: "abcdefg", + } + prvv, err := client.RegistryProviderVersions.Create(ctx, providerId, options) + require.NoError(t, err) + assert.NotEmpty(t, prvv.ID) + assert.Equal(t, options.Version, prvv.Version) + assert.Equal(t, options.KeyID, prvv.KeyID) + + t.Run("relationships are properly decoded", func(t *testing.T) { + assert.Equal(t, providerTest.ID, prvv.RegistryProvider.ID) + }) + + t.Run("timestamps are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, prvv.CreatedAt) + assert.NotEmpty(t, prvv.UpdatedAt) + }) + + t.Run("includes upload links", func(t *testing.T) { + _, err := prvv.ShasumsUploadURL() + assert.NoError(t, err) + _, err = prvv.ShasumsSigUploadURL() + assert.NoError(t, err) + expectedLinks := []string{ + "shasums-upload", + "shasums-sig-upload", + } + for _, l := range expectedLinks { + _, ok := prvv.Links[l].(string) + assert.True(t, ok, "Expect upload link: %s", l) + } + }) + + t.Run("doesn't include download links", func(t *testing.T) { + _, err := prvv.ShasumsDownloadURL() + assert.Error(t, err) + _, err = prvv.ShasumsSigDownloadURL() + assert.Error(t, err) + }) + }) + + t.Run("with invalid options", func(t *testing.T) { + t.Run("without a version", func(t *testing.T) { + options := RegistryProviderVersionCreateOptions{ + Version: "", + KeyID: "abcdefg", + } + rm, err := client.RegistryProviderVersions.Create(ctx, providerId, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidVersion.Error()) + }) + + t.Run("without a key-id", func(t *testing.T) { + options := RegistryProviderVersionCreateOptions{ + Version: "1.0.0", + KeyID: "", + } + rm, err := client.RegistryProviderVersions.Create(ctx, providerId, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidKeyID.Error()) + }) + + t.Run("with a public provider", func(t *testing.T) { + options := RegistryProviderVersionCreateOptions{ + Version: "1.0.0", + KeyID: "abcdefg", + } + providerId := RegistryProviderID{ + OrganizationName: providerTest.Organization.Name, + RegistryName: PublicRegistry, + Namespace: providerTest.Namespace, + Name: providerTest.Name, + } + rm, err := client.RegistryProviderVersions.Create(ctx, providerId, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrRequiredPrivateRegistry.Error()) + }) + + t.Run("without a valid provider id", func(t *testing.T) { + options := RegistryProviderVersionCreateOptions{ + Version: "1.0.0", + KeyID: "abcdefg", + } + providerId := RegistryProviderID{ + OrganizationName: badIdentifier, + RegistryName: providerTest.RegistryName, + Namespace: providerTest.Namespace, + Name: providerTest.Name, + } + rm, err := client.RegistryProviderVersions.Create(ctx, providerId, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) + }) +} + +func TestRegistryProviderVersionsList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + t.Run("with versions", func(t *testing.T) { + provider, providerCleanup := createPrivateRegistryProvider(t, client, nil) + defer providerCleanup() + + createN := 10 + versions := make([]*RegistryProviderVersion, 0) + // these providers will be destroyed when the org is cleaned up + for i := 0; i < createN; i++ { + version, _ := createRegistryProviderVersion(t, client, provider) + versions = append(versions, version) + } + versionN := len(versions) + + id := RegistryProviderID{ + OrganizationName: provider.Organization.Name, + Namespace: provider.Namespace, + Name: provider.Name, + RegistryName: provider.RegistryName, + } + + t.Run("returns all versions", func(t *testing.T) { + returnedVersions, err := client.RegistryProviderVersions.List(ctx, id, &RegistryProviderVersionListOptions{ + ListOptions: ListOptions{ + PageNumber: 0, + PageSize: versionN, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, returnedVersions.Items) + assert.Equal(t, versionN, returnedVersions.TotalCount) + assert.Equal(t, 1, returnedVersions.TotalPages) + for _, rv := range returnedVersions.Items { + foundVersion := false + for _, v := range versions { + if rv.ID == v.ID { + foundVersion = true + break + } + } + assert.True(t, foundVersion, "Expected to find version %s but did not:\nexpected:\n%v\nreturned\n%v", rv.ID, versions, returnedVersions) + } + }) + + t.Run("returns pages", func(t *testing.T) { + pageN := 2 + pageSize := versionN / pageN + + for page := 0; page < pageN; page++ { + testName := fmt.Sprintf("returns page %d of versions", page) + t.Run(testName, func(t *testing.T) { + returnedVersions, err := client.RegistryProviderVersions.List(ctx, id, &RegistryProviderVersionListOptions{ + ListOptions: ListOptions{ + PageNumber: page, + PageSize: pageSize, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, returnedVersions.Items) + assert.Equal(t, versionN, returnedVersions.TotalCount) + assert.Equal(t, pageN, returnedVersions.TotalPages) + assert.Equal(t, pageSize, len(returnedVersions.Items)) + for _, rv := range returnedVersions.Items { + foundVersion := false + for _, v := range versions { + if rv.ID == v.ID { + foundVersion = true + break + } + } + assert.True(t, foundVersion, "Expected to find version %s but did not:\nexpected:\n%v\nreturned\n%v", rv.ID, versions, returnedVersions) + } + }) + } + }) + }) + + t.Run("without versions", func(t *testing.T) { + provider, providerCleanup := createPrivateRegistryProvider(t, client, nil) + defer providerCleanup() + + id := RegistryProviderID{ + OrganizationName: provider.Organization.Name, + Namespace: provider.Namespace, + Name: provider.Name, + RegistryName: provider.RegistryName, + } + + versions, err := client.RegistryProviderVersions.List(ctx, id, nil) + require.NoError(t, err) + assert.Empty(t, versions.Items) + assert.Equal(t, 0, versions.TotalCount) + assert.Equal(t, 0, versions.TotalPages) + }) + + t.Run("with include provider platforms", func(t *testing.T) { + }) +} + +func TestRegistryProviderVersionsDelete(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + provider, providerCleanup := createPrivateRegistryProvider(t, client, nil) + defer providerCleanup() + + t.Run("with valid version", func(t *testing.T) { + version, _ := createRegistryProviderVersion(t, client, provider) + + versionId := RegistryProviderVersionID{ + RegistryProviderID: RegistryProviderID{ + OrganizationName: version.RegistryProvider.Organization.Name, + RegistryName: version.RegistryProvider.RegistryName, + Namespace: version.RegistryProvider.Namespace, + Name: version.RegistryProvider.Name, + }, + Version: version.Version, + } + + err := client.RegistryProviderVersions.Delete(ctx, versionId) + assert.NoError(t, err) + }) + + t.Run("with non existing version", func(t *testing.T) { + versionId := RegistryProviderVersionID{ + RegistryProviderID: RegistryProviderID{ + OrganizationName: provider.Organization.Name, + RegistryName: provider.RegistryName, + Namespace: provider.Namespace, + Name: provider.Name, + }, + Version: "1.0.0", + } + + err := client.RegistryProviderVersions.Delete(ctx, versionId) + assert.Error(t, err) + }) +} + +func TestRegistryProviderVersionsRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + t.Run("with valid version", func(t *testing.T) { + version, versionCleanup := createRegistryProviderVersion(t, client, nil) + defer versionCleanup() + + versionId := RegistryProviderVersionID{ + RegistryProviderID: RegistryProviderID{ + OrganizationName: version.RegistryProvider.Organization.Name, + RegistryName: version.RegistryProvider.RegistryName, + Namespace: version.RegistryProvider.Namespace, + Name: version.RegistryProvider.Name, + }, + Version: version.Version, + } + + readVersion, err := client.RegistryProviderVersions.Read(ctx, versionId, nil) + assert.NoError(t, err) + assert.Equal(t, version.ID, readVersion.ID) + assert.Equal(t, version.Version, readVersion.Version) + assert.Equal(t, version.KeyID, readVersion.KeyID) + + t.Run("relationships are properly decoded", func(t *testing.T) { + assert.Equal(t, version.RegistryProvider.ID, readVersion.RegistryProvider.ID) + }) + + t.Run("timestamps are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, readVersion.CreatedAt) + assert.NotEmpty(t, readVersion.UpdatedAt) + }) + + t.Run("includes upload links", func(t *testing.T) { + expectedLinks := []string{ + "shasums-upload", + "shasums-sig-upload", + } + for _, l := range expectedLinks { + _, ok := readVersion.Links[l].(string) + assert.True(t, ok, "Expect upload link: %s", l) + } + }) + }) + + t.Run("with non existing version", func(t *testing.T) { + provider, providerCleanup := createPrivateRegistryProvider(t, client, nil) + defer providerCleanup() + + versionId := RegistryProviderVersionID{ + RegistryProviderID: RegistryProviderID{ + OrganizationName: provider.Organization.Name, + RegistryName: provider.RegistryName, + Namespace: provider.Namespace, + Name: provider.Name, + }, + Version: "1.0.0", + } + + _, err := client.RegistryProviderVersions.Read(ctx, versionId, nil) + assert.Error(t, err) + }) + +} diff --git a/tfe.go b/tfe.go index bbc0e7f8d..56f03f213 100644 --- a/tfe.go +++ b/tfe.go @@ -129,6 +129,7 @@ type Client struct { PolicySets PolicySets RegistryModules RegistryModules RegistryProviders RegistryProviders + RegistryProviderVersions RegistryProviderVersions Runs Runs RunTasks RunTasks RunTriggers RunTriggers @@ -274,6 +275,7 @@ func NewClient(cfg *Config) (*Client, error) { client.PolicySets = &policySets{client: client} client.RegistryModules = ®istryModules{client: client} client.RegistryProviders = ®istryProviders{client: client} + client.RegistryProviderVersions = ®istryProviderVersions{client: client} client.Runs = &runs{client: client} client.RunTasks = &runTasks{client: client} client.RunTriggers = &runTriggers{client: client} From 73925120ccfae651685bcae2de06936f3cfa30bd Mon Sep 17 00:00:00 2001 From: Anna Winkler <3526523+annawinkler@users.noreply.github.com> Date: Thu, 5 May 2022 19:05:43 -0600 Subject: [PATCH 3/3] Add registry provider platform --- errors.go | 16 +- generate_mocks.sh | 1 + helper_test.go | 97 +++-- mocks/registry_provider_mocks.go | 95 +++++ mocks/registry_provider_platform_mocks.go | 95 +++++ mocks/registry_provider_version_mocks.go | 95 +++++ registry_provider.go | 97 ++--- registry_provider_integration_test.go | 119 ++---- registry_provider_platform.go | 221 +++++++++- ...stry_provider_platform_integration_test.go | 398 ++++++++++++++++++ registry_provider_version.go | 60 +-- registry_provider_version_integration_test.go | 37 +- tfe.go | 2 + 13 files changed, 1107 insertions(+), 226 deletions(-) create mode 100644 mocks/registry_provider_mocks.go create mode 100644 mocks/registry_provider_platform_mocks.go create mode 100644 mocks/registry_provider_version_mocks.go create mode 100644 registry_provider_platform_integration_test.go diff --git a/errors.go b/errors.go index 2491867b0..226d256e4 100644 --- a/errors.go +++ b/errors.go @@ -158,13 +158,13 @@ var ( ErrInvalidNamespace = errors.New("invalid value for namespace") - ErrInvalidPrivateProviderNamespaceDoesntMatchOrganization = errors.New("invalid namespace must match organization name for private providers") + ErrInvalidKeyID = errors.New("invalid value for key-id") - ErrInvalidRegistryName = errors.New("invalid value for registry-name") + ErrInvalidOS = errors.New("invalid value for OS") - ErrInvalidRegistryNameType = errors.New("invalid type for registry-name. Please use 'RegistryName'") + ErrInvalidArch = errors.New("invalid value for arch") - ErrInvalidKeyID = errors.New("invalid value for key-id") + ErrInvalidRegistryName = errors.New("invalid value for registry-name") ) // Missing values for required field/option @@ -280,4 +280,12 @@ var ( ErrInvalidEmail = errors.New("email is invalid") ErrRequiredPrivateRegistry = errors.New("only private registry is allowed") + + ErrRequiredOS = errors.New("OS is required") + + ErrRequiredArch = errors.New("arch is required") + + ErrRequiredShasum = errors.New("shasum is required") + + ErrRequiredFilename = errors.New("filename is required") ) diff --git a/generate_mocks.sh b/generate_mocks.sh index af9a0b20f..9eece4f4f 100755 --- a/generate_mocks.sh +++ b/generate_mocks.sh @@ -36,6 +36,7 @@ mockgen -source=policy_set_parameter.go -destination=mocks/policy_set_parameter_ mockgen -source=policy_set_version.go -destination=mocks/policy_set_version_mocks.go -package=mocks mockgen -source=registry_module.go -destination=mocks/registry_module_mocks.go -package=mocks mockgen -source=registry_provider.go -destination=mocks/registry_provider_mocks.go -package=mocks +mockgen -source=registry_provider_platform.go -destination=mocks/registry_provider_platform_mocks.go -package=mocks mockgen -source=registry_provider_version.go -destination=mocks/registry_provider_version_mocks.go -package=mocks mockgen -source=run.go -destination=mocks/run_mocks.go -package=mocks mockgen -source=run_task.go -destination=mocks/run_tasks.go -package=mocks diff --git a/helper_test.go b/helper_test.go index 94ad13f37..b82d374cf 100644 --- a/helper_test.go +++ b/helper_test.go @@ -745,24 +745,23 @@ 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) } @@ -780,19 +779,28 @@ 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 { org, orgCleanup = createOrganization(t, client) } + if (registryName != PublicRegistry) && (registryName != PrivateRegistry) { + t.Fatal("RegistryName must be public or private") + } + ctx := context.Background() + namespaceName := "test-namespace-" + randomString(t) + if registryName == PrivateRegistry { + namespaceName = org.Name + } + options := RegistryProviderCreateOptions{ - Name: "tst-name-" + randomString(t), - Namespace: org.Name, - RegistryName: PrivateRegistry, + Name: "test-registry-provider-" + randomString(t), + Namespace: namespaceName, + RegistryName: registryName, } prv, err := client.RegistryProviders.Create(ctx, org.Name, options) @@ -823,45 +831,63 @@ func createPrivateRegistryProvider(t *testing.T, client *Client, org *Organizati } } -func createPublicRegistryProvider(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) { - var orgCleanup func() +func createRegistryProviderPlatform(t *testing.T, client *Client, provider *RegistryProvider, version *RegistryProviderVersion) (*RegistryProviderPlatform, func()) { + var providerCleanup func() + var versionCleanup func() - if org == nil { - org, orgCleanup = createOrganization(t, client) + if provider == nil { + provider, providerCleanup = createRegistryProvider(t, client, nil, PrivateRegistry) + } + + providerID := RegistryProviderID{ + OrganizationName: provider.Organization.Name, + RegistryName: provider.RegistryName, + Namespace: provider.Namespace, + Name: provider.Name, + } + + if version == nil { + version, versionCleanup = createRegistryProviderVersion(t, client, provider) + } + + versionID := RegistryProviderVersionID{ + RegistryProviderID: providerID, + Version: version.Version, } ctx := context.Background() - options := RegistryProviderCreateOptions{ - Name: "tst-name-" + randomString(t), - Namespace: "tst-namespace-" + randomString(t), - RegistryName: PublicRegistry, + options := RegistryProviderPlatformCreateOptions{ + OS: randomString(t), + Arch: randomString(t), + Shasum: genSha(t, "secret", "data"), + Filename: randomString(t), } - prv, err := client.RegistryProviders.Create(ctx, org.Name, options) + rpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options) if err != nil { t.Fatal(err) } - prv.Organization = org - - return prv, func() { - id := RegistryProviderID{ - OrganizationName: org.Name, - RegistryName: prv.RegistryName, - Namespace: prv.Namespace, - Name: prv.Name, + return rpp, func() { + platformID := RegistryProviderPlatformID{ + RegistryProviderVersionID: versionID, + OS: rpp.OS, + Arch: rpp.Arch, } - if err := client.RegistryProviders.Delete(ctx, id); err != nil { - t.Errorf("Error destroying registry provider! WARNING: Dangling resources\n"+ + if err := client.RegistryProviderPlatforms.Delete(ctx, platformID); err != nil { + t.Errorf("Error destroying registry provider platform! 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) + "Registry Provider Version: %s/%s/%s/%s\nError: %s", rpp.RegistryProviderVersion.RegistryProvider.Namespace, rpp.RegistryProviderVersion.RegistryProvider.Name, rpp.OS, rpp.Arch, err) } - if orgCleanup != nil { - orgCleanup() + if versionCleanup != nil { + versionCleanup() + } + if providerCleanup != nil { + providerCleanup() } } } @@ -870,7 +896,7 @@ func createRegistryProviderVersion(t *testing.T, client *Client, provider *Regis var providerCleanup func() if provider == nil { - provider, providerCleanup = createPrivateRegistryProvider(t, client, nil) + provider, providerCleanup = createRegistryProvider(t, client, nil, PrivateRegistry) } providerID := RegistryProviderID{ @@ -883,8 +909,9 @@ func createRegistryProviderVersion(t *testing.T, client *Client, provider *Regis ctx := context.Background() options := RegistryProviderVersionCreateOptions{ - Version: randomSemver(t), - KeyID: randomString(t), + Version: randomSemver(t), + KeyID: randomString(t), + Protocols: []string{"4.0", "5.0", "6.0"}, } prvv, err := client.RegistryProviderVersions.Create(ctx, providerID, options) diff --git a/mocks/registry_provider_mocks.go b/mocks/registry_provider_mocks.go new file mode 100644 index 000000000..06285fdce --- /dev/null +++ b/mocks/registry_provider_mocks.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: registry_provider.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + tfe "github.com/hashicorp/go-tfe" +) + +// MockRegistryProviders is a mock of RegistryProviders interface. +type MockRegistryProviders struct { + ctrl *gomock.Controller + recorder *MockRegistryProvidersMockRecorder +} + +// MockRegistryProvidersMockRecorder is the mock recorder for MockRegistryProviders. +type MockRegistryProvidersMockRecorder struct { + mock *MockRegistryProviders +} + +// NewMockRegistryProviders creates a new mock instance. +func NewMockRegistryProviders(ctrl *gomock.Controller) *MockRegistryProviders { + mock := &MockRegistryProviders{ctrl: ctrl} + mock.recorder = &MockRegistryProvidersMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRegistryProviders) EXPECT() *MockRegistryProvidersMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRegistryProviders) Create(ctx context.Context, organization string, options tfe.RegistryProviderCreateOptions) (*tfe.RegistryProvider, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, organization, options) + ret0, _ := ret[0].(*tfe.RegistryProvider) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockRegistryProvidersMockRecorder) Create(ctx, organization, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryProviders)(nil).Create), ctx, organization, options) +} + +// Delete mocks base method. +func (m *MockRegistryProviders) Delete(ctx context.Context, providerID tfe.RegistryProviderID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, providerID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRegistryProvidersMockRecorder) Delete(ctx, providerID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRegistryProviders)(nil).Delete), ctx, providerID) +} + +// List mocks base method. +func (m *MockRegistryProviders) List(ctx context.Context, organization string, options *tfe.RegistryProviderListOptions) (*tfe.RegistryProviderList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, organization, options) + ret0, _ := ret[0].(*tfe.RegistryProviderList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockRegistryProvidersMockRecorder) List(ctx, organization, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRegistryProviders)(nil).List), ctx, organization, options) +} + +// Read mocks base method. +func (m *MockRegistryProviders) Read(ctx context.Context, providerID tfe.RegistryProviderID, options *tfe.RegistryProviderReadOptions) (*tfe.RegistryProvider, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", ctx, providerID, options) + ret0, _ := ret[0].(*tfe.RegistryProvider) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockRegistryProvidersMockRecorder) Read(ctx, providerID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRegistryProviders)(nil).Read), ctx, providerID, options) +} diff --git a/mocks/registry_provider_platform_mocks.go b/mocks/registry_provider_platform_mocks.go new file mode 100644 index 000000000..7c518abf4 --- /dev/null +++ b/mocks/registry_provider_platform_mocks.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: registry_provider_platform.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + tfe "github.com/hashicorp/go-tfe" +) + +// MockRegistryProviderPlatforms is a mock of RegistryProviderPlatforms interface. +type MockRegistryProviderPlatforms struct { + ctrl *gomock.Controller + recorder *MockRegistryProviderPlatformsMockRecorder +} + +// MockRegistryProviderPlatformsMockRecorder is the mock recorder for MockRegistryProviderPlatforms. +type MockRegistryProviderPlatformsMockRecorder struct { + mock *MockRegistryProviderPlatforms +} + +// NewMockRegistryProviderPlatforms creates a new mock instance. +func NewMockRegistryProviderPlatforms(ctrl *gomock.Controller) *MockRegistryProviderPlatforms { + mock := &MockRegistryProviderPlatforms{ctrl: ctrl} + mock.recorder = &MockRegistryProviderPlatformsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRegistryProviderPlatforms) EXPECT() *MockRegistryProviderPlatformsMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRegistryProviderPlatforms) Create(ctx context.Context, versionID tfe.RegistryProviderVersionID, options tfe.RegistryProviderPlatformCreateOptions) (*tfe.RegistryProviderPlatform, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, versionID, options) + ret0, _ := ret[0].(*tfe.RegistryProviderPlatform) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockRegistryProviderPlatformsMockRecorder) Create(ctx, versionID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryProviderPlatforms)(nil).Create), ctx, versionID, options) +} + +// Delete mocks base method. +func (m *MockRegistryProviderPlatforms) Delete(ctx context.Context, platformID tfe.RegistryProviderPlatformID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, platformID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRegistryProviderPlatformsMockRecorder) Delete(ctx, platformID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRegistryProviderPlatforms)(nil).Delete), ctx, platformID) +} + +// List mocks base method. +func (m *MockRegistryProviderPlatforms) List(ctx context.Context, versionID tfe.RegistryProviderVersionID, options *tfe.RegistryProviderPlatformListOptions) (*tfe.RegistryProviderPlatformList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, versionID, options) + ret0, _ := ret[0].(*tfe.RegistryProviderPlatformList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockRegistryProviderPlatformsMockRecorder) List(ctx, versionID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRegistryProviderPlatforms)(nil).List), ctx, versionID, options) +} + +// Read mocks base method. +func (m *MockRegistryProviderPlatforms) Read(ctx context.Context, platformID tfe.RegistryProviderPlatformID) (*tfe.RegistryProviderPlatform, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", ctx, platformID) + ret0, _ := ret[0].(*tfe.RegistryProviderPlatform) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockRegistryProviderPlatformsMockRecorder) Read(ctx, platformID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRegistryProviderPlatforms)(nil).Read), ctx, platformID) +} diff --git a/mocks/registry_provider_version_mocks.go b/mocks/registry_provider_version_mocks.go new file mode 100644 index 000000000..b9b84816c --- /dev/null +++ b/mocks/registry_provider_version_mocks.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: registry_provider_version.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + tfe "github.com/hashicorp/go-tfe" +) + +// MockRegistryProviderVersions is a mock of RegistryProviderVersions interface. +type MockRegistryProviderVersions struct { + ctrl *gomock.Controller + recorder *MockRegistryProviderVersionsMockRecorder +} + +// MockRegistryProviderVersionsMockRecorder is the mock recorder for MockRegistryProviderVersions. +type MockRegistryProviderVersionsMockRecorder struct { + mock *MockRegistryProviderVersions +} + +// NewMockRegistryProviderVersions creates a new mock instance. +func NewMockRegistryProviderVersions(ctrl *gomock.Controller) *MockRegistryProviderVersions { + mock := &MockRegistryProviderVersions{ctrl: ctrl} + mock.recorder = &MockRegistryProviderVersionsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRegistryProviderVersions) EXPECT() *MockRegistryProviderVersionsMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRegistryProviderVersions) Create(ctx context.Context, providerID tfe.RegistryProviderID, options tfe.RegistryProviderVersionCreateOptions) (*tfe.RegistryProviderVersion, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, providerID, options) + ret0, _ := ret[0].(*tfe.RegistryProviderVersion) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockRegistryProviderVersionsMockRecorder) Create(ctx, providerID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryProviderVersions)(nil).Create), ctx, providerID, options) +} + +// Delete mocks base method. +func (m *MockRegistryProviderVersions) Delete(ctx context.Context, versionID tfe.RegistryProviderVersionID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, versionID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRegistryProviderVersionsMockRecorder) Delete(ctx, versionID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRegistryProviderVersions)(nil).Delete), ctx, versionID) +} + +// List mocks base method. +func (m *MockRegistryProviderVersions) List(ctx context.Context, providerID tfe.RegistryProviderID, options *tfe.RegistryProviderVersionListOptions) (*tfe.RegistryProviderVersionList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, providerID, options) + ret0, _ := ret[0].(*tfe.RegistryProviderVersionList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockRegistryProviderVersionsMockRecorder) List(ctx, providerID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRegistryProviderVersions)(nil).List), ctx, providerID, options) +} + +// Read mocks base method. +func (m *MockRegistryProviderVersions) Read(ctx context.Context, versionID tfe.RegistryProviderVersionID) (*tfe.RegistryProviderVersion, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", ctx, versionID) + ret0, _ := ret[0].(*tfe.RegistryProviderVersion) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockRegistryProviderVersionsMockRecorder) Read(ctx, versionID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRegistryProviderVersions)(nil).Read), ctx, versionID) +} diff --git a/registry_provider.go b/registry_provider.go index 83d2d8964..db32da997 100644 --- a/registry_provider.go +++ b/registry_provider.go @@ -51,13 +51,13 @@ const ( // RegistryProvider represents a registry provider type RegistryProvider struct { - ID string `jsonapi:"primary,registry-providers"` - Name string `jsonapi:"attr,name"` - Namespace string `jsonapi:"attr,namespace"` - CreatedAt string `jsonapi:"attr,created-at"` - UpdatedAt string `jsonapi:"attr,updated-at"` - RegistryName RegistryName `jsonapi:"attr,registry-name"` - Permissions *RegistryProviderPermissions `jsonapi:"attr,permissions"` + ID string `jsonapi:"primary,registry-providers"` + Name string `jsonapi:"attr,name"` + Namespace string `jsonapi:"attr,namespace"` + CreatedAt string `jsonapi:"attr,created-at,iso8601"` + UpdatedAt string `jsonapi:"attr,updated-at,iso8601"` + RegistryName RegistryName `jsonapi:"attr,registry-name"` + Permissions RegistryProviderPermissions `jsonapi:"attr,permissions"` // Relations Organization *Organization `jsonapi:"relation,organization"` @@ -68,23 +68,22 @@ type RegistryProvider struct { } type RegistryProviderPermissions struct { - CanDelete bool `jsonapi:"attr,can-delete,omitempty"` - CanUploadAsset bool `jsonapi:"attr,can-upload-asset,omitempty"` + CanDelete bool `jsonapi:"attr,can-delete"` } type RegistryProviderListOptions struct { ListOptions - // A query string to filter by registry_name + // Optional: A query string to filter by registry_name RegistryName RegistryName `url:"filter[registry_name],omitempty"` - // A query string to filter by organization + // Optional: A query string to filter by organization OrganizationName string `url:"filter[organization_name],omitempty"` - // A query string to do a fuzzy search + // Optional: A query string to do a fuzzy search Search string `url:"q,omitempty"` - // Include related jsonapi relationships + // Optional: Include related jsonapi relationships Include *[]RegistryProviderIncludeOps `url:"include,omitempty"` } @@ -95,25 +94,41 @@ type RegistryProviderList struct { // RegistryProviderID is the multi key ID for addressing a provider type RegistryProviderID struct { - OrganizationName string `jsonapi:"attr,organization-name"` - RegistryName RegistryName `jsonapi:"attr,registry-name"` - Namespace string `jsonapi:"attr,namespace"` - Name string `jsonapi:"attr,name"` + OrganizationName string + RegistryName RegistryName + Namespace string + Name string +} + +// 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"` + + // Required: The name of the registry provider + Name string `jsonapi:"attr,name"` + + // Required: The namespace of the provider. For private providers, this is the same as the organization name + Namespace string `jsonapi:"attr,namespace"` + + // Required: Whether this is a publicly maintained provider or private. Must be either public or private. + RegistryName RegistryName `jsonapi:"attr,registry-name"` } type RegistryProviderReadOptions struct { - // Include related jsonapi relationships - Include *[]RegistryProviderIncludeOps `url:"include,omitempty"` + // Optional: Include related jsonapi relationships + Include []RegistryProviderIncludeOps `url:"include,omitempty"` } 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 - } + if err := options.valid(); err != nil { + return nil, err } u := fmt.Sprintf("organizations/%s/registry-providers", url.QueryEscape(organization)) @@ -131,19 +146,6 @@ func (r *registryProviders) List(ctx context.Context, organization string, optio 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"` - - Name string `jsonapi:"attr,name"` - Namespace string `jsonapi:"attr,namespace"` - RegistryName RegistryName `jsonapi:"attr,registry-name"` -} - func (r *registryProviders) Create(ctx context.Context, organization string, options RegistryProviderCreateOptions) (*RegistryProvider, error) { if !validStringID(&organization) { return nil, ErrInvalidOrg @@ -153,12 +155,6 @@ func (r *registryProviders) Create(ctx context.Context, organization string, opt return nil, err } - // For private providers, the organization name and namespace must be the same. - // This is enforced by the API as well - if options.RegistryName == PrivateRegistry && organization != options.Namespace { - return nil, ErrInvalidPrivateProviderNamespaceDoesntMatchOrganization - } - u := fmt.Sprintf( "organizations/%s/registry-providers", url.QueryEscape(organization), @@ -223,14 +219,6 @@ func (r *registryProviders) Delete(ctx context.Context, providerID RegistryProvi return r.client.do(ctx, req, nil) } -func (rn RegistryName) valid() error { - switch rn { - case PrivateRegistry, PublicRegistry: - return nil - } - return ErrInvalidRegistryName -} - func (o RegistryProviderCreateOptions) valid() error { if !validStringID(&o.Name) { return ErrInvalidName @@ -238,9 +226,6 @@ func (o RegistryProviderCreateOptions) valid() error { if !validStringID(&o.Namespace) { return ErrInvalidNamespace } - if err := o.RegistryName.valid(); err != nil { - return err - } return nil } @@ -254,12 +239,12 @@ func (id RegistryProviderID) valid() error { if !validStringID(&id.Namespace) { return ErrInvalidNamespace } - if err := id.RegistryName.valid(); err != nil { - return err + if !validStringID((*string)(&id.RegistryName)) { + return ErrInvalidRegistryName } return nil } -func (o RegistryProviderListOptions) valid() error { +func (o *RegistryProviderListOptions) valid() error { return nil } diff --git a/registry_provider_integration_test.go b/registry_provider_integration_test.go index 6b87b8e44..3e53cf292 100644 --- a/registry_provider_integration_test.go +++ b/registry_provider_integration_test.go @@ -22,15 +22,15 @@ func TestRegistryProvidersList(t *testing.T) { createN := 10 providers := make([]*RegistryProvider, 0) - // these providers will be destroyed when the org is cleaned up + // These providers will be destroyed when the org is cleaned up for i := 0; i < createN; i++ { // Create public providers - providerTest, _ := createPublicRegistryProvider(t, client, orgTest) + providerTest, _ := createRegistryProvider(t, client, orgTest, PublicRegistry) providers = append(providers, providerTest) } for i := 0; i < createN; i++ { // Create private providers - providerTest, _ := createPrivateRegistryProvider(t, client, orgTest) + providerTest, _ := createRegistryProvider(t, client, orgTest, PrivateRegistry) providers = append(providers, providerTest) } providerN := len(providers) @@ -47,48 +47,22 @@ func TestRegistryProvidersList(t *testing.T) { 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("with list options", func(t *testing.T) { + // Request a page number which is out of range. The result should + // be successful, but return no results if the paging options are + // properly passed along. + rpl, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{ + ListOptions: ListOptions{ + PageNumber: 999, + PageSize: 100, + }, + }) + require.NoError(t, err) + assert.Empty(t, rpl.Items) + assert.Equal(t, 999, rpl.CurrentPage) + assert.Equal(t, 20, rpl.TotalCount) }) t.Run("filters on registry name", func(t *testing.T) { @@ -120,17 +94,15 @@ func TestRegistryProvidersList(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] + 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) }) }) @@ -280,7 +252,8 @@ func TestRegistryProvidersCreate(t *testing.T) { } rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) assert.Nil(t, rm) - assert.EqualError(t, err, ErrInvalidRegistryName.Error()) + // This error is returned by the API + assert.EqualError(t, err, "invalid attribute\n\nRegistry name can't be blank\ninvalid attribute\n\nRegistry name is not included in the list") }) t.Run("with an invalid registry-name", func(t *testing.T) { @@ -291,7 +264,8 @@ func TestRegistryProvidersCreate(t *testing.T) { } rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) assert.Nil(t, rm) - assert.EqualError(t, err, ErrInvalidRegistryName.Error()) + // This error is returned by the API + assert.EqualError(t, err, "invalid attribute\n\nRegistry name is not included in the list") }) }) @@ -305,17 +279,6 @@ func TestRegistryProvidersCreate(t *testing.T) { 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: "name", - Namespace: "namespace", - RegistryName: PrivateRegistry, - } - rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) - assert.Nil(t, rm) - assert.EqualError(t, err, ErrInvalidPrivateProviderNamespaceDoesntMatchOrganization.Error()) - }) } func TestRegistryProvidersRead(t *testing.T) { @@ -326,18 +289,15 @@ func TestRegistryProvidersRead(t *testing.T) { defer orgTestCleanup() type ProviderContext struct { - ProviderCreator func(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) - RegistryName RegistryName + RegistryName RegistryName } providerContexts := []ProviderContext{ { - ProviderCreator: createPublicRegistryProvider, - RegistryName: PublicRegistry, + RegistryName: PublicRegistry, }, { - ProviderCreator: createPrivateRegistryProvider, - RegistryName: PrivateRegistry, + RegistryName: PrivateRegistry, }, } @@ -345,7 +305,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() id := RegistryProviderID{ @@ -411,7 +371,7 @@ func TestRegistryProvidersRead(t *testing.T) { } options := RegistryProviderReadOptions{ - Include: &[]RegistryProviderIncludeOps{ + Include: []RegistryProviderIncludeOps{ RegistryProviderVersionsInclude, }, } @@ -442,18 +402,15 @@ func TestRegistryProvidersDelete(t *testing.T) { defer orgTestCleanup() type ProviderContext struct { - ProviderCreator func(t *testing.T, client *Client, org *Organization) (*RegistryProvider, func()) - RegistryName RegistryName + RegistryName RegistryName } providerContexts := []ProviderContext{ { - ProviderCreator: createPublicRegistryProvider, - RegistryName: PublicRegistry, + RegistryName: PublicRegistry, }, { - ProviderCreator: createPrivateRegistryProvider, - RegistryName: PrivateRegistry, + RegistryName: PrivateRegistry, }, } @@ -461,7 +418,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) id := RegistryProviderID{ OrganizationName: orgTest.Name, @@ -559,16 +516,6 @@ func TestRegistryProvidersIDValidation(t *testing.T) { assert.EqualError(t, id.valid(), ErrInvalidRegistryName.Error()) }) - t.Run("with in invalid registry-name", func(t *testing.T) { - id := RegistryProviderID{ - OrganizationName: orgName, - RegistryName: "invalid registry name", - Namespace: "namespace", - Name: "name", - } - assert.EqualError(t, id.valid(), ErrInvalidRegistryName.Error()) - }) - t.Run("without a valid organization", func(t *testing.T) { id := RegistryProviderID{ OrganizationName: badIdentifier, diff --git a/registry_provider_platform.go b/registry_provider_platform.go index 4f6db5f6f..9f2d99756 100644 --- a/registry_provider_platform.go +++ b/registry_provider_platform.go @@ -1,12 +1,43 @@ package tfe +import ( + "context" + "fmt" + "net/url" +) + +// Compile-time proof of interface implementation +var _ RegistryProviderPlatforms = (*registryProviderPlatforms)(nil) + +// RegistryProviderPlatforms describes the registry provider platform methods supported by the Terraform Enterprise API. +// +// TFE API docs: https://www.terraform.io/cloud-docs/api-docs/private-registry/provider-versions-platforms#private-provider-versions-and-platforms-api +type RegistryProviderPlatforms interface { + // Create a provider platform for an organization + Create(ctx context.Context, versionID RegistryProviderVersionID, options RegistryProviderPlatformCreateOptions) (*RegistryProviderPlatform, error) + + // List all provider platforms for a single version + List(ctx context.Context, versionID RegistryProviderVersionID, options *RegistryProviderPlatformListOptions) (*RegistryProviderPlatformList, error) + + // Read a provider platform by ID + Read(ctx context.Context, platformID RegistryProviderPlatformID) (*RegistryProviderPlatform, error) + + // Delete a provider platform + Delete(ctx context.Context, platformID RegistryProviderPlatformID) error +} + +// registryProviders implements RegistryProviders +type registryProviderPlatforms struct { + client *Client +} + // RegistryProviderPlatform represents a registry provider platform type RegistryProviderPlatform struct { ID string `jsonapi:"primary,registry-provider-platforms"` - Os string `jsonapi:"attr,os"` + OS string `jsonapi:"attr,os"` Arch string `jsonapi:"attr,arch"` Filename string `jsonapi:"attr,filename"` - SHASUM string `jsonapi:"attr,shasum"` + Shasum string `jsonapi:"attr,shasum"` // Relations RegistryProviderVersion *RegistryProviderVersion `jsonapi:"relation,registry-provider-version"` @@ -14,3 +45,189 @@ type RegistryProviderPlatform struct { // Links Links map[string]interface{} `jsonapi:"links,omitempty"` } + +// RegistryProviderPlatformID is the multi key ID for identifying a provider platform +type RegistryProviderPlatformID struct { + RegistryProviderVersionID + OS string + Arch string +} + +// RegistryProviderPlatformCreateOptions represents the set of options for creating a registry provider platform +type RegistryProviderPlatformCreateOptions struct { + // Required: A valid operating system string + OS string `jsonapi:"attr,os"` + + // Required: A valid architecture string + Arch string `jsonapi:"attr,arch"` + + // Required: A valid shasum string + Shasum string `jsonapi:"attr,shasum"` + + // Required: A valid filename string + Filename string `jsonapi:"attr,filename"` +} + +type RegistryProviderPlatformList struct { + *Pagination + Items []*RegistryProviderPlatform +} + +type RegistryProviderPlatformListOptions struct { + ListOptions +} + +// Create a new registry provider platform +func (r *registryProviderPlatforms) Create(ctx context.Context, versionID RegistryProviderVersionID, options RegistryProviderPlatformCreateOptions) (*RegistryProviderPlatform, error) { + if err := versionID.valid(); err != nil { + return nil, err + } + + if err := options.valid(); err != nil { + return nil, err + } + + // POST /organizations/:organization_name/registry-providers/:registry_name/:namespace/:name/versions/:version/platforms + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s/versions/%s/platforms", + url.QueryEscape(versionID.OrganizationName), + url.QueryEscape(string(versionID.RegistryName)), + url.QueryEscape(versionID.Namespace), + url.QueryEscape(versionID.Name), + url.QueryEscape(versionID.Version), + ) + req, err := r.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + + rpp := &RegistryProviderPlatform{} + err = r.client.do(ctx, req, rpp) + if err != nil { + return nil, err + } + + return rpp, nil +} + +// List all provider platforms for a single version +func (r *registryProviderPlatforms) List(ctx context.Context, versionID RegistryProviderVersionID, options *RegistryProviderPlatformListOptions) (*RegistryProviderPlatformList, error) { + if err := versionID.valid(); err != nil { + return nil, err + } + if err := options.valid(); err != nil { + return nil, err + } + + // GET /organizations/:organization_name/registry-providers/:registry_name/:namespace/:name/versions/:version/platforms + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s/versions/%s/platforms", + url.QueryEscape(versionID.RegistryProviderID.OrganizationName), + url.QueryEscape(string(versionID.RegistryProviderID.RegistryName)), + url.QueryEscape(versionID.RegistryProviderID.Namespace), + url.QueryEscape(versionID.RegistryProviderID.Name), + url.QueryEscape(versionID.Version), + ) + req, err := r.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + ppl := &RegistryProviderPlatformList{} + err = r.client.do(ctx, req, ppl) + if err != nil { + return nil, err + } + + return ppl, nil +} + +// Read is used to read an organization's example by ID +func (r *registryProviderPlatforms) Read(ctx context.Context, platformID RegistryProviderPlatformID) (*RegistryProviderPlatform, error) { + if err := platformID.valid(); err != nil { + return nil, err + } + + // GET /organizations/:organization_name/registry-providers/:registry_name/:namespace/:name/versions/:version/platforms/:os/:arch + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s/versions/%s/platforms/%s/%s", + url.QueryEscape(platformID.RegistryProviderID.OrganizationName), + url.QueryEscape(string(platformID.RegistryProviderID.RegistryName)), + url.QueryEscape(platformID.RegistryProviderID.Namespace), + url.QueryEscape(platformID.RegistryProviderID.Name), + url.QueryEscape(platformID.RegistryProviderVersionID.Version), + url.QueryEscape(platformID.OS), + url.QueryEscape(platformID.Arch), + ) + req, err := r.client.newRequest("GET", u, nil) + if err != nil { + return nil, err + } + + rpp := &RegistryProviderPlatform{} + err = r.client.do(ctx, req, rpp) + + if err != nil { + return nil, err + } + + return rpp, nil +} + +// Delete a registry provider platform +func (r *registryProviderPlatforms) Delete(ctx context.Context, platformID RegistryProviderPlatformID) error { + if err := platformID.valid(); err != nil { + return err + } + + // DELETE /organizations/:organization_name/registry-providers/:registry_name/:namespace/:name/versions/:version/platforms/:os/:arch + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s/versions/%s/platforms/%s/%s", + url.QueryEscape(platformID.OrganizationName), + url.QueryEscape(string(platformID.RegistryName)), + url.QueryEscape(platformID.Namespace), + url.QueryEscape(platformID.Name), + url.QueryEscape(platformID.Version), + url.QueryEscape(platformID.OS), + url.QueryEscape(platformID.Arch), + ) + req, err := r.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return r.client.do(ctx, req, nil) +} + +func (id RegistryProviderPlatformID) valid() error { + if err := id.RegistryProviderID.valid(); err != nil { + return err + } + if !validString(&id.OS) { + return ErrInvalidOS + } + if !validString(&id.Arch) { + return ErrInvalidArch + } + return nil +} + +func (o RegistryProviderPlatformCreateOptions) valid() error { + if !validString(&o.OS) { + return ErrRequiredOS + } + if !validString(&o.Arch) { + return ErrRequiredArch + } + if !validStringID(&o.Shasum) { + return ErrRequiredShasum + } + if !validStringID(&o.Filename) { + return ErrRequiredFilename + } + return nil +} + +func (o *RegistryProviderPlatformListOptions) valid() error { + return nil +} diff --git a/registry_provider_platform_integration_test.go b/registry_provider_platform_integration_test.go new file mode 100644 index 000000000..b82f44f55 --- /dev/null +++ b/registry_provider_platform_integration_test.go @@ -0,0 +1,398 @@ +//go:build integration +// +build integration + +package tfe + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRegistryProviderPlatformsCreate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + provider, providerTestCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) + defer providerTestCleanup() + + version, versionCleanup := createRegistryProviderVersion(t, client, provider) + defer versionCleanup() + + versionID := RegistryProviderVersionID{ + RegistryProviderID: RegistryProviderID{ + OrganizationName: provider.Organization.Name, + RegistryName: provider.RegistryName, + Namespace: provider.Namespace, + Name: provider.Name, + }, + Version: version.Version, + } + + t.Run("with valid options", func(t *testing.T) { + options := RegistryProviderPlatformCreateOptions{ + OS: "foo", + Arch: "scrimbles", + Shasum: "shasum", + Filename: "filename", + } + + rpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options) + require.NoError(t, err) + assert.NotEmpty(t, rpp.ID) + assert.Equal(t, options.OS, rpp.OS) + assert.Equal(t, options.Arch, rpp.Arch) + assert.Equal(t, options.Shasum, rpp.Shasum) + assert.Equal(t, options.Filename, rpp.Filename) + + t.Run("relationships are properly decoded", func(t *testing.T) { + assert.Equal(t, version.ID, rpp.RegistryProviderVersion.ID) + }) + + t.Run("attributes are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, rpp.Arch) + assert.NotEmpty(t, rpp.OS) + assert.NotEmpty(t, rpp.Shasum) + assert.NotEmpty(t, rpp.Filename) + }) + }) + + t.Run("with invalid options", func(t *testing.T) { + t.Run("without an OS", func(t *testing.T) { + options := RegistryProviderPlatformCreateOptions{ + OS: "", + Arch: "scrimbles", + Shasum: "shasum", + Filename: "filename", + } + + sad_rpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options) + + assert.Nil(t, sad_rpp) + assert.EqualError(t, err, ErrRequiredOS.Error()) + }) + + t.Run("without an arch", func(t *testing.T) { + options := RegistryProviderPlatformCreateOptions{ + OS: "os", + Arch: "", + Shasum: "shasum", + Filename: "filename", + } + + sad_rpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options) + + assert.Nil(t, sad_rpp) + assert.EqualError(t, err, ErrRequiredArch.Error()) + }) + + t.Run("without a shasum", func(t *testing.T) { + options := RegistryProviderPlatformCreateOptions{ + OS: "os", + Arch: "scrimbles", + Shasum: "", + Filename: "filename", + } + + sad_rpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options) + + assert.Nil(t, sad_rpp) + assert.EqualError(t, err, ErrRequiredShasum.Error()) + }) + + t.Run("without a filename", func(t *testing.T) { + options := RegistryProviderPlatformCreateOptions{ + OS: "os", + Arch: "scrimbles", + Shasum: "shasum", + Filename: "", + } + + sad_rpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options) + + assert.Nil(t, sad_rpp) + assert.EqualError(t, err, ErrRequiredFilename.Error()) + }) + + t.Run("with a public provider", func(t *testing.T) { + options := RegistryProviderPlatformCreateOptions{ + OS: "os", + Arch: "scrimbles", + Shasum: "shasum", + Filename: "filename", + } + + versionID = RegistryProviderVersionID{ + RegistryProviderID: RegistryProviderID{ + OrganizationName: provider.Organization.Name, + RegistryName: PublicRegistry, + Namespace: provider.Namespace, + Name: provider.Name, + }, + Version: version.Version, + } + + rm, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrRequiredPrivateRegistry.Error()) + }) + + t.Run("without a valid registry provider version id", func(t *testing.T) { + options := RegistryProviderPlatformCreateOptions{ + OS: "os", + Arch: "scrimbles", + Shasum: "shasum", + Filename: "filename", + } + + versionID = RegistryProviderVersionID{ + RegistryProviderID: RegistryProviderID{ + OrganizationName: badIdentifier, + RegistryName: provider.RegistryName, + Namespace: provider.Namespace, + Name: provider.Name, + }, + Version: version.Version, + } + + rm, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) + }) +} + +func TestRegistryProviderPlatformsDelete(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) + defer providerCleanup() + + version, versionCleanup := createRegistryProviderVersion(t, client, provider) + defer versionCleanup() + + versionID := RegistryProviderVersionID{ + RegistryProviderID: RegistryProviderID{ + OrganizationName: provider.Organization.Name, + RegistryName: provider.RegistryName, + Namespace: provider.Namespace, + Name: provider.Name, + }, + Version: version.Version, + } + + t.Run("with a valid version", func(t *testing.T) { + platform, _ := createRegistryProviderPlatform(t, client, provider, version) + + platformID := RegistryProviderPlatformID{ + RegistryProviderVersionID: versionID, + OS: platform.OS, + Arch: platform.Arch, + } + + err := client.RegistryProviderPlatforms.Delete(ctx, platformID) + assert.NoError(t, err) + }) + + t.Run("with a non-existant version", func(t *testing.T) { + platformID := RegistryProviderPlatformID{ + RegistryProviderVersionID: versionID, + OS: "nope", + Arch: "no", + } + + err := client.RegistryProviderPlatforms.Delete(ctx, platformID) + assert.Error(t, err) + }) +} + +func TestRegistryProviderPlatformsRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) + defer providerCleanup() + + providerID := RegistryProviderID{ + OrganizationName: provider.Organization.Name, + Namespace: provider.Namespace, + Name: provider.Name, + RegistryName: provider.RegistryName, + } + + version, versionCleanup := createRegistryProviderVersion(t, client, provider) + defer versionCleanup() + + versionID := RegistryProviderVersionID{ + RegistryProviderID: providerID, + Version: version.Version, + } + + platform, platformCleanup := createRegistryProviderPlatform(t, client, provider, version) + defer platformCleanup() + + t.Run("with valid platform", func(t *testing.T) { + platformID := RegistryProviderPlatformID{ + RegistryProviderVersionID: versionID, + OS: platform.OS, + Arch: platform.Arch, + } + + readPlatform, err := client.RegistryProviderPlatforms.Read(ctx, platformID) + assert.NoError(t, err) + assert.Equal(t, platformID.OS, readPlatform.OS) + assert.Equal(t, platformID.Arch, readPlatform.Arch) + assert.Equal(t, platform.Filename, readPlatform.Filename) + assert.Equal(t, platform.Shasum, readPlatform.Shasum) + + t.Run("relationships are properly decoded", func(t *testing.T) { + assert.Equal(t, platform.RegistryProviderVersion.ID, readPlatform.RegistryProviderVersion.ID) + }) + + t.Run("includes provider binary upload link", func(t *testing.T) { + expectedLinks := []string{ + "provider-binary-upload", + } + for _, l := range expectedLinks { + _, ok := readPlatform.Links[l].(string) + assert.True(t, ok, "Expect upload link: %s", l) + } + }) + }) + + t.Run("with non-existant os", func(t *testing.T) { + platformID := RegistryProviderPlatformID{ + RegistryProviderVersionID: versionID, + OS: "DoesNotExist", + Arch: platform.Arch, + } + + _, err := client.RegistryProviderPlatforms.Read(ctx, platformID) + assert.Error(t, err) + }) + + t.Run("with non-existant arch", func(t *testing.T) { + platformID := RegistryProviderPlatformID{ + RegistryProviderVersionID: versionID, + OS: platform.OS, + Arch: "DoesNotExist", + } + + _, err := client.RegistryProviderPlatforms.Read(ctx, platformID) + assert.Error(t, err) + }) +} + +func TestRegistryProviderPlatformsList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + t.Run("with platforms", func(t *testing.T) { + provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) + defer providerCleanup() + + version, versionCleanup := createRegistryProviderVersion(t, client, provider) + defer versionCleanup() + + numToCreate := 10 + platforms := make([]*RegistryProviderPlatform, 0) + for i := 0; i < numToCreate; i++ { + platform, _ := createRegistryProviderPlatform(t, client, provider, version) + platforms = append(platforms, platform) + } + numPlatforms := len(platforms) + + providerID := RegistryProviderID{ + OrganizationName: provider.Organization.Name, + Namespace: provider.Namespace, + Name: provider.Name, + RegistryName: provider.RegistryName, + } + versionID := RegistryProviderVersionID{ + RegistryProviderID: providerID, + Version: version.Version, + } + + t.Run("returns all platforms", func(t *testing.T) { + returnedPlatforms, err := client.RegistryProviderPlatforms.List(ctx, versionID, &RegistryProviderPlatformListOptions{ + ListOptions: ListOptions{ + PageNumber: 0, + PageSize: numPlatforms, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, returnedPlatforms.Items) + assert.Equal(t, numPlatforms, returnedPlatforms.TotalCount) + assert.Equal(t, 1, returnedPlatforms.TotalPages) + for _, rp := range returnedPlatforms.Items { + foundPlatform := false + for _, p := range platforms { + if rp.ID == p.ID { + foundPlatform = true + break + } + } + assert.True(t, foundPlatform, "Expected to find platform %s but did not:\nexpected:\n%v\nreturned\n%v", rp.ID, platforms, returnedPlatforms) + } + }) + + t.Run("returns pages of platforms", func(t *testing.T) { + numPages := 2 + pageSize := numPlatforms / numPages + + for page := 0; page < numPages; page++ { + testName := fmt.Sprintf("returns page %d of platforms", page) + t.Run(testName, func(t *testing.T) { + returnedPlatforms, err := client.RegistryProviderPlatforms.List(ctx, versionID, &RegistryProviderPlatformListOptions{ + ListOptions: ListOptions{ + PageNumber: page, + PageSize: pageSize, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, returnedPlatforms.Items) + assert.Equal(t, numPlatforms, returnedPlatforms.TotalCount) + assert.Equal(t, numPages, returnedPlatforms.TotalPages) + assert.Equal(t, pageSize, len(returnedPlatforms.Items)) + for _, rp := range returnedPlatforms.Items { + foundPlatform := false + for _, p := range platforms { + if rp.ID == p.ID { + foundPlatform = true + break + } + } + assert.True(t, foundPlatform, "Expected to find platform %s but did not:\nexpected:\n%v\nreturned\n%v", rp.ID, platforms, returnedPlatforms) + } + }) + } + }) + }) + + t.Run("without platforms", func(t *testing.T) { + provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) + defer providerCleanup() + + version, versionCleanup := createRegistryProviderVersion(t, client, provider) + defer versionCleanup() + + versionID := RegistryProviderVersionID{ + RegistryProviderID: RegistryProviderID{ + OrganizationName: provider.Organization.Name, + Namespace: provider.Namespace, + Name: provider.Name, + RegistryName: provider.RegistryName, + }, + Version: version.Version, + } + platforms, err := client.RegistryProviderPlatforms.List(ctx, versionID, nil) + require.NoError(t, err) + assert.Empty(t, platforms.Items) + assert.Equal(t, 0, platforms.TotalCount) + assert.Equal(t, 0, platforms.TotalPages) + }) +} diff --git a/registry_provider_version.go b/registry_provider_version.go index a11230b24..f84554854 100644 --- a/registry_provider_version.go +++ b/registry_provider_version.go @@ -21,28 +21,28 @@ type RegistryProviderVersions interface { Create(ctx context.Context, providerID RegistryProviderID, options RegistryProviderVersionCreateOptions) (*RegistryProviderVersion, error) // Read a registry provider version. - Read(ctx context.Context, versionID RegistryProviderVersionID, options *RegistryProviderVersionReadOptions) (*RegistryProviderVersion, error) + Read(ctx context.Context, versionID RegistryProviderVersionID) (*RegistryProviderVersion, error) // Delete a registry provider version. Delete(ctx context.Context, versionID RegistryProviderVersionID) error } -// registryProviders implements RegistryProviders. +// registryProvidersVersions implements RegistryProvidersVersions type registryProviderVersions struct { client *Client } // RegistryProviderVersion represents a registry provider version type RegistryProviderVersion struct { - ID string `jsonapi:"primary,registry-provider-versions"` - Version string `jsonapi:"attr,version"` - CreatedAt string `jsonapi:"attr,created-at"` - UpdatedAt string `jsonapi:"attr,updated-at"` - KeyID string `jsonapi:"attr,key-id"` - Protocols []string `jsonapi:"attr,protocols,omitempty"` - Permissions *RegistryProviderPermissions `jsonapi:"attr,permissions"` - ShasumsUploaded bool `jsonapi:"attr,shasums-uploaded"` - ShasumsSigUploaded bool `jsonapi:"attr,sasums-sig-uploaded"` + ID string `jsonapi:"primary,registry-provider-versions"` + Version string `jsonapi:"attr,version"` + CreatedAt string `jsonapi:"attr,created-at,iso8601"` + UpdatedAt string `jsonapi:"attr,updated-at,iso8601"` + KeyID string `jsonapi:"attr,key-id"` + Protocols []string `jsonapi:"attr,protocols"` + Permissions RegistryProviderVersionPermissions `jsonapi:"attr,permissions"` + ShasumsUploaded bool `jsonapi:"attr,shasums-uploaded"` + ShasumsSigUploaded bool `jsonapi:"attr,shasums-sig-uploaded"` // Relations RegistryProvider *RegistryProvider `jsonapi:"relation,registry-provider"` @@ -55,7 +55,12 @@ type RegistryProviderVersion struct { // RegistryProviderVersionID is the multi key ID for addressing a version provider type RegistryProviderVersionID struct { RegistryProviderID - Version string `jsonapi:"attr,version"` + Version string +} + +type RegistryProviderVersionPermissions struct { + CanDelete bool `jsonapi:"attr,can-delete"` + CanUploadAsset bool `jsonapi:"attr,can-upload-asset"` } type RegistryProviderVersionList struct { @@ -67,11 +72,14 @@ type RegistryProviderVersionListOptions struct { ListOptions } -type RegistryProviderVersionReadOptions struct{} - type RegistryProviderVersionCreateOptions struct { + // Required: A valid semver version string. Version string `jsonapi:"attr,version"` + + // Required: A valid gpg-key string. KeyID string `jsonapi:"attr,key-id"` + + // Required: An array of Terraform provider API versions that this version supports. Protocols []string `jsonapi:"attr,protocols"` } @@ -80,10 +88,8 @@ func (r *registryProviderVersions) List(ctx context.Context, providerID Registry if err := providerID.valid(); err != nil { return nil, err } - if options != nil { - if err := options.valid(); err != nil { - return nil, err - } + if err := options.valid(); err != nil { + return nil, err } u := fmt.Sprintf( @@ -143,7 +149,7 @@ func (r *registryProviderVersions) Create(ctx context.Context, providerID Regist } // Read a registry provider version -func (r *registryProviderVersions) Read(ctx context.Context, versionID RegistryProviderVersionID, options *RegistryProviderVersionReadOptions) (*RegistryProviderVersion, error) { +func (r *registryProviderVersions) Read(ctx context.Context, versionID RegistryProviderVersionID) (*RegistryProviderVersion, error) { if err := versionID.valid(); err != nil { return nil, err } @@ -156,7 +162,7 @@ func (r *registryProviderVersions) Read(ctx context.Context, versionID RegistryP url.QueryEscape(versionID.Name), url.QueryEscape(versionID.Version), ) - req, err := r.client.newRequest("GET", u, options) + req, err := r.client.newRequest("GET", u, nil) if err != nil { return nil, err } @@ -192,7 +198,8 @@ func (r *registryProviderVersions) Delete(ctx context.Context, versionID Registr return r.client.do(ctx, req, nil) } -func (v RegistryProviderVersion) ShasumsUploadURL() (string, error) { +// ShasumsUploadURL returns the upload URL to upload shasums if one is available +func (v *RegistryProviderVersion) ShasumsUploadURL() (string, error) { uploadURL, ok := v.Links["shasums-upload"].(string) if !ok { return uploadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums upload link") @@ -203,7 +210,8 @@ func (v RegistryProviderVersion) ShasumsUploadURL() (string, error) { return uploadURL, nil } -func (v RegistryProviderVersion) ShasumsSigUploadURL() (string, error) { +// ShasumsSigUploadURL returns the URL to upload a shasums sig +func (v *RegistryProviderVersion) ShasumsSigUploadURL() (string, error) { uploadURL, ok := v.Links["shasums-sig-upload"].(string) if !ok { return uploadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums sig upload link") @@ -214,7 +222,8 @@ func (v RegistryProviderVersion) ShasumsSigUploadURL() (string, error) { return uploadURL, nil } -func (v RegistryProviderVersion) ShasumsDownloadURL() (string, error) { +// ShasumsDownloadURL returns the URL to download the shasums for the registry version +func (v *RegistryProviderVersion) ShasumsDownloadURL() (string, error) { downloadURL, ok := v.Links["shasums-download"].(string) if !ok { return downloadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums download link") @@ -225,7 +234,8 @@ func (v RegistryProviderVersion) ShasumsDownloadURL() (string, error) { return downloadURL, nil } -func (v RegistryProviderVersion) ShasumsSigDownloadURL() (string, error) { +// ShasumsSigDownloadURL returns the URL to download the shasums sig for the registry version +func (v *RegistryProviderVersion) ShasumsSigDownloadURL() (string, error) { downloadURL, ok := v.Links["shasums-sig-download"].(string) if !ok { return downloadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums sig download link") @@ -249,7 +259,7 @@ func (id RegistryProviderVersionID) valid() error { return nil } -func (o RegistryProviderVersionListOptions) valid() error { +func (o *RegistryProviderVersionListOptions) valid() error { return nil } diff --git a/registry_provider_version_integration_test.go b/registry_provider_version_integration_test.go index 964a27a40..8bd6f4266 100644 --- a/registry_provider_version_integration_test.go +++ b/registry_provider_version_integration_test.go @@ -89,7 +89,7 @@ func TestRegistryProviderVersionsCreate(t *testing.T) { client := testClient(t) ctx := context.Background() - providerTest, providerTestCleanup := createPrivateRegistryProvider(t, client, nil) + providerTest, providerTestCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) defer providerTestCleanup() providerId := RegistryProviderID{ @@ -202,7 +202,7 @@ func TestRegistryProviderVersionsList(t *testing.T) { ctx := context.Background() t.Run("with versions", func(t *testing.T) { - provider, providerCleanup := createPrivateRegistryProvider(t, client, nil) + provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) defer providerCleanup() createN := 10 @@ -214,7 +214,7 @@ func TestRegistryProviderVersionsList(t *testing.T) { } versionN := len(versions) - id := RegistryProviderID{ + providerID := RegistryProviderID{ OrganizationName: provider.Organization.Name, Namespace: provider.Namespace, Name: provider.Name, @@ -222,7 +222,7 @@ func TestRegistryProviderVersionsList(t *testing.T) { } t.Run("returns all versions", func(t *testing.T) { - returnedVersions, err := client.RegistryProviderVersions.List(ctx, id, &RegistryProviderVersionListOptions{ + returnedVersions, err := client.RegistryProviderVersions.List(ctx, providerID, &RegistryProviderVersionListOptions{ ListOptions: ListOptions{ PageNumber: 0, PageSize: versionN, @@ -251,7 +251,7 @@ func TestRegistryProviderVersionsList(t *testing.T) { for page := 0; page < pageN; page++ { testName := fmt.Sprintf("returns page %d of versions", page) t.Run(testName, func(t *testing.T) { - returnedVersions, err := client.RegistryProviderVersions.List(ctx, id, &RegistryProviderVersionListOptions{ + returnedVersions, err := client.RegistryProviderVersions.List(ctx, providerID, &RegistryProviderVersionListOptions{ ListOptions: ListOptions{ PageNumber: page, PageSize: pageSize, @@ -278,23 +278,24 @@ func TestRegistryProviderVersionsList(t *testing.T) { }) t.Run("without versions", func(t *testing.T) { - provider, providerCleanup := createPrivateRegistryProvider(t, client, nil) + provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) defer providerCleanup() - id := RegistryProviderID{ + providerID := RegistryProviderID{ OrganizationName: provider.Organization.Name, Namespace: provider.Namespace, Name: provider.Name, RegistryName: provider.RegistryName, } - versions, err := client.RegistryProviderVersions.List(ctx, id, nil) + versions, err := client.RegistryProviderVersions.List(ctx, providerID, nil) require.NoError(t, err) assert.Empty(t, versions.Items) assert.Equal(t, 0, versions.TotalCount) assert.Equal(t, 0, versions.TotalPages) }) + // TODO t.Run("with include provider platforms", func(t *testing.T) { }) } @@ -303,13 +304,13 @@ func TestRegistryProviderVersionsDelete(t *testing.T) { client := testClient(t) ctx := context.Background() - provider, providerCleanup := createPrivateRegistryProvider(t, client, nil) + provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) defer providerCleanup() t.Run("with valid version", func(t *testing.T) { version, _ := createRegistryProviderVersion(t, client, provider) - versionId := RegistryProviderVersionID{ + versionID := RegistryProviderVersionID{ RegistryProviderID: RegistryProviderID{ OrganizationName: version.RegistryProvider.Organization.Name, RegistryName: version.RegistryProvider.RegistryName, @@ -319,12 +320,12 @@ func TestRegistryProviderVersionsDelete(t *testing.T) { Version: version.Version, } - err := client.RegistryProviderVersions.Delete(ctx, versionId) + err := client.RegistryProviderVersions.Delete(ctx, versionID) assert.NoError(t, err) }) t.Run("with non existing version", func(t *testing.T) { - versionId := RegistryProviderVersionID{ + versionID := RegistryProviderVersionID{ RegistryProviderID: RegistryProviderID{ OrganizationName: provider.Organization.Name, RegistryName: provider.RegistryName, @@ -334,7 +335,7 @@ func TestRegistryProviderVersionsDelete(t *testing.T) { Version: "1.0.0", } - err := client.RegistryProviderVersions.Delete(ctx, versionId) + err := client.RegistryProviderVersions.Delete(ctx, versionID) assert.Error(t, err) }) } @@ -347,7 +348,7 @@ func TestRegistryProviderVersionsRead(t *testing.T) { version, versionCleanup := createRegistryProviderVersion(t, client, nil) defer versionCleanup() - versionId := RegistryProviderVersionID{ + versionID := RegistryProviderVersionID{ RegistryProviderID: RegistryProviderID{ OrganizationName: version.RegistryProvider.Organization.Name, RegistryName: version.RegistryProvider.RegistryName, @@ -357,7 +358,7 @@ func TestRegistryProviderVersionsRead(t *testing.T) { Version: version.Version, } - readVersion, err := client.RegistryProviderVersions.Read(ctx, versionId, nil) + readVersion, err := client.RegistryProviderVersions.Read(ctx, versionID) assert.NoError(t, err) assert.Equal(t, version.ID, readVersion.ID) assert.Equal(t, version.Version, readVersion.Version) @@ -385,10 +386,10 @@ func TestRegistryProviderVersionsRead(t *testing.T) { }) t.Run("with non existing version", func(t *testing.T) { - provider, providerCleanup := createPrivateRegistryProvider(t, client, nil) + provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry) defer providerCleanup() - versionId := RegistryProviderVersionID{ + versionID := RegistryProviderVersionID{ RegistryProviderID: RegistryProviderID{ OrganizationName: provider.Organization.Name, RegistryName: provider.RegistryName, @@ -398,7 +399,7 @@ func TestRegistryProviderVersionsRead(t *testing.T) { Version: "1.0.0", } - _, err := client.RegistryProviderVersions.Read(ctx, versionId, nil) + _, err := client.RegistryProviderVersions.Read(ctx, versionID) assert.Error(t, err) }) diff --git a/tfe.go b/tfe.go index 56f03f213..25941f3a5 100644 --- a/tfe.go +++ b/tfe.go @@ -129,6 +129,7 @@ type Client struct { PolicySets PolicySets RegistryModules RegistryModules RegistryProviders RegistryProviders + RegistryProviderPlatforms RegistryProviderPlatforms RegistryProviderVersions RegistryProviderVersions Runs Runs RunTasks RunTasks @@ -275,6 +276,7 @@ func NewClient(cfg *Config) (*Client, error) { client.PolicySets = &policySets{client: client} client.RegistryModules = ®istryModules{client: client} client.RegistryProviders = ®istryProviders{client: client} + client.RegistryProviderPlatforms = ®istryProviderPlatforms{client: client} client.RegistryProviderVersions = ®istryProviderVersions{client: client} client.Runs = &runs{client: client} client.RunTasks = &runTasks{client: client}