fix: add involves edge from task to agent:nomos at creation
Plus sync vendor directory for Docker build compatibility.
This commit is contained in:
515
vendor/github.com/infisical/go-sdk/client.go
generated
vendored
Normal file
515
vendor/github.com/infisical/go-sdk/client.go
generated
vendored
Normal file
@@ -0,0 +1,515 @@
|
||||
package infisical
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
"github.com/infisical/go-sdk/packages/models"
|
||||
"github.com/infisical/go-sdk/packages/util"
|
||||
)
|
||||
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
LogLevelDebug LogLevel = "debug"
|
||||
)
|
||||
|
||||
type InfisicalClient struct {
|
||||
authMethod util.AuthMethod
|
||||
credential interface{}
|
||||
tokenDetails MachineIdentityCredential
|
||||
lastFetchedTime time.Time
|
||||
firstFetchedTime time.Time
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
// refreshMu is used to prevent concurrent token refreshes.
|
||||
// Only one refresh should happen at a time to avoid race conditions.
|
||||
refreshMu sync.Mutex
|
||||
|
||||
cache *expirable.LRU[string, interface{}]
|
||||
|
||||
httpClient *resty.Client
|
||||
config Config
|
||||
|
||||
secrets SecretsInterface
|
||||
folders FoldersInterface
|
||||
auth AuthInterface
|
||||
dynamicSecrets DynamicSecretsInterface
|
||||
kms KmsInterface
|
||||
ssh SshInterface
|
||||
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
type InfisicalClientInterface interface {
|
||||
UpdateConfiguration(config Config)
|
||||
Secrets() SecretsInterface
|
||||
Folders() FoldersInterface
|
||||
Auth() AuthInterface
|
||||
DynamicSecrets() DynamicSecretsInterface
|
||||
Kms() KmsInterface
|
||||
Ssh() SshInterface
|
||||
}
|
||||
|
||||
type ExponentialBackoffStrategy struct {
|
||||
// Base delay between retries. Defaults to 1 second
|
||||
BaseDelay time.Duration
|
||||
|
||||
// Maximum number of retries. Defaults to 3
|
||||
MaxRetries int
|
||||
|
||||
// Maximum delay between retries. Defaults to 30 seconds
|
||||
MaxDelay time.Duration
|
||||
}
|
||||
|
||||
func (s *ExponentialBackoffStrategy) GetDelay(retryCount int) time.Duration {
|
||||
|
||||
if s.BaseDelay == 0 {
|
||||
s.BaseDelay = 1 * time.Second
|
||||
}
|
||||
|
||||
if s.MaxDelay == 0 {
|
||||
s.MaxDelay = 30 * time.Second
|
||||
}
|
||||
|
||||
if s.MaxRetries == 0 {
|
||||
s.MaxRetries = 3
|
||||
}
|
||||
|
||||
delay := s.BaseDelay * time.Duration(math.Pow(2, float64(retryCount)))
|
||||
|
||||
// if delay is greater than the user-configured max delay, set the delay to the max delay
|
||||
if delay > s.MaxDelay {
|
||||
delay = s.MaxDelay
|
||||
}
|
||||
|
||||
return s.Jitter(delay)
|
||||
}
|
||||
|
||||
func (s *ExponentialBackoffStrategy) Jitter(delay time.Duration) time.Duration {
|
||||
// 20% jitter, negative and positive
|
||||
|
||||
jitterFactor := 0.2
|
||||
|
||||
// generates random value in [-0.2, +0.2] range
|
||||
randomFactor := (rand.Float64()*2 - 1) * jitterFactor
|
||||
jitter := time.Duration(randomFactor * float64(delay))
|
||||
return delay + jitter
|
||||
}
|
||||
|
||||
type RetryRequestsConfig struct {
|
||||
ExponentialBackoff *ExponentialBackoffStrategy
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
SiteUrl string `default:"https://app.infisical.com"`
|
||||
CaCertificate string
|
||||
LogLevel LogLevel // Specify the log level for the SDK. If set to debug, the SDK will print to stdout with verbose logging. Defaults to no logging.
|
||||
UserAgent string `default:"infisical-go-sdk"` // User-Agent header to be used on requests sent by the SDK. Defaults to `infisical-go-sdk`. Do not modify this unless you have a reason to do so.
|
||||
AutoTokenRefresh *bool // Whether or not to automatically refresh the auth token after using one of the .Auth() methods. Defaults to `true` when nil. Pass a pointer to `false` to disable.
|
||||
SilentMode bool // If enabled, the SDK will not print any warnings to the console.
|
||||
CacheExpiryInSeconds int // Defines how long certain API responses should be cached in memory, in seconds. When set to a positive value, responses from specific fetch API requests (like secret fetching) will be cached for this duration. Set to 0 to disable caching. Defaults to 0.
|
||||
CustomHeaders map[string]string
|
||||
RetryRequestsConfig *RetryRequestsConfig
|
||||
LogWriter io.Writer
|
||||
}
|
||||
|
||||
func setupLogger(logLevel LogLevel, logWriter io.Writer) zerolog.Logger {
|
||||
// very annoying but zerolog doesn't allow us to change one color without changing all of them
|
||||
// these are the default colors for each level, except for warn
|
||||
levelColors := map[string]string{
|
||||
"trace": "\033[35m", // magenta
|
||||
"debug": "\033[33m", // yellow
|
||||
"info": "\033[32m", // green
|
||||
"warn": "\033[33m", // yellow (this one is custom, the default is red \033[31m)
|
||||
"error": "\033[31m", // red
|
||||
"fatal": "\033[31m", // red
|
||||
"panic": "\033[31m", // red
|
||||
}
|
||||
|
||||
// map full level names to abbreviated forms (default zerolog behavior)
|
||||
// see consoleDefaultFormatLevel, in zerolog for example
|
||||
levelAbbrev := map[string]string{
|
||||
"trace": "TRC",
|
||||
"debug": "DBG",
|
||||
"info": "INF",
|
||||
"warn": "WRN",
|
||||
"error": "ERR",
|
||||
"fatal": "FTL",
|
||||
"panic": "PNC",
|
||||
}
|
||||
|
||||
// default to stderr
|
||||
if logWriter == nil {
|
||||
logWriter = os.Stderr
|
||||
}
|
||||
|
||||
logger := log.Output(zerolog.ConsoleWriter{
|
||||
Out: logWriter,
|
||||
TimeFormat: time.RFC3339,
|
||||
|
||||
FormatLevel: func(i interface{}) string {
|
||||
level := fmt.Sprintf("%s", i)
|
||||
color := levelColors[level]
|
||||
if color == "" {
|
||||
color = "\033[0m" // no color for unknown levels
|
||||
}
|
||||
abbrev := levelAbbrev[level]
|
||||
if abbrev == "" {
|
||||
abbrev = strings.ToUpper(level) // fallback to uppercase if unknown
|
||||
}
|
||||
return color + abbrev + "\033[0m"
|
||||
},
|
||||
})
|
||||
|
||||
if logLevel != "" {
|
||||
level, err := zerolog.ParseLevel(string(logLevel))
|
||||
if err != nil {
|
||||
logger.Warn().Msgf("Invalid log level: %s", logLevel)
|
||||
} else {
|
||||
logger = logger.Level(level)
|
||||
logger.Debug().Msgf("Infisical SDK log level set to %s", logLevel)
|
||||
}
|
||||
} else {
|
||||
logger = logger.Level(zerolog.InfoLevel)
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
// autoTokenRefreshEnabled returns true if AutoTokenRefresh is unset (nil) or
|
||||
// explicitly set to true. nil is treated as the default-on case so that
|
||||
// callers who omit the field keep the historical behavior, while callers that
|
||||
// pass a pointer to false can actually disable the background refresh.
|
||||
func autoTokenRefreshEnabled(cfg Config) bool {
|
||||
return cfg.AutoTokenRefresh == nil || *cfg.AutoTokenRefresh
|
||||
}
|
||||
|
||||
func setDefaults(cfg *Config) {
|
||||
t := reflect.TypeOf(*cfg) // we need to dereference the pointer to get the struct type
|
||||
v := reflect.ValueOf(cfg).Elem()
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
defaultVal := field.Tag.Get("default")
|
||||
if defaultVal == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Int:
|
||||
if v.Field(i).Int() == 0 {
|
||||
val, _ := strconv.Atoi(defaultVal)
|
||||
v.Field(i).SetInt(int64(val))
|
||||
}
|
||||
case reflect.String:
|
||||
if v.Field(i).String() == "" {
|
||||
v.Field(i).SetString(defaultVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BoolPtr returns a pointer to the given bool value. Use this helper when
|
||||
// setting AutoTokenRefresh in Config, e.g.: AutoTokenRefresh: infisical.BoolPtr(false).
|
||||
func BoolPtr(v bool) *bool { return &v }
|
||||
|
||||
func (c *InfisicalClient) setAccessToken(tokenDetails MachineIdentityCredential, credential interface{}, authMethod util.AuthMethod) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.tokenDetails = tokenDetails
|
||||
c.lastFetchedTime = time.Now()
|
||||
|
||||
if c.authMethod != authMethod || c.firstFetchedTime.IsZero() {
|
||||
c.firstFetchedTime = time.Now()
|
||||
c.authMethod = authMethod
|
||||
}
|
||||
|
||||
c.credential = credential
|
||||
c.httpClient.SetAuthScheme("Bearer")
|
||||
c.httpClient.SetAuthToken(c.tokenDetails.AccessToken)
|
||||
}
|
||||
|
||||
func (c *InfisicalClient) clearAccessToken() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.tokenDetails = MachineIdentityCredential{}
|
||||
c.authMethod = ""
|
||||
c.httpClient.SetAuthScheme("")
|
||||
c.httpClient.SetAuthToken("")
|
||||
}
|
||||
func (c *InfisicalClient) setPlainAccessToken(accessToken string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.authMethod = util.ACCESS_TOKEN
|
||||
c.httpClient.SetAuthScheme("Bearer")
|
||||
c.httpClient.SetAuthToken(accessToken)
|
||||
|
||||
c.tokenDetails.AccessToken = accessToken
|
||||
c.credential = models.AccessTokenCredential{AccessToken: accessToken}
|
||||
}
|
||||
|
||||
func NewInfisicalClient(context context.Context, config Config) InfisicalClientInterface {
|
||||
logger := setupLogger(config.LogLevel, config.LogWriter)
|
||||
|
||||
client := &InfisicalClient{
|
||||
logger: logger,
|
||||
}
|
||||
setDefaults(&config)
|
||||
client.UpdateConfiguration(config) // set httpClient and config
|
||||
|
||||
// add interfaces here
|
||||
client.secrets = NewSecrets(client)
|
||||
client.folders = NewFolders(client)
|
||||
client.auth = NewAuth(client)
|
||||
client.dynamicSecrets = NewDynamicSecrets(client)
|
||||
client.kms = NewKms(client)
|
||||
client.ssh = NewSsh(client)
|
||||
if config.CacheExpiryInSeconds != 0 {
|
||||
// hard limit set at 1000 cache items until forced eviction
|
||||
client.cache = expirable.NewLRU[string, interface{}](1000, nil, time.Second*time.Duration(config.CacheExpiryInSeconds))
|
||||
}
|
||||
|
||||
if autoTokenRefreshEnabled(config) {
|
||||
go client.handleTokenLifeCycle(context)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *InfisicalClient) UpdateConfiguration(config Config) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
setDefaults(&config)
|
||||
config.SiteUrl = util.AppendAPIEndpoint(config.SiteUrl)
|
||||
c.config = config
|
||||
|
||||
if c.httpClient == nil {
|
||||
c.httpClient = resty.New().
|
||||
SetHeader("User-Agent", config.UserAgent).
|
||||
SetBaseURL(config.SiteUrl)
|
||||
|
||||
maxRetries := 3
|
||||
maxWaitTime := 30 * time.Second
|
||||
|
||||
if config.RetryRequestsConfig != nil && config.RetryRequestsConfig.ExponentialBackoff != nil {
|
||||
maxRetries = config.RetryRequestsConfig.ExponentialBackoff.MaxRetries
|
||||
maxWaitTime = 10 * time.Minute
|
||||
}
|
||||
|
||||
c.httpClient.SetRetryCount(maxRetries).
|
||||
SetRetryWaitTime(1 * time.Second).
|
||||
SetRetryMaxWaitTime(maxWaitTime).
|
||||
SetRetryAfter(func(rc *resty.Client, r *resty.Response) (time.Duration, error) {
|
||||
|
||||
if config.RetryRequestsConfig != nil && config.RetryRequestsConfig.ExponentialBackoff != nil {
|
||||
delay := config.RetryRequestsConfig.ExponentialBackoff.GetDelay(r.Request.Attempt)
|
||||
if !config.SilentMode {
|
||||
util.PrintWarning(c.logger, fmt.Sprintf("Request failed, [url=%s] [status=%d] [method=%s]\nRetrying in %s (attempt %d)", r.Request.URL, r.StatusCode(), r.Request.Method, delay.String(), r.Request.Attempt))
|
||||
}
|
||||
return delay, nil
|
||||
}
|
||||
|
||||
attempt := r.Request.Attempt + 1
|
||||
if attempt <= 0 {
|
||||
attempt = 1
|
||||
}
|
||||
waitTime := math.Min(float64(rc.RetryWaitTime)*math.Pow(2, float64(attempt-1)), float64(rc.RetryMaxWaitTime))
|
||||
|
||||
// Add jitter of +/-20%
|
||||
jitterFactor := 0.8 + (rand.Float64() * 0.4)
|
||||
waitTime = waitTime * jitterFactor
|
||||
|
||||
waitDuration := time.Duration(waitTime)
|
||||
return waitDuration, nil
|
||||
}).
|
||||
AddRetryCondition(func(r *resty.Response, err error) bool {
|
||||
// don't retry if there's no error or it's a timeout
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return false
|
||||
}
|
||||
|
||||
if err == nil && r == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if config.RetryRequestsConfig != nil && config.RetryRequestsConfig.ExponentialBackoff != nil {
|
||||
if (r != nil && r.IsError()) || err != nil {
|
||||
return r.Request.Attempt <= config.RetryRequestsConfig.ExponentialBackoff.MaxRetries
|
||||
}
|
||||
}
|
||||
|
||||
networkErrors := []string{
|
||||
"connection refused",
|
||||
"connection reset",
|
||||
"network",
|
||||
"connection",
|
||||
"no such host",
|
||||
"i/o timeout",
|
||||
"dial tcp",
|
||||
"broken pipe",
|
||||
"wsaetimeout",
|
||||
"wsaeconnreset",
|
||||
"econnreset",
|
||||
"econnrefused",
|
||||
"ehostunreach",
|
||||
"enetunreach",
|
||||
}
|
||||
|
||||
isConditionMet := false
|
||||
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) {
|
||||
return true
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
for _, netErr := range networkErrors {
|
||||
errMsg := err.Error()
|
||||
|
||||
if strings.Contains(strings.ToLower(errMsg), netErr) {
|
||||
isConditionMet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isConditionMet
|
||||
|
||||
})
|
||||
|
||||
// OnBeforeRequest hook to validate and refresh token before each request.
|
||||
// This is a safety net to catch cases where the background token lifecycle
|
||||
// goroutine might miss a refresh window due to timing issues (GC pauses,
|
||||
// CPU contention, etc.). Most requests will not trigger a refresh here
|
||||
// because the background goroutine handles proactive token management.
|
||||
if autoTokenRefreshEnabled(config) {
|
||||
c.httpClient.OnBeforeRequest(c.beforeRequestAuthInterceptor)
|
||||
}
|
||||
} else {
|
||||
c.httpClient.
|
||||
SetHeader("User-Agent", config.UserAgent).
|
||||
SetBaseURL(config.SiteUrl)
|
||||
}
|
||||
|
||||
if len(config.CustomHeaders) > 0 {
|
||||
c.httpClient.SetHeaders(config.CustomHeaders)
|
||||
}
|
||||
|
||||
if config.CaCertificate != "" {
|
||||
caCertPool, err := x509.SystemCertPool()
|
||||
if err != nil && !config.SilentMode {
|
||||
util.PrintWarning(c.logger, fmt.Sprintf("failed to load system root CA pool: %v", err))
|
||||
}
|
||||
|
||||
if ok := caCertPool.AppendCertsFromPEM([]byte(config.CaCertificate)); !ok && !config.SilentMode {
|
||||
util.PrintWarning(c.logger, "failed to append CA certificate")
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
|
||||
c.httpClient.SetTLSClientConfig(tlsConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *InfisicalClient) Secrets() SecretsInterface {
|
||||
return c.secrets
|
||||
}
|
||||
|
||||
func (c *InfisicalClient) Folders() FoldersInterface {
|
||||
return c.folders
|
||||
}
|
||||
|
||||
func (c *InfisicalClient) Auth() AuthInterface {
|
||||
return c.auth
|
||||
}
|
||||
|
||||
func (c *InfisicalClient) DynamicSecrets() DynamicSecretsInterface {
|
||||
return c.dynamicSecrets
|
||||
}
|
||||
|
||||
func (c *InfisicalClient) Kms() KmsInterface {
|
||||
return c.kms
|
||||
}
|
||||
|
||||
func (c *InfisicalClient) Ssh() SshInterface {
|
||||
return c.ssh
|
||||
}
|
||||
|
||||
func (c *InfisicalClient) handleTokenLifeCycle(context context.Context) {
|
||||
var warningPrinted = false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-context.Done():
|
||||
return // The context has been cancelled, clean up and return from the loop to stop the goroutine
|
||||
default:
|
||||
c.mu.RLock()
|
||||
config := c.config
|
||||
authMethod := c.authMethod
|
||||
tokenDetails := c.tokenDetails
|
||||
c.mu.RUnlock()
|
||||
|
||||
if autoTokenRefreshEnabled(config) && authMethod != "" && authMethod != util.ACCESS_TOKEN {
|
||||
// Print warning once for short TTLs
|
||||
if !config.SilentMode && !warningPrinted && tokenDetails.AccessTokenMaxTTL != 0 && tokenDetails.ExpiresIn != 0 {
|
||||
if tokenDetails.AccessTokenMaxTTL < 60 || tokenDetails.ExpiresIn < 60 {
|
||||
util.PrintWarning(c.logger, "Machine Identity access token TTL or max TTL is less than 60 seconds. This may cause excessive API calls, and you may be subject to rate-limits.")
|
||||
}
|
||||
warningPrinted = true
|
||||
}
|
||||
|
||||
// Check if token needs refresh (using the same buffer as OnBeforeRequest)
|
||||
if c.isTokenExpiringSoon(renewalBufferSeconds) {
|
||||
// Use refreshTokenSynchronously which handles both renewal and re-auth
|
||||
// Pass false for manualTrigger since this is from the background goroutine
|
||||
if err := c.refreshTokenSynchronously(false); err != nil {
|
||||
c.logger.Debug().Msgf("Background token refresh failed: %s", err.Error())
|
||||
}
|
||||
|
||||
// Re-read token details after refresh attempt
|
||||
c.mu.RLock()
|
||||
tokenDetails = c.tokenDetails
|
||||
c.mu.RUnlock()
|
||||
}
|
||||
|
||||
// Calculate sleep time until next check
|
||||
sleepTime := c.calculateSleepTime(tokenDetails, renewalBufferSeconds)
|
||||
|
||||
if err := util.SleepWithContext(context, sleepTime); err != nil && (err == util.ErrContextCanceled || errors.Is(err, util.ErrContextDeadlineExceeded)) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := util.SleepWithContext(context, 1*time.Second); err != nil && (err == util.ErrContextCanceled || errors.Is(err, util.ErrContextDeadlineExceeded)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user