diff --git a/packages/api/secrets/list_secrets.go b/packages/api/secrets/list_secrets.go index d6aa98e..13ab109 100644 --- a/packages/api/secrets/list_secrets.go +++ b/packages/api/secrets/list_secrets.go @@ -2,15 +2,69 @@ package api import ( "fmt" + "strings" "github.com/go-resty/resty/v2" "github.com/infisical/go-sdk/packages/errors" ) const callListSecretsV3RawOperation = "CallListSecretsV3Raw" +const callListSecretsWithETagV3RawOperation = "CallListSecretsWithETagV3Raw" -func CallListSecretsV3(httpClient *resty.Client, request ListSecretsV3RawRequest) (ListSecretsV3RawResponse, error) { +func CallListSecretsWithETagV3(httpClient *resty.Client, request ListSecretsV3RawWithETagRequest) (response ListSecretsV3RawResponse, serverETag string, isModified bool, err error) { + + secretsResponse := ListSecretsV3RawResponse{} + + if request.SecretPath == "" { + request.SecretPath = "/" + } + + if request.CurrentETag != "" { + + isWeakETag := strings.HasPrefix(request.CurrentETag, "W/") + if isWeakETag { + request.CurrentETag = strings.TrimPrefix(request.CurrentETag, "W/") + } + request.CurrentETag = strings.TrimPrefix(request.CurrentETag, "\"") + request.CurrentETag = strings.TrimSuffix(request.CurrentETag, "\"") + request.CurrentETag = fmt.Sprintf("\"%s\"", request.CurrentETag) + + if isWeakETag { + request.CurrentETag = fmt.Sprintf("W/%s", request.CurrentETag) + } + } + + res, err := httpClient.R(). + SetResult(&secretsResponse). + SetHeader("if-none-match", request.CurrentETag). + SetQueryParams(map[string]string{ + "workspaceId": request.ProjectID, + "workspaceSlug": request.ProjectSlug, + "environment": request.Environment, + "secretPath": request.SecretPath, + "expandSecretReferences": fmt.Sprintf("%t", request.ExpandSecretReferences), + "include_imports": fmt.Sprintf("%t", request.IncludeImports), + "recursive": fmt.Sprintf("%t", request.Recursive), + }).Get("/v3/secrets/raw") + + if err != nil { + return ListSecretsV3RawResponse{}, "", false, errors.NewRequestError(callListSecretsWithETagV3RawOperation, err) + } + + if res.IsError() { + return ListSecretsV3RawResponse{}, "", false, errors.NewAPIErrorWithResponse(callListSecretsWithETagV3RawOperation, res) + } + + var modified = true + if res.StatusCode() == 304 || (res.Header().Get("etag") == request.CurrentETag && request.CurrentETag != "") { + modified = false + } + + return secretsResponse, res.Header().Get("etag"), modified, nil +} + +func CallListSecretsV3(httpClient *resty.Client, request ListSecretsV3RawRequest) (ListSecretsV3RawResponse, error) { secretsResponse := ListSecretsV3RawResponse{} if request.SecretPath == "" { diff --git a/packages/api/secrets/models.go b/packages/api/secrets/models.go index 6c257b9..b381f0e 100644 --- a/packages/api/secrets/models.go +++ b/packages/api/secrets/models.go @@ -16,6 +16,19 @@ type ListSecretsV3RawRequest struct { SecretPath string `json:"secretPath,omitempty"` } +type ListSecretsV3RawWithETagRequest struct { + AttachToProcessEnv bool `json:"-"` + CurrentETag string `json:"-"` + // ProjectId and ProjectSlug are used to fetch secrets from the project. Only one of them is required. + ProjectID string `json:"workspaceId,omitempty"` + ProjectSlug string `json:"workspaceSlug,omitempty"` + Environment string `json:"environment"` + ExpandSecretReferences bool `json:"expandSecretReferences"` + IncludeImports bool `json:"include_imports"` + Recursive bool `json:"recursive"` + SecretPath string `json:"secretPath,omitempty"` +} + type ListSecretsV3RawResponse struct { Secrets []models.Secret `json:"secrets"` Imports []models.SecretImport `json:"imports"` @@ -84,3 +97,9 @@ type DeleteSecretV3RawRequest struct { type DeleteSecretV3RawResponse struct { Secret models.Secret `json:"secret"` } + +type ListSecretsWithETagResponse struct { + Secrets []models.Secret + ETag string + IsModified bool +} diff --git a/packages/models/secrets.go b/packages/models/secrets.go index ee7228c..6a9a821 100644 --- a/packages/models/secrets.go +++ b/packages/models/secrets.go @@ -12,6 +12,12 @@ type Secret struct { SecretPath string `json:"secretPath,omitempty"` } +type ListSecretsWithETagResult struct { + Secrets []Secret + ETag string + IsModified bool +} + type SecretImport struct { SecretPath string `json:"secretPath"` Environment string `json:"environment"` diff --git a/secrets.go b/secrets.go index 372dfbd..8936baa 100644 --- a/secrets.go +++ b/secrets.go @@ -9,6 +9,7 @@ import ( ) type ListSecretsOptions = api.ListSecretsV3RawRequest +type ListSecretsWithETagOptions = api.ListSecretsV3RawWithETagRequest type RetrieveSecretOptions = api.RetrieveSecretV3RawRequest type UpdateSecretOptions = api.UpdateSecretV3RawRequest type CreateSecretOptions = api.CreateSecretV3RawRequest @@ -16,6 +17,7 @@ type DeleteSecretOptions = api.DeleteSecretV3RawRequest type SecretsInterface interface { List(options ListSecretsOptions) ([]models.Secret, error) + ListWithETag(options ListSecretsWithETagOptions) (models.ListSecretsWithETagResult, error) Retrieve(options RetrieveSecretOptions) (models.Secret, error) Update(options UpdateSecretOptions) (models.Secret, error) Create(options CreateSecretOptions) (models.Secret, error) @@ -64,6 +66,51 @@ func (s *Secrets) List(options ListSecretsOptions) ([]models.Secret, error) { return util.SortSecretsByKeys(secrets), nil } +func (s *Secrets) ListWithETag(options ListSecretsWithETagOptions) (models.ListSecretsWithETagResult, error) { + res, etag, isModified, err := api.CallListSecretsWithETagV3(s.client.httpClient, options) + + if err != nil { + return models.ListSecretsWithETagResult{}, err + } + + if options.Recursive { + util.EnsureUniqueSecretsByKey(&res.Secrets) + } + + secrets := append([]models.Secret(nil), res.Secrets...) // Clone main secrets slice, we will modify this if imports are enabled + if options.IncludeImports { + + // Append secrets from imports + for _, importBlock := range res.Imports { + for _, importSecret := range importBlock.Secrets { + // Only append the secret if it is not already in the list, imports take precedence + if !util.ContainsSecret(secrets, importSecret.SecretKey) { + secrets = append(secrets, importSecret) + } + } + } + } + + if options.AttachToProcessEnv { + for _, secret := range secrets { + // Only set the environment variable if it is not already set + if os.Getenv(secret.SecretKey) == "" { + os.Setenv(secret.SecretKey, secret.SecretValue) + } + } + + } + + sortedSecrets := util.SortSecretsByKeys(secrets) + + return models.ListSecretsWithETagResult{ + Secrets: sortedSecrets, + ETag: etag, + IsModified: isModified, + }, nil + +} + func (s *Secrets) Retrieve(options RetrieveSecretOptions) (models.Secret, error) { res, err := api.CallRetrieveSecretV3(s.client.httpClient, options)