ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
1 changed files with 145336 additions and 0 deletions
246
packages/versitygw-terraform-provider/internal/client/client.go
Normal file
246
packages/versitygw-terraform-provider/internal/client/client.go
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Endpoint string
|
||||
Region string
|
||||
AdminAccessKey string
|
||||
AdminSecretKey string
|
||||
InsecureTLS bool
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
endpoint *url.URL
|
||||
region string
|
||||
creds aws.Credentials
|
||||
http *http.Client
|
||||
signer *v4.Signer
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
Access string `xml:"Access"`
|
||||
Secret string `xml:"Secret,omitempty"`
|
||||
Role string `xml:"Role"`
|
||||
UserID int64 `xml:"UserID"`
|
||||
GroupID int64 `xml:"GroupID"`
|
||||
ProjectID int64 `xml:"ProjectID"`
|
||||
}
|
||||
|
||||
type MutableProps struct {
|
||||
XMLName xml.Name `xml:"MutableProps"`
|
||||
Secret *string `xml:"Secret,omitempty"`
|
||||
Role string `xml:"Role,omitempty"`
|
||||
UserID *int64 `xml:"UserID,omitempty"`
|
||||
GroupID *int64 `xml:"GroupID,omitempty"`
|
||||
ProjectID *int64 `xml:"ProjectID,omitempty"`
|
||||
}
|
||||
|
||||
type ListUserAccountsResult struct {
|
||||
XMLName xml.Name `xml:"ListUserAccountsResult"`
|
||||
Accounts []Account `xml:"Accounts"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
XMLName xml.Name `xml:"Error"`
|
||||
Code string `xml:"Code"`
|
||||
Message string `xml:"Message"`
|
||||
RequestID string `xml:"RequestId"`
|
||||
HostID string `xml:"HostId"`
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Status int
|
||||
Code string
|
||||
Message string
|
||||
RequestID string
|
||||
HostID string
|
||||
RawBody string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
if e.Message == "" {
|
||||
return fmt.Sprintf("admin API status %d: %s", e.Status, e.Code)
|
||||
}
|
||||
return fmt.Sprintf("admin API status %d: %s: %s", e.Status, e.Code, e.Message)
|
||||
}
|
||||
|
||||
func IsErrorCode(err error, code string) bool {
|
||||
var apiErr *APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
return false
|
||||
}
|
||||
return apiErr.Code == code
|
||||
}
|
||||
|
||||
func New(cfg Config) (*Client, error) {
|
||||
if cfg.Endpoint == "" {
|
||||
return nil, fmt.Errorf("endpoint is required")
|
||||
}
|
||||
if cfg.Region == "" {
|
||||
cfg.Region = "us-east-1"
|
||||
}
|
||||
if cfg.AdminAccessKey == "" {
|
||||
return nil, fmt.Errorf("admin access key is required")
|
||||
}
|
||||
if cfg.AdminSecretKey == "" {
|
||||
return nil, fmt.Errorf("admin secret key is required")
|
||||
}
|
||||
|
||||
endpoint, err := url.Parse(cfg.Endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse endpoint: %w", err)
|
||||
}
|
||||
if endpoint.Scheme == "" || endpoint.Host == "" {
|
||||
return nil, fmt.Errorf("endpoint must include scheme and host")
|
||||
}
|
||||
|
||||
return &Client{
|
||||
endpoint: endpoint,
|
||||
region: cfg.Region,
|
||||
creds: aws.Credentials{
|
||||
AccessKeyID: cfg.AdminAccessKey,
|
||||
SecretAccessKey: cfg.AdminSecretKey,
|
||||
},
|
||||
http: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.InsecureTLS}, //nolint:gosec
|
||||
},
|
||||
},
|
||||
signer: v4.NewSigner(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateUser(ctx context.Context, account Account) error {
|
||||
body, err := xml.Marshal(account)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal create-user request: %w", err)
|
||||
}
|
||||
|
||||
_, err = c.do(ctx, http.MethodPatch, "/create-user", nil, body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) UpdateUser(ctx context.Context, access string, props MutableProps) error {
|
||||
body, err := xml.Marshal(props)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal update-user request: %w", err)
|
||||
}
|
||||
|
||||
query := url.Values{"access": []string{access}}
|
||||
_, err = c.do(ctx, http.MethodPatch, "/update-user", query, body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) DeleteUser(ctx context.Context, access string) error {
|
||||
query := url.Values{"access": []string{access}}
|
||||
_, err := c.do(ctx, http.MethodPatch, "/delete-user", query, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) GetUser(ctx context.Context, access string) (Account, bool, error) {
|
||||
accounts, err := c.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return Account{}, false, err
|
||||
}
|
||||
for _, account := range accounts {
|
||||
if account.Access == access {
|
||||
return account, true, nil
|
||||
}
|
||||
}
|
||||
return Account{}, false, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListUsers(ctx context.Context) ([]Account, error) {
|
||||
body, err := c.do(ctx, http.MethodPatch, "/list-users", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result ListUserAccountsResult
|
||||
if err := xml.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse list-users response: %w", err)
|
||||
}
|
||||
return result.Accounts, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path string, query url.Values, body []byte) ([]byte, error) {
|
||||
endpoint := *c.endpoint
|
||||
endpoint.Path = strings.TrimRight(c.endpoint.Path, "/") + path
|
||||
endpoint.RawQuery = query.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
|
||||
hashedPayload := sha256.Sum256(body)
|
||||
hexPayload := hex.EncodeToString(hashedPayload[:])
|
||||
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
|
||||
if len(body) > 0 {
|
||||
req.Header.Set("Content-Type", "application/xml")
|
||||
}
|
||||
|
||||
if err := c.signer.SignHTTP(ctx, c.creds, req, hexPayload, "s3", c.region, time.Now()); err != nil {
|
||||
return nil, fmt.Errorf("sign request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, parseError(resp.StatusCode, respBody)
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
func parseError(status int, body []byte) error {
|
||||
var apiErr ErrorResponse
|
||||
if err := xml.Unmarshal(body, &apiErr); err != nil {
|
||||
return &APIError{
|
||||
Status: status,
|
||||
RawBody: strings.TrimSpace(string(body)),
|
||||
Message: strings.TrimSpace(string(body)),
|
||||
}
|
||||
}
|
||||
if apiErr.Code == "" && apiErr.Message == "" {
|
||||
return &APIError{
|
||||
Status: status,
|
||||
RawBody: strings.TrimSpace(string(body)),
|
||||
Message: strings.TrimSpace(string(body)),
|
||||
}
|
||||
}
|
||||
return &APIError{
|
||||
Status: status,
|
||||
Code: apiErr.Code,
|
||||
Message: apiErr.Message,
|
||||
RequestID: apiErr.RequestID,
|
||||
HostID: apiErr.HostID,
|
||||
RawBody: strings.TrimSpace(string(body)),
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue