Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for Doppler #3134

Merged
merged 1 commit into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ sections:
type: bool
default: '`true`'
description: Show script contents
doppler:
args:
type: '[]string'
description: Extra args to Doppler CLI command
command:
default: '`doppler`'
description: Doppler CLI command
config:
type: string
description: Default config (aka environment) if none is specified
project:
type: string
description: Default project name if none is specified
edit:
apply:
type: bool
Expand Down
15 changes: 15 additions & 0 deletions assets/chezmoi.io/docs/reference/templates/doppler/doppler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# `doppler` *key* [*project* [*config*]]

`doppler` returns the secret for the specified project and configuration
from [Doppler](https://www.doppler.com) using `doppler secrets download --json --no-file`.

If either of *project* or *config* are empty or
omitted, then chezmoi will use the value from the
`doppler.project` and
`doppler.config` config variables if they are set and not empty.
equals03 marked this conversation as resolved.
Show resolved Hide resolved

!!! example

```
{{ doppler "SECRET_NAME" "project_name" "configuration_name" }}
```
halostatue marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# `dopplerProjectJson` [*project* [*config*]]

`dopplerProjectJson` returns the secret for the specified project and configuration
from [Doppler](https://www.doppler.com) using `doppler secrets download --json --no-file`
as `json` structured data.

If either of *project* or *config* are empty or
omitted, then chezmoi will use the value from the
`doppler.project` and
`doppler.config` config variables if they are set and not empty.

!!! example

```
{{ (dopplerProjectJson "project_name" "configuration_name").SECRET_NAME }}
```
9 changes: 9 additions & 0 deletions assets/chezmoi.io/docs/reference/templates/doppler/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Doppler

chezmoi includes support for [Doppler](https://www.doppler.com) using the `doppler`
CLI to expose data through the `doppler` and `dopplerProjectJson`
template functions.

!!! warning

Doppler is in beta and chezmoi's interface to it may change.
23 changes: 12 additions & 11 deletions assets/chezmoi.io/docs/user-guide/password-managers/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ configuration file. You can then invoke this command with the `secret` and
output respectively. All of the above secret managers can be supported in this
way:

| Secret Manager | `secret.command` | Template skeleton |
| ----------------- | ---------------- | -------------------------------------------------- |
| 1Password | `op` | `{{ secretJSON "get" "item" "$ID" }}` |
| Bitwarden | `bw` | `{{ secretJSON "get" "$ID" }}` |
| HashiCorp Vault | `vault` | `{{ secretJSON "kv" "get" "-format=json" "$ID" }}` |
| HCP Vault Secrets | `vlt` | `{{ secret "secrets" "get" "--plaintext" "$ID }}` |
| LastPass | `lpass` | `{{ secretJSON "show" "--json" "$ID" }}` |
| KeePassXC | `keepassxc-cli` | Not possible (interactive command only) |
| Keeper | `keeper` | `{{ secretJSON "get" "--format=json" "$ID" }}` |
| pass | `pass` | `{{ secret "show" "$ID" }}` |
| passhole | `ph` | `{{ secret "$ID" "password" }}` |
| Secret Manager | `secret.command` | Template skeleton |
| ----------------- | ---------------- | ---------------------------------------------------------------- |
| 1Password | `op` | `{{ secretJSON "get" "item" "$ID" }}` |
| Bitwarden | `bw` | `{{ secretJSON "get" "$ID" }}` |
| Doppler | `doppler` | `{{ secretJSON "secrets" "download" "--json" "--no-file" }}` |
| HashiCorp Vault | `vault` | `{{ secretJSON "kv" "get" "-format=json" "$ID" }}` |
| HCP Vault Secrets | `vlt` | `{{ secret "secrets" "get" "--plaintext" "$ID }}` |
| LastPass | `lpass` | `{{ secretJSON "show" "--json" "$ID" }}` |
| KeePassXC | `keepassxc-cli` | Not possible (interactive command only) |
| Keeper | `keeper` | `{{ secretJSON "get" "--format=json" "$ID" }}` |
| pass | `pass` | `{{ secret "show" "$ID" }}` |
| passhole | `ph` | `{{ secret "$ID" "password" }}` |
58 changes: 58 additions & 0 deletions assets/chezmoi.io/docs/user-guide/password-managers/doppler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Doppler

chezmoi includes support for [Doppler](https://www.doppler.com) using the `doppler`
CLI to expose data through the `doppler` and `dopplerProjectJson`
template functions.

!!! warning

Doppler is in beta and chezmoi's interface to it may change.
Note: Doppler only supports secrets in the `UPPER_SNAKE_CASE` format.

Log in using:

```console
$ doppler login
```

It is now possible to interact with the `doppler` CLI in two different, but similar, ways.
Both make use of the command `doppler secrets download --json --no-file` behind the scenes
but present a different experience.

The `doppler` function is used in the following way:
```
{{ doppler "SECRET_NAME" "project name" "config" }}
```

All secrets from the specified project/config combination are cached for subsequent access and
will not requery the `doppler` CLI for another secret in the same project/config.
This caching mechanism enhances performance and reduces unnecessary CLI calls.

The `dopplerProjectJson` presents the secrets as `json` structured data and is used in the following
way:
```
{{ (dopplerProjectJson "project" "config").PASSWORD }}
```

Additionally one can set the default values for the project and
config (aka environment) in your config file, for example:

```toml title="~/.config/chezmoi/chezmoi.toml"
[doppler]
project = "my-project"
config = "dev"
```
With these default values, you can omit them in the call to both `doppler` and `dopplerProjectJson`,
for example:
```
{{ doppler "SECRET_NAME" }}
{{ dopplerProjectJson.SECRET_NAME }}
```

It is important to note that neither of the above parse any individual secret as `json`.
This can be achieved by using the `fromJson` function, for example:
```
{{ (doppler "SECRET_NAME" | fromJson).created_by.email_address }}
{{ (dopplerProjectJson.SECRET_NAME | fromJson).created_by.email_address }}
```
Obviously the secret would have to be saved in `json` format for this to work as expected.
4 changes: 2 additions & 2 deletions assets/chezmoi.io/docs/what-does-chezmoi-do.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ format of your choice. chezmoi can retrieve secrets from
Manager](https://aws.amazon.com/secrets-manager/),
[Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/),
[Bitwarden](https://bitwarden.com/), [Dashlane](https://www.dashlane.com/),
[gopass](https://www.gopass.pw/), [HCP Vault
Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets),
[Doppler](https://www.doppler.com), [gopass](https://www.gopass.pw/),
[HCP Vault Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets),
[KeePassXC](https://keepassxc.org/), [Keeper](https://www.keepersecurity.com/),
[LastPass](https://lastpass.com/), [pass](https://www.passwordstore.org/),
[passhole](https://github.com/Evidlo/passhole),
Expand Down
4 changes: 4 additions & 0 deletions assets/chezmoi.io/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ nav:
- reference/templates/dashlane-functions/index.md
- dashlaneNote: reference/templates/dashlane-functions/dashlaneNote.md
- dashlanePassword: reference/templates/dashlane-functions/dashlanePassword.md
- Doppler functions:
- reference/templates/doppler-functions/index.md
- doppler: reference/templates/doppler-functions/doppler.md
- dopplerProjectJson: reference/templates/doppler-functions/dopplerProjectJson.md
- ejson functions:
- reference/templates/ejson-functions/index.md
- ejsonDecrypt: reference/templates/ejson-functions/ejsonDecrypt.md
Expand Down
6 changes: 6 additions & 0 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type ConfigFile struct {
AzureKeyVault azureKeyVaultConfig `json:"azureKeyVault" mapstructure:"azureKeyVault" yaml:"azureKeyVault"`
Bitwarden bitwardenConfig `json:"bitwarden" mapstructure:"bitwarden" yaml:"bitwarden"`
Dashlane dashlaneConfig `json:"dashlane" mapstructure:"dashlane" yaml:"dashlane"`
Doppler dopplerConfig `json:"doppler" mapstructure:"doppler" yaml:"doppler"`
Ejson ejsonConfig `json:"ejson" mapstructure:"ejson" yaml:"ejson"`
Gopass gopassConfig `json:"gopass" mapstructure:"gopass" yaml:"gopass"`
HCPVaultSecrets hcpVaultSecretConfig `json:"hcpVaultSecrets" mapstructure:"hcpVaultSecrets" yaml:"hcpVaultSecrets"`
Expand Down Expand Up @@ -396,6 +397,8 @@ func newConfig(options ...configOption) (*Config, error) {
"dashlanePassword": c.dashlanePasswordTemplateFunc,
"decrypt": c.decryptTemplateFunc,
"deleteValueAtPath": c.deleteValueAtPathTemplateFunc,
"doppler": c.dopplerTemplateFunc,
"dopplerProjectJson": c.dopplerProjectJSONTemplateFunc,
"ejsonDecrypt": c.ejsonDecryptTemplateFunc,
"ejsonDecryptWithKey": c.ejsonDecryptWithKeyTemplateFunc,
"encrypt": c.encryptTemplateFunc,
Expand Down Expand Up @@ -2588,6 +2591,9 @@ func newConfigFile(bds *xdg.BaseDirectorySpecification) ConfigFile {
Dashlane: dashlaneConfig{
Command: "dcli",
},
Doppler: dopplerConfig{
Command: "doppler",
},
Ejson: ejsonConfig{
KeyDir: firstNonEmptyString(os.Getenv("EJSON_KEYDIR"), "/opt/ejson/keys"),
},
Expand Down
8 changes: 8 additions & 0 deletions internal/cmd/doctorcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,14 @@ func (c *Config) runDoctorCmd(cmd *cobra.Command, args []string) error {
versionArgs: []string{"--version"},
versionRx: regexp.MustCompile(`^(\d+\.\d+\.\d+)`),
},
&binaryCheck{
name: "doppler-command",
binaryname: c.Doppler.Command,
ifNotSet: checkResultWarning,
ifNotExist: checkResultInfo,
versionArgs: []string{"--version"},
versionRx: regexp.MustCompile(`^v(\d+\.\d+\.\d+)`),
},
&binaryCheck{
name: "gopass-command",
binaryname: c.Gopass.Command,
Expand Down
111 changes: 111 additions & 0 deletions internal/cmd/dopplertemplatefuncs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"

"golang.org/x/exp/slices"

"github.com/twpayne/chezmoi/v2/internal/chezmoilog"
)

type dopplerConfig struct {
Command string `json:"command" mapstructure:"command" yaml:"command"`
Args []string `json:"args" mapstructure:"args" yaml:"args"`
Project string `json:"project" mapstructure:"project" yaml:"project"`
Config string `json:"config" mapstructure:"config" yaml:"config"`
outputCache map[string][]byte
}

func (c *Config) dopplerTemplateFunc(key string, additionalArgs ...string) any {
if len(additionalArgs) > 2 {
// Add one to the number of received arguments as the key
// is the first argument.
panic(fmt.Errorf("expected 1 to 3 arguments, got %d", len(additionalArgs)+1))
}

args := c.appendDopplerAdditionalArgs(
[]string{"secrets", "download", "--json", "--no-file"},
additionalArgs,
)

data, err := c.dopplerOutput(args)
if err != nil {
panic(err)
}
var value map[string]any
if err := json.Unmarshal(data, &value); err != nil {
panic(err)
}

secret, ok := value[key]
if !ok {
panic(fmt.Errorf("could not find requested secret: %s", key))
}

return secret
}

func (c *Config) dopplerProjectJSONTemplateFunc(additionalArgs ...string) any {
if len(additionalArgs) > 2 {
panic(fmt.Errorf("expected 0 to 2 arguments, got %d", len(additionalArgs)))
}
args := c.appendDopplerAdditionalArgs(
[]string{"secrets", "download", "--json", "--no-file"},
additionalArgs,
)

data, err := c.dopplerOutput(args)
if err != nil {
panic(err)
}
var value any
if err := json.Unmarshal(data, &value); err != nil {
panic(err)
}
return value
}

func (c *Config) appendDopplerAdditionalArgs(
args, additionalArgs []string,
) []string {
if len(additionalArgs) > 0 && additionalArgs[0] != "" {
args = append(args, "--project", additionalArgs[0])
} else if c.Doppler.Project != "" {
args = append(args, "--project", c.Doppler.Project)
}
if len(additionalArgs) > 1 && additionalArgs[1] != "" {
args = append(args, "--config", additionalArgs[1])
} else if c.Doppler.Config != "" {
args = append(args, "--config", c.Doppler.Config)
}

return args
}

func (c *Config) dopplerOutput(args []string) ([]byte, error) {
args = append(slices.Clone(c.Doppler.Args), args...)
key := strings.Join(args, "\x00")
if data, ok := c.Doppler.outputCache[key]; ok {
return data, nil
}
cmd := exec.Command(c.Doppler.Command, args...) //nolint:gosec
// Always run the doppler command in the destination path because doppler uses
// relative paths to find its .doppler.json config file.
cmd.Dir = c.DestDirAbsPath.String()
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
output, err := chezmoilog.LogCmdOutput(cmd)
if err != nil {
return nil, newCmdOutputError(cmd, output, err)
}

if c.Doppler.outputCache == nil {
c.Doppler.outputCache = make(map[string][]byte)
}
c.Doppler.outputCache[key] = output
return output, nil
}
6 changes: 6 additions & 0 deletions internal/cmd/testdata/scripts/doctor_unix.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
chmod 755 bin/age
chmod 755 bin/bw
chmod 755 bin/dcli
chmod 755 bin/doppler
chmod 755 bin/git
chmod 755 bin/gopass
chmod 755 bin/gpg
Expand Down Expand Up @@ -48,6 +49,7 @@ stdout '^ok\s+pinentry-command\s+'
stdout '^ok\s+1password-command\s+'
stdout '^ok\s+bitwarden-command\s+'
stdout '^ok\s+dashlane-command\s+'
stdout '^ok\s+doppler-command\s+'
stdout '^ok\s+gopass-command\s+'
stdout '^ok\s+keepassxc-command\s+'
stdout '^info\s+keepassxc-db\s+'
Expand Down Expand Up @@ -93,6 +95,10 @@ echo "1.12.1"
#!/bin/sh

echo 1.0.0
-- bin/doppler --
#!/bin/sh

echo "v3.65.1"
-- bin/git --
#!/bin/sh

Expand Down
Loading
Loading