Compare commits

...

4 Commits

2
.gitignore vendored

@ -1,3 +1,5 @@
cmd/ cmd/
.idea/ .idea/
.vscode/ .vscode/
.fleet/
.DS_Store

@ -1,6 +1,4 @@
## Version 1.5.7 ## Version 1.6.0-beta
### Bugfix ### Bugfix
- Remove the required tag of the start field in the CreateRequest model in cb/lb/create, since it is impossible to create an lb without starting it - Refactored client, made it concurrent safe
- Fix model the RecordGrid, add the ItemGridList model to cloudbroker/grid/models to correctly receive information on get and list requests
- Fix tag json field GID in model RecordResourcesConsumption cb/grid/models

@ -1,24 +1,31 @@
package decortsdk package decortsdk
import ( import (
"bytes"
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"strings" "strings"
"sync"
"time"
"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi"
"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker"
"github.com/google/go-querystring/query" "github.com/google/go-querystring/query"
"repository.basistech.ru/BASIS/decort-golang-sdk/config" "repository.basistech.ru/BASIS/decort-golang-sdk/config"
"repository.basistech.ru/BASIS/decort-golang-sdk/internal/client"
) )
// HTTP-client for platform // HTTP-client for platform
type DecortClient struct { type DecortClient struct {
decortURL string decortURL string
client *http.Client client *http.Client
cfg config.Config
expiryTime time.Time
mutex *sync.Mutex
} }
// Сlient builder // Сlient builder
@ -27,9 +34,25 @@ func New(cfg config.Config) *DecortClient {
cfg.Retries = 5 cfg.Retries = 5
} }
var expiryTime time.Time
if cfg.Token != "" {
expiryTime = time.Now().AddDate(0, 0, 1)
}
return &DecortClient{ return &DecortClient{
decortURL: cfg.DecortURL, decortURL: cfg.DecortURL,
client: client.NewHttpClient(cfg), client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: cfg.SSLSkipVerify,
},
},
},
cfg: cfg,
expiryTime: expiryTime,
mutex: &sync.Mutex{},
} }
} }
@ -56,7 +79,11 @@ func (dc *DecortClient) DecortApiCall(ctx context.Context, method, url string, p
return nil, err return nil, err
} }
resp, err := dc.client.Do(req) if err = dc.getToken(ctx); err != nil {
return nil, err
}
resp, err := dc.do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -73,3 +100,64 @@ func (dc *DecortClient) DecortApiCall(ctx context.Context, method, url string, p
return respBytes, nil return respBytes, nil
} }
func (dc *DecortClient) getToken(ctx context.Context) error {
dc.mutex.Lock()
defer dc.mutex.Unlock()
if dc.cfg.Token == "" || time.Now().After(dc.expiryTime) {
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")
resp, err := dc.client.Do(req)
if err != nil {
return fmt.Errorf("cannot get token: %w", err)
}
tokenBytes, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("cannot get token: %s", tokenBytes)
}
token := string(tokenBytes)
dc.cfg.Token = token
dc.expiryTime = time.Now().AddDate(0, 0, 1)
}
return nil
}
func (dc *DecortClient) do(req *http.Request) (*http.Response, error) {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Authorization", "bearer "+dc.cfg.Token)
req.Header.Set("Accept", "application/json")
var resp *http.Response
var err error
buf, _ := io.ReadAll(req.Body)
for i := uint64(0); i < dc.cfg.Retries; i++ {
req := req.Clone(req.Context())
req.Body = io.NopCloser(bytes.NewBuffer(buf))
resp, err = dc.client.Do(req)
if err == nil {
if resp.StatusCode == 200 {
return resp, err
}
respBytes, _ := io.ReadAll(resp.Body)
err = fmt.Errorf("%s", respBytes)
resp.Body.Close()
}
}
return nil, fmt.Errorf("could not execute request: %w", err)
}

@ -1,40 +0,0 @@
package client
import (
"crypto/tls"
"net/http"
"time"
"repository.basistech.ru/BASIS/decort-golang-sdk/config"
)
func NewHttpClient(cfg config.Config) *http.Client {
transCfg := &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: cfg.SSLSkipVerify,
},
}
var expiredTime time.Time
if cfg.Token != "" {
expiredTime = time.Now().AddDate(0, 0, 1)
}
return &http.Client{
Transport: &transport{
base: transCfg,
retries: cfg.Retries,
clientID: cfg.AppID,
clientSecret: cfg.AppSecret,
ssoURL: cfg.SSOURL,
token: cfg.Token,
expiryTime: expiredTime,
//TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Timeout: cfg.Timeout.Get(),
}
}

