mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-28 18:06:34 +02:00
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:
parent
6c3ed201da
commit
8d61575ada
9 changed files with 405 additions and 158 deletions
8
go.mod
8
go.mod
|
@ -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
20
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
|
|
137
internal/autocert/storage_gcs.go
Normal file
137
internal/autocert/storage_gcs.go
Normal 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
|
||||
}
|
74
internal/autocert/storage_locker.go
Normal file
74
internal/autocert/storage_locker.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
63
internal/testutil/gcs.go
Normal 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
|
||||
}
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue