mirror of https://github.com/ceph/go-ceph
rgwadmin: add support for RadosGW Admin Ops API
Following this discussion #492 This commit introduces a new package "rgw/admin" which helps you interact with the [RadosGW Admin Ops API](https://docs.ceph.com/en/latest/radosgw/adminops). Not all the API capabilities are covered by this commit, but this is a solid foundation for adding code on top. I'm expecting a few more iterations to make 100% complete. Also, the RadosGW Admin API is going to implement new functions soon (like bucket creation). So this library will live on and keep catching up. As many unit tests as possible have been added. A new integration test suite also runs. The "micro-osd.sh" now deploys a RGW and the integration suite tests on it. Thus the CI should cover it. Shout out to @QuentinPerez and @IrekFasikhov for their existing libraries. They were a very good inspiration to get started. Co-authored-by: Irek Fasikhov <malmyzh@gmail.com> Co-authored-by: Quentin Perez <qperez42@gmail.com> Signed-off-by: Sébastien Han <seb@redhat.com>
This commit is contained in:
parent
edd90d8de4
commit
6be8d370cb
|
@ -11,6 +11,7 @@ There are three main Go sub-packages that make up go-ceph:
|
|||
* rados - exports functionality from Ceph's librados
|
||||
* rbd - exports functionality from Ceph's librbd
|
||||
* cephfs - exports functionality from Ceph's libcephfs
|
||||
* rgw/admin - interact with [radosgw admin ops API](https://docs.ceph.com/en/latest/radosgw/adminops)
|
||||
|
||||
We aim to provide comprehensive support for the Ceph APIs over time. This
|
||||
includes both I/O related functions and management functions. If your project
|
||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module github.com/ceph/go-ceph
|
|||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.35.24
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3
|
||||
|
|
16
go.sum
16
go.sum
|
@ -1,15 +1,29 @@
|
|||
github.com/aws/aws-sdk-go v1.35.24 h1:U3GNTg8+7xSM6OAJ8zksiSM4bRqxBWmVwwehvOSNG3A=
|
||||
github.com/aws/aws-sdk-go v1.35.24/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
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 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
22
micro-osd.sh
22
micro-osd.sh
|
@ -28,11 +28,15 @@ MON_DATA=${DIR}/mon
|
|||
MDS_DATA=${DIR}/mds
|
||||
MOUNTPT=${MDS_DATA}/mnt
|
||||
OSD_DATA=${DIR}/osd
|
||||
mkdir ${LOG_DIR} ${MON_DATA} ${OSD_DATA} ${MDS_DATA} ${MOUNTPT}
|
||||
RGW_DATA=${DIR}/radosgw
|
||||
mkdir ${LOG_DIR} ${MON_DATA} ${OSD_DATA} ${MDS_DATA} ${MOUNTPT} ${RGW_DATA}
|
||||
MDS_NAME="Z"
|
||||
MON_NAME="a"
|
||||
MGR_NAME="x"
|
||||
MIRROR_ID="m"
|
||||
RGW_ID="r"
|
||||
S3_ACCESS_KEY=2262XNX11FZRR44XWIRD
|
||||
S3_SECRET_KEY=rmtuS1Uj1bIC08QFYGW18GfSHAbkPqdsuYynNudw
|
||||
|
||||
# cluster wide parameters
|
||||
cat >> ${DIR}/ceph.conf <<EOF
|
||||
|
@ -67,6 +71,16 @@ osd journal size = 100
|
|||
osd objectstore = memstore
|
||||
osd class load list = *
|
||||
osd class default list = *
|
||||
|
||||
[client.rgw.${RGW_ID}]
|
||||
rgw dns name = ${HOSTNAME}
|
||||
rgw enable usage log = true
|
||||
rgw usage log tick interval = 1
|
||||
rgw usage log flush threshold = 1
|
||||
rgw usage max shards = 32
|
||||
rgw usage max user shards = 1
|
||||
log file = /var/log/ceph/client.rgw.${RGW_ID}.log
|
||||
rgw frontends = beast port=80
|
||||
EOF
|
||||
|
||||
export CEPH_CONF=${DIR}/ceph.conf
|
||||
|
@ -100,6 +114,12 @@ ceph-mgr --id ${MGR_NAME}
|
|||
ceph auth get-or-create client.rbd-mirror.${MIRROR_ID} mon 'profile rbd-mirror' osd 'profile rbd'
|
||||
rbd-mirror --id ${MIRROR_ID} --log-file ${LOG_DIR}/rbd-mirror.log
|
||||
|
||||
# start an rgw
|
||||
ceph auth get-or-create client.rgw."${RGW_ID}" osd 'allow rwx' mon 'allow rw' -o ${RGW_DATA}/keyring
|
||||
radosgw -n client.rgw."${RGW_ID}" -k ${RGW_DATA}/keyring
|
||||
timeout 60 sh -c 'until [ $(ceph -s | grep -c "rgw:") -eq 1 ]; do echo "waiting for rgw to show up" && sleep 1; done'
|
||||
radosgw-admin user create --uid admin --display-name "Admin User" --caps "buckets=*;users=*;usage=read;metadata=read" --access-key="$S3_ACCESS_KEY" --secret-key="$S3_SECRET_KEY"
|
||||
|
||||
# test the setup
|
||||
ceph --version
|
||||
ceph status
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# Prerequisites
|
||||
|
||||
You must create an admin user like so:
|
||||
|
||||
```
|
||||
radosgw-admin user create --uid admin --display-name "Admin User" --caps "buckets=*;users=*;usage=read;metadata=read;zone=read --access-key=2262XNX11FZRR44XWIRD --secret-key=rmtuS1Uj1bIC08QFYGW18GfSHAbkPqdsuYynNudw
|
||||
```
|
||||
|
||||
Then use the `access_key` and `secret_key` for authentication.
|
||||
|
||||
Snippet usage example:
|
||||
|
||||
```golang
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ceph/go-ceph/rgw/admin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Generate a connection object
|
||||
co, err := admin.New("http://192.168.1.1", "2262XNX11FZRR44XWIRD", "rmtuS1Uj1bIC08QFYGW18GfSHAbkPqdsuYynNudw", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// To enable debug requests
|
||||
// co.Debug = true
|
||||
|
||||
// Get the "admin" user
|
||||
user, err := co.GetUser(context.Background(), "admin")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Print the user display name
|
||||
fmt.Println(user.DisplayName)
|
||||
}
|
||||
```
|
|
@ -0,0 +1,146 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Bucket describes an object store bucket
|
||||
type Bucket struct {
|
||||
Bucket string `json:"bucket" url:"bucket"`
|
||||
Zonegroup string `json:"zonegroup"`
|
||||
PlacementRule string `json:"placement_rule"`
|
||||
ExplicitPlacement struct {
|
||||
DataPool string `json:"data_pool"`
|
||||
DataExtraPool string `json:"data_extra_pool"`
|
||||
IndexPool string `json:"index_pool"`
|
||||
} `json:"explicit_placement"`
|
||||
ID string `json:"id"`
|
||||
Marker string `json:"marker"`
|
||||
IndexType string `json:"index_type"`
|
||||
Owner string `json:"owner"`
|
||||
Ver string `json:"ver"`
|
||||
MasterVer string `json:"master_ver"`
|
||||
Mtime string `json:"mtime"`
|
||||
MaxMarker string `json:"max_marker"`
|
||||
Usage struct {
|
||||
RgwMain struct {
|
||||
Size *uint64 `json:"size"`
|
||||
SizeActual *uint64 `json:"size_actual"`
|
||||
SizeUtilized *uint64 `json:"size_utilized"`
|
||||
SizeKb *uint64 `json:"size_kb"`
|
||||
SizeKbActual *uint64 `json:"size_kb_actual"`
|
||||
SizeKbUtilized *uint64 `json:"size_kb_utilized"`
|
||||
NumObjects *uint64 `json:"num_objects"`
|
||||
} `json:"rgw.main"`
|
||||
RgwMultimeta struct {
|
||||
Size *uint64 `json:"size"`
|
||||
SizeActual *uint64 `json:"size_actual"`
|
||||
SizeUtilized *uint64 `json:"size_utilized"`
|
||||
SizeKb *uint64 `json:"size_kb"`
|
||||
SizeKbActual *uint64 `json:"size_kb_actual"`
|
||||
SizeKbUtilized *uint64 `json:"size_kb_utilized"`
|
||||
NumObjects *uint64 `json:"num_objects"`
|
||||
} `json:"rgw.multimeta"`
|
||||
} `json:"usage"`
|
||||
BucketQuota struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
CheckOnRaw *bool `json:"check_on_raw"`
|
||||
MaxSize *uint64 `json:"max_size"`
|
||||
MaxSizeKb *uint64 `json:"max_size_kb"`
|
||||
MaxObjects *uint64 `json:"max_objects"`
|
||||
} `json:"bucket_quota"`
|
||||
Policy *bool `url:"policy"`
|
||||
PurgeObject *bool `url:"purge-objects"`
|
||||
}
|
||||
|
||||
// Policy describes a bucket policy
|
||||
type Policy struct {
|
||||
ACL struct {
|
||||
ACLUserMap []struct {
|
||||
User string `json:"user"`
|
||||
ACL *int `json:"acl"`
|
||||
} `json:"acl_user_map"`
|
||||
ACLGroupMap []interface{} `json:"acl_group_map"`
|
||||
GrantMap []struct {
|
||||
ID string `json:"id"`
|
||||
Grant struct {
|
||||
Type struct {
|
||||
Type int `json:"type"`
|
||||
} `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Permission struct {
|
||||
Flags int `json:"flags"`
|
||||
} `json:"permission"`
|
||||
Name string `json:"name"`
|
||||
Group *int `json:"group"`
|
||||
URLSpec string `json:"url_spec"`
|
||||
} `json:"grant"`
|
||||
} `json:"grant_map"`
|
||||
} `json:"acl"`
|
||||
Owner struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
} `json:"owner"`
|
||||
}
|
||||
|
||||
// ListBuckets will return the list of all buckets present in the object store
|
||||
func (api *API) ListBuckets(ctx context.Context) ([]string, error) {
|
||||
body, err := api.call(ctx, get, "/bucket", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var s []string
|
||||
err = json.Unmarshal(body, &s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s. %s. %w", unmarshalError, string(body), err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// GetBucketInfo will return various information about a specific token
|
||||
func (api *API) GetBucketInfo(ctx context.Context, bucket Bucket) (*Bucket, error) {
|
||||
body, err := api.call(ctx, get, "/bucket", valueToURLParams(bucket))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref := &Bucket{}
|
||||
err = json.Unmarshal(body, ref)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s. %s. %w", unmarshalError, string(body), err)
|
||||
}
|
||||
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
// GetBucketPolicy - http://docs.ceph.com/docs/mimic/radosgw/adminops/#get-bucket-or-object-policy
|
||||
func (api *API) GetBucketPolicy(ctx context.Context, bucket Bucket) (*Policy, error) {
|
||||
policy := true
|
||||
bucket.Policy = &policy
|
||||
body, err := api.call(ctx, get, "/bucket", valueToURLParams(bucket))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref := &Policy{}
|
||||
err = json.Unmarshal(body, ref)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s. %s. %w", unmarshalError, string(body), err)
|
||||
}
|
||||
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
// RemoveBucket will remove a given token from the object store
|
||||
func (api *API) RemoveBucket(ctx context.Context, bucket Bucket) error {
|
||||
_, err := api.call(ctx, delete, "/bucket", valueToURLParams(bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func (suite *RadosGWTestSuite) TestBucket() {
|
||||
suite.SetupConnection()
|
||||
co, err := New(suite.endpoint, suite.accessKey, suite.secretKey, nil)
|
||||
co.Debug = true
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
suite.T().Run("list buckets", func(t *testing.T) {
|
||||
buckets, err := co.ListBuckets(context.Background())
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), 0, len(buckets))
|
||||
})
|
||||
|
||||
suite.T().Run("info non-existing bucket", func(t *testing.T) {
|
||||
_, err := co.GetBucketInfo(context.Background(), Bucket{Bucket: "foo"})
|
||||
assert.Error(suite.T(), err)
|
||||
assert.True(suite.T(), errors.Is(err, ErrNoSuchBucket), err)
|
||||
})
|
||||
|
||||
suite.T().Run("remove non-existing bucket", func(t *testing.T) {
|
||||
err := co.RemoveBucket(context.Background(), Bucket{Bucket: "foo"})
|
||||
assert.Error(suite.T(), err)
|
||||
// TODO: report to rgw team, this should return NoSuchBucket?
|
||||
assert.True(suite.T(), errors.Is(err, ErrNoSuchKey))
|
||||
})
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/*
|
||||
Package admin contains a set of wrappers around Ceph's RGW Admin Ops API.
|
||||
*/
|
||||
package admin
|
|
@ -0,0 +1,116 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrUserExists - Attempt to create existing user
|
||||
ErrUserExists errorReason = "UserAlreadyExists"
|
||||
|
||||
// ErrNoSuchUser - Attempt to create existing user
|
||||
ErrNoSuchUser errorReason = "NoSuchUser"
|
||||
|
||||
// ErrInvalidAccessKey - Invalid access key specified
|
||||
ErrInvalidAccessKey errorReason = "InvalidAccessKey"
|
||||
|
||||
// ErrInvalidSecretKey - Invalid secret key specified
|
||||
ErrInvalidSecretKey errorReason = "InvalidSecretKey"
|
||||
|
||||
// ErrInvalidKeyType - Invalid key type specified
|
||||
ErrInvalidKeyType errorReason = "InvalidKeyType"
|
||||
|
||||
// ErrKeyExists - Provided access key exists and belongs to another user
|
||||
ErrKeyExists errorReason = "KeyExists"
|
||||
|
||||
// ErrEmailExists - Provided email address exists
|
||||
ErrEmailExists errorReason = "EmailExists"
|
||||
|
||||
// ErrInvalidCapability - Attempt to remove an invalid admin capability
|
||||
ErrInvalidCapability errorReason = "InvalidCapability"
|
||||
|
||||
// ErrSubuserExists - Specified subuser exists
|
||||
ErrSubuserExists errorReason = "SubuserExists"
|
||||
|
||||
// ErrInvalidAccess - Invalid subuser access specified
|
||||
ErrInvalidAccess errorReason = "InvalidAccess"
|
||||
|
||||
// ErrIndexRepairFailed - Bucket index repair failed
|
||||
ErrIndexRepairFailed errorReason = "IndexRepairFailed"
|
||||
|
||||
// ErrBucketNotEmpty - Attempted to delete non-empty bucket
|
||||
ErrBucketNotEmpty errorReason = "BucketNotEmpty"
|
||||
|
||||
// ErrObjectRemovalFailed - Unable to remove objects
|
||||
ErrObjectRemovalFailed errorReason = "ObjectRemovalFailed"
|
||||
|
||||
// ErrBucketUnlinkFailed - Unable to unlink bucket from specified user
|
||||
ErrBucketUnlinkFailed errorReason = "BucketUnlinkFailed"
|
||||
|
||||
// ErrBucketLinkFailed - Unable to link bucket to specified user
|
||||
ErrBucketLinkFailed errorReason = "BucketLinkFailed"
|
||||
|
||||
// ErrNoSuchObject - Specified object does not exist
|
||||
ErrNoSuchObject errorReason = "NoSuchObject"
|
||||
|
||||
// ErrIncompleteBody - Either bucket was not specified for a bucket policy request or bucket and object were not specified for an object policy request.
|
||||
ErrIncompleteBody errorReason = "IncompleteBody"
|
||||
|
||||
// ErrNoSuchCap - User does not possess specified capability
|
||||
ErrNoSuchCap errorReason = "NoSuchCap"
|
||||
|
||||
// ErrInternalError - Internal server error.
|
||||
ErrInternalError errorReason = "InternalError"
|
||||
|
||||
// ErrAccessDenied - Access denied.
|
||||
ErrAccessDenied errorReason = "AccessDenied"
|
||||
|
||||
// ErrNoSuchBucket - Bucket does not exist.
|
||||
ErrNoSuchBucket errorReason = "NoSuchBucket"
|
||||
|
||||
// ErrNoSuchKey - No such access key.
|
||||
ErrNoSuchKey errorReason = "NoSuchKey"
|
||||
|
||||
// ErrInvalidArgument - Invalid argument.
|
||||
ErrInvalidArgument errorReason = "InvalidArgument"
|
||||
|
||||
// ErrUnknown - reports an unknown error
|
||||
ErrUnknown errorReason = "Unknown"
|
||||
|
||||
unmarshalError = "failed to unmarshal radosgw http response"
|
||||
)
|
||||
|
||||
var (
|
||||
errMissingUserID = errors.New("missing user ID")
|
||||
errMissingUserDisplayName = errors.New("missing user display name")
|
||||
)
|
||||
|
||||
// errorReason is the reason of the error
|
||||
type errorReason string
|
||||
|
||||
// statusError is the API response when an error occurs
|
||||
type statusError struct {
|
||||
Code string `json:"Code,omitempty"`
|
||||
RequestID string `json:"RequestId,omitempty"`
|
||||
HostID string `json:"HostId,omitempty"`
|
||||
}
|
||||
|
||||
func handleStatusError(decodedResponse []byte) error {
|
||||
statusError := statusError{}
|
||||
err := json.Unmarshal(decodedResponse, &statusError)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s. %s. %w", unmarshalError, string(decodedResponse), err)
|
||||
}
|
||||
|
||||
return statusError
|
||||
}
|
||||
|
||||
func (e errorReason) Error() string { return string(e) }
|
||||
|
||||
// Is determines whether the error is known to be reported
|
||||
func (e statusError) Is(target error) bool { return target == errorReason(e.Code) }
|
||||
|
||||
// Error returns non-empty string if there was an error.
|
||||
func (e statusError) Error() string { return fmt.Sprintf("%s %s %s", e.Code, e.RequestID, e.HostID) }
|
|
@ -0,0 +1,18 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeGetUserError = []byte(`{"Code":"NoSuchUser","RequestId":"tx0000000000000000005a9-00608957a2-10496-my-store","HostId":"10496-my-store-my-store"}`)
|
||||
)
|
||||
|
||||
func TestHandleStatusError(t *testing.T) {
|
||||
err := handleStatusError(fakeGetUserError)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, ErrNoSuchUser), err)
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
authRegion = "default"
|
||||
service = "s3"
|
||||
connectionTimeout = time.Second * 3
|
||||
post verbHTTP = "POST"
|
||||
put verbHTTP = "PUT"
|
||||
get verbHTTP = "GET"
|
||||
delete verbHTTP = "DELETE"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoEndpoint = errors.New("endpoint not set")
|
||||
errNoAccessKey = errors.New("access key not set")
|
||||
errNoSecretKey = errors.New("secret key not set")
|
||||
)
|
||||
|
||||
type verbHTTP string
|
||||
|
||||
// API struct for New Client
|
||||
type API struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Endpoint string
|
||||
HTTPClient *http.Client
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// New returns client for Ceph RGW
|
||||
func New(endpoint, accessKey, secretKey string, httpClient *http.Client) (*API, error) {
|
||||
// validate endpoint
|
||||
if endpoint == "" {
|
||||
return nil, errNoEndpoint
|
||||
}
|
||||
|
||||
// validate access key
|
||||
if accessKey == "" {
|
||||
return nil, errNoAccessKey
|
||||
}
|
||||
|
||||
// validate secret key
|
||||
if secretKey == "" {
|
||||
return nil, errNoSecretKey
|
||||
}
|
||||
|
||||
// set http client, if TLS is desired it will have to be passed with an http client
|
||||
if httpClient == nil {
|
||||
httpClient = &http.Client{Timeout: connectionTimeout}
|
||||
}
|
||||
|
||||
return &API{
|
||||
Endpoint: endpoint,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
HTTPClient: httpClient,
|
||||
Debug: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// call makes request to the RGW Admin Ops API
|
||||
func (api *API) call(ctx context.Context, verb verbHTTP, path string, args url.Values) (body []byte, err error) {
|
||||
// Verify the endpoint URL is correct
|
||||
url, err := url.Parse(buildQueryPath(api.Endpoint, path, args.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build request
|
||||
request, err := http.NewRequestWithContext(ctx, string(verb), url.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build S3 authentication
|
||||
cred := credentials.NewStaticCredentials(api.AccessKey, api.SecretKey, "")
|
||||
signer := v4.NewSigner(cred)
|
||||
// This was present in https://github.com/IrekFasikhov/go-rgwadmin/ but it seems that the lib works without it
|
||||
// Let's keep it here just in case something shows up
|
||||
// signer.DisableRequestBodyOverwrite = true
|
||||
|
||||
// Sign in S3
|
||||
_, err = signer.Sign(request, nil, service, authRegion, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Print request if Debug is enabled
|
||||
if api.Debug {
|
||||
dump, err := httputil.DumpRequestOut(request, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("\n%s\n", string(dump))
|
||||
}
|
||||
|
||||
// Send HTTP request
|
||||
resp, err := api.HTTPClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Print response if Debug is enabled
|
||||
if api.Debug {
|
||||
dump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("\n%s\n", string(dump))
|
||||
}
|
||||
|
||||
// Decode HTTP response
|
||||
decodedResponse, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Body = ioutil.NopCloser(bytes.NewBuffer(decodedResponse))
|
||||
|
||||
// Handle error in response
|
||||
if resp.StatusCode >= 300 {
|
||||
return nil, handleStatusError(decodedResponse)
|
||||
}
|
||||
|
||||
return decodedResponse, nil
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type RadosGWTestSuite struct {
|
||||
suite.Suite
|
||||
endpoint string
|
||||
accessKey string
|
||||
secretKey string
|
||||
}
|
||||
|
||||
func TestRadosGWTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(RadosGWTestSuite))
|
||||
}
|
||||
|
||||
func (suite *RadosGWTestSuite) SetupConnection() {
|
||||
suite.accessKey = "2262XNX11FZRR44XWIRD"
|
||||
suite.secretKey = "rmtuS1Uj1bIC08QFYGW18GfSHAbkPqdsuYynNudw"
|
||||
hostname := os.Getenv("HOSTNAME")
|
||||
endpoint := hostname
|
||||
if hostname != "test_ceph_aio" {
|
||||
endpoint = "test_ceph_a"
|
||||
}
|
||||
suite.endpoint = "http://" + endpoint
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type args struct {
|
||||
endpoint string
|
||||
accessKey string
|
||||
secretKey string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *API
|
||||
wantErr error
|
||||
}{
|
||||
{"no endpoint", args{}, nil, errNoEndpoint},
|
||||
{"no accessKey", args{endpoint: "http://192.168.0.1"}, nil, errNoAccessKey},
|
||||
{"no secretKey", args{endpoint: "http://192.168.0.1", accessKey: "foo"}, nil, errNoSecretKey},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := New(tt.args.endpoint, tt.args.accessKey, tt.args.secretKey, nil)
|
||||
if tt.wantErr != err {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Usage struct
|
||||
type Usage struct {
|
||||
Entries []struct {
|
||||
User string `json:"user"`
|
||||
Buckets []struct {
|
||||
Bucket string `json:"bucket"`
|
||||
Time string `json:"time"`
|
||||
Epoch uint64 `json:"epoch"`
|
||||
Owner string `json:"owner"`
|
||||
Categories []struct {
|
||||
Category string `json:"category"`
|
||||
BytesSent uint64 `json:"bytes_sent"`
|
||||
BytesReceived uint64 `json:"bytes_received"`
|
||||
Ops uint64 `json:"ops"`
|
||||
SuccessfulOps uint64 `json:"successful_ops"`
|
||||
} `json:"categories"`
|
||||
} `json:"buckets"`
|
||||
} `json:"entries"`
|
||||
Summary []struct {
|
||||
User string `json:"user"`
|
||||
Categories []struct {
|
||||
Category string `json:"category"`
|
||||
BytesSent uint64 `json:"bytes_sent"`
|
||||
BytesReceived uint64 `json:"bytes_received"`
|
||||
Ops uint64 `json:"ops"`
|
||||
SuccessfulOps uint64 `json:"successful_ops"`
|
||||
} `json:"categories"`
|
||||
Total struct {
|
||||
BytesSent uint64 `json:"bytes_sent"`
|
||||
BytesReceived uint64 `json:"bytes_received"`
|
||||
Ops uint64 `json:"ops"`
|
||||
SuccessfulOps uint64 `json:"successful_ops"`
|
||||
} `json:"total"`
|
||||
} `json:"summary"`
|
||||
Start string `url:"start"` //Example: 2012-09-25 16:00:00
|
||||
End string `url:"end"`
|
||||
ShowEntries *bool `url:"show-entries"`
|
||||
ShowSummary *bool `url:"show-summary"`
|
||||
RemoveAll *bool `url:"remove-all"` //true
|
||||
}
|
||||
|
||||
// GetUsage request bandwidth usage information on the object store
|
||||
func (api *API) GetUsage(ctx context.Context, usage Usage) (*Usage, error) {
|
||||
body, err := api.call(ctx, get, "/usage", valueToURLParams(usage))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := &Usage{}
|
||||
err = json.Unmarshal(body, u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s. %s. %w", unmarshalError, string(body), err)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// TrimUsage removes bandwidth usage information. With no dates specified, removes all usage information.
|
||||
func (api *API) TrimUsage(ctx context.Context, usage Usage) error {
|
||||
_, err := api.call(ctx, delete, "/usage", valueToURLParams(usage))
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func (suite *RadosGWTestSuite) TestUsage() {
|
||||
suite.SetupConnection()
|
||||
co, err := New(suite.endpoint, suite.accessKey, suite.secretKey, nil)
|
||||
co.Debug = true
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
suite.T().Run("get usage", func(t *testing.T) {
|
||||
pTrue := true
|
||||
usage, err := co.GetUsage(context.Background(), Usage{ShowSummary: &pTrue})
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.NotEmpty(t, usage)
|
||||
})
|
||||
|
||||
suite.T().Run("trim usage", func(t *testing.T) {
|
||||
pFalse := false
|
||||
_, err := co.GetUsage(context.Background(), Usage{RemoveAll: &pFalse})
|
||||
assert.NoError(suite.T(), err)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// User is GO representation of the json output of a user creation
|
||||
type User struct {
|
||||
ID string `json:"user_id" url:"uid"`
|
||||
DisplayName string `json:"display_name" url:"display-name"`
|
||||
Email string `json:"email" url:"email"`
|
||||
Suspended *int `json:"suspended" url:"suspended"`
|
||||
MaxBuckets *int `json:"max_buckets" url:"max-buckets"`
|
||||
Subusers []interface{} `json:"subusers"`
|
||||
Keys []struct {
|
||||
User string `json:"user"`
|
||||
AccessKey string `json:"access_key" url:"access-key"`
|
||||
SecretKey string `json:"secret_key" url:"secret-key"`
|
||||
} `json:"keys"`
|
||||
SwiftKeys []interface{} `json:"swift_keys"`
|
||||
Caps []struct {
|
||||
Type string `json:"type"`
|
||||
Perm string `json:"perm"`
|
||||
} `json:"caps"`
|
||||
OpMask string `json:"op_mask"`
|
||||
DefaultPlacement string `json:"default_placement"`
|
||||
DefaultStorageClass string `json:"default_storage_class"`
|
||||
PlacementTags []interface{} `json:"placement_tags"`
|
||||
BucketQuota struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
CheckOnRaw *bool `json:"check_on_raw"`
|
||||
MaxSize *int `json:"max_size"`
|
||||
MaxSizeKb *int `json:"max_size_kb"`
|
||||
MaxObjects *int `json:"max_objects"`
|
||||
} `json:"bucket_quota"`
|
||||
UserQuota struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
CheckOnRaw *bool `json:"check_on_raw"`
|
||||
MaxSize *int `json:"max_size"`
|
||||
MaxSizeKb *int `json:"max_size_kb"`
|
||||
MaxObjects *int `json:"max_objects"`
|
||||
} `json:"user_quota"`
|
||||
TempURLKeys []interface{} `json:"temp_url_keys"`
|
||||
Type string `json:"type"`
|
||||
MfaIds []interface{} `json:"mfa_ids"`
|
||||
KeyType string `url:"key-type"`
|
||||
Tenant string `url:"tenant"`
|
||||
GenerateKey *bool `url:"generate-key"`
|
||||
PurgeData *int `url:"purge-data"`
|
||||
}
|
||||
|
||||
// GetUser retrieves a given object store user
|
||||
func (api *API) GetUser(ctx context.Context, user User) (*User, error) {
|
||||
if user.ID == "" {
|
||||
return nil, errMissingUserID
|
||||
}
|
||||
|
||||
body, err := api.call(ctx, get, "/user", valueToURLParams(user))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &User{}
|
||||
err = json.Unmarshal(body, u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s. %s. %w", unmarshalError, string(body), err)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// GetUsers lists all object store users
|
||||
func (api *API) GetUsers(ctx context.Context) (*[]string, error) {
|
||||
body, err := api.call(ctx, get, "/metadata/user", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var users *[]string
|
||||
err = json.Unmarshal(body, &users)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s. %s. %w", unmarshalError, string(body), err)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a user in the object store
|
||||
func (api *API) CreateUser(ctx context.Context, user User) (*User, error) {
|
||||
if user.ID == "" {
|
||||
return nil, errMissingUserID
|
||||
}
|
||||
if user.DisplayName == "" {
|
||||
return nil, errMissingUserDisplayName
|
||||
}
|
||||
|
||||
// Send request
|
||||
body, err := api.call(ctx, put, "/user", valueToURLParams(user))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal response into Go type
|
||||
u := &User{}
|
||||
err = json.Unmarshal(body, u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s. %s. %w", unmarshalError, string(body), err)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// RemoveUser remove an user from the object store
|
||||
func (api *API) RemoveUser(ctx context.Context, user User) error {
|
||||
if user.ID == "" {
|
||||
return errMissingUserID
|
||||
}
|
||||
|
||||
_, err := api.call(ctx, delete, "/user", valueToURLParams(user))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ModifyUser - http://docs.ceph.com/docs/latest/radosgw/adminops/#modify-user
|
||||
func (api *API) ModifyUser(ctx context.Context, user User) (*User, error) {
|
||||
if user.ID == "" {
|
||||
return nil, errMissingUserID
|
||||
}
|
||||
|
||||
body, err := api.call(ctx, post, "/user", valueToURLParams(user))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &User{}
|
||||
err = json.Unmarshal(body, u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s. %s. %w", unmarshalError, string(body), err)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeUserResponse = []byte(`
|
||||
{
|
||||
"tenant": "",
|
||||
"user_id": "dashboard-admin",
|
||||
"display_name": "dashboard-admin",
|
||||
"email": "",
|
||||
"suspended": 0,
|
||||
"max_buckets": 1000,
|
||||
"subusers": [],
|
||||
"keys": [
|
||||
{
|
||||
"user": "dashboard-admin",
|
||||
"access_key": "4WD1FGM5PXKLC97YC0SZ",
|
||||
"secret_key": "YSaT5bEcJTjBJCDG5yvr2NhGQ9xzoTIg8B1gQHa3"
|
||||
}
|
||||
],
|
||||
"swift_keys": [],
|
||||
"caps": [],
|
||||
"op_mask": "read, write, delete",
|
||||
"system": "true",
|
||||
"admin": "false",
|
||||
"default_placement": "",
|
||||
"default_storage_class": "",
|
||||
"placement_tags": [],
|
||||
"bucket_quota": {
|
||||
"enabled": false,
|
||||
"check_on_raw": false,
|
||||
"max_size": -1,
|
||||
"max_size_kb": 0,
|
||||
"max_objects": -1
|
||||
},
|
||||
"user_quota": {
|
||||
"enabled": false,
|
||||
"check_on_raw": false,
|
||||
"max_size": -1,
|
||||
"max_size_kb": 0,
|
||||
"max_objects": -1
|
||||
},
|
||||
"temp_url_keys": [],
|
||||
"type": "rgw",
|
||||
"mfa_ids": []
|
||||
}`)
|
||||
)
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
u := &User{}
|
||||
err := json.Unmarshal(fakeUserResponse, &u)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func (suite *RadosGWTestSuite) TestUser() {
|
||||
suite.SetupConnection()
|
||||
co, err := New(suite.endpoint, suite.accessKey, suite.secretKey, nil)
|
||||
co.Debug = true
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
suite.T().Run("fail to create user since no UID provided", func(t *testing.T) {
|
||||
_, err = co.CreateUser(context.Background(), User{Email: "leseb@example.com"})
|
||||
assert.Error(suite.T(), err)
|
||||
assert.EqualError(suite.T(), err, errMissingUserID.Error())
|
||||
})
|
||||
|
||||
suite.T().Run("fail to create user since no no display name provided", func(t *testing.T) {
|
||||
_, err = co.CreateUser(context.Background(), User{ID: "leseb", Email: "leseb@example.com"})
|
||||
assert.Error(suite.T(), err)
|
||||
assert.EqualError(suite.T(), err, errMissingUserDisplayName.Error())
|
||||
})
|
||||
|
||||
suite.T().Run("user creation success", func(t *testing.T) {
|
||||
user, err := co.CreateUser(context.Background(), User{ID: "leseb", DisplayName: "This is leseb", Email: "leseb@example.com"})
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), "leseb@example.com", user.Email)
|
||||
})
|
||||
|
||||
suite.T().Run("get user leseb", func(t *testing.T) {
|
||||
user, err := co.GetUser(context.Background(), User{ID: "leseb"})
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), "leseb@example.com", user.Email)
|
||||
})
|
||||
|
||||
suite.T().Run("modify user email", func(t *testing.T) {
|
||||
user, err := co.ModifyUser(context.Background(), User{ID: "leseb", Email: "leseb@leseb.com"})
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), "leseb@leseb.com", user.Email)
|
||||
})
|
||||
|
||||
suite.T().Run("user already exists", func(t *testing.T) {
|
||||
_, err := co.CreateUser(context.Background(), User{ID: "admin", DisplayName: "Admin user"})
|
||||
assert.Error(suite.T(), err)
|
||||
assert.True(suite.T(), errors.Is(err, ErrUserExists), fmt.Sprintf("%+v", err))
|
||||
})
|
||||
|
||||
suite.T().Run("get users", func(t *testing.T) {
|
||||
users, err := co.GetUsers(context.Background())
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), 2, len(*users))
|
||||
})
|
||||
|
||||
suite.T().Run("remove user", func(t *testing.T) {
|
||||
err = co.RemoveUser(context.Background(), User{ID: "leseb"})
|
||||
assert.NoError(suite.T(), err)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
queryAdminPath = "/admin"
|
||||
)
|
||||
|
||||
func buildQueryPath(endpoint, path, args string) string {
|
||||
return fmt.Sprintf("%s%s%s?%s", endpoint, queryAdminPath, path, args)
|
||||
}
|
||||
|
||||
// valueToURLParams encodes structs into URL query parameters.
|
||||
func valueToURLParams(i interface{}) url.Values {
|
||||
values := url.Values{}
|
||||
|
||||
// Always return json
|
||||
values.Add("format", "json")
|
||||
|
||||
getReflect(i, &values)
|
||||
return values
|
||||
}
|
||||
|
||||
func getReflect(i interface{}, values *url.Values) {
|
||||
t := reflect.TypeOf(i)
|
||||
v := reflect.ValueOf(i)
|
||||
|
||||
for b := 0; b < v.NumField(); b++ {
|
||||
v2 := v.Field(b)
|
||||
name := t.Field(b).Tag.Get("url")
|
||||
|
||||
for _, name := range strings.Split(name, ",") {
|
||||
if v2.Kind() == reflect.Struct {
|
||||
getReflect(v2.Interface(), values)
|
||||
}
|
||||
|
||||
if v2.Kind() == reflect.Slice {
|
||||
for i := 0; i < v2.Len(); i++ {
|
||||
item := v2.Index(i)
|
||||
getReflect(item.Interface(), values)
|
||||
}
|
||||
}
|
||||
|
||||
if v2.Kind() == reflect.String ||
|
||||
v2.Kind() == reflect.Bool ||
|
||||
v2.Kind() == reflect.Int {
|
||||
|
||||
_v2 := fmt.Sprint(v2)
|
||||
if len(_v2) > 0 && len(name) > 0 {
|
||||
values.Add(name, _v2)
|
||||
}
|
||||
}
|
||||
|
||||
if v2.Kind() == reflect.Ptr && v2.IsValid() && !v2.IsNil() {
|
||||
_v2 := fmt.Sprint(v2.Elem())
|
||||
values.Add(name, _v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getDefaultValue() url.Values {
|
||||
values := url.Values{}
|
||||
values.Add("format", "json")
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func TestBuildQueryPath(t *testing.T) {
|
||||
queryPath := buildQueryPath("http://192.168.0.1", "/user", getDefaultValue().Encode())
|
||||
assert.Equal(t, "http://192.168.0.1/admin/user?format=json", queryPath)
|
||||
}
|
||||
|
||||
func TestValueToURLParams(t *testing.T) {
|
||||
type args struct {
|
||||
i interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"default", args{User{ID: "leseb"}}, "format=json&uid=leseb"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := valueToURLParams(tt.args.i)
|
||||
if !reflect.DeepEqual(got.Encode(), tt.want) {
|
||||
t.Errorf("valueToURLParams() = %v, want %v", got.Encode(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/*
|
||||
Package rgw contains sub-packages pertaining to the administration of RGW. It currently contains no functionally.
|
||||
*/
|
||||
package rgw
|
Loading…
Reference in New Issue