Skip to content

Commit

Permalink
Add support for plan exports
Browse files Browse the repository at this point in the history
  • Loading branch information
davidcelis committed Apr 26, 2019
1 parent 44f5072 commit 0c64a8f
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 0 deletions.
21 changes: 21 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,27 @@ func createAppliedRun(t *testing.T, client *Client, w *Workspace) (*Run, func())
}
}

func createPlanExport(t *testing.T, client *Client, plan *Plan) (*PlanExport, func()) {
var runCleanup func()
var run *Run

if plan == nil {
run, runCleanup = createPlannedRun(t, client, nil)
plan = run.Plan
}

ctx := context.Background()
pe, err := client.PlanExports.Create(ctx, PlanExportCreateOptions{
Plan: plan,
DataType: PlanExportType(PlanExportSentinelMockBundleV0),
})
if err != nil {
t.Fatal(err)
}

return pe, runCleanup
}

func createSSHKey(t *testing.T, client *Client, org *Organization) (*SSHKey, func()) {
var orgCleanup func()

Expand Down
3 changes: 3 additions & 0 deletions plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ type Plan struct {
ResourceDestructions int `jsonapi:"attr,resource-destructions"`
Status PlanStatus `jsonapi:"attr,status"`
StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"`

// Relations
Exports []*PlanExport `jsonapi:"relation,exports"`
}

// PlanStatusTimestamps holds the timestamps for individual plan statuses.
Expand Down
175 changes: 175 additions & 0 deletions plan_export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package tfe

import (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"time"
)

// Compile-time proof of interface implementation.
var _ PlanExports = (*planExports)(nil)

// PlanExports describes all the plan export related methods that the Terraform Enterprise
// API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/plan-exports.html
type PlanExports interface {
// Export a plan by its ID with the given options.
Create(ctx context.Context, options PlanExportCreateOptions) (*PlanExport, error)

// Read a plan export by its ID.
Read(ctx context.Context, planExportID string) (*PlanExport, error)

// Download the data of an plan export.
Download(ctx context.Context, planExportID string) ([]byte, error)

// Delete a plan export by its ID.
Delete(ctx context.Context, planExportID string) error
}

// planExports implements PlanExports.
type planExports struct {
client *Client
}

// PlanExportDataType represents the type of data exported from a plan.
type PlanExportDataType string

//List all available plan export data types
const (
PlanExportSentinelMockBundleV0 PlanExportDataType = "sentinel-mock-bundle-v0"
)

// PlanExportStatus represents a plan export state.
type PlanExportStatus string

//List all available plan export statuses.
const (
PlanExportCanceled PlanExportStatus = "canceled"
PlanExportErrored PlanExportStatus = "errored"
PlanExportExpired PlanExportStatus = "expired"
PlanExportFinished PlanExportStatus = "finished"
PlanExportPending PlanExportStatus = "pending"
PlanExportQueued PlanExportStatus = "queued"
)

// PlanExportStatusTimestamps holds the timestamps for individual plan export statuses.
type PlanExportStatusTimestamps struct {
CanceledAt time.Time `json:"canceled-at"`
ErroredAt time.Time `json:"errored-at"`
ExpiredAt time.Time `json:"expired-at"`
FinishedAt time.Time `json:"finished-at"`
QueuedAt time.Time `json:"queued-at"`
}

// PlanExport represents an export of Terraform Enterprise plan data.
type PlanExport struct {
ID string `jsonapi:"primary,plan-exports"`
DataType PlanExportDataType `jsonapi:"attr,data-type"`
Status PlanExportStatus `jsonapi:"attr,status"`
StatusTimestamps *PlanExportStatusTimestamps `jsonapi:"attr,status-timestamps"`
}

// PlanExportCreateOptions represents the options for exporting data from a plan.
type PlanExportCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,plan-exports"`

// The plan to export.
Plan *Plan `jsonapi:"relation,plan"`

// The name of the policy set.
DataType *PlanExportDataType `jsonapi:"attr,data-type"`
}

func (o PlanExportCreateOptions) valid() error {
if o.Plan == nil {
return errors.New("plan is required")
}
if o.DataType == nil {
return errors.New("data type is required")
}
return nil
}

func (s *planExports) Create(ctx context.Context, options PlanExportCreateOptions) (*PlanExport, error) {
if err := options.valid(); err != nil {
return nil, err
}

// Make sure we don't send a user provided ID.
options.ID = ""

req, err := s.client.newRequest("POST", "plan-exports", &options)
if err != nil {
return nil, err
}

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

return pe, err
}

