diff --git a/errors.go b/errors.go index 6698b2a22..231d1b8c3 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 fe4d0eac7..1a1341089 100644 --- a/helper_test.go +++ b/helper_test.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io/ioutil" + "math/rand" "os" "sync" "testing" @@ -717,23 +718,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) } @@ -751,6 +753,139 @@ 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() + + options := RegistryProviderCreateOptions{ + 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() { + 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) + } + + 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() + + options := RegistryProviderCreateOptions{ + 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() { + 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) + } + + if orgCleanup != nil { + orgCleanup() + } + } +} + +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() @@ -1271,6 +1406,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 new file mode 100644 index 000000000..83d2d8964 --- /dev/null +++ b/registry_provider.go @@ -0,0 +1,265 @@ +package tfe + +import ( + "context" + "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, providerID RegistryProviderID, options *RegistryProviderReadOptions) (*RegistryProvider, error) + + // Delete a registry provider. + Delete(ctx context.Context, providerID RegistryProviderID) 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" +) + +// 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"` + 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"` + + // Relations + 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,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"` + + // 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"` + + // Include related jsonapi relationships + Include *[]RegistryProviderIncludeOps `url:"include,omitempty"` +} + +type RegistryProviderList struct { + *Pagination + Items []*RegistryProvider +} + +// 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"` +} + +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 + } + 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"` + + 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 + } + + // 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), + ) + 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 +} + +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(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 + } + + 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, providerID RegistryProviderID) error { + if err := providerID.valid(); err != nil { + return err + } + + u := fmt.Sprintf( + "organizations/%s/registry-providers/%s/%s/%s", + 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 { + return err + } + + 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 new file mode 100644 index 000000000..6b87b8e44 --- /dev/null +++ b/registry_provider_integration_test.go @@ -0,0 +1,581 @@ +//go:build integration +// +build integration + +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++ { + // 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) + } + 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) { + returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{ + RegistryName: PublicRegistry, + 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, 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) + } + }) + + 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) + }) + + 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) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + t.Run("with valid options", func(t *testing.T) { + + publicProviderOptions := RegistryProviderCreateOptions{ + Name: "provider_name", + Namespace: "public_namespace", + RegistryName: PublicRegistry, + } + privateProviderOptions := RegistryProviderCreateOptions{ + Name: "provider_name", + Namespace: orgTest.Name, + RegistryName: PrivateRegistry, + } + + 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: "namespace", + RegistryName: PublicRegistry, + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidName.Error()) + }) + + t.Run("with an invalid name", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: "invalid name", + Namespace: "namespace", + RegistryName: PublicRegistry, + } + 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: "name", + RegistryName: PublicRegistry, + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidNamespace.Error()) + }) + + t.Run("with an invalid namespace", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: "name", + Namespace: "invalid namespace", + RegistryName: PublicRegistry, + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrInvalidNamespace.Error()) + }) + + t.Run("without a registry-name", func(t *testing.T) { + options := RegistryProviderCreateOptions{ + Name: "name", + Namespace: "namespace", + } + rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + 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: "name", + Namespace: "namespace", + RegistryName: PublicRegistry, + } + 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: "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) { + 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() + + 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) + 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) { + 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("populates version relationships", 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} + + id := RegistryProviderID{ + OrganizationName: provider.Organization.Name, + RegistryName: provider.RegistryName, + Namespace: provider.Namespace, + Name: provider.Name, + } + + 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) { + 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) + + 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, id, nil) + assert.Nil(t, prv) + assert.Error(t, err) + }) + + t.Run("when the registry provider does not exist", func(t *testing.T) { + 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) + }) + }) + } +} + +func TestRegistryProvidersIDValidation(t *testing.T) { + orgName := "orgName" + registryName := PublicRegistry + + 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 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 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 namespace", func(t *testing.T) { + id := RegistryProviderID{ + OrganizationName: orgName, + RegistryName: registryName, + Namespace: "", + Name: "name", + } + assert.EqualError(t, id.valid(), ErrInvalidNamespace.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_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..a11230b24 --- /dev/null +++ b/registry_provider_version.go @@ -0,0 +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,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 22512c0cf..5ec530dba 100644 --- a/tfe.go +++ b/tfe.go @@ -128,6 +128,8 @@ type Client struct { PolicySetVersions PolicySetVersions PolicySets PolicySets RegistryModules RegistryModules + RegistryProviders RegistryProviders + RegistryProviderVersions RegistryProviderVersions Runs Runs RunTasks RunTasks RunTriggers RunTriggers @@ -272,6 +274,8 @@ 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.RegistryProviderVersions = ®istryProviderVersions{client: client} client.Runs = &runs{client: client} client.RunTasks = &runTasks{client: client} client.RunTriggers = &runTriggers{client: client}