@ -1,40 +0,0 @@
package client
import (
"crypto/tls"
"net/http"
"net/url"
"time"
"repository.basistech.ru/BASIS/decort-golang-sdk/config"
)
// NewLegacyHttpClient creates legacy HTTP Client
func NewLegacyHttpClient(cfg config.LegacyConfig) *http.Client {
transCfg := &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: cfg.SSLSkipVerify,
},
}
var expiredTime time.Time
if cfg.Token != "" {
expiredTime = time.Now().AddDate(0, 0, 1)
}
return &http.Client{
Transport: &transportLegacy{
base: transCfg,
username: url.QueryEscape(cfg.Username),
password: url.QueryEscape(cfg.Password),
retries: cfg.Retries,
token: cfg.Token,
decortURL: cfg.DecortURL,
expiryTime: expiredTime,
},
Timeout: cfg.Timeout.Get(),
}
}

@ -1,74 +0,0 @@
package client
import (
"fmt"
"io"
"net/http"
"strings"
"time"
)
type transportLegacy struct {
base http.RoundTripper
username string
password string
retries uint64
token string
decortURL string
expiryTime time.Time
}
func (t *transportLegacy) RoundTrip(request *http.Request) (*http.Response, error) {
if t.token == "" || time.Now().After(t.expiryTime) {
body := fmt.Sprintf("username=%s&password=%s", t.username, t.password)
bodyReader := strings.NewReader(body)
req, _ := http.NewRequestWithContext(request.Context(), "POST", t.decortURL+"/restmachine/cloudapi/user/authenticate", bodyReader)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := t.base.RoundTrip(req)
if err != nil {
return nil, fmt.Errorf("unable to get token: %w", err)
}
tokenBytes, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("unable to get token: %s", tokenBytes)
}
token := string(tokenBytes)
t.token = token
t.expiryTime = time.Now().AddDate(0, 0, 1)
}
tokenValue := fmt.Sprintf("&authkey=%s", t.token)
tokenReader := strings.NewReader(tokenValue)
newBody := io.MultiReader(request.Body, tokenReader)
req, _ := http.NewRequestWithContext(request.Context(), request.Method, request.URL.String(), newBody)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
var resp *http.Response
var err error
for i := uint64(0); i < t.retries; i++ {
resp, err = t.base.RoundTrip(req)
if err == nil {
if resp.StatusCode == 200 {
return resp, nil
}
respBytes, _ := io.ReadAll(resp.Body)
err = fmt.Errorf("%s", respBytes)
resp.Body.Close()
}
if err != nil {
return nil, fmt.Errorf("could not execute request: %w", err)
}
time.Sleep(time.Second * 5)
}
return nil, fmt.Errorf("could not execute request: %w", err)
}

@ -1,69 +0,0 @@
package client
import (
"fmt"
"io"
"net/http"
"strings"
"time"
)
type transport struct {
base http.RoundTripper
retries uint64
clientID string
clientSecret string
token string
ssoURL string
expiryTime time.Time
}
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.token == "" || time.Now().After(t.expiryTime) {
body := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s&response_type=id_token", t.clientID, t.clientSecret)
bodyReader := strings.NewReader(body)
t.ssoURL = strings.TrimSuffix(t.ssoURL, "/")
req, _ := http.NewRequestWithContext(req.Context(), "POST", t.ssoURL+"/v1/oauth/access_token", bodyReader)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := t.base.RoundTrip(req)
if err != nil {
return nil, fmt.Errorf("cannot get token: %w", err)
}
tokenBytes, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("cannot get token: %s", tokenBytes)
}
token := string(tokenBytes)
t.token = token
t.expiryTime = time.Now().AddDate(0, 0, 1)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Authorization", "bearer "+t.token)
req.Header.Set("Accept", "application/json")
var resp *http.Response
var err error
for i := uint64(0); i < t.retries; i++ {
resp, err = t.base.RoundTrip(req)
if err == nil {
if resp.StatusCode == 200 {
return resp, nil
}
respBytes, _ := io.ReadAll(resp.Body)
err = fmt.Errorf("%s", respBytes)
resp.Body.Close()
}
//logrus.Errorf("Could not execute request: %v. Retrying %d/%d", err, i+1, t.retries)
time.Sleep(time.Second * 5)
}
return nil, fmt.Errorf("could not execute request: %w", err)
}