// Read a plan export by its ID.
func (s *planExports) Read(ctx context.Context, planExportID string) (*PlanExport, error) {
if !validStringID(&planExportID) {
return nil, errors.New("invalid value for plan export ID")
}

u := fmt.Sprintf("plan-exports/%s", url.QueryEscape(planExportID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}

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

return pe, nil
}

// Download a plan export's data. Data is exported in a .tar.gz format.
func (s *planExports) Download(ctx context.Context, planExportID string) ([]byte, error) {
if !validStringID(&planExportID) {
return nil, errors.New("invalid value for plan export ID")
}

u := fmt.Sprintf("plan-exports/%s/download", url.QueryEscape(planExportID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}

var buf bytes.Buffer
err = s.client.do(ctx, req, &buf)
if err != nil {
return nil, err
}

return buf.Bytes(), nil
}

// Delete a plan export by ID.
func (s *planExports) Delete(ctx context.Context, planExportID string) error {
if !validStringID(&planExportID) {
return errors.New("invalid value for plan export ID")
}

u := fmt.Sprintf("plan-exports/%s", url.QueryEscape(planExportID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}

return s.client.do(ctx, req, nil)
}
106 changes: 106 additions & 0 deletions plan_export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package tfe

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
)

func TestPlanExportsCreate(t *testing.T) {
client := testClient(t)
ctx := context.Background()

rTest, rTestCleanup := createPlannedRun(t, client, nil)
defer rTestCleanup()

pTest, err := client.Plans.Read(ctx, rTest.Plan.ID)
assert.NoError(t, err)

t.Run("with valid options", func(t *testing.T) {
options := PlanExportCreateOptions{
Plan: pTest,
DataType: PlanExportType(PlanExportSentinelMockBundleV0),
}

pe, err := client.PlanExports.Create(ctx, options)
assert.NoError(t, err)
assert.NotEmpty(t, pe.ID)
assert.Equal(t, PlanExportSentinelMockBundleV0, pe.DataType)
})

t.Run("without a plan", func(t *testing.T) {
options := PlanExportCreateOptions{
Plan: nil,
DataType: PlanExportType(PlanExportSentinelMockBundleV0),
}

pe, err := client.PlanExports.Create(ctx, options)
assert.Nil(t, pe)
assert.EqualError(t, err, "plan is required")
})

t.Run("without a data type", func(t *testing.T) {
options := PlanExportCreateOptions{
Plan: pTest,
DataType: nil,
}

pe, err := client.PlanExports.Create(ctx, options)
assert.Nil(t, pe)
assert.EqualError(t, err, "data type is required")
})
}

func TestPlanExportsRead(t *testing.T) {
client := testClient(t)
ctx := context.Background()

peTest, peTestCleanup := createPlanExport(t, client, nil)
defer peTestCleanup()

t.Run("without a valid ID", func(t *testing.T) {
_, err := client.PlanExports.Read(ctx, badIdentifier)
assert.EqualError(t, err, "invalid value for plan export ID")
})

t.Run("with a valid ID", func(t *testing.T) {
pe, err := client.PlanExports.Read(ctx, peTest.ID)
assert.NoError(t, err)
assert.Equal(t, peTest.ID, pe.ID)
assert.Equal(t, peTest.DataType, pe.DataType)
})
}

func TestPlanExportsDownload(t *testing.T) {
client := testClient(t)
ctx := context.Background()

t.Run("without a valid ID", func(t *testing.T) {
_, err := client.PlanExports.Download(ctx, badIdentifier)
assert.EqualError(t, err, "invalid value for plan export ID")
})
}

func TestPlanExportsDelete(t *testing.T) {
client := testClient(t)
ctx := context.Background()

peTest, peTestCleanup := createPlanExport(t, client, nil)
defer peTestCleanup()

t.Run("when the export does not exist", func(t *testing.T) {
err := client.Policies.Delete(ctx, "pe-doesntexist")
assert.Equal(t, err, ErrResourceNotFound)
})

t.Run("without a valid ID", func(t *testing.T) {
err := client.PlanExports.Delete(ctx, badIdentifier)
assert.EqualError(t, err, "invalid value for plan export ID")
})

t.Run("with a valid ID", func(t *testing.T) {
err := client.PlanExports.Delete(ctx, peTest.ID)
assert.NoError(t, err)
})
}
2 changes: 2 additions & 0 deletions tfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ type Client struct {
Organizations Organizations
OrganizationTokens OrganizationTokens
Plans Plans
PlanExports PlanExports
Policies Policies
PolicyChecks PolicyChecks
PolicySets PolicySets
Expand Down Expand Up @@ -203,6 +204,7 @@ func NewClient(cfg *Config) (*Client, error) {
client.Organizations = &organizations{client: client}
client.OrganizationTokens = &organizationTokens{client: client}
client.Plans = &plans{client: client}
client.PlanExports = &planExports{client: client}
client.Policies = &policies{client: client}
client.PolicyChecks = &policyChecks{client: client}
client.PolicySets = &policySets{client: client}
Expand Down
5 changes: 5 additions & 0 deletions type_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ func NotificationDestination(v NotificationDestinationType) *NotificationDestina
return &v
}

// PlanExportType returns a pointer to the given plan export data type.
func PlanExportType(v PlanExportDataType) *PlanExportDataType {
return &v
}

// ServiceProvider returns a pointer to the given service provider type.
func ServiceProvider(v ServiceProviderType) *ServiceProviderType {
return &v
Expand Down

0 comments on commit 0c64a8f

Please sign in to comment.