refactor: move scripts and utils to hack/ directory

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
This commit is contained in:
David Karlsson
2024-11-15 09:28:19 +01:00
parent acb264a20c
commit c7511a228f
14 changed files with 9 additions and 9 deletions

57
hack/releaser/Dockerfile Normal file
View File

@@ -0,0 +1,57 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.23
FROM scratch AS sitedir
FROM golang:${GO_VERSION}-alpine AS base
RUN apk add --no-cache openssl
ENV CGO_ENABLED=0
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
FROM base AS releaser
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /out/releaser .
FROM base AS aws-s3-update-config
ARG DRY_RUN=false
ARG AWS_REGION
ARG AWS_S3_BUCKET
ARG AWS_S3_CONFIG
RUN --mount=type=bind,target=. \
--mount=type=bind,from=releaser,source=/out/releaser,target=/usr/bin/releaser \
--mount=type=secret,id=AWS_ACCESS_KEY_ID \
--mount=type=secret,id=AWS_SECRET_ACCESS_KEY \
--mount=type=secret,id=AWS_SESSION_TOKEN \
releaser aws s3-update-config
FROM base AS aws-lambda-invoke
ARG DRY_RUN=false
ARG AWS_REGION
ARG AWS_LAMBDA_FUNCTION
RUN --mount=type=bind,from=releaser,source=/out/releaser,target=/usr/bin/releaser \
--mount=type=secret,id=AWS_ACCESS_KEY_ID \
--mount=type=secret,id=AWS_SECRET_ACCESS_KEY \
--mount=type=secret,id=AWS_SESSION_TOKEN \
releaser aws lambda-invoke
FROM base AS aws-cloudfront-update
ARG DRY_RUN=false
ARG AWS_REGION
ARG AWS_LAMBDA_FUNCTION
ARG AWS_CLOUDFRONT_ID
ARG AWS_LAMBDA_FUNCTION_FILE="cloudfront-lambda-redirects.js"
ARG REDIRECTS_FILE="/site/redirects.json"
ARG REDIRECTS_PREFIXES_FILE="redirects-prefixes.json"
RUN --mount=type=bind,target=. \
--mount=type=bind,from=sitedir,target=/site \
--mount=type=bind,from=releaser,source=/out/releaser,target=/usr/bin/releaser \
--mount=type=secret,id=AWS_ACCESS_KEY_ID \
--mount=type=secret,id=AWS_SECRET_ACCESS_KEY \
--mount=type=secret,id=AWS_SESSION_TOKEN \
releaser aws cloudfront-update

329
hack/releaser/aws.go Normal file
View File

@@ -0,0 +1,329 @@
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(&params)
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"),
},
},
},
)
}

View File

@@ -0,0 +1,56 @@
'use strict';
exports.handler = (event, context, callback) => {
//console.log("event", JSON.stringify(event));
const request = event.Records[0].cf.request;
const requestUrl = request.uri.replace(/\/$/, "")
const redirects = JSON.parse(`{{.RedirectsJSON}}`);
for (let key in redirects) {
const redirectTarget = key.replace(/\/$/, "")
if (redirectTarget !== requestUrl) {
continue;
}
//console.log(`redirect: ${requestUrl} to ${redirects[key]}`);
const response = {
status: '301',
statusDescription: 'Moved Permanently',
headers: {
location: [{
key: 'Location',
value: redirects[key],
}],
},
}
callback(null, response);
return
}
const redirectsPrefixes = JSON.parse(`{{.RedirectsPrefixesJSON}}`);
for (let x in redirectsPrefixes) {
const rp = redirectsPrefixes[x];
if (!request.uri.startsWith(`/${rp['prefix']}`)) {
continue;
}
let newlocation = "/";
if (rp['strip']) {
let re = new RegExp(`(^/${rp['prefix']})`, 'gi');
newlocation = request.uri.replace(re,'/');
}
//console.log(`redirect: ${request.uri} to ${redirectsPrefixes[key]}`);
const response = {
status: '301',
statusDescription: 'Moved Permanently',
headers: {
location: [{
key: 'Location',
value: newlocation,
}],
},
}
callback(null, response);
return
}
callback(null, request);
};

14
hack/releaser/go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/docker/docs/hack/releaser
go 1.22
require (
github.com/alecthomas/kong v1.4.0
github.com/aws/aws-sdk-go v1.55.5
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

24
hack/releaser/go.sum Normal file
View File

@@ -0,0 +1,24 @@
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.4.0 h1:UL7tzGMnnY0YRMMvJyITIRX1EpO6RbBRZDNcCevy3HA=
github.com/alecthomas/kong v1.4.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

44
hack/releaser/main.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"log"
"os"
"path/filepath"
"github.com/alecthomas/kong"
)
var (
version = "dev"
cli struct {
Version kong.VersionFlag
Aws AwsCmd `kong:"cmd,name=aws"`
}
)
func main() {
log.SetFlags(0)
ctx := kong.Parse(&cli,
kong.Name("releaser"),
kong.UsageOnError(),
kong.Vars{
"version": version,
},
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
Summary: true,
}))
ctx.FatalIfErrorf(ctx.Run())
}
// getEnvOrSecret retrieves secret's value from secret file or env
func getEnvOrSecret(name string) string {
if v, ok := os.LookupEnv(name); ok {
return v
}
b, err := os.ReadFile(filepath.Join("/run/secrets", name))
if err != nil {
return ""
}
return string(b)
}

View File

@@ -0,0 +1,110 @@
[
{
"prefix": "compliance/",
"strip": false
},
{
"prefix": "datacenter/",
"strip": false
},
{
"prefix": "docker-hub-enterprise/",
"strip": false
},
{
"prefix": "docker-trusted-registry/",
"strip": false
},
{
"prefix": "ee/",
"strip": false
},
{
"prefix": "reference/dtr/",
"strip": false
},
{
"prefix": "reference/ucp/",
"strip": false
},
{
"prefix": "ucp/",
"strip": false
},
{
"prefix": "v1.4/",
"strip": true
},
{
"prefix": "v1.5/",
"strip": true
},
{
"prefix": "v1.6/",
"strip": true
},
{
"prefix": "v1.7/",
"strip": true
},
{
"prefix": "v1.8/",
"strip": true
},
{
"prefix": "v1.9/",
"strip": true
},
{
"prefix": "v1.10/",
"strip": true
},
{
"prefix": "v1.11/",
"strip": true
},
{
"prefix": "v1.12/",
"strip": true
},
{
"prefix": "v1.13/",
"strip": true
},
{
"prefix": "v17.03/",
"strip": true
},
{
"prefix": "v17.06/enterprise/",
"strip": false
},
{
"prefix": "v17.06/",
"strip": true
},
{
"prefix": "v17.09/",
"strip": true
},
{
"prefix": "v17.12/",
"strip": true
},
{
"prefix": "v18.03/ee/",
"strip": false
},
{
"prefix": "v18.03/",
"strip": true
},
{
"prefix": "v18.09/ee/",
"strip": false
},
{
"prefix": "v18.09/",
"strip": true
}
]

View File

@@ -0,0 +1,9 @@
{
"ErrorDocument": {
"Key": "404.html"
},
"IndexDocument": {
"Suffix": "index.html"
},
"RedirectAllRequestsTo": null
}