Skip to content
Permalink
Browse files

deploy: Support invalidating a CloudFront CDN cache

  • Loading branch information...
vangent authored and bep committed May 1, 2019
1 parent 2838d58 commit f4956d9aae69b1cb5715114cf5242fd80a9cabc7
Showing with 93 additions and 25 deletions.
  1. +1 −0 commands/deploy.go
  2. +1 −0 commands/hugo.go
  3. +51 −0 deploy/cloudfront.go
  4. +32 −25 deploy/deploy.go
  5. +2 −0 deploy/deployConfig.go
  6. +5 −0 deploy/deployConfig_test.go
  7. +1 −0 go.mod
@@ -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
@@ -213,6 +213,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
"force",
"gc",
"i18n-warnings",
"invalidateCDN",
"layoutDir",
"logFile",
"maxDeletes",
@@ -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
}
@@ -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,32 +66,33 @@ 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
}

// Deploy deploys the site to a target.
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
}

@@ -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
@@ -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
@@ -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

0 comments on commit f4956d9

Please sign in to comment.
You can’t perform that action at this time.