From 1972956aeb1d06b762d029fcc56b4f87bc90c562 Mon Sep 17 00:00:00 2001 From: Nikita Sorokin Date: Fri, 15 Sep 2023 12:41:04 +0300 Subject: [PATCH] v1.6.0-gamma --- .gitignore | 4 +- CHANGELOG.md | 2 +- client.go | 98 +++++++++++++++++++++++++-- internal/client/http-client.go | 42 ------------ internal/client/legacy-http-client.go | 42 ------------ internal/client/legacy-transport.go | 78 --------------------- internal/client/transport.go | 74 -------------------- legacy-client.go | 98 +++++++++++++++++++++++++-- 8 files changed, 189 insertions(+), 249 deletions(-) delete mode 100644 internal/client/http-client.go delete mode 100644 internal/client/legacy-http-client.go delete mode 100644 internal/client/legacy-transport.go delete mode 100644 internal/client/transport.go diff --git a/.gitignore b/.gitignore index 224dd8c..09215c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ cmd/ .idea/ -.vscode/ \ No newline at end of file +.vscode/ +.fleet/ +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 97bab99..9c5abec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ ## Version 1.6.0-beta ### Bugfix -- Fixed RoudTrip bug in HTTP transport, made it concurrent safe \ No newline at end of file +- Refactored client, made it concurrent safe \ No newline at end of file diff --git a/client.go b/client.go index 9f8ded6..a327d91 100644 --- a/client.go +++ b/client.go @@ -1,24 +1,31 @@ package decortsdk import ( + "bytes" "context" + "crypto/tls" "errors" + "fmt" "io" "net/http" "strings" + "sync" + "time" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker" "github.com/google/go-querystring/query" "repository.basistech.ru/BASIS/decort-golang-sdk/config" - "repository.basistech.ru/BASIS/decort-golang-sdk/internal/client" ) // HTTP-client for platform type DecortClient struct { - decortURL string - client *http.Client + decortURL string + client *http.Client + cfg config.Config + expiryTime time.Time + mutex *sync.Mutex } // Сlient builder @@ -27,9 +34,25 @@ func New(cfg config.Config) *DecortClient { cfg.Retries = 5 } + var expiryTime time.Time + + if cfg.Token != "" { + expiryTime = time.Now().AddDate(0, 0, 1) + } + return &DecortClient{ 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 } - resp, err := dc.client.Do(req) + if err = dc.getToken(ctx); err != nil { + return nil, err + } + + resp, err := dc.do(req) if err != nil { return nil, err } @@ -73,3 +100,64 @@ func (dc *DecortClient) DecortApiCall(ctx context.Context, method, url string, p 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) +} diff --git a/internal/client/http-client.go b/internal/client/http-client.go deleted file mode 100644 index 82c18cf..0000000 --- a/internal/client/http-client.go +++ /dev/null @@ -1,42 +0,0 @@ -package client - -import ( - "crypto/tls" - "net/http" - "sync" - "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, - mutex: &sync.Mutex{}, - //TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - - Timeout: cfg.Timeout.Get(), - } -} diff --git a/internal/client/legacy-http-client.go b/internal/client/legacy-http-client.go deleted file mode 100644 index c870d08..0000000 --- a/internal/client/legacy-http-client.go +++ /dev/null @@ -1,42 +0,0 @@ -package client - -import ( - "crypto/tls" - "net/http" - "net/url" - "sync" - "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, - mutex: &sync.Mutex{}, - }, - - Timeout: cfg.Timeout.Get(), - } -} diff --git a/internal/client/legacy-transport.go b/internal/client/legacy-transport.go deleted file mode 100644 index de705a0..0000000 --- a/internal/client/legacy-transport.go +++ /dev/null @@ -1,78 +0,0 @@ -package client - -import ( - "fmt" - "io" - "net/http" - "strings" - "sync" - "time" -) - -type transportLegacy struct { - base http.RoundTripper - username string - password string - retries uint64 - token string - decortURL string - mutex *sync.Mutex - 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++ { - t.mutex.Lock() - resp, err = t.base.RoundTrip(req) - t.mutex.Unlock() - 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) -} diff --git a/internal/client/transport.go b/internal/client/transport.go deleted file mode 100644 index d159de3..0000000 --- a/internal/client/transport.go +++ /dev/null @@ -1,74 +0,0 @@ -package client - -import ( - "fmt" - "io" - "net/http" - "strings" - "sync" - "time" -) - -type transport struct { - base http.RoundTripper - retries uint64 - clientID string - clientSecret string - token string - ssoURL string - expiryTime time.Time - mutex *sync.Mutex -} - -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++ { - t.mutex.Lock() - resp, err = t.base.RoundTrip(req) - t.mutex.Unlock() - 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) -} diff --git a/legacy-client.go b/legacy-client.go index d1f71c4..6cb8996 100644 --- a/legacy-client.go +++ b/legacy-client.go @@ -1,23 +1,31 @@ package decortsdk import ( + "bytes" "context" + "crypto/tls" "errors" + "fmt" "io" "net/http" + "net/url" "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/client" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker" ) // Legacy HTTP-client for platform type LegacyDecortClient struct { - decortURL string - client *http.Client + decortURL string + client *http.Client + cfg config.LegacyConfig + expiryTime time.Time + mutex *sync.Mutex } // Legacy client builder @@ -26,9 +34,25 @@ func NewLegacy(cfg config.LegacyConfig) *LegacyDecortClient { cfg.Retries = 5 } + var expiryTime time.Time + + if cfg.Token != "" { + expiryTime = time.Now().AddDate(0, 0, 1) + } + return &LegacyDecortClient{ 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 } - 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) if err != nil { return nil, err } - resp, err := ldc.client.Do(req) + resp, err := ldc.do(req) if err != nil { return nil, err } @@ -72,3 +101,60 @@ func (ldc *LegacyDecortClient) DecortApiCall(ctx context.Context, method, url st 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) +}