deploy: Support invalidating a CloudFront CDN cache - hugo - [fork] hugo port for 9front
 (HTM) git clone git@git.drkhsh.at/hugo.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit f4956d9aae69b1cb5715114cf5242fd80a9cabc7
 (DIR) parent 2838d58b1daa0f6a337125c5a64d06215901c5d6
 (HTM) Author: Robert van Gent <rvangent@google.com>
       Date:   Wed,  1 May 2019 13:25:06 -0700
       
       deploy: Support invalidating a CloudFront CDN cache
       
       Diffstat:
         M commands/deploy.go                  |       1 +
         M commands/hugo.go                    |       1 +
         A deploy/cloudfront.go                |      51 +++++++++++++++++++++++++++++++
         M deploy/deploy.go                    |      57 +++++++++++++++++--------------
         M deploy/deployConfig.go              |       2 ++
         M deploy/deployConfig_test.go         |       5 +++++
         M go.mod                              |       1 +
       
       7 files changed, 93 insertions(+), 25 deletions(-)
       ---
 (DIR) diff --git a/commands/deploy.go b/commands/deploy.go
       @@ -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
 (DIR) diff --git a/commands/hugo.go b/commands/hugo.go
       @@ -213,6 +213,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
                        "force",
                        "gc",
                        "i18n-warnings",
       +                "invalidateCDN",
                        "layoutDir",
                        "logFile",
                        "maxDeletes",
 (DIR) diff --git a/deploy/cloudfront.go b/deploy/cloudfront.go
       @@ -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
       +}
 (DIR) diff --git a/deploy/deploy.go b/deploy/deploy.go
       @@ -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
        }
        
 (DIR) diff --git a/deploy/deployConfig.go b/deploy/deployConfig.go
       @@ -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
 (DIR) diff --git a/deploy/deployConfig_test.go b/deploy/deployConfig_test.go
       @@ -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)
 (DIR) diff --git a/go.mod b/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