feat(lib/store): add s3api storage backend (#1089)
* feat(lib/store): add s3api storage backend Signed-off-by: Xe Iaso <me@xeiaso.net> * docs(store/s3api): replace fake S3 API keys with the bee movie script Signed-off-by: Xe Iaso <me@xeiaso.net> * docs(store/s3api): fix spelling sin Signed-off-by: Xe Iaso <me@xeiaso.net> * fix(store/s3api): remove vestigal experiment Signed-off-by: Xe Iaso <me@xeiaso.net> * chore: spelling Signed-off-by: Xe Iaso <me@xeiaso.net> * chore: spelling Signed-off-by: Xe Iaso <me@xeiaso.net> * chore(store/s3api): support IsPersistent call Ref #1088 Signed-off-by: Xe Iaso <me@xeiaso.net> * chore: spelling Signed-off-by: Xe Iaso <me@xeiaso.net> * chore(test): go mod tidy Signed-off-by: Xe Iaso <me@xeiaso.net> --------- Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
parent
82099d9e05
commit
98945fb56f
12 changed files with 518 additions and 5 deletions
3
.github/actions/spelling/allow.txt
vendored
3
.github/actions/spelling/allow.txt
vendored
|
|
@ -5,4 +5,5 @@ ubuntu
|
||||||
workarounds
|
workarounds
|
||||||
rjack
|
rjack
|
||||||
msgbox
|
msgbox
|
||||||
xeact
|
xeact
|
||||||
|
ABee
|
||||||
|
|
|
||||||
5
.github/actions/spelling/expect.txt
vendored
5
.github/actions/spelling/expect.txt
vendored
|
|
@ -140,6 +140,7 @@ headermap
|
||||||
healthcheck
|
healthcheck
|
||||||
healthz
|
healthz
|
||||||
hec
|
hec
|
||||||
|
Hetzner
|
||||||
hmc
|
hmc
|
||||||
homelab
|
homelab
|
||||||
hostable
|
hostable
|
||||||
|
|
@ -237,7 +238,6 @@ pki
|
||||||
podkova
|
podkova
|
||||||
podman
|
podman
|
||||||
poststart
|
poststart
|
||||||
poxied
|
|
||||||
prebaked
|
prebaked
|
||||||
privkey
|
privkey
|
||||||
promauto
|
promauto
|
||||||
|
|
@ -250,7 +250,6 @@ pwuser
|
||||||
qualys
|
qualys
|
||||||
qwant
|
qwant
|
||||||
qwantbot
|
qwantbot
|
||||||
QWEN
|
|
||||||
rac
|
rac
|
||||||
rawler
|
rawler
|
||||||
rcvar
|
rcvar
|
||||||
|
|
@ -282,7 +281,6 @@ shirou
|
||||||
Sidetrade
|
Sidetrade
|
||||||
simprint
|
simprint
|
||||||
sitemap
|
sitemap
|
||||||
Slackware
|
|
||||||
sls
|
sls
|
||||||
Smartphone
|
Smartphone
|
||||||
sni
|
sni
|
||||||
|
|
@ -360,6 +358,7 @@ XOriginal
|
||||||
XReal
|
XReal
|
||||||
yae
|
yae
|
||||||
YAMLTo
|
YAMLTo
|
||||||
|
Yda
|
||||||
yeet
|
yeet
|
||||||
yeetfile
|
yeetfile
|
||||||
yourdomain
|
yourdomain
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
<!-- This changes the project to: -->
|
<!-- This changes the project to: -->
|
||||||
|
|
||||||
|
- Add the [`s3api` storage backend](./admin/policies.mdx#s3api) to allow Anubis to use S3 API compatible object storage as its storage backend.
|
||||||
|
|
||||||
## v1.22.0: Yda Hext
|
## v1.22.0: Yda Hext
|
||||||
|
|
||||||
> Someone has to make an effort at reconciliation if these conflicts are ever going to end.
|
> Someone has to make an effort at reconciliation if these conflicts are ever going to end.
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,83 @@ store:
|
||||||
path: /data/anubis.bdb
|
path: /data/anubis.bdb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `s3api`
|
||||||
|
|
||||||
|
A network-backed storage layer backed by [object storage](https://en.wikipedia.org/wiki/Object_storage), specifically using the [S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html). This can be backed by any S3-compatible object storage service such as:
|
||||||
|
|
||||||
|
- [AWS S3](https://aws.amazon.com/s3/)
|
||||||
|
- [Cloudflare R2](https://www.cloudflare.com/developer-platform/products/r2/)
|
||||||
|
- [Hetzner Object Storage](https://www.hetzner.com/storage/object-storage/)
|
||||||
|
- [Minio](https://www.min.io/)
|
||||||
|
- [Tigris](https://www.tigrisdata.com/)
|
||||||
|
|
||||||
|
If you are using a cloud platform, they likely provide an S3 compatible object storage service. If not, you may want to choose [one of the fastest options](https://www.tigrisdata.com/blog/benchmark-small-objects/).
|
||||||
|
|
||||||
|
| Should I use this backend? | Yes/no |
|
||||||
|
| :------------------------------------------------------------ | :----- |
|
||||||
|
| Are you running only one instance of Anubis for this service? | 🚫 No |
|
||||||
|
| Does your service get a lot of traffic? | ✅ Yes |
|
||||||
|
| Do you want to store data persistently when Anubis restarts? | ✅ Yes |
|
||||||
|
| Do you run Anubis without mutable filesystem storage? | ✅ Yes |
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
Using this backend will cause a lot of S3 operations, at least one for creating challenges, one for invalidating challenges, one for updating challenges to prevent double-spends, and one for removing challenges.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
The `s3api` backend takes the following configuration options:
|
||||||
|
|
||||||
|
| Name | Type | Example | Description |
|
||||||
|
| :----------- | :------ | :------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `bucketName` | string | The name of the dedicated bucket for Anubis to store information in. |
|
||||||
|
| `pathStyle` | boolean | `false` | If true, use path-style S3 API operations. Please consult your storage provider's documentation if you don't know what you should put here. |
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
You should probably enable a lifecycle expiration rule for buckets containing Anubis data. Here is an example policy:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"Status": "Enabled",
|
||||||
|
"Expiration": {
|
||||||
|
"Days": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust this as facts and circumstances demand, but 7 days should be enough for anyone.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
Assuming your environment looks like this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# All of the following are fake credentials that look like real ones.
|
||||||
|
AWS_ACCESS_KEY_ID=accordingToAllKnownRulesOfAviation
|
||||||
|
AWS_SECRET_ACCESS_KEY=thereIsNoWayABeeShouldBeAbleToFly
|
||||||
|
AWS_REGION=yow
|
||||||
|
AWS_ENDPOINT_URL_S3=https://yow.s3.probably-not-malware.lol
|
||||||
|
```
|
||||||
|
|
||||||
|
Then your configuration would look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
store:
|
||||||
|
backend: s3api
|
||||||
|
parameters:
|
||||||
|
bucketName: techaro-prod-anubis
|
||||||
|
pathStyle: false
|
||||||
|
```
|
||||||
|
|
||||||
### `valkey`
|
### `valkey`
|
||||||
|
|
||||||
[Valkey](https://valkey.io/) is an in-memory key/value store that clients access over the network. This allows multiple instances of Anubis to share information and does not require each instance of Anubis to have persistent filesystem storage.
|
[Valkey](https://valkey.io/) is an in-memory key/value store that clients access over the network. This allows multiple instances of Anubis to share information and does not require each instance of Anubis to have persistent filesystem storage.
|
||||||
|
|
|
||||||
18
go.mod
18
go.mod
|
|
@ -5,6 +5,9 @@ go 1.24.2
|
||||||
require (
|
require (
|
||||||
github.com/TecharoHQ/thoth-proto v0.4.0
|
github.com/TecharoHQ/thoth-proto v0.4.0
|
||||||
github.com/a-h/templ v0.3.924
|
github.com/a-h/templ v0.3.924
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.38.3
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3
|
||||||
github.com/cespare/xxhash/v2 v2.3.0
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
||||||
github.com/gaissmai/bart v0.23.0
|
github.com/gaissmai/bart v0.23.0
|
||||||
|
|
@ -49,6 +52,21 @@ require (
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
|
||||||
|
github.com/aws/smithy-go v1.23.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||||
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||||
|
|
|
||||||
36
go.sum
36
go.sum
|
|
@ -51,6 +51,42 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 h1:R0tNFJqfjHL3900cqhXuwQ+1K4G0xc9Yf8EDbFXCKEw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6/go.mod h1:y/7sDdu+aJvPtGXr4xYosdpq9a6T9Z0jkXfugmti0rI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 h1:hncKj/4gR+TPauZgTAsxOxNcvBayhUlYZ6LO/BYiQ30=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6/go.mod h1:OiIh45tp6HdJDDJGnja0mw8ihQGz3VGrUflLqSL0SmM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 h1:nEXUSAwyUfLTgnc9cxlDWy637qsq4UWwp3sNAfl0Z3Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6/go.mod h1:HGzIULx4Ge3Do2V0FaiYKcyKzOqwrhUZgCI77NisswQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 h1:ETkfWcXP2KNPLecaDa++5bsQhCRa5M5sLUJa5DWYIIg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3/go.mod h1:+/3ZTqoYb3Ur7DObD00tarKMLMuKg8iqz5CHEanqTnw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
|
||||||
|
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||||
|
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,6 @@ package all
|
||||||
import (
|
import (
|
||||||
_ "github.com/TecharoHQ/anubis/lib/store/bbolt"
|
_ "github.com/TecharoHQ/anubis/lib/store/bbolt"
|
||||||
_ "github.com/TecharoHQ/anubis/lib/store/memory"
|
_ "github.com/TecharoHQ/anubis/lib/store/memory"
|
||||||
|
_ "github.com/TecharoHQ/anubis/lib/store/s3api"
|
||||||
_ "github.com/TecharoHQ/anubis/lib/store/valkey"
|
_ "github.com/TecharoHQ/anubis/lib/store/valkey"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
107
lib/store/s3api/factory.go
Normal file
107
lib/store/s3api/factory.go
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
package s3api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/lib/store"
|
||||||
|
awsConfig "github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoRegion = errors.New("s3api.Config: no region env var name defined")
|
||||||
|
ErrNoAccessKeyID = errors.New("s3api.Config: no access key id env var name defined")
|
||||||
|
ErrNoSecretAccessKey = errors.New("s3api.Config: no secret access key env var name defined")
|
||||||
|
ErrNoBucketName = errors.New("s3api.Config: no bucket name env var name defined")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
store.Register("s3api", Factory{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3API is the subset of the AWS S3 client used by this store. It enables mocking in tests.
|
||||||
|
type S3API interface {
|
||||||
|
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
|
||||||
|
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
|
||||||
|
DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
|
||||||
|
HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory builds an S3-backed store. Tests can inject a Mock via Client.
|
||||||
|
// Factory can optionally carry a preconstructed S3 client (e.g., a mock in tests).
|
||||||
|
type Factory struct {
|
||||||
|
Client S3API
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface, error) {
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(data), &config); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.Valid(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BucketName == "" {
|
||||||
|
return nil, fmt.Errorf("%w: %s", store.ErrBadConfig, ErrNoBucketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a client was injected (e.g., tests), use it directly.
|
||||||
|
if f.Client != nil {
|
||||||
|
return &Store{
|
||||||
|
s3: f.Client,
|
||||||
|
bucket: config.BucketName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := awsConfig.LoadDefaultConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't load AWS config from environment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||||
|
o.UsePathStyle = config.PathStyle
|
||||||
|
})
|
||||||
|
|
||||||
|
return &Store{
|
||||||
|
s3: client,
|
||||||
|
bucket: config.BucketName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Factory) Valid(data json.RawMessage) error {
|
||||||
|
var config Config
|
||||||
|
if err := json.Unmarshal([]byte(data), &config); err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.Valid(); err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PathStyle bool `json:"pathStyle"`
|
||||||
|
BucketName string `json:"bucketName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Valid() error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if c.BucketName == "" {
|
||||||
|
errs = append(errs, ErrNoBucketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return fmt.Errorf("s3api.Config: invalid config: %w", errors.Join(errs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
78
lib/store/s3api/s3api.go
Normal file
78
lib/store/s3api/s3api.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
package s3api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/lib/store"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
s3 S3API
|
||||||
|
bucket string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Delete(ctx context.Context, key string) error {
|
||||||
|
normKey := strings.ReplaceAll(key, ":", "/")
|
||||||
|
// Emulate not found by probing first.
|
||||||
|
if _, err := s.s3.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &s.bucket, Key: &normKey}); err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", store.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
if _, err := s.s3.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &s.bucket, Key: &normKey}); err != nil {
|
||||||
|
return fmt.Errorf("can't delete from s3: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Get(ctx context.Context, key string) ([]byte, error) {
|
||||||
|
normKey := strings.ReplaceAll(key, ":", "/")
|
||||||
|
out, err := s.s3.GetObject(ctx, &s3.GetObjectInput{
|
||||||
|
Bucket: &s.bucket,
|
||||||
|
Key: &normKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", store.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
defer out.Body.Close()
|
||||||
|
if msStr, ok := out.Metadata["x-anubis-expiry-ms"]; ok && msStr != "" {
|
||||||
|
if ms, err := strconv.ParseInt(msStr, 10, 64); err == nil {
|
||||||
|
if time.Now().UnixMilli() >= ms {
|
||||||
|
_, _ = s.s3.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &s.bucket, Key: &normKey})
|
||||||
|
return nil, store.ErrNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b, err := io.ReadAll(out.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't read s3 object: %w", err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
|
||||||
|
normKey := strings.ReplaceAll(key, ":", "/")
|
||||||
|
// S3 has no native TTL; we store object with metadata X-Anubis-Expiry as epoch seconds.
|
||||||
|
var meta map[string]string
|
||||||
|
if expiry > 0 {
|
||||||
|
exp := time.Now().Add(expiry).UnixMilli()
|
||||||
|
meta = map[string]string{"x-anubis-expiry-ms": fmt.Sprintf("%d", exp)}
|
||||||
|
}
|
||||||
|
_, err := s.s3.PutObject(ctx, &s3.PutObjectInput{
|
||||||
|
Bucket: &s.bucket,
|
||||||
|
Key: &normKey,
|
||||||
|
Body: bytes.NewReader(value),
|
||||||
|
Metadata: meta,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't put s3 object: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Store) IsPersistent() bool { return true }
|
||||||
140
lib/store/s3api/s3api_test.go
Normal file
140
lib/store/s3api/s3api_test.go
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
package s3api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/lib/store/storetest"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockS3 is an in-memory mock of the methods we use.
|
||||||
|
type mockS3 struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
bucket string
|
||||||
|
data map[string][]byte
|
||||||
|
meta map[string]map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) PutObject(ctx context.Context, in *s3.PutObjectInput, _ ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if m.data == nil {
|
||||||
|
m.data = map[string][]byte{}
|
||||||
|
}
|
||||||
|
if m.meta == nil {
|
||||||
|
m.meta = map[string]map[string]string{}
|
||||||
|
}
|
||||||
|
b, _ := io.ReadAll(in.Body)
|
||||||
|
m.data[aws.ToString(in.Key)] = bytes.Clone(b)
|
||||||
|
if in.Metadata != nil {
|
||||||
|
m.meta[aws.ToString(in.Key)] = map[string]string{}
|
||||||
|
for k, v := range in.Metadata {
|
||||||
|
m.meta[aws.ToString(in.Key)][k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.bucket = aws.ToString(in.Bucket)
|
||||||
|
return &s3.PutObjectOutput{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) GetObject(ctx context.Context, in *s3.GetObjectInput, _ ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
b, ok := m.data[aws.ToString(in.Key)]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("not found")
|
||||||
|
}
|
||||||
|
out := &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(b))}
|
||||||
|
if md, ok := m.meta[aws.ToString(in.Key)]; ok {
|
||||||
|
out.Metadata = md
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) DeleteObject(ctx context.Context, in *s3.DeleteObjectInput, _ ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
delete(m.data, aws.ToString(in.Key))
|
||||||
|
delete(m.meta, aws.ToString(in.Key))
|
||||||
|
return &s3.DeleteObjectOutput{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockS3) HeadObject(ctx context.Context, in *s3.HeadObjectInput, _ ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if _, ok := m.data[aws.ToString(in.Key)]; !ok {
|
||||||
|
return nil, fmt.Errorf("not found")
|
||||||
|
}
|
||||||
|
return &s3.HeadObjectOutput{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImpl(t *testing.T) {
|
||||||
|
mock := &mockS3{}
|
||||||
|
f := Factory{Client: mock}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(Config{
|
||||||
|
BucketName: "bucket",
|
||||||
|
})
|
||||||
|
|
||||||
|
storetest.Common(t, f, json.RawMessage(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyNormalization(t *testing.T) {
|
||||||
|
mock := &mockS3{}
|
||||||
|
f := Factory{Client: mock}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(Config{
|
||||||
|
BucketName: "anubis",
|
||||||
|
})
|
||||||
|
|
||||||
|
s, err := f.Build(t.Context(), json.RawMessage(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := "a:b:c"
|
||||||
|
val := []byte("value")
|
||||||
|
if err := s.Set(t.Context(), key, val, 0); err != nil {
|
||||||
|
t.Fatalf("Set failed: %v", err)
|
||||||
|
}
|
||||||
|
// Ensure mock saw normalized key
|
||||||
|
mock.mu.RLock()
|
||||||
|
_, hasRaw := mock.data["a:b:c"]
|
||||||
|
got, hasNorm := mock.data["a/b/c"]
|
||||||
|
mock.mu.RUnlock()
|
||||||
|
if hasRaw {
|
||||||
|
t.Fatalf("mock contains raw key with colon; normalization failed")
|
||||||
|
}
|
||||||
|
if !hasNorm || !bytes.Equal(got, val) {
|
||||||
|
t.Fatalf("normalized key missing or wrong value: got=%q", string(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get using colon key should work
|
||||||
|
out, err := s.Get(t.Context(), key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(out, val) {
|
||||||
|
t.Fatalf("Get returned wrong value: got=%q", string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete using colon key should delete normalized object
|
||||||
|
if err := s.Delete(t.Context(), key); err != nil {
|
||||||
|
t.Fatalf("Delete failed: %v", err)
|
||||||
|
}
|
||||||
|
// Give any async cleanup in tests a tick (not needed for mock, but harmless)
|
||||||
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
mock.mu.RLock()
|
||||||
|
_, exists := mock.data["a/b/c"]
|
||||||
|
mock.mu.RUnlock()
|
||||||
|
if exists {
|
||||||
|
t.Fatalf("normalized key still exists after Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
20
test/go.mod
20
test/go.mod
|
|
@ -5,7 +5,7 @@ go 1.24.5
|
||||||
replace github.com/TecharoHQ/anubis => ..
|
replace github.com/TecharoHQ/anubis => ..
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/TecharoHQ/anubis v1.21.3
|
github.com/TecharoHQ/anubis v1.22.0
|
||||||
github.com/docker/docker v28.3.2+incompatible
|
github.com/docker/docker v28.3.2+incompatible
|
||||||
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
|
@ -18,6 +18,24 @@ require (
|
||||||
github.com/TecharoHQ/thoth-proto v0.4.0 // indirect
|
github.com/TecharoHQ/thoth-proto v0.4.0 // indirect
|
||||||
github.com/a-h/templ v0.3.924 // indirect
|
github.com/a-h/templ v0.3.924 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
|
||||||
|
github.com/aws/smithy-go v1.23.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
|
|
||||||
36
test/go.sum
36
test/go.sum
|
|
@ -16,6 +16,42 @@ github.com/a-h/templ v0.3.924 h1:t5gZqTneXqvehpNZsgtnlOscnBboNh9aASBH2MgV/0k=
|
||||||
github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 h1:R0tNFJqfjHL3900cqhXuwQ+1K4G0xc9Yf8EDbFXCKEw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6/go.mod h1:y/7sDdu+aJvPtGXr4xYosdpq9a6T9Z0jkXfugmti0rI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 h1:hncKj/4gR+TPauZgTAsxOxNcvBayhUlYZ6LO/BYiQ30=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6/go.mod h1:OiIh45tp6HdJDDJGnja0mw8ihQGz3VGrUflLqSL0SmM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 h1:nEXUSAwyUfLTgnc9cxlDWy637qsq4UWwp3sNAfl0Z3Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6/go.mod h1:HGzIULx4Ge3Do2V0FaiYKcyKzOqwrhUZgCI77NisswQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 h1:ETkfWcXP2KNPLecaDa++5bsQhCRa5M5sLUJa5DWYIIg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3/go.mod h1:+/3ZTqoYb3Ur7DObD00tarKMLMuKg8iqz5CHEanqTnw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
|
||||||
|
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||||
|
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue