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("confirm", false, "ask for confirmation before making changes to the target")
cc.cmd.Flags().Bool("dryRun", false, "dry run") cc.cmd.Flags().Bool("dryRun", false, "dry run")
cc.cmd.Flags().Bool("force", false, "force upload of all files") 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") cc.cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
return cc return cc

View file

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

View file

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

View file

@ -32,9 +32,12 @@ someOtherValue = "foo"
[[deployment.targets]] [[deployment.targets]]
Name = "name1" Name = "name1"
URL = "url1" URL = "url1"
CloudFrontDistributionID = "cdn1"
[[deployment.targets]] [[deployment.targets]]
name = "name2" name = "name2"
url = "url2" url = "url2"
cloudfrontdistributionid = "cdn2"
[[deployment.matchers]] [[deployment.matchers]]
Pattern = "^pattern1$" Pattern = "^pattern1$"
@ -59,8 +62,10 @@ content-type = "contenttype2"
assert.Equal(2, len(dcfg.Targets)) assert.Equal(2, len(dcfg.Targets))
assert.Equal("name1", dcfg.Targets[0].Name) assert.Equal("name1", dcfg.Targets[0].Name)
assert.Equal("url1", dcfg.Targets[0].URL) assert.Equal("url1", dcfg.Targets[0].URL)
assert.Equal("cdn1", dcfg.Targets[0].CloudFrontDistributionID)
assert.Equal("name2", dcfg.Targets[1].Name) assert.Equal("name2", dcfg.Targets[1].Name)
assert.Equal("url2", dcfg.Targets[1].URL) assert.Equal("url2", dcfg.Targets[1].URL)
assert.Equal("cdn2", dcfg.Targets[1].CloudFrontDistributionID)
assert.Equal(2, len(dcfg.Matchers)) assert.Equal(2, len(dcfg.Matchers))
assert.Equal("^pattern1$", dcfg.Matchers[0].Pattern) 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/assert v0.0.0-20170929043011-405dbfeb8e38
github.com/alecthomas/chroma v0.6.3 github.com/alecthomas/chroma v0.6.3
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect 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/debounce v1.2.0
github.com/bep/gitmap v1.0.0 github.com/bep/gitmap v1.0.0
github.com/bep/go-tocss v0.6.0 github.com/bep/go-tocss v0.6.0