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

View file

@ -68,6 +68,7 @@ func newDeployCmd() *deployCmd {
cc.cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
cc.cmd.Flags().Bool("dryRun", false, "dry run")
cc.cmd.Flags().Bool("force", false, "force upload of all files")
cc.cmd.Flags().Bool("invalidateCDN", true, "invalidate the CDN cache via the CloudFrontDistributionID listed in the deployment target")
cc.cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
return cc

View file

@ -213,6 +213,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
"force",
"gc",
"i18n-warnings",
"invalidateCDN",
"layoutDir",
"logFile",
"maxDeletes",

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)

1
go.mod
View file

@ -8,6 +8,7 @@ require (
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38
github.com/alecthomas/chroma v0.6.3
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect
github.com/aws/aws-sdk-go v1.16.23
github.com/bep/debounce v1.2.0
github.com/bep/gitmap v1.0.0
github.com/bep/go-tocss v0.6.0