Skip to content

Commit

Permalink
Added support for GPG Key API
Browse files Browse the repository at this point in the history
  • Loading branch information
sebasslash committed Jun 27, 2022
1 parent 2cb5c65 commit 9be5c23
Show file tree
Hide file tree
Showing 9 changed files with 652 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

## Enhancements
* Adds `RetryServerErrors` field to the `Config` object by @sebasslash [#439](https://github.com/hashicorp/go-tfe/pull/439)
* Adds support for the GPG Keys API by @sebasslash [#429](https://github.com/hashicorp/go-tfe/pull/429)

# v1.3.0

Expand Down
6 changes: 6 additions & 0 deletions errors.go
Expand Up @@ -14,6 +14,10 @@ var (

// ErrMissingDirectory is returned when the path does not have an existing directory.
ErrMissingDirectory = errors.New("path needs to be an existing directory")

// ErrNamespaceNotAuthorized is returned when a user attempts to perform an action
// on a namespace (organization) they do not have access to.
ErrNamespaceNotAuthorized = errors.New("namespace not authorized")
)

// Options/fields that cannot be defined
Expand Down Expand Up @@ -288,4 +292,6 @@ var (
ErrRequiredShasum = errors.New("shasum is required")

ErrRequiredFilename = errors.New("filename is required")

ErrInvalidAsciiArmor = errors.New("ascii armor is invalid")
)
1 change: 1 addition & 0 deletions generate_mocks.sh
Expand Up @@ -20,6 +20,7 @@ mockgen -source=apply.go -destination=mocks/apply_mocks.go -package=mocks
mockgen -source=audit_trail.go -destination=mocks/audit_trail.go -package=mocks
mockgen -source=configuration_version.go -destination=mocks/configuration_version_mocks.go -package=mocks
mockgen -source=cost_estimate.go -destination=mocks/cost_estimate_mocks.go -package=mocks
mockgen -source=gpg_key.go -destination=mocks/gpg_key_mocks.go -package=mocks
mockgen -source=ip_ranges.go -destination=mocks/ip_ranges_mocks.go -package=mocks
mockgen -source=logreader.go -destination=mocks/logreader_mocks.go -package=mocks
mockgen -source=notification_configuration.go -destination=mocks/notification_configuration_mocks.go -package=mocks
Expand Down
200 changes: 200 additions & 0 deletions gpg_key.go
@@ -0,0 +1,200 @@
package tfe

import (
"context"
"fmt"
"net/url"
"strings"
"time"
)

// Compile-time proof of interface implementation
var _ GPGKeys = (*gpgKeys)(nil)

// GPGKeys describes all the GPG key related methods that the Terraform Private Registry API supports.
//
// TFE API Docs: https://www.terraform.io/cloud-docs/api-docs/private-registry/gpg-keys
type GPGKeys interface {
// Uploads a GPG Key to a private registry scoped with a namespace.
Create(ctx context.Context, registryName RegistryName, options GPGKeyCreateOptions) (*GPGKey, error)

// Read a GPG key.
Read(ctx context.Context, keyID GPGKeyID) (*GPGKey, error)

// Update a GPG key.
Update(ctx context.Context, keyID GPGKeyID, options GPGKeyUpdateOptions) (*GPGKey, error)

// Delete a GPG key.
Delete(ctx context.Context, keyID GPGKeyID) error
}

// gpgKeys implements GPGKeys
type gpgKeys struct {
client *Client
}

// GPGKey represents a signed GPG key for a TFC/E private provider.
type GPGKey struct {
ID string `jsonapi:"primary,gpg-keys"`
AsciiArmor string `jsonapi:"attr,ascii-armor"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
KeyID string `jsonapi:"attr,key-id"`
Namespace string `jsonapi:"attr,namespace"`
Source string `jsonapi:"attr,source"`
SourceURL *string `jsonapi:"attr,source-url"`
TrustSignature string `jsonapi:"attr,trust-signature"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
}

// GPGKeyID represents the set of identifiers used to fetch a GPG key.
type GPGKeyID struct {
RegistryName RegistryName
Namespace string
KeyID string
}

// GPGKeyCreateOptions represents all the available options used to create a GPG key.
type GPGKeyCreateOptions struct {
Type string `jsonapi:"primary,gpg-keys"`
Namespace string `jsonapi:"attr,namespace"`
AsciiArmor string `jsonapi:"attr,ascii-armor"`
}

// GPGKeyCreateOptions represents all the available options used to update a GPG key.
type GPGKeyUpdateOptions struct {
Type string `jsonapi:"primary,gpg-keys"`
Namespace string `jsonapi:"attr,namespace"`
}

func (s *gpgKeys) Create(ctx context.Context, registryName RegistryName, options GPGKeyCreateOptions) (*GPGKey, error) {
if err := options.valid(); err != nil {
return nil, err
}

if registryName != PrivateRegistry {
return nil, ErrInvalidRegistryName
}

u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys", url.QueryEscape(string(registryName)))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}

g := &GPGKey{}
err = s.client.do(ctx, req, g)
if err != nil {
return nil, err
}

return g, nil
}

func (s *gpgKeys) Read(ctx context.Context, keyID GPGKeyID) (*GPGKey, error) {
if err := keyID.valid(); err != nil {
return nil, err
}

u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys/%s/%s",
url.QueryEscape(string(keyID.RegistryName)),
url.QueryEscape(keyID.Namespace),
url.QueryEscape(keyID.KeyID),
)
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}

g := &GPGKey{}
err = s.client.do(ctx, req, g)
if err != nil {
return nil, err
}

return g, nil
}

func (s *gpgKeys) Update(ctx context.Context, keyID GPGKeyID, options GPGKeyUpdateOptions) (*GPGKey, error) {
if err := options.valid(); err != nil {
return nil, err
}

if err := keyID.valid(); err != nil {
return nil, err
}

u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys/%s/%s",
url.QueryEscape(string(keyID.RegistryName)),
url.QueryEscape(keyID.Namespace),
url.QueryEscape(keyID.KeyID),
)
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}

g := &GPGKey{}
err = s.client.do(ctx, req, g)
if err != nil {
if strings.Contains(err.Error(), "namespace not authorized") {
return nil, ErrNamespaceNotAuthorized
}
return nil, err
}

return g, nil
}

func (s *gpgKeys) Delete(ctx context.Context, keyID GPGKeyID) error {
if err := keyID.valid(); err != nil {
return err
}

u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys/%s/%s",
url.QueryEscape(string(keyID.RegistryName)),
url.QueryEscape(keyID.Namespace),
url.QueryEscape(keyID.KeyID),
)
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}

return s.client.do(ctx, req, nil)
}

func (o GPGKeyID) valid() error {
if o.RegistryName != PrivateRegistry {
return ErrInvalidRegistryName
}

if !validString(&o.Namespace) {
return ErrInvalidNamespace
}

if !validString(&o.KeyID) {
return ErrInvalidKeyID
}

return nil
}

func (o GPGKeyCreateOptions) valid() error {
if !validString(&o.Namespace) {
return ErrInvalidNamespace
}

if !validString(&o.AsciiArmor) {
return ErrInvalidAsciiArmor
}

return nil
}

func (o GPGKeyUpdateOptions) valid() error {
if !validString(&o.Namespace) {
return ErrInvalidNamespace
}

return nil
}

0 comments on commit 9be5c23

Please sign in to comment.