Skip to content

Commit ed12ff3

Browse files
bengesoffwjam
andauthored
Updates needed to support Terraform TLS resources (#259)
* Change TLS certificate `name` attribute to be optional Custom TLS certificate without names default to the certificates common name or first SAN entry * Not use uint pointers in ListPrivateKeysInput struct This was causing problems when trying to use the page[number] parameter so I brought it more in line with the other methods. * Change TLS Activation `configuration` attribute to be optional (#28) Activations without the configuration specified default to the default configuration * Add TLS domains endpoint support * Drop use of reflection to build request parameters * Add documentation * TLS Subscriptions endpoints (#30) * Add ListTLSSubscriptions function * Add CreateTLSSubscriptions function * Add GetTLSSubscriptions function * Add DeleteTLSSubscriptions function * Add integration test with all endpoints * Add TLS Authorizations to TLS Subscriptions (#31) I had a lot of trouble deserialising the API response into appropriate structs. Initially I could only get it to work by deserialising into a map[string]interface{} but this didn't give the rich typing I wanted to return to the user. After a long time digging through the JSONAPI implementation and an associated PR to allow nested structs (google/jsonapi#99), I found that the go-fastly dependency was on an older version of the library before this PR came in. Updating the version, and playing around some more with the slice of structs instead of slice of pointers, finally got this to work. Furthermore I found that the Header in ListTLSSubscriptions needed to include the "Accept" header for "application/vnd.api+json" so I added it back in. I previously removed it as it didn't seem to do anything with the Filter and Pagination parameters, but it seems to be required for Include to work. * Avoid use pointers and uint in ListBulkCertificatesInput struct * Add allow_untrusted_root field to platform TLS * Get Certificate ID for TLS Subscriptions when available The certificate can't really be queried in the same way as the custom TLS certificate, but the ID will be present in the TLS activation automatically created for the TLS subscription, so can help filter for this activation. * Remove (TLS)\w* from field names of TLS structs * Add CommonName to Create, Get, and List TLSSubscription functions * Add "force" flag to TLS Subscription deletion Allows deleting a subscription with active domains; a potentially dangerous operation if the subscription handles production traffic. By using a flag, the user opts in to this risky behaviour and takes responsibility for knowing what they are doing. * go mod tidy * Add comments describing new structs and function * Document TLSAuthorizations struct Co-authored-by: Will May <will.j.may@gmail.com>
1 parent af10658 commit ed12ff3

19 files changed

Lines changed: 819 additions & 61 deletions

fastly/custom_tls_activation.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func (c *Client) GetTLSActivation(i *GetTLSActivationInput) (*TLSActivation, err
129129
type CreateTLSActivationInput struct {
130130
ID string `jsonapi:"primary,tls_activation"` // ID value does not need to be set.
131131
Certificate *CustomTLSCertificate `jsonapi:"relation,tls_certificate"` // Only ID of CustomTLSCertificate needs to be set.
132-
Configuration *TLSConfiguration `jsonapi:"relation,tls_configuration"`
132+
Configuration *TLSConfiguration `jsonapi:"relation,tls_configuration,omitempty"`
133133
Domain *TLSDomain `jsonapi:"relation,tls_domain"`
134134
}
135135

@@ -138,9 +138,6 @@ func (c *Client) CreateTLSActivation(i *CreateTLSActivationInput) (*TLSActivatio
138138
if i.Certificate == nil {
139139
return nil, ErrMissingTLSCertificate
140140
}
141-
if i.Configuration == nil {
142-
return nil, ErrMissingTLSConfiguration
143-
}
144141
if i.Domain == nil {
145142
return nil, ErrMissingTLSDomain
146143
}

fastly/custom_tls_activation_test.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,6 @@ func TestClient_CreateTLSActivation_validation(t *testing.T) {
125125
t.Errorf("bad error: %s", err)
126126
}
127127

128-
_, err = testClient.CreateTLSActivation(&CreateTLSActivationInput{
129-
Certificate: &CustomTLSCertificate{ID: "CERTIFICATE_ID"},
130-
Domain: &TLSDomain{ID: "DOMAIN_NAME"},
131-
})
132-
if err != ErrMissingTLSConfiguration {
133-
t.Errorf("bad error: %s", err)
134-
}
135-
136128
_, err = testClient.CreateTLSActivation(&CreateTLSActivationInput{
137129
Certificate: &CustomTLSCertificate{ID: "CERTIFICATE_ID"},
138130
Configuration: &TLSConfiguration{ID: "CONFIGURATION_ID"},

fastly/custom_tls_certificate.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ type CustomTLSCertificate struct {
2020
Replace bool `jsonapi:"attr,replace"`
2121
SerialNumber string `jsonapi:"attr,serial_number"`
2222
SignatureAlgorithm string `jsonapi:"attr,signature_algorithm"`
23-
TLSDomains []*TLSDomain `jsonapi:"relation,tls_domains"`
23+
Domains []*TLSDomain `jsonapi:"relation,tls_domains"`
2424
CreatedAt *time.Time `jsonapi:"attr,created_at,iso8601"`
2525
UpdatedAt *time.Time `jsonapi:"attr,updated_at,iso8601"`
2626
}
2727

28-
// ListCustomTLSCertificatesInput is used as input to the ListCustomTLSCertificatesInput function.
28+
// ListCustomTLSCertificatesInput is used as input to the Client.ListCustomTLSCertificates function.
2929
type ListCustomTLSCertificatesInput struct {
3030
FilterNotAfter string // Limit the returned certificates to those that expire prior to the specified date in UTC. Accepts parameters: lte (e.g., filter[not_after][lte]=2020-05-05).
3131
FilterTLSDomainsID string // Limit the returned certificates to those that include the specific domain.
@@ -124,17 +124,14 @@ func (c *Client) GetCustomTLSCertificate(i *GetCustomTLSCertificateInput) (*Cust
124124
type CreateCustomTLSCertificateInput struct {
125125
ID string `jsonapi:"primary,tls_certificate"` // ID value does not need to be set.
126126
CertBlob string `jsonapi:"attr,cert_blob"`
127-
Name string `jsonapi:"attr,name"`
127+
Name string `jsonapi:"attr,name,omitempty"`
128128
}
129129

130130
// CreateCustomTLSCertificate creates a custom TLS certificate.
131131
func (c *Client) CreateCustomTLSCertificate(i *CreateCustomTLSCertificateInput) (*CustomTLSCertificate, error) {
132132
if i.CertBlob == "" {
133133
return nil, ErrMissingCertBlob
134134
}
135-
if i.Name == "" {
136-
return nil, ErrMissingName
137-
}
138135

139136
p := "/tls/certificates"
140137

@@ -155,7 +152,7 @@ func (c *Client) CreateCustomTLSCertificate(i *CreateCustomTLSCertificateInput)
155152
type UpdateCustomTLSCertificateInput struct {
156153
ID string `jsonapi:"primary,tls_certificate"`
157154
CertBlob string `jsonapi:"attr,cert_blob"`
158-
Name string `jsonapi:"attr,name"`
155+
Name string `jsonapi:"attr,name,omitempty"`
159156
}
160157

161158
// UpdateCustomTLSCertificate replace a certificate with a newly reissued certificate.
@@ -171,10 +168,6 @@ func (c *Client) UpdateCustomTLSCertificate(i *UpdateCustomTLSCertificateInput)
171168
return nil, ErrMissingCertBlob
172169
}
173170

174-
if i.Name == "" {
175-
return nil, ErrMissingName
176-
}
177-
178171
path := fmt.Sprintf("/tls/certificates/%s", i.ID)
179172
resp, err := c.PatchJSONAPI(path, i, nil)
180173
if err != nil {

fastly/custom_tls_certificate_test.go

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ func TestClient_CustomTLSCertificate(t *testing.T) {
5656
if cc.ID != gcc.ID {
5757
t.Errorf("bad ID: %q (%q)", cc.ID, gcc.ID)
5858
}
59-
if gcc.TLSDomains == nil {
60-
t.Errorf("TLSDomains should not be nil: %v", cc.TLSDomains)
59+
if gcc.Domains == nil {
60+
t.Errorf("Domains should not be nil: %v", cc.Domains)
6161
}
62-
if len(gcc.TLSDomains) < 1 {
63-
t.Errorf("TLSDomains should not be an empty slice: %v", cc.TLSDomains)
62+
if len(gcc.Domains) < 1 {
63+
t.Errorf("Domains should not be an empty slice: %v", cc.Domains)
6464
}
65-
if cc.TLSDomains[0].ID != gcc.TLSDomains[0].ID {
66-
t.Errorf("bad Domain ID: %q (%q)", cc.TLSDomains[0].ID, gcc.TLSDomains[0].ID)
65+
if cc.Domains[0].ID != gcc.Domains[0].ID {
66+
t.Errorf("bad Domain ID: %q (%q)", cc.Domains[0].ID, gcc.Domains[0].ID)
6767
}
6868

6969
// Update
@@ -107,13 +107,6 @@ func TestClient_CreateCustomTLSCertificate_validation(t *testing.T) {
107107
t.Fatal(err)
108108
}
109109

110-
_, err = testClient.CreateCustomTLSCertificate(&CreateCustomTLSCertificateInput{
111-
CertBlob: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
112-
})
113-
if err != ErrMissingName {
114-
t.Errorf("bad error: %s", err)
115-
}
116-
117110
_, err = testClient.CreateCustomTLSCertificate(&CreateCustomTLSCertificateInput{
118111
Name: "My certificate",
119112
})
@@ -202,12 +195,4 @@ func TestClient_UpdateCustomTLSCertificate_validation(t *testing.T) {
202195
if err != ErrMissingCertBlob {
203196
t.Errorf("bad error: %s", err)
204197
}
205-
206-
_, err = testClient.UpdateCustomTLSCertificate(&UpdateCustomTLSCertificateInput{
207-
ID: "CERTIFICATE_ID",
208-
CertBlob: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
209-
})
210-
if err != ErrMissingName {
211-
t.Errorf("bad error: %s", err)
212-
}
213198
}

fastly/custom_tls_domain.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package fastly
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strconv"
7+
8+
"github.com/google/jsonapi"
9+
)
10+
11+
// ListTLSDomainsInput is used as input to Client.ListTLSDomains.
12+
type ListTLSDomainsInput struct {
13+
// Limit the returned domains to those currently using Fastly to terminate TLS with SNI (that is, domains considered "in use")
14+
FilterInUse *bool
15+
// Limit the returned domains to those listed in the given TLS certificate's SAN list
16+
FilterTLSCertificateID string
17+
// Limit the returned domains to those for a given TLS subscription
18+
FilterTLSSubscriptionID string
19+
// Include related objects
20+
Include string
21+
// Current page
22+
PageNumber int
23+
// Number of records per page
24+
PageSize int
25+
// The order in which to list the results by creation date
26+
Sort string
27+
}
28+
29+
// formatFilters converts user input into query parameters for filtering.
30+
func (l *ListTLSDomainsInput) formatFilters() map[string]string {
31+
result := map[string]string{}
32+
pairings := map[string]interface{}{
33+
"filter[in_use]": l.FilterInUse,
34+
"filter[tls_certificate.id]": l.FilterTLSCertificateID,
35+
"filter[tls_subscriptions.id]": l.FilterTLSSubscriptionID,
36+
"include": l.Include,
37+
"page[number]": l.PageNumber,
38+
"page[size]": l.PageSize,
39+
"sort": l.Sort,
40+
}
41+
42+
for key, value := range pairings {
43+
switch t := value.(type) {
44+
case string:
45+
if t != "" {
46+
result[key] = t
47+
}
48+
case int:
49+
if t != 0 {
50+
result[key] = strconv.Itoa(t)
51+
}
52+
case *bool:
53+
if t != nil {
54+
result[key] = strconv.FormatBool(*t)
55+
}
56+
}
57+
}
58+
59+
return result
60+
}
61+
62+
// ListTLSDomains retrieves a page of TLS domains.
63+
func (c *Client) ListTLSDomains(i *ListTLSDomainsInput) ([]*TLSDomain, error) {
64+
p := "/tls/domains"
65+
filters := &RequestOptions{
66+
Params: i.formatFilters(),
67+
Headers: map[string]string{
68+
"Accept": "application/vnd.api+json", // this is required otherwise the filters don't work
69+
},
70+
}
71+
72+
r, err := c.Get(p, filters)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
data, err := jsonapi.UnmarshalManyPayload(r.Body, reflect.TypeOf(new(TLSDomain)))
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
a := make([]*TLSDomain, len(data))
83+
for i := range data {
84+
typed, ok := data[i].(*TLSDomain)
85+
if !ok {
86+
return nil, fmt.Errorf("unexpected response type: %T", data[i])
87+
}
88+
a[i] = typed
89+
}
90+
91+
return a, nil
92+
}

fastly/custom_tls_domain_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package fastly
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestClient_ListTLSDomains(t *testing.T) {
8+
t.Parallel()
9+
10+
fixtureBase := "custom_tls_domain/"
11+
12+
var err error
13+
14+
// List
15+
var ldom []*TLSDomain
16+
record(t, fixtureBase+"list", func(c *Client) {
17+
ldom, err = c.ListTLSDomains(&ListTLSDomainsInput{
18+
PageSize: 10,
19+
})
20+
})
21+
if err != nil {
22+
t.Fatal(err)
23+
}
24+
if len(ldom) < 1 {
25+
t.Errorf("bad tls domains: %v", ldom)
26+
}
27+
28+
}

fastly/errors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ var ErrMissingTLSConfiguration = NewFieldError("TLSConfiguration")
179179
// requires a "TLSDomain" key, but one was not set.
180180
var ErrMissingTLSDomain = NewFieldError("TLSDomain")
181181

182+
// ErrCommonNameNotInDomains is an error that is returned when an input struct
183+
// requires that the domain in "CommonName" is also in "Domains"
184+
var ErrCommonNameNotInDomains = NewFieldError("CommonName").Message("CommonName must be in Domains")
185+
182186
// ErrMissingTo is an error that is returned when an input struct
183187
// requires a "To" key, but one was not set.
184188
var ErrMissingTo = NewFieldError("To")
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
version: 1
3+
interactions:
4+
- request:
5+
body: ""
6+
form: {}
7+
headers:
8+
Accept:
9+
- application/vnd.api+json
10+
User-Agent:
11+
- FastlyGo/2.1.0 (+github.com/fastly/go-fastly; go1.15.4)
12+
url: https://api.fastly.com/tls/domains?page%5Bsize%5D=10
13+
method: GET
14+
response:
15+
body: '{"data":[{"id":"a-real-tld.example","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"6RltCYkOfFfzPVitOyLCnV","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}},{"id":"along-with.localhost","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"64JrNBzdRRzkefaHXLvg3e","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}},{"id":"also-real.invalid","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"2TMKp6R51PW2VMJKiWW3eO","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}},{"id":"and-this-is-real.test","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"1f44cer2CdblNR2WVgfGnU","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}},{"id":"dummy.test","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"1jBkBddyG0bcMlaLp6i1Ur","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}},{"id":"first.example","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"5tjYkVLnZKc4yscdDQ4lzz","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}},{"id":"hello-world.example","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"4RWSfyNCwPAfUIWqtTKg2N","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}},{"id":"pagination-pad1.test","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"7d34I8GLEja5zVq4gCq294","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}},{"id":"pagination-pad2.test","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"77MQa3kQ1Ke3BodIuEdr3Y","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}},{"id":"pagination-pad3.test","type":"tls_domain","relationships":{"tls_activations":{"data":[]},"tls_certificates":{"data":[{"id":"7eQZD0jAsebDtWAWphdu3p","type":"tls_certificate"}]},"tls_subscriptions":{"data":[]}}}],"links":{"self":"https://api.fastly.com/tls/domains?page%5Bnumber%5D=1\u0026page%5Bsize%5D=10","first":"https://api.fastly.com/tls/domains?page%5Bnumber%5D=1\u0026page%5Bsize%5D=10","prev":null,"next":"https://api.fastly.com/tls/domains?page%5Bnumber%5D=2\u0026page%5Bsize%5D=10","last":"https://api.fastly.com/tls/domains?page%5Bnumber%5D=2\u0026page%5Bsize%5D=10"},"meta":{"record_count":12,"current_page":1,"per_page":10,"total_pages":2}}'
16+
headers:
17+
Accept-Ranges:
18+
- bytes
19+
Content-Length:
20+
- "2599"
21+
Content-Type:
22+
- application/vnd.api+json
23+
Date:
24+
- Mon, 25 Jan 2021 15:02:43 GMT
25+
Status:
26+
- 200 OK
27+
Strict-Transport-Security:
28+
- max-age=31536000
29+
Via:
30+
- 1.1 varnish, 1.1 varnish
31+
X-Cache:
32+
- MISS, MISS
33+
X-Cache-Hits:
34+
- 0, 0
35+
X-Content-Type-Options:
36+
- nosniff
37+
X-Served-By:
38+
- cache-control-slwdc9035-CONTROL-SLWDC, cache-lcy19235-LCY
39+
X-Timer:
40+
- S1611586963.753130,VS0,VE572
41+
status: 200 OK
42+
code: 200
43+
duration: ""
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
version: 1
3+
interactions:
4+
- request:
5+
body: ""
6+
form: {}
7+
headers:
8+
User-Agent:
9+
- FastlyGo/2.1.0 (+github.com/fastly/go-fastly; go1.15.6)
10+
url: https://api.fastly.com/tls/subscriptions/SUBSCRIPTION_ID
11+
method: DELETE
12+
response:
13+
body: ""
14+
headers:
15+
Accept-Ranges:
16+
- bytes
17+
Date:
18+
- Mon, 25 Jan 2021 18:00:06 GMT
19+
Status:
20+
- 204 No Content
21+
Strict-Transport-Security:
22+
- max-age=31536000
23+
Via:
24+
- 1.1 varnish, 1.1 varnish
25+
X-Cache:
26+
- MISS, MISS
27+
X-Cache-Hits:
28+
- 0, 0
29+
X-Content-Type-Options:
30+
- nosniff
31+
X-Served-By:
32+
- cache-control-slwdc9037-CONTROL-SLWDC, cache-lcy19251-LCY
33+
X-Timer:
34+
- S1611597606.927024,VS0,VE145
35+
status: 204 No Content
36+
code: 204
37+
duration: ""
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
version: 1
3+
interactions:
4+
- request:
5+
body: |
6+
{"data":{"type":"tls_subscription","relationships":{"tls_domain":{"data":[{"type":"tls_domain"}]}}},"included":[{"type":"tls_domain","attributes":{"type":""}}]}
7+
form: {}
8+
headers:
9+
Accept:
10+
- application/vnd.api+json
11+
Content-Type:
12+
- application/vnd.api+json
13+
User-Agent:
14+
- FastlyGo/2.1.0 (+github.com/fastly/go-fastly; go1.15.6)
15+
url: https://api.fastly.com/tls/subscriptions
16+
method: POST
17+
response:
18+
body: '{"data":{"id":"SUBSCRIPTION_ID","type":"tls_subscription","attributes":{"certificate_authority":"lets-encrypt","created_at":"2021-01-25T17:11:41.000Z","state":"pending","updated_at":"2021-01-25T17:11:41.000Z"},"relationships":{"tls_authorizations":{"data":[{"id":"AUTHORIZATION_ID","type":"tls_authorization"}]},"tls_certificates":{"data":[]},"tls_domains":{"data":[{"id":"DOMAIN_NAME","type":"tls_domain"}]},"common_name":{"data":{"id":"DOMAIN_NAME","type":"tls_domain"}},"tls_configuration":{"data":{"id":"CONFIGURATION_ID","type":"tls_configuration"}}}},"included":[{"id":"AUTHORIZATION_ID","type":"tls_authorization","attributes":{"challenges":[{"type":"managed-dns","record_type":"CNAME","record_name":"CNAME_CHALLENGE_DOMAIN_NAME","values":["CNAME_CHALLENGE_DOMAIN_NAME_TARGET"]},{"type":"managed-http-cname","record_type":"CNAME","record_name":"DOMAIN_NAME","values":["j.sni.global.fastly.net"]},{"type":"managed-http-a","record_type":"A","record_name":"DOMAIN_NAME","values":["151.101.2.132","151.101.66.132","151.101.130.132","151.101.194.132"]}],"created_at":"2021-01-25T17:11:41.000Z","state":"pending","updated_at":"2021-01-25T17:11:41.000Z","warnings":null},"relationships":{"tls_domain":{"data":{"id":"DOMAIN_NAME","type":"tls_domain"}}}}]}'
19+
headers:
20+
Accept-Ranges:
21+
- bytes
22+
Content-Length:
23+
- "1322"
24+
Content-Type:
25+
- application/vnd.api+json
26+
Date:
27+
- Mon, 25 Jan 2021 17:11:41 GMT
28+
Status:
29+
- 201 Created
30+
Strict-Transport-Security:
31+
- max-age=31536000
32+
Via:
33+
- 1.1 varnish, 1.1 varnish
34+
X-Cache:
35+
- MISS, MISS
36+
X-Cache-Hits:
37+
- 0, 0
38+
X-Content-Type-Options:
39+
- nosniff
40+
X-Served-By:
41+
- cache-control-slwdc9035-CONTROL-SLWDC, cache-lcy19271-LCY
42+
X-Timer:
43+
- S1611594701.776296,VS0,VE800
44+
status: 201 Created
45+
code: 201
46+
duration: ""

0 commit comments

Comments
 (0)