@ -8,6 +8,7 @@ import (
"io"
"mime/multipart"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
@ -15,13 +16,12 @@ import (
"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/pkg/cloudapi"
k8s_ca "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi/k8s"
"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker"
k8s_cb "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker/k8s"
)
// HTTP-client for platform
// DecortClient is HTTP-client for platform
type DecortClient struct {
decortURL string
client * http . Client
@ -70,35 +70,54 @@ func (dc *DecortClient) CloudBroker() *cloudbroker.CloudBroker {
// DecortApiCall method for sending requests to the platform
func ( dc * DecortClient ) DecortApiCall ( ctx context . Context , method , url string , params interface { } ) ( [ ] byte , error ) {
k8sCaCreateReq , okCa := params . ( k8s_ca . CreateRequest )
k8sCbCreateReq , okCb := params . ( k8s_cb . CreateRequest )
var body * bytes . Buffer
var ctype string
if okCa {
body , ctype = createK8sCloudApi ( k8sCaCreateReq )
} else if okCb {
body , ctype = createK8sCloudBroker ( k8sCbCreateReq )
} else {
values , err := query . Values ( params )
if err != nil {
return nil , err
}
body = bytes . NewBufferString ( values . Encode ( ) )
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 , "" )
if err != nil {
return nil , err
}
return respBytes , err
}
req , err := http . NewRequestWithContext ( ctx , method , dc . decortURL + "/restmachine" + url , body )
// 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
var respBytes [ ] byte
respBytes , err = dc . do ( req , ctype )
respBytes , err := dc . do ( req , ctype )
if err != nil {
return nil , err
}
return respBytes , err
}
@ -107,7 +126,12 @@ func (dc *DecortClient) getToken(ctx context.Context) error {
dc . mutex . Lock ( )
defer dc . mutex . Unlock ( )
if dc . cfg . Token == "" || time . Now ( ) . After ( dc . expiryTime ) {
// 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 )
@ -116,32 +140,39 @@ func (dc *DecortClient) getToken(ctx context.Context) error {
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 {
if err ! = nil || resp = = nil {
return fmt . Errorf ( "cannot get token: %w" , err )
}
defer resp . Body . Close ( )
tokenBytes , _ := io . ReadAll ( resp . Body )
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 ) {
if ctype != "" {
req . Header . Add ( "Content-Type" , ctype )
} else {
// 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 )
@ -152,13 +183,51 @@ func (dc *DecortClient) do(req *http.Request, ctype string) ([]byte, error) {
return nil , err
}
req . Body . Close ( )
req . Body = io . NopCloser ( bytes . NewBuffer ( buf ) )
resp , err := dc . client . Do ( req )
if err != nil || resp == nil {
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
}
defer resp . Body . Close ( )
if resp == nil {
return nil , fmt . Errorf ( "got empty response without error" )
}
// handle successful request
respBytes , _ := io . ReadAll ( resp . Body )
@ -171,230 +240,100 @@ func (dc *DecortClient) do(req *http.Request, ctype string) ([]byte, error) {
return nil , fmt . Errorf ( "could not execute request: %w" , err )
}
func createK8sCloudApi ( req k8s_ca . CreateRequest ) ( * bytes . Buffer , string ) {
// 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 )
if req . OidcCertificate != "" {
part , _ := writer . CreateFormFile ( "oidcCertificate" , "ca.crt" )
_ , _ = io . Copy ( part , strings . NewReader ( req . OidcCertificate ) )
}
_ = writer . WriteField ( "name" , req . Name )
_ = writer . WriteField ( "rgId" , strconv . FormatUint ( req . RGID , 10 ) )
_ = writer . WriteField ( "k8ciId" , strconv . FormatUint ( req . K8SCIID , 10 ) )
_ = writer . WriteField ( "workerGroupName" , req . WorkerGroupName )
_ = writer . WriteField ( "networkPlugin" , req . NetworkPlugin )
if req . MasterSEPID != 0 {
_ = writer . WriteField ( "masterSepId" , strconv . FormatUint ( req . MasterSEPID , 10 ) )
}
if req . MasterSEPPool != "" {
_ = writer . WriteField ( "masterSepPool" , req . MasterSEPPool )
}
if req . WorkerSEPID != 0 {
_ = writer . WriteField ( "workerSepId" , strconv . FormatUint ( req . WorkerSEPID , 10 ) )
}
if req . WorkerSEPPool != "" {
_ = writer . WriteField ( "workerSepPool" , req . WorkerSEPPool )
values := reflect . ValueOf ( params )
types := values . Type ( )
defer writer . Close ( )
for i := 0 ; i < values . NumField ( ) ; i ++ {
if ! values . Field ( i ) . IsValid ( ) {
continue
}
if req . Labels != nil {
for _ , v := range req . Labels {
_ = writer . WriteField ( "labels" , v )
}
}
if req . Taints != nil {
for _ , v := range req . Taints {
_ = writer . WriteField ( "taints" , v )
}
}
if req . Annotations != nil {
for _ , v := range req . Annotations {
_ = writer . WriteField ( "annotations" , v )
}
if values . Field ( i ) . IsZero ( ) {
continue
}
if req . MasterCPU != 0 {
_ = writer . WriteField ( "masterCpu" , strconv . FormatUint ( uint64 ( req . MasterCPU ) , 10 ) )
}
if req . MasterNum != 0 {
_ = writer . WriteField ( "masterNum" , strconv . FormatUint ( uint64 ( req . MasterNum ) , 10 ) )
}
if req . MasterRAM != 0 {
_ = writer . WriteField ( "masterRam" , strconv . FormatUint ( uint64 ( req . MasterRAM ) , 10 ) )
}
if req . MasterDisk != 0 {
_ = writer . WriteField ( "masterDisk" , strconv . FormatUint ( uint64 ( req . MasterDisk ) , 10 ) )
}
if req . WorkerCPU != 0 {
_ = writer . WriteField ( "workerCpu" , strconv . FormatUint ( uint64 ( req . WorkerCPU ) , 10 ) )
}
if req . WorkerNum != 0 {
_ = writer . WriteField ( "workerNum" , strconv . FormatUint ( uint64 ( req . WorkerNum ) , 10 ) )
}
if req . WorkerRAM != 0 {
_ = writer . WriteField ( "workerRam" , strconv . FormatUint ( uint64 ( req . WorkerRAM ) , 10 ) )
}
if req . WorkerDisk != 0 {
_ = writer . WriteField ( "workerDisk" , strconv . FormatUint ( uint64 ( req . WorkerDisk ) , 10 ) )
}
if req . ExtNetID != 0 {
_ = writer . WriteField ( "extnetId" , strconv . FormatUint ( req . ExtNetID , 10 ) )
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
}
if req . VinsId != 0 {
_ = writer . WriteField ( "vinsId" , strconv . FormatUint ( req . VinsId , 10 ) )
_ , err = io . Copy ( part , strings . NewReader ( valueToString ( values . Field ( i ) . Interface ( ) ) ) )
if err != nil {
return & bytes . Buffer { } , "" , err
}
if ! req . WithLB {
_ = writer . WriteField ( "withLB" , strconv . FormatBool ( req . WithLB ) )
continue
}
_ = writer . WriteField ( "highlyAvailableLB" , strconv . FormatBool ( req . HighlyAvailable ) )
if req . AdditionalSANs != nil {
for _ , v := range req . AdditionalSANs {
_ = writer . WriteField ( "additionalSANs" , v )
}
}
if req . InitConfiguration != "" {
_ = writer . WriteField ( "initConfiguration" , req . InitConfiguration )
}
if req . ClusterConfiguration != "" {
_ = writer . WriteField ( "clusterConfiguration" , req . ClusterConfiguration )
}
if req . KubeletConfiguration != "" {
_ = writer . WriteField ( "kubeletConfiguration" , req . KubeletConfiguration )
}
if req . KubeProxyConfiguration != "" {
_ = writer . WriteField ( "kubeProxyConfiguration" , req . KubeProxyConfiguration )
}
if req . JoinConfiguration != "" {
_ = writer . WriteField ( "joinConfiguration" , req . JoinConfiguration )
}
if req . Description != "" {
_ = writer . WriteField ( "desc" , req . Description )
}
if req . UserData != "" {
_ = writer . WriteField ( "userData" , req . UserData )
if values . Field ( i ) . Type ( ) . Kind ( ) == reflect . Slice {
switch slice := values . Field ( i ) . Interface ( ) . ( type ) {
case [ ] string :
for _ , val := range slice {
err := writer . WriteField ( trimString ( types . Field ( i ) ) , val )
if err != nil {
return & bytes . Buffer { } , "" , err
}
_ = writer . WriteField ( "extnetOnly" , strconv . FormatBool ( req . ExtNetOnly ) )
ct := writer . FormDataContentType ( )
writer . Close ( )
return reqBody , ct
}
func createK8sCloudBroker ( req k8s_cb . CreateRequest ) ( * bytes . Buffer , string ) {
reqBody := & bytes . Buffer { }
writer := multipart . NewWriter ( reqBody )
if req . OidcCertificate != "" {
part , _ := writer . CreateFormFile ( "oidcCertificate" , "ca.crt" )
_ , _ = io . Copy ( part , strings . NewReader ( req . OidcCertificate ) )
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
}
_ = writer . WriteField ( "name" , req . Name )
_ = writer . WriteField ( "rgId" , strconv . FormatUint ( req . RGID , 10 ) )
_ = writer . WriteField ( "k8ciId" , strconv . FormatUint ( req . K8CIID , 10 ) )
_ = writer . WriteField ( "workerGroupName" , req . WorkerGroupName )
_ = writer . WriteField ( "networkPlugin" , req . NetworkPlugin )
if req . MasterSEPID != 0 {
_ = writer . WriteField ( "masterSepId" , strconv . FormatUint ( req . MasterSEPID , 10 ) )
}
if req . MasterSEPPool != "" {
_ = writer . WriteField ( "masterSepPool" , req . MasterSEPPool )
case [ ] uint64 :
for _ , val := range slice {
err := writer . WriteField ( trimString ( types . Field ( i ) ) , strconv . FormatUint ( val , 10 ) )
if err != nil {
return & bytes . Buffer { } , "" , err
}
if req . WorkerSEPID != 0 {
_ = writer . WriteField ( "workerSepId" , strconv . FormatUint ( req . WorkerSEPID , 10 ) )
}
if req . WorkerSEPPool != "" {
_ = writer . WriteField ( "workerSepPool" , req . WorkerSEPPool )
default :
return & bytes . Buffer { } , "" , fmt . Errorf ( "unsupported slice type:%T" , slice )
}
if req . Labels != nil {
for _ , v := range req . Labels {
_ = writer . WriteField ( "labels" , v )
}
}
if req . Taints != nil {
for _ , v := range req . Taints {
_ = writer . WriteField ( "taints" , v )
}
}
if req . Annotations != nil {
for _ , v := range req . Annotations {
_ = writer . WriteField ( "annotations" , v )
}
continue
}
if req . MasterCPU != 0 {
_ = writer . WriteField ( "masterCpu" , strconv . FormatUint ( req . MasterCPU , 10 ) )
}
if req . MasterNum != 0 {
_ = writer . WriteField ( "masterNum" , strconv . FormatUint ( req . MasterNum , 10 ) )
}
if req . MasterRAM != 0 {
_ = writer . WriteField ( "masterRam" , strconv . FormatUint ( req . MasterRAM , 10 ) )
}
if req . MasterDisk != 0 {
_ = writer . WriteField ( "masterDisk" , strconv . FormatUint ( req . MasterDisk , 10 ) )
}
if req . WorkerCPU != 0 {
_ = writer . WriteField ( "workerCpu" , strconv . FormatUint ( req . WorkerCPU , 10 ) )
}
if req . WorkerNum != 0 {
_ = writer . WriteField ( "workerNum" , strconv . FormatUint ( req . WorkerNum , 10 ) )
}
if req . WorkerRAM != 0 {
_ = writer . WriteField ( "workerRam" , strconv . FormatUint ( req . WorkerRAM , 10 ) )
}
if req . WorkerDisk != 0 {
_ = writer . WriteField ( "workerDisk" , strconv . FormatUint ( req . WorkerDisk , 10 ) )
}
if req . ExtNetID != 0 {
_ = writer . WriteField ( "extnetId" , strconv . FormatUint ( req . ExtNetID , 10 ) )
err := writer . WriteField ( trimString ( types . Field ( i ) ) , valueToString ( values . Field ( i ) . Interface ( ) ) )
if err != nil {
return & bytes . Buffer { } , "" , err
}
if req . VinsId != 0 {
_ = writer . WriteField ( "vinsId" , strconv . FormatUint ( req . VinsId , 10 ) )
}
if ! req . WithLB {
_ = writer . WriteField ( "withLB" , strconv . FormatBool ( req . WithLB ) )
ct := writer . FormDataContentType ( )
return reqBody , ct , nil
}
_ = writer . WriteField ( "highlyAvailableLB" , strconv . FormatBool ( req . HighlyAvailable ) )
if req . AdditionalSANs != nil {
for _ , v := range req . AdditionalSANs {
_ = writer . WriteField ( "additionalSANs" , v )
}
}
if req . InitConfiguration != "" {
_ = writer . WriteField ( "initConfiguration" , req . InitConfiguration )
}
if req . ClusterConfiguration != "" {
_ = writer . WriteField ( "clusterConfiguration" , req . ClusterConfiguration )
}
if req . KubeletConfiguration != "" {
_ = writer . WriteField ( "kubeletConfiguration" , req . KubeletConfiguration )
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 ""
}
if req . KubeProxyConfiguration != "" {
_ = writer . WriteField ( "kubeProxyConfiguration" , req . KubeProxyConfiguration )
}
if req . JoinConfiguration != "" {
_ = writer . WriteField ( "joinConfiguration" , req . JoinConfiguration )
}
if req . Description != "" {
_ = writer . WriteField ( "desc" , req . Description )
}
if req . UserData != "" {
_ = writer . WriteField ( "userData" , req . UserData )
}
_ = writer . WriteField ( "extnetOnly" , strconv . FormatBool ( req . ExtNetOnly ) )
ct := writer . FormDataContentType ( )
writer . Close ( )
return reqBody, ct
func trimString ( el reflect . StructField ) string {
return strings . TrimSuffix ( el . Tag . Get ( "url" ) , ",omitempty" )
}