@ -1,23 +1,31 @@
package decortsdk package decortsdk
import ( import (
"bytes"
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"strings" "strings"
"sync"
"time"
"github.com/google/go-querystring/query" "github.com/google/go-querystring/query"
"repository.basistech.ru/BASIS/decort-golang-sdk/config" "repository.basistech.ru/BASIS/decort-golang-sdk/config"
"repository.basistech.ru/BASIS/decort-golang-sdk/internal/client"
"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi"
"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker"
) )
// Legacy HTTP-client for platform // Legacy HTTP-client for platform
type LegacyDecortClient struct { type LegacyDecortClient struct {
decortURL string decortURL string
client *http.Client client *http.Client
cfg config.LegacyConfig
expiryTime time.Time
mutex *sync.Mutex
} }
// Legacy client builder // Legacy client builder
@ -26,9 +34,25 @@ func NewLegacy(cfg config.LegacyConfig) *LegacyDecortClient {
cfg.Retries = 5 cfg.Retries = 5
} }
var expiryTime time.Time
if cfg.Token != "" {
expiryTime = time.Now().AddDate(0, 0, 1)
}
return &LegacyDecortClient{ return &LegacyDecortClient{
decortURL: cfg.DecortURL, decortURL: cfg.DecortURL,
client: client.NewLegacyHttpClient(cfg), client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: cfg.SSLSkipVerify,
},
},
},
cfg: cfg,
expiryTime: expiryTime,
mutex: &sync.Mutex{},
} }
} }
@ -49,13 +73,18 @@ func (ldc *LegacyDecortClient) DecortApiCall(ctx context.Context, method, url st
return nil, err return nil, err
} }
body := strings.NewReader(values.Encode()) if err = ldc.getToken(ctx); err != nil {
return nil, err
}
body := strings.NewReader(values.Encode() + fmt.Sprintf("&authkey=%s", ldc.cfg.Token))
req, err := http.NewRequestWithContext(ctx, method, ldc.decortURL+"/restmachine"+url, body) req, err := http.NewRequestWithContext(ctx, method, ldc.decortURL+"/restmachine"+url, body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
resp, err := ldc.client.Do(req) resp, err := ldc.do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -72,3 +101,60 @@ func (ldc *LegacyDecortClient) DecortApiCall(ctx context.Context, method, url st
return respBytes, nil return respBytes, nil
} }
func (ldc *LegacyDecortClient) getToken(ctx context.Context) error {
ldc.mutex.Lock()
defer ldc.mutex.Unlock()
if ldc.cfg.Token == "" || time.Now().After(ldc.expiryTime) {
body := fmt.Sprintf("username=%s&password=%s", url.QueryEscape(ldc.cfg.Username), url.QueryEscape(ldc.cfg.Password))
bodyReader := strings.NewReader(body)
req, _ := http.NewRequestWithContext(ctx, "POST", ldc.cfg.DecortURL+"/restmachine/cloudapi/user/authenticate", bodyReader)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := ldc.client.Do(req)
if err != nil {
return fmt.Errorf("unable to get token: %w", err)
}
tokenBytes, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("unable to get token: %s", tokenBytes)
}
token := string(tokenBytes)
ldc.cfg.Token = token
ldc.expiryTime = time.Now().AddDate(0, 0, 1)
}
return nil
}
func (ldc *LegacyDecortClient) do(req *http.Request) (*http.Response, error) {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
var resp *http.Response
var err error
buf, _ := io.ReadAll(req.Body)
for i := uint64(0); i < ldc.cfg.Retries; i++ {
req := req.Clone(req.Context())
req.Body = io.NopCloser(bytes.NewBuffer(buf))
resp, err = ldc.client.Do(req)
if err == nil {
if resp.StatusCode == 200 {
return resp, err
}
respBytes, _ := io.ReadAll(resp.Body)
err = fmt.Errorf("%s", respBytes)
resp.Body.Close()
}
}
return nil, fmt.Errorf("could not execute request: %w", err)
}

@ -106,6 +106,10 @@ type CreateRequest struct {
// Text description of this Kubernetes cluster // Text description of this Kubernetes cluster
// Required: false // Required: false
Description string `url:"desc,omitempty" json:"desc,omitempty"` Description string `url:"desc,omitempty" json:"desc,omitempty"`
//Use only selected ExtNet for infrastructure connections
// Required: false
ExtNetOnly bool `url:"extnetOnly,omitempty" json:"extnetOnly,omitempty"`
} }
// Create creates a new Kubernetes cluster in the specified Resource Group // Create creates a new Kubernetes cluster in the specified Resource Group

@ -2,6 +2,7 @@ package lb
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
"strings" "strings"
@ -20,16 +21,16 @@ type CreateRequest struct {
Name string `url:"name" json:"name" validate:"required"` Name string `url:"name" json:"name" validate:"required"`
// External network to connect this load balancer to // External network to connect this load balancer to
// Required: true // Required: false
ExtNetID uint64 `url:"extnetId" json:"extnetId" validate:"required"` ExtNetID uint64 `url:"extnetId" json:"extnetId"`
// Internal network (VINS) to connect this load balancer to // Internal network (VINS) to connect this load balancer to
// Required: true // Required: false
VINSID uint64 `url:"vinsId" json:"vinsId" validate:"required"` VINSID uint64 `url:"vinsId" json:"vinsId"`
// Start now Load balancer // Start now Load balancer
// Required: true // Required: false
Start bool `url:"start" json:"start" validate:"required"` Start bool `url:"start" json:"start"`
// Text description of this load balancer // Text description of this load balancer
// Required: false // Required: false
@ -45,6 +46,10 @@ func (l LB) Create(ctx context.Context, req CreateRequest) (string, error) {
} }
} }
if req.ExtNetID == 0 && req.VINSID == 0 {
return "", errors.New ("vinsId and extNetId cannot be both in the value 0")
}
url := "/cloudapi/lb/create" url := "/cloudapi/lb/create"
res, err := l.client.DecortApiCall(ctx, http.MethodPost, url, req) res, err := l.client.DecortApiCall(ctx, http.MethodPost, url, req)

Loading…
Cancel
Save