diff --git a/go.mod b/go.mod index 202640b..fa27851 100644 --- a/go.mod +++ b/go.mod @@ -2,29 +2,89 @@ module github.com/hashicorp/boundary-plugin-minio go 1.21.5 -require github.com/hashicorp/boundary/sdk v0.0.42 +require ( + github.com/google/go-cmp v0.6.0 + github.com/google/uuid v1.5.0 + github.com/hashicorp/boundary/sdk v0.0.42 + github.com/minio/madmin-go/v3 v3.0.48 + github.com/minio/minio-go/v7 v7.0.67 + github.com/ory/dockertest/v3 v3.10.0 + github.com/stretchr/testify v1.8.4 + google.golang.org/grpc v1.59.0 + google.golang.org/protobuf v1.32.0 +) require ( + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/containerd/continuity v0.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v20.10.17+incompatible // indirect + github.com/docker/docker v20.10.7+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/eventlogger v0.2.6-0.20231025104552-802587e608f0 // indirect github.com/hashicorp/eventlogger/filters/encrypt v0.1.8-0.20231025104552-802587e608f0 // indirect github.com/hashicorp/go-kms-wrapping/v2 v2.0.14 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.5 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/prometheus/prom2json v1.3.3 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/safchain/ethtool v0.3.0 // indirect + github.com/secure-io/sio-go v0.3.1 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.7.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect - google.golang.org/grpc v1.59.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7a9e9a2..1efc3da 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,78 @@ +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= +github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/boundary/sdk v0.0.42 h1:iRDdnM6/6q9qLtBP7/cpq/FWQGf69TFSsBQFxgGK8Jw= github.com/hashicorp/boundary/sdk v0.0.42/go.mod h1:QJhm4rky2YMtk9DS5Rj8QJhDKB/6zQbVrm3R0QgdYmQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -77,18 +125,43 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f h1:E87tDTVS5W65euzixn7clSzK66puSt1H4I5SC0EmHH4= github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW7GN5ngLm8YUZIPzf394= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE= +github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= 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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/madmin-go/v3 v3.0.48 h1:a7FZdWJNeWinDibsL6PeGnLB4JFgwO59cIYHWEtYb+Y= +github.com/minio/madmin-go/v3 v3.0.48/go.mod h1:ZDF7kf5fhmxLhbGTqyq5efs4ao0v4eWf7nOuef/ljJs= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.67 h1:BeBvZWAS+kRJm1vGTMJYVjKUNoo0FoEt/wUWdUtfmh8= +github.com/minio/minio-go/v7 v7.0.67/go.mod h1:+UXocnUeZ3wHvVh5s95gcrA4YjMIbccT6ubB+1m054A= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -102,62 +175,195 @@ github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQh github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo= +github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= +github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +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/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -168,6 +374,7 @@ google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -176,8 +383,16 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= diff --git a/internal/errors/error.go b/internal/errors/error.go new file mode 100644 index 0000000..92f8a6e --- /dev/null +++ b/internal/errors/error.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package errors + +import ( + "fmt" + "sort" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// InvalidArgumentError returns an grpc invalid argument status error. +func InvalidArgumentError(msg string, f map[string]string) error { + var fieldMsgs []string + for field, val := range f { + fieldMsgs = append(fieldMsgs, fmt.Sprintf("%s: %s", field, val)) + } + if len(fieldMsgs) > 0 { + sort.Strings(fieldMsgs) + msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", ")) + } + return status.Error(codes.InvalidArgument, msg) +} diff --git a/internal/errors/error_test.go b/internal/errors/error_test.go new file mode 100644 index 0000000..a2bac93 --- /dev/null +++ b/internal/errors/error_test.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package errors + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInvalidArgumentError(t *testing.T) { + // No error message. + err := InvalidArgumentError("", map[string]string{ + "foo_key": "foo_value", + "bar_key": "bar_value", + "baz_key": "baz_value", + }) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = : [bar_key: bar_value, baz_key: baz_value, foo_key: foo_value]") + + // Nil fields + err = InvalidArgumentError("error message", nil) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = error message") + + // Empty fields. + err = InvalidArgumentError("error message", map[string]string{}) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = error message") + + // Everything. + err = InvalidArgumentError("error message", map[string]string{ + "foo_key": "foo_value", + "bar_key": "bar_value", + "baz_key": "baz_value", + }) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = error message: [bar_key: bar_value, baz_key: baz_value, foo_key: foo_value]") + +} diff --git a/internal/testing/testing.go b/internal/testing/testing.go new file mode 100644 index 0000000..8af0118 --- /dev/null +++ b/internal/testing/testing.go @@ -0,0 +1,191 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testing + +import ( + "context" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/ory/dockertest/v3" + "github.com/stretchr/testify/require" +) + +const ( + rootUsername = "minio" + rootPassword = "minio1234567890" + apiAddress = ":9000" + consoleAddress = ":9090" +) + +var ( + defaultRepository = "minio/minio" + defaultVersion = "latest" +) + +type MinioServer struct { + // RootUsername is the root account username on the MinIO instance. + RootUsername string + // RootPassword is the root account password on the MinIO instance. + RootPassword string + // ServiceAccountAccessKeyId is the access key id for the default service + // account created in NewMinioServer. + ServiceAccountAccessKeyId string + // ServiceAccountSecretAccessKey is the secret access key for the default + // service account created in NewMinioServer. + ServiceAccountSecretAccessKey string + // ApiAddr is the MinIO S3 API address. + ApiAddr string + // Client is a MinIO SDK client, created with the default service account + // credentials. + Client *minio.Client + // AdminClient is a Madmin client created with the root account credentials. + AdminClient *madmin.AdminClient +} + +type Option func(testing.TB, *options) + +type options struct { + repository string + version string + skipCleanup bool +} + +func defaultOptions(t testing.TB) options { + return options{ + repository: defaultRepository, + version: defaultVersion, + skipCleanup: false, + } +} + +func getOptions(t testing.TB, opts ...Option) options { + t.Helper() + do := defaultOptions(t) + for _, o := range opts { + o(t, &do) + } + return do +} + +// WithRepository controls the MinIO docker image used. +func WithRepository(r string) Option { + return func(t testing.TB, o *options) { + t.Helper() + o.repository = r + } +} + +// WithVersion controls the MinIO docker image version used. +func WithVersion(v string) Option { + return func(t testing.TB, o *options) { + t.Helper() + o.version = v + } +} + +// WithSkipCleanup controls whether the MinIO instance gets destroyed after the +// test is run. If this option is passed, the instance will not be destroyed. +func WithSkipCleanup(s bool) Option { + return func(t testing.TB, o *options) { + t.Helper() + o.skipCleanup = s + } +} + +func init() { + mirror := os.Getenv("DOCKER_MIRROR") + if mirror != "" { + defaultRepository = strings.Join([]string{mirror, defaultRepository}, "/") + } +} + +// NewMinioServer uses docker to create a MinIO instance, ensures the instance +// is healthy, then creates a service account before returning a MinIOServer +// object, which contains the SDK client for API operations and the Madmin +// client for MinIO Admin operations. +func NewMinioServer(t testing.TB, inOpts ...Option) *MinioServer { + pool, err := dockertest.NewPool("") + require.NoError(t, err) + + opts := getOptions(t, inOpts...) + + dockerOptions := &dockertest.RunOptions{ + Repository: opts.repository, + Tag: opts.version, + Env: []string{ + fmt.Sprintf("%s=%s", "MINIO_ROOT_USER", rootUsername), + fmt.Sprintf("%s=%s", "MINIO_ROOT_PASSWORD", rootPassword), + fmt.Sprintf("%s=%s", "MINIO_ADDRESS", apiAddress), + fmt.Sprintf("%s=%s", "MINIO_CONSOLE_ADDRESS", consoleAddress), + fmt.Sprintf("%s=%s", "MINIO_VOLUMES", "/mnt/data"), + }, + Cmd: []string{"server"}, + } + + r, err := pool.RunWithOptions(dockerOptions) + require.NoError(t, err) + if !opts.skipCleanup { + t.Cleanup(func() { r.Close() }) + } + + server := &MinioServer{ + RootUsername: rootUsername, + RootPassword: rootPassword, + ApiAddr: fmt.Sprintf("localhost:%s", r.GetPort("9000/tcp")), + } + + // Ensure the instance is healthy. + healthCl, err := minio.New(server.ApiAddr, &minio.Options{Secure: false}) + require.NoError(t, err) + cancelFn, err := healthCl.HealthCheck(time.Second) + require.NoError(t, err) + + healthy := false + retries := 5 + interval := 500 * time.Millisecond + for i := 0; i < retries; i++ { + if healthCl.IsOnline() { + healthy = true + break + } + <-time.After(interval) + } + if !healthy { + cancelFn() + require.NoError(t, fmt.Errorf("minio instance not healthy after %s", time.Duration(retries)*interval)) + return nil + } + + // Create service account. + acl, err := madmin.NewWithOptions(server.ApiAddr, &madmin.Options{ + Creds: credentials.NewStaticV4(server.RootUsername, server.RootPassword, ""), + Secure: false, + }) + require.NoError(t, err) + server.AdminClient = acl + + creds, err := acl.AddServiceAccount(context.Background(), madmin.AddServiceAccountReq{ + Name: "Boundary MinIO Plugin Test", + Description: "MinIO credentials for Boundary MinIO plugin testing", + }) + require.NoError(t, err) + server.ServiceAccountAccessKeyId = creds.AccessKey + server.ServiceAccountSecretAccessKey = creds.SecretKey + + cl, err := minio.New(server.ApiAddr, &minio.Options{ + Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""), + Secure: false, + }) + require.NoError(t, err) + server.Client = cl + + return server +} diff --git a/internal/values/proto.go b/internal/values/proto.go new file mode 100644 index 0000000..7d553b7 --- /dev/null +++ b/internal/values/proto.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package values + +import ( + "fmt" + + "google.golang.org/protobuf/types/known/structpb" +) + +// GetStringValue returns a string value and no error if the given key is found +// in the provided proto struct input. An error is returned if the key is not +// found or the value type is not a string. An error is returned if the +// parameter argument "required" is set to true and the value from the proto +// struct input is empty. +func GetStringValue(in *structpb.Struct, k string, required bool) (string, error) { + mv := in.GetFields() + v, ok := mv[k] + if !ok { + if required { + return "", fmt.Errorf("missing required value %q", k) + } + + return "", nil + } + + s, ok := v.AsInterface().(string) + if !ok { + return "", fmt.Errorf("unexpected type for value %q: want string, got %T", k, v.AsInterface()) + } + + if s == "" && required { + return "", fmt.Errorf("value %q cannot be empty", k) + } + + return s, nil +} + +// StructFields returns a map[string]struct{} of the proto struct input. +func StructFields(s *structpb.Struct) map[string]struct{} { + m := make(map[string]struct{}, len(s.GetFields())) + for k := range s.GetFields() { + m[k] = struct{}{} + } + + return m +} diff --git a/internal/values/proto_test.go b/internal/values/proto_test.go new file mode 100644 index 0000000..0aa4b53 --- /dev/null +++ b/internal/values/proto_test.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package values + +import ( + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestGetStringValue(t *testing.T) { + cases := []struct { + name string + in map[string]any + key string + required bool + expected string + expectedErr string + }{ + { + name: "required missing", + in: map[string]any{}, + key: "foo", + required: true, + expectedErr: "missing required value \"foo\"", + }, + { + name: "optional missing", + in: map[string]any{}, + key: "foo", + expected: "", + }, + { + name: "non-string value", + in: map[string]any{ + "foo": 1, + }, + key: "foo", + expectedErr: "unexpected type for value \"foo\": want string, got float64", + }, + { + name: "required empty", + in: map[string]any{ + "foo": "", + }, + key: "foo", + required: true, + expectedErr: "value \"foo\" cannot be empty", + }, + { + name: "good", + in: map[string]any{ + "foo": "bar", + }, + key: "foo", + expected: "bar", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + input, err := structpb.NewStruct(tc.in) + require.NoError(err) + + actual, err := GetStringValue(input, tc.key, tc.required) + if tc.expectedErr != "" { + require.EqualError(err, tc.expectedErr) + return + } + + require.NoError(err) + require.Equal(tc.expected, actual) + }) + } +} diff --git a/plugin/service/storage/attributes.go b/plugin/service/storage/attributes.go new file mode 100644 index 0000000..6ff380f --- /dev/null +++ b/plugin/service/storage/attributes.go @@ -0,0 +1,116 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package storage + +import ( + "fmt" + "strings" + + "github.com/hashicorp/boundary-plugin-minio/internal/errors" + "github.com/hashicorp/boundary-plugin-minio/internal/values" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +const ( + ConstEndpointUrl = "endpoint_url" + ConstAccessKeyId = "access_key_id" + ConstSecretAccessKey = "secret_access_key" + ConstRegion = "region" +) + +type StorageAttributes struct { + // AccessKeyId is the MinIO Access Key Id. This field is required and comes + // from user input. + AccessKeyId string + // SecretAccessKey is the MinIO Secret Access Key. This field is required + // and comes from user input. + SecretAccessKey string + // EndpointUrl is the MinIO server URL. This field is required and comes + // from user input. + EndpointUrl string + // Region is the MinIO region. This field is optional and comes from user + // input. + Region string + // UseSSL determines if the MinIO SDK will use SSL when communicating with + // the server. This field is determined based on the endpoint URL prefix. + UseSSL bool +} + +// ToMap returns a map StorageAttributes's secret fields as a map. +func (sa *StorageAttributes) SecretsToMap() map[string]any { + return map[string]any{ + ConstAccessKeyId: sa.AccessKeyId, + ConstSecretAccessKey: sa.SecretAccessKey, + } +} + +func getStorageAttributes(inAttributes *structpb.Struct, inSecrets *structpb.Struct) (*StorageAttributes, error) { + if inAttributes == nil || inAttributes.GetFields() == nil || len(inAttributes.GetFields()) == 0 { + return nil, status.Error(codes.InvalidArgument, "empty attributes input") + } + if inSecrets == nil || inSecrets.GetFields() == nil || len(inSecrets.GetFields()) == 0 { + return nil, status.Error(codes.InvalidArgument, "empty secrets input") + } + + badFields := make(map[string]string) + + unknownAttrFields := values.StructFields(inAttributes) + endpointUrl, err := values.GetStringValue(inAttributes, ConstEndpointUrl, true) + if err != nil { + badFields[fmt.Sprintf("attributes.%s", ConstEndpointUrl)] = err.Error() + } + delete(unknownAttrFields, ConstEndpointUrl) + + // The MinIO SDK doesn't want a fully-qualified URL, so we have to strip it + // here and transform it to a combination of URL + useSSL. + useSSL := true // Assume SSL by default. + if strings.HasPrefix(endpointUrl, "https://") { + endpointUrl = strings.TrimPrefix(endpointUrl, "https://") + } else if strings.HasPrefix(endpointUrl, "http://") { + useSSL = false + endpointUrl = strings.TrimPrefix(endpointUrl, "http://") + } else { + badFields[fmt.Sprintf("attributes.%s.format", ConstEndpointUrl)] = "unknown protocol, should be http:// or https://" + } + + region, err := values.GetStringValue(inAttributes, ConstRegion, false) + if err != nil { + badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error() + } + delete(unknownAttrFields, ConstRegion) + + for s := range unknownAttrFields { + badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field" + } + + unknownSecretFields := values.StructFields(inSecrets) + accessKeyId, err := values.GetStringValue(inSecrets, ConstAccessKeyId, true) + if err != nil { + badFields[fmt.Sprintf("secrets.%s", ConstAccessKeyId)] = err.Error() + } + delete(unknownSecretFields, ConstAccessKeyId) + + secretAccessKey, err := values.GetStringValue(inSecrets, ConstSecretAccessKey, true) + if err != nil { + badFields[fmt.Sprintf("secrets.%s", ConstSecretAccessKey)] = err.Error() + } + delete(unknownSecretFields, ConstSecretAccessKey) + + for s := range unknownSecretFields { + badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field" + } + + if len(badFields) > 0 { + return nil, errors.InvalidArgumentError("invalid or unrecognized attributes/secrets found", badFields) + } + return &StorageAttributes{ + AccessKeyId: accessKeyId, + SecretAccessKey: secretAccessKey, + EndpointUrl: endpointUrl, + Region: region, + UseSSL: useSSL, + }, nil +} diff --git a/plugin/service/storage/attributes_test.go b/plugin/service/storage/attributes_test.go new file mode 100644 index 0000000..a50d54b --- /dev/null +++ b/plugin/service/storage/attributes_test.go @@ -0,0 +1,375 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package storage + +import ( + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestGetStorageAttributes(t *testing.T) { + tests := []struct { + name string + inAttributes *structpb.Struct + inSecrets *structpb.Struct + expStorageAttributes *StorageAttributes + expErrMsg string + }{ + { + name: "nilAttributes", + inAttributes: nil, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "empty attributes input", + }, + { + name: "nilAttributesFields", + inAttributes: &structpb.Struct{Fields: nil}, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "empty attributes input", + }, + { + name: "emptyAttributesFields", + inAttributes: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "empty attributes input", + }, + { + name: "nilSecrets", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + inSecrets: nil, + expErrMsg: "empty secrets input", + }, + { + name: "nilSecretsFields", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + inSecrets: &structpb.Struct{Fields: nil}, + expErrMsg: "empty secrets input", + }, + { + name: "emptySecretsFields", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + inSecrets: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + expErrMsg: "empty secrets input", + }, + { + name: "noEndpointUrlAttribute", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "attributes.endpoint_url: missing required value \"endpoint_url\"", + }, + { + name: "emptyEndpointUrlAttribute", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue(""), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "attributes.endpoint_url: value \"endpoint_url\" cannot be empty", + }, + { + name: "endpointUrlAttributeBadType", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewBoolValue(true), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "attributes.endpoint_url: unexpected type for value \"endpoint_url\": want string, got bool", + }, + { + name: "badEndpointUrlAttributeFormat", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("bad://foo.bar"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "attributes.endpoint_url.format: unknown protocol, should be http:// or https://", + }, + { + name: "regionAttributeBadType", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewBoolValue(true), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "attributes.region: unexpected type for value \"region\": want string, got bool", + }, + { + name: "unknownAttribute", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + "foo": structpb.NewStringValue("bar"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "attributes.foo: unrecognized field", + }, + { + name: "noAccessKeyIdSecret", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "secrets.access_key_id: missing required value \"access_key_id\"", + }, + { + name: "emptyAccessKeyIdSecret", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(""), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "secrets.access_key_id: value \"access_key_id\" cannot be empty", + }, + { + name: "accessKeyIdSecretBadType", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewBoolValue(true), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "secrets.access_key_id: unexpected type for value \"access_key_id\": want string, got bool", + }, + { + name: "noSecretAccessKeySecret", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "secrets.secret_access_key: missing required value \"secret_access_key\"", + }, + { + name: "emptySecretAccessKeySecret", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + ConstSecretAccessKey: structpb.NewStringValue(""), + }, + }, + expErrMsg: "secrets.secret_access_key: value \"secret_access_key\" cannot be empty", + }, + { + name: "secretAccessKeySecretBadType", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + ConstSecretAccessKey: structpb.NewBoolValue(true), + }, + }, + expErrMsg: "secrets.secret_access_key: unexpected type for value \"secret_access_key\": want string, got bool", + }, + { + name: "unknownSecret", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("bar"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + "foo": structpb.NewStringValue("bar"), + }, + }, + expErrMsg: "secrets.foo: unrecognized field", + }, + { + name: "successWithNoSSL", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("foo"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expStorageAttributes: &StorageAttributes{ + AccessKeyId: "foo", + SecretAccessKey: "bar", + EndpointUrl: "foo.bar", + Region: "us-east-1", + UseSSL: false, + }, + }, + { + name: "successWithSSL", + inAttributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("https://foo.bar"), + ConstRegion: structpb.NewStringValue("us-east-1"), + }, + }, + inSecrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("foo"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + expStorageAttributes: &StorageAttributes{ + AccessKeyId: "foo", + SecretAccessKey: "bar", + EndpointUrl: "foo.bar", + Region: "us-east-1", + UseSSL: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sa, err := getStorageAttributes(tt.inAttributes, tt.inSecrets) + if tt.expErrMsg != "" { + require.ErrorContains(t, err, tt.expErrMsg) + require.Nil(t, sa) + return + } + + require.NoError(t, err) + require.NotNil(t, sa) + require.EqualValues(t, tt.expStorageAttributes, sa) + }) + } +} + +func TestStorageAttributesSecretsToMap(t *testing.T) { + tests := []struct { + name string + in *StorageAttributes + expMap map[string]any + }{ + { + name: "completeObject", + in: &StorageAttributes{ + AccessKeyId: "access_key_id_value", + SecretAccessKey: "secret_access_key_value", + EndpointUrl: "endpoint_url_value", + Region: "region_value", + UseSSL: true, + }, + expMap: map[string]any{ + "access_key_id": "access_key_id_value", + "secret_access_key": "secret_access_key_value", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := tt.in.SecretsToMap() + require.EqualValues(t, tt.expMap, m) + }) + } +} diff --git a/plugin/service/storage/plugin.go b/plugin/service/storage/plugin.go index c615aae..dd92bb2 100644 --- a/plugin/service/storage/plugin.go +++ b/plugin/service/storage/plugin.go @@ -4,10 +4,20 @@ package storage import ( + "bytes" "context" "fmt" + "path" + "github.com/google/uuid" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" + madmin "github.com/minio/madmin-go/v3" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" ) var _ pb.StoragePluginServiceServer = (*StoragePlugin)(nil) @@ -20,7 +30,45 @@ type StoragePlugin struct { // OnCreateStorageBucket is a hook that runs when a storage bucket is created. func (sp *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCreateStorageBucketRequest) (*pb.OnCreateStorageBucketResponse, error) { - return nil, fmt.Errorf("unimplemented") + bucket := req.GetBucket() + if bucket == nil { + return nil, status.Error(codes.InvalidArgument, "no storage bucket information found") + } + if bucket.GetBucketName() == "" { + return nil, status.Error(codes.InvalidArgument, "storage bucket name is required") + } + + sa, err := getStorageAttributes(bucket.GetAttributes(), bucket.GetSecrets()) + if err != nil { + return nil, err // getStorageAttributes already returns a "status"-type error. + } + + err = ensureServiceAccount(ctx, sa) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to ensure service account: %v", err) + } + + cl, err := minio.New(sa.EndpointUrl, &minio.Options{ + Creds: credentials.NewStaticV4(sa.AccessKeyId, sa.SecretAccessKey, ""), + Secure: sa.UseSSL, + Region: sa.Region, + }) + if err != nil { + return nil, status.Errorf(codes.Unknown, "failed to create minio sdk client: %v", err) + } + + err = dryRun(ctx, cl, bucket) + if err != nil { + return nil, status.Errorf(codes.Unknown, "failed to verify provided minio environment: %v", err) + } + + persistedData, err := structpb.NewStruct(sa.SecretsToMap()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert storage attributes to protobuf struct: %v", err) + } + return &pb.OnCreateStorageBucketResponse{ + Persisted: &storagebuckets.StorageBucketPersisted{Data: persistedData}, + }, nil } // OnUpdateStorageBucket is a hook that runs when a storage bucket is updated. @@ -36,7 +84,39 @@ func (sp *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDe // ValidatePermissions is a hook that checks if the secrets associated with the // storage bucket meet the requirements of the plugin. func (sp *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.ValidatePermissionsRequest) (*pb.ValidatePermissionsResponse, error) { - return nil, fmt.Errorf("unimplemented") + bucket := req.GetBucket() + if bucket == nil { + return nil, status.Error(codes.InvalidArgument, "no storage bucket information found") + } + if bucket.GetBucketName() == "" { + return nil, status.Error(codes.InvalidArgument, "storage bucket name is required") + } + + sa, err := getStorageAttributes(bucket.GetAttributes(), bucket.GetSecrets()) + if err != nil { + return nil, err // getStorageAttributes already returns a "status"-type error. + } + + err = ensureServiceAccount(ctx, sa) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to ensure service account: %v", err) + } + + cl, err := minio.New(sa.EndpointUrl, &minio.Options{ + Creds: credentials.NewStaticV4(sa.AccessKeyId, sa.SecretAccessKey, ""), + Secure: sa.UseSSL, + Region: sa.Region, + }) + if err != nil { + return nil, status.Errorf(codes.Unknown, "failed to create minio sdk client: %v", err) + } + + err = dryRun(ctx, cl, bucket) + if err != nil { + return nil, status.Errorf(codes.Unknown, "failed to verify provided minio environment: %v", err) + } + + return &pb.ValidatePermissionsResponse{}, nil } // HeadObject is a hook that retrieves metadata about an object. @@ -60,3 +140,56 @@ func (sp *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest func (sp *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjectsRequest) (*pb.DeleteObjectsResponse, error) { return nil, fmt.Errorf("unimplemented") } + +func dryRun(ctx context.Context, cl *minio.Client, bucket *storagebuckets.StorageBucket) error { + objectKey := path.Join(bucket.GetBucketPrefix(), uuid.New().String()) + rd := bytes.NewReader([]byte("hashicorp boundary minio plugin access test")) + _, err := cl.PutObject(ctx, bucket.GetBucketName(), objectKey, rd, rd.Size(), minio.PutObjectOptions{}) + if err != nil { + return fmt.Errorf("failed to put object at %q: %w", objectKey, err) + } + + _, err = cl.StatObject(ctx, bucket.GetBucketName(), objectKey, minio.StatObjectOptions{}) + if err != nil { + return fmt.Errorf("failed to stat object at %q: %w", objectKey, err) + } + + _, err = cl.GetObject(ctx, bucket.GetBucketName(), objectKey, minio.GetObjectOptions{}) + if err != nil { + return fmt.Errorf("failed to get object at %q: %w", objectKey, err) + } + + oiCh := cl.ListObjects(ctx, bucket.GetBucketName(), minio.ListObjectsOptions{Recursive: true}) + for oi := range oiCh { + if oi.Err != nil { + return fmt.Errorf("failed to list objects in bucket %q: %w", bucket.GetBucketName(), oi.Err) + } + } + + err = cl.RemoveObject(ctx, bucket.GetBucketName(), objectKey, minio.RemoveObjectOptions{}) + if err != nil { + return fmt.Errorf("failed to remove object at %q: %w", objectKey, err) + } + + return nil +} + +// ensureServiceAccount ensures the credentials we received belong to a MinIO +// service account. This plugin does not support using user credentials for its +// configuration. +func ensureServiceAccount(ctx context.Context, sa *StorageAttributes) error { + cl, err := madmin.NewWithOptions(sa.EndpointUrl, &madmin.Options{ + Creds: credentials.NewStaticV4(sa.AccessKeyId, sa.SecretAccessKey, ""), + Secure: sa.UseSSL, + }) + if err != nil { + return fmt.Errorf("failed to create madmin client: %w", err) + } + + _, err = cl.InfoServiceAccount(ctx, sa.AccessKeyId) + if err != nil { + return fmt.Errorf("failed to obtain service account info: %w", err) + } + + return nil +} diff --git a/plugin/service/storage/plugin_test.go b/plugin/service/storage/plugin_test.go index 661f5f7..02cad4c 100644 --- a/plugin/service/storage/plugin_test.go +++ b/plugin/service/storage/plugin_test.go @@ -4,16 +4,374 @@ package storage import ( + "context" "testing" + + "github.com/google/go-cmp/cmp" + internaltest "github.com/hashicorp/boundary-plugin-minio/internal/testing" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets" + "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/minio/madmin-go/v3" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" ) -func TestOnCreateStorageBucket(t *testing.T) {} +var ( + policyDenyPutObject = []byte(`{ + "Statement": [ + { + "Action": [ "s3:*" ], + "Effect": "Allow", + "Resource": "arn:aws:s3:::*" + }, + { + "Action": [ "s3:PutObject" ], + "Effect": "Deny", + "Resource": "arn:aws:s3:::*" + }, + { + "Action": [ + "admin:CreateServiceAccount", + "admin:RemoveServiceAccount" + ], + "Effect": "Allow" + } + ], + "Version": "2012-10-17" + }`) + policyDenyStatObject = []byte(`{ + "Statement": [ + { + "Action": [ "s3:*" ], + "Effect": "Allow", + "Resource": "arn:aws:s3:::*" + }, + { + "Action": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetObjectAttributes", + "s3:GetObjectVersionAttributes" + ], + "Effect": "Deny", + "Resource": "arn:aws:s3:::*" + }, + { + "Action": [ + "admin:CreateServiceAccount", + "admin:RemoveServiceAccount" + ], + "Effect": "Allow" + } + ], + "Version": "2012-10-17" + }`) + policyDenyListBucket = []byte(`{ + "Statement": [ + { + "Action": [ "s3:*" ], + "Effect": "Allow", + "Resource": "arn:aws:s3:::*" + }, + { + "Action": [ "s3:ListBucket" ], + "Effect": "Deny", + "Resource": "arn:aws:s3:::*" + }, + { + "Action": [ + "admin:CreateServiceAccount", + "admin:RemoveServiceAccount" + ], + "Effect": "Allow" + } + ], + "Version": "2012-10-17" + }`) + policyDenyDeleteObject = []byte(`{ + "Statement": [ + { + "Action": [ "s3:*" ], + "Effect": "Allow", + "Resource": "arn:aws:s3:::*" + }, + { + "Action": [ "s3:DeleteObject" ], + "Effect": "Deny", + "Resource": "arn:aws:s3:::*" + }, + { + "Action": [ + "admin:CreateServiceAccount", + "admin:RemoveServiceAccount" + ], + "Effect": "Allow" + } + ], + "Version": "2012-10-17" + }`) +) + +func TestOnCreateStorageBucket(t *testing.T) { + ctx := context.Background() + server := internaltest.NewMinioServer(t) + + bucketName := "test-bucket" + err := server.Client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) + require.NoError(t, err) + + tests := []struct { + name string + minioClFn func(t *testing.T) *minio.Client + req *plugin.OnCreateStorageBucketRequest + expRsp *plugin.OnCreateStorageBucketResponse + expErrMsg string + }{ + { + name: "nilBucket", + req: &plugin.OnCreateStorageBucketRequest{Bucket: nil}, + expErrMsg: "no storage bucket information found", + }, + { + name: "noBucketName", + req: &plugin.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "", + }, + }, + expErrMsg: "storage bucket name is required", + }, + { + name: "badStorageAttributes", + req: &plugin.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + }, + }, + expErrMsg: "empty attributes input", + }, + { + name: "usingNonServiceAccount", + req: &plugin.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(server.RootUsername), + ConstSecretAccessKey: structpb.NewStringValue(server.RootPassword), + }, + }, + }, + }, + expErrMsg: "The specified service account is not found (Specified service account does not exist)", + }, + { + name: "dryRunFail", + req: &plugin.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr), + }, + }, + Secrets: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ + Policy: policyDenyPutObject, + }) + require.NoError(t, err) + + return map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(creds.AccessKey), + ConstSecretAccessKey: structpb.NewStringValue(creds.SecretKey), + } + }(), + }, + }, + }, + expErrMsg: "failed to verify provided minio environment: failed to put object", + }, + { + name: "success", + req: &plugin.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId), + ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey), + }, + }, + }, + }, + expRsp: &plugin.OnCreateStorageBucketResponse{ + Persisted: &storagebuckets.StorageBucketPersisted{ + Data: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId), + ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey), + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sp := new(StoragePlugin) + rsp, err := sp.OnCreateStorageBucket(ctx, tt.req) + if tt.expErrMsg != "" { + require.ErrorContains(t, err, tt.expErrMsg) + require.Nil(t, rsp) + return + } + + require.NoError(t, err) + require.NotNil(t, rsp) + require.Empty(t, cmp.Diff(tt.expRsp, rsp, protocmp.Transform())) + }) + } +} func TestOnUpdateStorageBucket(t *testing.T) {} func TestOnDeleteStorageBucket(t *testing.T) {} -func TestValidatePermissions(t *testing.T) {} +func TestValidatePermissions(t *testing.T) { + ctx := context.Background() + server := internaltest.NewMinioServer(t) + + bucketName := "test-bucket" + err := server.Client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) + require.NoError(t, err) + + tests := []struct { + name string + minioClFn func(t *testing.T) *minio.Client + req *plugin.ValidatePermissionsRequest + expErrMsg string + }{ + { + name: "nilBucket", + req: &plugin.ValidatePermissionsRequest{Bucket: nil}, + expErrMsg: "no storage bucket information found", + }, + { + name: "noBucketName", + req: &plugin.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "", + }, + }, + expErrMsg: "storage bucket name is required", + }, + { + name: "badStorageAttributes", + req: &plugin.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + }, + }, + expErrMsg: "empty attributes input", + }, + { + name: "usingNonServiceAccount", + req: &plugin.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(server.RootUsername), + ConstSecretAccessKey: structpb.NewStringValue(server.RootPassword), + }, + }, + }, + }, + expErrMsg: "The specified service account is not found (Specified service account does not exist)", + }, + { + name: "dryRunFail", + req: &plugin.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr), + }, + }, + Secrets: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + creds, err := server.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ + Policy: policyDenyPutObject, + }) + require.NoError(t, err) + + return map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(creds.AccessKey), + ConstSecretAccessKey: structpb.NewStringValue(creds.SecretKey), + } + }(), + }, + }, + }, + expErrMsg: "failed to verify provided minio environment: failed to put object", + }, + { + name: "success", + req: &plugin.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId), + ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey), + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sp := new(StoragePlugin) + rsp, err := sp.ValidatePermissions(ctx, tt.req) + if tt.expErrMsg != "" { + require.ErrorContains(t, err, tt.expErrMsg) + require.Nil(t, rsp) + return + } + + require.NoError(t, err) + require.NotNil(t, rsp) + }) + } +} func TestHeadObject(t *testing.T) {} @@ -22,3 +380,210 @@ func TestGetObject(t *testing.T) {} func TestPutObject(t *testing.T) {} func TestDeleteObjects(t *testing.T) {} + +func TestDryRun(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + minioClFn func(t *testing.T, s *internaltest.MinioServer) *minio.Client + makeBucketFn func(t *testing.T, cl *minio.Client) string + // This test verifies that each message in the err slice is contained in + // the error string, effectively &&-ing the checks together. + expErrMsgs []string + }{ + { + name: "bucketNotExists", + makeBucketFn: func(t *testing.T, cl *minio.Client) string { return "unknownbucket" }, + expErrMsgs: []string{"The specified bucket does not exist"}, + }, + { + name: "putObjectFail", + minioClFn: func(t *testing.T, s *internaltest.MinioServer) *minio.Client { + creds, err := s.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ + Policy: policyDenyPutObject, + }) + require.NoError(t, err) + + cl, err := minio.New(s.ApiAddr, &minio.Options{ + Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""), + Secure: false, + }) + require.NoError(t, err) + + return cl + }, + makeBucketFn: func(t *testing.T, cl *minio.Client) string { + bucketName := "testdryrun-putobjectfail" + err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) + require.NoError(t, err) + + return bucketName + }, + expErrMsgs: []string{ + "failed to put object", + "Access Denied", + }, + }, + { + name: "statObjectFail", + minioClFn: func(t *testing.T, s *internaltest.MinioServer) *minio.Client { + creds, err := s.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ + Policy: policyDenyStatObject, + }) + require.NoError(t, err) + + cl, err := minio.New(s.ApiAddr, &minio.Options{ + Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""), + Secure: false, + }) + require.NoError(t, err) + + return cl + }, + makeBucketFn: func(t *testing.T, cl *minio.Client) string { + bucketName := "testdryrun-statobjectfail" + err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) + require.NoError(t, err) + + return bucketName + }, + expErrMsgs: []string{ + "failed to stat object", + "Access Denied", + }, + }, + { + name: "listObjectsFail", + minioClFn: func(t *testing.T, s *internaltest.MinioServer) *minio.Client { + creds, err := s.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ + Policy: policyDenyListBucket, + }) + require.NoError(t, err) + + cl, err := minio.New(s.ApiAddr, &minio.Options{ + Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""), + Secure: false, + }) + require.NoError(t, err) + + return cl + }, + makeBucketFn: func(t *testing.T, cl *minio.Client) string { + bucketName := "testdryrun-listobjectsfail" + err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) + require.NoError(t, err) + + return bucketName + }, + expErrMsgs: []string{ + "failed to list objects in bucket", + "Access Denied", + }, + }, + { + name: "removeObjectFail", + minioClFn: func(t *testing.T, s *internaltest.MinioServer) *minio.Client { + creds, err := s.AdminClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ + Policy: policyDenyDeleteObject, + }) + require.NoError(t, err) + + cl, err := minio.New(s.ApiAddr, &minio.Options{ + Creds: credentials.NewStaticV4(creds.AccessKey, creds.SecretKey, ""), + Secure: false, + }) + require.NoError(t, err) + + return cl + }, + makeBucketFn: func(t *testing.T, cl *minio.Client) string { + bucketName := "testdryrun-removeobjectfail" + err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) + require.NoError(t, err) + + return bucketName + }, + expErrMsgs: []string{ + "failed to remove object", + "Access Denied", + }, + }, + { + name: "success", + makeBucketFn: func(t *testing.T, cl *minio.Client) string { + bucketName := "testdryrun-success" + err := cl.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) + require.NoError(t, err) + return bucketName + }, + }, + } + + server := internaltest.NewMinioServer(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + minioClient := server.Client + if tt.minioClFn != nil { + minioClient = tt.minioClFn(t, server) + } + + bucketName := tt.makeBucketFn(t, minioClient) + err := dryRun(ctx, minioClient, &storagebuckets.StorageBucket{ + BucketName: bucketName, + }) + if len(tt.expErrMsgs) > 0 { + for _, errMsg := range tt.expErrMsgs { + require.ErrorContains(t, err, errMsg) + } + return + } + + require.NoError(t, err) + }) + } +} + +func TestEnsureServiceAccount(t *testing.T) { + tests := []struct { + name string + saFunc func(s *internaltest.MinioServer) *StorageAttributes + expErrMsg string + }{ + { + name: "nonServiceAccount", + saFunc: func(s *internaltest.MinioServer) *StorageAttributes { + return &StorageAttributes{ + AccessKeyId: s.RootUsername, + SecretAccessKey: s.RootPassword, + EndpointUrl: s.ApiAddr, + UseSSL: false, + } + }, + expErrMsg: "The specified service account is not found (Specified service account does not exist)", + }, + { + name: "serviceAccount", + saFunc: func(s *internaltest.MinioServer) *StorageAttributes { + return &StorageAttributes{ + AccessKeyId: s.ServiceAccountAccessKeyId, + SecretAccessKey: s.ServiceAccountSecretAccessKey, + EndpointUrl: s.ApiAddr, + UseSSL: false, + } + }, + }, + } + + server := internaltest.NewMinioServer(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ensureServiceAccount(context.Background(), tt.saFunc(server)) + if tt.expErrMsg != "" { + require.ErrorContains(t, err, tt.expErrMsg) + return + } + require.NoError(t, err) + }) + } +}