mirror of https://github.com/docker/docs.git
330 lines
10 KiB
Go
330 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/cloudfront"
|
|
"github.com/aws/aws-sdk-go/service/lambda"
|
|
"github.com/aws/aws-sdk-go/service/s3"
|
|
)
|
|
|
|
type AwsCmd struct {
|
|
S3UpdateConfig AwsS3UpdateConfigCmd `kong:"cmd,name=s3-update-config"`
|
|
LambdaInvoke AwsLambdaInvokeCmd `kong:"cmd,name=lambda-invoke"`
|
|
CloudfrontUpdate AwsCloudfrontUpdateCmd `kong:"cmd,name=cloudfront-update"`
|
|
}
|
|
|
|
type AwsS3UpdateConfigCmd struct {
|
|
Region string `kong:"name='region',env='AWS_REGION'"`
|
|
S3Bucket string `kong:"name='s3-bucket',env='AWS_S3_BUCKET'"`
|
|
S3Config string `kong:"name='s3-website-config',env='AWS_S3_CONFIG'"`
|
|
DryRun bool `kong:"name='dry-run',env='DRY_RUN'"`
|
|
}
|
|
|
|
func (s *AwsS3UpdateConfigCmd) Run() error {
|
|
if s.DryRun {
|
|
log.Printf("INFO: Dry run mode enabled. Configuration:\nRegion: %s\nS3Bucket: %s\nS3Config: %s\n", s.Region, s.S3Bucket, s.S3Config)
|
|
return nil
|
|
}
|
|
|
|
file, err := os.ReadFile(s.S3Config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read s3 config file %s: %w", s.S3Config, err)
|
|
}
|
|
|
|
data := s3.WebsiteConfiguration{}
|
|
err = json.Unmarshal(file, &data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse JSON from %s: %w", s.S3Config, err)
|
|
}
|
|
|
|
sess, err := session.NewSession(&aws.Config{
|
|
Credentials: awsCredentials(),
|
|
Region: aws.String(s.Region),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create session: %w", err)
|
|
}
|
|
|
|
svc := s3.New(sess)
|
|
|
|
// Create SetBucketWebsite parameters based on the JSON file input
|
|
params := s3.PutBucketWebsiteInput{
|
|
Bucket: aws.String(s.S3Bucket),
|
|
WebsiteConfiguration: &data,
|
|
}
|
|
|
|
// Set the website configuration on the bucket.
|
|
// Replacing any existing configuration.
|
|
_, err = svc.PutBucketWebsite(¶ms)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to set bucket %q website configuration: %w", s.S3Bucket, err)
|
|
}
|
|
|
|
log.Printf("INFO: successfully set bucket %q website configuration\n", s.S3Bucket)
|
|
return nil
|
|
}
|
|
|
|
type AwsLambdaInvokeCmd struct {
|
|
Region string `kong:"name='region',env='AWS_REGION'"`
|
|
LambdaFunction string `kong:"name='lambda-function',env='AWS_LAMBDA_FUNCTION'"`
|
|
DryRun bool `kong:"name='dry-run',env='DRY_RUN'"`
|
|
}
|
|
|
|
func (s *AwsLambdaInvokeCmd) Run() error {
|
|
if s.DryRun {
|
|
log.Printf("INFO: Dry run mode enabled. Configuration:\nRegion: %s\nLambdaFunction: %s\n", s.Region, s.LambdaFunction)
|
|
return nil
|
|
}
|
|
|
|
svc := lambda.New(session.Must(session.NewSessionWithOptions(session.Options{
|
|
SharedConfigState: session.SharedConfigEnable,
|
|
})), &aws.Config{
|
|
Credentials: awsCredentials(),
|
|
Region: aws.String(s.Region),
|
|
})
|
|
|
|
_, err := svc.Invoke(&lambda.InvokeInput{
|
|
FunctionName: aws.String(s.LambdaFunction),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("INFO: lambda function %q invoked successfully\n", s.LambdaFunction)
|
|
return nil
|
|
}
|
|
|
|
type AwsCloudfrontUpdateCmd struct {
|
|
Region string `kong:"name='region',env='AWS_REGION'"`
|
|
Function string `kong:"name='lambda-function',env='AWS_LAMBDA_FUNCTION'"`
|
|
FunctionFile string `kong:"name='lambda-function-file',env='AWS_LAMBDA_FUNCTION_FILE'"`
|
|
CloudfrontID string `kong:"name='cloudfront-id',env='AWS_CLOUDFRONT_ID'"`
|
|
RedirectsFile string `kong:"name='redirects-file',env='REDIRECTS_FILE'"`
|
|
RedirectsPrefixesFile string `kong:"name='redirects-prefixes-file',env='REDIRECTS_PREFIXES_FILE'"`
|
|
DryRun bool `kong:"name='dry-run',env='DRY_RUN'"`
|
|
}
|
|
|
|
func (s *AwsCloudfrontUpdateCmd) Run() error {
|
|
var err error
|
|
ver := time.Now().UTC().Format(time.RFC3339)
|
|
|
|
zipdt, err := getLambdaFunctionZip(s.FunctionFile, s.RedirectsFile, s.RedirectsPrefixesFile, s.DryRun)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create lambda function zip: %w", err)
|
|
}
|
|
|
|
if s.DryRun {
|
|
log.Printf("INFO: Dry run mode enabled. Configuration:\nRegion: %s\nFunction: %s\nFunctionFile: %s\nCloudfrontID: %s\nRedirectsFile: %s\nRedirectsPrefixesFile: %s\n",
|
|
s.Region, s.Function, s.FunctionFile, s.CloudfrontID, s.RedirectsFile, s.RedirectsPrefixesFile)
|
|
return nil
|
|
}
|
|
|
|
svc := lambda.New(session.Must(session.NewSessionWithOptions(session.Options{
|
|
SharedConfigState: session.SharedConfigEnable,
|
|
})), &aws.Config{
|
|
Credentials: awsCredentials(),
|
|
Region: aws.String(s.Region),
|
|
})
|
|
|
|
function, err := svc.GetFunction(&lambda.GetFunctionInput{
|
|
FunctionName: aws.String(s.Function),
|
|
})
|
|
if err != nil {
|
|
var aerr awserr.Error
|
|
if errors.As(err, &aerr) && aerr.Code() != lambda.ErrCodeResourceNotFoundException {
|
|
return fmt.Errorf("cannot find lambda function %q: %w", s.Function, err)
|
|
}
|
|
_, err = svc.CreateFunction(&lambda.CreateFunctionInput{
|
|
FunctionName: aws.String(s.Function),
|
|
})
|
|
if errors.As(err, &aerr) && aerr.Code() != lambda.ErrCodeResourceConflictException {
|
|
return err
|
|
}
|
|
}
|
|
codeSha256 := *function.Configuration.CodeSha256
|
|
log.Printf("INFO: updating lambda function %q\n", s.Function)
|
|
|
|
updateConfig, err := svc.UpdateFunctionCode(&lambda.UpdateFunctionCodeInput{
|
|
FunctionName: aws.String(s.Function),
|
|
ZipFile: zipdt,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update lambda function code: %s", err)
|
|
}
|
|
log.Printf("INFO: lambda function updated successfully (%s)\n", *updateConfig.FunctionArn)
|
|
|
|
if codeSha256 == *updateConfig.CodeSha256 {
|
|
log.Printf("INFO: lambda function code has not changed. skipping publication...")
|
|
return nil
|
|
}
|
|
|
|
log.Printf("INFO: waiting for lambda function to be processed\n")
|
|
// the lambda function code image is never ready right away, AWS has to
|
|
// process it, so we wait 8 seconds before trying to publish the version.
|
|
time.Sleep(8 * time.Second)
|
|
|
|
publishConfig, err := svc.PublishVersion(&lambda.PublishVersionInput{
|
|
FunctionName: aws.String(s.Function),
|
|
CodeSha256: aws.String(*updateConfig.CodeSha256),
|
|
Description: aws.String(ver),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to publish lambda function version %q for %q: %w", ver, s.Function, err)
|
|
}
|
|
log.Printf("INFO: lambda function version %q published successfully (%s)\n", ver, *publishConfig.FunctionArn)
|
|
|
|
sess, err := session.NewSession(&aws.Config{
|
|
Credentials: awsCredentials(),
|
|
Region: aws.String(s.Region)},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create session: %w", err)
|
|
}
|
|
|
|
cfrt := cloudfront.New(sess)
|
|
cfrtDistrib, err := cfrt.GetDistribution(&cloudfront.GetDistributionInput{
|
|
Id: aws.String(s.CloudfrontID),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("cannot find cloudfront distribution %q: %w", s.CloudfrontID, err)
|
|
}
|
|
log.Printf("INFO: cloudfront distribution %q loaded\n", *cfrtDistrib.Distribution.Id)
|
|
|
|
cfrtDistribConfig, err := cfrt.GetDistributionConfig(&cloudfront.GetDistributionConfigInput{
|
|
Id: aws.String(s.CloudfrontID),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("cannot load cloudfront distribution config: %w", err)
|
|
}
|
|
log.Printf("INFO: cloudfront distribution configuration loaded\n")
|
|
|
|
distribConfig := cfrtDistribConfig.DistributionConfig
|
|
if distribConfig.DefaultCacheBehavior == nil {
|
|
log.Printf("INFO: cloudfront distribution default cache behavior not found. skipping...")
|
|
return nil
|
|
}
|
|
|
|
for _, funcAssoc := range distribConfig.DefaultCacheBehavior.LambdaFunctionAssociations.Items {
|
|
if *funcAssoc.EventType != cloudfront.EventTypeViewerRequest {
|
|
continue
|
|
}
|
|
log.Printf("INFO: cloudfront distribution viewer request function ARN found: %q\n", *funcAssoc.LambdaFunctionARN)
|
|
}
|
|
|
|
log.Printf("INFO: updating cloudfront config with viewer request function ARN %q", *publishConfig.FunctionArn)
|
|
distribConfig.DefaultCacheBehavior.LambdaFunctionAssociations = &cloudfront.LambdaFunctionAssociations{
|
|
Quantity: aws.Int64(1),
|
|
Items: []*cloudfront.LambdaFunctionAssociation{
|
|
{
|
|
EventType: aws.String(cloudfront.EventTypeViewerRequest),
|
|
IncludeBody: aws.Bool(false),
|
|
LambdaFunctionARN: publishConfig.FunctionArn,
|
|
},
|
|
},
|
|
}
|
|
_, err = cfrt.UpdateDistribution(&cloudfront.UpdateDistributionInput{
|
|
Id: aws.String(s.CloudfrontID),
|
|
IfMatch: cfrtDistrib.ETag,
|
|
DistributionConfig: distribConfig,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("INFO: cloudfront config updated successfully\n")
|
|
return nil
|
|
}
|
|
|
|
func getLambdaFunctionZip(funcFilename, redirectsFile, redirectsPrefixesFile string, dryrun bool) ([]byte, error) {
|
|
funcdt, err := os.ReadFile(funcFilename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read lambda function file %q: %w", funcFilename, err)
|
|
}
|
|
|
|
redirects, err := os.ReadFile(redirectsFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read redirects file %q: %w", redirectsFile, err)
|
|
}
|
|
|
|
redirectsPrefixes, err := os.ReadFile(redirectsPrefixesFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read redirects prefixes file %q: %w", redirectsPrefixesFile, err)
|
|
}
|
|
|
|
var funcbuf bytes.Buffer
|
|
functpl := template.Must(template.New("").Parse(string(funcdt)))
|
|
if err = functpl.Execute(&funcbuf, struct {
|
|
RedirectsJSON string
|
|
RedirectsPrefixesJSON string
|
|
}{
|
|
string(redirects),
|
|
string(redirectsPrefixes),
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if dryrun {
|
|
log.Printf("INFO: Dry run mode enabled. Lambda Function Definition:\n\n%s\n", funcbuf.String())
|
|
return nil, nil
|
|
}
|
|
|
|
tmpdir, err := os.MkdirTemp("", "lambda-zip")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer os.RemoveAll(tmpdir)
|
|
|
|
zipfile, err := os.Create(path.Join(tmpdir, "lambda-function.zip"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer zipfile.Close()
|
|
|
|
zipwrite := zip.NewWriter(zipfile)
|
|
zipindex, err := zipwrite.Create("index.js")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err = zipindex.Write(funcbuf.Bytes()); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = zipwrite.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
zipdt, err := os.ReadFile(zipfile.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return zipdt, nil
|
|
}
|
|
|
|
func awsCredentials() *credentials.Credentials {
|
|
return credentials.NewChainCredentials(
|
|
[]credentials.Provider{
|
|
&credentials.StaticProvider{
|
|
Value: credentials.Value{
|
|
AccessKeyID: getEnvOrSecret("AWS_ACCESS_KEY_ID"),
|
|
SecretAccessKey: getEnvOrSecret("AWS_SECRET_ACCESS_KEY"),
|
|
SessionToken: getEnvOrSecret("AWS_SESSION_TOKEN"),
|
|
},
|
|
},
|
|
},
|
|
)
|
|
}
|