diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b918f817..fc5d3adf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * Project names were being incorrectly validated as ID's @brandonc [#608](https://github.com/hashicorp/go-tfe/pull/608) ## Enhancements +* Adds `List()` method to `GPGKeys` interface by @sebasslash [#602](https://github.com/hashicorp/go-tfe/pull/602) +* Adds `ProviderBinaryUploaded` field to `RegistryPlatforms` struct by @sebasslash [#602](https://github.com/hashicorp/go-tfe/pull/602) # v1.15.0 diff --git a/gpg_key.go b/gpg_key.go index 656074aee..c4f00d87b 100644 --- a/gpg_key.go +++ b/gpg_key.go @@ -15,6 +15,9 @@ var _ GPGKeys = (*gpgKeys)(nil) // // TFE API Docs: https://www.terraform.io/cloud-docs/api-docs/private-registry/gpg-keys type GPGKeys interface { + // Lists GPG keys in a private registry. + ListPrivate(ctx context.Context, options GPGKeyListOptions) (*GPGKeyList, error) + // Uploads a GPG Key to a private registry scoped with a namespace. Create(ctx context.Context, registryName RegistryName, options GPGKeyCreateOptions) (*GPGKey, error) @@ -33,6 +36,12 @@ type gpgKeys struct { client *Client } +// GPGKeyList represents a list of GPG keys. +type GPGKeyList struct { + *Pagination + Items []*GPGKey +} + // GPGKey represents a signed GPG key for a TFC/E private provider. type GPGKey struct { ID string `jsonapi:"primary,gpg-keys"` @@ -53,6 +62,14 @@ type GPGKeyID struct { KeyID string } +// GPGKeyListOptions represents all the available options to list keys in a registry. +type GPGKeyListOptions struct { + ListOptions + + // Required: A list of one or more namespaces. Must be authorized TFC/E organization names. + Namespaces []string `url:"filter[namespace]"` +} + // GPGKeyCreateOptions represents all the available options used to create a GPG key. type GPGKeyCreateOptions struct { Type string `jsonapi:"primary,gpg-keys"` @@ -66,6 +83,27 @@ type GPGKeyUpdateOptions struct { Namespace string `jsonapi:"attr,namespace"` } +// ListPrivate lists the private registry GPG keys for specified namespaces. +func (s *gpgKeys) ListPrivate(ctx context.Context, options GPGKeyListOptions) (*GPGKeyList, error) { + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys", url.QueryEscape(string(PrivateRegistry))) + req, err := s.client.NewRequest("GET", u, &options) + if err != nil { + return nil, err + } + + keyl := &GPGKeyList{} + err = req.Do(ctx, keyl) + if err != nil { + return nil, err + } + + return keyl, nil +} + func (s *gpgKeys) Create(ctx context.Context, registryName RegistryName, options GPGKeyCreateOptions) (*GPGKey, error) { if err := options.valid(); err != nil { return nil, err @@ -179,6 +217,20 @@ func (o GPGKeyID) valid() error { return nil } +func (o *GPGKeyListOptions) valid() error { + if len(o.Namespaces) == 0 { + return ErrInvalidNamespace + } + + for _, namespace := range o.Namespaces { + if namespace == "" || !validString(&namespace) { + return ErrInvalidNamespace + } + } + + return nil +} + func (o GPGKeyCreateOptions) valid() error { if !validString(&o.Namespace) { return ErrInvalidNamespace diff --git a/gpg_key_integration_test.go b/gpg_key_integration_test.go index 838909e66..78299dbeb 100644 --- a/gpg_key_integration_test.go +++ b/gpg_key_integration_test.go @@ -8,6 +8,91 @@ import ( "github.com/stretchr/testify/require" ) +func TestGPGKeyList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + org1, org1Cleanup := createOrganization(t, client) + t.Cleanup(org1Cleanup) + + org2, org2Cleanup := createOrganization(t, client) + t.Cleanup(org2Cleanup) + + upgradeOrganizationSubscription(t, client, org1) + upgradeOrganizationSubscription(t, client, org2) + + provider1, provider1Cleanup := createRegistryProvider(t, client, org1, PrivateRegistry) + t.Cleanup(provider1Cleanup) + + provider2, provider2Cleanup := createRegistryProvider(t, client, org2, PrivateRegistry) + t.Cleanup(provider2Cleanup) + + gpgKey1, gpgKey1Cleanup := createGPGKey(t, client, org1, provider1) + t.Cleanup(gpgKey1Cleanup) + + gpgKey2, gpgKey2Cleanup := createGPGKey(t, client, org2, provider2) + t.Cleanup(gpgKey2Cleanup) + + t.Run("with single namespace", func(t *testing.T) { + opts := GPGKeyListOptions{ + Namespaces: []string{org1.Name}, + } + + keyl, err := client.GPGKeys.ListPrivate(ctx, opts) + require.NoError(t, err) + + require.Len(t, keyl.Items, 1) + assert.Equal(t, gpgKey1.ID, keyl.Items[0].ID) + assert.Equal(t, gpgKey1.KeyID, keyl.Items[0].KeyID) + }) + + t.Run("with multiple namespaces", func(t *testing.T) { + t.Skip("Skipping due to GPG Key API not returning keys for multiple namespaces") + + opts := GPGKeyListOptions{ + Namespaces: []string{org1.Name, org2.Name}, + } + + keyl, err := client.GPGKeys.ListPrivate(ctx, opts) + require.NoError(t, err) + + require.Len(t, keyl.Items, 2) + for i, key := range []*GPGKey{ + gpgKey1, + gpgKey2, + } { + assert.Equal(t, key.ID, keyl.Items[i].ID) + assert.Equal(t, key.KeyID, keyl.Items[i].KeyID) + } + }) + + t.Run("with list options", func(t *testing.T) { + opts := GPGKeyListOptions{ + Namespaces: []string{org1.Name}, + ListOptions: ListOptions{ + PageNumber: 999, + PageSize: 100, + }, + } + + keyl, err := client.GPGKeys.ListPrivate(ctx, opts) + require.NoError(t, err) + require.Empty(t, keyl.Items) + assert.Equal(t, 999, keyl.CurrentPage) + assert.Equal(t, 1, keyl.TotalCount) + }) + + t.Run("with invalid options", func(t *testing.T) { + t.Run("invalid namespace", func(t *testing.T) { + opts := GPGKeyListOptions{ + Namespaces: []string{}, + } + _, err := client.GPGKeys.ListPrivate(ctx, opts) + require.EqualError(t, err, ErrInvalidNamespace.Error()) + }) + }) +} + func TestGPGKeyCreate(t *testing.T) { client := testClient(t) ctx := context.Background() diff --git a/mocks/gpg_key_mocks.go b/mocks/gpg_key_mocks.go index 34e7c2714..4cfa0e959 100644 --- a/mocks/gpg_key_mocks.go +++ b/mocks/gpg_key_mocks.go @@ -64,6 +64,21 @@ func (mr *MockGPGKeysMockRecorder) Delete(ctx, keyID interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockGPGKeys)(nil).Delete), ctx, keyID) } +// ListPrivate mocks base method. +func (m *MockGPGKeys) ListPrivate(ctx context.Context, options tfe.GPGKeyListOptions) (*tfe.GPGKeyList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPrivate", ctx, options) + ret0, _ := ret[0].(*tfe.GPGKeyList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPrivate indicates an expected call of ListPrivate. +func (mr *MockGPGKeysMockRecorder) ListPrivate(ctx, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPrivate", reflect.TypeOf((*MockGPGKeys)(nil).ListPrivate), ctx, options) +} + // Read mocks base method. func (m *MockGPGKeys) Read(ctx context.Context, keyID tfe.GPGKeyID) (*tfe.GPGKey, error) { m.ctrl.T.Helper() diff --git a/registry_provider_platform.go b/registry_provider_platform.go index c64ce84bc..1b67f1f61 100644 --- a/registry_provider_platform.go +++ b/registry_provider_platform.go @@ -33,11 +33,12 @@ type registryProviderPlatforms struct { // 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"` + 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"` + ProviderBinaryUploaded bool `jsonapi:"attr,provider-binary-uploaded"` // Relations RegistryProviderVersion *RegistryProviderVersion `jsonapi:"relation,registry-provider-version"` diff --git a/registry_provider_platform_integration_test.go b/registry_provider_platform_integration_test.go index 983fa2472..3f495efd3 100644 --- a/registry_provider_platform_integration_test.go +++ b/registry_provider_platform_integration_test.go @@ -43,6 +43,7 @@ func TestRegistryProviderPlatformsCreate(t *testing.T) { assert.Equal(t, options.Arch, rpp.Arch) assert.Equal(t, options.Shasum, rpp.Shasum) assert.Equal(t, options.Filename, rpp.Filename) + assert.False(t, rpp.ProviderBinaryUploaded) t.Run("relationships are properly decoded", func(t *testing.T) { assert.Equal(t, version.ID, rpp.RegistryProviderVersion.ID) @@ -245,6 +246,7 @@ func TestRegistryProviderPlatformsRead(t *testing.T) { assert.Equal(t, platformID.Arch, readPlatform.Arch) assert.Equal(t, platform.Filename, readPlatform.Filename) assert.Equal(t, platform.Shasum, readPlatform.Shasum) + assert.Equal(t, platform.ProviderBinaryUploaded, readPlatform.ProviderBinaryUploaded) t.Run("relationships are properly decoded", func(t *testing.T) { assert.Equal(t, platform.RegistryProviderVersion.ID, readPlatform.RegistryProviderVersion.ID)