diff --git a/README.md b/README.md index 74be8364..95aa0782 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ It supports various backends including: - Conjur - HCP Vault Secrets - Bitwarden +- [Yandex Cloud Lockbox](https://yandex.cloud/en/docs/lockbox/) - HTTP JSON - Keychain @@ -246,6 +247,8 @@ Please see the [relevant unit test cases](https://github.com/helmfile/vals/blob/ - [Conjur](#conjur) - [HCP Vault Secrets](#hcp-vault-secrets) - [Bitwarden](#bitwarden) + - [Yandex Cloud Lockbox](#yandex-cloud-lockbox) + - [Authentication](#authentication-2) - [HTTP JSON](#http-json) - [Fetch string value](#fetch-string-value) - [Fetch integer value](#fetch-integer-value) @@ -898,6 +901,23 @@ Examples: - `ref+bw://4d084b01-87e7-4411-8de9-2476ab9f3f48/{username,password,uri,notes,item}` gets username, password, uri, notes or the whole item of the given item id - `ref+bw://4d084b01-87e7-4411-8de9-2476ab9f3f48/notes#/key1` gets the *key1* from the yaml stored as note in the item +### Yandex Cloud Lockbox + +Retrieve secrets from [Yandex Cloud Lockbox](https://yandex.cloud/en/docs/lockbox/). Path is used to specify secret ID. Optionally a specific secret version can be retrieved (using current version by default). If fragment is specified, retrieves a specific key from the secret. + +- `ref+yclockbox://SECRET_ID[?version_id=VERSION][#KEY]` + +Examples: + +- `ref+yclockbox://e6qeoqvd88dcpf044n5i` - get whole secret `e6qeoqvd88dcpf044n5i` from the current version +- `ref+yclockbox://e6qeoqvd88dcpf044n5i?version_id=e6qn22seoaprg9cbe1dj` - get whole secret `e6qeoqvd88dcpf044n5i` from the `e6qn22seoaprg9cbe1dj` version +- `ref+yclockbox://e6qeoqvd88dcpf044n5i?version_id=e6qn22seoaprg9cbe1dj#oauth_secret` - get secret entry from the `oauth_secret` key of `e6qn22seoaprg9cbe1dj` version of `e6qeoqvd88dcpf044n5i` secret +- `ref+yclockbox://e6qeoqvd88dcpf044n5i#oauth_secret` - get secret entry from the `oauth_secret` key of current version of `e6qeoqvd88dcpf044n5i` secret + +#### Authentication + +Vals aquires Yandex Cloud IAM token from the `YC_TOKEN` environment variable. The easiest way to get it is to run `yc iam create-token`. See [Yandex Cloud Lockbox docs](https://yandex.cloud/en/docs/lockbox/api-ref/authentication) for more details on authentication + ### HTTP JSON This provider retrieves values stored in JSON hosted by a HTTP frontend. diff --git a/go.mod b/go.mod index faae58fe..9f7d4e59 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,8 @@ require ( github.com/hashicorp/vault/api v1.20.0 github.com/stretchr/testify v1.10.0 github.com/tidwall/gjson v1.18.0 + github.com/yandex-cloud/go-genproto v0.12.0 + github.com/yandex-cloud/go-sdk v0.11.0 golang.org/x/oauth2 v0.30.0 google.golang.org/api v0.246.0 gopkg.in/yaml.v3 v3.0.1 @@ -53,6 +55,7 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/extism/go-sdk v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/errors v0.22.1 // indirect diff --git a/go.sum b/go.sum index 2342bb3d..7bf7a97f 100644 --- a/go.sum +++ b/go.sum @@ -223,6 +223,8 @@ github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM= github.com/getsops/sops/v3 v3.10.2 h1:7t7lBXFcXJPsDMrpYoI36r8xIhjWUmEc8Qdjuwyo+WY= github.com/getsops/sops/v3 v3.10.2/go.mod h1:Dmtg1qKzFsAl+yqvMgjtnLGTC0l7RnSM6DDtFG7TEsk= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -383,6 +385,8 @@ github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2Em github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -481,6 +485,10 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yandex-cloud/go-genproto v0.12.0 h1:jg8UhoVs0+XpZW9cttdyOTFsCSmhyB1Qsf962UW1/Qg= +github.com/yandex-cloud/go-genproto v0.12.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= +github.com/yandex-cloud/go-sdk v0.11.0 h1:f79LZWX3PLVXSb8QIp7efCWkja69OuGu8hcnToWURFw= +github.com/yandex-cloud/go-sdk v0.11.0/go.mod h1:n9XO+J+P1//Em9eWxOhulhU+6TZp1iCUfzVcmFn/g9U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= diff --git a/pkg/providers/yclockbox/yclockbox.go b/pkg/providers/yclockbox/yclockbox.go new file mode 100644 index 00000000..ae85725a --- /dev/null +++ b/pkg/providers/yclockbox/yclockbox.go @@ -0,0 +1,109 @@ +package yclockbox + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1" + sdk "github.com/yandex-cloud/go-sdk" + + "github.com/helmfile/vals/pkg/api" + "github.com/helmfile/vals/pkg/log" +) + +const ( + TOKEN_ENV = "YC_TOKEN" +) + +// Format: ref+yclockbox://SECRET_ID[?version_id=VERSION_ID][#KEY] +type provider struct { + logger *log.Logger + client lockbox.PayloadServiceClient + versionId string +} + +func New(l *log.Logger, cfg api.StaticConfig) *provider { + token, ok := os.LookupEnv(TOKEN_ENV) + if !ok { + l.Debugf("yclockbox: Missing %s environment variable", TOKEN_ENV) + } + + sdk, err := sdk.Build( + context.TODO(), + sdk.Config{ + Credentials: sdk.NewIAMTokenCredentials( + token, + ), + }, + ) + + if err != nil { + l.Debugf("yclockbox: SDK initialization error: %s", err) + return nil + } + + p := &provider{ + logger: l, + client: sdk.LockboxPayload().Payload(), + } + + if v := cfg.String("version_id"); cfg.Exists("version_id") { + p.versionId = v + } + + return p +} + +func (p *provider) GetString(key string) (string, error) { + if p == nil { + return "", fmt.Errorf("yclockbox: provider is nil") + } + secret, err := p.GetStringMap(key) + + if err != nil { + p.logger.Debugf("yclockbox: get string failed: key=%s", key) + return "", err + } + + res, err := json.Marshal(secret) + + if err != nil { + p.logger.Debugf("yclockbox: marshaling failed") + return "", err + } + + return string(res), nil +} + +func (p *provider) GetStringMap(key string) (map[string]any, error) { + if p == nil { + return nil, fmt.Errorf("yclockbox: provider is nil") + } + secret, err := p.client.Get( + context.Background(), + &lockbox.GetPayloadRequest{ + SecretId: key, + VersionId: p.versionId, + }, + ) + if err != nil { + p.logger.Debugf("yclockbox: %s", err) + return nil, err + } + + res := map[string]interface{}{} + + for _, entry := range secret.Entries { + var value string + if entry.GetTextValue() != "" { + value = entry.GetTextValue() + } else { + value = string(entry.GetBinaryValue()) + } + res[entry.GetKey()] = value + } + + return res, nil +} diff --git a/vals.go b/vals.go index a0c47909..36426419 100644 --- a/vals.go +++ b/vals.go @@ -46,6 +46,7 @@ import ( "github.com/helmfile/vals/pkg/providers/ssm" "github.com/helmfile/vals/pkg/providers/tfstate" "github.com/helmfile/vals/pkg/providers/vault" + "github.com/helmfile/vals/pkg/providers/yclockbox" "github.com/helmfile/vals/pkg/stringmapprovider" "github.com/helmfile/vals/pkg/stringprovider" ) @@ -103,6 +104,7 @@ const ( ProviderHCPVaultSecrets = "hcpvaultsecrets" ProviderHttpJsonManager = "httpjson" ProviderBitwarden = "bw" + ProviderLockbox = "yclockbox" ) var ( @@ -279,6 +281,9 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) { case ProviderBitwarden: p := bitwarden.New(r.logger, conf) return p, nil + case ProviderLockbox: + p := yclockbox.New(r.logger, conf) + return p, nil } return nil, fmt.Errorf("no provider registered for scheme %q", scheme) }