diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 184874f8a..9eab5e759 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -434,6 +434,34 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques return getRawSecretsV3Response, nil } +func CallFetchSingleSecretByName(httpClient *resty.Client, request GetRawSecretV3ByNameRequest) (GetRawSecretV3ByNameResponse, error) { + var getRawSecretV3ByNameResponse GetRawSecretV3ByNameResponse + response, err := httpClient. + R(). + SetHeader("User-Agent", USER_AGENT). + SetResult(&getRawSecretV3ByNameResponse). + SetBody(request). + SetQueryParam("expandSecretReferences", "true"). + SetQueryParam("include_imports", "true"). + SetQueryParam("environment", request.Environment). + SetQueryParam("secretPath", request.SecretPath). + SetQueryParam("workspaceId", request.WorkspaceID). + SetQueryParam("type", "shared"). + Get(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return GetRawSecretV3ByNameResponse{}, fmt.Errorf("CallFetchSingleSecretByName: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return GetRawSecretV3ByNameResponse{}, fmt.Errorf("CallFetchSingleSecretByName: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + getRawSecretV3ByNameResponse.ETag = response.Header().Get(("etag")) + + return getRawSecretV3ByNameResponse, nil +} + func CallCreateDynamicSecretLeaseV1(httpClient *resty.Client, request CreateDynamicSecretLeaseV1Request) (CreateDynamicSecretLeaseV1Response, error) { var createDynamicSecretLeaseResponse CreateDynamicSecretLeaseV1Response response, err := httpClient. diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index e09f2275f..dc577bf9d 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -590,3 +590,25 @@ type GetRawSecretsV3Response struct { Imports []ImportedRawSecretV3 `json:"imports"` ETag string } + +type GetRawSecretV3ByNameRequest struct { + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Type string `json:"type,omitempty"` + Environment string `json:"environment"` + SecretPath string `json:"secretPath,omitempty"` +} + +type GetRawSecretV3ByNameResponse struct { + Secret struct { + ID string `json:"_id"` + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKey string `json:"secretKey"` + SecretValue string `json:"secretValue"` + SecretComment string `json:"secretComment"` + } `json:"secret"` + ETag string +} diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index 267b22df8..59e496ec7 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -327,6 +327,21 @@ func secretTemplateFunction(accessToken string, existingEtag string, currentEtag } } +func getSingleSecretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string, string) (models.SingleEnvironmentVariable, error) { + return func(projectID, envSlug, secretPath, secretName string) (models.SingleEnvironmentVariable, error) { + secret, requestEtag, err := util.GetSinglePlainTextSecretByNameV3(accessToken, projectID, envSlug, secretPath, secretName) + if err != nil { + return models.SingleEnvironmentVariable{}, err + } + + if existingEtag != requestEtag { + *currentEtag = requestEtag + } + + return secret, nil + } +} + func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *DynamicSecretLeaseManager, templateId int) func(...string) (map[string]interface{}, error) { return func(args ...string) (map[string]interface{}, error) { argLength := len(args) @@ -358,9 +373,12 @@ func ProcessTemplate(templateId int, templatePath string, data interface{}, acce // custom template function to fetch secrets from Infisical secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretManager, templateId) + getSingleSecretFunction := getSingleSecretTemplateFunction(accessToken, existingEtag, currentEtag) funcs := template.FuncMap{ - "secret": secretFunction, - "dynamic_secret": dynamicSecretFunction, + "secret": secretFunction, // depreciated + "listSecrets": secretFunction, + "dynamic_secret": dynamicSecretFunction, + "getSecretByName": getSingleSecretFunction, "minus": func(a, b int) int { return a - b }, diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 4b02cb6f8..9bdc26fbc 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -35,6 +35,7 @@ type SingleEnvironmentVariable struct { Workspace string `json:"workspace"` } `json:"tags"` Comment string `json:"comment"` + Etag string `json:"Etag"` } type PlaintextSecretResult struct { diff --git a/cli/packages/util/agent.go b/cli/packages/util/agent.go index 188ae5de2..215e43551 100644 --- a/cli/packages/util/agent.go +++ b/cli/packages/util/agent.go @@ -24,7 +24,7 @@ func ConvertPollingIntervalToTime(pollingInterval string) (time.Duration, error) switch unit { case "s": if number < 60 { - return 0, fmt.Errorf("polling interval should be at least 60 seconds") + return 0, fmt.Errorf("polling interval must be at least 60 seconds") } return time.Duration(number) * time.Second, nil case "m": diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 53c11abbf..c51338d61 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -118,6 +118,36 @@ func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentNa }, nil } +func GetSinglePlainTextSecretByNameV3(accessToken string, workspaceId string, environmentName string, secretsPath string, secretName string) (models.SingleEnvironmentVariable, string, error) { + httpClient := resty.New() + httpClient.SetAuthToken(accessToken). + SetHeader("Accept", "application/json") + + getSecretsRequest := api.GetRawSecretV3ByNameRequest{ + WorkspaceID: workspaceId, + Environment: environmentName, + SecretName: secretName, + SecretPath: secretsPath, + } + + rawSecret, err := api.CallFetchSingleSecretByName(httpClient, getSecretsRequest) + + if err != nil { + return models.SingleEnvironmentVariable{}, "", err + } + + formattedSecrets := models.SingleEnvironmentVariable{ + Key: rawSecret.Secret.SecretKey, + WorkspaceId: rawSecret.Secret.Workspace, + Value: rawSecret.Secret.SecretValue, + Type: rawSecret.Secret.Type, + ID: rawSecret.Secret.ID, + Comment: rawSecret.Secret.SecretComment, + } + + return formattedSecrets, rawSecret.ETag, nil +} + func CreateDynamicSecretLease(accessToken string, projectSlug string, environmentName string, secretsPath string, slug string, ttl string) (models.DynamicSecretLease, error) { httpClient := resty.New() httpClient.SetAuthToken(accessToken). diff --git a/docs/integrations/platforms/infisical-agent.mdx b/docs/integrations/platforms/infisical-agent.mdx index b397b7352..bb1261208 100644 --- a/docs/integrations/platforms/infisical-agent.mdx +++ b/docs/integrations/platforms/infisical-agent.mdx @@ -252,7 +252,7 @@ To install the Infisical agent, you must first install the [Infisical CLI](../cl Once you have the CLI installed, you will need to provision programmatic access for the agent via [Universal Auth](/documentation/platform/identities/universal-auth). To obtain a **Client ID** and a **Client Secret**, follow the step by step guide outlined [here](/documentation/platform/identities/universal-auth). -Next, create agent config file as shown below. +Next, create agent config file as shown below. The example agent configuration file that defines the token authentication method, one sink location, and a secret template. ```yaml example-agent-config-file.yaml infisical: @@ -277,8 +277,8 @@ templates: command: ./reload-app.sh ``` -Above is an example agent configuration file that defines the token authentication method, one sink location (where to deposit access tokens after renewal) and a secret template. - +The secret template below will be used to render the secrets with the key and the value separated by `=` sign. You'll notice that a custom function named `secret` is used to fetch the secrets. +This function takes the following arguments: `secret "" "" ""`. ```text my-dot-ev-secret-template {{- with secret "6553ccb2b7da580d7f6e7260" "dev" "/" }} @@ -288,11 +288,42 @@ Above is an example agent configuration file that defines the token authenticati {{- end }} ``` -The secret template above will be used to render the secrets where the key and the value are separated by `=` sign. You'll notice that a custom function named `secret` is used to fetch the secrets. -This function takes the following arguments: `secret "" "" ""`. +After defining the agent configuration file, run the command below pointing to the path where the agent configuration file is located. + ```bash infisical agent --config example-agent-config-file.yaml ``` -After defining the agent configuration file, run the command above pointing to the path where the agent configuration is located. + +### Available secret template functions + + ```bash + listSecrets "" "environment-slug" "" + ``` + ```bash example-template-usage + {{- with listSecrets "6553ccb2b7da580d7f6e7260" "dev" "/" }} + {{- range . }} + {{ .Key }}={{ .Value }} + {{- end }} + {{- end }} + ``` + This function can be used to render the full list of secrets within a given project, environment and secret path. + + + + + ```bash + getSecretByName "" "" "" "" + ``` + + ```bash example-template-usage + {{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}} + {{ if .Value }} + password = "{{ .Value }}" + {{ end }} + {{ end }} + ``` + + This function can be used to render a single secret by it's name. +