diff --git a/CHANGELOG.md b/CHANGELOG.md index 332987eca..af579f58e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Bug Fixes ## Enhancements +* Adds `List()` method to `GPGKeys` interface by @sebasslash [#]() # v1.15.0 diff --git a/gpg_key.go b/gpg_key.go index 656074aee..b7987c9c8 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. + List(ctx context.Context, registryName RegistryName, 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,30 @@ type GPGKeyUpdateOptions struct { Namespace string `jsonapi:"attr,namespace"` } +func (s *gpgKeys) List(ctx context.Context, registryName RegistryName, options *GPGKeyListOptions) (*GPGKeyList, error) { + if registryName != PrivateRegistry { + return nil, ErrInvalidRegistryName + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys", url.QueryEscape(string(registryName))) + 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 +220,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..2237cc054 100644 --- a/gpg_key_integration_test.go +++ b/gpg_key_integration_test.go @@ -8,6 +8,98 @@ 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.List(ctx, PrivateRegistry, 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.List(ctx, PrivateRegistry, 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.List(ctx, PrivateRegistry, 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 registry name", func(t *testing.T) { + opts := &GPGKeyListOptions{ + Namespaces: []string{org1.Name}, + } + _, err := client.GPGKeys.List(ctx, PublicRegistry, opts) + require.EqualError(t, err, ErrInvalidRegistryName.Error()) + }) + t.Run("invalid namespace", func(t *testing.T) { + opts := &GPGKeyListOptions{ + Namespaces: []string{}, + } + _, err := client.GPGKeys.List(ctx, PrivateRegistry, opts) + require.EqualError(t, err, ErrInvalidNamespace.Error()) + }) + }) +} + func TestGPGKeyCreate(t *testing.T) { client := testClient(t) ctx := context.Background()