171 lines
4.1 KiB
Go
171 lines
4.1 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/KubelanCloud/kks-provider-plugin/pkg/csi/provisioner"
|
|
)
|
|
|
|
type ClientConfig struct {
|
|
BaseURL string
|
|
Token string
|
|
Timeout time.Duration
|
|
}
|
|
|
|
type Client struct {
|
|
baseURL string
|
|
token string
|
|
client *http.Client
|
|
}
|
|
|
|
func NewClient(cfg ClientConfig) *Client {
|
|
timeout := cfg.Timeout
|
|
if timeout <= 0 {
|
|
timeout = 30 * time.Second
|
|
}
|
|
return &Client{
|
|
baseURL: strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/"),
|
|
token: strings.TrimSpace(cfg.Token),
|
|
client: &http.Client{Timeout: timeout},
|
|
}
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) ClusterInfo(ctx context.Context) (provisioner.ClusterInfo, error) {
|
|
var out provisioner.ClusterInfo
|
|
if err := c.doJSON(ctx, http.MethodGet, "/v1/cluster", nil, &out); err != nil {
|
|
return provisioner.ClusterInfo{}, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) CreateVolume(ctx context.Context, req provisioner.CreateVolumeRequest) (provisioner.Volume, error) {
|
|
var out provisioner.Volume
|
|
if err := c.doJSON(ctx, http.MethodPost, "/v1/volumes", req, &out); err != nil {
|
|
return provisioner.Volume{}, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) DeleteVolume(ctx context.Context, volumeID string) error {
|
|
err := c.doJSON(ctx, http.MethodDelete, "/v1/volumes/"+escapePath(volumeID), nil, nil)
|
|
if isNotFound(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (c *Client) VolumeExists(ctx context.Context, volumeID string) (bool, error) {
|
|
var out provisioner.VolumeExistsResponse
|
|
err := c.doJSON(ctx, http.MethodGet, "/v1/volumes/"+escapePath(volumeID), nil, &out)
|
|
if err != nil {
|
|
if isNotFound(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
return out.Exists, nil
|
|
}
|
|
|
|
func (c *Client) PublishVolume(ctx context.Context, volumeID, nodeID string) (provisioner.PublishVolumeResponse, error) {
|
|
var out provisioner.PublishVolumeResponse
|
|
req := provisioner.PublishVolumeRequest{NodeID: nodeID}
|
|
path := "/v1/volumes/" + escapePath(volumeID) + "/publish"
|
|
if err := c.doJSON(ctx, http.MethodPost, path, req, &out); err != nil {
|
|
return provisioner.PublishVolumeResponse{}, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) UnpublishVolume(ctx context.Context, volumeID, nodeID string) error {
|
|
req := provisioner.UnpublishVolumeRequest{
|
|
NodeID: nodeID,
|
|
VolumeID: volumeID,
|
|
}
|
|
return c.doJSON(ctx, http.MethodPost, "/v1/volumes/unpublish", req, nil)
|
|
}
|
|
|
|
func (c *Client) doJSON(ctx context.Context, method, path string, reqBody any, respBody any) error {
|
|
var body io.Reader
|
|
if reqBody != nil {
|
|
raw, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
body = bytes.NewReader(raw)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
if reqBody != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
if c.token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
|
}
|
|
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("request %s %s: %w", method, path, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return &HTTPError{
|
|
StatusCode: resp.StatusCode,
|
|
Method: method,
|
|
Path: path,
|
|
Body: strings.TrimSpace(string(raw)),
|
|
}
|
|
}
|
|
|
|
if respBody == nil || len(raw) == 0 {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(raw, respBody); err != nil {
|
|
return fmt.Errorf("decode response: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type HTTPError struct {
|
|
StatusCode int
|
|
Method string
|
|
Path string
|
|
Body string
|
|
}
|
|
|
|
func (e *HTTPError) Error() string {
|
|
if e.Body == "" {
|
|
return fmt.Sprintf("%s %s: status %d", e.Method, e.Path, e.StatusCode)
|
|
}
|
|
return fmt.Sprintf("%s %s: status %d: %s", e.Method, e.Path, e.StatusCode, e.Body)
|
|
}
|
|
|
|
func isNotFound(err error) bool {
|
|
httpErr, ok := err.(*HTTPError)
|
|
return ok && httpErr.StatusCode == http.StatusNotFound
|
|
}
|
|
|
|
func escapePath(value string) string {
|
|
return strings.ReplaceAll(value, "/", "%2F")
|
|
}
|