mirror of
https://dev.azure.com/schwarzit/schwarzit.stackit-public/_git/audit-go
synced 2026-02-09 01:27:26 +00:00
220 lines
6.9 KiB
Go
220 lines
6.9 KiB
Go
package messaging
|
|
|
|
// terraform-provider-solacebroker
|
|
//
|
|
// Copyright 2024 Solace Corporation. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
)
|
|
|
|
var (
|
|
ErrResourceNotFound = errors.New("resource not found")
|
|
)
|
|
|
|
var firstRequest = true
|
|
|
|
type Client struct {
|
|
*retryablehttp.Client
|
|
url string
|
|
username string
|
|
password string
|
|
bearerToken string
|
|
retries uint
|
|
retryMinInterval time.Duration
|
|
retryMaxInterval time.Duration
|
|
requestMinInterval time.Duration
|
|
requestTimeout time.Duration
|
|
rateLimiter <-chan time.Time
|
|
}
|
|
|
|
type Option func(*Client)
|
|
|
|
func BasicAuth(username, password string) Option {
|
|
return func(client *Client) {
|
|
client.username = username
|
|
client.password = password
|
|
}
|
|
}
|
|
|
|
//func BearerToken(bearerToken string) Option {
|
|
// return func(client *Client) {
|
|
// client.bearerToken = bearerToken
|
|
// }
|
|
//}
|
|
//
|
|
//func Retries(numRetries uint, retryMinInterval, retryMaxInterval time.Duration) Option {
|
|
// return func(client *Client) {
|
|
// client.retries = numRetries
|
|
// client.retryMinInterval = retryMinInterval
|
|
// client.retryMaxInterval = retryMaxInterval
|
|
// }
|
|
//}
|
|
//
|
|
//func RequestLimits(requestTimeoutDuration, requestMinInterval time.Duration) Option {
|
|
// return func(client *Client) {
|
|
// client.requestTimeout = requestTimeoutDuration
|
|
// client.requestMinInterval = requestMinInterval
|
|
// }
|
|
//}
|
|
|
|
func NewClient(url string, insecure_skip_verify bool, providerClient bool, options ...Option) *Client {
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure_skip_verify},
|
|
MaxIdleConnsPerHost: 10,
|
|
}
|
|
retryClient := retryablehttp.NewClient()
|
|
retryClient.HTTPClient.Transport = tr
|
|
if !providerClient {
|
|
retryClient.Logger = nil
|
|
}
|
|
client := &Client{
|
|
Client: retryClient,
|
|
url: url,
|
|
retries: 10, // default 3
|
|
retryMinInterval: time.Second,
|
|
retryMaxInterval: time.Second * 10,
|
|
}
|
|
for _, o := range options {
|
|
o(client)
|
|
}
|
|
client.Client.RetryMax = int(client.retries)
|
|
client.Client.RetryWaitMin = client.retryMinInterval
|
|
client.Client.RetryWaitMax = client.retryMaxInterval
|
|
client.HTTPClient.Timeout = client.requestTimeout
|
|
client.HTTPClient.Jar, _ = cookiejar.New(nil)
|
|
if client.requestMinInterval > 0 {
|
|
client.rateLimiter = time.NewTicker(client.requestMinInterval).C
|
|
} else {
|
|
ch := make(chan time.Time)
|
|
// closing the channel will make receiving from the channel non-blocking (the value received will be the
|
|
// zero value)
|
|
close(ch)
|
|
client.rateLimiter = ch
|
|
}
|
|
firstRequest = true
|
|
return client
|
|
}
|
|
|
|
func (c *Client) RequestWithBody(ctx context.Context, method, url string, body any) (map[string]any, error) {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, method, c.url+url, bytes.NewBuffer(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slog.Debug(fmt.Sprintf("===== %v to %v =====", request.Method, request.URL))
|
|
rawBody, err := c.doRequest(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseResponseAsObject(ctx, request, rawBody)
|
|
}
|
|
|
|
func (c *Client) doRequest(request *http.Request) ([]byte, error) {
|
|
if !firstRequest {
|
|
// the value doesn't matter, it is waiting for the value that matters
|
|
<-c.rateLimiter
|
|
} else {
|
|
// only skip rate limiter for the first request
|
|
firstRequest = false
|
|
}
|
|
if request.Method != http.MethodGet {
|
|
request.Header.Set("Content-Type", "application/json")
|
|
}
|
|
// Prefer OAuth even if Basic Auth credentials provided
|
|
if c.bearerToken != "" {
|
|
request.Header.Set("Authorization", "Bearer "+c.bearerToken)
|
|
} else if c.username != "" {
|
|
request.SetBasicAuth(c.username, c.password)
|
|
} else {
|
|
return nil, fmt.Errorf("either username or bearer token must be provided to access the broker")
|
|
}
|
|
var response *http.Response
|
|
var err error
|
|
response, err = c.StandardClient().Do(request)
|
|
if err != nil || response == nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
rawBody, err := io.ReadAll(response.Body)
|
|
if err != nil || (response.StatusCode != http.StatusOK && response.StatusCode != http.StatusBadRequest) {
|
|
return nil, fmt.Errorf("could not perform request: status %v (%v) during %v to %v, response body:\n%s", response.StatusCode, response.Status, request.Method, request.URL, rawBody)
|
|
}
|
|
if _, err := io.Copy(io.Discard, response.Body); err != nil {
|
|
return nil, fmt.Errorf("response processing error: during %v to %v", request.Method, request.URL)
|
|
}
|
|
return rawBody, nil
|
|
}
|
|
|
|
func parseResponseAsObject(_ context.Context, request *http.Request, dataResponse []byte) (map[string]any, error) {
|
|
data := map[string]any{}
|
|
err := json.Unmarshal(dataResponse, &data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse response body from %v to %v, response body was:\n%s", request.Method, request.URL, dataResponse)
|
|
}
|
|
rawData, ok := data["data"]
|
|
if ok {
|
|
// Valid data
|
|
data, _ = rawData.(map[string]any)
|
|
return data, nil
|
|
} else {
|
|
// Analize response metadata details
|
|
rawData, ok = data["meta"]
|
|
if ok {
|
|
data, _ = rawData.(map[string]any)
|
|
if data["responseCode"].(float64) == http.StatusOK {
|
|
// this is valid response for delete
|
|
return nil, nil
|
|
}
|
|
description := data["error"].(map[string]interface{})["description"].(string)
|
|
status := data["error"].(map[string]interface{})["status"].(string)
|
|
if status == "NOT_FOUND" {
|
|
// resource not found is a special type we want to return
|
|
return nil, fmt.Errorf("request failed from %v to %v, %v, %v, %w", request.Method, request.URL, description, status, ErrResourceNotFound)
|
|
}
|
|
slog.Error(fmt.Sprintf("SEMP request returned %v, %v", description, status))
|
|
return nil, fmt.Errorf("request failed for %v using %v, %v, %v", request.URL, request.Method, description, status)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("could not parse response details from %v to %v, response body was:\n%s", request.Method, request.URL, dataResponse)
|
|
}
|
|
|
|
func (c *Client) RequestWithoutBody(ctx context.Context, method, url string) (map[string]interface{}, error) {
|
|
request, err := http.NewRequestWithContext(ctx, method, c.url+url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slog.Debug(fmt.Sprintf("===== %v to %v =====", request.Method, request.URL))
|
|
rawBody, err := c.doRequest(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseResponseAsObject(ctx, request, rawBody)
|
|
}
|