You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
decort-golang-sdk/client.go

378 lines
9.2 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package decortsdk
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/google/go-querystring/query"
"repository.basistech.ru/BASIS/decort-golang-sdk/config"
"repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants"
"repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators"
"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi"
"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker"
)
// DecortClient is HTTP-client for platform
type DecortClient struct {
decortURL string
client *http.Client
cfg config.Config
expiryTime time.Time
mutex *sync.Mutex
}
// Сlient builder
func New(cfg config.Config) *DecortClient {
if cfg.Retries == 0 {
cfg.Retries = 5
}
var expiryTime time.Time
if cfg.Token != "" {
expiryTime = time.Now().AddDate(0, 0, 1)
}
return &DecortClient{
decortURL: cfg.DecortURL,
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: cfg.SSLSkipVerify,
},
},
},
cfg: trimConfig(&cfg),
expiryTime: expiryTime,
mutex: &sync.Mutex{},
}
}
// CloudAPI builder
func (dc *DecortClient) CloudAPI() *cloudapi.CloudAPI {
return cloudapi.New(dc)
}
// CloudBroker builder
func (dc *DecortClient) CloudBroker() *cloudbroker.CloudBroker {
return cloudbroker.New(dc)
}
// DecortApiCall method for sending requests to the platform
func (dc *DecortClient) DecortApiCall(ctx context.Context, method, url string, params interface{}) ([]byte, error) {
var body *bytes.Buffer
var ctype string
byteSlice, ok := params.([]byte)
if ok {
body = bytes.NewBuffer(byteSlice)
// ctype = "application/x-iso9660-image"
ctype = "application/octet-stream"
} else {
values, err := query.Values(params)
if err != nil {
return nil, err
}
body = bytes.NewBufferString(values.Encode())
}
req, err := http.NewRequestWithContext(ctx, method, dc.decortURL+constants.RESTMACHINE+url, body)
if err != nil {
return nil, err
}
// get token
if err = dc.getToken(ctx); err != nil {
return nil, err
}
// perform request
respBytes, err := dc.do(req, ctype)
if err != nil {
return nil, err
}
return respBytes, err
}
// DecortApiCallMP method for sending requests to the platform
func (dc *DecortClient) DecortApiCallMP(ctx context.Context, method, url string, params interface{}) ([]byte, error) {
body, ctype, err := multiPartReq(params)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, method, dc.decortURL+constants.RESTMACHINE+url, body)
if err != nil {
return nil, err
}
// get token
if err = dc.getToken(ctx); err != nil {
return nil, err
}
// perform request
respBytes, err := dc.do(req, ctype)
if err != nil {
return nil, err
}
return respBytes, err
}
func (dc *DecortClient) getToken(ctx context.Context) error {
dc.mutex.Lock()
defer dc.mutex.Unlock()
// new token is not needed
if dc.cfg.Token != "" && !time.Now().After(dc.expiryTime) {
return nil
}
// set up request headers and body
body := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s&response_type=id_token", dc.cfg.AppID, dc.cfg.AppSecret)
bodyReader := strings.NewReader(body)
dc.cfg.SSOURL = strings.TrimSuffix(dc.cfg.SSOURL, "/")
req, _ := http.NewRequestWithContext(ctx, "POST", dc.cfg.SSOURL+"/v1/oauth/access_token", bodyReader)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
// request token
resp, err := dc.client.Do(req)
if err != nil || resp == nil {
return fmt.Errorf("cannot get token: %w", err)
}
defer resp.Body.Close()
var tokenBytes []byte
tokenBytes, err = io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("cannot get token: %w", err)
}
if resp.StatusCode != 200 {
return fmt.Errorf("cannot get token: %s", tokenBytes)
}
// save token in config
token := string(tokenBytes)
dc.cfg.Token = token
dc.expiryTime = time.Now().AddDate(0, 0, 1)
return nil
}
// do method performs request and returns response as an array of bytes and nil error in case of response status code 200.
// In any other cases do returns nil response and error.
// Retries are implemented in case of connection reset errors.
func (dc *DecortClient) do(req *http.Request, ctype string) ([]byte, error) {
// set up request headers and body
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
if ctype != "" {
req.Header.Set("Content-Type", ctype)
}
req.Header.Add("Authorization", "bearer "+dc.cfg.Token)
req.Header.Set("Accept", "application/json")
buf, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
req.Body.Close()
req.Body = io.NopCloser(bytes.NewBuffer(buf))
resp, err := dc.client.Do(req)
if resp != nil {
defer resp.Body.Close()
}
// retries logic GOES HERE
// get http response
//var resp *http.Response
//for i := uint64(0); i < dc.cfg.Retries; i++ {
// req := req.Clone(req.Context())
// req.Body = io.NopCloser(bytes.NewBuffer(buf))
//
// if i > 0 {
// time.Sleep(5 * time.Second) // no time sleep for the first request
// }
//
// resp, err = dc.client.Do(req)
//
// // stop retries on success and close response body
// if resp != nil {
// defer resp.Body.Close()
// }
// if err == nil {
// break
// }
//
// // retries in case of connection errors with time sleep
// if isConnectionError(err) {
// continue
// }
//
// // return error in case of non-connection error
// return nil, err
//}
// handle http request errors
if err != nil {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("got empty response without error")
}
// handle successful request
respBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 200 {
return respBytes, nil
}
// handle errors with status code other than 200
err = fmt.Errorf("%s", respBytes)
return nil, fmt.Errorf("could not execute request: %w", err)
}
// isConnectionError checks if given error falls within specific and associated connection errors
//func isConnectionError(err error) bool {
// if strings.Contains(err.Error(), "connection reset by peer") {
// return true
// }
// if errors.Is(err, io.EOF) {
// return true
// }
//
// return false
//}
// multiPartReq writes the request structure to the request body, and also returns string of the content-type
func multiPartReq(params interface{}) (*bytes.Buffer, string, error) {
reqBody := &bytes.Buffer{}
writer := multipart.NewWriter(reqBody)
values := reflect.ValueOf(params)
types := values.Type()
defer writer.Close()
for i := 0; i < values.NumField(); i++ {
if !values.Field(i).IsValid() {
continue
}
if values.Field(i).IsZero() {
continue
}
if file, ok := constants.FileName[types.Field(i).Name]; ok {
part, err := writer.CreateFormFile(trimString(types.Field(i)), file)
if err != nil {
return &bytes.Buffer{}, "", err
}
_, err = io.Copy(part, strings.NewReader(valueToString(values.Field(i).Interface())))
if err != nil {
return &bytes.Buffer{}, "", err
}
continue
}
if values.Field(i).Type().Kind() == reflect.Slice {
switch slice := values.Field(i).Interface().(type) {
case []string:
if validators.IsInSlice(trimString(types.Field(i)), constants.K8sValues) {
code, err := json.Marshal(slice)
if err != nil {
return &bytes.Buffer{}, "", err
}
err = writer.WriteField(trimString(types.Field(i)), string(code))
if err != nil {
return &bytes.Buffer{}, "", err
}
} else {
for _, val := range slice {
err := writer.WriteField(trimString(types.Field(i)), val)
if err != nil {
return &bytes.Buffer{}, "", err
}
}
}
case []uint:
for _, val := range slice {
err := writer.WriteField(trimString(types.Field(i)), strconv.FormatUint(uint64(val), 10))
if err != nil {
return &bytes.Buffer{}, "", err
}
}
case []uint64:
for _, val := range slice {
err := writer.WriteField(trimString(types.Field(i)), strconv.FormatUint(val, 10))
if err != nil {
return &bytes.Buffer{}, "", err
}
}
case []map[string]interface{}:
for _, val := range slice {
encodeStr, err := json.Marshal(val)
if err != nil {
return &bytes.Buffer{}, "", err
}
err = writer.WriteField(trimString(types.Field(i)), string(encodeStr))
if err != nil {
return &bytes.Buffer{}, "", err
}
}
default:
return &bytes.Buffer{}, "", fmt.Errorf("unsupported slice type:%T", slice)
}
continue
}
err := writer.WriteField(trimString(types.Field(i)), valueToString(values.Field(i).Interface()))
if err != nil {
return &bytes.Buffer{}, "", err
}
}
ct := writer.FormDataContentType()
return reqBody, ct, nil
}
func valueToString(a any) string {
switch str := a.(type) {
case string:
return str
case uint:
return strconv.FormatUint(uint64(str), 10)
case uint64:
return strconv.FormatUint(str, 10)
case bool:
return strconv.FormatBool(str)
default:
return ""
}
}
func trimString(el reflect.StructField) string {
return strings.TrimSuffix(el.Tag.Get("url"), ",omitempty")
}
func trimConfig(cfg *config.Config) config.Config {
cfg.SSOURL = strings.TrimSuffix(cfg.SSOURL, "/")
cfg.DecortURL = strings.TrimSuffix(cfg.DecortURL, "/")
return *cfg
}