diff --git a/go.mod b/go.mod index 662c660d9..53280d2ed 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/mapstructure v1.5.0 github.com/natefinch/atomic v1.0.1 + github.com/oapi-codegen/runtime v1.1.0 github.com/open-policy-agent/opa v0.59.0 github.com/openzipkin/zipkin-go v0.4.2 github.com/ory/dockertest/v3 v3.10.0 @@ -50,7 +51,6 @@ require ( github.com/pomerium/csrf v1.7.0 github.com/pomerium/datasource v0.18.2-0.20221108160055-c6134b5ed524 github.com/pomerium/webauthn v0.0.0-20221118023040-00a9c430578b - github.com/pomerium/zero-sdk v0.0.0-20231127153820-dcd408d87b54 github.com/prometheus/client_golang v1.17.0 github.com/prometheus/client_model v0.5.0 github.com/prometheus/common v0.45.0 @@ -67,7 +67,7 @@ require ( go.uber.org/automaxprocs v1.5.3 go.uber.org/zap v1.26.0 golang.org/x/crypto v0.16.0 - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/net v0.19.0 golang.org/x/oauth2 v0.15.0 golang.org/x/sync v0.5.0 @@ -75,7 +75,7 @@ require ( google.golang.org/api v0.153.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f google.golang.org/grpc v1.59.0 - google.golang.org/protobuf v1.31.0 + google.golang.org/protobuf v1.31.1-0.20231027082548-f4a6c1f6e5c1 gopkg.in/yaml.v3 v3.0.1 namespacelabs.dev/go-filenotify v0.0.0-20220511192020-53ea11be7eaa sigs.k8s.io/yaml v1.4.0 @@ -110,45 +110,31 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 // indirect github.com/aws/smithy-go v1.16.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bufbuild/buf v1.26.1 // indirect - github.com/bufbuild/connect-go v1.9.0 // indirect - github.com/bufbuild/connect-opentelemetry-go v0.4.0 // indirect - github.com/bufbuild/protocompile v0.6.0 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect github.com/containerd/continuity v0.4.2 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/deepmap/oapi-codegen v1.15.0 // indirect - github.com/docker/cli v24.0.4+incompatible // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.0 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/cli v24.0.7+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/felixge/fgprof v0.9.3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect - github.com/getkin/kin-openapi v0.118.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/swag v0.21.1 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gofrs/uuid/v5 v5.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/go-containerregistry v0.15.2 // indirect github.com/google/go-tpm v0.3.3 // indirect - github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect @@ -157,65 +143,49 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.13 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/yaml v0.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect - github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect - github.com/klauspost/pgzip v1.2.6 // indirect github.com/lib/pq v1.10.7 // indirect github.com/libdns/libdns v0.2.1 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/miekg/dns v1.1.55 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/oapi-codegen/runtime v1.0.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/opencontainers/runc v1.1.5 // indirect github.com/pelletier/go-toml/v2 v2.0.9 // indirect - github.com/perimeterx/marshmallow v1.1.4 // indirect github.com/philhofer/fwd v1.0.0 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/profile v1.7.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rs/xid v1.5.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect - github.com/tetratelabs/wazero v1.3.1 // indirect github.com/tinylib/msgp v1.1.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/uber/jaeger-client-go v2.25.0+incompatible // indirect - github.com/vbatts/tar-split v0.11.3 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -227,19 +197,16 @@ require ( go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/sdk v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect - go.uber.org/atomic v1.11.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.12.0 // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.12.0 // indirect + golang.org/x/tools v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 // indirect gopkg.in/DataDog/dd-trace-go.v1 v1.22.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect diff --git a/go.sum b/go.sum index f88eff07f..c3b2f08bd 100644 --- a/go.sum +++ b/go.sum @@ -55,7 +55,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg= github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM= @@ -140,14 +139,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bits-and-blooms/bitset v1.11.0 h1:RMyy2mBBShArUAhfVRZJ2xyBO58KCBCtZFShw3umo6k= github.com/bits-and-blooms/bitset v1.11.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/bufbuild/buf v1.26.1 h1:+GdU4z2paCmDclnjLv7MqnVi3AGviImlIKhG0MHH9FA= -github.com/bufbuild/buf v1.26.1/go.mod h1:UMPncXMWgrmIM+0QpwTEwjNr2SA0z2YIVZZsmNflvB4= -github.com/bufbuild/connect-go v1.9.0 h1:JIgAeNuFpo+SUPfU19Yt5TcWlznsN5Bv10/gI/6Pjoc= -github.com/bufbuild/connect-go v1.9.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8= -github.com/bufbuild/connect-opentelemetry-go v0.4.0 h1:6JAn10SNqlQ/URhvRNGrIlczKw1wEXknBUUtmWqOiak= -github.com/bufbuild/connect-opentelemetry-go v0.4.0/go.mod h1:nwPXYoDOoc2DGyKE/6pT1Q9MPSi2Et2e6BieMD0l6WU= -github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY= -github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= @@ -179,8 +170,6 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWH github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= -github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= -github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -194,18 +183,12 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 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/deepmap/oapi-codegen v1.15.0 h1:SQqViaeb4k2vMul8gx12oDOIadEtoRqTdLkxjzqtQ90= -github.com/deepmap/oapi-codegen v1.15.0/go.mod h1:a6KoHV7lMRwsPoEg2C6NDHiXYV3EQfiFocOlJ8dgJQE= github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= @@ -214,14 +197,14 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/docker/cli v24.0.4+incompatible h1:Y3bYF9ekNTm2VFz5U/0BlMdJy73D+Y1iAAZ8l63Ydzw= -github.com/docker/cli v24.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg= +github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= -github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -243,8 +226,6 @@ github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= -github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= -github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -261,8 +242,6 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= -github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM= -github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= @@ -291,25 +270,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= -github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= -github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -379,8 +347,6 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.15.2 h1:MMkSh+tjSdnmJZO7ljvEqV1DjfekB6VUEAZgy3a+TQE= -github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI= @@ -406,9 +372,6 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= -github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= @@ -426,7 +389,6 @@ github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qK github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= @@ -456,14 +418,9 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= -github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -472,15 +429,9 @@ github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co= -github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= -github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= -github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -502,7 +453,6 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -526,11 +476,6 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/martinlindhe/base36 v1.1.1 h1:1F1MZ5MGghBXDZ2KJ3QfxmiydlWOGB8HCEtkap5NkVg= github.com/martinlindhe/base36 v1.1.1/go.mod h1:vMS8PaZ5e/jV9LwFKlm0YLnXl/hpOihiBxKkIoc3g08= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -552,7 +497,6 @@ github.com/minio/minio-go/v7 v7.0.65 h1:sOlB8T3nQK+TApTpuN3k4WD5KasvZIE3vVFzyyCa github.com/minio/minio-go/v7 v7.0.65/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= @@ -569,8 +513,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= @@ -578,12 +520,11 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= -github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= +github.com/oapi-codegen/runtime v1.1.0 h1:rJpoNUawn5XTvekgfkvSZr0RqEnoYpFkyvrzfWeFKWM= +github.com/oapi-codegen/runtime v1.1.0/go.mod h1:BeSfBkWWWnAnGdyS+S/GnlbmHKzf8/hwkvelJZDeKA8= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -613,8 +554,6 @@ github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnz github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= -github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= @@ -623,15 +562,11 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1 h1:VGcrWe3yk6o+t7BdV github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= -github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -641,8 +576,6 @@ github.com/pomerium/datasource v0.18.2-0.20221108160055-c6134b5ed524 h1:3YQY1sb5 github.com/pomerium/datasource v0.18.2-0.20221108160055-c6134b5ed524/go.mod h1:7fGbUYJnU8RcxZJvUvhukOIBv1G7LWDAHMfDxAf5+Y0= github.com/pomerium/webauthn v0.0.0-20221118023040-00a9c430578b h1:oll/aOfJudnqFAwCvoXK9+WN2zVjTzHVPLXCggHQmHk= github.com/pomerium/webauthn v0.0.0-20221118023040-00a9c430578b/go.mod h1:KswTenBBh4y1pmhU2dpm8VgJQCgSErCg7OOFTeebrNc= -github.com/pomerium/zero-sdk v0.0.0-20231127153820-dcd408d87b54 h1:pIjWOs15zJq9XhU1uHKhBjqc6X7cJHRYbx+OW4v3EiA= -github.com/pomerium/zero-sdk v0.0.0-20231127153820-dcd408d87b54/go.mod h1:b3979nx45mwDaeIQQe5T11XjxMUQntc4H3v+szdsHUc= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= @@ -690,8 +623,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= @@ -700,8 +633,6 @@ github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -716,7 +647,6 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -729,8 +659,6 @@ github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -765,8 +693,6 @@ github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNG github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= -github.com/tetratelabs/wazero v1.3.1 h1:rnb9FgOEQRLLR8tgoD1mfjNjMhFeWRUk+a4b4j/GpUM= -github.com/tetratelabs/wazero v1.3.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= @@ -779,19 +705,11 @@ github.com/tniswong/go.rfcx v0.0.0-20181019234604-07783c52761f/go.mod h1:N+sR0vL github.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U= github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g= github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= -github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= -github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/volatiletech/null/v9 v9.0.0 h1:JCdlHEiSRVxOi7/MABiEfdsqmuj9oTV20Ao7VvZ0JkE= @@ -847,15 +765,11 @@ go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ3 go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= -go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -889,8 +803,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -904,8 +818,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -918,8 +830,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1051,11 +963,9 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210629170331-7dc0b73dc9fb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1066,7 +976,6 @@ golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1077,8 +986,6 @@ golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1148,8 +1055,8 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1248,8 +1155,6 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1265,16 +1170,14 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.1-0.20231027082548-f4a6c1f6e5c1 h1:fk72uXZyuZiTtW5tgd63jyVK6582lF61nRC/kGv6vCA= +google.golang.org/protobuf v1.31.1-0.20231027082548-f4a6c1f6e5c1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/DataDog/dd-trace-go.v1 v1.22.0 h1:gpWsqqkwUldNZXGJqT69NU9MdEDhLboK1C4nMgR0MWw= gopkg.in/DataDog/dd-trace-go.v1 v1.22.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1296,7 +1199,6 @@ gopkg.in/yaml.v2 v2.3.0/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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/zero/bootstrap/bootstrap.go b/internal/zero/bootstrap/bootstrap.go index fe3409d7f..08af3157b 100644 --- a/internal/zero/bootstrap/bootstrap.go +++ b/internal/zero/bootstrap/bootstrap.go @@ -20,8 +20,8 @@ import ( "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/retry" - sdk "github.com/pomerium/zero-sdk" - connect_mux "github.com/pomerium/zero-sdk/connect-mux" + sdk "github.com/pomerium/pomerium/pkg/zero" + connect_mux "github.com/pomerium/pomerium/pkg/zero/connect-mux" ) const ( diff --git a/internal/zero/bootstrap/file.go b/internal/zero/bootstrap/file.go index 8b248a2ae..ed6b74bff 100644 --- a/internal/zero/bootstrap/file.go +++ b/internal/zero/bootstrap/file.go @@ -15,7 +15,7 @@ import ( "os" "github.com/pomerium/pomerium/pkg/cryptutil" - cluster_api "github.com/pomerium/zero-sdk/cluster" + cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster" ) // LoadBootstrapConfigFromFile loads the bootstrap configuration from a file. diff --git a/internal/zero/bootstrap/file_test.go b/internal/zero/bootstrap/file_test.go index 93aa410e8..e409b084f 100644 --- a/internal/zero/bootstrap/file_test.go +++ b/internal/zero/bootstrap/file_test.go @@ -8,7 +8,7 @@ import ( "github.com/pomerium/pomerium/internal/zero/bootstrap" "github.com/pomerium/pomerium/pkg/cryptutil" - cluster_api "github.com/pomerium/zero-sdk/cluster" + cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster" ) func TestFile(t *testing.T) { diff --git a/internal/zero/bootstrap/new.go b/internal/zero/bootstrap/new.go index 0426a55f3..d848d3947 100644 --- a/internal/zero/bootstrap/new.go +++ b/internal/zero/bootstrap/new.go @@ -16,7 +16,7 @@ import ( "github.com/pomerium/pomerium/internal/deterministicecdsa" "github.com/pomerium/pomerium/pkg/cryptutil" "github.com/pomerium/pomerium/pkg/netutil" - sdk "github.com/pomerium/zero-sdk" + sdk "github.com/pomerium/pomerium/pkg/zero" ) // Source is a base config layer for Pomerium diff --git a/internal/zero/bootstrap/source.go b/internal/zero/bootstrap/source.go index f9831d02c..7e64d383e 100644 --- a/internal/zero/bootstrap/source.go +++ b/internal/zero/bootstrap/source.go @@ -9,7 +9,7 @@ import ( "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/atomicutil" - cluster_api "github.com/pomerium/zero-sdk/cluster" + cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster" ) var ( diff --git a/internal/zero/controller/controller.go b/internal/zero/controller/controller.go index dc4a9fde1..c702b57da 100644 --- a/internal/zero/controller/controller.go +++ b/internal/zero/controller/controller.go @@ -16,7 +16,7 @@ import ( "github.com/pomerium/pomerium/internal/zero/reconciler" "github.com/pomerium/pomerium/pkg/cmd/pomerium" "github.com/pomerium/pomerium/pkg/grpc/databroker" - sdk "github.com/pomerium/zero-sdk" + sdk "github.com/pomerium/pomerium/pkg/zero" ) // Run runs Pomerium is managed mode using the provided token. diff --git a/internal/zero/controller/mux_log.go b/internal/zero/controller/mux_log.go index b0cad8d58..9e29d9ec2 100644 --- a/internal/zero/controller/mux_log.go +++ b/internal/zero/controller/mux_log.go @@ -6,7 +6,7 @@ import ( "github.com/rs/zerolog" "github.com/pomerium/pomerium/internal/log" - connect_mux "github.com/pomerium/zero-sdk/connect-mux" + connect_mux "github.com/pomerium/pomerium/pkg/zero/connect-mux" ) func (c *controller) RunConnectLog(ctx context.Context) error { diff --git a/internal/zero/reconciler/config.go b/internal/zero/reconciler/config.go index d9f48546f..6e66c0a9d 100644 --- a/internal/zero/reconciler/config.go +++ b/internal/zero/reconciler/config.go @@ -7,7 +7,7 @@ import ( "time" "github.com/pomerium/pomerium/pkg/grpc/databroker" - sdk "github.com/pomerium/zero-sdk" + sdk "github.com/pomerium/pomerium/pkg/zero" ) // reconcilerConfig contains the configuration for the resource bundles reconciler. diff --git a/internal/zero/reconciler/download_cache.go b/internal/zero/reconciler/download_cache.go index ec6b295c0..c7df9adde 100644 --- a/internal/zero/reconciler/download_cache.go +++ b/internal/zero/reconciler/download_cache.go @@ -15,7 +15,7 @@ import ( "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/pkg/grpc/databroker" "github.com/pomerium/pomerium/pkg/protoutil" - zero_sdk "github.com/pomerium/zero-sdk" + zero_sdk "github.com/pomerium/pomerium/pkg/zero" ) // BundleCacheEntry is a cache entry for a bundle diff --git a/internal/zero/reconciler/download_cache_test.go b/internal/zero/reconciler/download_cache_test.go index f0d608a98..88d351210 100644 --- a/internal/zero/reconciler/download_cache_test.go +++ b/internal/zero/reconciler/download_cache_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/pomerium/pomerium/internal/zero/reconciler" - zero_sdk "github.com/pomerium/zero-sdk" + zero_sdk "github.com/pomerium/pomerium/pkg/zero" ) func TestCacheEntryProto(t *testing.T) { diff --git a/internal/zero/reconciler/report_status.go b/internal/zero/reconciler/report_status.go index 9331253fa..7b677c22d 100644 --- a/internal/zero/reconciler/report_status.go +++ b/internal/zero/reconciler/report_status.go @@ -4,7 +4,7 @@ import ( "context" "github.com/pomerium/pomerium/internal/log" - cluster_api "github.com/pomerium/zero-sdk/cluster" + cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster" ) const ( diff --git a/internal/zero/reconciler/service.go b/internal/zero/reconciler/service.go index 7ab537ae5..c6e7e6055 100644 --- a/internal/zero/reconciler/service.go +++ b/internal/zero/reconciler/service.go @@ -15,7 +15,7 @@ import ( "github.com/pomerium/pomerium/internal/atomicutil" "github.com/pomerium/pomerium/pkg/grpc/databroker" - connect_mux "github.com/pomerium/zero-sdk/connect-mux" + connect_mux "github.com/pomerium/pomerium/pkg/zero/connect-mux" ) type service struct { diff --git a/pkg/fanout/config.go b/pkg/fanout/config.go new file mode 100644 index 000000000..36d56c49d --- /dev/null +++ b/pkg/fanout/config.go @@ -0,0 +1,116 @@ +package fanout + +import "time" + +const ( + defaultPublishTimeout = time.Second + minPublishTimeout = time.Millisecond * 100 + + defaultReceiverCallbackTimeout = time.Second + minReceiverCallbackTimeout = time.Millisecond * 100 + + defaultAddSubscriberTimeout = time.Millisecond * 100 + minAddSubscriberTimeout = time.Millisecond * 100 + + defaultReceiverBufferSize = 100 + defaultMessageBufferSize = 1024 + defaultSubscriberBufferSize = 100 +) + +type config struct { + publishTimeout time.Duration + receiverBufferSize int + receiverCallbackTimeout time.Duration + publishBufferSize int + subscriberBufferSize int + addSubscriberTimeout time.Duration +} + +// Option configures a FanOut +type Option func(*config) + +// WithPublishTimeout sets the internal timeout for publishing messages to the fanout +func WithPublishTimeout(timeout time.Duration) Option { + if timeout < defaultPublishTimeout { + timeout = defaultPublishTimeout + } + + return func(c *config) { + c.publishTimeout = timeout + } +} + +// WithReceiverBufferSize sets the buffer size for the buffer between fanout and subscriber receiver +func WithReceiverBufferSize(size int) Option { + if size < 1 { + size = 1 + } + return func(c *config) { + c.receiverBufferSize = size + } +} + +// WithReceiverCallbackTimeout sets the timeout for the callback function of the receiver +func WithReceiverCallbackTimeout(timeout time.Duration) Option { + if timeout < minReceiverCallbackTimeout { + timeout = minReceiverCallbackTimeout + } + + return func(c *config) { + c.receiverCallbackTimeout = timeout + } +} + +// WithMessagesBufferSize sets the buffer size for the buffer that holds messages to be published +func WithMessagesBufferSize(size int) Option { + if size < 1 { + size = 1 + } + return func(c *config) { + c.publishBufferSize = size + } +} + +// WithSubscriberBufferSize sets the new subscriber requsts buffer size +func WithSubscriberBufferSize(size int) Option { + if size < 1 { + size = 1 + } + return func(c *config) { + c.subscriberBufferSize = size + } +} + +// WithAddSubscriberTimeout sets the timeout for adding a subscriber +// If it is not possible to add a subscriber within the timeout, +// it means the fanout is at capacity, and it is better to reject the subscriber, +// that will likely be propagated to the downstream, +// which would retry and eventually succeed with another instance +func WithAddSubscriberTimeout(timeout time.Duration) Option { + if timeout < minAddSubscriberTimeout { + timeout = minAddSubscriberTimeout + } + + return func(c *config) { + c.addSubscriberTimeout = timeout + } +} + +func defaultFanOutConfig() config { + var c config + c.apply( + WithPublishTimeout(defaultPublishTimeout), + WithMessagesBufferSize(defaultMessageBufferSize), + WithReceiverCallbackTimeout(defaultReceiverCallbackTimeout), + WithReceiverBufferSize(defaultReceiverBufferSize), + WithSubscriberBufferSize(defaultSubscriberBufferSize), + WithAddSubscriberTimeout(defaultAddSubscriberTimeout), + ) + return c +} + +func (c *config) apply(opts ...Option) { + for _, opt := range opts { + opt(c) + } +} diff --git a/pkg/fanout/fanout.go b/pkg/fanout/fanout.go new file mode 100644 index 000000000..e4f14bf11 --- /dev/null +++ b/pkg/fanout/fanout.go @@ -0,0 +1,70 @@ +// Package fanout implements a fan-out pattern that allows publishing messages to multiple subscribers +package fanout + +import ( + "context" + "errors" +) + +var ( + // ErrSubscriberClosed is returned when a subscriber is closed on the subscriber side (Receive) + ErrSubscriberClosed = errors.New("subscriber closed") + // ErrSubscriberEvicted is returned when a subscriber is unable to keep up with the messages + ErrSubscriberEvicted = errors.New("subscriber evicted, cannot keep up consuming messages") + // ErrStopped is returned when the fanout is stopped + ErrStopped = errors.New("fanout is stopped, no more messages will be accepted") +) + +// FanOut is a fan-out pattern implementation that allows publishing messages to multiple subscribers +type FanOut[T any] struct { + cfg config + done <-chan struct{} + messages chan T + subscribers chan *subscriber[T] +} + +// Start creates and runs a new FanOut +func Start[T any](ctx context.Context, opts ...Option) *FanOut[T] { + cfg := defaultFanOutConfig() + cfg.apply(opts...) + + f := &FanOut[T]{ + cfg: cfg, + done: ctx.Done(), + messages: make(chan T, cfg.publishBufferSize), + subscribers: make(chan *subscriber[T], cfg.subscriberBufferSize), + } + go f.dispatchLoop(ctx) + return f +} + +func (f *FanOut[T]) dispatchLoop(ctx context.Context) { + subscribers := make(subscribers[T]) + defer subscribers.closeAll(ErrStopped) + + for { + select { + case <-ctx.Done(): + return + case sub := <-f.subscribers: + subscribers.add(sub) + continue + case msg := <-f.messages: + subscribers.dispatch(ctx, msg) + } + } +} + +func (f *FanOut[T]) addSubscriber(ctx context.Context, sub *subscriber[T]) error { + ctx, cancel := context.WithTimeout(ctx, f.cfg.addSubscriberTimeout) + defer cancel() + + select { + case <-ctx.Done(): + return context.Cause(ctx) + case <-f.done: + return ErrStopped + case f.subscribers <- sub: + return nil + } +} diff --git a/pkg/fanout/fanout_test.go b/pkg/fanout/fanout_test.go new file mode 100644 index 000000000..8d2a53662 --- /dev/null +++ b/pkg/fanout/fanout_test.go @@ -0,0 +1,182 @@ +package fanout_test + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + + "github.com/pomerium/pomerium/pkg/fanout" +) + +func TestFanOutStopped(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + f := fanout.Start[int](ctx, fanout.WithPublishTimeout(time.Millisecond*10)) + assert.Eventually(t, func() bool { + return errors.Is(f.Publish(context.Background(), 1), fanout.ErrStopped) + }, 5*time.Second, 10*time.Millisecond) + + err := f.Receive(context.Background(), func(ctx context.Context, msg int) error { + return nil + }) + assert.ErrorIs(t, err, fanout.ErrStopped) +} + +func TestFanOutEvictSlowSubscriber(t *testing.T) { + t.Parallel() + + timeout := time.Second * 5 + ctx, cancel := context.WithTimeout(context.Background(), timeout) + t.Cleanup(cancel) + + f := fanout.Start[int](ctx, + fanout.WithReceiverBufferSize(1), + fanout.WithReceiverCallbackTimeout(timeout), + ) + + subscriberAdded := make(chan struct{}) + + eg, ctx := errgroup.WithContext(ctx) + eg.Go(func() error { + err := f.Receive(ctx, func(ctx context.Context, msg int) error { + select { + case <-ctx.Done(): + // context was canceled as expected + // when the subscriber was evicted + case <-time.After(timeout / 2): + t.Error("receiver context was not canceled") + } + return nil + }, fanout.WithOnSubscriberAdded[int](func() { + close(subscriberAdded) + })) + assert.ErrorIs(t, err, fanout.ErrSubscriberEvicted, "expect explicit error indicating subscriber eviction") + return nil + }) + eg.Go(func() error { + select { + case <-ctx.Done(): + return fmt.Errorf("timed out waiting for subscriber: %w", ctx.Err()) + case <-subscriberAdded: + } + // this message will be consumed by the subscriber above, which will block in the callback + assert.NoError(t, f.Publish(ctx, 1)) + // this message will get into fanout-receiver's buffer as the subscriber is blocked + assert.NoError(t, f.Publish(ctx, 1)) + // this messsage will cause the subscriber to be evicted as all buffers are full + assert.NoError(t, f.Publish(ctx, 1)) + return nil + }) + require.NoError(t, eg.Wait()) +} + +func TestFanOutReceiverCancelOnError(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + f := fanout.Start[int](ctx) + receiverErr := errors.New("receiver error") + errch := make(chan error, 1) + + ready := make(chan struct{}) + go func() { + errch <- f.Receive(ctx, func(ctx context.Context, msg int) error { + return receiverErr + }, fanout.WithOnSubscriberAdded[int](func() { close(ready) })) + }() + + <-ready + require.NoError(t, f.Publish(ctx, 1)) + assert.ErrorIs(t, <-errch, receiverErr) +} + +func TestFanOutFilter(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + + f := fanout.Start[int](ctx) + ready := make(chan struct{}) + results := make(chan int) + go func() { + _ = f.Receive(ctx, func(ctx context.Context, msg int) error { + results <- msg + return nil + }, + fanout.WithFilter(func(msg int) bool { return msg%2 == 0 }), + fanout.WithOnSubscriberAdded[int](func() { close(ready) }), + ) + }() + <-ready + t.Log("ready to publish") + for i := 0; i < 10; i++ { + assert.NoError(t, f.Publish(ctx, i)) + } + t.Log("published all messages") + + for i := 0; i < 9; i += 2 { + assert.Equal(t, i, <-results) + } +} + +func BenchmarkFanout(b *testing.B) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) + b.Cleanup(cancel) + + cycles := 1 + + f := fanout.Start[int](ctx) + errStopReceiver := errors.New("stop receiver") + eg, ctx := errgroup.WithContext(ctx) + eg.SetLimit(-1) + ready := make(chan struct{}, b.N) + for i := 0; i < b.N; i++ { + want := i + eg.Go(func() error { + seen := 0 + err := f.Receive(ctx, func(ctx context.Context, _ int) error { + if seen++; seen == cycles { + return errStopReceiver + } + return nil + }, + fanout.WithOnSubscriberAdded[int](func() { ready <- struct{}{} }), + fanout.WithFilter(func(msg int) bool { return msg == want }), + ) + if !errors.Is(err, errStopReceiver) && !errors.Is(err, context.Canceled) { + b.Error(err) + return err + } + return nil + }) + } + eg.Go(func() error { + for i := 0; i < b.N; i++ { + <-ready + } + + for c := 0; c < cycles; c++ { + for i := 0; i < b.N; i++ { + err := f.Publish(ctx, i) + if err != nil { + b.Error(err) + return err + } + } + } + return nil + }) + require.NoError(b, eg.Wait()) +} diff --git a/pkg/fanout/publish.go b/pkg/fanout/publish.go new file mode 100644 index 000000000..3bd549d22 --- /dev/null +++ b/pkg/fanout/publish.go @@ -0,0 +1,19 @@ +package fanout + +import "context" + +// Publish publishes a message to all currently registered subscribers +// if the fanout is closed, ErrStopped is returned +func (f *FanOut[T]) Publish(ctx context.Context, msg T) error { + ctx, cancel := context.WithTimeout(ctx, f.cfg.publishTimeout) + defer cancel() + + select { + case <-ctx.Done(): + return ctx.Err() + case <-f.done: + return ErrStopped + case f.messages <- msg: + return nil + } +} diff --git a/pkg/fanout/receive.go b/pkg/fanout/receive.go new file mode 100644 index 000000000..318fa0c25 --- /dev/null +++ b/pkg/fanout/receive.go @@ -0,0 +1,106 @@ +package fanout + +import ( + "context" + "fmt" + "time" +) + +// ReceiverCallback is the callback function that is called for each message received +// if an error is returned, Receive will return immediately with that error, closing the subscriber +type ReceiverCallback[T any] func(ctx context.Context, msg T) error + +// ReceiveOption is an option for receiver +type ReceiveOption[T any] func(*subscriber[T]) + +// WithFilter returns a ReceiveOption that filters messages for the subscriber +// if the filter returns false, the message is not sent to the subscriber +// this function is called for each message received and subsequently for each subscriber +// and should not be computationally expensive or block +func WithFilter[T any](filter func(T) bool) ReceiveOption[T] { + return func(sub *subscriber[T]) { + sub.filter = filter + } +} + +// WithOnSubscriberAdded should only be used for tests +func WithOnSubscriberAdded[T any](onAdded func()) ReceiveOption[T] { + return func(sub *subscriber[T]) { + sub.onAdded = onAdded + } +} + +// Receive subscribes to receive messages until the context is canceled or an error occurs +// onMessage is called for each message received. +// if an error is returned, Receive will return immediately +func (f *FanOut[T]) Receive(ctx context.Context, onMessage ReceiverCallback[T], opts ...ReceiveOption[T]) error { + ctx, cancel := context.WithCancelCause(ctx) + defer cancel(nil) + + messages := make(chan T, f.cfg.receiverBufferSize) + sub := newSubscriber[T](messages, f.done, cancel, opts...) + + err := f.addSubscriber(ctx, sub) + if err != nil { + return fmt.Errorf("add subscriber: %w", err) + } + + err = f.receiveLoop(ctx, messages, onMessage) + if err != nil { + return fmt.Errorf("receive: %w", err) + } + + return nil +} + +func newSubscriber[T any]( + messages chan<- T, + done <-chan struct{}, + cancel context.CancelCauseFunc, + opts ...ReceiveOption[T], +) *subscriber[T] { + sub := &subscriber[T]{ + messages: messages, + done: done, + cancel: cancel, + } + for _, opt := range opts { + opt(sub) + } + return sub +} + +func (f *FanOut[T]) receiveLoop( + ctx context.Context, + messages <-chan T, + onMessage ReceiverCallback[T], +) error { + for { + select { + case <-ctx.Done(): + return context.Cause(ctx) + case <-f.done: + return ErrStopped + case msg, ok := <-messages: + if !ok { + return ErrSubscriberEvicted + } + err := callWithTimeout(ctx, f.cfg.receiverCallbackTimeout, onMessage, msg) + if err != nil { + return fmt.Errorf("onMessage callback: %w", err) + } + } + } +} + +func callWithTimeout[T any]( + ctx context.Context, + timeout time.Duration, + cb ReceiverCallback[T], + msg T, +) error { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + return cb(ctx, msg) +} diff --git a/pkg/fanout/subscribers.go b/pkg/fanout/subscribers.go new file mode 100644 index 000000000..9a68b505a --- /dev/null +++ b/pkg/fanout/subscribers.go @@ -0,0 +1,65 @@ +package fanout + +import ( + "context" +) + +// subscriber represents an individual subscriber to a fanout +type subscriber[T any] struct { + // messages is the channel that the subscriber receives messages on + messages chan<- T + // done is closed when the subscriber is closed + // it is used to signal the dispatchLoop that the subscriber is closed and should be removed + done <-chan struct{} + // cancel is passed so that dispatchLoop can cancel the subscriber if it discards it from its side + cancel context.CancelCauseFunc + // filter identifies the messages that the subscriber is interested in. + filter func(T) bool + // onAdded is called when the subscriber is added to the fanout + // it is only used for tests + onAdded func() +} + +// subscriberCloseFn is a function that closes a subscriber and propagates an error to it +type subscriberCloseFn func(err error) + +// subscribers is a map of subscribers to their close functions +type subscribers[T any] map[*subscriber[T]]subscriberCloseFn + +// closeAll closes all subscribers and propagates the given error to them +func (s subscribers[T]) closeAll(err error) { + for _, close := range s { + close(err) + } +} + +// add adds subscriber to the fanout +func (s subscribers[T]) add(sub *subscriber[T]) { + s[sub] = func(err error) { + close(sub.messages) + sub.cancel(err) + delete(s, sub) + } + if sub.onAdded != nil { + sub.onAdded() + } +} + +// dispatch dispatches the given message to all subscribers +func (s subscribers[T]) dispatch(ctx context.Context, msg T) { + for sub, close := range s { + if sub.filter != nil && !sub.filter(msg) { + continue + } + + select { + case <-ctx.Done(): + return + case sub.messages <- msg: + case <-sub.done: + close(ErrSubscriberClosed) + default: + close(ErrSubscriberEvicted) + } + } +} diff --git a/pkg/zero/api.go b/pkg/zero/api.go new file mode 100644 index 000000000..70141bff1 --- /dev/null +++ b/pkg/zero/api.go @@ -0,0 +1,123 @@ +// Package zero contains the pomerium zero configuration API client +package zero + +import ( + "context" + "fmt" + + "github.com/pomerium/pomerium/pkg/fanout" + "github.com/pomerium/pomerium/pkg/zero/apierror" + cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster" + connect_api "github.com/pomerium/pomerium/pkg/zero/connect" + connect_mux "github.com/pomerium/pomerium/pkg/zero/connect-mux" + token_api "github.com/pomerium/pomerium/pkg/zero/token" +) + +// API is a Pomerium Zero Cluster API client +type API struct { + cfg *config + cluster cluster_api.ClientWithResponsesInterface + mux *connect_mux.Mux + downloadURLCache *cluster_api.URLCache +} + +// WatchOption defines which events to watch for +type WatchOption = connect_mux.WatchOption + +// NewAPI creates a new API client +func NewAPI(ctx context.Context, opts ...Option) (*API, error) { + cfg, err := newConfig(opts...) + if err != nil { + return nil, err + } + + fetcher, err := cluster_api.NewTokenFetcher(cfg.clusterAPIEndpoint, + cluster_api.WithHTTPClient(cfg.httpClient), + ) + if err != nil { + return nil, fmt.Errorf("error creating token fetcher: %w", err) + } + + tokenCache := token_api.NewCache(fetcher, cfg.apiToken) + + clusterClient, err := cluster_api.NewAuthorizedClient(cfg.clusterAPIEndpoint, tokenCache.GetToken, cfg.httpClient) + if err != nil { + return nil, fmt.Errorf("error creating cluster client: %w", err) + } + + connectClient, err := connect_api.NewAuthorizedConnectClient(ctx, cfg.connectAPIEndpoint, tokenCache.GetToken) + if err != nil { + return nil, fmt.Errorf("error creating connect client: %w", err) + } + + return &API{ + cfg: cfg, + cluster: clusterClient, + mux: connect_mux.New(connectClient), + downloadURLCache: cluster_api.NewURLCache(), + }, nil +} + +// Connect connects to the connect API and allows watching for changes +func (api *API) Connect(ctx context.Context, opts ...fanout.Option) error { + return api.mux.Run(ctx, opts...) +} + +// Watch dispatches API updates +func (api *API) Watch(ctx context.Context, opts ...WatchOption) error { + return api.mux.Watch(ctx, opts...) +} + +// GetClusterBootstrapConfig fetches the bootstrap configuration from the cluster API +func (api *API) GetClusterBootstrapConfig(ctx context.Context) (*cluster_api.BootstrapConfig, error) { + return apierror.CheckResponse[cluster_api.BootstrapConfig]( + api.cluster.GetClusterBootstrapConfigWithResponse(ctx), + ) +} + +// GetClusterResourceBundles fetches the resource bundles from the cluster API +func (api *API) GetClusterResourceBundles(ctx context.Context) (*cluster_api.GetBundlesResponse, error) { + return apierror.CheckResponse[cluster_api.GetBundlesResponse]( + api.cluster.GetClusterResourceBundlesWithResponse(ctx), + ) +} + +// ReportBundleAppliedSuccess reports a successful bundle application +func (api *API) ReportBundleAppliedSuccess(ctx context.Context, bundleID string, metadata map[string]string) error { + status := cluster_api.BundleStatus{ + Success: &cluster_api.BundleStatusSuccess{ + Metadata: metadata, + }, + } + + _, err := apierror.CheckResponse[cluster_api.EmptyResponse]( + api.cluster.ReportClusterResourceBundleStatusWithResponse(ctx, bundleID, status), + ) + if err != nil { + return fmt.Errorf("error reporting bundle status: %w", err) + } + return err +} + +// ReportBundleAppliedFailure reports a failed bundle application +func (api *API) ReportBundleAppliedFailure( + ctx context.Context, + bundleID string, + source cluster_api.BundleStatusFailureSource, + err error, +) error { + status := cluster_api.BundleStatus{ + Failure: &cluster_api.BundleStatusFailure{ + Message: err.Error(), + Source: source, + }, + } + + _, err = apierror.CheckResponse[cluster_api.EmptyResponse]( + api.cluster.ReportClusterResourceBundleStatusWithResponse(ctx, bundleID, status), + ) + if err != nil { + return fmt.Errorf("error reporting bundle status: %w", err) + } + return err +} diff --git a/pkg/zero/apierror/request_id.go b/pkg/zero/apierror/request_id.go new file mode 100644 index 000000000..7e4d22251 --- /dev/null +++ b/pkg/zero/apierror/request_id.go @@ -0,0 +1,42 @@ +package apierror + +import ( + "fmt" + "net/http" +) + +// RequestIDError is an error that wraps another error and includes the response ID +type RequestIDError struct { + Err error + ResponseID *string +} + +// Error implements error for RequestIDError +func (e *RequestIDError) Error() string { + if e.ResponseID == nil { + return e.Err.Error() + } + return fmt.Sprintf("[x-response-id:%s]: %v", *e.ResponseID, e.Err) +} + +// Unwrap implements errors.Unwrap for RequestIDError +func (e *RequestIDError) Unwrap() error { + return e.Err +} + +// Is implements errors.Is for RequestIDError +func (e *RequestIDError) Is(err error) bool { + //nolint:errorlint + _, ok := err.(*RequestIDError) + return ok +} + +// WithRequestID creates a new RequestIDError +func WithRequestID(err error, headers http.Header) *RequestIDError { + r := &RequestIDError{Err: err} + id := headers.Get("X-Response-Id") + if id != "" { + r.ResponseID = &id + } + return r +} diff --git a/pkg/zero/apierror/response.go b/pkg/zero/apierror/response.go new file mode 100644 index 000000000..ef991c917 --- /dev/null +++ b/pkg/zero/apierror/response.go @@ -0,0 +1,56 @@ +// Package apierror provides a consistent way to handle errors from API calls +package apierror + +import ( + "fmt" + "net/http" +) + +// CheckResponse checks the response for errors and returns the value or an error +func CheckResponse[T any](resp APIResponse[T], err error) (*T, error) { + if err != nil { + return nil, err + } + + value := resp.GetValue() + if value != nil { + return value, nil + } + + //nolint:bodyclose + return nil, WithRequestID(responseError(resp), resp.GetHTTPResponse().Header) +} + +// APIResponse is the interface that wraps the response from an API call +type APIResponse[T any] interface { + // GetHTTPResponse returns the HTTP response + GetHTTPResponse() *http.Response + // GetInternalServerError returns the internal server error + GetInternalServerError() (string, bool) + // GetBadRequestError returns the bad request error + GetBadRequestError() (string, bool) + // GetValue returns the value + GetValue() *T +} + +// Error is the interface that wraps the error returned from an API call +type Error interface { + GetError() string +} + +func responseError[T any](resp APIResponse[T]) error { + reason, ok := resp.GetBadRequestError() + if ok { + return NewTerminalError(fmt.Errorf("bad request: %v", reason)) + } + reason, ok = resp.GetInternalServerError() + if ok { + return fmt.Errorf("internal server error: %v", reason) + } + //nolint:bodyclose + httpResp := resp.GetHTTPResponse() + if httpResp == nil { + return fmt.Errorf("unexpected response: nil") + } + return fmt.Errorf("unexpected response: %v", httpResp.StatusCode) +} diff --git a/pkg/zero/apierror/response_test.go b/pkg/zero/apierror/response_test.go new file mode 100644 index 000000000..ce90a1cfa --- /dev/null +++ b/pkg/zero/apierror/response_test.go @@ -0,0 +1,40 @@ +package apierror_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/pomerium/pomerium/pkg/zero/apierror" + "github.com/pomerium/pomerium/pkg/zero/cluster" +) + +func TestResponse(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + err error + response apierror.APIResponse[cluster.ExchangeTokenResponse] + wantVal *cluster.ExchangeTokenResponse + wantErr error + }{ + { + name: "success", + response: &cluster.ExchangeClusterIdentityTokenResp{ + HTTPResponse: &http.Response{}, + JSON200: &cluster.ExchangeTokenResponse{}, + }, + err: nil, + wantVal: &cluster.ExchangeTokenResponse{}, + wantErr: nil, + }, + } { + t.Run(tc.name, func(t *testing.T) { + gotVal, gotErr := apierror.CheckResponse(tc.response, tc.err) + assert.Equal(t, tc.wantVal, gotVal) + assert.Equal(t, tc.wantErr, gotErr) + }) + } +} diff --git a/pkg/zero/apierror/terminal.go b/pkg/zero/apierror/terminal.go new file mode 100644 index 000000000..6cefb05c2 --- /dev/null +++ b/pkg/zero/apierror/terminal.go @@ -0,0 +1,44 @@ +package apierror + +import ( + "errors" + "fmt" +) + +// terminalError is an error that should not be retried +type terminalError struct { + Err error +} + +// Error implements error for terminalError +func (e *terminalError) Error() string { + return fmt.Sprintf("terminal error: %v", e.Err) +} + +// Unwrap implements errors.Unwrap for terminalError +func (e *terminalError) Unwrap() error { + return e.Err +} + +// Is implements errors.Is for terminalError +func (e *terminalError) Is(err error) bool { + //nolint:errorlint + _, ok := err.(*terminalError) + return ok +} + +func (e *terminalError) IsTerminal() {} + +// NewTerminalError creates a new terminal error that cannot be retried +func NewTerminalError(err error) error { + return &terminalError{Err: err} +} + +// IsTerminalError returns true if the error is a terminal error +func IsTerminalError(err error) bool { + if err == nil { + return false + } + var te *terminalError + return errors.As(err, &te) +} diff --git a/pkg/zero/cluster/client.gen.go b/pkg/zero/cluster/client.gen.go new file mode 100644 index 000000000..84397258c --- /dev/null +++ b/pkg/zero/cluster/client.gen.go @@ -0,0 +1,813 @@ +// Package cluster provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.16.2 DO NOT EDIT. +package cluster + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/oapi-codegen/runtime" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetClusterBootstrapConfig request + GetClusterBootstrapConfig(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetClusterResourceBundles request + GetClusterResourceBundles(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DownloadClusterResourceBundle request + DownloadClusterResourceBundle(ctx context.Context, bundleId BundleId, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ReportClusterResourceBundleStatusWithBody request with any body + ReportClusterResourceBundleStatusWithBody(ctx context.Context, bundleId BundleId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + ReportClusterResourceBundleStatus(ctx context.Context, bundleId BundleId, body ReportClusterResourceBundleStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ExchangeClusterIdentityTokenWithBody request with any body + ExchangeClusterIdentityTokenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + ExchangeClusterIdentityToken(ctx context.Context, body ExchangeClusterIdentityTokenJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetClusterBootstrapConfig(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetClusterBootstrapConfigRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetClusterResourceBundles(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetClusterResourceBundlesRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DownloadClusterResourceBundle(ctx context.Context, bundleId BundleId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDownloadClusterResourceBundleRequest(c.Server, bundleId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ReportClusterResourceBundleStatusWithBody(ctx context.Context, bundleId BundleId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewReportClusterResourceBundleStatusRequestWithBody(c.Server, bundleId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ReportClusterResourceBundleStatus(ctx context.Context, bundleId BundleId, body ReportClusterResourceBundleStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewReportClusterResourceBundleStatusRequest(c.Server, bundleId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ExchangeClusterIdentityTokenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExchangeClusterIdentityTokenRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ExchangeClusterIdentityToken(ctx context.Context, body ExchangeClusterIdentityTokenJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExchangeClusterIdentityTokenRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetClusterBootstrapConfigRequest generates requests for GetClusterBootstrapConfig +func NewGetClusterBootstrapConfigRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/bootstrap") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetClusterResourceBundlesRequest generates requests for GetClusterResourceBundles +func NewGetClusterResourceBundlesRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/bundles") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDownloadClusterResourceBundleRequest generates requests for DownloadClusterResourceBundle +func NewDownloadClusterResourceBundleRequest(server string, bundleId BundleId) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "bundleId", runtime.ParamLocationPath, bundleId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/bundles/%s/download", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewReportClusterResourceBundleStatusRequest calls the generic ReportClusterResourceBundleStatus builder with application/json body +func NewReportClusterResourceBundleStatusRequest(server string, bundleId BundleId, body ReportClusterResourceBundleStatusJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewReportClusterResourceBundleStatusRequestWithBody(server, bundleId, "application/json", bodyReader) +} + +// NewReportClusterResourceBundleStatusRequestWithBody generates requests for ReportClusterResourceBundleStatus with any type of body +func NewReportClusterResourceBundleStatusRequestWithBody(server string, bundleId BundleId, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "bundleId", runtime.ParamLocationPath, bundleId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/bundles/%s/status", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewExchangeClusterIdentityTokenRequest calls the generic ExchangeClusterIdentityToken builder with application/json body +func NewExchangeClusterIdentityTokenRequest(server string, body ExchangeClusterIdentityTokenJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewExchangeClusterIdentityTokenRequestWithBody(server, "application/json", bodyReader) +} + +// NewExchangeClusterIdentityTokenRequestWithBody generates requests for ExchangeClusterIdentityToken with any type of body +func NewExchangeClusterIdentityTokenRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/exchangeToken") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetClusterBootstrapConfigWithResponse request + GetClusterBootstrapConfigWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetClusterBootstrapConfigResp, error) + + // GetClusterResourceBundlesWithResponse request + GetClusterResourceBundlesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetClusterResourceBundlesResp, error) + + // DownloadClusterResourceBundleWithResponse request + DownloadClusterResourceBundleWithResponse(ctx context.Context, bundleId BundleId, reqEditors ...RequestEditorFn) (*DownloadClusterResourceBundleResp, error) + + // ReportClusterResourceBundleStatusWithBodyWithResponse request with any body + ReportClusterResourceBundleStatusWithBodyWithResponse(ctx context.Context, bundleId BundleId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ReportClusterResourceBundleStatusResp, error) + + ReportClusterResourceBundleStatusWithResponse(ctx context.Context, bundleId BundleId, body ReportClusterResourceBundleStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*ReportClusterResourceBundleStatusResp, error) + + // ExchangeClusterIdentityTokenWithBodyWithResponse request with any body + ExchangeClusterIdentityTokenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ExchangeClusterIdentityTokenResp, error) + + ExchangeClusterIdentityTokenWithResponse(ctx context.Context, body ExchangeClusterIdentityTokenJSONRequestBody, reqEditors ...RequestEditorFn) (*ExchangeClusterIdentityTokenResp, error) +} + +type GetClusterBootstrapConfigResp struct { + Body []byte + HTTPResponse *http.Response + JSON200 *GetBootstrapConfigResponse + JSON400 *ErrorResponse + JSON500 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r GetClusterBootstrapConfigResp) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetClusterBootstrapConfigResp) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetClusterResourceBundlesResp struct { + Body []byte + HTTPResponse *http.Response + JSON200 *GetBundlesResponse + JSON400 *ErrorResponse + JSON500 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r GetClusterResourceBundlesResp) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetClusterResourceBundlesResp) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DownloadClusterResourceBundleResp struct { + Body []byte + HTTPResponse *http.Response + JSON200 *DownloadBundleResponse + JSON400 *ErrorResponse + JSON404 *ErrorResponse + JSON500 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r DownloadClusterResourceBundleResp) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DownloadClusterResourceBundleResp) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ReportClusterResourceBundleStatusResp struct { + Body []byte + HTTPResponse *http.Response + JSON400 *ErrorResponse + JSON500 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r ReportClusterResourceBundleStatusResp) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ReportClusterResourceBundleStatusResp) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ExchangeClusterIdentityTokenResp struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ExchangeTokenResponse + JSON400 *ErrorResponse + JSON500 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r ExchangeClusterIdentityTokenResp) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ExchangeClusterIdentityTokenResp) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetClusterBootstrapConfigWithResponse request returning *GetClusterBootstrapConfigResp +func (c *ClientWithResponses) GetClusterBootstrapConfigWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetClusterBootstrapConfigResp, error) { + rsp, err := c.GetClusterBootstrapConfig(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetClusterBootstrapConfigResp(rsp) +} + +// GetClusterResourceBundlesWithResponse request returning *GetClusterResourceBundlesResp +func (c *ClientWithResponses) GetClusterResourceBundlesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetClusterResourceBundlesResp, error) { + rsp, err := c.GetClusterResourceBundles(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetClusterResourceBundlesResp(rsp) +} + +// DownloadClusterResourceBundleWithResponse request returning *DownloadClusterResourceBundleResp +func (c *ClientWithResponses) DownloadClusterResourceBundleWithResponse(ctx context.Context, bundleId BundleId, reqEditors ...RequestEditorFn) (*DownloadClusterResourceBundleResp, error) { + rsp, err := c.DownloadClusterResourceBundle(ctx, bundleId, reqEditors...) + if err != nil { + return nil, err + } + return ParseDownloadClusterResourceBundleResp(rsp) +} + +// ReportClusterResourceBundleStatusWithBodyWithResponse request with arbitrary body returning *ReportClusterResourceBundleStatusResp +func (c *ClientWithResponses) ReportClusterResourceBundleStatusWithBodyWithResponse(ctx context.Context, bundleId BundleId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ReportClusterResourceBundleStatusResp, error) { + rsp, err := c.ReportClusterResourceBundleStatusWithBody(ctx, bundleId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseReportClusterResourceBundleStatusResp(rsp) +} + +func (c *ClientWithResponses) ReportClusterResourceBundleStatusWithResponse(ctx context.Context, bundleId BundleId, body ReportClusterResourceBundleStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*ReportClusterResourceBundleStatusResp, error) { + rsp, err := c.ReportClusterResourceBundleStatus(ctx, bundleId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseReportClusterResourceBundleStatusResp(rsp) +} + +// ExchangeClusterIdentityTokenWithBodyWithResponse request with arbitrary body returning *ExchangeClusterIdentityTokenResp +func (c *ClientWithResponses) ExchangeClusterIdentityTokenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ExchangeClusterIdentityTokenResp, error) { + rsp, err := c.ExchangeClusterIdentityTokenWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseExchangeClusterIdentityTokenResp(rsp) +} + +func (c *ClientWithResponses) ExchangeClusterIdentityTokenWithResponse(ctx context.Context, body ExchangeClusterIdentityTokenJSONRequestBody, reqEditors ...RequestEditorFn) (*ExchangeClusterIdentityTokenResp, error) { + rsp, err := c.ExchangeClusterIdentityToken(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseExchangeClusterIdentityTokenResp(rsp) +} + +// ParseGetClusterBootstrapConfigResp parses an HTTP response from a GetClusterBootstrapConfigWithResponse call +func ParseGetClusterBootstrapConfigResp(rsp *http.Response) (*GetClusterBootstrapConfigResp, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetClusterBootstrapConfigResp{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest GetBootstrapConfigResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseGetClusterResourceBundlesResp parses an HTTP response from a GetClusterResourceBundlesWithResponse call +func ParseGetClusterResourceBundlesResp(rsp *http.Response) (*GetClusterResourceBundlesResp, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetClusterResourceBundlesResp{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest GetBundlesResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseDownloadClusterResourceBundleResp parses an HTTP response from a DownloadClusterResourceBundleWithResponse call +func ParseDownloadClusterResourceBundleResp(rsp *http.Response) (*DownloadClusterResourceBundleResp, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DownloadClusterResourceBundleResp{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DownloadBundleResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseReportClusterResourceBundleStatusResp parses an HTTP response from a ReportClusterResourceBundleStatusWithResponse call +func ParseReportClusterResourceBundleStatusResp(rsp *http.Response) (*ReportClusterResourceBundleStatusResp, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ReportClusterResourceBundleStatusResp{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseExchangeClusterIdentityTokenResp parses an HTTP response from a ExchangeClusterIdentityTokenWithResponse call +func ParseExchangeClusterIdentityTokenResp(rsp *http.Response) (*ExchangeClusterIdentityTokenResp, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ExchangeClusterIdentityTokenResp{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ExchangeTokenResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} diff --git a/pkg/zero/cluster/client.go b/pkg/zero/cluster/client.go new file mode 100644 index 000000000..d2ddea8c0 --- /dev/null +++ b/pkg/zero/cluster/client.go @@ -0,0 +1,49 @@ +// Package cluster is an API client for the cluster service +package cluster + +import ( + "context" + "fmt" + "net/http" + "time" +) + +const ( + defaultMinTokenTTL = time.Minute * 5 +) + +type client struct { + tokenProvider TokenProviderFn + httpClient *http.Client + minTokenTTL time.Duration +} + +// TokenProviderFn is a function that returns a token that is expected to be valid for at least minTTL +type TokenProviderFn func(ctx context.Context, minTTL time.Duration) (string, error) + +// NewAuthorizedClient creates a new HTTP client that will automatically add an authorization header +func NewAuthorizedClient( + endpoint string, + tokenProvider TokenProviderFn, + httpClient *http.Client, +) (ClientWithResponsesInterface, error) { + c := &client{ + minTokenTTL: defaultMinTokenTTL, + httpClient: httpClient, + } + + c.tokenProvider = tokenProvider + + return NewClientWithResponses(endpoint, WithHTTPClient(c)) +} + +func (c *client) Do(req *http.Request) (*http.Response, error) { + ctx := req.Context() + token, err := c.tokenProvider(ctx, c.minTokenTTL) + if err != nil { + return nil, fmt.Errorf("error getting token: %w", err) + } + req.Header.Set("Authorization", "Bearer "+token) + + return c.httpClient.Do(req) +} diff --git a/pkg/zero/cluster/client.yaml b/pkg/zero/cluster/client.yaml new file mode 100644 index 000000000..d1e7c1b65 --- /dev/null +++ b/pkg/zero/cluster/client.yaml @@ -0,0 +1,10 @@ +package: cluster +generate: + client: true + models: false +output: client.gen.go +output-options: + skip-prune: true + # We use Response suffix internally throughout the response objects, + # that conflicts with generated client + response-type-suffix: Resp diff --git a/pkg/zero/cluster/client_errors.go b/pkg/zero/cluster/client_errors.go new file mode 100644 index 000000000..74761c499 --- /dev/null +++ b/pkg/zero/cluster/client_errors.go @@ -0,0 +1,148 @@ +package cluster + +import ( + "net/http" + + "github.com/pomerium/pomerium/pkg/zero/apierror" +) + +// EmptyResponse is an empty response +type EmptyResponse struct{} + +var ( + _ apierror.APIResponse[ExchangeTokenResponse] = (*ExchangeClusterIdentityTokenResp)(nil) + _ apierror.APIResponse[BootstrapConfig] = (*GetClusterBootstrapConfigResp)(nil) + _ apierror.APIResponse[GetBundlesResponse] = (*GetClusterResourceBundlesResp)(nil) + _ apierror.APIResponse[DownloadBundleResponse] = (*DownloadClusterResourceBundleResp)(nil) + _ apierror.APIResponse[EmptyResponse] = (*ReportClusterResourceBundleStatusResp)(nil) +) + +// GetBadRequestError implements apierror.APIResponse +func (r *ExchangeClusterIdentityTokenResp) GetBadRequestError() (string, bool) { + if r.JSON400 == nil { + return "", false + } + return r.JSON400.Error, true +} + +// GetInternalServerError implements apierror.APIResponse +func (r *ExchangeClusterIdentityTokenResp) GetInternalServerError() (string, bool) { + if r.JSON500 == nil { + return "", false + } + return r.JSON500.Error, true +} + +// GetValue implements apierror.APIResponse +func (r *ExchangeClusterIdentityTokenResp) GetValue() *ExchangeTokenResponse { + return r.JSON200 +} + +// GetHTTPResponse implements apierror.APIResponse +func (r *ExchangeClusterIdentityTokenResp) GetHTTPResponse() *http.Response { + return r.HTTPResponse +} + +// GetBadRequestError implements apierror.APIResponse +func (r *GetClusterBootstrapConfigResp) GetBadRequestError() (string, bool) { + if r.JSON400 == nil { + return "", false + } + return r.JSON400.Error, true +} + +// GetInternalServerError implements apierror.APIResponse +func (r *GetClusterBootstrapConfigResp) GetInternalServerError() (string, bool) { + if r.JSON500 == nil { + return "", false + } + return r.JSON500.Error, true +} + +// GetValue implements apierror.APIResponse +func (r *GetClusterBootstrapConfigResp) GetValue() *BootstrapConfig { + return r.JSON200 +} + +// GetHTTPResponse implements apierror.APIResponse +func (r *GetClusterBootstrapConfigResp) GetHTTPResponse() *http.Response { + return r.HTTPResponse +} + +// GetBadRequestError implements apierror.APIResponse +func (r *GetClusterResourceBundlesResp) GetBadRequestError() (string, bool) { + if r.JSON400 == nil { + return "", false + } + return r.JSON400.Error, true +} + +// GetInternalServerError implements apierror.APIResponse +func (r *GetClusterResourceBundlesResp) GetInternalServerError() (string, bool) { + if r.JSON500 == nil { + return "", false + } + return r.JSON500.Error, true +} + +// GetValue implements apierror.APIResponse +func (r *GetClusterResourceBundlesResp) GetValue() *GetBundlesResponse { + return r.JSON200 +} + +// GetHTTPResponse implements apierror.APIResponse +func (r *GetClusterResourceBundlesResp) GetHTTPResponse() *http.Response { + return r.HTTPResponse +} + +// GetBadRequestError implements apierror.APIResponse +func (r *DownloadClusterResourceBundleResp) GetBadRequestError() (string, bool) { + if r.JSON400 == nil { + return "", false + } + return r.JSON400.Error, true +} + +// GetInternalServerError implements apierror.APIResponse +func (r *DownloadClusterResourceBundleResp) GetInternalServerError() (string, bool) { + if r.JSON500 == nil { + return "", false + } + return r.JSON500.Error, true +} + +// GetValue implements apierror.APIResponse +func (r *DownloadClusterResourceBundleResp) GetValue() *DownloadBundleResponse { + return r.JSON200 +} + +// GetHTTPResponse implements apierror.APIResponse +func (r *DownloadClusterResourceBundleResp) GetHTTPResponse() *http.Response { + return r.HTTPResponse +} + +// GetBadRequestError implements apierror.APIResponse +func (r *ReportClusterResourceBundleStatusResp) GetBadRequestError() (string, bool) { + if r.JSON400 == nil { + return "", false + } + return r.JSON400.Error, true +} + +// GetInternalServerError implements apierror.APIResponse +func (r *ReportClusterResourceBundleStatusResp) GetInternalServerError() (string, bool) { + if r.JSON500 == nil { + return "", false + } + return r.JSON500.Error, true +} + +// GetValue implements apierror.APIResponse +func (r *ReportClusterResourceBundleStatusResp) GetValue() *EmptyResponse { + return &EmptyResponse{} +} + +// GetHTTPResponse implements apierror.APIResponse +func (r *ReportClusterResourceBundleStatusResp) GetHTTPResponse() *http.Response { + return r.HTTPResponse +} diff --git a/pkg/zero/cluster/client_test.go b/pkg/zero/cluster/client_test.go new file mode 100644 index 000000000..6c317a874 --- /dev/null +++ b/pkg/zero/cluster/client_test.go @@ -0,0 +1,59 @@ +package cluster_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + api "github.com/pomerium/pomerium/pkg/zero/cluster" + "github.com/pomerium/pomerium/pkg/zero/token" +) + +func TestAPIClient(t *testing.T) { + t.Parallel() + + respond := func(w http.ResponseWriter, status int, body any) { + t.Helper() + data, err := json.Marshal(body) + require.NoError(t, err) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + + _, err = w.Write(data) + require.NoError(t, err) + } + + idToken := "id-token" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/exchangeToken": + respond(w, http.StatusOK, api.ExchangeTokenResponse{ + IdToken: idToken, + ExpiresInSeconds: "3600", + }) + default: + t.Error("unexpected request", r.URL.Path) + } + })) + t.Cleanup(srv.Close) + + fetcher, err := api.NewTokenFetcher(srv.URL) + require.NoError(t, err) + + tokenCache := token.NewCache(fetcher, "refresh-token") + client, err := api.NewAuthorizedClient(srv.URL, tokenCache.GetToken, http.DefaultClient) + require.NoError(t, err) + + resp, err := client.ExchangeClusterIdentityTokenWithResponse(context.Background(), + api.ExchangeTokenRequest{ + RefreshToken: "refresh-token", + }, + ) + require.NoError(t, err) + require.Equal(t, idToken, resp.JSON200.IdToken) +} diff --git a/pkg/zero/cluster/generate.go b/pkg/zero/cluster/generate.go new file mode 100644 index 000000000..87b8fb40d --- /dev/null +++ b/pkg/zero/cluster/generate.go @@ -0,0 +1,5 @@ +package cluster + +//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --config=models.yaml openapi.yaml +//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --config=server.yaml openapi.yaml +//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --config=client.yaml openapi.yaml diff --git a/pkg/zero/cluster/models.gen.go b/pkg/zero/cluster/models.gen.go new file mode 100644 index 000000000..cfe91c6a2 --- /dev/null +++ b/pkg/zero/cluster/models.gen.go @@ -0,0 +1,99 @@ +// Package cluster provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.16.2 DO NOT EDIT. +package cluster + +const ( + BearerAuthScopes = "bearerAuth.Scopes" +) + +// Defines values for BundleStatusFailureSource. +const ( + DatabrokerError BundleStatusFailureSource = "databroker_error" + DownloadError BundleStatusFailureSource = "download_error" + InvalidBundle BundleStatusFailureSource = "invalid_bundle" + IoError BundleStatusFailureSource = "io_error" + UnknownError BundleStatusFailureSource = "unknown_error" +) + +// BootstrapConfig defines model for BootstrapConfig. +type BootstrapConfig struct { + // DatabrokerStorageConnection databroker storage connection string + DatabrokerStorageConnection *string `json:"databrokerStorageConnection,omitempty"` +} + +// Bundle defines model for Bundle. +type Bundle struct { + // Id bundle id + Id string `json:"id"` +} + +// BundleStatus defines model for BundleStatus. +type BundleStatus struct { + Failure *BundleStatusFailure `json:"failure,omitempty"` + Success *BundleStatusSuccess `json:"success,omitempty"` +} + +// BundleStatusFailure defines model for BundleStatusFailure. +type BundleStatusFailure struct { + Message string `json:"message"` + + // Source source of the failure + Source BundleStatusFailureSource `json:"source"` +} + +// BundleStatusFailureSource source of the failure +type BundleStatusFailureSource string + +// BundleStatusSuccess defines model for BundleStatusSuccess. +type BundleStatusSuccess struct { + // Metadata bundle metadata + Metadata map[string]string `json:"metadata"` +} + +// DownloadBundleResponse defines model for DownloadBundleResponse. +type DownloadBundleResponse struct { + // CaptureMetadataHeaders bundle metadata that need be picked up by the client from the download URL + CaptureMetadataHeaders []string `json:"captureMetadataHeaders"` + ExpiresInSeconds string `json:"expiresInSeconds"` + + // Url download URL + Url string `json:"url"` +} + +// ErrorResponse defines model for ErrorResponse. +type ErrorResponse struct { + // Error Error message + Error string `json:"error"` +} + +// ExchangeTokenRequest defines model for ExchangeTokenRequest. +type ExchangeTokenRequest struct { + // RefreshToken cluster identity token + RefreshToken string `json:"refreshToken"` +} + +// ExchangeTokenResponse defines model for ExchangeTokenResponse. +type ExchangeTokenResponse struct { + ExpiresInSeconds string `json:"expiresInSeconds"` + + // IdToken ID token + IdToken string `json:"idToken"` +} + +// GetBootstrapConfigResponse defines model for GetBootstrapConfigResponse. +type GetBootstrapConfigResponse = BootstrapConfig + +// GetBundlesResponse defines model for GetBundlesResponse. +type GetBundlesResponse struct { + Bundles []Bundle `json:"bundles"` +} + +// BundleId defines model for bundleId. +type BundleId = string + +// ReportClusterResourceBundleStatusJSONRequestBody defines body for ReportClusterResourceBundleStatus for application/json ContentType. +type ReportClusterResourceBundleStatusJSONRequestBody = BundleStatus + +// ExchangeClusterIdentityTokenJSONRequestBody defines body for ExchangeClusterIdentityToken for application/json ContentType. +type ExchangeClusterIdentityTokenJSONRequestBody = ExchangeTokenRequest diff --git a/pkg/zero/cluster/models.yaml b/pkg/zero/cluster/models.yaml new file mode 100644 index 000000000..4beb208d9 --- /dev/null +++ b/pkg/zero/cluster/models.yaml @@ -0,0 +1,4 @@ +package: cluster +generate: + models: true +output: models.gen.go diff --git a/pkg/zero/cluster/openapi.yaml b/pkg/zero/cluster/openapi.yaml new file mode 100644 index 000000000..dc5398160 --- /dev/null +++ b/pkg/zero/cluster/openapi.yaml @@ -0,0 +1,269 @@ +openapi: 3.0.0 + +info: + title: "Pomerium Zero SDK API" + version: 0.1.0 + +servers: + - url: /cluster/v1 + +security: + - bearerAuth: [] + +paths: + /bootstrap: + get: + description: Get cluster bootstrap configuration + operationId: getClusterBootstrapConfig + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/GetBootstrapConfigResponse" + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /bundles: + get: + description: Get all cluster resource bundles + operationId: getClusterResourceBundles + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/GetBundlesResponse" + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /bundles/{bundleId}/download: + get: + description: Download cluster resource bundle + operationId: downloadClusterResourceBundle + parameters: + - $ref: "#/components/parameters/bundleId" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/DownloadBundleResponse" + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /bundles/{bundleId}/status: + post: + description: Report cluster resource bundle status + operationId: reportClusterResourceBundleStatus + parameters: + - $ref: "#/components/parameters/bundleId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/BundleStatus" + responses: + "204": + description: OK + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /exchangeToken: + post: + description: Exchange cluster identity token for bearer token + operationId: exchangeClusterIdentityToken + tags: [token] + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ExchangeTokenRequest" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ExchangeTokenResponse" + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + +components: + parameters: + bundleId: + name: bundleId + in: path + description: bundle id + required: true + schema: + type: string + + schemas: + BootstrapConfig: + type: object + properties: + databrokerStorageConnection: + type: string + description: databroker storage connection string + + Bundle: + type: object + properties: + id: + type: string + description: bundle id + required: + - id + BundleStatus: + type: object + properties: + success: + $ref: "#/components/schemas/BundleStatusSuccess" + failure: + $ref: "#/components/schemas/BundleStatusFailure" + BundleStatusSuccess: + type: object + properties: + metadata: + type: object + description: bundle metadata + additionalProperties: + type: string + required: + - metadata + BundleStatusFailure: + type: object + properties: + message: + type: string + source: + type: string + description: source of the failure + enum: + - download_error + - io_error + - invalid_bundle + - databroker_error + - unknown_error + required: + - source + - message + + DownloadBundleResponse: + type: object + properties: + url: + type: string + description: download URL + expiresInSeconds: + type: string + format: int64 + captureMetadataHeaders: + type: array + items: + type: string + description: bundle metadata that need be picked up by the client from the download URL + required: + - url + - expiresInSeconds + - captureMetadataHeaders + ErrorResponse: + type: object + properties: + error: + type: string + description: Error message + required: + - error + ExchangeTokenRequest: + type: object + properties: + refreshToken: + type: string + description: cluster identity token + required: + - refreshToken + ExchangeTokenResponse: + type: object + properties: + idToken: + type: string + description: ID token + expiresInSeconds: + type: string + format: int64 + required: + - idToken + - expiresInSeconds + GetBootstrapConfigResponse: + $ref: "#/components/schemas/BootstrapConfig" + GetBundlesResponse: + type: object + properties: + bundles: + type: array + items: + $ref: "#/components/schemas/Bundle" + required: + - bundles diff --git a/pkg/zero/cluster/server.gen.go b/pkg/zero/cluster/server.gen.go new file mode 100644 index 000000000..4244ea3ee --- /dev/null +++ b/pkg/zero/cluster/server.gen.go @@ -0,0 +1,677 @@ +// Package cluster provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.16.2 DO NOT EDIT. +package cluster + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/oapi-codegen/runtime" + strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /bootstrap) + GetClusterBootstrapConfig(w http.ResponseWriter, r *http.Request) + + // (GET /bundles) + GetClusterResourceBundles(w http.ResponseWriter, r *http.Request) + + // (GET /bundles/{bundleId}/download) + DownloadClusterResourceBundle(w http.ResponseWriter, r *http.Request, bundleId BundleId) + + // (POST /bundles/{bundleId}/status) + ReportClusterResourceBundleStatus(w http.ResponseWriter, r *http.Request, bundleId BundleId) + + // (POST /exchangeToken) + ExchangeClusterIdentityToken(w http.ResponseWriter, r *http.Request) +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. + +type Unimplemented struct{} + +// (GET /bootstrap) +func (_ Unimplemented) GetClusterBootstrapConfig(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// (GET /bundles) +func (_ Unimplemented) GetClusterResourceBundles(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// (GET /bundles/{bundleId}/download) +func (_ Unimplemented) DownloadClusterResourceBundle(w http.ResponseWriter, r *http.Request, bundleId BundleId) { + w.WriteHeader(http.StatusNotImplemented) +} + +// (POST /bundles/{bundleId}/status) +func (_ Unimplemented) ReportClusterResourceBundleStatus(w http.ResponseWriter, r *http.Request, bundleId BundleId) { + w.WriteHeader(http.StatusNotImplemented) +} + +// (POST /exchangeToken) +func (_ Unimplemented) ExchangeClusterIdentityToken(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// GetClusterBootstrapConfig operation middleware +func (siw *ServerInterfaceWrapper) GetClusterBootstrapConfig(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetClusterBootstrapConfig(w, r) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetClusterResourceBundles operation middleware +func (siw *ServerInterfaceWrapper) GetClusterResourceBundles(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetClusterResourceBundles(w, r) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// DownloadClusterResourceBundle operation middleware +func (siw *ServerInterfaceWrapper) DownloadClusterResourceBundle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "bundleId" ------------- + var bundleId BundleId + + err = runtime.BindStyledParameterWithLocation("simple", false, "bundleId", runtime.ParamLocationPath, chi.URLParam(r, "bundleId"), &bundleId) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bundleId", Err: err}) + return + } + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DownloadClusterResourceBundle(w, r, bundleId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// ReportClusterResourceBundleStatus operation middleware +func (siw *ServerInterfaceWrapper) ReportClusterResourceBundleStatus(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "bundleId" ------------- + var bundleId BundleId + + err = runtime.BindStyledParameterWithLocation("simple", false, "bundleId", runtime.ParamLocationPath, chi.URLParam(r, "bundleId"), &bundleId) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bundleId", Err: err}) + return + } + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ReportClusterResourceBundleStatus(w, r, bundleId) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// ExchangeClusterIdentityToken operation middleware +func (siw *ServerInterfaceWrapper) ExchangeClusterIdentityToken(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ExchangeClusterIdentityToken(w, r) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshalingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshalingParamError) Error() string { + return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshalingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +type ChiServerOptions struct { + BaseURL string + BaseRouter chi.Router + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseRouter: r, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseURL: baseURL, + BaseRouter: r, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { + r := options.BaseRouter + + if r == nil { + r = chi.NewRouter() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/bootstrap", wrapper.GetClusterBootstrapConfig) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/bundles", wrapper.GetClusterResourceBundles) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/bundles/{bundleId}/download", wrapper.DownloadClusterResourceBundle) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/bundles/{bundleId}/status", wrapper.ReportClusterResourceBundleStatus) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/exchangeToken", wrapper.ExchangeClusterIdentityToken) + }) + + return r +} + +type GetClusterBootstrapConfigRequestObject struct { +} + +type GetClusterBootstrapConfigResponseObject interface { + VisitGetClusterBootstrapConfigResponse(w http.ResponseWriter) error +} + +type GetClusterBootstrapConfig200JSONResponse GetBootstrapConfigResponse + +func (response GetClusterBootstrapConfig200JSONResponse) VisitGetClusterBootstrapConfigResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetClusterBootstrapConfig400JSONResponse ErrorResponse + +func (response GetClusterBootstrapConfig400JSONResponse) VisitGetClusterBootstrapConfigResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type GetClusterBootstrapConfig500JSONResponse ErrorResponse + +func (response GetClusterBootstrapConfig500JSONResponse) VisitGetClusterBootstrapConfigResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetClusterResourceBundlesRequestObject struct { +} + +type GetClusterResourceBundlesResponseObject interface { + VisitGetClusterResourceBundlesResponse(w http.ResponseWriter) error +} + +type GetClusterResourceBundles200JSONResponse GetBundlesResponse + +func (response GetClusterResourceBundles200JSONResponse) VisitGetClusterResourceBundlesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetClusterResourceBundles400JSONResponse ErrorResponse + +func (response GetClusterResourceBundles400JSONResponse) VisitGetClusterResourceBundlesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type GetClusterResourceBundles500JSONResponse ErrorResponse + +func (response GetClusterResourceBundles500JSONResponse) VisitGetClusterResourceBundlesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type DownloadClusterResourceBundleRequestObject struct { + BundleId BundleId `json:"bundleId"` +} + +type DownloadClusterResourceBundleResponseObject interface { + VisitDownloadClusterResourceBundleResponse(w http.ResponseWriter) error +} + +type DownloadClusterResourceBundle200JSONResponse DownloadBundleResponse + +func (response DownloadClusterResourceBundle200JSONResponse) VisitDownloadClusterResourceBundleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type DownloadClusterResourceBundle400JSONResponse ErrorResponse + +func (response DownloadClusterResourceBundle400JSONResponse) VisitDownloadClusterResourceBundleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type DownloadClusterResourceBundle404JSONResponse ErrorResponse + +func (response DownloadClusterResourceBundle404JSONResponse) VisitDownloadClusterResourceBundleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type DownloadClusterResourceBundle500JSONResponse ErrorResponse + +func (response DownloadClusterResourceBundle500JSONResponse) VisitDownloadClusterResourceBundleResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ReportClusterResourceBundleStatusRequestObject struct { + BundleId BundleId `json:"bundleId"` + Body *ReportClusterResourceBundleStatusJSONRequestBody +} + +type ReportClusterResourceBundleStatusResponseObject interface { + VisitReportClusterResourceBundleStatusResponse(w http.ResponseWriter) error +} + +type ReportClusterResourceBundleStatus204Response struct { +} + +func (response ReportClusterResourceBundleStatus204Response) VisitReportClusterResourceBundleStatusResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type ReportClusterResourceBundleStatus400JSONResponse ErrorResponse + +func (response ReportClusterResourceBundleStatus400JSONResponse) VisitReportClusterResourceBundleStatusResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type ReportClusterResourceBundleStatus500JSONResponse ErrorResponse + +func (response ReportClusterResourceBundleStatus500JSONResponse) VisitReportClusterResourceBundleStatusResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ExchangeClusterIdentityTokenRequestObject struct { + Body *ExchangeClusterIdentityTokenJSONRequestBody +} + +type ExchangeClusterIdentityTokenResponseObject interface { + VisitExchangeClusterIdentityTokenResponse(w http.ResponseWriter) error +} + +type ExchangeClusterIdentityToken200JSONResponse ExchangeTokenResponse + +func (response ExchangeClusterIdentityToken200JSONResponse) VisitExchangeClusterIdentityTokenResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ExchangeClusterIdentityToken400JSONResponse ErrorResponse + +func (response ExchangeClusterIdentityToken400JSONResponse) VisitExchangeClusterIdentityTokenResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type ExchangeClusterIdentityToken500JSONResponse ErrorResponse + +func (response ExchangeClusterIdentityToken500JSONResponse) VisitExchangeClusterIdentityTokenResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (GET /bootstrap) + GetClusterBootstrapConfig(ctx context.Context, request GetClusterBootstrapConfigRequestObject) (GetClusterBootstrapConfigResponseObject, error) + + // (GET /bundles) + GetClusterResourceBundles(ctx context.Context, request GetClusterResourceBundlesRequestObject) (GetClusterResourceBundlesResponseObject, error) + + // (GET /bundles/{bundleId}/download) + DownloadClusterResourceBundle(ctx context.Context, request DownloadClusterResourceBundleRequestObject) (DownloadClusterResourceBundleResponseObject, error) + + // (POST /bundles/{bundleId}/status) + ReportClusterResourceBundleStatus(ctx context.Context, request ReportClusterResourceBundleStatusRequestObject) (ReportClusterResourceBundleStatusResponseObject, error) + + // (POST /exchangeToken) + ExchangeClusterIdentityToken(ctx context.Context, request ExchangeClusterIdentityTokenRequestObject) (ExchangeClusterIdentityTokenResponseObject, error) +} + +type StrictHandlerFunc = strictnethttp.StrictHttpHandlerFunc +type StrictMiddlewareFunc = strictnethttp.StrictHttpMiddlewareFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// GetClusterBootstrapConfig operation middleware +func (sh *strictHandler) GetClusterBootstrapConfig(w http.ResponseWriter, r *http.Request) { + var request GetClusterBootstrapConfigRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetClusterBootstrapConfig(ctx, request.(GetClusterBootstrapConfigRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetClusterBootstrapConfig") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetClusterBootstrapConfigResponseObject); ok { + if err := validResponse.VisitGetClusterBootstrapConfigResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetClusterResourceBundles operation middleware +func (sh *strictHandler) GetClusterResourceBundles(w http.ResponseWriter, r *http.Request) { + var request GetClusterResourceBundlesRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetClusterResourceBundles(ctx, request.(GetClusterResourceBundlesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetClusterResourceBundles") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetClusterResourceBundlesResponseObject); ok { + if err := validResponse.VisitGetClusterResourceBundlesResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// DownloadClusterResourceBundle operation middleware +func (sh *strictHandler) DownloadClusterResourceBundle(w http.ResponseWriter, r *http.Request, bundleId BundleId) { + var request DownloadClusterResourceBundleRequestObject + + request.BundleId = bundleId + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.DownloadClusterResourceBundle(ctx, request.(DownloadClusterResourceBundleRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "DownloadClusterResourceBundle") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(DownloadClusterResourceBundleResponseObject); ok { + if err := validResponse.VisitDownloadClusterResourceBundleResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ReportClusterResourceBundleStatus operation middleware +func (sh *strictHandler) ReportClusterResourceBundleStatus(w http.ResponseWriter, r *http.Request, bundleId BundleId) { + var request ReportClusterResourceBundleStatusRequestObject + + request.BundleId = bundleId + + var body ReportClusterResourceBundleStatusJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ReportClusterResourceBundleStatus(ctx, request.(ReportClusterResourceBundleStatusRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ReportClusterResourceBundleStatus") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ReportClusterResourceBundleStatusResponseObject); ok { + if err := validResponse.VisitReportClusterResourceBundleStatusResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ExchangeClusterIdentityToken operation middleware +func (sh *strictHandler) ExchangeClusterIdentityToken(w http.ResponseWriter, r *http.Request) { + var request ExchangeClusterIdentityTokenRequestObject + + var body ExchangeClusterIdentityTokenJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ExchangeClusterIdentityToken(ctx, request.(ExchangeClusterIdentityTokenRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ExchangeClusterIdentityToken") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ExchangeClusterIdentityTokenResponseObject); ok { + if err := validResponse.VisitExchangeClusterIdentityTokenResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} diff --git a/pkg/zero/cluster/server.yaml b/pkg/zero/cluster/server.yaml new file mode 100644 index 000000000..085a27b19 --- /dev/null +++ b/pkg/zero/cluster/server.yaml @@ -0,0 +1,8 @@ +package: cluster +generate: + chi-server: true + strict-server: true + models: false +compatibility: + apply-chi-middleware-first-to-last: true +output: server.gen.go diff --git a/pkg/zero/cluster/token_fetcher.go b/pkg/zero/cluster/token_fetcher.go new file mode 100644 index 000000000..32f92350a --- /dev/null +++ b/pkg/zero/cluster/token_fetcher.go @@ -0,0 +1,40 @@ +package cluster + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/pomerium/pomerium/pkg/zero/apierror" + "github.com/pomerium/pomerium/pkg/zero/token" +) + +// NewTokenFetcher creates a new authorization token fetcher +func NewTokenFetcher(endpoint string, opts ...ClientOption) (token.Fetcher, error) { + client, err := NewClientWithResponses(endpoint, opts...) + if err != nil { + return nil, fmt.Errorf("error creating client: %w", err) + } + + return func(ctx context.Context, refreshToken string) (*token.Token, error) { + now := time.Now() + + resp, err := apierror.CheckResponse[ExchangeTokenResponse](client.ExchangeClusterIdentityTokenWithResponse(ctx, ExchangeTokenRequest{ + RefreshToken: refreshToken, + })) + if err != nil { + return nil, fmt.Errorf("error exchanging token: %w", err) + } + + expiresSeconds, err := strconv.ParseInt(resp.ExpiresInSeconds, 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing expires in: %w", err) + } + + return &token.Token{ + Bearer: resp.IdToken, + Expires: now.Add(time.Duration(expiresSeconds) * time.Second), + }, nil + }, nil +} diff --git a/pkg/zero/cluster/urlcache.go b/pkg/zero/cluster/urlcache.go new file mode 100644 index 000000000..09350c931 --- /dev/null +++ b/pkg/zero/cluster/urlcache.go @@ -0,0 +1,55 @@ +package cluster + +import ( + "net/url" + "sync" + "time" +) + +// URLCache is a cache of URLs to download bundles from. +type URLCache struct { + mx sync.RWMutex + cache map[string]DownloadCacheEntry +} + +// DownloadCacheEntry is a cache entry for a URL to download a bundle from. +type DownloadCacheEntry struct { + // URL is the URL to download the bundle from. + URL url.URL + // ExpiresAt is the time at which the URL expires. + ExpiresAt time.Time + // CaptureHeaders is a list of headers to capture from the response. + CaptureHeaders []string +} + +// NewURLCache creates a new URL cache. +func NewURLCache() *URLCache { + return &URLCache{ + cache: make(map[string]DownloadCacheEntry), + } +} + +// Get gets the cache entry for the given key, if it exists and has not expired. +func (c *URLCache) Get(key string, minTTL time.Duration) (*DownloadCacheEntry, bool) { + c.mx.RLock() + defer c.mx.RUnlock() + + entry, ok := c.cache[key] + if !ok { + return nil, false + } + + if time.Until(entry.ExpiresAt) < minTTL { + return nil, false + } + + return &entry, true +} + +// Set sets the cache entry for the given key. +func (c *URLCache) Set(key string, entry DownloadCacheEntry) { + c.mx.Lock() + defer c.mx.Unlock() + + c.cache[key] = entry +} diff --git a/pkg/zero/config.go b/pkg/zero/config.go new file mode 100644 index 000000000..a2bf2a71a --- /dev/null +++ b/pkg/zero/config.go @@ -0,0 +1,87 @@ +package zero + +import ( + "fmt" + "net/http" + "time" +) + +// Option is a functional option for the SDK +type Option func(*config) + +type config struct { + clusterAPIEndpoint string + connectAPIEndpoint string + apiToken string + httpClient *http.Client + downloadURLCacheTTL time.Duration +} + +// WithClusterAPIEndpoint sets the cluster API endpoint +func WithClusterAPIEndpoint(endpoint string) Option { + return func(cfg *config) { + cfg.clusterAPIEndpoint = endpoint + } +} + +// WithConnectAPIEndpoint sets the connect API endpoint +func WithConnectAPIEndpoint(endpoint string) Option { + return func(cfg *config) { + cfg.connectAPIEndpoint = endpoint + } +} + +// WithAPIToken sets the API token +func WithAPIToken(token string) Option { + return func(cfg *config) { + cfg.apiToken = token + } +} + +// WithHTTPClient sets the HTTP client +func WithHTTPClient(client *http.Client) Option { + return func(cfg *config) { + cfg.httpClient = client + } +} + +// WithDownloadURLCacheTTL sets the minimum TTL for download URL cache entries +func WithDownloadURLCacheTTL(ttl time.Duration) Option { + return func(cfg *config) { + cfg.downloadURLCacheTTL = ttl + } +} + +func newConfig(opts ...Option) (*config, error) { + cfg := new(config) + for _, opt := range []Option{ + WithHTTPClient(http.DefaultClient), + WithDownloadURLCacheTTL(15 * time.Minute), + } { + opt(cfg) + } + + for _, opt := range opts { + opt(cfg) + } + if err := cfg.validate(); err != nil { + return nil, err + } + return cfg, nil +} + +func (c *config) validate() error { + if c.clusterAPIEndpoint == "" { + return fmt.Errorf("cluster API endpoint is required") + } + if c.connectAPIEndpoint == "" { + return fmt.Errorf("connect API endpoint is required") + } + if c.apiToken == "" { + return fmt.Errorf("API token is required") + } + if c.httpClient == nil { + return fmt.Errorf("HTTP client is required") + } + return nil +} diff --git a/pkg/zero/connect-mux/config.go b/pkg/zero/connect-mux/config.go new file mode 100644 index 000000000..d7da0412c --- /dev/null +++ b/pkg/zero/connect-mux/config.go @@ -0,0 +1,57 @@ +package mux + +import "context" + +type config struct { + onConnected func(ctx context.Context) + onDisconnected func(ctx context.Context) + onBundleUpdated func(ctx context.Context, key string) + onBootstrapConfigUpdated func(ctx context.Context) +} + +// WatchOption allows to specify callbacks for various events +type WatchOption func(*config) + +// WithOnConnected sets the callback for when the connection is established +func WithOnConnected(onConnected func(context.Context)) WatchOption { + return func(cfg *config) { + cfg.onConnected = onConnected + } +} + +// WithOnDisconnected sets the callback for when the connection is lost +func WithOnDisconnected(onDisconnected func(context.Context)) WatchOption { + return func(cfg *config) { + cfg.onDisconnected = onDisconnected + } +} + +// WithOnBundleUpdated sets the callback for when the bundle is updated +func WithOnBundleUpdated(onBundleUpdated func(ctx context.Context, key string)) WatchOption { + return func(cfg *config) { + cfg.onBundleUpdated = onBundleUpdated + } +} + +// WithOnBootstrapConfigUpdated sets the callback for when the bootstrap config is updated +func WithOnBootstrapConfigUpdated(onBootstrapConfigUpdated func(context.Context)) WatchOption { + return func(cfg *config) { + cfg.onBootstrapConfigUpdated = onBootstrapConfigUpdated + } +} + +func newConfig(opts ...WatchOption) *config { + cfg := &config{} + for _, opt := range []WatchOption{ + WithOnConnected(func(_ context.Context) {}), + WithOnDisconnected(func(_ context.Context) {}), + WithOnBundleUpdated(func(_ context.Context, key string) {}), + WithOnBootstrapConfigUpdated(func(_ context.Context) {}), + } { + opt(cfg) + } + for _, opt := range opts { + opt(cfg) + } + return cfg +} diff --git a/pkg/zero/connect-mux/messages.go b/pkg/zero/connect-mux/messages.go new file mode 100644 index 000000000..83aaa5b49 --- /dev/null +++ b/pkg/zero/connect-mux/messages.go @@ -0,0 +1,109 @@ +package mux + +import ( + "context" + "fmt" + + "github.com/pomerium/pomerium/pkg/zero/apierror" + "github.com/pomerium/pomerium/pkg/zero/connect" +) + +// Watch watches for changes to the config until either context is canceled, +// or an error occurs while muxing +func (svc *Mux) Watch(ctx context.Context, opts ...WatchOption) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-svc.ready: + } + + cfg := newConfig(opts...) + + connected := svc.connected.Load() + if connected { + cfg.onConnected(ctx) + } else { + cfg.onDisconnected(ctx) + } + + return svc.mux.Receive(ctx, func(ctx context.Context, msg message) error { + return dispatch(ctx, cfg, msg) + }) +} + +func dispatch(ctx context.Context, cfg *config, msg message) error { + switch { + case msg.stateChange != nil: + switch *msg.stateChange { + case connected: + cfg.onConnected(ctx) + case disconnected: + cfg.onDisconnected(ctx) + default: + return fmt.Errorf("unknown state change") + } + case msg.Message != nil: + switch msg.Message.Message.(type) { + case *connect.Message_ConfigUpdated: + cfg.onBundleUpdated(ctx, "config") + case *connect.Message_BootstrapConfigUpdated: + cfg.onBootstrapConfigUpdated(ctx) + default: + return fmt.Errorf("unknown message type") + } + default: + return fmt.Errorf("unknown message payload") + } + return nil +} + +type message struct { + *stateChange + *connect.Message +} + +type stateChange string + +const ( + connected stateChange = "connected" + disconnected stateChange = "disconnected" +) + +// Publish publishes a message to the fanout +// we treat errors returned from the fanout as terminal, +// as they are generally non recoverable +func (svc *Mux) publish(ctx context.Context, msg message) error { + err := svc.mux.Publish(ctx, msg) + if err != nil { + return apierror.NewTerminalError(err) + } + return nil +} + +func (svc *Mux) onConnected(ctx context.Context) error { + s := connected + svc.connected.Store(true) + err := svc.publish(ctx, message{stateChange: &s}) + if err != nil { + return fmt.Errorf("onConnected: %w", err) + } + return nil +} + +func (svc *Mux) onDisconnected(ctx context.Context) error { + s := disconnected + svc.connected.Store(false) + err := svc.publish(ctx, message{stateChange: &s}) + if err != nil { + return fmt.Errorf("onDisconnected: %w", err) + } + return nil +} + +func (svc *Mux) onMessage(ctx context.Context, msg *connect.Message) error { + err := svc.publish(ctx, message{Message: msg}) + if err != nil { + return fmt.Errorf("onMessage: %w", err) + } + return nil +} diff --git a/pkg/zero/connect-mux/service.go b/pkg/zero/connect-mux/service.go new file mode 100644 index 000000000..cda6ba3c9 --- /dev/null +++ b/pkg/zero/connect-mux/service.go @@ -0,0 +1,108 @@ +// Package mux provides the way to listen for updates from the cloud +package mux + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/hashicorp/go-multierror" + "github.com/rs/zerolog/log" + + "github.com/pomerium/pomerium/pkg/fanout" + "github.com/pomerium/pomerium/pkg/zero/apierror" + "github.com/pomerium/pomerium/pkg/zero/connect" +) + +// Mux is the service that listens for updates from the cloud +type Mux struct { + client connect.ConnectClient + mux *fanout.FanOut[message] + + ready chan struct{} + + connected atomic.Bool +} + +// New creates a new mux service that listens for updates from the cloud +func New(client connect.ConnectClient) *Mux { + svc := &Mux{ + client: client, + ready: make(chan struct{}), + } + return svc +} + +// Run starts the service +func (svc *Mux) Run(ctx context.Context, opts ...fanout.Option) error { + ctx, cancel := context.WithCancelCause(ctx) + defer func() { cancel(ctx.Err()) }() + + svc.mux = fanout.Start[message](ctx, opts...) + close(svc.ready) + + err := svc.run(ctx) + if err != nil { + cancel(err) + return err + } + return nil +} + +func (svc *Mux) run(ctx context.Context) error { + bo := backoff.NewExponentialBackOff() + bo.MaxElapsedTime = 0 + + ticker := time.NewTicker(time.Microsecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + + err := svc.subscribeAndDispatch(ctx, bo.Reset) + if err != nil { + ticker.Reset(bo.NextBackOff()) + } + + if apierror.IsTerminalError(err) { + return err + } + } +} + +func (svc *Mux) subscribeAndDispatch(ctx context.Context, onConnected func()) (err error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + stream, err := svc.client.Subscribe(ctx, &connect.SubscribeRequest{}) + if err != nil { + return fmt.Errorf("subscribe: %w", err) + } + onConnected() + + if err = svc.onConnected(ctx); err != nil { + return err + } + defer func() { + err = multierror.Append(err, svc.onDisconnected(ctx)).ErrorOrNil() + }() + + log.Ctx(ctx).Info().Msg("subscribed to connect service") + for { + msg, err := stream.Recv() + log.Ctx(ctx).Info().Interface("msg", msg).Err(err).Msg("receive") + if err != nil { + return fmt.Errorf("receive: %w", err) + } + err = svc.onMessage(ctx, msg) + if err != nil { + return err + } + } +} diff --git a/pkg/zero/connect/buf.gen.yaml b/pkg/zero/connect/buf.gen.yaml new file mode 100644 index 000000000..1b638e717 --- /dev/null +++ b/pkg/zero/connect/buf.gen.yaml @@ -0,0 +1,10 @@ + version: v1 + plugins: + - name: go + out: . + opt: paths=source_relative + - name: go-grpc + out: . + opt: + - paths=source_relative + - require_unimplemented_servers=false diff --git a/pkg/zero/connect/buf.yaml b/pkg/zero/connect/buf.yaml new file mode 100644 index 000000000..e5f123b62 --- /dev/null +++ b/pkg/zero/connect/buf.yaml @@ -0,0 +1,8 @@ +version: v1 +build: {} +lint: + use: + - DEFAULT +breaking: + use: + - FILE diff --git a/pkg/zero/connect/client.go b/pkg/zero/connect/client.go new file mode 100644 index 000000000..73046b766 --- /dev/null +++ b/pkg/zero/connect/client.go @@ -0,0 +1,83 @@ +package connect + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc" + grpc_backoff "google.golang.org/grpc/backoff" +) + +const ( + defaultDialTimeout = time.Hour +) + +type client struct { + config *Config + tokenProvider TokenProviderFn + minTokenTTL time.Duration +} + +// TokenProviderFn is a function that returns a token that is expected to be valid for at least minTTL +type TokenProviderFn func(ctx context.Context, minTTL time.Duration) (string, error) + +// NewAuthorizedConnectClient creates a new gRPC client for the connect service +func NewAuthorizedConnectClient( + ctx context.Context, + endpoint string, + tokenProvider TokenProviderFn, +) (ConnectClient, error) { + cfg, err := NewConfig(endpoint) + if err != nil { + return nil, err + } + + cc := &client{ + tokenProvider: tokenProvider, + config: cfg, + // streaming connection would reset based on token duration, + // so we need it be close to max duration 1hr + minTokenTTL: time.Minute * 55, + } + + grpcConn, err := cc.getGRPCConn(ctx) + if err != nil { + return nil, err + } + + return NewConnectClient(grpcConn), nil +} + +func (c *client) getGRPCConn(ctx context.Context) (*grpc.ClientConn, error) { + conn, err := grpc.DialContext(ctx, + c.config.GetConnectionURI(), + append(c.config.GetDialOptions(), + grpc.WithPerRPCCredentials(c), + grpc.WithConnectParams(grpc.ConnectParams{ + Backoff: grpc_backoff.DefaultConfig, + // the MinConnectTimeout is confusing and is actually the max timeout as per grpc implementation + MinConnectTimeout: c.config.GetDialTimeout(), + }), + )...) + if err != nil { + return nil, fmt.Errorf("error dialing grpc server: %w", err) + } + return conn, nil +} + +// GetRequestMetadata implements credentials.PerRPCCredentials +func (c *client) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) { + token, err := c.tokenProvider(ctx, c.minTokenTTL) + if err != nil { + return nil, err + } + return map[string]string{ + "authorization": fmt.Sprintf("Bearer %s", token), + }, nil +} + +// RequireTransportSecurity implements credentials.PerRPCCredentials +func (c *client) RequireTransportSecurity() bool { + return c.config.RequireTLS() +} diff --git a/pkg/zero/connect/client_test.go b/pkg/zero/connect/client_test.go new file mode 100644 index 000000000..5fd64f5db --- /dev/null +++ b/pkg/zero/connect/client_test.go @@ -0,0 +1,96 @@ +package connect_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster" + "github.com/pomerium/pomerium/pkg/zero/connect" + "github.com/pomerium/pomerium/pkg/zero/token" +) + +func TestConfig(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + endpoint string + connectionURI string + requireTLS bool + expectError bool + }{ + {"http://localhost:8721", "dns:localhost:8721", false, false}, + {"https://localhost:8721", "dns:localhost:8721", true, false}, + {"http://localhost:8721/", "dns:localhost:8721", false, false}, + {"https://localhost:8721/", "dns:localhost:8721", true, false}, + {"http://localhost", "dns:localhost:80", false, false}, + {"https://localhost", "dns:localhost:443", true, false}, + + {endpoint: "", expectError: true}, + {endpoint: "http://", expectError: true}, + {endpoint: "https://", expectError: true}, + {endpoint: "localhost:8721", expectError: true}, + {endpoint: "http://localhost:8721/path", expectError: true}, + {endpoint: "https://localhost:8721/path", expectError: true}, + } { + tc := tc + t.Run(tc.endpoint, func(t *testing.T) { + t.Parallel() + cfg, err := connect.NewConfig(tc.endpoint) + if tc.expectError { + require.Error(t, err) + return + } + if assert.NoError(t, err) { + assert.Equal(t, tc.connectionURI, cfg.GetConnectionURI(), "connection uri") + assert.Equal(t, tc.requireTLS, cfg.RequireTLS(), "require tls") + } + }) + } +} + +func TestConnectClient(t *testing.T) { + refreshToken := os.Getenv("CONNECT_CLUSTER_IDENTITY_TOKEN") + if refreshToken == "" { + t.Skip("CONNECT_CLUSTER_IDENTITY_TOKEN not set") + } + + connectServerEndpoint := os.Getenv("CONNECT_SERVER_ENDPOINT") + if connectServerEndpoint == "" { + connectServerEndpoint = "http://localhost:8721" + } + + clusterAPIEndpoint := os.Getenv("CLUSTER_API_ENDPOINT") + if clusterAPIEndpoint == "" { + clusterAPIEndpoint = "http://localhost:8720/cluster/v1" + } + + fetcher, err := cluster_api.NewTokenFetcher(clusterAPIEndpoint) + require.NoError(t, err, "error creating token fetcher") + + ctx := context.Background() + deadline, ok := t.Deadline() + if ok { + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(ctx, deadline.Add(-1*time.Second)) + t.Cleanup(cancel) + } + + tokenCache := token.NewCache(fetcher, refreshToken) + + connectClient, err := connect.NewAuthorizedConnectClient(ctx, connectServerEndpoint, tokenCache.GetToken) + require.NoError(t, err, "error creating connect client") + + stream, err := connectClient.Subscribe(ctx, &connect.SubscribeRequest{}) + require.NoError(t, err, "error subscribing") + + for { + msg, err := stream.Recv() + require.NoError(t, err, "error receiving message") + t.Log(msg) + } +} diff --git a/pkg/zero/connect/config.go b/pkg/zero/connect/config.go new file mode 100644 index 000000000..9f32dd865 --- /dev/null +++ b/pkg/zero/connect/config.go @@ -0,0 +1,121 @@ +package connect + +import ( + "crypto/tls" + "fmt" + "net" + "net/url" + "regexp" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +// Config is the configuration for the gRPC client +type Config struct { + connectionURI string + // requireTLS is whether TLS should be used or cleartext + requireTLS bool + // opts are additional options to pass to the gRPC client + opts []grpc.DialOption +} + +// NewConfig returns a new Config from an endpoint string, that has to be in a URL format. +// The endpoint can be either http:// or https:// that will be used to determine whether TLS should be used. +// if port is not specified, it will be inferred from the scheme (80 for http, 443 for https). +func NewConfig(endpoint string) (*Config, error) { + c := new(Config) + err := c.parseEndpoint(endpoint) + if err != nil { + return nil, fmt.Errorf("invalid endpoint: %w", err) + } + c.buildTLSOptions() + return c, nil +} + +// GetConnectionURI returns connection string conforming to https://github.com/grpc/grpc/blob/master/doc/naming.md +func (c *Config) GetConnectionURI() string { + return c.connectionURI +} + +// GetDialTimeout returns the timeout for the dial operation +func (c *Config) GetDialTimeout() time.Duration { + return defaultDialTimeout +} + +// RequireTLS returns whether TLS should be used or cleartext +func (c *Config) RequireTLS() bool { + return c.requireTLS +} + +// GetDialOptions returns the dial options to pass to the gRPC client +func (c *Config) GetDialOptions() []grpc.DialOption { + return c.opts +} + +func (c *Config) buildTLSOptions() { + creds := insecure.NewCredentials() + if c.requireTLS { + creds = credentials.NewTLS(&tls.Config{ + MinVersion: tls.VersionTLS12, + }) + } + c.opts = append(c.opts, grpc.WithTransportCredentials(creds)) +} + +func (c *Config) parseEndpoint(endpoint string) error { + u, err := url.Parse(endpoint) + if err != nil { + return fmt.Errorf("error parsing endpoint url: %w", err) + } + + if u.Path != "" && u.Path != "/" { + return fmt.Errorf("endpoint path is not supported: %s", u.Path) + } + + host, port, err := splitHostPort(u.Host) + if err != nil { + return fmt.Errorf("error splitting host and port: %w", err) + } + + var requireTLS bool + if u.Scheme == "http" { + requireTLS = false + if port == "" { + port = "80" + } + } else if u.Scheme == "https" { + requireTLS = true + if port == "" { + port = "443" + } + } else { + return fmt.Errorf("unsupported url scheme: %s", u.Scheme) + } + + c.connectionURI = fmt.Sprintf("dns:%s:%s", host, port) + c.requireTLS = requireTLS + + return nil +} + +var rePort = regexp.MustCompile(`:(\d+)$`) + +func splitHostPort(hostport string) (host, port string, err error) { + if hostport == "" { + return "", "", fmt.Errorf("empty hostport") + } + if rePort.MatchString(hostport) { + host, port, err = net.SplitHostPort(hostport) + if host == "" { + return "", "", fmt.Errorf("empty host") + } + if port == "" { + return "", "", fmt.Errorf("empty port") + } + return host, port, err + } + return hostport, "", nil +} diff --git a/pkg/zero/connect/connect.pb.go b/pkg/zero/connect/connect.pb.go new file mode 100644 index 000000000..d60bbe32e --- /dev/null +++ b/pkg/zero/connect/connect.pb.go @@ -0,0 +1,384 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc (unknown) +// source: connect.proto + +package connect + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// SubscribeRequest is used to subscribe to a stream of messages +// from the Zero Cloud to the Pomerium Core. +// +// The Authorization: Bearer header must contain a valid token, +// that belongs to a cluster identity with appropriate claims set. +type SubscribeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SubscribeRequest) Reset() { + *x = SubscribeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubscribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeRequest) ProtoMessage() {} + +func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_connect_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeRequest.ProtoReflect.Descriptor instead. +func (*SubscribeRequest) Descriptor() ([]byte, []int) { + return file_connect_proto_rawDescGZIP(), []int{0} +} + +// Message is an aggregate of all possible messages that can be sent +// from the cloud to the core in managed mode. +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Message: + // + // *Message_ConfigUpdated + // *Message_BootstrapConfigUpdated + Message isMessage_Message `protobuf_oneof:"message"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_connect_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_connect_proto_rawDescGZIP(), []int{1} +} + +func (m *Message) GetMessage() isMessage_Message { + if m != nil { + return m.Message + } + return nil +} + +func (x *Message) GetConfigUpdated() *ConfigUpdated { + if x, ok := x.GetMessage().(*Message_ConfigUpdated); ok { + return x.ConfigUpdated + } + return nil +} + +func (x *Message) GetBootstrapConfigUpdated() *BootstrapConfigUpdated { + if x, ok := x.GetMessage().(*Message_BootstrapConfigUpdated); ok { + return x.BootstrapConfigUpdated + } + return nil +} + +type isMessage_Message interface { + isMessage_Message() +} + +type Message_ConfigUpdated struct { + ConfigUpdated *ConfigUpdated `protobuf:"bytes,1,opt,name=config_updated,json=configUpdated,proto3,oneof"` +} + +type Message_BootstrapConfigUpdated struct { + BootstrapConfigUpdated *BootstrapConfigUpdated `protobuf:"bytes,2,opt,name=bootstrap_config_updated,json=bootstrapConfigUpdated,proto3,oneof"` +} + +func (*Message_ConfigUpdated) isMessage_Message() {} + +func (*Message_BootstrapConfigUpdated) isMessage_Message() {} + +// ConfigUpdated is sent when the configuration has been updated +// for the connected Pomerium Core deployment +type ConfigUpdated struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // version of the configuration changeset + ChangesetVersion int64 `protobuf:"varint,1,opt,name=changeset_version,json=changesetVersion,proto3" json:"changeset_version,omitempty"` +} + +func (x *ConfigUpdated) Reset() { + *x = ConfigUpdated{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConfigUpdated) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigUpdated) ProtoMessage() {} + +func (x *ConfigUpdated) ProtoReflect() protoreflect.Message { + mi := &file_connect_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigUpdated.ProtoReflect.Descriptor instead. +func (*ConfigUpdated) Descriptor() ([]byte, []int) { + return file_connect_proto_rawDescGZIP(), []int{2} +} + +func (x *ConfigUpdated) GetChangesetVersion() int64 { + if x != nil { + return x.ChangesetVersion + } + return 0 +} + +// BootstrapConfigUpdated is sent when the bootstrap configuration has been +// updated. Bootstrap configuration is received via cluster API directly, and +// does not involve long running operations to construct it, like with a regular +// config. +type BootstrapConfigUpdated struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *BootstrapConfigUpdated) Reset() { + *x = BootstrapConfigUpdated{} + if protoimpl.UnsafeEnabled { + mi := &file_connect_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BootstrapConfigUpdated) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BootstrapConfigUpdated) ProtoMessage() {} + +func (x *BootstrapConfigUpdated) ProtoReflect() protoreflect.Message { + mi := &file_connect_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BootstrapConfigUpdated.ProtoReflect.Descriptor instead. +func (*BootstrapConfigUpdated) Descriptor() ([]byte, []int) { + return file_connect_proto_rawDescGZIP(), []int{3} +} + +var File_connect_proto protoreflect.FileDescriptor + +var file_connect_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x0d, 0x70, 0x6f, 0x6d, 0x65, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x7a, 0x65, 0x72, 0x6f, 0x22, 0x12, + 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0xbe, 0x01, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x45, + 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x6d, 0x65, 0x72, 0x69, 0x75, + 0x6d, 0x2e, 0x7a, 0x65, 0x72, 0x6f, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x61, 0x0a, 0x18, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, + 0x61, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x70, 0x6f, 0x6d, 0x65, 0x72, 0x69, + 0x75, 0x6d, 0x2e, 0x7a, 0x65, 0x72, 0x6f, 0x2e, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, + 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, + 0x52, 0x16, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x22, 0x3c, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, + 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x10, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x22, 0x18, 0x0a, 0x16, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x32, 0x51, 0x0a, 0x07, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x46, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x62, 0x65, 0x12, 0x1f, 0x2e, 0x70, 0x6f, 0x6d, 0x65, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x7a, + 0x65, 0x72, 0x6f, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x6f, 0x6d, 0x65, 0x72, 0x69, 0x75, 0x6d, 0x2e, + 0x7a, 0x65, 0x72, 0x6f, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x42, 0x2f, + 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6f, 0x6d, + 0x65, 0x72, 0x69, 0x75, 0x6d, 0x2f, 0x70, 0x6f, 0x6d, 0x65, 0x72, 0x69, 0x75, 0x6d, 0x2f, 0x70, + 0x6b, 0x67, 0x2f, 0x7a, 0x65, 0x72, 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_connect_proto_rawDescOnce sync.Once + file_connect_proto_rawDescData = file_connect_proto_rawDesc +) + +func file_connect_proto_rawDescGZIP() []byte { + file_connect_proto_rawDescOnce.Do(func() { + file_connect_proto_rawDescData = protoimpl.X.CompressGZIP(file_connect_proto_rawDescData) + }) + return file_connect_proto_rawDescData +} + +var file_connect_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_connect_proto_goTypes = []interface{}{ + (*SubscribeRequest)(nil), // 0: pomerium.zero.SubscribeRequest + (*Message)(nil), // 1: pomerium.zero.Message + (*ConfigUpdated)(nil), // 2: pomerium.zero.ConfigUpdated + (*BootstrapConfigUpdated)(nil), // 3: pomerium.zero.BootstrapConfigUpdated +} +var file_connect_proto_depIdxs = []int32{ + 2, // 0: pomerium.zero.Message.config_updated:type_name -> pomerium.zero.ConfigUpdated + 3, // 1: pomerium.zero.Message.bootstrap_config_updated:type_name -> pomerium.zero.BootstrapConfigUpdated + 0, // 2: pomerium.zero.Connect.Subscribe:input_type -> pomerium.zero.SubscribeRequest + 1, // 3: pomerium.zero.Connect.Subscribe:output_type -> pomerium.zero.Message + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_connect_proto_init() } +func file_connect_proto_init() { + if File_connect_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_connect_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubscribeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_connect_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_connect_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConfigUpdated); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_connect_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BootstrapConfigUpdated); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_connect_proto_msgTypes[1].OneofWrappers = []interface{}{ + (*Message_ConfigUpdated)(nil), + (*Message_BootstrapConfigUpdated)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_connect_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_connect_proto_goTypes, + DependencyIndexes: file_connect_proto_depIdxs, + MessageInfos: file_connect_proto_msgTypes, + }.Build() + File_connect_proto = out.File + file_connect_proto_rawDesc = nil + file_connect_proto_goTypes = nil + file_connect_proto_depIdxs = nil +} diff --git a/pkg/zero/connect/connect.proto b/pkg/zero/connect/connect.proto new file mode 100644 index 000000000..1b80f11fe --- /dev/null +++ b/pkg/zero/connect/connect.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package pomerium.zero; +option go_package = "github.com/pomerium/pomerium/pkg/zero/connect"; + +// SubscribeRequest is used to subscribe to a stream of messages +// from the Zero Cloud to the Pomerium Core. +// +// The Authorization: Bearer header must contain a valid token, +// that belongs to a cluster identity with appropriate claims set. +message SubscribeRequest {} + +// Message is an aggregate of all possible messages that can be sent +// from the cloud to the core in managed mode. +message Message { + oneof message { + ConfigUpdated config_updated = 1; + BootstrapConfigUpdated bootstrap_config_updated = 2; + } +} + +// ConfigUpdated is sent when the configuration has been updated +// for the connected Pomerium Core deployment +message ConfigUpdated { + // version of the configuration changeset + int64 changeset_version = 1; +} + +// BootstrapConfigUpdated is sent when the bootstrap configuration has been +// updated. Bootstrap configuration is received via cluster API directly, and +// does not involve long running operations to construct it, like with a regular +// config. +message BootstrapConfigUpdated {} + +// Connect service is used to maintain a persistent connection between the +// Pomerium Core and Zero Cloud and receive messages from the cloud. +service Connect { + // Subscribe is used to send a stream of messages from the Zero Cloud to the + // Pomerium Core in managed mode. + rpc Subscribe(SubscribeRequest) returns (stream Message); +} diff --git a/pkg/zero/connect/connect_grpc.pb.go b/pkg/zero/connect/connect_grpc.pb.go new file mode 100644 index 000000000..43c46936f --- /dev/null +++ b/pkg/zero/connect/connect_grpc.pb.go @@ -0,0 +1,138 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: connect.proto + +package connect + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Connect_Subscribe_FullMethodName = "/pomerium.zero.Connect/Subscribe" +) + +// ConnectClient is the client API for Connect service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ConnectClient interface { + // Subscribe is used to send a stream of messages from the Zero Cloud to the + // Pomerium Core in managed mode. + Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (Connect_SubscribeClient, error) +} + +type connectClient struct { + cc grpc.ClientConnInterface +} + +func NewConnectClient(cc grpc.ClientConnInterface) ConnectClient { + return &connectClient{cc} +} + +func (c *connectClient) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (Connect_SubscribeClient, error) { + stream, err := c.cc.NewStream(ctx, &Connect_ServiceDesc.Streams[0], Connect_Subscribe_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &connectSubscribeClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Connect_SubscribeClient interface { + Recv() (*Message, error) + grpc.ClientStream +} + +type connectSubscribeClient struct { + grpc.ClientStream +} + +func (x *connectSubscribeClient) Recv() (*Message, error) { + m := new(Message) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// ConnectServer is the server API for Connect service. +// All implementations should embed UnimplementedConnectServer +// for forward compatibility +type ConnectServer interface { + // Subscribe is used to send a stream of messages from the Zero Cloud to the + // Pomerium Core in managed mode. + Subscribe(*SubscribeRequest, Connect_SubscribeServer) error +} + +// UnimplementedConnectServer should be embedded to have forward compatible implementations. +type UnimplementedConnectServer struct { +} + +func (UnimplementedConnectServer) Subscribe(*SubscribeRequest, Connect_SubscribeServer) error { + return status.Errorf(codes.Unimplemented, "method Subscribe not implemented") +} + +// UnsafeConnectServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ConnectServer will +// result in compilation errors. +type UnsafeConnectServer interface { + mustEmbedUnimplementedConnectServer() +} + +func RegisterConnectServer(s grpc.ServiceRegistrar, srv ConnectServer) { + s.RegisterService(&Connect_ServiceDesc, srv) +} + +func _Connect_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SubscribeRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ConnectServer).Subscribe(m, &connectSubscribeServer{stream}) +} + +type Connect_SubscribeServer interface { + Send(*Message) error + grpc.ServerStream +} + +type connectSubscribeServer struct { + grpc.ServerStream +} + +func (x *connectSubscribeServer) Send(m *Message) error { + return x.ServerStream.SendMsg(m) +} + +// Connect_ServiceDesc is the grpc.ServiceDesc for Connect service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Connect_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "pomerium.zero.Connect", + HandlerType: (*ConnectServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Subscribe", + Handler: _Connect_Subscribe_Handler, + ServerStreams: true, + }, + }, + Metadata: "connect.proto", +} diff --git a/pkg/zero/connect/generate.go b/pkg/zero/connect/generate.go new file mode 100644 index 000000000..f9fe1fb8a --- /dev/null +++ b/pkg/zero/connect/generate.go @@ -0,0 +1,4 @@ +// Package connect provides the way to listen for updates from the cloud +package connect + +//go:generate go run github.com/bufbuild/buf/cmd/buf@v1.28.1 generate --path connect.proto --config buf.yaml diff --git a/pkg/zero/download.go b/pkg/zero/download.go new file mode 100644 index 000000000..be96e0875 --- /dev/null +++ b/pkg/zero/download.go @@ -0,0 +1,251 @@ +package zero + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/xml" + "fmt" + "io" + "mime" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/rs/zerolog/log" + + "github.com/pomerium/pomerium/pkg/zero/apierror" + cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster" +) + +const ( + maxErrorResponseBodySize = 2 << 14 // 32kb + maxUncompressedBlobSize = 2 << 30 // 1gb +) + +// DownloadClusterResourceBundle downloads given cluster resource bundle to given writer. +func (api *API) DownloadClusterResourceBundle( + ctx context.Context, + dst io.Writer, + id string, + current *DownloadConditional, +) (*DownloadResult, error) { + req, err := api.getDownloadRequest(ctx, id, current) + if err != nil { + return nil, fmt.Errorf("get download request: %w", err) + } + + resp, err := api.cfg.httpClient.Do(req.Request) + if err != nil { + return nil, fmt.Errorf("do request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotModified { + return &DownloadResult{NotModified: true}, nil + } + + if resp.StatusCode != http.StatusOK { + return nil, httpDownloadError(ctx, resp) + } + + var r io.Reader = resp.Body + if resp.Header.Get("Content-Encoding") == "gzip" { + zr, err := gzip.NewReader(r) + if err != nil { + return nil, fmt.Errorf("gzip reader: %w", err) + } + defer zr.Close() + + r = io.LimitReader(zr, maxUncompressedBlobSize) + } + + _, err = io.Copy(dst, r) + if err != nil { + return nil, fmt.Errorf("write body: %w", err) + } + + updated, err := newConditionalFromResponse(resp) + if err != nil { + return nil, fmt.Errorf("cannot obtain cache conditions from response: %w", err) + } + + return &DownloadResult{ + DownloadConditional: updated, + Metadata: extractMetadata(resp.Header, req.CaptureHeaders), + }, nil +} + +type downloadRequest struct { + *http.Request + cluster_api.DownloadCacheEntry +} + +func (api *API) getDownloadRequest(ctx context.Context, id string, current *DownloadConditional) (*downloadRequest, error) { + params, err := api.getDownloadParams(ctx, id) + if err != nil { + return nil, fmt.Errorf("get download URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, params.URL.String(), nil) + if err != nil { + return nil, fmt.Errorf("new request: %w", err) + } + req.Header.Set("Accept-Encoding", "gzip") + + err = current.SetHeaders(req) + if err != nil { + return nil, fmt.Errorf("set conditional download headers: %w", err) + } + + return &downloadRequest{ + Request: req, + DownloadCacheEntry: *params, + }, nil +} + +func (api *API) getDownloadParams(ctx context.Context, id string) (*cluster_api.DownloadCacheEntry, error) { + param, ok := api.downloadURLCache.Get(id, api.cfg.downloadURLCacheTTL) + if ok { + return param, nil + } + + return api.updateBundleDownloadParams(ctx, id) +} + +func (api *API) updateBundleDownloadParams(ctx context.Context, id string) (*cluster_api.DownloadCacheEntry, error) { + now := time.Now() + + resp, err := apierror.CheckResponse[cluster_api.DownloadBundleResponse]( + api.cluster.DownloadClusterResourceBundleWithResponse(ctx, id), + ) + if err != nil { + return nil, fmt.Errorf("get bundle download URL: %w", err) + } + + expiresSeconds, err := strconv.ParseInt(resp.ExpiresInSeconds, 10, 64) + if err != nil { + return nil, fmt.Errorf("parse expiration: %w", err) + } + + u, err := url.Parse(resp.Url) + if err != nil { + return nil, fmt.Errorf("parse url: %w", err) + } + + param := cluster_api.DownloadCacheEntry{ + URL: *u, + ExpiresAt: now.Add(time.Duration(expiresSeconds) * time.Second), + CaptureHeaders: resp.CaptureMetadataHeaders, + } + api.downloadURLCache.Set(id, param) + return ¶m, nil +} + +// DownloadResult contains the result of a download operation +type DownloadResult struct { + // NotModified is true if the bundle has not been modified + NotModified bool + // DownloadConditional contains the new conditional + *DownloadConditional + // Metadata contains the metadata of the downloaded bundle + Metadata map[string]string +} + +// DownloadConditional contains the conditional headers for a download operation +type DownloadConditional struct { + ETag string + LastModified string +} + +// Validate validates the conditional headers +func (c *DownloadConditional) Validate() error { + if c.ETag == "" && c.LastModified == "" { + return fmt.Errorf("either ETag or LastModified must be set") + } + return nil +} + +// SetHeaders sets the conditional headers on the given request +func (c *DownloadConditional) SetHeaders(req *http.Request) error { + if c == nil { + return nil + } + if err := c.Validate(); err != nil { + return err + } + req.Header.Set("If-None-Match", c.ETag) + req.Header.Set("If-Modified-Since", c.LastModified) + return nil +} + +func newConditionalFromResponse(resp *http.Response) (*DownloadConditional, error) { + c := &DownloadConditional{ + ETag: resp.Header.Get("ETag"), + LastModified: resp.Header.Get("Last-Modified"), + } + if err := c.Validate(); err != nil { + return nil, err + } + return c, nil +} + +type xmlError struct { + XMLName xml.Name `xml:"Error"` + Code string `xml:"Code"` + Message string `xml:"Message"` + Details string `xml:"Details"` +} + +func (e xmlError) Error() string { + return fmt.Sprintf("%s: %s", e.Code, e.Message) +} + +func tryXMLError(body []byte) (bool, error) { + var xmlErr xmlError + err := xml.Unmarshal(body, &xmlErr) + if err != nil { + return false, fmt.Errorf("unmarshal xml error: %w", err) + } + + return true, xmlErr +} + +func httpDownloadError(ctx context.Context, resp *http.Response) error { + var buf bytes.Buffer + _, err := io.Copy(&buf, io.LimitReader(resp.Body, maxErrorResponseBodySize)) + + if isXML(resp.Header.Get("Content-Type")) { + ok, err := tryXMLError(buf.Bytes()) + if ok { + return err + } + } + + log.Ctx(ctx).Debug().Err(err). + Str("error", resp.Status). + Str("body", buf.String()).Msg("bundle download error") + + return fmt.Errorf("download error: %s", resp.Status) +} + +// isXML parses content-type for application/xml +func isXML(ct string) bool { + mediaType, _, err := mime.ParseMediaType(ct) + if err != nil { + return false + } + return mediaType == "application/xml" +} + +func extractMetadata(header http.Header, keys []string) map[string]string { + m := make(map[string]string) + for _, k := range keys { + v := header.Get(k) + if v != "" { + m[k] = v + } + } + return m +} diff --git a/pkg/zero/token/cache.go b/pkg/zero/token/cache.go new file mode 100644 index 000000000..abd0a1a06 --- /dev/null +++ b/pkg/zero/token/cache.go @@ -0,0 +1,100 @@ +// Package token provides a thread-safe cache of a authorization token that may be used across http and grpc clients +package token + +import ( + "context" + "fmt" + "sync/atomic" + "time" +) + +const ( + maxLockWait = 30 * time.Second +) + +// Cache is a thread-safe cache of a authorization token +// that may be used across http and grpc clients +type Cache struct { + TimeNow func() time.Time + + refreshToken string + fetcher Fetcher + + lock chan struct{} + token atomic.Value +} + +// Fetcher is a function that fetches a new token +type Fetcher func(ctx context.Context, refreshToken string) (*Token, error) + +// Token is a bearer token +type Token struct { + // Bearer is the bearer token + Bearer string + // Expires is the time the token expires + Expires time.Time +} + +// ExpiresAfter returns true if the token expires after the given time +func (t *Token) ExpiresAfter(tm time.Time) bool { + return t != nil && t.Expires.After(tm) +} + +// NewCache creates a new token cache +func NewCache(fetcher Fetcher, refreshToken string) *Cache { + return &Cache{ + lock: make(chan struct{}, 1), + fetcher: fetcher, + refreshToken: refreshToken, + } +} + +func (c *Cache) timeNow() time.Time { + if c.TimeNow != nil { + return c.TimeNow() + } + return time.Now() +} + +// GetToken returns the current token if its at least `minTTL` from expiration, or fetches a new one. +func (c *Cache) GetToken(ctx context.Context, minTTL time.Duration) (string, error) { + minExpiration := c.timeNow().Add(minTTL) + + token, ok := c.token.Load().(*Token) + if ok && token.ExpiresAfter(minExpiration) { + return token.Bearer, nil + } + + return c.forceRefreshToken(ctx, minExpiration) +} + +func (c *Cache) forceRefreshToken(ctx context.Context, minExpiration time.Time) (string, error) { + select { + case c.lock <- struct{}{}: + case <-ctx.Done(): + return "", ctx.Err() + } + defer func() { + <-c.lock + }() + + ctx, cancel := context.WithTimeout(ctx, maxLockWait) + defer cancel() + + token, ok := c.token.Load().(*Token) + if ok && token.ExpiresAfter(minExpiration) { + return token.Bearer, nil + } + + token, err := c.fetcher(ctx, c.refreshToken) + if err != nil { + return "", err + } + c.token.Store(token) + + if token.Expires.Before(minExpiration) { + return "", fmt.Errorf("new token cannot satisfy TTL: %v", minExpiration.Sub(token.Expires)) + } + + return token.Bearer, nil +} diff --git a/pkg/zero/token/cache_test.go b/pkg/zero/token/cache_test.go new file mode 100644 index 000000000..39456bdc1 --- /dev/null +++ b/pkg/zero/token/cache_test.go @@ -0,0 +1,65 @@ +package token_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pomerium/pomerium/pkg/zero/token" +) + +func TestCache(t *testing.T) { + t.Parallel() + + t.Run("token expired, fetch new", func(t *testing.T) { + t.Parallel() + + var testToken *token.Token + var testError error + fetcher := func(ctx context.Context, refreshToken string) (*token.Token, error) { + if testToken != nil { + token := *testToken + return &token, nil + } + return nil, testError + } + + c := token.NewCache(fetcher, "test-refresh-token") + now := time.Now() + c.TimeNow = func() time.Time { return now } + + testToken = &token.Token{"bearer-1", now.Add(time.Hour)} + bearer, err := c.GetToken(context.Background(), time.Minute) + require.NoError(t, err) + assert.Equal(t, "bearer-1", bearer) + + now = now.Add(time.Minute * 30) + testToken.Bearer = "bearer-2" + + // token is still valid, so we should get the same one + bearer, err = c.GetToken(context.Background(), time.Minute*20) + require.NoError(t, err) + assert.Equal(t, "bearer-1", bearer) + + now = now.Add(time.Minute * 30) + testToken = &token.Token{"bearer-3", now.Add(time.Hour)} + bearer, err = c.GetToken(context.Background(), time.Minute*30) + require.NoError(t, err) + assert.Equal(t, "bearer-3", bearer) + }) + + t.Run("token cannot fit minTTL", func(t *testing.T) { + t.Parallel() + + fetcher := func(ctx context.Context, refreshToken string) (*token.Token, error) { + return &token.Token{"ok-bearer", time.Now().Add(time.Minute)}, nil + } + + c := token.NewCache(fetcher, "test-refresh-token") + _, err := c.GetToken(context.Background(), time.Minute*2) + assert.Error(t, err) + }) +}