autocert: add support for storage in gcs (#3794)

* autocert: add support for storage in s3

* go mod tidy

* skip on mac

* autocert: add support for storage in gcs
This commit is contained in:
Caleb Doxsey 2022-12-09 08:22:32 -07:00 committed by GitHub
parent 6c3ed201da
commit 8d61575ada
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 405 additions and 158 deletions

8
go.mod
View file

@ -72,7 +72,7 @@ require (
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783
golang.org/x/sync v0.1.0
google.golang.org/api v0.103.0
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c
google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c
google.golang.org/grpc v1.51.0
google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v3 v3.0.1
@ -81,6 +81,8 @@ require (
)
require (
cloud.google.com/go v0.105.0 // indirect
cloud.google.com/go/iam v0.7.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.20 // indirect
@ -97,6 +99,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.17.6 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
@ -104,12 +107,15 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
)
require (
4d63.com/gochecknoglobals v0.1.0 // indirect
cloud.google.com/go/compute v1.12.1 // indirect
cloud.google.com/go/compute/metadata v0.2.1 // indirect
cloud.google.com/go/storage v1.28.1
github.com/Abirdcfly/dupword v0.0.7 // indirect
github.com/Antonboom/errname v0.1.7 // indirect
github.com/Antonboom/nilnil v0.1.1 // indirect

20
go.sum
View file

@ -32,6 +32,8 @@ cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Ud
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -51,6 +53,9 @@ cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxB
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@ -62,6 +67,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
contrib.go.opencensus.io/exporter/jaeger v0.2.1 h1:yGBYzYMewVL0yO9qqJv3Z5+IRhPdU7e9o/2oKpX4YvI=
contrib.go.opencensus.io/exporter/jaeger v0.2.1/go.mod h1:Y8IsLgdxqh1QxYxPC5IgXVmBaeLUeQFfBeBi9PbeZd0=
contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg=
@ -480,9 +487,11 @@ github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51B
github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0=
github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -515,6 +524,8 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 h1:PVRE9d4AQKmbelZ7emNig1+NT27DUmKZn5qXxfio54U=
@ -1409,7 +1420,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1513,6 +1525,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@ -1643,8 +1657,8 @@ google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c h1:S34D59DS2GWOEwWNt4fYmTcFrtlOgukG2k9WsomZ7tg=
google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

View file

@ -7,6 +7,7 @@ import (
"regexp"
"strings"
"cloud.google.com/go/storage"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
@ -17,6 +18,7 @@ var (
errUnknownStorageProvider = errors.New("unknown storage provider")
s3virtualRE = regexp.MustCompile(`^([-a-zA-Z0-9]+)\.s3\.([-a-zA-Z0-9]+)\.amazonaws\.com(/.*)?$`)
s3hostRE = regexp.MustCompile(`^([^/]+)/([^/]+)(/.*)?$`)
gcsRE = regexp.MustCompile(`^([^/]+)(/.*)?$`)
)
// GetCertMagicStorage gets the certmagic storage provider based on the destination.
@ -28,6 +30,28 @@ func GetCertMagicStorage(ctx context.Context, dst string) (certmagic.Storage, er
scheme := dst[:idx]
switch scheme {
case "gs":
bucket := ""
prefix := ""
if match := gcsRE.FindStringSubmatch(dst[idx+3:]); len(match) == 3 {
bucket = match[1]
prefix = strings.TrimPrefix(match[2], "/")
} else {
return nil, fmt.Errorf("autocert: invalid gcs storage location")
}
if prefix != "" && !strings.HasSuffix(prefix, "/") {
prefix += "/"
}
client, err := storage.NewClient(ctx)
if err != nil {
return nil, fmt.Errorf("autocert: error creating gcs storage client: %w", err)
}
return newGCSStorage(client, bucket, prefix), nil
case "s3":
bucket := ""
prefix := ""
@ -36,15 +60,13 @@ func GetCertMagicStorage(ctx context.Context, dst string) (certmagic.Storage, er
if match := s3virtualRE.FindStringSubmatch(dst[idx+3:]); len(match) == 4 {
// s3://{bucket}.s3.{region}.amazonaws.com/{prefix}
bucket = match[1]
prefix = match[3]
prefix = strings.TrimPrefix(match[3], "/")
options = append(options, config.WithRegion(match[2]))
} else if match := s3hostRE.FindStringSubmatch(dst[idx+3:]); len(match) == 4 {
// s3://{host}/{bucket-name}/{prefix}
host := match[1]
bucket = match[2]
if strings.HasPrefix(match[3], "/") {
prefix = match[3][1:]
}
prefix = strings.TrimPrefix(match[3][1:], "/")
options = append(options,
config.WithRegion("us-east-1"),
config.WithEndpointResolver(aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
@ -55,6 +77,8 @@ func GetCertMagicStorage(ctx context.Context, dst string) (certmagic.Storage, er
HostnameImmutable: true,
}, nil
})))
} else {
return nil, fmt.Errorf("autocert: invalid s3 storage location")
}
if prefix != "" && !strings.HasSuffix(prefix, "/") {
@ -68,11 +92,7 @@ func GetCertMagicStorage(ctx context.Context, dst string) (certmagic.Storage, er
client := s3.NewFromConfig(cfg)
return &s3Storage{
client: client,
bucket: bucket,
prefix: prefix,
}, nil
return newS3Storage(client, bucket, prefix), nil
}
return nil, errUnknownStorageProvider

View file

@ -0,0 +1,137 @@
package autocert
import (
"context"
"errors"
"io"
"io/fs"
"cloud.google.com/go/storage"
"github.com/caddyserver/certmagic"
"google.golang.org/api/iterator"
)
type gcsStorage struct {
client *storage.Client
bucket string
prefix string
*locker
}
func newGCSStorage(client *storage.Client, bucket, prefix string) *gcsStorage {
s := &gcsStorage{
client: client,
bucket: bucket,
prefix: prefix,
}
s.locker = &locker{
store: s.Store,
load: s.Load,
delete: s.Delete,
}
return s
}
func (s *gcsStorage) Store(ctx context.Context, key string, value []byte) error {
obj := s.client.
Bucket(s.bucket).
Object(key)
w := obj.NewWriter(ctx)
_, err := w.Write(value)
if err != nil {
_ = w.CloseWithError(err)
return err
}
err = w.Close()
if err != nil {
return err
}
return nil
}
func (s *gcsStorage) Load(ctx context.Context, key string) ([]byte, error) {
r, err := s.client.
Bucket(s.bucket).
Object(key).
NewReader(ctx)
if errors.Is(err, storage.ErrObjectNotExist) {
return nil, fs.ErrNotExist
} else if err != nil {
return nil, err
}
defer r.Close()
return io.ReadAll(r)
}
func (s *gcsStorage) Delete(ctx context.Context, key string) error {
err := s.client.
Bucket(s.bucket).
Object(key).
Delete(ctx)
if errors.Is(err, storage.ErrObjectNotExist) {
return nil
}
return err
}
func (s *gcsStorage) Exists(ctx context.Context, key string) bool {
_, err := s.client.
Bucket(s.bucket).
Object(key).
Attrs(ctx)
return err == nil
}
func (s *gcsStorage) List(ctx context.Context, prefix string, recursive bool) ([]string, error) {
var delimiter string
if !recursive {
delimiter = "/"
}
it := s.client.
Bucket(s.bucket).
Objects(ctx, &storage.Query{
Delimiter: delimiter,
Prefix: prefix,
})
var keys []string
for {
attrs, err := it.Next()
if errors.Is(err, iterator.Done) {
break
} else if err != nil {
return nil, err
}
if attrs.Prefix != "" {
keys = append(keys, attrs.Prefix)
} else {
keys = append(keys, attrs.Name)
}
}
return keys, nil
}
func (s *gcsStorage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) {
attrs, err := s.client.
Bucket(s.bucket).
Object(key).
Attrs(ctx)
if errors.Is(err, storage.ErrObjectNotExist) {
return certmagic.KeyInfo{}, fs.ErrNotExist
} else if err != nil {
return certmagic.KeyInfo{}, err
}
return certmagic.KeyInfo{
Key: key,
Modified: attrs.Updated,
Size: attrs.Size,
IsTerminal: true,
}, nil
}

View file

@ -0,0 +1,74 @@
package autocert
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"time"
"github.com/google/uuid"
)
const (
lockDuration = time.Second * 30
lockPollInterval = time.Second
)
type lockState struct {
ID string
Expires time.Time
}
type locker struct {
store func(ctx context.Context, key string, value []byte) error
load func(ctx context.Context, key string) ([]byte, error)
delete func(ctx context.Context, key string) error
}
func (l *locker) Lock(ctx context.Context, name string) error {
key := fmt.Sprintf("locks/%s", name)
lockID := uuid.NewString()
for {
data, err := l.load(ctx, key)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
var ls lockState
if json.Unmarshal(data, &ls) == nil {
if ls.ID == lockID {
return nil
} else if ls.Expires.Before(time.Now()) {
// ignore the existing lock and take it ourselves
} else {
// wait
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(lockPollInterval):
}
continue
}
}
ls.ID = lockID
ls.Expires = time.Now().Add(lockDuration)
data, err = json.Marshal(ls)
if err != nil {
return err
}
err = l.store(ctx, key, data)
if err != nil {
return err
}
}
}
func (l *locker) Unlock(ctx context.Context, name string) error {
key := fmt.Sprintf("locks/%s", name)
return l.delete(ctx, key)
}

View file

@ -3,73 +3,37 @@ package autocert
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"sort"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/caddyserver/certmagic"
"github.com/google/uuid"
)
const (
s3lockDuration = time.Second * 30
s3lockPollInterval = time.Second
)
type s3lock struct {
ID string
Expires time.Time
}
type s3Storage struct {
client *s3.Client
bucket string
prefix string
*locker
}
func (s *s3Storage) Lock(ctx context.Context, name string) error {
lockID := uuid.NewString()
for {
lock, err := s.getLock(ctx, name)
if err != nil {
return err
}
if lock != nil {
if lock.ID == lockID {
return nil
} else if lock.Expires.Before(time.Now()) {
// ignore the existing lock and take it ourselves
} else {
// wait
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(s3lockPollInterval):
}
continue
}
}
// take the lock
lock = &s3lock{ID: lockID, Expires: time.Now().Add(s3lockDuration)}
err = s.putLock(ctx, name, lock)
if err != nil {
return err
}
func newS3Storage(client *s3.Client, bucket, prefix string) *s3Storage {
s := &s3Storage{
client: client,
bucket: bucket,
prefix: prefix,
}
}
func (s *s3Storage) Unlock(ctx context.Context, name string) error {
return s.deleteLock(ctx, name)
s.locker = &locker{
store: s.Store,
load: s.Load,
delete: s.Delete,
}
return s
}
func (s *s3Storage) Store(ctx context.Context, key string, value []byte) error {
@ -158,52 +122,3 @@ func (s *s3Storage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, er
IsTerminal: true,
}, nil
}
func (s *s3Storage) getLock(ctx context.Context, name string) (*s3lock, error) {
key := fmt.Sprintf("locks/%s", name)
output, err := s.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
var nsk *types.NoSuchKey
if err != nil && errors.As(err, &nsk) {
return nil, nil
} else if err != nil {
return nil, err
}
defer output.Body.Close()
var lock s3lock
err = json.NewDecoder(output.Body).Decode(&lock)
if err != nil {
return nil, err
}
return &lock, nil
}
func (s *s3Storage) putLock(ctx context.Context, name string, lock *s3lock) error {
key := fmt.Sprintf("locks/%s", name)
bs, err := json.Marshal(lock)
if err != nil {
return err
}
_, err = s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: bytes.NewReader(bs),
})
return err
}
func (s *s3Storage) deleteLock(ctx context.Context, name string) error {
key := fmt.Sprintf("locks/%s", name)
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
return err
}

View file

@ -14,7 +14,24 @@ import (
"github.com/pomerium/pomerium/internal/testutil"
)
func TestStorage(t *testing.T) {
func TestGCSStorage(t *testing.T) {
t.Skip("fakeserver doesn't support multipart uploads")
ctx, clearTimeout := context.WithTimeout(context.Background(), time.Second*30)
t.Cleanup(clearTimeout)
require.NoError(t, testutil.WithTestGCS(t, "bucket", func() error {
s, err := GetCertMagicStorage(ctx, "gs://bucket/some/prefix")
if !assert.NoError(t, err) {
return nil
}
runStorageTests(t, s)
return nil
}))
}
func TestS3Storage(t *testing.T) {
if os.Getenv("GITHUB_ACTION") != "" && runtime.GOOS == "darwin" {
t.Skip("Github action can not run docker on MacOS")
}
@ -22,50 +39,51 @@ func TestStorage(t *testing.T) {
ctx, clearTimeout := context.WithTimeout(context.Background(), time.Second*30)
t.Cleanup(clearTimeout)
runTests := func(t *testing.T, s certmagic.Storage) {
t.Helper()
for _, key := range []string{"1", "a/1", "b/c/2"} {
assert.NoError(t, s.Store(ctx, key, []byte{1, 2, 3}))
assert.True(t, s.Exists(ctx, key))
data, err := s.Load(ctx, key)
if assert.NoError(t, err) {
assert.Equal(t, []byte{1, 2, 3}, data)
}
ki, err := s.Stat(ctx, key)
if assert.NoError(t, err) {
assert.Equal(t, true, ki.IsTerminal)
}
}
keys, err := s.List(ctx, "", true)
assert.NoError(t, err)
assert.Equal(t, []string{"1", "a/1", "b/c/2"}, keys)
keys, err = s.List(ctx, "b/", false)
assert.NoError(t, err)
assert.Equal(t, []string{"b/c/"}, keys)
assert.NoError(t, s.Delete(ctx, "a/b/c"))
_, err = s.Load(ctx, "a/b/c")
assert.Error(t, err)
assert.NoError(t, s.Lock(ctx, "a"))
time.AfterFunc(time.Second*2, func() {
s.Unlock(ctx, "a")
})
assert.NoError(t, s.Lock(ctx, "a"))
}
t.Run("s3", func(t *testing.T) {
require.NoError(t, testutil.WithTestMinIO(t, "bucket", func(endpoint string) error {
s, err := GetCertMagicStorage(ctx, "s3://"+endpoint+"/bucket/some/prefix")
if !assert.NoError(t, err) {
return nil
}
runTests(t, s)
require.NoError(t, testutil.WithTestMinIO(t, "bucket", func(endpoint string) error {
s, err := GetCertMagicStorage(ctx, "s3://"+endpoint+"/bucket/some/prefix")
if !assert.NoError(t, err) {
return nil
}))
})
}
runStorageTests(t, s)
return nil
}))
}
func runStorageTests(t *testing.T, s certmagic.Storage) {
t.Helper()
ctx, clearTimeout := context.WithTimeout(context.Background(), time.Second*30)
t.Cleanup(clearTimeout)
for _, key := range []string{"1", "a/1", "b/c/2"} {
assert.NoError(t, s.Store(ctx, key, []byte{1, 2, 3}), "should store")
assert.True(t, s.Exists(ctx, key), "should exist after storing")
data, err := s.Load(ctx, key)
if assert.NoError(t, err, "should load") {
assert.Equal(t, []byte{1, 2, 3}, data)
}
ki, err := s.Stat(ctx, key)
if assert.NoError(t, err) {
assert.Equal(t, true, ki.IsTerminal)
}
}
keys, err := s.List(ctx, "", true)
assert.NoError(t, err, "should list recursively")
assert.Equal(t, []string{"1", "a/1", "b/c/2"}, keys)
keys, err = s.List(ctx, "b/", false)
assert.NoError(t, err, "should list non-recursively")
assert.Equal(t, []string{"b/c/"}, keys)
assert.NoError(t, s.Delete(ctx, "a/b/c"), "should delete")
_, err = s.Load(ctx, "a/b/c")
assert.Error(t, err)
assert.NoError(t, s.Lock(ctx, "a"), "should lock")
time.AfterFunc(time.Second*2, func() {
s.Unlock(ctx, "a")
})
assert.NoError(t, s.Lock(ctx, "a"), "should re-lock")
}

63
internal/testutil/gcs.go Normal file
View file

@ -0,0 +1,63 @@
package testutil
import (
"context"
"fmt"
"testing"
"cloud.google.com/go/storage"
"github.com/ory/dockertest/v3"
)
// WithTestGCS starts a GCS storage emulator.
func WithTestGCS(t *testing.T, bucket string, handler func() error) error {
t.Helper()
ctx, clearTimeout := context.WithTimeout(context.Background(), maxWait)
defer clearTimeout()
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
pool, err := dockertest.NewPool("")
if err != nil {
return err
}
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "fsouza/fake-gcs-server",
Tag: "1.42.2",
Cmd: []string{"-scheme", "http"},
})
if err != nil {
return err
}
_ = resource.Expire(uint(maxWait.Seconds()))
go tailLogs(ctx, t, pool, resource)
t.Setenv("STORAGE_EMULATOR_HOST", fmt.Sprintf("localhost:%s", resource.GetPort("4443/tcp")))
if err := pool.Retry(func() error {
client, err := storage.NewClient(ctx)
if err != nil {
t.Logf("gcs: %s", err)
return err
}
err = client.Bucket(bucket).Create(ctx, "", nil)
if err != nil {
t.Logf("gcs: %s", err)
return err
}
return nil
}); err != nil {
_ = pool.Purge(resource)
return err
}
e := handler()
if err := pool.Purge(resource); err != nil {
return err
}
return e
}

View file

@ -80,7 +80,7 @@ func tailLogs(ctx context.Context, t *testing.T, pool *dockertest.Pool, resource
go func() {
s := bufio.NewScanner(pr)
for s.Scan() {
t.Logf("minio: %s", s.Text())
t.Logf("%s: %s", resource.Container.Config.Image, s.Text())
}
}()
defer pw.Close()