Skip to content

Commit

Permalink
feat: add spinnaker_pipeline_template_v2 resource
Browse files Browse the repository at this point in the history
Notable changes:

- add `ResponseError` to make working with API errors easier
- add validation to the `template` field of the new
  `spinnaker_pipeline_template_v2` resource to catch common issues on
  `terraform plan`
- tests for error impl and parsing/validation logic

More tests require general changes to the way the API client is
instantiated and passed around. This is out of scope here and will be
addressed in a separate change.
  • Loading branch information
martinohmann committed May 27, 2021
1 parent 3195eaa commit dd6cd2d
Show file tree
Hide file tree
Showing 10 changed files with 541 additions and 3 deletions.
31 changes: 31 additions & 0 deletions docs/resources/pipeline_template_v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "spinnaker_pipeline_template_v2 Resource - terraform-provider-spinnaker"
subcategory: ""
description: |-
Provides a V2 pipeline template. See https://spinnaker.io/reference/pipeline/templates/ for more details.
---

# spinnaker_pipeline_template_v2 (Resource)

Provides a V2 pipeline template. See https://spinnaker.io/reference/pipeline/templates/ for more details.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- **template** (String) JSON schema of the V2 pipeline template.
- **template_id** (String) ID of the template.

### Optional

- **id** (String) The ID of this resource.

### Read-Only

- **reference** (String) The URL for referencing the template in a pipeline instance.


3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ module github.com/Bonial-International-GmbH/terraform-provider-spinnaker
go 1.16

require (
github.com/antihax/optional v1.0.0
github.com/cenkalti/backoff/v4 v4.1.0
github.com/ghodss/yaml v1.0.0
github.com/hashicorp/go-multierror v1.0.0
github.com/hashicorp/terraform-plugin-sdk v1.17.2
github.com/mitchellh/mapstructure v1.4.1
github.com/spinnaker/spin v1.22.0
github.com/stretchr/testify v1.7.0
)
68 changes: 65 additions & 3 deletions spinnaker/api/errors/errors.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package errors

import "regexp"
import (
"errors"
"fmt"
"net/http"
"regexp"

var (
pipelineAlreadyExistsRegexp = regexp.MustCompile(`.*A pipeline with name .* already exists.*`)
gateapi "github.com/spinnaker/spin/gateapi"
)

var pipelineAlreadyExistsRegexp = regexp.MustCompile(`.*A pipeline with name .* already exists.*`)

// IsPipelineAlreadyExists returns true if the error indicates that a pipeline
// already exists.
func IsPipelineAlreadyExists(err error) bool {
Expand All @@ -15,3 +20,60 @@ func IsPipelineAlreadyExists(err error) bool {

return pipelineAlreadyExistsRegexp.MatchString(err.Error())
}

// IsNotFound returns true if err resembles an HTTP NotFound error.
func IsNotFound(err error) bool {
return HasCode(http.StatusNotFound, err)
}

// HasCode returns true if err resembles an HTTP error with status code.
func HasCode(code int, err error) bool {
var respErr *ResponseError

if errors.As(err, &respErr) {
return respErr.Code() == code
}

return false
}

// ResponseError wraps a (potentially nil) *http.Response and an error.
type ResponseError struct {
resp *http.Response
err error
}

// NewResponseError creates a new *ResponseError.
func NewResponseError(resp *http.Response, err error) *ResponseError {
return &ResponseError{
resp: resp,
err: err,
}
}

// Code returns the HTTP status code if the error includes an *http.Response.
// Otherwise returns 0.
func (e *ResponseError) Code() int {
if e.resp != nil {
return e.resp.StatusCode
}

return 0
}

// Error implements the error interface.
func (e *ResponseError) Error() string {
if e.resp == nil {
return e.err.Error()
}

code := e.Code()

var gateErr gateapi.GenericSwaggerError

if errors.As(e.err, &gateErr) {
return fmt.Sprintf("%v, Code: %d, Body: %s", gateErr, code, string(gateErr.Body()))
}

return fmt.Sprintf("%v, Code: %d", e.err, code)
}
27 changes: 27 additions & 0 deletions spinnaker/api/errors/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package errors

import (
"errors"
"net/http"
"testing"

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

func TestResponseError(t *testing.T) {
err := errors.New("the-error")
notFoundResp := &http.Response{StatusCode: http.StatusNotFound}

require.EqualError(t, NewResponseError(nil, err), "the-error")
require.EqualError(t, NewResponseError(notFoundResp, err), "the-error, Code: 404")
}

func TestIsNotFound(t *testing.T) {
err := errors.New("the-error")
respErr := NewResponseError(nil, err)
notFoundErr := NewResponseError(&http.Response{StatusCode: http.StatusNotFound}, err)

require.False(t, IsNotFound(err))
require.False(t, IsNotFound(respErr))
require.True(t, IsNotFound(notFoundErr))
}
33 changes: 33 additions & 0 deletions spinnaker/api/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package api

// PipelineTemplateV2 defines the schema of the pipeline template JSON.
type PipelineTemplateV2 struct {
ID string `json:"id,omitempty"`
Metadata PipelineTemplateV2Metadata `json:"metadata"`
Pipeline interface{} `json:"pipeline"`
Protect *bool `json:"protect,omitempty"`
Schema string `json:"schema"`
Variables []PipelineTemplateV2Variable `json:"variables,omitempty"`
}

type PipelineTemplateV2Metadata struct {
Name string `json:"name"`
Description string `json:"description"`
Owner *string `json:"owner,omitempty"`
Scopes []string `json:"scopes,omitempty"`
}

type PipelineTemplateV2Variable struct {
Name string `json:"name"`
Description *string `json:"description,omitempty"`
DefaultValue interface{} `json:"defaultValue,omitempty"`
Type string `json:"type"`
Group *string `json:"group,omitempty"`
Example *string `json:"example,omitempty"`
}

type PipelineTemplateV2Version struct {
ID string `json:"id"`
Digest string `json:"digest"`
Tag string `json:"tag"`
}
File renamed without changes.
97 changes: 97 additions & 0 deletions spinnaker/api/pipeline_template_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package api

import (
"net/http"

"github.com/Bonial-International-GmbH/terraform-provider-spinnaker/spinnaker/api/errors"
"github.com/antihax/optional"
"github.com/mitchellh/mapstructure"
gate "github.com/spinnaker/spin/cmd/gateclient"
gateapi "github.com/spinnaker/spin/gateapi"
)

// CreatePipelineTemplateV2 creates a pipeline template.
func CreatePipelineTemplateV2(client *gate.GatewayClient, template *PipelineTemplateV2) error {
_, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
return client.V2PipelineTemplatesControllerApi.CreateUsingPOST1(client.Context, template, nil)
})
if err != nil || (resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated) {
return errors.NewResponseError(resp, err)
}

return nil
}

// GetPipelineTemplateV2 fetches the pipeline template with templateID.
func GetPipelineTemplateV2(client *gate.GatewayClient, templateID string) (*PipelineTemplateV2, error) {
payload, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
return client.V2PipelineTemplatesControllerApi.GetUsingGET2(client.Context, templateID, nil)
})
if err != nil || resp.StatusCode != http.StatusOK {
return nil, errors.NewResponseError(resp, err)
}

var template PipelineTemplateV2

if err := mapstructure.Decode(payload, &template); err != nil {
return nil, err
}

return &template, nil
}

// DeletePipelineTemplateV2 deletes the pipeline template with templateID.
// Either digest or tag can be set on a delete request, but not both.
func DeletePipelineTemplateV2(client *gate.GatewayClient, templateID, tag, digest string) error {
opts := &gateapi.V2PipelineTemplatesControllerApiDeleteUsingDELETE1Opts{}
if digest != "" {
opts.Digest = optional.NewString(digest)
} else if tag != "" {
opts.Tag = optional.NewString(tag)
}

_, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
return client.V2PipelineTemplatesControllerApi.DeleteUsingDELETE1(client.Context, templateID, opts)
})
if err != nil || (resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent) {
return errors.NewResponseError(resp, err)
}

return nil
}

// UpdatePipelineTemplateV2 updates the pipeline template with templateID with
// the data in template.
func UpdatePipelineTemplateV2(client *gate.GatewayClient, template *PipelineTemplateV2) error {
_, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
return client.V2PipelineTemplatesControllerApi.UpdateUsingPOST1(client.Context, template.ID, template, nil)
})
if err != nil || (resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated) {
return errors.NewResponseError(resp, err)
}

return nil
}

// ListPipelineTemplateV2Versions lists versions of all available pipeline
// templates. The resulting map is keyed by template ID.
func ListPipelineTemplateV2Versions(client *gate.GatewayClient) (map[string][]*PipelineTemplateV2Version, error) {
var payload interface{}

_, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
v, resp, err := client.V2PipelineTemplatesControllerApi.ListVersionsUsingGET(client.Context, nil)
payload = v
return nil, resp, err
})
if err != nil || resp.StatusCode != http.StatusOK {
return nil, errors.NewResponseError(resp, err)
}

var versionMap map[string][]*PipelineTemplateV2Version

if err = mapstructure.Decode(payload, &versionMap); err != nil {
return nil, err
}

return versionMap, nil
}
1 change: 1 addition & 0 deletions spinnaker/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func Provider() *schema.Provider {
"spinnaker_pipeline": resourcePipeline(),
"spinnaker_pipeline_template": resourcePipelineTemplate(),
"spinnaker_pipeline_template_config": resourcePipelineTemplateConfig(),
"spinnaker_pipeline_template_v2": resourcePipelineTemplateV2(),
},
DataSourcesMap: map[string]*schema.Resource{
"spinnaker_pipeline": datasourcePipeline(),
Expand Down
Loading

0 comments on commit dd6cd2d

Please sign in to comment.