deploy: Support invalidating a CloudFront CDN cache

This commit is contained in:
Robert van Gent 2019-05-01 13:25:06 -07:00 committed by Bjørn Erik Pedersen
parent 2838d58b1d
commit f4956d9aae
7 changed files with 93 additions and 25 deletions

51
deploy/cloudfront.go Normal file
View file

@ -0,0 +1,51 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package deploy
import (
"context"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudfront"
)
// InvalidateCloudFront invalidates the CloudFront cache for distributionID.
// It uses the default AWS credentials from the environment.
func InvalidateCloudFront(ctx context.Context, distributionID string) error {
// SharedConfigEnable enables loading "shared config (~/.aws/config) and
// shared credentials (~/.aws/credentials) files".
// See https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ for more
// details.
// This is the same codepath used by Go CDK when creating an s3 URL.
// TODO: Update this to a Go CDK helper once available
// (https://github.com/google/go-cloud/issues/2003).
sess, err := session.NewSessionWithOptions(session.Options{SharedConfigState: session.SharedConfigEnable})
if err != nil {
return err
}
req := &cloudfront.CreateInvalidationInput{
DistributionId: aws.String(distributionID),
InvalidationBatch: &cloudfront.InvalidationBatch{
CallerReference: aws.String(time.Now().Format("20060102150405")),
Paths: &cloudfront.Paths{
Items: []*string{aws.String("/*")},
Quantity: aws.Int64(1),
},
},
}
_, err = cloudfront.New(sess).CreateInvalidationWithContext(ctx, req)
return err
}

View file

@ -45,18 +45,19 @@ import (
type Deployer struct {
localFs afero.Fs
targetURL string // the Go Cloud blob URL to deploy to
matchers []*matcher // matchers to apply to uploaded files
quiet bool // true reduces STDOUT
confirm bool // true enables confirmation before making changes
dryRun bool // true skips conformations and prints changes instead of applying them
force bool // true forces upload of all files
maxDeletes int // caps the # of files to delete; -1 to disable
target *target // the target to deploy to
matchers []*matcher // matchers to apply to uploaded files
quiet bool // true reduces STDOUT
confirm bool // true enables confirmation before making changes
dryRun bool // true skips conformations and prints changes instead of applying them
force bool // true forces upload of all files
invalidateCDN bool // true enables invalidate CDN cache (if possible)
maxDeletes int // caps the # of files to delete; -1 to disable
}
// New constructs a new *Deployer.
func New(cfg config.Provider, localFs afero.Fs) (*Deployer, error) {
target := cfg.GetString("target")
targetName := cfg.GetString("target")
// Load the [deployment] section of the config.
dcfg, err := decodeConfig(cfg)
@ -65,24 +66,25 @@ func New(cfg config.Provider, localFs afero.Fs) (*Deployer, error) {
}
// Find the target to deploy to.
var targetURL string
var tgt *target
for _, t := range dcfg.Targets {
if t.Name == target {
targetURL = t.URL
if t.Name == targetName {
tgt = t
}
}
if targetURL == "" {
return nil, fmt.Errorf("deployment target %q not found", target)
if tgt == nil {
return nil, fmt.Errorf("deployment target %q not found", targetName)
}
return &Deployer{
localFs: localFs,
targetURL: targetURL,
matchers: dcfg.Matchers,
quiet: cfg.GetBool("quiet"),
confirm: cfg.GetBool("confirm"),
dryRun: cfg.GetBool("dryRun"),
force: cfg.GetBool("force"),
maxDeletes: cfg.GetInt("maxDeletes"),
localFs: localFs,
target: tgt,
matchers: dcfg.Matchers,
quiet: cfg.GetBool("quiet"),
confirm: cfg.GetBool("confirm"),
dryRun: cfg.GetBool("dryRun"),
force: cfg.GetBool("force"),
invalidateCDN: cfg.GetBool("invalidateCDN"),
maxDeletes: cfg.GetInt("maxDeletes"),
}, nil
}
@ -90,7 +92,7 @@ func New(cfg config.Provider, localFs afero.Fs) (*Deployer, error) {
func (d *Deployer) Deploy(ctx context.Context) error {
// TODO: This opens the root path in the bucket/container.
// Consider adding support for targeting a subdirectory.
bucket, err := blob.OpenBucket(ctx, d.targetURL)
bucket, err := blob.OpenBucket(ctx, d.target.URL)
if err != nil {
return err
}
@ -203,9 +205,14 @@ func (d *Deployer) Deploy(ctx context.Context) error {
jww.FEEDBACK.Println("Success!")
}
// TODO: Add support for CloudFront invalidation similar to s3deploy,
// and possibly similar functionality for other providers.
if d.invalidateCDN && d.target.CloudFrontDistributionID != "" {
jww.FEEDBACK.Println("Invalidating CloudFront CDN...")
if err := InvalidateCloudFront(ctx, d.target.CloudFrontDistributionID); err != nil {
jww.FEEDBACK.Printf("Failed to invalidate CloudFront CDN: %v\n", err)
return err
}
jww.FEEDBACK.Println("Success!")
}
return nil
}

View file

@ -32,6 +32,8 @@ type deployConfig struct {
type target struct {
Name string
URL string
CloudFrontDistributionID string
}
// matcher represents configuration to be applied to files whose paths match

View file

@ -32,9 +32,12 @@ someOtherValue = "foo"
[[deployment.targets]]
Name = "name1"
URL = "url1"
CloudFrontDistributionID = "cdn1"
[[deployment.targets]]
name = "name2"
url = "url2"
cloudfrontdistributionid = "cdn2"
[[deployment.matchers]]
Pattern = "^pattern1$"
@ -59,8 +62,10 @@ content-type = "contenttype2"
assert.Equal(2, len(dcfg.Targets))
assert.Equal("name1", dcfg.Targets[0].Name)
assert.Equal("url1", dcfg.Targets[0].URL)
assert.Equal("cdn1", dcfg.Targets[0].CloudFrontDistributionID)
assert.Equal("name2", dcfg.Targets[1].Name)
assert.Equal("url2", dcfg.Targets[1].URL)
assert.Equal("cdn2", dcfg.Targets[1].CloudFrontDistributionID)
assert.Equal(2, len(dcfg.Matchers))
assert.Equal("^pattern1$", dcfg.Matchers[0].Pattern)