shithub: hugo

Download patch

ref: f4956d9aae69b1cb5715114cf5242fd80a9cabc7
parent: 2838d58b1daa0f6a337125c5a64d06215901c5d6
author: Robert van Gent <rvangent@google.com>
date: Wed May 1 09:25:06 EDT 2019

deploy: Support invalidating a CloudFront CDN cache

--- a/commands/deploy.go
+++ b/commands/deploy.go
@@ -68,6 +68,7 @@
 	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
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -213,6 +213,7 @@
 		"force",
 		"gc",
 		"i18n-warnings",
+		"invalidateCDN",
 		"layoutDir",
 		"logFile",
 		"maxDeletes",
--- /dev/null
+++ b/deploy/cloudfront.go
@@ -1,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
+}
--- a/deploy/deploy.go
+++ b/deploy/deploy.go
@@ -45,18 +45,19 @@
 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 @@
 	}
 
 	// 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 (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 @@
 		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
 }
 
--- a/deploy/deployConfig.go
+++ b/deploy/deployConfig.go
@@ -32,6 +32,8 @@
 type target struct {
 	Name string
 	URL  string
+
+	CloudFrontDistributionID string
 }
 
 // matcher represents configuration to be applied to files whose paths match
--- a/deploy/deployConfig_test.go
+++ b/deploy/deployConfig_test.go
@@ -32,9 +32,12 @@
 [[deployment.targets]]
 Name = "name1"
 URL = "url1"
+CloudFrontDistributionID = "cdn1"
+
 [[deployment.targets]]
 name = "name2"
 url = "url2"
+cloudfrontdistributionid = "cdn2"
 
 [[deployment.matchers]]
 Pattern = "^pattern1$"
@@ -59,8 +62,10 @@
 	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)
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@
 	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