Compare commits

...

108 Commits
v0.1 ... v0.4

Author SHA1 Message Date
Ben McClelland
38ddbc4712 Merge pull request #126 from versity/admin-api-routing
Admin api routing
2023-07-06 14:42:22 -07:00
jonaustin09
cb193c42b4 fix: Up to date with main 2023-07-06 21:21:59 +04:00
jonaustin09
fbafc6b34c feat: Changed admin api http methods, some cleanup in admin cli commands, bug fix in delete user IAM service 2023-07-06 21:21:20 +04:00
Ben McClelland
d26b8856c1 Merge pull request #125 from versity/v4-auth-payload-support
V4 payload header support
2023-07-06 10:17:01 -07:00
Ben McClelland
23f738f37f Merge pull request #124 from versity/ben/copy_obj
feat: implement posix UploadCopyPart
2023-07-06 10:16:20 -07:00
jonaustin09
a10729b3ff fix: Fixed staticcheck error 2023-07-06 19:14:01 +04:00
jonaustin09
0330685c5c feat: Added support for unsigned, streamable and trailign payload header in sigv4 authentication 2023-07-06 19:03:19 +04:00
Ben McClelland
47dea2db7c feat: implement posix UploadCopyPart 2023-07-05 19:06:19 -07:00
Ben McClelland
db484eb900 Merge pull request #123 from versity/unit-testing-cleanup
Unit testing cleanup
2023-07-03 12:41:09 -07:00
Ben McClelland
140d41de40 Merge pull request #122 from versity/fe-upload-part-copy
Upload-part-copy FE
2023-07-03 12:37:19 -07:00
jonaustin09
39803cb158 feat: Some cleanup in controller unit tests, removed backend unsupported unit tests, added test cases for admin controller functions 2023-07-03 20:35:40 +04:00
jonaustin09
9c858b0396 feat: Added UploadPartCopy action in FE 2023-07-03 18:47:32 +04:00
jonaustin09
f63545c9b7 feat: Added UploadPartCopy action in FE 2023-07-03 17:14:46 +04:00
Ben McClelland
2894d4d5f3 Merge pull request #119 from versity/unit-test-coverage
Unit testing coverage
2023-06-30 12:49:06 -07:00
jonaustin09
46097fbf70 fix: Up to date with main 2023-06-30 22:06:25 +04:00
jonaustin09
9db01362a0 feat: increased unit testing coverage in controllers, utility functions and server functions. Fixed bucket owner bug in putbucketacl. 2 more minor changes in controllers 2023-06-30 22:04:46 +04:00
Ben McClelland
fbd7bce530 Merge pull request #118 from versity/ben/copy_obj
posix: cleanup extra debug output
2023-06-29 11:58:45 -07:00
Ben McClelland
7e34078d6a posix: cleanup extra debug output 2023-06-29 11:18:00 -07:00
Jon Austin
3c69c6922a Integration test cases for HeadBucket, CopyObject, DeleteObject actions (#117)
* feat: Added integration test cases for HeadBucket, CopyObject, DeleteObjects
* feat: Added logger for debugging
2023-06-29 10:40:54 -07:00
Ben McClelland
08db927634 Merge pull request #116 from versity/ben/fix_range
fix range gets with unspecified end range
2023-06-29 09:29:06 -07:00
Ben McClelland
6d99c69953 fix range gets with unspecified end range
The aws cli will send range gets of an object with ranges like
the following:
bytes=0-8388607
bytes=8388608-16777215
bytes=16777216-25165823
bytes=25165824-

The last one with the end offset unspecified just means the rest of
the object. So this fixes that case where there is only one offset
in the range.
2023-06-28 23:09:49 -07:00
Jon Austin
4bfb3d84d3 Acl integration test (#115)
* feat: Added test an integration test case for acl actions(get, put), fixed PutBucketAcl actions bugs, fixed iam bugs on getting and creating user accounts

* fix: Fixed acl unit tests

* fix: Fixed cli path in exec command in acl integration test

* fix: fixed account creation bug
2023-06-28 19:38:35 -07:00
Jon Austin
30dbd02a83 Tag actions integrations tests (#114)
* feat: Added an integration test case for for tag actions(get, put, delete)
2023-06-26 14:25:24 -07:00
Ben McClelland
f8afeec0a0 Merge pull request #112 from versity/ben/readme
update README.md with some content clarifications
2023-06-26 12:30:35 -07:00
Jon Austin
45e3c0922d Tag actions FE (#113)
* feat: Added get-object-tagging, put-object-tagging, delete-object-tagging actions in fe
2023-06-26 12:29:56 -07:00
Ben McClelland
a3f95520a8 update README.md with some content clarifications 2023-06-26 10:18:50 -07:00
Ben McClelland
c45280b7db Merge pull request #111 from versity/ben/tests
add functional tests to github actions
2023-06-26 08:36:39 -07:00
Ben McClelland
77b0759f86 fix full flow mising TestRangeGet test 2023-06-25 11:00:54 -07:00
Ben McClelland
1da0c1ceba add coverage report for actions tests 2023-06-25 10:54:24 -07:00
Ben McClelland
1d476c6d4d add signal handler for clean shutdown 2023-06-25 10:29:14 -07:00
Ben McClelland
c4f5f958eb add functional tests to github actions 2023-06-23 18:38:19 -07:00
Jon Austin
f84cfe58e7 Bench test (#110)
* feat: test CLI command set up for client side testing, test cases are corresponded with subcommands, added full-flow test case

* fix: TLS configuration removed

* feat: Added benchmark test for client side testing in the CLI

* fix: Removed unused variables

* fix: fixed staticcheck error
2023-06-23 09:55:04 -07:00
Ben McClelland
59a1e68e15 Merge pull request #107 from versity/test-cli-setup
Test CLI setup
2023-06-22 10:21:23 -07:00
jonaustin09
672027f4aa fix: TLS configuration removed 2023-06-22 10:08:11 -07:00
jonaustin09
24ae7a2e86 feat: test CLI command set up for client side testing, test cases are corresponded with subcommands, added full-flow test case 2023-06-22 10:08:11 -07:00
Ben McClelland
696d68c977 Merge pull request #109 from versity/fix/scoutfs-dir-obj-key
fix: fixed object directory key for scoutfs fileToObj function
2023-06-22 10:03:36 -07:00
jonaustin09
b770daa3b5 fix: fixed object directory key for scoutfs fileToObj function 2023-06-22 20:51:37 +04:00
Ben McClelland
065c126096 Merge pull request #108 from versity/fix/dir-obj-key
fix: fixed directory object key prefix
2023-06-22 09:42:28 -07:00
jonaustin09
ed047f5046 fix: fixed directory object key prefix 2023-06-22 20:12:17 +04:00
Ben McClelland
286299d44b Merge pull request #105 from versity/ben/scoutfs_glacier
Ben/scoutfs glacier
2023-06-21 11:31:39 -07:00
Ben McClelland
c4e0aa69a8 scoutfs: add support for glacier emulation mode 2023-06-21 10:27:14 -07:00
Ben McClelland
5ce010b1fa refactor walk to allow for more general obj translation 2023-06-20 13:51:47 -07:00
Ben McClelland
4d50f7665a Merge pull request #104 from versity/logging-system
Logging system
2023-06-20 11:06:14 -07:00
jonaustin09
c01d3ed542 feat: control over logging in debug mode and control logging for specific actions 2023-06-20 19:39:58 +04:00
jonaustin09
0209ca4bc0 fix: fixed merge conflicts 2023-06-19 23:20:33 +04:00
jonaustin09
127b79e148 feat: Logging system set up 2023-06-19 23:18:16 +04:00
Ben McClelland
4850ac34fc Merge pull request #103 from versity/ben/auth
refactor move auth to top level
2023-06-19 12:01:40 -07:00
Ben McClelland
0f733ae0c8 refactor move auth to top level 2023-06-19 11:15:19 -07:00
Ben McClelland
776fda027c Merge pull request #101 from versity/ben/auth_iam
Ben/auth iam
2023-06-19 10:58:49 -07:00
Ben McClelland
33673de160 fix case where bucket directory is created without acl 2023-06-19 10:34:45 -07:00
Ben McClelland
d2eab5bce3 posix: move iam data store to file
Storing to a file will allow more than 64k of storage that the xattr
would be limited to. This attempts to resolve racing updates between
multiple gateways without an explicit coordination between gateways.

This wil also setup a default IAM file on init.
2023-06-19 10:21:53 -07:00
Ben McClelland
94808bb4a9 refactor iam service for blind backend store 2023-06-19 09:53:19 -07:00
Ben McClelland
e7f6f76fb4 Merge pull request #100 from versity/ben/acls
refactor ACLs to separate out ACL logic from backend
2023-06-19 09:26:34 -07:00
Ben McClelland
2427c67171 refactor ACLs to separate out ACL logic from backend 2023-06-16 16:47:05 -07:00
Ben McClelland
b45cab6b05 Merge pull request #99 from versity/ben/update_deps
update dependencies
2023-06-16 11:28:40 -07:00
Ben McClelland
3b1be966d5 update dependencies 2023-06-16 11:04:09 -07:00
Ben McClelland
61c4e31fa1 Merge pull request #93 from versity/ben/scoutfs
feat: scoutfs backend with move blocks multipart optimized
2023-06-16 10:32:38 -07:00
Ben McClelland
09e8889e75 feat: scoutfs backend with move blocks multipart optimized 2023-06-16 10:25:52 -07:00
Ben McClelland
3ba5f21f51 Merge pull request #94 from versity/ben/list_buckets
fix list buckets response for single bucket entry
2023-06-16 10:25:36 -07:00
Ben McClelland
5c61604e82 fix list buckets response for single bucket entry
The xml encoding of the s3.ListBucketsOutput return type was not giving
correct results when there is only a single bucket. This revives the
old aws xsd schema and generated types that will give more accurate xml
encoding results.
2023-06-16 10:22:25 -07:00
Ben McClelland
246dbe4f6b Merge pull request #95 from versity/acl-checker
ACL setup
2023-06-16 10:19:22 -07:00
jonaustin09
36653ac996 fix: Merge conflicts merged 2023-06-16 20:59:01 +04:00
jonaustin09
49af6f0049 feat: ACL set up finished: added VerifyACL function, added admin checker function on list buckets, fixed all the unit tests 2023-06-16 20:55:23 +04:00
Jon Austin
ad09d98891 feat: Implemented GetBucketACL, PutBucketACL posix functions, fixed a… (#92)
* feat: Implemented GetBucketACL, PutBucketACL posix functions, fixed authentication middleware signed headers bug

* fix: Fixed GetBucketAcl return type, fixed staticcheck uppercase error, fixed unit tests for PutActions
2023-06-15 10:49:17 -07:00
jonaustin09
3d7ce4210a fix: Fixed GetBucketAcl return type, fixed staticcheck uppercase error, fixed unit tests for PutActions 2023-06-15 20:39:20 +04:00
jonaustin09
114d9fdf63 fix: Branch up to date 2023-06-14 22:41:44 +04:00
jonaustin09
21f0fea5a7 feat: Implemented GetBucketACL, PutBucketACL posix functions, fixed authentication middleware signed headers bug 2023-06-14 22:39:27 +04:00
Ben McClelland
6abafe2169 Merge pull request #91 from versity/ben/err_log
fix: only print request headers on error
2023-06-14 09:28:52 -07:00
Ben McClelland
ae1f5cda2f fix: only print request headers on error 2023-06-14 09:16:41 -07:00
Ben McClelland
66e68a5d1a Merge pull request #90 from versity/ben/fix_linux_otmp
fix: linux otmp object and part uploads
2023-06-14 09:14:14 -07:00
Ben McClelland
20638aee49 fix: linux otmp object and part uploads
We were missing the object and directory name in the O_TMPFILE uploads,
so were incorrectly trying to link these into the top level directory.
2023-06-14 08:42:39 -07:00
Jon Austin
1bcdf948ba feat: Move IAM configuration file creation on backend running, set up… (#89)
* feat: Move IAM configuration file creation on backend running
2023-06-13 11:13:18 -07:00
Ben McClelland
16a9b6b507 Merge pull request #86 from versity/ben/err_log
add internal error log to non-xml response
2023-06-13 09:31:26 -07:00
Ben McClelland
32efd670e1 add internal error log to non-xml response 2023-06-13 09:11:44 -07:00
Ben McClelland
78545d9205 Merge pull request #84 from versity/ben/spellcheek
fix some spelling errors
2023-06-12 14:08:31 -07:00
Ben McClelland
dfd8709777 fix some spelling errors 2023-06-12 14:00:10 -07:00
Ben McClelland
eaedc434c6 Merge pull request #83 from versity/ben/backend_cleanup
cleanup unused backend interface
2023-06-12 12:15:30 -07:00
Ben McClelland
7157280627 cleanup unused backend interface 2023-06-12 11:49:57 -07:00
Ben McClelland
f25ba05038 Merge pull request #82 from versity/ben/readme_logo
add logo to footer of README.md
2023-06-12 10:43:16 -07:00
Ben McClelland
6592ec5ae1 add logo to footer of README.md 2023-06-12 10:29:35 -07:00
Ben McClelland
e4d1041ea1 Merge pull request #81 from versity/ben/actions
change github workflow to use latest stable go version
2023-06-12 09:35:31 -07:00
Ben McClelland
53840f27c9 change github workflow to use latest stable go version 2023-06-12 09:29:03 -07:00
Ben McClelland
067f9e07c3 Merge pull request #80 from versity/admin-delete-api
feat: Added admin api and admin CLI aciton to delete a user
2023-06-12 09:21:31 -07:00
jonaustin09
def500d464 fix: Merged main branch into admin-delete-api 2023-06-12 20:00:34 +04:00
jonaustin09
b98f48ce2c feat: Added admin api and admin CLI aciton to delete a user 2023-06-12 19:58:28 +04:00
Ben McClelland
41ee0bf487 Merge pull request #79 from versity/ben/coc
Create CODE_OF_CONDUCT.md
2023-06-12 08:41:14 -07:00
Ben McClelland
afb40db50e Create CODE_OF_CONDUCT.md 2023-06-12 08:41:02 -07:00
Ben McClelland
a95d03c498 Merge pull request #78 from versity/ben/cleanup_base
Ben/cleanup base
2023-06-12 08:00:05 -07:00
Ben McClelland
feace16fa9 set response headers for get object 2023-06-12 07:46:09 -07:00
Ben McClelland
33e1d39138 cleanup responses to split out expected xml body response 2023-06-12 07:46:09 -07:00
Ben McClelland
115910eafe Merge pull request #72 from versity/ben/posix_multipart
Ben/posix multipart
2023-06-12 07:45:35 -07:00
Ben McClelland
ef06d11d7c fix: get simple multipart upload tests passing 2023-06-12 07:37:21 -07:00
Ben McClelland
2697edd40a head object time format 2023-06-12 07:15:57 -07:00
Ben McClelland
f88cb9fa7f Merge pull request #70 from versity/ben/internal_error_log
feat: add log for internal server errors not of type s3err.APIError
2023-06-12 07:15:16 -07:00
Ben McClelland
38bb042a32 Merge pull request #74 from versity/benmcclelland-patch-1
Added dark/light theme logo and footer to README.md
2023-06-10 20:21:26 -07:00
Ben McClelland
7682defa95 Added dark/light theme logo and footer to README.md 2023-06-10 20:21:07 -07:00
Ben McClelland
12df87577b Merge pull request #73 from versity/benmcclelland-patch-1
Add documentation/wiki links to README.md
2023-06-10 13:57:05 -07:00
Ben McClelland
92a763e53a Add documentation/wiki links to README.md 2023-06-10 13:56:03 -07:00
Ben McClelland
c3aaf1538e Merge pull request #71 from versity/ben/readme_updates
update README
2023-06-09 10:59:34 -07:00
Ben McClelland
c7625c9b58 update README 2023-06-09 10:58:30 -07:00
Ben McClelland
50357ce61a feat: add log for internal server errors not of type s3err.APIError 2023-06-09 10:35:21 -07:00
Jon Austin
160a99cbbd feat: Added admin CLI, created api endpoint for creating new user, cr… (#68)
* feat: Added admin CLI, created api endpoint for creating new user, created action for admin CLI to create a new user, changed the authentication middleware to verify the users from db

* feat: Added both single and multi user support, added caching layer for getting IAM users

* fix: Added all the files
2023-06-09 10:30:20 -07:00
Ben McClelland
0350215e2e Merge pull request #69 from versity/ben/dir_objects
Ben/dir objects
2023-06-09 10:25:32 -07:00
Ben McClelland
de346816fc fix put directory object 2023-06-08 22:32:54 -07:00
Ben McClelland
f1ac6b808b fix list objects for directory type objects 2023-06-08 22:04:08 -07:00
Ben McClelland
8ade0c96cf Merge pull request #67 from versity/ben/list
fix list objects
2023-06-08 10:33:54 -07:00
Ben McClelland
f4400edaa0 fix list objects 2023-06-07 22:57:00 -07:00
meghanmcclelland
f337aa288d Update README.md (#66)
* Update README.md

* Update README.md
2023-06-07 17:34:01 -07:00
56 changed files with 9035 additions and 2900 deletions

30
.github/workflows/functional.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: functional tests
on: pull_request
jobs:
build:
name: RunTests
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 'stable'
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Get Dependencies
run: |
go get -v -t -d ./...
- name: Build and Run
run: |
make testbin
./runtests.sh
- name: Coverage Report
run: |
go tool covdata percent -i=/tmp/covdata

View File

@@ -8,13 +8,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: 'stable'
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Verify all files pass gofmt formatting
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then gofmt -s -d .; exit 1; fi
@@ -24,10 +24,10 @@ jobs:
go get -v -t -d ./...
- name: Build
run: go build -o versitygw cmd/versitygw/*.go
run: make
- name: Test
run: go test -v -timeout 30s -tags=github ./...
run: go test -coverprofile profile.txt -race -v -timeout 30s -tags=github ./...
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest

2
.gitignore vendored
View File

@@ -32,3 +32,5 @@ VERSION
/versitygw.spec
*.tar
*.tar.gz
**/rand.data
/profile.txt

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
versitygw@versity.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -34,6 +34,9 @@ build: $(BIN)
$(BIN):
$(GOBUILD) $(LDFLAGS) -o $(BIN) cmd/$(BIN)/*.go
testbin:
$(GOBUILD) $(LDFLAGS) -o $(BIN) -cover -race cmd/$(BIN)/*.go
.PHONY: test
test:
$(GOTEST) ./...

View File

@@ -1,24 +1,35 @@
# Versity S3 Gateway
# The Versity Gateway:<br/>A High-Performance S3 to Storage System Translation Service
[![Versity Logo](https://www.versity.com/wp-content/themes/versity-theme/assets/img/svg/logo.svg)](https://www.versity.com)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/versity/versitygw/blob/assets/assets/logo-white.svg">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/versity/versitygw/blob/assets/assets/logo.svg">
<a href="https://www.versity.com"><img alt="Versity Software logo image." src="https://github.com/versity/versitygw/blob/assets/assets/logo.svg"></a>
</picture>
[![Apache V2 License](https://img.shields.io/badge/license-Apache%20V2-blue.svg)](https://github.com/versity/versitygw/blob/main/LICENSE)
The Versity S3 Gateway provides an S3 server that translates S3 client access to a modular backend service. The server translates incoming S3 API requests and transforms them into equivalent operations to the backend service. By leveraging this gateway server, applications can interact with the S3-compatible API on top of already existing storage systems. This project enables leveraging existing infrastructure investments while seamlessly integrating with S3-compatible systems, offering increased flexibility and compatibility in managing data storage.
**Current status:** Alpha, in development not yet suited for production use
The Versity S3 Gateway is focused on performance, simplicity, and expandability. New backend types can be added to support new storage systems. The initial backend is a posix filesystem. The posix backend allows standing up an S3 compatible server from an existing filesystem mount with a simple command.
See project [documentation](https://github.com/versity/versitygw/wiki) on the wiki.
The gateway is completely stateless. Mutliple gateways can host the same backend service and clients can load balance across the gateways.
Versity Gateway, a simple to use tool for seamless inline translation between AWS S3 object commands and storage systems. The Versity Gateway bridges the gap between S3-reliant applications and other storage systems, enabling enhanced compatibility and integration while offering exceptional scalability.
The server translates incoming S3 API requests and transforms them into equivalent operations to the backend service. By leveraging this gateway server, applications can interact with the S3-compatible API on top of already existing storage systems. This project enables leveraging existing infrastructure investments while seamlessly integrating with S3-compatible systems, offering increased flexibility and compatibility in managing data storage.
The Versity Gateway is focused on performance, simplicity, and expandability. The Versity Gateway is designed with modularity in mind, enabling future extensions to support additional backend storage systems. At present, the Versity Gateway supports any generic POSIX file backend storage and Versitys open source ScoutFS filesystem.
The gateway is completely stateless. Multiple Versity Gateway instances may be deployed in a cluster to increase aggregate throughput. The Versity Gateways stateless architecture allows any request to be serviced by any gateway thereby distributing workloads and enhancing performance. Load balancers may be used to evenly distribute requests across the cluster of gateways for optimal performance.
The S3 HTTP(S) server and routing is implemented using the [Fiber](https://gofiber.io) web framework. This framework is actively developed with a focus on performance. S3 API compatibility leverages the official [aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) whenever possible for maximum service compatibility with AWS S3.
## Getting Started
See the [Quickstart](https://github.com/versity/versitygw/wiki/Quickstart) documentation.
### Run the gateway with posix backend:
```
mkdir /tmp/vgw
ADMIN_ACCESS_KEY="testuser" ADMIN_SECRET_KEY="secret" ./versitygw --port :10000 posix /tmp/vgw
ROOT_ACCESS_KEY="testuser" ROOT_SECRET_KEY="secret" ./versitygw --port :10000 posix /tmp/vgw
```
This will enable an S3 server on the current host listening on port 10000 and hosting the directory `/tmp/vgw`.
@@ -34,3 +45,19 @@ The command format is
versitygw [global options] command [command options] [arguments...]
```
The global options are specified before the backend type and the backend options are specified after.
***
#### Versity gives you clarity and control over your archival storage, so you can allocate more resources to your core mission.
### Contact
![versity logo](https://www.versity.com/wp-content/uploads/2022/12/cropped-android-chrome-512x512-1-32x32.png)
info@versity.com <br />
+1 844 726 8826
### @versitysoftware
[![linkedin](https://github.com/versity/versitygw/blob/assets/assets/linkedin.jpg)](https://www.linkedin.com/company/versity/) &nbsp;
[![twitter](https://github.com/versity/versitygw/blob/assets/assets/twitter.jpg)](https://twitter.com/VersitySoftware) &nbsp;
[![facebook](https://github.com/versity/versitygw/blob/assets/assets/facebook.jpg)](https://www.facebook.com/versitysoftware) &nbsp;
[![instagram](https://github.com/versity/versitygw/blob/assets/assets/instagram.jpg)](https://www.instagram.com/versitysoftware/) &nbsp;

265
auth/acl.go Normal file
View File

@@ -0,0 +1,265 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package auth
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
)
type ACL struct {
ACL types.BucketCannedACL
Owner string
Grantees []Grantee
}
type Grantee struct {
Permission types.Permission
Access string
}
type GetBucketAclOutput struct {
Owner *types.Owner
AccessControlList AccessControlList
}
type AccessControlList struct {
Grants []types.Grant `xml:"Grant"`
}
type AccessControlPolicy struct {
AccessControlList AccessControlList `xml:"AccessControlList"`
Owner types.Owner
}
func ParseACL(data []byte) (ACL, error) {
if len(data) == 0 {
return ACL{}, nil
}
var acl ACL
if err := json.Unmarshal(data, &acl); err != nil {
return acl, fmt.Errorf("parse acl: %w", err)
}
return acl, nil
}
func ParseACLOutput(data []byte) (GetBucketAclOutput, error) {
var acl ACL
if err := json.Unmarshal(data, &acl); err != nil {
return GetBucketAclOutput{}, fmt.Errorf("parse acl: %w", err)
}
grants := []types.Grant{}
for _, elem := range acl.Grantees {
acs := elem.Access
grants = append(grants, types.Grant{Grantee: &types.Grantee{ID: &acs}, Permission: elem.Permission})
}
return GetBucketAclOutput{
Owner: &types.Owner{
ID: &acl.Owner,
},
AccessControlList: AccessControlList{
Grants: grants,
},
}, nil
}
func UpdateACL(input *s3.PutBucketAclInput, acl ACL, iam IAMService) ([]byte, error) {
if input == nil {
return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
}
if acl.Owner != *input.AccessControlPolicy.Owner.ID {
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
}
// if the ACL is specified, set the ACL, else replace the grantees
if input.ACL != "" {
acl.ACL = input.ACL
acl.Grantees = []Grantee{}
} else {
grantees := []Grantee{}
accs := []string{}
if input.GrantRead != nil {
fullControlList, readList, readACPList, writeList, writeACPList := []string{}, []string{}, []string{}, []string{}, []string{}
if *input.GrantFullControl != "" {
fullControlList = splitUnique(*input.GrantFullControl, ",")
fmt.Println(fullControlList)
for _, str := range fullControlList {
grantees = append(grantees, Grantee{Access: str, Permission: "FULL_CONTROL"})
}
}
if *input.GrantRead != "" {
readList = splitUnique(*input.GrantRead, ",")
for _, str := range readList {
grantees = append(grantees, Grantee{Access: str, Permission: "READ"})
}
}
if *input.GrantReadACP != "" {
readACPList = splitUnique(*input.GrantReadACP, ",")
for _, str := range readACPList {
grantees = append(grantees, Grantee{Access: str, Permission: "READ_ACP"})
}
}
if *input.GrantWrite != "" {
writeList = splitUnique(*input.GrantWrite, ",")
for _, str := range writeList {
grantees = append(grantees, Grantee{Access: str, Permission: "WRITE"})
}
}
if *input.GrantWriteACP != "" {
writeACPList = splitUnique(*input.GrantWriteACP, ",")
for _, str := range writeACPList {
grantees = append(grantees, Grantee{Access: str, Permission: "WRITE_ACP"})
}
}
accs = append(append(append(append(fullControlList, readList...), writeACPList...), readACPList...), writeList...)
} else {
cache := make(map[string]bool)
for _, grt := range input.AccessControlPolicy.Grants {
grantees = append(grantees, Grantee{Access: *grt.Grantee.ID, Permission: grt.Permission})
if _, ok := cache[*grt.Grantee.ID]; !ok {
cache[*grt.Grantee.ID] = true
accs = append(accs, *grt.Grantee.ID)
}
}
}
// Check if the specified accounts exist
accList, err := checkIfAccountsExist(accs, iam)
if err != nil {
return nil, err
}
if len(accList) > 0 {
return nil, fmt.Errorf("accounts does not exist: %s", strings.Join(accList, ", "))
}
acl.Grantees = grantees
acl.ACL = ""
}
result, err := json.Marshal(acl)
if err != nil {
return nil, err
}
return result, nil
}
func checkIfAccountsExist(accs []string, iam IAMService) ([]string, error) {
result := []string{}
for _, acc := range accs {
_, err := iam.GetUserAccount(acc)
if err != nil && err != ErrNoSuchUser {
return nil, fmt.Errorf("check user account: %w", err)
}
if err == ErrNoSuchUser {
result = append(result, acc)
}
}
return result, nil
}
func splitUnique(s, divider string) []string {
elements := strings.Split(s, divider)
uniqueElements := make(map[string]bool)
result := make([]string, 0, len(elements))
for _, element := range elements {
if _, ok := uniqueElements[element]; !ok {
result = append(result, element)
uniqueElements[element] = true
}
}
return result
}
func VerifyACL(acl ACL, bucket, access string, permission types.Permission, isRoot bool) error {
if isRoot {
return nil
}
if acl.Owner == access {
return nil
}
if acl.ACL != "" {
if (permission == "READ" || permission == "READ_ACP") && (acl.ACL != "public-read" && acl.ACL != "public-read-write") {
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
if (permission == "WRITE" || permission == "WRITE_ACP") && acl.ACL != "public-read-write" {
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
return nil
} else {
grantee := Grantee{Access: access, Permission: permission}
granteeFullCtrl := Grantee{Access: access, Permission: "FULL_CONTROL"}
isFound := false
for _, grt := range acl.Grantees {
if grt == grantee || grt == granteeFullCtrl {
isFound = true
break
}
}
if isFound {
return nil
}
}
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
func IsAdmin(access string, isRoot bool) error {
var data IAMConfig
if isRoot {
return nil
}
file, err := os.ReadFile("users.json")
if err != nil {
return fmt.Errorf("unable to read config file: %w", err)
}
if err := json.Unmarshal(file, &data); err != nil {
return err
}
acc, ok := data.AccessAccounts[access]
if !ok {
return fmt.Errorf("user does not exist")
}
if acc.Role == "admin" {
return nil
}
return fmt.Errorf("only admin users have access to this resource")
}

36
auth/iam.go Normal file
View File

@@ -0,0 +1,36 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package auth
import (
"errors"
)
// Account is a gateway IAM account
type Account struct {
Secret string `json:"secret"`
Role string `json:"role"`
}
// IAMService is the interface for all IAM service implementations
//
//go:generate moq -out ../s3api/controllers/iam_moq_test.go -pkg controllers . IAMService
type IAMService interface {
CreateAccount(access string, account Account) error
GetUserAccount(access string) (Account, error)
DeleteUserAccount(access string) error
}
var ErrNoSuchUser = errors.New("user not found")

181
auth/iam_internal.go Normal file
View File

@@ -0,0 +1,181 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package auth
import (
"encoding/json"
"fmt"
"hash/crc32"
"sync"
)
// IAMServiceInternal manages the internal IAM service
type IAMServiceInternal struct {
storer Storer
mu sync.RWMutex
accts IAMConfig
serial uint32
}
// UpdateAcctFunc accepts the current data and returns the new data to be stored
type UpdateAcctFunc func([]byte) ([]byte, error)
// Storer is the interface to manage the peristent IAM data for the internal
// IAM service
type Storer interface {
InitIAM() error
GetIAM() ([]byte, error)
StoreIAM(UpdateAcctFunc) error
}
// IAMConfig stores all internal IAM accounts
type IAMConfig struct {
AccessAccounts map[string]Account `json:"accessAccounts"`
}
var _ IAMService = &IAMServiceInternal{}
// NewInternal creates a new instance for the Internal IAM service
func NewInternal(s Storer) (*IAMServiceInternal, error) {
i := &IAMServiceInternal{
storer: s,
}
err := i.updateCache()
if err != nil {
return nil, fmt.Errorf("refresh iam cache: %w", err)
}
return i, nil
}
// CreateAccount creates a new IAM account. Returns an error if the account
// already exists.
func (s *IAMServiceInternal) CreateAccount(access string, account Account) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.storer.StoreIAM(func(data []byte) ([]byte, error) {
var conf IAMConfig
if len(data) > 0 {
if err := json.Unmarshal(data, &conf); err != nil {
return nil, fmt.Errorf("failed to parse iam: %w", err)
}
} else {
conf = IAMConfig{AccessAccounts: map[string]Account{}}
}
_, ok := conf.AccessAccounts[access]
if ok {
return nil, fmt.Errorf("account already exists")
}
conf.AccessAccounts[access] = account
b, err := json.Marshal(conf)
if err != nil {
return nil, fmt.Errorf("failed to serialize iam: %w", err)
}
s.accts = conf
return b, nil
})
}
// GetUserAccount retrieves account info for the requested user. Returns
// ErrNoSuchUser if the account does not exist.
func (s *IAMServiceInternal) GetUserAccount(access string) (Account, error) {
s.mu.RLock()
defer s.mu.RUnlock()
data, err := s.storer.GetIAM()
if err != nil {
return Account{}, fmt.Errorf("get iam data: %w", err)
}
serial := crc32.ChecksumIEEE(data)
if serial != s.serial {
s.mu.RUnlock()
err := s.updateCache()
s.mu.RLock()
if err != nil {
return Account{}, fmt.Errorf("refresh iam cache: %w", err)
}
}
acct, ok := s.accts.AccessAccounts[access]
if !ok {
return Account{}, ErrNoSuchUser
}
return acct, nil
}
// updateCache must be called with no locks held
func (s *IAMServiceInternal) updateCache() error {
s.mu.Lock()
defer s.mu.Unlock()
data, err := s.storer.GetIAM()
if err != nil {
return fmt.Errorf("get iam data: %w", err)
}
serial := crc32.ChecksumIEEE(data)
if len(data) > 0 {
if err := json.Unmarshal(data, &s.accts); err != nil {
return fmt.Errorf("failed to parse the config file: %w", err)
}
} else {
s.accts.AccessAccounts = make(map[string]Account)
}
s.serial = serial
return nil
}
// DeleteUserAccount deletes the specified user account. Does not check if
// account exists.
func (s *IAMServiceInternal) DeleteUserAccount(access string) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.storer.StoreIAM(func(data []byte) ([]byte, error) {
if len(data) == 0 {
// empty config, do nothing
return data, nil
}
var conf IAMConfig
if err := json.Unmarshal(data, &conf); err != nil {
return nil, fmt.Errorf("failed to parse iam: %w", err)
}
delete(conf.AccessAccounts, access)
b, err := json.Marshal(conf)
if err != nil {
return nil, fmt.Errorf("failed to serialize iam: %w", err)
}
s.accts = conf
return b, nil
})
}

View File

@@ -21,43 +21,41 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3response"
)
//go:generate moq -out backend_moq_test.go . Backend
//go:generate moq -out ../s3api/controllers/backend_moq_test.go -pkg controllers . Backend
type Backend interface {
fmt.Stringer
Shutdown()
ListBuckets() (*s3.ListBucketsOutput, error)
ListBuckets() (s3response.ListAllMyBucketsResult, error)
HeadBucket(bucket string) (*s3.HeadBucketOutput, error)
GetBucketAcl(bucket string) (*s3.GetBucketAclOutput, error)
PutBucket(bucket string) error
PutBucketAcl(*s3.PutBucketAclInput) error
GetBucketAcl(bucket string) ([]byte, error)
PutBucket(bucket, owner string) error
PutBucketAcl(bucket string, data []byte) error
DeleteBucket(bucket string) error
CreateMultipartUpload(*s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error)
CompleteMultipartUpload(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error)
AbortMultipartUpload(*s3.AbortMultipartUploadInput) error
ListMultipartUploads(output *s3.ListMultipartUploadsInput) (*s3.ListMultipartUploadsOutput, error)
ListObjectParts(bucket, object, uploadID string, partNumberMarker int, maxParts int) (*s3.ListPartsOutput, error)
CopyPart(srcBucket, srcObject, DstBucket, uploadID, rangeHeader string, part int) (*types.CopyPartResult, error)
ListMultipartUploads(*s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error)
ListObjectParts(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error)
PutObjectPart(bucket, object, uploadID string, part int, length int64, r io.Reader) (etag string, err error)
UploadPartCopy(*s3.UploadPartCopyInput) (s3response.CopyObjectResult, error)
PutObject(*s3.PutObjectInput) (string, error)
HeadObject(bucket, object string) (*s3.HeadObjectOutput, error)
GetObject(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error)
GetObjectAcl(bucket, object string) (*s3.GetObjectAclOutput, error)
GetObjectAttributes(bucket, object string, attributes []string) (*s3.GetObjectAttributesOutput, error)
CopyObject(srcBucket, srcObject, DstBucket, dstObject string) (*s3.CopyObjectOutput, error)
CopyObject(srcBucket, srcObject, dstBucket, dstObject string) (*s3.CopyObjectOutput, error)
ListObjects(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error)
ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsV2Output, error)
DeleteObject(bucket, object string) error
DeleteObjects(bucket string, objects *s3.DeleteObjectsInput) error
PutObjectAcl(*s3.PutObjectAclInput) error
RestoreObject(bucket, object string, restoreRequest *s3.RestoreObjectInput) error
UploadPart(bucket, object, uploadId string, Body io.ReadSeeker) (*s3.UploadPartOutput, error)
UploadPartCopy(*s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error)
GetTags(bucket, object string) (map[string]string, error)
SetTags(bucket, object string, tags map[string]string) error
@@ -75,10 +73,10 @@ func (BackendUnsupported) Shutdown() {}
func (BackendUnsupported) String() string {
return "Unsupported"
}
func (BackendUnsupported) ListBuckets() (*s3.ListBucketsOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
func (BackendUnsupported) ListBuckets() (s3response.ListAllMyBucketsResult, error) {
return s3response.ListAllMyBucketsResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutBucketAcl(*s3.PutBucketAclInput) error {
func (BackendUnsupported) PutBucketAcl(bucket string, data []byte) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutObjectAcl(*s3.PutObjectAclInput) error {
@@ -87,19 +85,16 @@ func (BackendUnsupported) PutObjectAcl(*s3.PutObjectAclInput) error {
func (BackendUnsupported) RestoreObject(bucket, object string, restoreRequest *s3.RestoreObjectInput) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) UploadPartCopy(*s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
func (BackendUnsupported) UploadPartCopy(*s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) UploadPart(bucket, object, uploadId string, Body io.ReadSeeker) (*s3.UploadPartOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) GetBucketAcl(bucket string) (*s3.GetBucketAclOutput, error) {
func (BackendUnsupported) GetBucketAcl(bucket string) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) HeadBucket(bucket string) (*s3.HeadBucketOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutBucket(bucket string) error {
func (BackendUnsupported) PutBucket(bucket, owner string) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) DeleteBucket(bucket string) error {
@@ -115,14 +110,11 @@ func (BackendUnsupported) CompleteMultipartUpload(bucket, object, uploadID strin
func (BackendUnsupported) AbortMultipartUpload(input *s3.AbortMultipartUploadInput) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListMultipartUploads(output *s3.ListMultipartUploadsInput) (*s3.ListMultipartUploadsOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
func (BackendUnsupported) ListMultipartUploads(output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
return s3response.ListMultipartUploadsResponse{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) ListObjectParts(bucket, object, uploadID string, partNumberMarker int, maxParts int) (*s3.ListPartsOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) CopyPart(srcBucket, srcObject, DstBucket, uploadID, rangeHeader string, part int) (*types.CopyPartResult, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
func (BackendUnsupported) ListObjectParts(bucket, object, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) {
return s3response.ListPartsResponse{}, s3err.GetAPIError(s3err.ErrNotImplemented)
}
func (BackendUnsupported) PutObjectPart(bucket, object, uploadID string, part int, length int64, r io.Reader) (etag string, err error) {
return "", s3err.GetAPIError(s3err.ErrNotImplemented)

File diff suppressed because it is too large Load Diff

View File

@@ -1,218 +0,0 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package backend
import (
"context"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
)
func TestBackend_ListBuckets(t *testing.T) {
type args struct {
ctx context.Context
}
type test struct {
name string
c Backend
args args
wantErr bool
}
var tests []test
tests = append(tests, test{
name: "list-Bucket",
c: &BackendMock{
ListBucketsFunc: func() (*s3.ListBucketsOutput, error) {
return &s3.ListBucketsOutput{
Buckets: []types.Bucket{
{
Name: aws.String("t1"),
},
},
}, s3err.GetAPIError(0)
},
},
args: args{
ctx: context.Background(),
},
wantErr: false,
}, test{
name: "list-Bucket-error",
c: &BackendMock{
ListBucketsFunc: func() (*s3.ListBucketsOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
},
},
args: args{
ctx: context.Background(),
},
wantErr: true,
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := tt.c.ListBuckets(); (err.(s3err.APIError).Code != "") != tt.wantErr {
t.Errorf("Backend.ListBuckets() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestBackend_HeadBucket(t *testing.T) {
type args struct {
ctx context.Context
BucketName string
}
type test struct {
name string
c Backend
args args
wantErr bool
}
var tests []test
tests = append(tests, test{
name: "head-buckets-error",
c: &BackendMock{
HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
},
},
args: args{
ctx: context.Background(),
BucketName: "b1",
},
wantErr: true,
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := tt.c.HeadBucket(tt.args.BucketName); (err.(s3err.APIError).Code != "") != tt.wantErr {
t.Errorf("Backend.HeadBucket() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestBackend_GetBucketAcl(t *testing.T) {
type args struct {
ctx context.Context
bucketName string
}
type test struct {
name string
c Backend
args args
wantErr bool
}
var tests []test
tests = append(tests, test{
name: "get bucket acl error",
c: &BackendMock{
GetBucketAclFunc: func(bucket string) (*s3.GetBucketAclOutput, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
},
},
args: args{
ctx: context.Background(),
bucketName: "b1",
},
wantErr: true,
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := tt.c.GetBucketAcl(tt.args.bucketName); (err.(s3err.APIError).Code != "") != tt.wantErr {
t.Errorf("Backend.GetBucketAcl() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestBackend_PutBucket(t *testing.T) {
type args struct {
ctx context.Context
bucketName string
}
type test struct {
name string
c Backend
args args
wantErr bool
}
var tests []test
tests = append(tests, test{
name: "put bucket ",
c: &BackendMock{
PutBucketFunc: func(bucket string) error {
return s3err.GetAPIError(0)
},
},
args: args{
ctx: context.Background(),
bucketName: "b1",
},
wantErr: false,
}, test{
name: "put bucket error",
c: &BackendMock{
PutBucketFunc: func(bucket string) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
},
},
args: args{
ctx: context.Background(),
bucketName: "b2",
},
wantErr: true,
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.c.PutBucket(tt.args.bucketName); (err.(s3err.APIError).Code != "") != tt.wantErr {
t.Errorf("Backend.PutBucket() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestBackend_DeleteBucket(t *testing.T) {
type args struct {
ctx context.Context
bucketName string
}
type test struct {
name string
c Backend
args args
wantErr bool
}
var tests []test
tests = append(tests, test{
name: "Delete Bucket Error",
c: &BackendMock{
DeleteBucketFunc: func(bucket string) error {
return s3err.GetAPIError(s3err.ErrNotImplemented)
},
},
args: args{
ctx: context.Background(),
bucketName: "b1",
},
wantErr: true,
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.c.DeleteBucket(tt.args.bucketName); (err.(s3err.APIError).Code != "") != tt.wantErr {
t.Errorf("Backend.DeleteBucket() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -15,22 +15,31 @@
package backend
import (
"errors"
"crypto/md5"
"encoding/hex"
"fmt"
"io/fs"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3response"
)
var (
// RFC3339TimeFormat RFC3339 time format
RFC3339TimeFormat = "2006-01-02T15:04:05.999Z"
)
func IsValidBucketName(name string) bool { return true }
type ByBucketName []types.Bucket
type ByBucketName []s3response.ListAllMyBucketsEntry
func (d ByBucketName) Len() int { return len(d) }
func (d ByBucketName) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
func (d ByBucketName) Less(i, j int) bool { return *d[i].Name < *d[j].Name }
func (d ByBucketName) Less(i, j int) bool { return d[i].Name < d[j].Name }
type ByObjectName []types.Object
@@ -46,6 +55,12 @@ func GetTimePtr(t time.Time) *time.Time {
return &t
}
var (
errInvalidRange = s3err.GetAPIError(s3err.ErrInvalidRequest)
)
// ParseRange parses input range header and returns startoffset, length, and
// error. If no endoffset specified, then length is set to -1.
func ParseRange(file fs.FileInfo, acceptRange string) (int64, int64, error) {
if acceptRange == "" {
return 0, file.Size(), nil
@@ -54,27 +69,54 @@ func ParseRange(file fs.FileInfo, acceptRange string) (int64, int64, error) {
rangeKv := strings.Split(acceptRange, "=")
if len(rangeKv) < 2 {
return 0, 0, errors.New("invalid range parameter")
return 0, 0, errInvalidRange
}
bRange := strings.Split(rangeKv[1], "-")
if len(bRange) < 2 {
return 0, 0, errors.New("invalid range parameter")
if len(bRange) < 1 || len(bRange) > 2 {
return 0, 0, errInvalidRange
}
startOffset, err := strconv.ParseInt(bRange[0], 10, 64)
if err != nil {
return 0, 0, errors.New("invalid range parameter")
return 0, 0, errInvalidRange
}
endOffset, err := strconv.ParseInt(bRange[1], 10, 64)
endOffset := int64(-1)
if len(bRange) == 1 || bRange[1] == "" {
return startOffset, endOffset, nil
}
endOffset, err = strconv.ParseInt(bRange[1], 10, 64)
if err != nil {
return 0, 0, errors.New("invalid range parameter")
return 0, 0, errInvalidRange
}
if endOffset < startOffset {
return 0, 0, errors.New("invalid range parameter")
return 0, 0, errInvalidRange
}
return int64(startOffset), int64(endOffset - startOffset + 1), nil
return startOffset, endOffset - startOffset + 1, nil
}
func GetMultipartMD5(parts []types.Part) string {
var partsEtagBytes []byte
for _, part := range parts {
partsEtagBytes = append(partsEtagBytes, getEtagBytes(*part.ETag)...)
}
s3MD5 := fmt.Sprintf("%s-%d", md5String(partsEtagBytes), len(parts))
return s3MD5
}
func getEtagBytes(etag string) []byte {
decode, err := hex.DecodeString(strings.ReplaceAll(etag, string('"'), ""))
if err != nil {
return []byte(etag)
}
return decode
}
func md5String(data []byte) string {
sum := md5.Sum(data)
return hex.EncodeToString(sum[:])
}

File diff suppressed because it is too large Load Diff

View File

@@ -76,7 +76,7 @@ func (tmp *tmpfile) link() error {
func (tmp *tmpfile) Write(b []byte) (int, error) {
if int64(len(b)) > tmp.size {
return 0, fmt.Errorf("write exceeds content length")
return 0, fmt.Errorf("write exceeds content length %v", tmp.size)
}
n, err := tmp.f.Write(b)

View File

@@ -68,7 +68,7 @@ func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) {
// later to link file into namespace
f := os.NewFile(uintptr(fd), filepath.Join(procfddir, strconv.Itoa(fd)))
tmp := &tmpfile{f: f, isOTmp: true, size: size}
tmp := &tmpfile{f: f, bucket: bucket, objname: obj, isOTmp: true, size: size}
// falloc is best effort, its fine if this fails
if size > 0 {
tmp.falloc()
@@ -117,7 +117,8 @@ func (tmp *tmpfile) link() error {
err = unix.Linkat(int(procdir.Fd()), filepath.Base(tmp.f.Name()),
int(dir.Fd()), filepath.Base(objPath), unix.AT_SYMLINK_FOLLOW)
if err != nil {
return fmt.Errorf("link tmpfile: %w", err)
return fmt.Errorf("link tmpfile (%q in %q): %w",
filepath.Dir(objPath), filepath.Base(tmp.f.Name()), err)
}
err = tmp.f.Close()
@@ -150,7 +151,7 @@ func (tmp *tmpfile) fallbackLink() error {
func (tmp *tmpfile) Write(b []byte) (int, error) {
if int64(len(b)) > tmp.size {
return 0, fmt.Errorf("write exceeds content length")
return 0, fmt.Errorf("write exceeds content length %v", tmp.size)
}
n, err := tmp.f.Write(b)

View File

@@ -15,12 +15,734 @@
package scoutfs
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/pkg/xattr"
"github.com/versity/scoutfs-go"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/backend/posix"
"github.com/versity/versitygw/s3err"
)
type ScoutFS struct {
*posix.Posix
rootfd *os.File
rootdir string
// glaciermode enables the following behavior:
// GET object: if file offline, return invalid object state
// HEAD object: if file offline, set obj storage class to GLACIER
// if file offline and staging, x-amz-restore: ongoing-request="true"
// if file offline and not staging, x-amz-restore: ongoing-request="false"
// if file online, x-amz-restore: ongoing-request="false", expiry-date="Fri, 2 Dec 2050 00:00:00 GMT"
// note: this expiry-date is not used but provided for client glacier compatibility
// ListObjects: if file offline, set obj storage class to GLACIER
// RestoreObject: add batch stage request to file
glaciermode bool
}
var _ backend.Backend = ScoutFS{}
var _ backend.Backend = &ScoutFS{}
const (
metaTmpDir = ".sgwtmp"
metaTmpMultipartDir = metaTmpDir + "/multipart"
tagHdr = "X-Amz-Tagging"
emptyMD5 = "d41d8cd98f00b204e9800998ecf8427e"
etagkey = "user.etag"
)
var (
stageComplete = "ongoing-request=\"false\", expiry-date=\"Fri, 2 Dec 2050 00:00:00 GMT\""
stageInProgress = "true"
stageNotInProgress = "false"
)
const (
// ScoutFS special xattr types
systemPrefix = "scoutfs.hide."
onameAttr = systemPrefix + "objname"
flagskey = systemPrefix + "sam_flags"
stagecopykey = systemPrefix + "sam_stagereq"
)
const (
// ScoutAM Flags
// Staging - file requested stage
Staging uint64 = 1 << iota
// StageFail - all copies failed to stage
StageFail
// NoArchive - no archive copies of file should be made
NoArchive
// ExtCacheRequested means file policy requests Ext Cache
ExtCacheRequested
// ExtCacheDone means this file ext cache copy has been
// created already (and possibly pruned, so may not exist)
ExtCacheDone
)
// Option sets various options for scoutfs
type Option func(s *ScoutFS)
// WithGlacierEmulation sets glacier mode emulation
func WithGlacierEmulation() Option {
return func(s *ScoutFS) { s.glaciermode = true }
}
func (s *ScoutFS) Shutdown() {
s.Posix.Shutdown()
s.rootfd.Close()
_ = s.rootdir
}
func (*ScoutFS) String() string {
return "ScoutFS Gateway"
}
// CompleteMultipartUpload scoutfs complete upload uses scoutfs move blocks
// ioctl to not have to read and copy the part data to the final object. This
// saves a read and write cycle for all mutlipart uploads.
func (s *ScoutFS) CompleteMultipartUpload(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
if err != nil {
return nil, fmt.Errorf("stat bucket: %w", err)
}
sum, err := s.checkUploadIDExists(bucket, object, uploadID)
if err != nil {
return nil, err
}
objdir := filepath.Join(bucket, metaTmpMultipartDir, fmt.Sprintf("%x", sum))
// check all parts ok
last := len(parts) - 1
partsize := int64(0)
var totalsize int64
for i, p := range parts {
partPath := filepath.Join(objdir, uploadID, fmt.Sprintf("%v", p.PartNumber))
fi, err := os.Lstat(partPath)
if err != nil {
return nil, s3err.GetAPIError(s3err.ErrInvalidPart)
}
if i == 0 {
partsize = fi.Size()
}
totalsize += fi.Size()
// all parts except the last need to be the same size
if i < last && partsize != fi.Size() {
return nil, s3err.GetAPIError(s3err.ErrInvalidPart)
}
// non-last part sizes need to be multiples of 4k for move blocks
// TODO: fallback to no move blocks if not 4k aligned?
if i == 0 && i < last && fi.Size()%4096 != 0 {
return nil, s3err.GetAPIError(s3err.ErrInvalidPart)
}
b, err := xattr.Get(partPath, "user.etag")
etag := string(b)
if err != nil {
etag = ""
}
parts[i].ETag = &etag
}
// use totalsize=0 because we wont be writing to the file, only moving
// extents around. so we dont want to fallocate this.
f, err := openTmpFile(filepath.Join(bucket, metaTmpDir), bucket, object, 0)
if err != nil {
return nil, fmt.Errorf("open temp file: %w", err)
}
defer f.cleanup()
for _, p := range parts {
pf, err := os.Open(filepath.Join(objdir, uploadID, fmt.Sprintf("%v", p.PartNumber)))
if err != nil {
return nil, fmt.Errorf("open part %v: %v", p.PartNumber, err)
}
// scoutfs move data is a metadata only operation that moves the data
// extent references from the source, appeding to the destination.
// this needs to be 4k aligned.
err = scoutfs.MoveData(pf, f.f)
pf.Close()
if err != nil {
return nil, fmt.Errorf("move blocks part %v: %v", p.PartNumber, err)
}
}
userMetaData := make(map[string]string)
upiddir := filepath.Join(objdir, uploadID)
loadUserMetaData(upiddir, userMetaData)
objname := filepath.Join(bucket, object)
dir := filepath.Dir(objname)
if dir != "" {
if err = mkdirAll(dir, os.FileMode(0755), bucket, object); err != nil {
if err != nil {
return nil, s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory)
}
}
}
err = f.link()
if err != nil {
return nil, fmt.Errorf("link object in namespace: %w", err)
}
for k, v := range userMetaData {
err = xattr.Set(objname, "user."+k, []byte(v))
if err != nil {
// cleanup object if returning error
os.Remove(objname)
return nil, fmt.Errorf("set user attr %q: %w", k, err)
}
}
// Calculate s3 compatible md5sum for complete multipart.
s3MD5 := backend.GetMultipartMD5(parts)
err = xattr.Set(objname, "user.etag", []byte(s3MD5))
if err != nil {
// cleanup object if returning error
os.Remove(objname)
return nil, fmt.Errorf("set etag attr: %w", err)
}
// cleanup tmp dirs
os.RemoveAll(upiddir)
// use Remove for objdir in case there are still other uploads
// for same object name outstanding
os.Remove(objdir)
return &s3.CompleteMultipartUploadOutput{
Bucket: &bucket,
ETag: &s3MD5,
Key: &object,
}, nil
}
func (s *ScoutFS) checkUploadIDExists(bucket, object, uploadID string) ([32]byte, error) {
sum := sha256.Sum256([]byte(object))
objdir := filepath.Join(bucket, metaTmpMultipartDir, fmt.Sprintf("%x", sum))
_, err := os.Stat(filepath.Join(objdir, uploadID))
if errors.Is(err, fs.ErrNotExist) {
return [32]byte{}, s3err.GetAPIError(s3err.ErrNoSuchUpload)
}
if err != nil {
return [32]byte{}, fmt.Errorf("stat upload: %w", err)
}
return sum, nil
}
func loadUserMetaData(path string, m map[string]string) (contentType, contentEncoding string) {
ents, err := xattr.List(path)
if err != nil || len(ents) == 0 {
return
}
for _, e := range ents {
if !isValidMeta(e) {
continue
}
b, err := xattr.Get(path, e)
if err == syscall.ENODATA {
m[strings.TrimPrefix(e, "user.")] = ""
continue
}
if err != nil {
continue
}
m[strings.TrimPrefix(e, "user.")] = string(b)
}
b, err := xattr.Get(path, "user.content-type")
contentType = string(b)
if err != nil {
contentType = ""
}
if contentType != "" {
m["content-type"] = contentType
}
b, err = xattr.Get(path, "user.content-encoding")
contentEncoding = string(b)
if err != nil {
contentEncoding = ""
}
if contentEncoding != "" {
m["content-encoding"] = contentEncoding
}
return
}
func isValidMeta(val string) bool {
if strings.HasPrefix(val, "user.X-Amz-Meta") {
return true
}
if strings.EqualFold(val, "user.Expires") {
return true
}
return false
}
// mkdirAll is similar to os.MkdirAll but it will return ErrObjectParentIsFile
// when appropriate
func mkdirAll(path string, perm os.FileMode, bucket, object string) error {
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
dir, err := os.Stat(path)
if err == nil {
if dir.IsDir() {
return nil
}
return s3err.GetAPIError(s3err.ErrObjectParentIsFile)
}
// Slow path: make sure parent exists and then call Mkdir for path.
i := len(path)
for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator.
i--
}
j := i
for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element.
j--
}
if j > 1 {
// Create parent.
err = mkdirAll(path[:j-1], perm, bucket, object)
if err != nil {
return err
}
}
// Parent now exists; invoke Mkdir and use its result.
err = os.Mkdir(path, perm)
if err != nil {
// Handle arguments like "foo/." by
// double-checking that directory doesn't exist.
dir, err1 := os.Lstat(path)
if err1 == nil && dir.IsDir() {
return nil
}
return s3err.GetAPIError(s3err.ErrObjectParentIsFile)
}
return nil
}
func (s *ScoutFS) HeadObject(bucket, object string) (*s3.HeadObjectOutput, error) {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
if err != nil {
return nil, fmt.Errorf("stat bucket: %w", err)
}
objPath := filepath.Join(bucket, object)
fi, err := os.Stat(objPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if err != nil {
return nil, fmt.Errorf("stat object: %w", err)
}
userMetaData := make(map[string]string)
contentType, contentEncoding := loadUserMetaData(objPath, userMetaData)
b, err := xattr.Get(objPath, etagkey)
etag := string(b)
if err != nil {
etag = ""
}
stclass := types.StorageClassStandard
requestOngoing := ""
if s.glaciermode {
requestOngoing = stageComplete
// Check if there are any offline exents associated with this file.
// If so, we will set storage class to glacier.
st, err := scoutfs.StatMore(objPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if err != nil {
return nil, fmt.Errorf("stat more: %w", err)
}
if st.Offline_blocks != 0 {
stclass = types.StorageClassGlacier
requestOngoing = stageNotInProgress
ok, err := isStaging(objPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if err != nil {
return nil, fmt.Errorf("check stage status: %w", err)
}
if ok {
requestOngoing = stageInProgress
}
}
}
return &s3.HeadObjectOutput{
ContentLength: fi.Size(),
ContentType: &contentType,
ContentEncoding: &contentEncoding,
ETag: &etag,
LastModified: backend.GetTimePtr(fi.ModTime()),
Metadata: userMetaData,
StorageClass: stclass,
Restore: &requestOngoing,
}, nil
}
func (s *ScoutFS) GetObject(bucket, object, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
if err != nil {
return nil, fmt.Errorf("stat bucket: %w", err)
}
objPath := filepath.Join(bucket, object)
fi, err := os.Stat(objPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if err != nil {
return nil, fmt.Errorf("stat object: %w", err)
}
startOffset, length, err := backend.ParseRange(fi, acceptRange)
if err != nil {
return nil, err
}
if length == -1 {
length = fi.Size() - startOffset + 1
}
if startOffset+length > fi.Size() {
return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
}
if s.glaciermode {
// Check if there are any offline exents associated with this file.
// If so, we will return the InvalidObjectState error.
st, err := scoutfs.StatMore(objPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if err != nil {
return nil, fmt.Errorf("stat more: %w", err)
}
if st.Offline_blocks != 0 {
return nil, s3err.GetAPIError(s3err.ErrInvalidObjectState)
}
}
f, err := os.Open(objPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if err != nil {
return nil, fmt.Errorf("open object: %w", err)
}
defer f.Close()
rdr := io.NewSectionReader(f, startOffset, length)
_, err = io.Copy(writer, rdr)
if err != nil {
return nil, fmt.Errorf("copy data: %w", err)
}
userMetaData := make(map[string]string)
contentType, contentEncoding := loadUserMetaData(objPath, userMetaData)
b, err := xattr.Get(objPath, etagkey)
etag := string(b)
if err != nil {
etag = ""
}
tags, err := s.getXattrTags(bucket, object)
if err != nil {
return nil, fmt.Errorf("get object tags: %w", err)
}
return &s3.GetObjectOutput{
AcceptRanges: &acceptRange,
ContentLength: length,
ContentEncoding: &contentEncoding,
ContentType: &contentType,
ETag: &etag,
LastModified: backend.GetTimePtr(fi.ModTime()),
Metadata: userMetaData,
TagCount: int32(len(tags)),
StorageClass: types.StorageClassStandard,
}, nil
}
func (s *ScoutFS) getXattrTags(bucket, object string) (map[string]string, error) {
tags := make(map[string]string)
b, err := xattr.Get(filepath.Join(bucket, object), "user."+tagHdr)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if isNoAttr(err) {
return tags, nil
}
if err != nil {
return nil, fmt.Errorf("get tags: %w", err)
}
err = json.Unmarshal(b, &tags)
if err != nil {
return nil, fmt.Errorf("unmarshal tags: %w", err)
}
return tags, nil
}
func (s *ScoutFS) ListObjects(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsOutput, error) {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
if err != nil {
return nil, fmt.Errorf("stat bucket: %w", err)
}
fileSystem := os.DirFS(bucket)
results, err := backend.Walk(fileSystem, prefix, delim, marker, maxkeys,
s.fileToObj(bucket), []string{metaTmpDir})
if err != nil {
return nil, fmt.Errorf("walk %v: %w", bucket, err)
}
return &s3.ListObjectsOutput{
CommonPrefixes: results.CommonPrefixes,
Contents: results.Objects,
Delimiter: &delim,
IsTruncated: results.Truncated,
Marker: &marker,
MaxKeys: int32(maxkeys),
Name: &bucket,
NextMarker: &results.NextMarker,
Prefix: &prefix,
}, nil
}
func (s *ScoutFS) ListObjectsV2(bucket, prefix, marker, delim string, maxkeys int) (*s3.ListObjectsV2Output, error) {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
if err != nil {
return nil, fmt.Errorf("stat bucket: %w", err)
}
fileSystem := os.DirFS(bucket)
results, err := backend.Walk(fileSystem, prefix, delim, marker, maxkeys,
s.fileToObj(bucket), []string{metaTmpDir})
if err != nil {
return nil, fmt.Errorf("walk %v: %w", bucket, err)
}
return &s3.ListObjectsV2Output{
CommonPrefixes: results.CommonPrefixes,
Contents: results.Objects,
Delimiter: &delim,
IsTruncated: results.Truncated,
ContinuationToken: &marker,
MaxKeys: int32(maxkeys),
Name: &bucket,
NextContinuationToken: &results.NextMarker,
Prefix: &prefix,
}, nil
}
func (s *ScoutFS) fileToObj(bucket string) backend.GetObjFunc {
return func(path string, d fs.DirEntry) (types.Object, error) {
objPath := filepath.Join(bucket, path)
if d.IsDir() {
// directory object only happens if directory empty
// check to see if this is a directory object by checking etag
etagBytes, err := xattr.Get(objPath, etagkey)
if isNoAttr(err) || errors.Is(err, fs.ErrNotExist) {
return types.Object{}, backend.ErrSkipObj
}
if err != nil {
return types.Object{}, fmt.Errorf("get etag: %w", err)
}
etag := string(etagBytes)
fi, err := d.Info()
if errors.Is(err, fs.ErrNotExist) {
return types.Object{}, backend.ErrSkipObj
}
if err != nil {
return types.Object{}, fmt.Errorf("get fileinfo: %w", err)
}
key := path + "/"
return types.Object{
ETag: &etag,
Key: &key,
LastModified: backend.GetTimePtr(fi.ModTime()),
}, nil
}
// file object, get object info and fill out object data
etagBytes, err := xattr.Get(objPath, etagkey)
if errors.Is(err, fs.ErrNotExist) {
return types.Object{}, backend.ErrSkipObj
}
if err != nil && !isNoAttr(err) {
return types.Object{}, fmt.Errorf("get etag: %w", err)
}
etag := string(etagBytes)
fi, err := d.Info()
if errors.Is(err, fs.ErrNotExist) {
return types.Object{}, backend.ErrSkipObj
}
if err != nil {
return types.Object{}, fmt.Errorf("get fileinfo: %w", err)
}
sc := types.ObjectStorageClassStandard
if s.glaciermode {
// Check if there are any offline exents associated with this file.
// If so, we will return the InvalidObjectState error.
st, err := scoutfs.StatMore(objPath)
if errors.Is(err, fs.ErrNotExist) {
return types.Object{}, backend.ErrSkipObj
}
if err != nil {
return types.Object{}, fmt.Errorf("stat more: %w", err)
}
if st.Offline_blocks != 0 {
sc = types.ObjectStorageClassGlacier
}
}
return types.Object{
ETag: &etag,
Key: &path,
LastModified: backend.GetTimePtr(fi.ModTime()),
Size: fi.Size(),
StorageClass: sc,
}, nil
}
}
// RestoreObject will set stage request on file if offline and do nothing if
// file is online
func (s *ScoutFS) RestoreObject(bucket, object string, restoreRequest *s3.RestoreObjectInput) error {
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
return s3err.GetAPIError(s3err.ErrNoSuchBucket)
}
if err != nil {
return fmt.Errorf("stat bucket: %w", err)
}
err = setStaging(filepath.Join(bucket, object))
if errors.Is(err, fs.ErrNotExist) {
return s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if err != nil {
return fmt.Errorf("stage object: %w", err)
}
return nil
}
func setStaging(objname string) error {
b, err := xattr.Get(objname, flagskey)
if err != nil && !isNoAttr(err) {
return err
}
var oldflags uint64
if !isNoAttr(err) {
err = json.Unmarshal(b, &oldflags)
if err != nil {
return err
}
}
newflags := oldflags | Staging
if newflags == oldflags {
// no flags change, just return
return nil
}
return fSetNewGlobalFlags(objname, newflags)
}
func isStaging(objname string) (bool, error) {
b, err := xattr.Get(objname, flagskey)
if err != nil && !isNoAttr(err) {
return false, err
}
var flags uint64
if !isNoAttr(err) {
err = json.Unmarshal(b, &flags)
if err != nil {
return false, err
}
}
return flags&Staging == Staging, nil
}
func fSetNewGlobalFlags(objname string, flags uint64) error {
b, err := json.Marshal(&flags)
if err != nil {
return err
}
return xattr.Set(objname, flagskey, b)
}
func isNoAttr(err error) bool {
if err == nil {
return false
}
xerr, ok := err.(*xattr.Error)
if ok && xerr.Err == xattr.ENOATTR {
return true
}
if err == syscall.ENODATA {
return true
}
return false
}

View File

@@ -0,0 +1,48 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package scoutfs
import (
"errors"
"fmt"
"os"
)
func New(rootdir string, opts ...Option) (*ScoutFS, error) {
return nil, fmt.Errorf("scoutfs only available on linux")
}
type tmpfile struct {
f *os.File
}
var (
errNotSupported = errors.New("not supported")
)
func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) {
return nil, errNotSupported
}
func (tmp *tmpfile) link() error {
return errNotSupported
}
func (tmp *tmpfile) Write(b []byte) (int, error) {
return 0, errNotSupported
}
func (tmp *tmpfile) cleanup() {
}

View File

@@ -0,0 +1,184 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package scoutfs
import (
"crypto/sha256"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"syscall"
"golang.org/x/sys/unix"
"github.com/versity/versitygw/backend/posix"
)
func New(rootdir string, opts ...Option) (*ScoutFS, error) {
p, err := posix.New(rootdir)
if err != nil {
return nil, err
}
f, err := os.Open(rootdir)
if err != nil {
return nil, fmt.Errorf("open %v: %w", rootdir, err)
}
s := &ScoutFS{Posix: p, rootfd: f, rootdir: rootdir}
for _, opt := range opts {
opt(s)
}
return s, nil
}
const procfddir = "/proc/self/fd"
type tmpfile struct {
f *os.File
bucket string
objname string
isOTmp bool
size int64
}
func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) {
// O_TMPFILE allows for a file handle to an unnamed file in the filesystem.
// This can help reduce contention within the namespace (parent directories),
// etc. And will auto cleanup the inode on close if we never link this
// file descriptor into the namespace.
// Not all filesystems support this, so fallback to CreateTemp for when
// this is not supported.
fd, err := unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, 0666)
if err != nil {
// O_TMPFILE not supported, try fallback
err := os.MkdirAll(dir, 0700)
if err != nil {
return nil, fmt.Errorf("make temp dir: %w", err)
}
f, err := os.CreateTemp(dir,
fmt.Sprintf("%x.", sha256.Sum256([]byte(obj))))
if err != nil {
return nil, err
}
tmp := &tmpfile{f: f, bucket: bucket, objname: obj, size: size}
// falloc is best effort, its fine if this fails
if size > 0 {
tmp.falloc()
}
return tmp, nil
}
// for O_TMPFILE, filename is /proc/self/fd/<fd> to be used
// later to link file into namespace
f := os.NewFile(uintptr(fd), filepath.Join(procfddir, strconv.Itoa(fd)))
tmp := &tmpfile{f: f, bucket: bucket, objname: obj, isOTmp: true, size: size}
// falloc is best effort, its fine if this fails
if size > 0 {
tmp.falloc()
}
return tmp, nil
}
func (tmp *tmpfile) falloc() error {
err := syscall.Fallocate(int(tmp.f.Fd()), 0, 0, tmp.size)
if err != nil {
return fmt.Errorf("fallocate: %v", err)
}
return nil
}
func (tmp *tmpfile) link() error {
// We use Linkat/Rename as the atomic operation for object puts. The
// upload is written to a temp (or unnamed/O_TMPFILE) file to not conflict
// with any other simultaneous uploads. The final operation is to move the
// temp file into place for the object. This ensures the object semantics
// of last upload completed wins and is not some combination of writes
// from simultaneous uploads.
objPath := filepath.Join(tmp.bucket, tmp.objname)
err := os.Remove(objPath)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("remove stale path: %w", err)
}
if !tmp.isOTmp {
// O_TMPFILE not suported, use fallback
return tmp.fallbackLink()
}
procdir, err := os.Open(procfddir)
if err != nil {
return fmt.Errorf("open proc dir: %w", err)
}
defer procdir.Close()
dir, err := os.Open(filepath.Dir(objPath))
if err != nil {
return fmt.Errorf("open parent dir: %w", err)
}
defer dir.Close()
err = unix.Linkat(int(procdir.Fd()), filepath.Base(tmp.f.Name()),
int(dir.Fd()), filepath.Base(objPath), unix.AT_SYMLINK_FOLLOW)
if err != nil {
return fmt.Errorf("link tmpfile: %w", err)
}
err = tmp.f.Close()
if err != nil {
return fmt.Errorf("close tmpfile: %w", err)
}
return nil
}
func (tmp *tmpfile) fallbackLink() error {
tempname := tmp.f.Name()
// cleanup in case anything goes wrong, if rename succeeds then
// this will no longer exist
defer os.Remove(tempname)
err := tmp.f.Close()
if err != nil {
return fmt.Errorf("close tmpfile: %w", err)
}
objPath := filepath.Join(tmp.bucket, tmp.objname)
err = os.Rename(tempname, objPath)
if err != nil {
return fmt.Errorf("rename tmpfile: %w", err)
}
return nil
}
func (tmp *tmpfile) Write(b []byte) (int, error) {
if int64(len(b)) > tmp.size {
return 0, fmt.Errorf("write exceeds content length %v", tmp.size)
}
n, err := tmp.f.Write(b)
tmp.size -= int64(n)
return n, err
}
func (tmp *tmpfile) cleanup() {
tmp.f.Close()
}

View File

@@ -0,0 +1,48 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package scoutfs
import (
"errors"
"fmt"
"os"
)
func New(rootdir string, opts ...Option) (*ScoutFS, error) {
return nil, fmt.Errorf("scoutfs only available on linux")
}
type tmpfile struct {
f *os.File
}
var (
errNotSupported = errors.New("not supported")
)
func openTmpFile(dir, bucket, obj string, size int64) (*tmpfile, error) {
return nil, errNotSupported
}
func (tmp *tmpfile) link() error {
return errNotSupported
}
func (tmp *tmpfile) Write(b []byte) (int, error) {
return 0, errNotSupported
}
func (tmp *tmpfile) cleanup() {
}

View File

@@ -15,6 +15,7 @@
package backend
import (
"errors"
"fmt"
"io/fs"
"os"
@@ -31,9 +32,13 @@ type WalkResults struct {
NextMarker string
}
type GetObjFunc func(path string, d fs.DirEntry) (types.Object, error)
var ErrSkipObj = errors.New("skip this object")
// Walk walks the supplied fs.FS and returns results compatible with list
// objects responses
func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int) (WalkResults, error) {
func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int, getObj GetObjFunc, skipdirs []string) (WalkResults, error) {
cpmap := make(map[string]struct{})
var objects []types.Object
@@ -63,16 +68,40 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int) (WalkResu
return nil
}
// If prefix is defined and the directory does not match prefix,
// do not descend into the directory because nothing will
// match this prefix. Make sure to append the / at the end of
// directories since this is implied as a directory path name.
if prefix != "" && !strings.HasPrefix(path+string(os.PathSeparator), prefix) {
if contains(d.Name(), skipdirs) {
return fs.SkipDir
}
// TODO: special case handling if directory is empty
// and was "PUT" explicitly
// If prefix is defined and the directory does not match prefix,
// do not descend into the directory because nothing will
// match this prefix. Make sure to append the / at the end of
// directories since this is implied as a directory path name.
// If path is a prefix of prefix, then path could still be
// building to match. So only skip if path isnt a prefix of prefix
// and prefix isnt a prefix of path.
if prefix != "" &&
!strings.HasPrefix(path+string(os.PathSeparator), prefix) &&
!strings.HasPrefix(prefix, path+string(os.PathSeparator)) {
return fs.SkipDir
}
// TODO: can we do better here rather than a second readdir
// per directory?
ents, err := fs.ReadDir(fileSystem, path)
if err != nil {
return fmt.Errorf("readdir %q: %w", path, err)
}
if len(ents) == 0 {
dirobj, err := getObj(path, d)
if err == ErrSkipObj {
return nil
}
if err != nil {
return fmt.Errorf("directory to object %q: %w", path, err)
}
objects = append(objects, dirobj)
}
return nil
}
@@ -83,28 +112,27 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int) (WalkResu
pastMarker = true
}
// If object doesnt have prefix, dont include in results.
// If object doesn't have prefix, don't include in results.
if prefix != "" && !strings.HasPrefix(path, prefix) {
return nil
}
if delimiter == "" {
// If no delimeter specified, then all files with matching
// If no delimiter specified, then all files with matching
// prefix are included in results
fi, err := d.Info()
if err != nil {
return fmt.Errorf("get info for %v: %w", path, err)
obj, err := getObj(path, d)
if err == ErrSkipObj {
return nil
}
if err != nil {
return fmt.Errorf("file to object %q: %w", path, err)
}
objects = append(objects, obj)
objects = append(objects, types.Object{
ETag: new(string),
Key: &path,
LastModified: GetTimePtr(fi.ModTime()),
Size: fi.Size(),
})
if (len(objects) + len(cpmap)) == max {
if max > 0 && (len(objects)+len(cpmap)) == max {
pastMax = true
}
return nil
}
@@ -115,7 +143,7 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int) (WalkResu
//
// For example:
// prefix = A/
// delimeter = /
// delimiter = /
// and objects:
// A/file
// A/B/file
@@ -124,24 +152,22 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int) (WalkResu
// objects: A/file
// common prefix: A/B/
//
// Note: No obects are included past the common prefix since
// Note: No objects are included past the common prefix since
// these are all rolled up into the common prefix.
// Note: The delimeter can be anything, so we have to operate on
// the full path without any assumptions on posix directory heirarchy
// here. Usually the delimeter with be "/", but thats not required.
// Note: The delimiter can be anything, so we have to operate on
// the full path without any assumptions on posix directory hierarchy
// here. Usually the delimiter will be "/", but thats not required.
suffix := strings.TrimPrefix(path, prefix)
before, _, found := strings.Cut(suffix, delimiter)
if !found {
fi, err := d.Info()
if err != nil {
return fmt.Errorf("get info for %v: %w", path, err)
obj, err := getObj(path, d)
if err == ErrSkipObj {
return nil
}
objects = append(objects, types.Object{
ETag: new(string),
Key: &path,
LastModified: GetTimePtr(fi.ModTime()),
Size: fi.Size(),
})
if err != nil {
return fmt.Errorf("file to object %q: %w", path, err)
}
objects = append(objects, obj)
if (len(objects) + len(cpmap)) == max {
pastMax = true
}
@@ -150,7 +176,7 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int) (WalkResu
// Common prefixes are a set, so should not have duplicates.
// These are abstractly a "directory", so need to include the
// delimeter at the end.
// delimiter at the end.
cpmap[prefix+before+delimiter] = struct{}{}
if (len(objects) + len(cpmap)) == max {
pastMax = true
@@ -162,15 +188,16 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int) (WalkResu
return WalkResults{}, err
}
commonPrefixStrings := make([]string, 0, len(cpmap))
var commonPrefixStrings []string
for k := range cpmap {
commonPrefixStrings = append(commonPrefixStrings, k)
}
sort.Strings(commonPrefixStrings)
commonPrefixes := make([]types.CommonPrefix, 0, len(commonPrefixStrings))
for _, cp := range commonPrefixStrings {
pfx := cp
commonPrefixes = append(commonPrefixes, types.CommonPrefix{
Prefix: &cp,
Prefix: &pfx,
})
}
@@ -181,3 +208,12 @@ func Walk(fileSystem fs.FS, prefix, delimiter, marker string, max int) (WalkResu
NextMarker: newMarker,
}, nil
}
func contains(a string, strs []string) bool {
for _, s := range strs {
if s == a {
return true
}
}
return false
}

View File

@@ -15,6 +15,9 @@
package backend_test
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io/fs"
"testing"
"testing/fstest"
@@ -26,31 +29,84 @@ import (
type walkTest struct {
fsys fs.FS
expected backend.WalkResults
getobj backend.GetObjFunc
}
func getObj(path string, d fs.DirEntry) (types.Object, error) {
if d.IsDir() {
etag := getMD5(path)
fi, err := d.Info()
if err != nil {
return types.Object{}, fmt.Errorf("get fileinfo: %w", err)
}
return types.Object{
ETag: &etag,
Key: &path,
LastModified: backend.GetTimePtr(fi.ModTime()),
}, nil
}
etag := getMD5(path)
fi, err := d.Info()
if err != nil {
return types.Object{}, fmt.Errorf("get fileinfo: %w", err)
}
return types.Object{
ETag: &etag,
Key: &path,
LastModified: backend.GetTimePtr(fi.ModTime()),
Size: fi.Size(),
}, nil
}
func getMD5(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}
func TestWalk(t *testing.T) {
tests := []walkTest{{
// test case from
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-prefixes.html
fsys: fstest.MapFS{
"sample.jpg": {},
"photos/2006/January/sample.jpg": {},
"photos/2006/February/sample2.jpg": {},
"photos/2006/February/sample3.jpg": {},
"photos/2006/February/sample4.jpg": {},
tests := []walkTest{
{
// test case from
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-prefixes.html
fsys: fstest.MapFS{
"sample.jpg": {},
"photos/2006/January/sample.jpg": {},
"photos/2006/February/sample2.jpg": {},
"photos/2006/February/sample3.jpg": {},
"photos/2006/February/sample4.jpg": {},
},
expected: backend.WalkResults{
CommonPrefixes: []types.CommonPrefix{{
Prefix: backend.GetStringPtr("photos/"),
}},
Objects: []types.Object{{
Key: backend.GetStringPtr("sample.jpg"),
}},
},
getobj: getObj,
},
expected: backend.WalkResults{
CommonPrefixes: []types.CommonPrefix{{
Prefix: backend.GetStringPtr("photos/"),
}},
Objects: []types.Object{{
Key: backend.GetStringPtr("sample.jpg"),
}},
{
// test case single dir/single file
fsys: fstest.MapFS{
"test/file": {},
},
expected: backend.WalkResults{
CommonPrefixes: []types.CommonPrefix{{
Prefix: backend.GetStringPtr("test/"),
}},
Objects: []types.Object{},
},
getobj: getObj,
},
}}
}
for _, tt := range tests {
res, err := backend.Walk(tt.fsys, "", "/", "", 1000)
res, err := backend.Walk(tt.fsys, "", "/", "", 1000, tt.getobj, []string{})
if err != nil {
t.Fatalf("walk: %v", err)
}
@@ -67,13 +123,16 @@ func compareResults(got, wanted backend.WalkResults, t *testing.T) {
}
if !compareObjects(got.Objects, wanted.Objects) {
t.Errorf("unexpected common prefix, got %v wanted %v",
t.Errorf("unexpected object, got %v wanted %v",
printObjects(got.Objects),
printObjects(wanted.Objects))
}
}
func compareCommonPrefix(a, b []types.CommonPrefix) bool {
if len(a) == 0 && len(b) == 0 {
return true
}
if len(a) != len(b) {
return false
}
@@ -108,6 +167,9 @@ func printCommonPrefixes(list []types.CommonPrefix) string {
}
func compareObjects(a, b []types.Object) bool {
if len(a) == 0 && len(b) == 0 {
return true
}
if len(a) != len(b) {
return false
}

192
cmd/versitygw/admin.go Normal file
View File

@@ -0,0 +1,192 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/urfave/cli/v2"
)
var (
adminAccess string
adminSecret string
adminRegion string
)
func adminCommand() *cli.Command {
return &cli.Command{
Name: "admin",
Usage: "admin CLI tool",
Description: `admin CLI tool for interacting with admin api.
Here is the available api list:
create-user
`,
Subcommands: []*cli.Command{
{
Name: "create-user",
Usage: "Create a new user",
Action: createUser,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "access",
Usage: "access value for the new user",
Required: true,
Aliases: []string{"a"},
},
&cli.StringFlag{
Name: "secret",
Usage: "secret value for the new user",
Required: true,
Aliases: []string{"s"},
},
&cli.StringFlag{
Name: "role",
Usage: "role for the new user",
Required: true,
Aliases: []string{"r"},
},
},
},
{
Name: "delete-user",
Usage: "Delete a user",
Action: deleteUser,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "access",
Usage: "access value for the user to be deleted",
Required: true,
Aliases: []string{"a"},
},
},
},
},
Flags: []cli.Flag{
// TODO: create a configuration file for this
&cli.StringFlag{
Name: "access",
Usage: "admin access account",
EnvVars: []string{"ADMIN_ACCESS_KEY_ID", "ADMIN_ACCESS_KEY"},
Aliases: []string{"a"},
Destination: &adminAccess,
},
&cli.StringFlag{
Name: "secret",
Usage: "admin secret access key",
EnvVars: []string{"ADMIN_SECRET_ACCESS_KEY", "ADMIN_SECRET_KEY"},
Aliases: []string{"s"},
Destination: &adminSecret,
},
&cli.StringFlag{
Name: "region",
Usage: "s3 region string",
Value: "us-east-1",
Destination: &adminRegion,
Aliases: []string{"r"},
},
},
}
}
func createUser(ctx *cli.Context) error {
access, secret, role := ctx.String("access"), ctx.String("secret"), ctx.String("role")
if access == "" || secret == "" {
return fmt.Errorf("invalid input parameters for the new user")
}
if role != "admin" && role != "user" {
return fmt.Errorf("invalid input parameter for role")
}
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:7070/create-user?access=%v&secret=%v&role=%v", access, secret, role), nil)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
signer := v4.NewSigner()
hashedPayload := sha256.Sum256([]byte{})
hexPayload := hex.EncodeToString(hashedPayload[:])
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", adminRegion, time.Now())
if signErr != nil {
return fmt.Errorf("failed to sign the request: %w", err)
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Printf("%s", body)
return nil
}
func deleteUser(ctx *cli.Context) error {
access := ctx.String("access")
if access == "" {
return fmt.Errorf("invalid input parameter for the new user")
}
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:7070/delete-user?access=%v", access), nil)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
signer := v4.NewSigner()
hashedPayload := sha256.Sum256([]byte{})
hexPayload := hex.EncodeToString(hashedPayload[:])
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: adminAccess, SecretAccessKey: adminSecret}, req, hexPayload, "s3", adminRegion, time.Now())
if signErr != nil {
return fmt.Errorf("failed to sign the request: %w", err)
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send the request: %w", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Printf("%s", body)
return nil
}

View File

@@ -15,6 +15,7 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
@@ -22,16 +23,16 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/urfave/cli/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/backend/auth"
"github.com/versity/versitygw/s3api"
"github.com/versity/versitygw/s3api/middlewares"
)
var (
port string
adminAccess string
adminSecret string
rootUserAccess string
rootUserSecret string
region string
certFile, keyFile string
debug bool
@@ -47,13 +48,25 @@ var (
)
func main() {
setupSignalHandler()
app := initApp()
app.Commands = []*cli.Command{
posixCommand(),
scoutfsCommand(),
adminCommand(),
testCommand(),
}
if err := app.Run(os.Args); err != nil {
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-sigDone
fmt.Fprintf(os.Stderr, "terminating signal caught, shutting down\n")
cancel()
}()
if err := app.RunContext(ctx, os.Args); err != nil {
log.Fatal(err)
}
}
@@ -94,21 +107,24 @@ func initFlags() []cli.Flag {
},
&cli.StringFlag{
Name: "access",
Usage: "admin access account",
Destination: &adminAccess,
EnvVars: []string{"ADMIN_ACCESS_KEY_ID", "ADMIN_ACCESS_KEY"},
Usage: "root user access key",
EnvVars: []string{"ROOT_ACCESS_KEY_ID", "ROOT_ACCESS_KEY"},
Aliases: []string{"a"},
Destination: &rootUserAccess,
},
&cli.StringFlag{
Name: "secret",
Usage: "admin secret access key",
Destination: &adminSecret,
EnvVars: []string{"ADMIN_SECRET_ACCESS_KEY", "ADMIN_SECRET_KEY"},
Usage: "root user secret access key",
EnvVars: []string{"ROOT_SECRET_ACCESS_KEY", "ROOT_SECRET_KEY"},
Aliases: []string{"s"},
Destination: &rootUserSecret,
},
&cli.StringFlag{
Name: "region",
Usage: "s3 region string",
Value: "us-east-1",
Destination: &region,
Aliases: []string{"r"},
},
&cli.StringFlag{
Name: "cert",
@@ -128,10 +144,11 @@ func initFlags() []cli.Flag {
}
}
func runGateway(be backend.Backend) error {
func runGateway(ctx *cli.Context, be backend.Backend, s auth.Storer) error {
app := fiber.New(fiber.Config{
AppName: "versitygw",
ServerHeader: "VERSITYGW",
BodyLimit: 5 * 1024 * 1024 * 1024,
})
var opts []s3api.Option
@@ -155,15 +172,33 @@ func runGateway(be backend.Backend) error {
opts = append(opts, s3api.WithDebug())
}
srv, err := s3api.New(app, be, port,
middlewares.AdminConfig{
AdminAccess: adminAccess,
AdminSecret: adminSecret,
Region: region,
}, auth.IAMServiceUnsupported{}, opts...)
err := s.InitIAM()
if err != nil {
return fmt.Errorf("init iam: %w", err)
}
iam, err := auth.NewInternal(s)
if err != nil {
return fmt.Errorf("setup internal iam service: %w", err)
}
srv, err := s3api.New(app, be, middlewares.RootUserConfig{
Access: rootUserAccess,
Secret: rootUserSecret,
}, port, region, iam, opts...)
if err != nil {
return fmt.Errorf("init gateway: %v", err)
}
return srv.Serve()
c := make(chan error, 1)
go func() { c <- srv.Serve() }()
select {
case <-ctx.Done():
be.Shutdown()
return ctx.Err()
case err := <-c:
be.Shutdown()
return err
}
}

View File

@@ -49,5 +49,5 @@ func runPosix(ctx *cli.Context) error {
return fmt.Errorf("init posix: %v", err)
}
return runGateway(be)
return runGateway(ctx, be, be)
}

73
cmd/versitygw/scoutfs.go Normal file
View File

@@ -0,0 +1,73 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package main
import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/versity/versitygw/backend/scoutfs"
)
var (
glacier bool
)
func scoutfsCommand() *cli.Command {
return &cli.Command{
Name: "scoutfs",
Usage: "scoutfs filesystem storage backend",
Description: `Support for ScoutFS.
The top level directory for the gateway must be provided. All sub directories
of the top level directory are treated as buckets, and all files/directories
below the "bucket directory" are treated as the objects. The object name is
split on "/" separator to translate to posix storage.
For example:
top level: /mnt/fs/gwroot
bucket: mybucket
object: a/b/c/myobject
will be translated into the file /mnt/fs/gwroot/mybucket/a/b/c/myobject
ScoutFS contains optimizations for multipart uploads using extent
move interfaces as well as support for tiered filesystems.`,
Action: runScoutfs,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "glacier",
Usage: "enable glacier emulation mode",
Aliases: []string{"g"},
Destination: &glacier,
},
},
}
}
func runScoutfs(ctx *cli.Context) error {
if ctx.NArg() == 0 {
return fmt.Errorf("no directory provided for operation")
}
var opts []scoutfs.Option
if glacier {
opts = append(opts, scoutfs.WithGlacierEmulation())
}
be, err := scoutfs.New(ctx.Args().Get(0), opts...)
if err != nil {
return fmt.Errorf("init scoutfs: %v", err)
}
return runGateway(ctx, be, be)
}

View File

@@ -12,26 +12,31 @@
// specific language governing permissions and limitations
// under the License.
package auth
package main
import "github.com/versity/versitygw/s3err"
import (
"fmt"
"os"
"os/signal"
"syscall"
)
type IAMConfig struct {
AccessAccounts map[string]string
}
type IAMService interface {
GetIAMConfig() (*IAMConfig, error)
}
type IAMServiceUnsupported struct{}
var _ IAMService = &IAMServiceUnsupported{}
func New() IAMService {
return &IAMServiceUnsupported{}
}
func (IAMServiceUnsupported) GetIAMConfig() (*IAMConfig, error) {
return nil, s3err.GetAPIError(s3err.ErrNotImplemented)
var (
sigDone = make(chan bool, 1)
)
func setupSignalHandler() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
go func() {
for sig := range sigs {
fmt.Fprintf(os.Stderr, "caught signal %v\n", sig)
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
sigDone <- true
case syscall.SIGHUP:
}
}
}()
}

311
cmd/versitygw/test.go Normal file
View File

@@ -0,0 +1,311 @@
package main
import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/versity/versitygw/integration"
)
var (
awsID string
awsSecret string
endpoint string
prefix string
dstBucket string
partSize int64
objSize int64
concurrency int
files int
upload bool
download bool
pathStyle bool
checksumDisable bool
)
func testCommand() *cli.Command {
return &cli.Command{
Name: "test",
Usage: "Client side testing command for the gateway",
Description: `The testing CLI is used to test group of versitygw actions.
It also includes some performance and stress testing`,
Subcommands: initTestCommands(),
Flags: initTestFlags(),
}
}
func initTestFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "access",
Usage: "aws user access key",
EnvVars: []string{"AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY"},
Aliases: []string{"a"},
Destination: &awsID,
},
&cli.StringFlag{
Name: "secret",
Usage: "aws user secret access key",
EnvVars: []string{"AWS_SECRET_ACCESS_KEY", "AWS_SECRET_KEY"},
Aliases: []string{"s"},
Destination: &awsSecret,
},
&cli.StringFlag{
Name: "endpoint",
Usage: "s3 server endpoint",
Destination: &endpoint,
Aliases: []string{"e"},
},
&cli.BoolFlag{
Name: "debug",
Usage: "enable debug mode",
Aliases: []string{"d"},
Destination: &debug,
},
}
}
func initTestCommands() []*cli.Command {
return []*cli.Command{
{
Name: "bucket-actions",
Usage: "Test bucket creation, checking the existence, deletes it.",
Description: `Calls s3 gateway create-bucket action to create a new bucket,
calls head-bucket action to check the existence, then calls delete-bucket action to delete the bucket.`,
Action: getAction(integration.TestMakeBucket),
},
{
Name: "object-actions",
Usage: "Test put/get/delete/copy objects.",
Description: `Creates a bucket with s3 gateway action, puts an object in it,
tries to copy into another bucket, that doesn't exist, creates the destination bucket for copying,
copies the object, get's the object to check the length and content,
get's the copied object to check the length and content, deletes all the objects inside the source bucket,
deletes both the objects and buckets.`,
Action: getAction(integration.TestPutGetObject),
},
{
Name: "put-get-mp-object",
Usage: "Test put & get multipart object.",
Description: `Creates a bucket with s3 gateway action, puts an object in it with multipart upload,
gets the object from the bucket, deletes both the object and bucket.`,
Action: getAction(integration.TestPutGetMPObject),
},
{
Name: "put-dir-object",
Usage: "Test put directory object.",
Description: `Creates a bucket with s3 gateway action, puts a directory object in it,
lists the bucket's objects, deletes both the objects and bucket.`,
Action: getAction(integration.TestPutDirObject),
},
{
Name: "list-objects",
Usage: "Test list-objects action.",
Description: `Creates a bucket with s3 gateway action, puts 2 directory objects in it,
lists the bucket's objects, deletes both the objects and bucket.`,
Action: getAction(integration.TestListObject),
},
{
Name: "abort-mp",
Usage: "Tests abort-multipart-upload action.",
Description: `Creates a bucket with s3 gateway action, creates a multipart upload,
lists the multipart upload, aborts the multipart upload, lists the multipart upload again,
deletes both the objects and bucket.`,
Action: getAction(integration.TestListAbortMultiPartObject),
},
{
Name: "list-parts",
Usage: "Tests list-parts action.",
Description: `Creates a bucket with s3 gateway action, creates a multipart upload,
lists the upload parts, deletes both the objects and bucket.`,
Action: getAction(integration.TestListMultiParts),
},
{
Name: "incorrect-mp",
Usage: "Tests incorrect multipart case.",
Description: `Creates a bucket with s3 gateway action, creates a multipart upload,
uploads different parts, completes the multipart upload with incorrect part numbers,
calls the head-object action, compares the content length, removes both the object and bucket`,
Action: getAction(integration.TestIncorrectMultiParts),
},
{
Name: "incomplete-mp",
Usage: "Tests incomplete multi parts.",
Description: `Creates a bucket with s3 gateway action, creates a multipart upload,
upload a part, lists the parts, checks if the uploaded part is in the list,
removes both the object and the bucket`,
Action: getAction(integration.TestIncompleteMultiParts),
},
{
Name: "incomplete-put-object",
Usage: "Tests incomplete put objects case.",
Description: `Creates a bucket with s3 gateway action, puts an object in it,
gets the object with head-object action, expects the object to be got,
removes both the object and bucket`,
Action: getAction(integration.TestIncompletePutObject),
},
{
Name: "get-range",
Usage: "Tests get object by range.",
Description: `Creates a bucket with s3 gateway action, puts an object in it,
gets the object by specifying the object range, compares the range with the original one,
removes both the object and the bucket`,
Action: getAction(integration.TestRangeGet),
},
{
Name: "invalid-mp",
Usage: "Tests invalid multi part case.",
Description: `Creates a bucket with s3 gateway action, creates a multi part upload,
uploads an invalid part, gets the object with head-object action, expects to get error,
removes both the object and bucket`,
Action: getAction(integration.TestInvalidMultiParts),
},
{
Name: "object-tag-actions",
Usage: "Tests get/put/delete object tag actions.",
Description: `Creates a bucket with s3 gateway action, puts an object in it,
puts some tags for the object, gets the tags, compares the results, removes the tags,
gets the tags again, checks it to be empty, then removes both the object and bucket`,
Action: getAction(integration.TestPutGetRemoveTags),
},
{
Name: "bucket-acl-actions",
Usage: "Tests put/get bucket actions.",
Description: `Creates a bucket with s3 gateway action, puts some bucket acls
gets the acl, verifies it, then removes the bucket`,
Action: getAction(integration.TestAclActions),
},
{
Name: "full-flow",
Usage: "Tests the full flow of gateway.",
Description: `Runs all the available tests to test the full flow of the gateway.`,
Action: getAction(integration.TestFullFlow),
},
{
Name: "bench",
Usage: "Runs download/upload performance test on the gateway",
Description: `Uploads/downloads some number(specified by flags) of files with some capacity(bytes).
Logs the results to the console`,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "files",
Usage: "Number of objects to read/write",
Value: 1,
Destination: &files,
},
&cli.Int64Flag{
Name: "objsize",
Usage: "Uploading object size",
Value: 0,
Destination: &objSize,
},
&cli.StringFlag{
Name: "prefix",
Usage: "Object name prefix",
Destination: &prefix,
},
&cli.BoolFlag{
Name: "upload",
Usage: "Upload data to the gateway",
Value: false,
Destination: &upload,
},
&cli.BoolFlag{
Name: "download",
Usage: "Download data to the gateway",
Value: false,
Destination: &download,
},
&cli.StringFlag{
Name: "bucket",
Usage: "Destination bucket name to read/write data",
Destination: &dstBucket,
},
&cli.Int64Flag{
Name: "partSize",
Usage: "Upload/download size per thread",
Value: 64 * 1024 * 1024,
Destination: &partSize,
},
&cli.IntFlag{
Name: "concurrency",
Usage: "Upload/download threads per object",
Value: 1,
Destination: &concurrency,
},
&cli.BoolFlag{
Name: "pathStyle",
Usage: "Use Pathstyle bucket addressing",
Value: false,
Destination: &pathStyle,
},
&cli.BoolFlag{
Name: "checksumDis",
Usage: "Disable server checksum",
Value: false,
Destination: &checksumDisable,
},
},
Action: func(ctx *cli.Context) error {
if upload && download {
return fmt.Errorf("must only specify one of upload or download")
}
if !upload && !download {
return fmt.Errorf("must specify one of upload or download")
}
if dstBucket == "" {
return fmt.Errorf("must specify bucket")
}
opts := []integration.Option{
integration.WithAccess(awsID),
integration.WithSecret(awsSecret),
integration.WithRegion(region),
integration.WithEndpoint(endpoint),
integration.WithConcurrency(concurrency),
integration.WithPartSize(partSize),
}
if debug {
opts = append(opts, integration.WithDebug())
}
if pathStyle {
opts = append(opts, integration.WithPathStyle())
}
if checksumDisable {
opts = append(opts, integration.WithDisableChecksum())
}
s3conf := integration.NewS3Conf(opts...)
return integration.TestPerformance(s3conf, upload, download, files, objSize, dstBucket, prefix)
},
},
}
}
type testFunc func(*integration.S3Conf)
func getAction(tf testFunc) func(*cli.Context) error {
return func(ctx *cli.Context) error {
opts := []integration.Option{
integration.WithAccess(awsID),
integration.WithSecret(awsSecret),
integration.WithRegion(region),
integration.WithEndpoint(endpoint),
}
if debug {
opts = append(opts, integration.WithDebug())
}
s := integration.NewS3Conf(opts...)
tf(s)
fmt.Println()
fmt.Println("RAN:", integration.RunCount, "PASS:", integration.PassCount, "FAIL:", integration.FailCount)
if integration.FailCount > 0 {
return fmt.Errorf("test failed with %v errors", integration.FailCount)
}
return nil
}
}

35
go.mod
View File

@@ -3,29 +3,42 @@ module github.com/versity/versitygw
go 1.20
require (
github.com/aws/aws-sdk-go-v2 v1.18.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
github.com/aws/aws-sdk-go-v2 v1.18.1
github.com/aws/aws-sdk-go-v2/service/s3 v1.35.0
github.com/aws/smithy-go v1.13.5
github.com/gofiber/fiber/v2 v2.46.0
github.com/google/uuid v1.3.0
github.com/pkg/xattr v0.4.9
github.com/urfave/cli/v2 v2.25.4
github.com/urfave/cli/v2 v2.25.6
github.com/valyala/fasthttp v1.47.0
golang.org/x/sys v0.8.0
github.com/versity/scoutfs-go v0.0.0-20230606232754-0474b14343b9
golang.org/x/sys v0.9.0
)
require (
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.27
github.com/aws/aws-sdk-go-v2/credentials v1.13.26
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.70
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/compress v1.16.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect

65
go.sum
View File

@@ -1,25 +1,43 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g=
github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA=
github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw=
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk=
github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.70 h1:4bh28MeeXoBFTjb0JjQ5sVatzlf5xA1DziV8mZed9v4=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.70/go.mod h1:9yI5NXzqy2yOiMytv6QLZHvlyHLwYxO9iIq+bZIbrFg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 h1:wscW+pnn3J1OYnanMnza5ZVYXLX4cKk5rAvUAl4Qu+c=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26/go.mod h1:MtYiox5gvyB+OyP0Mr0Sm/yzbEAIPL9eijj/ouHAPw0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 h1:O+9nAy9Bb6bJFTpeNFtd9UfHbgxO1o4ZDAM9rQp5NsY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 h1:zZSLP3v3riMOP14H7b4XP0uyfREDQOYv2cqIrvTXDNQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29/go.mod h1:z7EjRjVwZ6pWcWdI2H64dKttvzaP99jRIj5hphW0M5U=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 h1:dBL3StFxHtpBzJJ/mNEsjXVgfO+7jR0dAIEwLqMapEA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3/go.mod h1:f1QyiAsvIv4B49DmCqrhlXqyaR+0IxMmyX+1P+AnzOM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.34.1 h1:rYYwwsGqbwvGgQHjBkqgDt8MynXk+I8xgS0IEj5gOT0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.34.1/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.35.0 h1:ya7fmrN2fE7s1P2gaPbNg5MTkERVWfsH8ToP1YC4Z9o=
github.com/aws/aws-sdk-go-v2/service/s3 v1.35.0/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
@@ -31,10 +49,11 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -62,14 +81,16 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/urfave/cli/v2 v2.25.4 h1:HyYwPrTO3im9rYhUff/ZNs78eolxt0nJ4LN+9yJKSH4=
github.com/urfave/cli/v2 v2.25.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/urfave/cli/v2 v2.25.6 h1:yuSkgDSZfH3L1CjF2/5fNNg2KbM47pY2EvjBq4ESQnU=
github.com/urfave/cli/v2 v2.25.6/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/versity/scoutfs-go v0.0.0-20230606232754-0474b14343b9 h1:ZfmQR01Kk6/kQh6+zlqfBYszVY02fzf9xYrchOY4NFM=
github.com/versity/scoutfs-go v0.0.0-20230606232754-0474b14343b9/go.mod h1:gJsq73k+4685y+rbDIpPY8i/5GbsiwP6JFoFyUDB1fQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -102,8 +123,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.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 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=

84
integration/data-io.go Normal file
View File

@@ -0,0 +1,84 @@
package integration
import (
"crypto/rand"
"crypto/sha256"
"hash"
"io"
)
type RReader struct {
buf []byte
dataleft int
hash hash.Hash
}
func NewDataReader(totalsize, bufsize int) *RReader {
b := make([]byte, bufsize)
rand.Read(b)
return &RReader{
buf: b,
dataleft: totalsize,
hash: sha256.New(),
}
}
func (r *RReader) Read(p []byte) (int, error) {
n := min(len(p), len(r.buf), r.dataleft)
r.dataleft -= n
err := error(nil)
if n == 0 {
err = io.EOF
}
r.hash.Write(r.buf[:n])
return copy(p, r.buf[:n]), err
}
func (r *RReader) Sum() []byte {
return r.hash.Sum(nil)
}
type ZReader struct {
buf []byte
dataleft int
}
func NewZeroReader(totalsize, bufsize int) *ZReader {
b := make([]byte, bufsize)
return &ZReader{buf: b, dataleft: totalsize}
}
func (r *ZReader) Read(p []byte) (int, error) {
n := min(len(p), len(r.buf), r.dataleft)
r.dataleft -= n
err := error(nil)
if n == 0 {
err = io.EOF
}
return copy(p, r.buf[:n]), err
}
func min(values ...int) int {
if len(values) == 0 {
return 0
}
min := values[0]
for _, v := range values {
if v < min {
min = v
}
}
return min
}
type NW struct{}
func NewNullWriter() NW {
return NW{}
}
func (NW) WriteAt(p []byte, off int64) (n int, err error) {
return len(p), nil
}

31
integration/output.go Normal file
View File

@@ -0,0 +1,31 @@
package integration
import "fmt"
var (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorCyan = "\033[36m"
)
var (
RunCount = 0
PassCount = 0
FailCount = 0
)
func runF(format string, a ...interface{}) {
RunCount++
fmt.Printf(colorCyan+"RUN "+colorReset+format+"\n", a...)
}
func failF(format string, a ...interface{}) {
FailCount++
fmt.Printf(colorRed+"FAIL "+colorReset+format+"\n", a...)
}
func passF(format string, a ...interface{}) {
PassCount++
fmt.Printf(colorGreen+"PASS "+colorReset+format+"\n", a...)
}

153
integration/s3conf.go Normal file
View File

@@ -0,0 +1,153 @@
package integration
import (
"context"
"io"
"log"
"net/http"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/smithy-go/middleware"
)
type S3Conf struct {
awsID string
awsSecret string
awsRegion string
endpoint string
checksumDisable bool
pathStyle bool
PartSize int64
Concurrency int
debug bool
}
func NewS3Conf(opts ...Option) *S3Conf {
s := &S3Conf{}
for _, opt := range opts {
opt(s)
}
return s
}
type Option func(*S3Conf)
func WithAccess(ak string) Option {
return func(s *S3Conf) { s.awsID = ak }
}
func WithSecret(sk string) Option {
return func(s *S3Conf) { s.awsSecret = sk }
}
func WithRegion(r string) Option {
return func(s *S3Conf) { s.awsRegion = r }
}
func WithEndpoint(e string) Option {
return func(s *S3Conf) { s.endpoint = e }
}
func WithDisableChecksum() Option {
return func(s *S3Conf) { s.checksumDisable = true }
}
func WithPathStyle() Option {
return func(s *S3Conf) { s.pathStyle = true }
}
func WithPartSize(p int64) Option {
return func(s *S3Conf) { s.PartSize = p }
}
func WithConcurrency(c int) Option {
return func(s *S3Conf) { s.Concurrency = c }
}
func WithDebug() Option {
return func(s *S3Conf) { s.debug = true }
}
func (c *S3Conf) getCreds() credentials.StaticCredentialsProvider {
// TODO support token/IAM
if c.awsSecret == "" {
c.awsSecret = os.Getenv("AWS_SECRET_ACCESS_KEY")
}
if c.awsSecret == "" {
log.Fatal("no AWS_SECRET_ACCESS_KEY found")
}
return credentials.NewStaticCredentialsProvider(c.awsID, c.awsSecret, "")
}
func (c *S3Conf) ResolveEndpoint(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
PartitionID: "aws",
URL: c.endpoint,
SigningRegion: c.awsRegion,
HostnameImmutable: true,
}, nil
}
func (c *S3Conf) Config() aws.Config {
creds := c.getCreds()
tr := &http.Transport{}
client := &http.Client{Transport: tr}
opts := []func(*config.LoadOptions) error{
config.WithRegion(c.awsRegion),
config.WithCredentialsProvider(creds),
config.WithHTTPClient(client),
}
if c.endpoint != "" && c.endpoint != "aws" {
opts = append(opts,
config.WithEndpointResolverWithOptions(c))
}
if c.checksumDisable {
opts = append(opts,
config.WithAPIOptions([]func(*middleware.Stack) error{v4.SwapComputePayloadSHA256ForUnsignedPayloadMiddleware}))
}
if c.debug {
opts = append(opts,
config.WithClientLogMode(aws.LogSigning|aws.LogRetries|aws.LogRequest|aws.LogResponse|aws.LogRequestEventMessage|aws.LogResponseEventMessage))
}
cfg, err := config.LoadDefaultConfig(
context.TODO(), opts...)
if err != nil {
log.Fatalln("error:", err)
}
return cfg
}
func (c *S3Conf) UploadData(r io.Reader, bucket, object string) error {
uploader := manager.NewUploader(s3.NewFromConfig(c.Config()))
uploader.PartSize = c.PartSize
uploader.Concurrency = c.Concurrency
upinfo := &s3.PutObjectInput{
Body: r,
Bucket: &bucket,
Key: &object,
}
_, err := uploader.Upload(context.Background(), upinfo)
return err
}
func (c *S3Conf) DownloadData(w io.WriterAt, bucket, object string) (int64, error) {
downloader := manager.NewDownloader(s3.NewFromConfig(c.Config()))
downloader.PartSize = c.PartSize
downloader.Concurrency = c.Concurrency
downinfo := &s3.GetObjectInput{
Bucket: &bucket,
Key: &object,
}
return downloader.Download(context.Background(), w, downinfo)
}

1469
integration/tests.go Normal file

File diff suppressed because it is too large Load Diff

156
integration/utils.go Normal file
View File

@@ -0,0 +1,156 @@
package integration
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
func setup(s *S3Conf, bucket string) error {
s3client := s3.NewFromConfig(s.Config())
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
})
cancel()
return err
}
func teardown(s *S3Conf, bucket string) error {
s3client := s3.NewFromConfig(s.Config())
deleteObject := func(bucket, key, versionId *string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: bucket,
Key: key,
VersionId: versionId,
})
cancel()
if err != nil {
return fmt.Errorf("failed to delete object %v: %v", *key, err)
}
return nil
}
in := &s3.ListObjectsV2Input{Bucket: &bucket}
for {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListObjectsV2(ctx, in)
cancel()
if err != nil {
return fmt.Errorf("failed to list objects: %v", err)
}
for _, item := range out.Contents {
err = deleteObject(&bucket, item.Key, nil)
if err != nil {
return err
}
}
if out.IsTruncated {
in.ContinuationToken = out.ContinuationToken
} else {
break
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{
Bucket: &bucket,
})
cancel()
return err
}
func isEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i, d := range a {
if d != b[i] {
return false
}
}
return true
}
func contains(name string, list []types.Object) bool {
for _, item := range list {
fmt.Println(*item.Key)
if strings.EqualFold(name, *item.Key) {
return true
}
}
return false
}
func containsUID(name, id string, list []types.MultipartUpload) bool {
for _, item := range list {
if strings.EqualFold(name, *item.Key) && strings.EqualFold(id, *item.UploadId) {
return true
}
}
return false
}
func containsPart(part int32, list []types.Part) bool {
for _, item := range list {
if item.PartNumber == part {
return true
}
}
return false
}
func areTagsSame(tags1, tags2 []types.Tag) bool {
if len(tags1) != len(tags2) {
return false
}
for _, tag := range tags1 {
if !containsTag(tag, tags2) {
return false
}
}
return true
}
func containsTag(tag types.Tag, list []types.Tag) bool {
for _, item := range list {
if *item.Key == *tag.Key && *item.Value == *tag.Value {
return true
}
}
return false
}
func checkGrants(grts1, grts2 []types.Grant) bool {
if len(grts1) != len(grts2) {
return false
}
for i, grt := range grts1 {
if grt.Permission != grts2[i].Permission {
return false
}
if *grt.Grantee.ID != *grts2[i].Grantee.ID {
return false
}
}
return true
}
func execCommand(args ...string) ([]byte, error) {
cmd := exec.Command("./versitygw", args...)
return cmd.CombinedOutput()
}

37
runtests.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# make temp dirs
mkdir /tmp/gw
rm -rf /tmp/covdata
mkdir /tmp/covdata
# run server in background
GOCOVERDIR=/tmp/covdata ./versitygw -a user -s pass posix /tmp/gw &
GW_PID=$!
# wait a second for server to start up
sleep 1
# check if server is still running
if ! kill -0 $GW_PID; then
echo "server no longer running"
exit 1
fi
# run tests
if ! ./versitygw test -a user -s pass -e http://127.0.0.1:7070 full-flow; then
echo "tests failed"
kill $GW_PID
exit 1
fi
# kill off server
kill $GW_PID
exit 0
# if the above binary was built with -cover enabled (make testbin),
# then the following can be used for code coverage reports:
# go tool covdata percent -i=/tmp/covdata
# go tool covdata textfmt -i=/tmp/covdata -o profile.txt
# go tool cover -html=profile.txt

View File

@@ -0,0 +1,62 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package controllers
import (
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
)
type AdminController struct {
IAMService auth.IAMService
}
func (c AdminController) CreateUser(ctx *fiber.Ctx) error {
access, secret, role := ctx.Query("access"), ctx.Query("secret"), ctx.Query("role")
requesterRole := ctx.Locals("role").(string)
if requesterRole != "admin" {
return fmt.Errorf("access denied: only admin users have access to this resource")
}
if role != "user" && role != "admin" {
return fmt.Errorf("invalid parameters: user role have to be one of the following: 'user', 'admin'")
}
user := auth.Account{Secret: secret, Role: role}
err := c.IAMService.CreateAccount(access, user)
if err != nil {
return fmt.Errorf("failed to create a user: %w", err)
}
return ctx.SendString("The user has been created successfully")
}
func (c AdminController) DeleteUser(ctx *fiber.Ctx) error {
access := ctx.Query("access")
requesterRole := ctx.Locals("role").(string)
if requesterRole != "admin" {
return fmt.Errorf("access denied: only admin users have access to this resource")
}
err := c.IAMService.DeleteUserAccount(access)
if err != nil {
return err
}
return ctx.SendString("The user has been deleted successfully")
}

View File

@@ -0,0 +1,173 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package controllers
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
)
func TestAdminController_CreateUser(t *testing.T) {
type args struct {
req *http.Request
}
adminController := AdminController{
IAMService: &IAMServiceMock{
CreateAccountFunc: func(access string, account auth.Account) error {
return nil
},
},
}
app := fiber.New()
app.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
app.Patch("/create-user", adminController.CreateUser)
appErr := fiber.New()
appErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "user")
return ctx.Next()
})
appErr.Patch("/create-user", adminController.CreateUser)
tests := []struct {
name string
app *fiber.App
args args
wantErr bool
statusCode int
}{
{
name: "Admin-create-user-success",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/create-user?access=test&secret=test&role=user", nil),
},
wantErr: false,
statusCode: 200,
},
{
name: "Admin-create-user-invalid-user-role",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/create-user?access=test&secret=test&role=invalid", nil),
},
wantErr: false,
statusCode: 500,
},
{
name: "Admin-create-user-invalid-requester-role",
app: appErr,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/create-user?access=test&secret=test&role=admin", nil),
},
wantErr: false,
statusCode: 500,
},
}
for _, tt := range tests {
resp, err := tt.app.Test(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("AdminController.CreateUser() error = %v, wantErr %v", err, tt.wantErr)
}
if resp.StatusCode != tt.statusCode {
t.Errorf("AdminController.CreateUser() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
}
}
}
func TestAdminController_DeleteUser(t *testing.T) {
type args struct {
req *http.Request
}
adminController := AdminController{
IAMService: &IAMServiceMock{
DeleteUserAccountFunc: func(access string) error {
return nil
},
},
}
app := fiber.New()
app.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "admin")
return ctx.Next()
})
app.Patch("/delete-user", adminController.DeleteUser)
appErr := fiber.New()
appErr.Use(func(ctx *fiber.Ctx) error {
ctx.Locals("role", "user")
return ctx.Next()
})
appErr.Patch("/delete-user", adminController.DeleteUser)
tests := []struct {
name string
app *fiber.App
args args
wantErr bool
statusCode int
}{
{
name: "Admin-delete-user-success",
app: app,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/delete-user?access=test", nil),
},
wantErr: false,
statusCode: 200,
},
{
name: "Admin-delete-user-invalid-requester-role",
app: appErr,
args: args{
req: httptest.NewRequest(http.MethodPatch, "/delete-user?access=test", nil),
},
wantErr: false,
statusCode: 500,
},
}
for _, tt := range tests {
resp, err := tt.app.Test(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("AdminController.DeleteUser() error = %v, wantErr %v", err, tt.wantErr)
}
if resp.StatusCode != tt.statusCode {
t.Errorf("AdminController.DeleteUser() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
}
}
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3response"
"io"
"sync"
)
@@ -27,12 +28,9 @@ var _ backend.Backend = &BackendMock{}
// CompleteMultipartUploadFunc: func(bucket string, object string, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) {
// panic("mock out the CompleteMultipartUpload method")
// },
// CopyObjectFunc: func(srcBucket string, srcObject string, DstBucket string, dstObject string) (*s3.CopyObjectOutput, error) {
// CopyObjectFunc: func(srcBucket string, srcObject string, dstBucket string, dstObject string) (*s3.CopyObjectOutput, error) {
// panic("mock out the CopyObject method")
// },
// CopyPartFunc: func(srcBucket string, srcObject string, DstBucket string, uploadID string, rangeHeader string, part int) (*types.CopyPartResult, error) {
// panic("mock out the CopyPart method")
// },
// CreateMultipartUploadFunc: func(createMultipartUploadInput *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
// panic("mock out the CreateMultipartUpload method")
// },
@@ -45,7 +43,7 @@ var _ backend.Backend = &BackendMock{}
// DeleteObjectsFunc: func(bucket string, objects *s3.DeleteObjectsInput) error {
// panic("mock out the DeleteObjects method")
// },
// GetBucketAclFunc: func(bucket string) (*s3.GetBucketAclOutput, error) {
// GetBucketAclFunc: func(bucket string) ([]byte, error) {
// panic("mock out the GetBucketAcl method")
// },
// GetObjectFunc: func(bucket string, object string, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error) {
@@ -66,13 +64,13 @@ var _ backend.Backend = &BackendMock{}
// HeadObjectFunc: func(bucket string, object string) (*s3.HeadObjectOutput, error) {
// panic("mock out the HeadObject method")
// },
// ListBucketsFunc: func() (*s3.ListBucketsOutput, error) {
// ListBucketsFunc: func() (s3response.ListAllMyBucketsResult, error) {
// panic("mock out the ListBuckets method")
// },
// ListMultipartUploadsFunc: func(output *s3.ListMultipartUploadsInput) (*s3.ListMultipartUploadsOutput, error) {
// ListMultipartUploadsFunc: func(listMultipartUploadsInput *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
// panic("mock out the ListMultipartUploads method")
// },
// ListObjectPartsFunc: func(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (*s3.ListPartsOutput, error) {
// ListObjectPartsFunc: func(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) {
// panic("mock out the ListObjectParts method")
// },
// ListObjectsFunc: func(bucket string, prefix string, marker string, delim string, maxkeys int) (*s3.ListObjectsOutput, error) {
@@ -81,10 +79,10 @@ var _ backend.Backend = &BackendMock{}
// ListObjectsV2Func: func(bucket string, prefix string, marker string, delim string, maxkeys int) (*s3.ListObjectsV2Output, error) {
// panic("mock out the ListObjectsV2 method")
// },
// PutBucketFunc: func(bucket string) error {
// PutBucketFunc: func(bucket string, owner string) error {
// panic("mock out the PutBucket method")
// },
// PutBucketAclFunc: func(putBucketAclInput *s3.PutBucketAclInput) error {
// PutBucketAclFunc: func(bucket string, data []byte) error {
// panic("mock out the PutBucketAcl method")
// },
// PutObjectFunc: func(putObjectInput *s3.PutObjectInput) (string, error) {
@@ -111,10 +109,7 @@ var _ backend.Backend = &BackendMock{}
// StringFunc: func() string {
// panic("mock out the String method")
// },
// UploadPartFunc: func(bucket string, object string, uploadId string, Body io.ReadSeeker) (*s3.UploadPartOutput, error) {
// panic("mock out the UploadPart method")
// },
// UploadPartCopyFunc: func(uploadPartCopyInput *s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
// UploadPartCopyFunc: func(uploadPartCopyInput *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
// panic("mock out the UploadPartCopy method")
// },
// }
@@ -131,10 +126,7 @@ type BackendMock struct {
CompleteMultipartUploadFunc func(bucket string, object string, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error)
// CopyObjectFunc mocks the CopyObject method.
CopyObjectFunc func(srcBucket string, srcObject string, DstBucket string, dstObject string) (*s3.CopyObjectOutput, error)
// CopyPartFunc mocks the CopyPart method.
CopyPartFunc func(srcBucket string, srcObject string, DstBucket string, uploadID string, rangeHeader string, part int) (*types.CopyPartResult, error)
CopyObjectFunc func(srcBucket string, srcObject string, dstBucket string, dstObject string) (*s3.CopyObjectOutput, error)
// CreateMultipartUploadFunc mocks the CreateMultipartUpload method.
CreateMultipartUploadFunc func(createMultipartUploadInput *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error)
@@ -149,7 +141,7 @@ type BackendMock struct {
DeleteObjectsFunc func(bucket string, objects *s3.DeleteObjectsInput) error
// GetBucketAclFunc mocks the GetBucketAcl method.
GetBucketAclFunc func(bucket string) (*s3.GetBucketAclOutput, error)
GetBucketAclFunc func(bucket string) ([]byte, error)
// GetObjectFunc mocks the GetObject method.
GetObjectFunc func(bucket string, object string, acceptRange string, writer io.Writer) (*s3.GetObjectOutput, error)
@@ -170,13 +162,13 @@ type BackendMock struct {
HeadObjectFunc func(bucket string, object string) (*s3.HeadObjectOutput, error)
// ListBucketsFunc mocks the ListBuckets method.
ListBucketsFunc func() (*s3.ListBucketsOutput, error)
ListBucketsFunc func() (s3response.ListAllMyBucketsResult, error)
// ListMultipartUploadsFunc mocks the ListMultipartUploads method.
ListMultipartUploadsFunc func(output *s3.ListMultipartUploadsInput) (*s3.ListMultipartUploadsOutput, error)
ListMultipartUploadsFunc func(listMultipartUploadsInput *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error)
// ListObjectPartsFunc mocks the ListObjectParts method.
ListObjectPartsFunc func(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (*s3.ListPartsOutput, error)
ListObjectPartsFunc func(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error)
// ListObjectsFunc mocks the ListObjects method.
ListObjectsFunc func(bucket string, prefix string, marker string, delim string, maxkeys int) (*s3.ListObjectsOutput, error)
@@ -185,10 +177,10 @@ type BackendMock struct {
ListObjectsV2Func func(bucket string, prefix string, marker string, delim string, maxkeys int) (*s3.ListObjectsV2Output, error)
// PutBucketFunc mocks the PutBucket method.
PutBucketFunc func(bucket string) error
PutBucketFunc func(bucket string, owner string) error
// PutBucketAclFunc mocks the PutBucketAcl method.
PutBucketAclFunc func(putBucketAclInput *s3.PutBucketAclInput) error
PutBucketAclFunc func(bucket string, data []byte) error
// PutObjectFunc mocks the PutObject method.
PutObjectFunc func(putObjectInput *s3.PutObjectInput) (string, error)
@@ -214,11 +206,8 @@ type BackendMock struct {
// StringFunc mocks the String method.
StringFunc func() string
// UploadPartFunc mocks the UploadPart method.
UploadPartFunc func(bucket string, object string, uploadId string, Body io.ReadSeeker) (*s3.UploadPartOutput, error)
// UploadPartCopyFunc mocks the UploadPartCopy method.
UploadPartCopyFunc func(uploadPartCopyInput *s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error)
UploadPartCopyFunc func(uploadPartCopyInput *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error)
// calls tracks calls to the methods.
calls struct {
@@ -244,26 +233,11 @@ type BackendMock struct {
SrcBucket string
// SrcObject is the srcObject argument value.
SrcObject string
// DstBucket is the DstBucket argument value.
// DstBucket is the dstBucket argument value.
DstBucket string
// DstObject is the dstObject argument value.
DstObject string
}
// CopyPart holds details about calls to the CopyPart method.
CopyPart []struct {
// SrcBucket is the srcBucket argument value.
SrcBucket string
// SrcObject is the srcObject argument value.
SrcObject string
// DstBucket is the DstBucket argument value.
DstBucket string
// UploadID is the uploadID argument value.
UploadID string
// RangeHeader is the rangeHeader argument value.
RangeHeader string
// Part is the part argument value.
Part int
}
// CreateMultipartUpload holds details about calls to the CreateMultipartUpload method.
CreateMultipartUpload []struct {
// CreateMultipartUploadInput is the createMultipartUploadInput argument value.
@@ -344,8 +318,8 @@ type BackendMock struct {
}
// ListMultipartUploads holds details about calls to the ListMultipartUploads method.
ListMultipartUploads []struct {
// Output is the output argument value.
Output *s3.ListMultipartUploadsInput
// ListMultipartUploadsInput is the listMultipartUploadsInput argument value.
ListMultipartUploadsInput *s3.ListMultipartUploadsInput
}
// ListObjectParts holds details about calls to the ListObjectParts method.
ListObjectParts []struct {
@@ -390,11 +364,15 @@ type BackendMock struct {
PutBucket []struct {
// Bucket is the bucket argument value.
Bucket string
// Owner is the owner argument value.
Owner string
}
// PutBucketAcl holds details about calls to the PutBucketAcl method.
PutBucketAcl []struct {
// PutBucketAclInput is the putBucketAclInput argument value.
PutBucketAclInput *s3.PutBucketAclInput
// Bucket is the bucket argument value.
Bucket string
// Data is the data argument value.
Data []byte
}
// PutObject holds details about calls to the PutObject method.
PutObject []struct {
@@ -452,17 +430,6 @@ type BackendMock struct {
// String holds details about calls to the String method.
String []struct {
}
// UploadPart holds details about calls to the UploadPart method.
UploadPart []struct {
// Bucket is the bucket argument value.
Bucket string
// Object is the object argument value.
Object string
// UploadId is the uploadId argument value.
UploadId string
// Body is the Body argument value.
Body io.ReadSeeker
}
// UploadPartCopy holds details about calls to the UploadPartCopy method.
UploadPartCopy []struct {
// UploadPartCopyInput is the uploadPartCopyInput argument value.
@@ -472,7 +439,6 @@ type BackendMock struct {
lockAbortMultipartUpload sync.RWMutex
lockCompleteMultipartUpload sync.RWMutex
lockCopyObject sync.RWMutex
lockCopyPart sync.RWMutex
lockCreateMultipartUpload sync.RWMutex
lockDeleteBucket sync.RWMutex
lockDeleteObject sync.RWMutex
@@ -499,7 +465,6 @@ type BackendMock struct {
lockSetTags sync.RWMutex
lockShutdown sync.RWMutex
lockString sync.RWMutex
lockUploadPart sync.RWMutex
lockUploadPartCopy sync.RWMutex
}
@@ -580,7 +545,7 @@ func (mock *BackendMock) CompleteMultipartUploadCalls() []struct {
}
// CopyObject calls CopyObjectFunc.
func (mock *BackendMock) CopyObject(srcBucket string, srcObject string, DstBucket string, dstObject string) (*s3.CopyObjectOutput, error) {
func (mock *BackendMock) CopyObject(srcBucket string, srcObject string, dstBucket string, dstObject string) (*s3.CopyObjectOutput, error) {
if mock.CopyObjectFunc == nil {
panic("BackendMock.CopyObjectFunc: method is nil but Backend.CopyObject was just called")
}
@@ -592,13 +557,13 @@ func (mock *BackendMock) CopyObject(srcBucket string, srcObject string, DstBucke
}{
SrcBucket: srcBucket,
SrcObject: srcObject,
DstBucket: DstBucket,
DstBucket: dstBucket,
DstObject: dstObject,
}
mock.lockCopyObject.Lock()
mock.calls.CopyObject = append(mock.calls.CopyObject, callInfo)
mock.lockCopyObject.Unlock()
return mock.CopyObjectFunc(srcBucket, srcObject, DstBucket, dstObject)
return mock.CopyObjectFunc(srcBucket, srcObject, dstBucket, dstObject)
}
// CopyObjectCalls gets all the calls that were made to CopyObject.
@@ -623,58 +588,6 @@ func (mock *BackendMock) CopyObjectCalls() []struct {
return calls
}
// CopyPart calls CopyPartFunc.
func (mock *BackendMock) CopyPart(srcBucket string, srcObject string, DstBucket string, uploadID string, rangeHeader string, part int) (*types.CopyPartResult, error) {
if mock.CopyPartFunc == nil {
panic("BackendMock.CopyPartFunc: method is nil but Backend.CopyPart was just called")
}
callInfo := struct {
SrcBucket string
SrcObject string
DstBucket string
UploadID string
RangeHeader string
Part int
}{
SrcBucket: srcBucket,
SrcObject: srcObject,
DstBucket: DstBucket,
UploadID: uploadID,
RangeHeader: rangeHeader,
Part: part,
}
mock.lockCopyPart.Lock()
mock.calls.CopyPart = append(mock.calls.CopyPart, callInfo)
mock.lockCopyPart.Unlock()
return mock.CopyPartFunc(srcBucket, srcObject, DstBucket, uploadID, rangeHeader, part)
}
// CopyPartCalls gets all the calls that were made to CopyPart.
// Check the length with:
//
// len(mockedBackend.CopyPartCalls())
func (mock *BackendMock) CopyPartCalls() []struct {
SrcBucket string
SrcObject string
DstBucket string
UploadID string
RangeHeader string
Part int
} {
var calls []struct {
SrcBucket string
SrcObject string
DstBucket string
UploadID string
RangeHeader string
Part int
}
mock.lockCopyPart.RLock()
calls = mock.calls.CopyPart
mock.lockCopyPart.RUnlock()
return calls
}
// CreateMultipartUpload calls CreateMultipartUploadFunc.
func (mock *BackendMock) CreateMultipartUpload(createMultipartUploadInput *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
if mock.CreateMultipartUploadFunc == nil {
@@ -812,7 +725,7 @@ func (mock *BackendMock) DeleteObjectsCalls() []struct {
}
// GetBucketAcl calls GetBucketAclFunc.
func (mock *BackendMock) GetBucketAcl(bucket string) (*s3.GetBucketAclOutput, error) {
func (mock *BackendMock) GetBucketAcl(bucket string) ([]byte, error) {
if mock.GetBucketAclFunc == nil {
panic("BackendMock.GetBucketAclFunc: method is nil but Backend.GetBucketAcl was just called")
}
@@ -1068,7 +981,7 @@ func (mock *BackendMock) HeadObjectCalls() []struct {
}
// ListBuckets calls ListBucketsFunc.
func (mock *BackendMock) ListBuckets() (*s3.ListBucketsOutput, error) {
func (mock *BackendMock) ListBuckets() (s3response.ListAllMyBucketsResult, error) {
if mock.ListBucketsFunc == nil {
panic("BackendMock.ListBucketsFunc: method is nil but Backend.ListBuckets was just called")
}
@@ -1095,19 +1008,19 @@ func (mock *BackendMock) ListBucketsCalls() []struct {
}
// ListMultipartUploads calls ListMultipartUploadsFunc.
func (mock *BackendMock) ListMultipartUploads(output *s3.ListMultipartUploadsInput) (*s3.ListMultipartUploadsOutput, error) {
func (mock *BackendMock) ListMultipartUploads(listMultipartUploadsInput *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResponse, error) {
if mock.ListMultipartUploadsFunc == nil {
panic("BackendMock.ListMultipartUploadsFunc: method is nil but Backend.ListMultipartUploads was just called")
}
callInfo := struct {
Output *s3.ListMultipartUploadsInput
ListMultipartUploadsInput *s3.ListMultipartUploadsInput
}{
Output: output,
ListMultipartUploadsInput: listMultipartUploadsInput,
}
mock.lockListMultipartUploads.Lock()
mock.calls.ListMultipartUploads = append(mock.calls.ListMultipartUploads, callInfo)
mock.lockListMultipartUploads.Unlock()
return mock.ListMultipartUploadsFunc(output)
return mock.ListMultipartUploadsFunc(listMultipartUploadsInput)
}
// ListMultipartUploadsCalls gets all the calls that were made to ListMultipartUploads.
@@ -1115,10 +1028,10 @@ func (mock *BackendMock) ListMultipartUploads(output *s3.ListMultipartUploadsInp
//
// len(mockedBackend.ListMultipartUploadsCalls())
func (mock *BackendMock) ListMultipartUploadsCalls() []struct {
Output *s3.ListMultipartUploadsInput
ListMultipartUploadsInput *s3.ListMultipartUploadsInput
} {
var calls []struct {
Output *s3.ListMultipartUploadsInput
ListMultipartUploadsInput *s3.ListMultipartUploadsInput
}
mock.lockListMultipartUploads.RLock()
calls = mock.calls.ListMultipartUploads
@@ -1127,7 +1040,7 @@ func (mock *BackendMock) ListMultipartUploadsCalls() []struct {
}
// ListObjectParts calls ListObjectPartsFunc.
func (mock *BackendMock) ListObjectParts(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (*s3.ListPartsOutput, error) {
func (mock *BackendMock) ListObjectParts(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (s3response.ListPartsResponse, error) {
if mock.ListObjectPartsFunc == nil {
panic("BackendMock.ListObjectPartsFunc: method is nil but Backend.ListObjectParts was just called")
}
@@ -1271,19 +1184,21 @@ func (mock *BackendMock) ListObjectsV2Calls() []struct {
}
// PutBucket calls PutBucketFunc.
func (mock *BackendMock) PutBucket(bucket string) error {
func (mock *BackendMock) PutBucket(bucket string, owner string) error {
if mock.PutBucketFunc == nil {
panic("BackendMock.PutBucketFunc: method is nil but Backend.PutBucket was just called")
}
callInfo := struct {
Bucket string
Owner string
}{
Bucket: bucket,
Owner: owner,
}
mock.lockPutBucket.Lock()
mock.calls.PutBucket = append(mock.calls.PutBucket, callInfo)
mock.lockPutBucket.Unlock()
return mock.PutBucketFunc(bucket)
return mock.PutBucketFunc(bucket, owner)
}
// PutBucketCalls gets all the calls that were made to PutBucket.
@@ -1292,9 +1207,11 @@ func (mock *BackendMock) PutBucket(bucket string) error {
// len(mockedBackend.PutBucketCalls())
func (mock *BackendMock) PutBucketCalls() []struct {
Bucket string
Owner string
} {
var calls []struct {
Bucket string
Owner string
}
mock.lockPutBucket.RLock()
calls = mock.calls.PutBucket
@@ -1303,19 +1220,21 @@ func (mock *BackendMock) PutBucketCalls() []struct {
}
// PutBucketAcl calls PutBucketAclFunc.
func (mock *BackendMock) PutBucketAcl(putBucketAclInput *s3.PutBucketAclInput) error {
func (mock *BackendMock) PutBucketAcl(bucket string, data []byte) error {
if mock.PutBucketAclFunc == nil {
panic("BackendMock.PutBucketAclFunc: method is nil but Backend.PutBucketAcl was just called")
}
callInfo := struct {
PutBucketAclInput *s3.PutBucketAclInput
Bucket string
Data []byte
}{
PutBucketAclInput: putBucketAclInput,
Bucket: bucket,
Data: data,
}
mock.lockPutBucketAcl.Lock()
mock.calls.PutBucketAcl = append(mock.calls.PutBucketAcl, callInfo)
mock.lockPutBucketAcl.Unlock()
return mock.PutBucketAclFunc(putBucketAclInput)
return mock.PutBucketAclFunc(bucket, data)
}
// PutBucketAclCalls gets all the calls that were made to PutBucketAcl.
@@ -1323,10 +1242,12 @@ func (mock *BackendMock) PutBucketAcl(putBucketAclInput *s3.PutBucketAclInput) e
//
// len(mockedBackend.PutBucketAclCalls())
func (mock *BackendMock) PutBucketAclCalls() []struct {
PutBucketAclInput *s3.PutBucketAclInput
Bucket string
Data []byte
} {
var calls []struct {
PutBucketAclInput *s3.PutBucketAclInput
Bucket string
Data []byte
}
mock.lockPutBucketAcl.RLock()
calls = mock.calls.PutBucketAcl
@@ -1620,52 +1541,8 @@ func (mock *BackendMock) StringCalls() []struct {
return calls
}
// UploadPart calls UploadPartFunc.
func (mock *BackendMock) UploadPart(bucket string, object string, uploadId string, Body io.ReadSeeker) (*s3.UploadPartOutput, error) {
if mock.UploadPartFunc == nil {
panic("BackendMock.UploadPartFunc: method is nil but Backend.UploadPart was just called")
}
callInfo := struct {
Bucket string
Object string
UploadId string
Body io.ReadSeeker
}{
Bucket: bucket,
Object: object,
UploadId: uploadId,
Body: Body,
}
mock.lockUploadPart.Lock()
mock.calls.UploadPart = append(mock.calls.UploadPart, callInfo)
mock.lockUploadPart.Unlock()
return mock.UploadPartFunc(bucket, object, uploadId, Body)
}
// UploadPartCalls gets all the calls that were made to UploadPart.
// Check the length with:
//
// len(mockedBackend.UploadPartCalls())
func (mock *BackendMock) UploadPartCalls() []struct {
Bucket string
Object string
UploadId string
Body io.ReadSeeker
} {
var calls []struct {
Bucket string
Object string
UploadId string
Body io.ReadSeeker
}
mock.lockUploadPart.RLock()
calls = mock.calls.UploadPart
mock.lockUploadPart.RUnlock()
return calls
}
// UploadPartCopy calls UploadPartCopyFunc.
func (mock *BackendMock) UploadPartCopy(uploadPartCopyInput *s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
func (mock *BackendMock) UploadPartCopy(uploadPartCopyInput *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) {
if mock.UploadPartCopyFunc == nil {
panic("BackendMock.UploadPartCopyFunc: method is nil but Backend.UploadPartCopy was just called")
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package controllers
import (
"github.com/versity/versitygw/auth"
"sync"
)
// Ensure, that IAMServiceMock does implement auth.IAMService.
// If this is not the case, regenerate this file with moq.
var _ auth.IAMService = &IAMServiceMock{}
// IAMServiceMock is a mock implementation of auth.IAMService.
//
// func TestSomethingThatUsesIAMService(t *testing.T) {
//
// // make and configure a mocked auth.IAMService
// mockedIAMService := &IAMServiceMock{
// CreateAccountFunc: func(access string, account auth.Account) error {
// panic("mock out the CreateAccount method")
// },
// DeleteUserAccountFunc: func(access string) error {
// panic("mock out the DeleteUserAccount method")
// },
// GetUserAccountFunc: func(access string) (auth.Account, error) {
// panic("mock out the GetUserAccount method")
// },
// }
//
// // use mockedIAMService in code that requires auth.IAMService
// // and then make assertions.
//
// }
type IAMServiceMock struct {
// CreateAccountFunc mocks the CreateAccount method.
CreateAccountFunc func(access string, account auth.Account) error
// DeleteUserAccountFunc mocks the DeleteUserAccount method.
DeleteUserAccountFunc func(access string) error
// GetUserAccountFunc mocks the GetUserAccount method.
GetUserAccountFunc func(access string) (auth.Account, error)
// calls tracks calls to the methods.
calls struct {
// CreateAccount holds details about calls to the CreateAccount method.
CreateAccount []struct {
// Access is the access argument value.
Access string
// Account is the account argument value.
Account auth.Account
}
// DeleteUserAccount holds details about calls to the DeleteUserAccount method.
DeleteUserAccount []struct {
// Access is the access argument value.
Access string
}
// GetUserAccount holds details about calls to the GetUserAccount method.
GetUserAccount []struct {
// Access is the access argument value.
Access string
}
}
lockCreateAccount sync.RWMutex
lockDeleteUserAccount sync.RWMutex
lockGetUserAccount sync.RWMutex
}
// CreateAccount calls CreateAccountFunc.
func (mock *IAMServiceMock) CreateAccount(access string, account auth.Account) error {
if mock.CreateAccountFunc == nil {
panic("IAMServiceMock.CreateAccountFunc: method is nil but IAMService.CreateAccount was just called")
}
callInfo := struct {
Access string
Account auth.Account
}{
Access: access,
Account: account,
}
mock.lockCreateAccount.Lock()
mock.calls.CreateAccount = append(mock.calls.CreateAccount, callInfo)
mock.lockCreateAccount.Unlock()
return mock.CreateAccountFunc(access, account)
}
// CreateAccountCalls gets all the calls that were made to CreateAccount.
// Check the length with:
//
// len(mockedIAMService.CreateAccountCalls())
func (mock *IAMServiceMock) CreateAccountCalls() []struct {
Access string
Account auth.Account
} {
var calls []struct {
Access string
Account auth.Account
}
mock.lockCreateAccount.RLock()
calls = mock.calls.CreateAccount
mock.lockCreateAccount.RUnlock()
return calls
}
// DeleteUserAccount calls DeleteUserAccountFunc.
func (mock *IAMServiceMock) DeleteUserAccount(access string) error {
if mock.DeleteUserAccountFunc == nil {
panic("IAMServiceMock.DeleteUserAccountFunc: method is nil but IAMService.DeleteUserAccount was just called")
}
callInfo := struct {
Access string
}{
Access: access,
}
mock.lockDeleteUserAccount.Lock()
mock.calls.DeleteUserAccount = append(mock.calls.DeleteUserAccount, callInfo)
mock.lockDeleteUserAccount.Unlock()
return mock.DeleteUserAccountFunc(access)
}
// DeleteUserAccountCalls gets all the calls that were made to DeleteUserAccount.
// Check the length with:
//
// len(mockedIAMService.DeleteUserAccountCalls())
func (mock *IAMServiceMock) DeleteUserAccountCalls() []struct {
Access string
} {
var calls []struct {
Access string
}
mock.lockDeleteUserAccount.RLock()
calls = mock.calls.DeleteUserAccount
mock.lockDeleteUserAccount.RUnlock()
return calls
}
// GetUserAccount calls GetUserAccountFunc.
func (mock *IAMServiceMock) GetUserAccount(access string) (auth.Account, error) {
if mock.GetUserAccountFunc == nil {
panic("IAMServiceMock.GetUserAccountFunc: method is nil but IAMService.GetUserAccount was just called")
}
callInfo := struct {
Access string
}{
Access: access,
}
mock.lockGetUserAccount.Lock()
mock.calls.GetUserAccount = append(mock.calls.GetUserAccount, callInfo)
mock.lockGetUserAccount.Unlock()
return mock.GetUserAccountFunc(access)
}
// GetUserAccountCalls gets all the calls that were made to GetUserAccount.
// Check the length with:
//
// len(mockedIAMService.GetUserAccountCalls())
func (mock *IAMServiceMock) GetUserAccountCalls() []struct {
Access string
} {
var calls []struct {
Access string
}
mock.lockGetUserAccount.RLock()
calls = mock.calls.GetUserAccount
mock.lockGetUserAccount.RUnlock()
return calls
}

View File

@@ -25,7 +25,7 @@ import (
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/smithy-go/logging"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/backend/auth"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
@@ -35,127 +35,150 @@ const (
iso8601Format = "20060102T150405Z"
)
type AdminConfig struct {
AdminAccess string
AdminSecret string
Region string
type RootUserConfig struct {
Access string
Secret string
}
func VerifyV4Signature(config AdminConfig, iam auth.IAMService, debug bool) fiber.Handler {
acct := accounts{
admin: config,
iam: iam,
}
func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, debug bool) fiber.Handler {
acct := accounts{root: root, iam: iam}
return func(ctx *fiber.Ctx) error {
authorization := ctx.Get("Authorization")
if authorization == "" {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty))
}
// Check the signature version
authParts := strings.Split(authorization, " ")
if len(authParts) < 4 {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrMissingFields))
}
if authParts[0] != "AWS4-HMAC-SHA256" {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported))
authParts := strings.Split(authorization, ",")
for i, el := range authParts {
authParts[i] = strings.TrimSpace(el)
}
credKv := strings.Split(authParts[1], "=")
if len(authParts) != 3 {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingFields))
}
startParts := strings.Split(authParts[0], " ")
if startParts[0] != "AWS4-HMAC-SHA256" {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported))
}
credKv := strings.Split(startParts[1], "=")
if len(credKv) != 2 {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrCredMalformed))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed))
}
creds := strings.Split(credKv[1], "/")
if len(creds) < 4 {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrCredMalformed))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed))
}
signHdrKv := strings.Split(authParts[2], "=")
signHdrKv := strings.Split(authParts[1], "=")
if len(signHdrKv) != 2 {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrCredMalformed))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed))
}
signedHdrs := strings.Split(signHdrKv[1], ";")
secret, ok := acct.getAcctSecret(creds[0])
if !ok {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID))
account, err := acct.getAccount(creds[0])
if err == auth.ErrNoSuchUser {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID))
}
if err != nil {
return controllers.SendResponse(ctx, err)
}
// Check X-Amz-Date header
date := ctx.Get("X-Amz-Date")
if date == "" {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrMissingDateHeader))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingDateHeader))
}
// Parse the date and check the date validity
tdate, err := time.Parse(iso8601Format, date)
if err != nil {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrMalformedDate))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedDate))
}
// Calculate the hash of the request payload
hashedPayload := sha256.Sum256(ctx.Body())
hexPayload := hex.EncodeToString(hashedPayload[:])
hashPayloadHeader := ctx.Get("X-Amz-Content-Sha256")
ok := isSpecialPayload(hashPayloadHeader)
// Compare the calculated hash with the hash provided
if hashPayloadHeader != hexPayload {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch))
if !ok {
// Calculate the hash of the request payload
hashedPayload := sha256.Sum256(ctx.Body())
hexPayload := hex.EncodeToString(hashedPayload[:])
// Compare the calculated hash with the hash provided
if hashPayloadHeader != hexPayload {
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch))
}
}
// Create a new http request instance from fasthttp request
req, err := utils.CreateHttpRequestFromCtx(ctx, signedHdrs)
if err != nil {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrInternalError))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInternalError))
}
signer := v4.NewSigner()
signErr := signer.SignHTTP(req.Context(), aws.Credentials{
AccessKeyID: creds[0],
SecretAccessKey: secret,
}, req, hexPayload, creds[3], config.Region, tdate, func(options *v4.SignerOptions) {
SecretAccessKey: account.Secret,
}, req, hashPayloadHeader, creds[3], region, tdate, func(options *v4.SignerOptions) {
if debug {
options.LogSigning = true
options.Logger = logging.NewStandardLogger(os.Stderr)
}
})
if signErr != nil {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrInternalError))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInternalError))
}
parts := strings.Split(req.Header.Get("Authorization"), " ")
if len(parts) < 4 {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrMissingFields))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingFields))
}
calculatedSign := strings.Split(parts[3], "=")[1]
expectedSign := strings.Split(authParts[3], "=")[1]
expectedSign := strings.Split(authParts[2], "=")[1]
if expectedSign != calculatedSign {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch))
}
ctx.Locals("role", account.Role)
ctx.Locals("access", creds[0])
ctx.Locals("isRoot", creds[0] == root.Access)
return ctx.Next()
}
}
type accounts struct {
admin AdminConfig
iam auth.IAMService
root RootUserConfig
iam auth.IAMService
}
func (a accounts) getAcctSecret(access string) (string, bool) {
if a.admin.AdminAccess == access {
return a.admin.AdminSecret, true
func (a accounts) getAccount(access string) (auth.Account, error) {
if access == a.root.Access {
return auth.Account{
Secret: a.root.Secret,
Role: "admin",
}, nil
}
conf, err := a.iam.GetIAMConfig()
if err != nil {
return "", false
}
secret, ok := conf.AccessAccounts[access]
return secret, ok
return a.iam.GetUserAccount(access)
}
func isSpecialPayload(str string) bool {
specialValues := map[string]bool{
"UNSIGNED-PAYLOAD": true,
"STREAMING-UNSIGNED-PAYLOAD-TRAILER": true,
"STREAMING-AWS4-HMAC-SHA256-PAYLOAD": true,
"STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER": true,
"STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD": true,
"STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER": true,
}
return specialValues[str]
}

View File

@@ -0,0 +1,44 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package middlewares
import (
"fmt"
"log"
"github.com/gofiber/fiber/v2"
)
func RequestLogger(isDebug bool) fiber.Handler {
return func(ctx *fiber.Ctx) error {
ctx.Locals("isDebug", isDebug)
if isDebug {
log.Println("Request headers: ")
ctx.Request().Header.VisitAll(func(key, val []byte) {
log.Printf("%s: %s", key, val)
})
if ctx.Request().URI().QueryArgs().Len() != 0 {
fmt.Println()
log.Println("Request query arguments: ")
ctx.Request().URI().QueryArgs().VisitAll(func(key, val []byte) {
log.Printf("%s: %s", key, val)
})
}
}
return ctx.Next()
}
}

View File

@@ -34,7 +34,7 @@ func VerifyMD5Body() fiber.Handler {
calculatedSum := base64.StdEncoding.EncodeToString(sum[:])
if incomingSum != calculatedSum {
return controllers.Responce[any](ctx, nil, s3err.GetAPIError(s3err.ErrInvalidDigest))
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidDigest))
}
return ctx.Next()

View File

@@ -16,14 +16,21 @@ package s3api
import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3api/controllers"
)
type S3ApiRouter struct{}
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend) {
s3ApiController := controllers.New(be)
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService) {
s3ApiController := controllers.New(be, iam)
adminController := controllers.AdminController{IAMService: iam}
app.Patch("/create-user", adminController.CreateUser)
// Admin Delete api
app.Patch("/delete-user", adminController.DeleteUser)
// ListBuckets action
app.Get("/", s3ApiController.ListBuckets)

View File

@@ -18,6 +18,7 @@ import (
"testing"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
)
@@ -25,6 +26,7 @@ func TestS3ApiRouter_Init(t *testing.T) {
type args struct {
app *fiber.App
be backend.Backend
iam auth.IAMService
}
tests := []struct {
name string
@@ -37,12 +39,13 @@ func TestS3ApiRouter_Init(t *testing.T) {
args: args{
app: fiber.New(),
be: backend.BackendUnsupported{},
iam: &auth.IAMServiceInternal{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.sa.Init(tt.args.app, tt.args.be)
tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam)
})
}
}

View File

@@ -19,8 +19,8 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/backend/auth"
"github.com/versity/versitygw/s3api/middlewares"
)
@@ -33,7 +33,7 @@ type S3ApiServer struct {
debug bool
}
func New(app *fiber.App, be backend.Backend, port string, adminUser middlewares.AdminConfig, iam auth.IAMService, opts ...Option) (*S3ApiServer, error) {
func New(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, port, region string, iam auth.IAMService, opts ...Option) (*S3ApiServer, error) {
server := &S3ApiServer{
app: app,
backend: be,
@@ -45,10 +45,16 @@ func New(app *fiber.App, be backend.Backend, port string, adminUser middlewares.
opt(server)
}
app.Use(middlewares.VerifyV4Signature(adminUser, iam, server.debug))
// Logging middlewares
app.Use(logger.New())
app.Use(middlewares.RequestLogger(server.debug))
// Authentication middlewares
app.Use(middlewares.VerifyV4Signature(root, iam, region, server.debug))
app.Use(middlewares.VerifyMD5Body())
server.router.Init(app, be)
server.router.Init(app, be, iam)
return server, nil
}

View File

@@ -15,21 +15,22 @@
package s3api
import (
"crypto/tls"
"reflect"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/backend/auth"
"github.com/versity/versitygw/s3api/middlewares"
)
func TestNew(t *testing.T) {
type args struct {
app *fiber.App
be backend.Backend
port string
adminUser middlewares.AdminConfig
app *fiber.App
be backend.Backend
port string
root middlewares.RootUserConfig
}
app := fiber.New()
@@ -46,10 +47,10 @@ func TestNew(t *testing.T) {
{
name: "Create S3 api server",
args: args{
app: app,
be: be,
port: port,
adminUser: middlewares.AdminConfig{},
app: app,
be: be,
port: port,
root: middlewares.RootUserConfig{},
},
wantS3ApiServer: &S3ApiServer{
app: app,
@@ -62,8 +63,8 @@ func TestNew(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotS3ApiServer, err := New(tt.args.app, tt.args.be,
tt.args.port, tt.args.adminUser, auth.IAMServiceUnsupported{})
gotS3ApiServer, err := New(tt.args.app, tt.args.be, tt.args.root,
tt.args.port, "us-east-1", &auth.IAMServiceInternal{})
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
@@ -82,15 +83,26 @@ func TestS3ApiServer_Serve(t *testing.T) {
wantErr bool
}{
{
name: "Return error when serving S3 api server with invalid address",
name: "Serve-invalid-address",
wantErr: true,
sa: &S3ApiServer{
app: fiber.New(),
backend: backend.BackendUnsupported{},
port: "Wrong address",
port: "Invalid address",
router: &S3ApiRouter{},
},
},
{
name: "Serve-invalid-address-with-certificate",
wantErr: true,
sa: &S3ApiServer{
app: fiber.New(),
backend: backend.BackendUnsupported{},
port: "Invalid address",
router: &S3ApiRouter{},
cert: &tls.Certificate{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

55
s3api/utils/logger.go Normal file
View File

@@ -0,0 +1,55 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package utils
import (
"fmt"
"log"
"github.com/gofiber/fiber/v2"
)
func LogCtxDetails(ctx *fiber.Ctx, respBody []byte) {
isDebug, ok := ctx.Locals("isDebug").(bool)
_, notLogReqBody := ctx.Locals("logReqBody").(bool)
_, notLogResBody := ctx.Locals("logResBody").(bool)
if isDebug && ok {
// Log request body
if !notLogReqBody {
fmt.Println()
log.Printf("Request Body: %s", ctx.Request().Body())
}
// Log path parameters
fmt.Println()
log.Println("Path parameters: ")
for key, val := range ctx.AllParams() {
log.Printf("%s: %s", key, val)
}
// Log response headers
fmt.Println()
log.Println("Response Headers: ")
ctx.Response().Header.VisitAll(func(key, val []byte) {
log.Printf("%s: %s", key, val)
})
// Log response body
if !notLogResBody && len(respBody) > 0 {
fmt.Println()
log.Printf("Response body %s", ctx.Response().Body())
}
}
}

View File

@@ -2,7 +2,6 @@ package utils
import (
"bytes"
"fmt"
"net/http"
"reflect"
"testing"
@@ -62,8 +61,6 @@ func TestCreateHttpRequestFromCtx(t *testing.T) {
return
}
fmt.Println(got.Header, tt.want.Header)
if !reflect.DeepEqual(got.Header, tt.want.Header) {
t.Errorf("CreateHttpRequestFromCtx() got = %v, want %v", got, tt.want)
}
@@ -120,3 +117,39 @@ func TestGetUserMetaData(t *testing.T) {
})
}
}
func Test_includeHeader(t *testing.T) {
type args struct {
hdr string
signedHdrs []string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "include-header-falsy-case",
args: args{
hdr: "Content-Type",
signedHdrs: []string{"X-Amz-Acl", "Content-Encoding"},
},
want: false,
},
{
name: "include-header-falsy-case",
args: args{
hdr: "Content-Type",
signedHdrs: []string{"X-Amz-Acl", "Content-Type"},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := includeHeader(tt.args.hdr, tt.args.signedHdrs); got != tt.want {
t.Errorf("includeHeader() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -105,6 +105,7 @@ const (
ErrAuthNotSetup
ErrNotImplemented
ErrPreconditionFailed
ErrInvalidObjectState
ErrExistingObjectIsDirectory
ErrObjectParentIsFile
@@ -368,6 +369,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "At least one of the pre-conditions you specified did not hold",
HTTPStatusCode: http.StatusPreconditionFailed,
},
ErrInvalidObjectState: {
Code: "InvalidObjectState",
Description: "The operation is not valid for the current state of the object",
HTTPStatusCode: http.StatusForbidden,
},
ErrExistingObjectIsDirectory: {
Code: "ExistingObjectIsDirectory",
Description: "Existing Object is a directory.",

692
s3response/AmazonS3.xsd Normal file
View File

@@ -0,0 +1,692 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema
xmlns:tns="http://s3.amazonaws.com/doc/2006-03-01/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="http://s3.amazonaws.com/doc/2006-03-01/">
<xsd:element name="CreateBucket">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="AccessControlList" type="tns:AccessControlList" minOccurs="0"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="MetadataEntry">
<xsd:sequence>
<xsd:element name="Name" type="xsd:string"/>
<xsd:element name="Value" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="CreateBucketResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="CreateBucketReturn" type="tns:CreateBucketResult"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="Status">
<xsd:sequence>
<xsd:element name="Code" type="xsd:int"/>
<xsd:element name="Description" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Result">
<xsd:sequence>
<xsd:element name="Status" type="tns:Status"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CreateBucketResult">
<xsd:sequence>
<xsd:element name="BucketName" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="DeleteBucket">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="DeleteBucketResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="DeleteBucketResponse" type="tns:Status"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="BucketLoggingStatus">
<xsd:sequence>
<xsd:element name="LoggingEnabled" type="tns:LoggingSettings" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="LoggingSettings">
<xsd:sequence>
<xsd:element name="TargetBucket" type="xsd:string"/>
<xsd:element name="TargetPrefix" type="xsd:string"/>
<xsd:element name="TargetGrants" type="tns:AccessControlList" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="GetBucketLoggingStatus">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetBucketLoggingStatusResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="GetBucketLoggingStatusResponse" type="tns:BucketLoggingStatus"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="SetBucketLoggingStatus">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
<xsd:element name="BucketLoggingStatus" type="tns:BucketLoggingStatus"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="SetBucketLoggingStatusResponse">
<xsd:complexType>
<xsd:sequence/>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetObjectAccessControlPolicy">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetObjectAccessControlPolicyResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="GetObjectAccessControlPolicyResponse" type="tns:AccessControlPolicy"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetBucketAccessControlPolicy">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetBucketAccessControlPolicyResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="GetBucketAccessControlPolicyResponse" type="tns:AccessControlPolicy"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType abstract="true" name="Grantee"/>
<xsd:complexType name="User" abstract="true">
<xsd:complexContent>
<xsd:extension base="tns:Grantee"/>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="AmazonCustomerByEmail">
<xsd:complexContent>
<xsd:extension base="tns:User">
<xsd:sequence>
<xsd:element name="EmailAddress" type="xsd:string"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="CanonicalUser">
<xsd:complexContent>
<xsd:extension base="tns:User">
<xsd:sequence>
<xsd:element name="ID" type="xsd:string"/>
<xsd:element name="DisplayName" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="Group">
<xsd:complexContent>
<xsd:extension base="tns:Grantee">
<xsd:sequence>
<xsd:element name="URI" type="xsd:string"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:simpleType name="Permission">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="READ"/>
<xsd:enumeration value="WRITE"/>
<xsd:enumeration value="READ_ACP"/>
<xsd:enumeration value="WRITE_ACP"/>
<xsd:enumeration value="FULL_CONTROL"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="StorageClass">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="STANDARD"/>
<xsd:enumeration value="REDUCED_REDUNDANCY"/>
<xsd:enumeration value="GLACIER"/>
<xsd:enumeration value="UNKNOWN"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="Grant">
<xsd:sequence>
<xsd:element name="Grantee" type="tns:Grantee"/>
<xsd:element name="Permission" type="tns:Permission"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="AccessControlList">
<xsd:sequence>
<xsd:element name="Grant" type="tns:Grant" minOccurs="0" maxOccurs="100"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CreateBucketConfiguration">
<xsd:sequence>
<xsd:element name="LocationConstraint" type="tns:LocationConstraint"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="LocationConstraint">
<xsd:simpleContent>
<xsd:extension base="xsd:string"/>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="AccessControlPolicy">
<xsd:sequence>
<xsd:element name="Owner" type="tns:CanonicalUser"/>
<xsd:element name="AccessControlList" type="tns:AccessControlList"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="SetObjectAccessControlPolicy">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="AccessControlList" type="tns:AccessControlList"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="SetObjectAccessControlPolicyResponse">
<xsd:complexType>
<xsd:sequence/>
</xsd:complexType>
</xsd:element>
<xsd:element name="SetBucketAccessControlPolicy">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="AccessControlList" type="tns:AccessControlList" minOccurs="0"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="SetBucketAccessControlPolicyResponse">
<xsd:complexType>
<xsd:sequence/>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetObject">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="GetMetadata" type="xsd:boolean"/>
<xsd:element name="GetData" type="xsd:boolean"/>
<xsd:element name="InlineData" type="xsd:boolean"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetObjectResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="GetObjectResponse" type="tns:GetObjectResult"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="GetObjectResult">
<xsd:complexContent>
<xsd:extension base="tns:Result">
<xsd:sequence>
<xsd:element name="Metadata" type="tns:MetadataEntry" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="Data" type="xsd:base64Binary" nillable="true"/>
<xsd:element name="LastModified" type="xsd:dateTime"/>
<xsd:element name="ETag" type="xsd:string"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="GetObjectExtended">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="GetMetadata" type="xsd:boolean"/>
<xsd:element name="GetData" type="xsd:boolean"/>
<xsd:element name="InlineData" type="xsd:boolean"/>
<xsd:element name="ByteRangeStart" type="xsd:long" minOccurs="0"/>
<xsd:element name="ByteRangeEnd" type="xsd:long" minOccurs="0"/>
<xsd:element name="IfModifiedSince" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="IfUnmodifiedSince" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="IfMatch" type="xsd:string" minOccurs="0" maxOccurs="100"/>
<xsd:element name="IfNoneMatch" type="xsd:string" minOccurs="0" maxOccurs="100"/>
<xsd:element name="ReturnCompleteObjectOnConditionFailure" type="xsd:boolean" minOccurs="0"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetObjectExtendedResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="GetObjectResponse" type="tns:GetObjectResult"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="PutObject">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="Metadata" type="tns:MetadataEntry" minOccurs="0" maxOccurs="100"/>
<xsd:element name="ContentLength" type="xsd:long"/>
<xsd:element name="AccessControlList" type="tns:AccessControlList" minOccurs="0"/>
<xsd:element name="StorageClass" type="tns:StorageClass" minOccurs="0"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="PutObjectResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="PutObjectResponse" type="tns:PutObjectResult"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="PutObjectResult">
<xsd:sequence>
<xsd:element name="ETag" type="xsd:string"/>
<xsd:element name="LastModified" type="xsd:dateTime"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="PutObjectInline">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element minOccurs="0" maxOccurs="100" name="Metadata" type="tns:MetadataEntry"/>
<xsd:element name="Data" type="xsd:base64Binary"/>
<xsd:element name="ContentLength" type="xsd:long"/>
<xsd:element name="AccessControlList" type="tns:AccessControlList" minOccurs="0"/>
<xsd:element name="StorageClass" type="tns:StorageClass" minOccurs="0"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="PutObjectInlineResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="PutObjectInlineResponse" type="tns:PutObjectResult"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="DeleteObject">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="DeleteObjectResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="DeleteObjectResponse" type="tns:Status"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="ListBucket">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="Prefix" type="xsd:string" minOccurs="0"/>
<xsd:element name="Marker" type="xsd:string" minOccurs="0"/>
<xsd:element name="MaxKeys" type="xsd:int" minOccurs="0"/>
<xsd:element name="Delimiter" type="xsd:string" minOccurs="0"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="ListBucketResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="ListBucketResponse" type="tns:ListBucketResult"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="ListVersionsResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="ListVersionsResponse" type="tns:ListVersionsResult"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="ListEntry">
<xsd:sequence>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="LastModified" type="xsd:dateTime"/>
<xsd:element name="ETag" type="xsd:string"/>
<xsd:element name="Size" type="xsd:long"/>
<xsd:element name="Owner" type="tns:CanonicalUser" minOccurs="0"/>
<xsd:element name="StorageClass" type="tns:StorageClass"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="VersionEntry">
<xsd:sequence>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="VersionId" type="xsd:string"/>
<xsd:element name="IsLatest" type="xsd:boolean"/>
<xsd:element name="LastModified" type="xsd:dateTime"/>
<xsd:element name="ETag" type="xsd:string"/>
<xsd:element name="Size" type="xsd:long"/>
<xsd:element name="Owner" type="tns:CanonicalUser" minOccurs="0"/>
<xsd:element name="StorageClass" type="tns:StorageClass"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="DeleteMarkerEntry">
<xsd:sequence>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="VersionId" type="xsd:string"/>
<xsd:element name="IsLatest" type="xsd:boolean"/>
<xsd:element name="LastModified" type="xsd:dateTime"/>
<xsd:element name="Owner" type="tns:CanonicalUser" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="PrefixEntry">
<xsd:sequence>
<xsd:element name="Prefix" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ListBucketResult">
<xsd:sequence>
<xsd:element name="Metadata" type="tns:MetadataEntry" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="Name" type="xsd:string"/>
<xsd:element name="Prefix" type="xsd:string"/>
<xsd:element name="Marker" type="xsd:string"/>
<xsd:element name="NextMarker" type="xsd:string" minOccurs="0"/>
<xsd:element name="MaxKeys" type="xsd:int"/>
<xsd:element name="Delimiter" type="xsd:string" minOccurs="0"/>
<xsd:element name="IsTruncated" type="xsd:boolean"/>
<xsd:element name="Contents" type="tns:ListEntry" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="CommonPrefixes" type="tns:PrefixEntry" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ListVersionsResult">
<xsd:sequence>
<xsd:element name="Metadata" type="tns:MetadataEntry" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="Name" type="xsd:string"/>
<xsd:element name="Prefix" type="xsd:string"/>
<xsd:element name="KeyMarker" type="xsd:string"/>
<xsd:element name="VersionIdMarker" type="xsd:string"/>
<xsd:element name="NextKeyMarker" type="xsd:string" minOccurs="0"/>
<xsd:element name="NextVersionIdMarker" type="xsd:string" minOccurs="0"/>
<xsd:element name="MaxKeys" type="xsd:int"/>
<xsd:element name="Delimiter" type="xsd:string" minOccurs="0"/>
<xsd:element name="IsTruncated" type="xsd:boolean"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="Version" type="tns:VersionEntry"/>
<xsd:element name="DeleteMarker" type="tns:DeleteMarkerEntry"/>
</xsd:choice>
<xsd:element name="CommonPrefixes" type="tns:PrefixEntry" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="ListAllMyBuckets">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="ListAllMyBucketsResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="ListAllMyBucketsResponse" type="tns:ListAllMyBucketsResult"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="ListAllMyBucketsEntry">
<xsd:sequence>
<xsd:element name="Name" type="xsd:string"/>
<xsd:element name="CreationDate" type="xsd:dateTime"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ListAllMyBucketsResult">
<xsd:sequence>
<xsd:element name="Owner" type="tns:CanonicalUser"/>
<xsd:element name="Buckets" type="tns:ListAllMyBucketsList"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ListAllMyBucketsList">
<xsd:sequence>
<xsd:element name="Bucket" type="tns:ListAllMyBucketsEntry" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="PostResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Location" type="xsd:anyURI"/>
<xsd:element name="Bucket" type="xsd:string"/>
<xsd:element name="Key" type="xsd:string"/>
<xsd:element name="ETag" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:simpleType name="MetadataDirective">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="COPY"/>
<xsd:enumeration value="REPLACE"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:element name="CopyObject">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="SourceBucket" type="xsd:string"/>
<xsd:element name="SourceKey" type="xsd:string"/>
<xsd:element name="DestinationBucket" type="xsd:string"/>
<xsd:element name="DestinationKey" type="xsd:string"/>
<xsd:element name="MetadataDirective" type="tns:MetadataDirective" minOccurs="0"/>
<xsd:element name="Metadata" type="tns:MetadataEntry" minOccurs="0" maxOccurs="100"/>
<xsd:element name="AccessControlList" type="tns:AccessControlList" minOccurs="0"/>
<xsd:element name="CopySourceIfModifiedSince" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="CopySourceIfUnmodifiedSince" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="CopySourceIfMatch" type="xsd:string" minOccurs="0" maxOccurs="100"/>
<xsd:element name="CopySourceIfNoneMatch" type="xsd:string" minOccurs="0" maxOccurs="100"/>
<xsd:element name="StorageClass" type="tns:StorageClass" minOccurs="0"/>
<xsd:element name="AWSAccessKeyId" type="xsd:string" minOccurs="0"/>
<xsd:element name="Timestamp" type="xsd:dateTime" minOccurs="0"/>
<xsd:element name="Signature" type="xsd:string" minOccurs="0"/>
<xsd:element name="Credential" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="CopyObjectResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="CopyObjectResult" type="tns:CopyObjectResult" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="CopyObjectResult">
<xsd:sequence>
<xsd:element name="LastModified" type="xsd:dateTime"/>
<xsd:element name="ETag" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="RequestPaymentConfiguration">
<xsd:sequence>
<xsd:element name="Payer" type="tns:Payer" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="Payer">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="BucketOwner"/>
<xsd:enumeration value="Requester"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="VersioningConfiguration">
<xsd:sequence>
<xsd:element name="Status" type="tns:VersioningStatus" minOccurs="0"/>
<xsd:element name="MfaDelete" type="tns:MfaDeleteStatus" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="MfaDeleteStatus">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Enabled"/>
<xsd:enumeration value="Disabled"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="VersioningStatus">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Enabled"/>
<xsd:enumeration value="Suspended"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="NotificationConfiguration">
<xsd:sequence>
<xsd:element name="TopicConfiguration" minOccurs="0" maxOccurs="unbounded" type="tns:TopicConfiguration"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="TopicConfiguration">
<xsd:sequence>
<xsd:element name="Topic" minOccurs="1" maxOccurs="1" type="xsd:string"/>
<xsd:element name="Event" minOccurs="1" maxOccurs="unbounded" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

6
s3response/README.txt Normal file
View File

@@ -0,0 +1,6 @@
https://doc.s3.amazonaws.com/2006-03-01/AmazonS3.xsd
see https://blog.aqwari.net/xml-schema-go/
go install aqwari.net/xml/cmd/xsdgen@latest
xsdgen -o s3api_xsd_generated.go -pkg s3response AmazonS3.xsd

File diff suppressed because it is too large Load Diff

109
s3response/s3response.go Normal file
View File

@@ -0,0 +1,109 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3response
import (
"encoding/xml"
)
// Part describes part metadata.
type Part struct {
PartNumber int
LastModified string
ETag string
Size int64
}
// ListPartsResponse - s3 api list parts response.
type ListPartsResponse struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListPartsResult" json:"-"`
Bucket string
Key string
UploadID string `xml:"UploadId"`
Initiator Initiator
Owner Owner
// The class of storage used to store the object.
StorageClass string
PartNumberMarker int
NextPartNumberMarker int
MaxParts int
IsTruncated bool
// List of parts.
Parts []Part `xml:"Part"`
}
// ListMultipartUploadsResponse - s3 api list multipart uploads response.
type ListMultipartUploadsResponse struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListMultipartUploadsResult" json:"-"`
Bucket string
KeyMarker string
UploadIDMarker string `xml:"UploadIdMarker"`
NextKeyMarker string
NextUploadIDMarker string `xml:"NextUploadIdMarker"`
Delimiter string
Prefix string
EncodingType string `xml:"EncodingType,omitempty"`
MaxUploads int
IsTruncated bool
// List of pending uploads.
Uploads []Upload `xml:"Upload"`
// Delimed common prefixes.
CommonPrefixes []CommonPrefix
}
// Upload desribes in progress multipart upload
type Upload struct {
Key string
UploadID string `xml:"UploadId"`
Initiator Initiator
Owner Owner
StorageClass string
Initiated string
}
// CommonPrefix ListObjectsResponse common prefixes (directory abstraction)
type CommonPrefix struct {
Prefix string
}
// Initiator same fields as Owner
type Initiator Owner
// Owner bucket ownership
type Owner struct {
ID string
DisplayName string
}
type Tag struct {
Key string `xml:"Key"`
Value string `xml:"Value"`
}
type TagSet struct {
Tags []Tag `xml:"Tag"`
}
type Tagging struct {
TagSet TagSet `xml:"TagSet"`
}