Skip to content

Commit c20e8f8

Browse files
authored
feat: add spinnaker_pipeline_template_v2 resource (#13)
* feat: add spinnaker_pipeline_template_v2 resource 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. * fix: remove obsolete PipelineTemplateV2Variable fields
1 parent 3195eaa commit c20e8f8

File tree

10 files changed

+539
-3
lines changed

10 files changed

+539
-3
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "spinnaker_pipeline_template_v2 Resource - terraform-provider-spinnaker"
4+
subcategory: ""
5+
description: |-
6+
Provides a V2 pipeline template. See https://spinnaker.io/reference/pipeline/templates/ for more details.
7+
---
8+
9+
# spinnaker_pipeline_template_v2 (Resource)
10+
11+
Provides a V2 pipeline template. See https://spinnaker.io/reference/pipeline/templates/ for more details.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Required
19+
20+
- **template** (String) JSON schema of the V2 pipeline template.
21+
- **template_id** (String) ID of the template.
22+
23+
### Optional
24+
25+
- **id** (String) The ID of this resource.
26+
27+
### Read-Only
28+
29+
- **reference** (String) The URL for referencing the template in a pipeline instance.
30+
31+

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ module github.com/Bonial-International-GmbH/terraform-provider-spinnaker
33
go 1.16
44

55
require (
6+
github.com/antihax/optional v1.0.0
67
github.com/cenkalti/backoff/v4 v4.1.0
78
github.com/ghodss/yaml v1.0.0
9+
github.com/hashicorp/go-multierror v1.0.0
810
github.com/hashicorp/terraform-plugin-sdk v1.17.2
911
github.com/mitchellh/mapstructure v1.4.1
1012
github.com/spinnaker/spin v1.22.0
13+
github.com/stretchr/testify v1.7.0
1114
)

spinnaker/api/errors/errors.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package errors
22

3-
import "regexp"
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
"regexp"
48

5-
var (
6-
pipelineAlreadyExistsRegexp = regexp.MustCompile(`.*A pipeline with name .* already exists.*`)
9+
gateapi "github.com/spinnaker/spin/gateapi"
710
)
811

12+
var pipelineAlreadyExistsRegexp = regexp.MustCompile(`.*A pipeline with name .* already exists.*`)
13+
914
// IsPipelineAlreadyExists returns true if the error indicates that a pipeline
1015
// already exists.
1116
func IsPipelineAlreadyExists(err error) bool {
@@ -15,3 +20,60 @@ func IsPipelineAlreadyExists(err error) bool {
1520

1621
return pipelineAlreadyExistsRegexp.MatchString(err.Error())
1722
}
23+
24+
// IsNotFound returns true if err resembles an HTTP NotFound error.
25+
func IsNotFound(err error) bool {
26+
return HasCode(http.StatusNotFound, err)
27+
}
28+
29+
// HasCode returns true if err resembles an HTTP error with status code.
30+
func HasCode(code int, err error) bool {
31+
var respErr *ResponseError
32+
33+
if errors.As(err, &respErr) {
34+
return respErr.Code() == code
35+
}
36+
37+
return false
38+
}
39+
40+
// ResponseError wraps a (potentially nil) *http.Response and an error.
41+
type ResponseError struct {
42+
resp *http.Response
43+
err error
44+
}
45+
46+
// NewResponseError creates a new *ResponseError.
47+
func NewResponseError(resp *http.Response, err error) *ResponseError {
48+
return &ResponseError{
49+
resp: resp,
50+
err: err,
51+
}
52+
}
53+
54+
// Code returns the HTTP status code if the error includes an *http.Response.
55+
// Otherwise returns 0.
56+
func (e *ResponseError) Code() int {
57+
if e.resp != nil {
58+
return e.resp.StatusCode
59+
}
60+
61+
return 0
62+
}
63+
64+
// Error implements the error interface.
65+
func (e *ResponseError) Error() string {
66+
if e.resp == nil {
67+
return e.err.Error()
68+
}
69+
70+
code := e.Code()
71+
72+
var gateErr gateapi.GenericSwaggerError
73+
74+
if errors.As(e.err, &gateErr) {
75+
return fmt.Sprintf("%v, Code: %d, Body: %s", gateErr, code, string(gateErr.Body()))
76+
}
77+
78+
return fmt.Sprintf("%v, Code: %d", e.err, code)
79+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package errors
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestResponseError(t *testing.T) {
12+
err := errors.New("the-error")
13+
notFoundResp := &http.Response{StatusCode: http.StatusNotFound}
14+
15+
require.EqualError(t, NewResponseError(nil, err), "the-error")
16+
require.EqualError(t, NewResponseError(notFoundResp, err), "the-error, Code: 404")
17+
}
18+
19+
func TestIsNotFound(t *testing.T) {
20+
err := errors.New("the-error")
21+
respErr := NewResponseError(nil, err)
22+
notFoundErr := NewResponseError(&http.Response{StatusCode: http.StatusNotFound}, err)
23+
24+
require.False(t, IsNotFound(err))
25+
require.False(t, IsNotFound(respErr))
26+
require.True(t, IsNotFound(notFoundErr))
27+
}

spinnaker/api/models.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package api
2+
3+
// PipelineTemplateV2 defines the schema of the pipeline template JSON.
4+
type PipelineTemplateV2 struct {
5+
ID string `json:"id,omitempty"`
6+
Metadata PipelineTemplateV2Metadata `json:"metadata"`
7+
Pipeline interface{} `json:"pipeline"`
8+
Protect *bool `json:"protect,omitempty"`
9+
Schema string `json:"schema"`
10+
Variables []PipelineTemplateV2Variable `json:"variables,omitempty"`
11+
}
12+
13+
type PipelineTemplateV2Metadata struct {
14+
Name string `json:"name"`
15+
Description string `json:"description"`
16+
Owner *string `json:"owner,omitempty"`
17+
Scopes []string `json:"scopes,omitempty"`
18+
}
19+
20+
type PipelineTemplateV2Variable struct {
21+
Name string `json:"name"`
22+
Description *string `json:"description,omitempty"`
23+
DefaultValue interface{} `json:"defaultValue,omitempty"`
24+
Type string `json:"type"`
25+
}
26+
27+
type PipelineTemplateV2Version struct {
28+
ID string `json:"id"`
29+
Digest string `json:"digest"`
30+
Tag string `json:"tag"`
31+
}
File renamed without changes.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/Bonial-International-GmbH/terraform-provider-spinnaker/spinnaker/api/errors"
7+
"github.com/antihax/optional"
8+
"github.com/mitchellh/mapstructure"
9+
gate "github.com/spinnaker/spin/cmd/gateclient"
10+
gateapi "github.com/spinnaker/spin/gateapi"
11+
)
12+
13+
// CreatePipelineTemplateV2 creates a pipeline template.
14+
func CreatePipelineTemplateV2(client *gate.GatewayClient, template *PipelineTemplateV2) error {
15+
_, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
16+
return client.V2PipelineTemplatesControllerApi.CreateUsingPOST1(client.Context, template, nil)
17+
})
18+
if err != nil || (resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated) {
19+
return errors.NewResponseError(resp, err)
20+
}
21+
22+
return nil
23+
}
24+
25+
// GetPipelineTemplateV2 fetches the pipeline template with templateID.
26+
func GetPipelineTemplateV2(client *gate.GatewayClient, templateID string) (*PipelineTemplateV2, error) {
27+
payload, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
28+
return client.V2PipelineTemplatesControllerApi.GetUsingGET2(client.Context, templateID, nil)
29+
})
30+
if err != nil || resp.StatusCode != http.StatusOK {
31+
return nil, errors.NewResponseError(resp, err)
32+
}
33+
34+
var template PipelineTemplateV2
35+
36+
if err := mapstructure.Decode(payload, &template); err != nil {
37+
return nil, err
38+
}
39+
40+
return &template, nil
41+
}
42+
43+
// DeletePipelineTemplateV2 deletes the pipeline template with templateID.
44+
// Either digest or tag can be set on a delete request, but not both.
45+
func DeletePipelineTemplateV2(client *gate.GatewayClient, templateID, tag, digest string) error {
46+
opts := &gateapi.V2PipelineTemplatesControllerApiDeleteUsingDELETE1Opts{}
47+
if digest != "" {
48+
opts.Digest = optional.NewString(digest)
49+
} else if tag != "" {
50+
opts.Tag = optional.NewString(tag)
51+
}
52+
53+
_, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
54+
return client.V2PipelineTemplatesControllerApi.DeleteUsingDELETE1(client.Context, templateID, opts)
55+
})
56+
if err != nil || (resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent) {
57+
return errors.NewResponseError(resp, err)
58+
}
59+
60+
return nil
61+
}
62+
63+
// UpdatePipelineTemplateV2 updates the pipeline template with templateID with
64+
// the data in template.
65+
func UpdatePipelineTemplateV2(client *gate.GatewayClient, template *PipelineTemplateV2) error {
66+
_, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
67+
return client.V2PipelineTemplatesControllerApi.UpdateUsingPOST1(client.Context, template.ID, template, nil)
68+
})
69+
if err != nil || (resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated) {
70+
return errors.NewResponseError(resp, err)
71+
}
72+
73+
return nil
74+
}
75+
76+
// ListPipelineTemplateV2Versions lists versions of all available pipeline
77+
// templates. The resulting map is keyed by template ID.
78+
func ListPipelineTemplateV2Versions(client *gate.GatewayClient) (map[string][]*PipelineTemplateV2Version, error) {
79+
var payload interface{}
80+
81+
_, resp, err := retry(func() (map[string]interface{}, *http.Response, error) {
82+
v, resp, err := client.V2PipelineTemplatesControllerApi.ListVersionsUsingGET(client.Context, nil)
83+
payload = v
84+
return nil, resp, err
85+
})
86+
if err != nil || resp.StatusCode != http.StatusOK {
87+
return nil, errors.NewResponseError(resp, err)
88+
}
89+
90+
var versionMap map[string][]*PipelineTemplateV2Version
91+
92+
if err = mapstructure.Decode(payload, &versionMap); err != nil {
93+
return nil, err
94+
}
95+
96+
return versionMap, nil
97+
}

spinnaker/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func Provider() *schema.Provider {
4242
"spinnaker_pipeline": resourcePipeline(),
4343
"spinnaker_pipeline_template": resourcePipelineTemplate(),
4444
"spinnaker_pipeline_template_config": resourcePipelineTemplateConfig(),
45+
"spinnaker_pipeline_template_v2": resourcePipelineTemplateV2(),
4546
},
4647
DataSourcesMap: map[string]*schema.Resource{
4748
"spinnaker_pipeline": datasourcePipeline(),

0 commit comments

Comments
 (0)