Skip to content

Commit

Permalink
feat: add config command and refine help
Browse files Browse the repository at this point in the history
Signed-off-by: Tobias Brumhard <code@brumhard.com>
  • Loading branch information
brumhard committed Jun 6, 2022
1 parent e1992c0 commit a5b775f
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 55 deletions.
33 changes: 33 additions & 0 deletions cmd/earthly-secret-provider-vault/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"fmt"

"earthly-vault-provider/pkg/provider"

"github.com/spf13/cobra"
)

func buildConfigCommand(p *provider.Provider) *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: fmt.Sprintf("Set a configuration value for %s", cli),
Long: fmt.Sprintf(`Set a configuration value for %[1]s.
Support config fields to be set are "address", "token" and "prefix".
This must be used to set the Vault address and token prior to using the secret provider,
since currently the secret providers cannot read any env vars.
To set the vault address in a vault aware system for example do:
$ %[1]s config address $VAULT_ADDR
To unset config values you can just use the zero value, like for example
$ %[1]s config address ""`, cli),
Args: cobra.ExactArgs(2),
ValidArgs: []string{"address", "token", "prefix"},
RunE: func(_ *cobra.Command, args []string) error {
return p.SetConfigKey(args[0], args[1])
},
}

return cmd
}
50 changes: 0 additions & 50 deletions cmd/earthly-secret-provider-vault/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ package main

import (
"context"
"earthly-vault-provider/pkg/provider"
"errors"
"fmt"
"log"
"os"
"os/signal"
"syscall"

"github.com/moby/buildkit/session/secrets"
"github.com/spf13/cobra"
)

const cli = "earthly-secret-provider-vault"
Expand All @@ -31,50 +28,3 @@ func main() {
os.Exit(1)
}
}

func buildRootCommand() *cobra.Command {
p := provider.New()

cmd := &cobra.Command{
Use: cli,
Short: fmt.Sprintf("%s is a secret provider for Earthly that connects to Vault", cli),
Long: fmt.Sprintf(`%[1]s is a secret provider for Earthly that connects to Hashicorp's Vault.
For docs on how to configure this take a look here: https://docs.earthly.dev/docs/earthly-config#secret_provider-experimental.
Since the contract for secret providers is fairly simple you can test this provider by running:
$ %[1]s <vault-path>
This print the secret on stdout.
Generally the CLI will look at ~/.vault-token and ~/.earthly/vault.yml for the configuration.
The token from ~/.vault-token will be used if it exists, otherwise the token from ~/.earthly/vault.yml will be used.
vault.yml should be used to set the Vault address and optionally a lookup secret can be added.
To set a config option in the vault.yml file, use the config subcommand.`, cli),
// don't show errors and usage on errors in any RunE function.
SilenceErrors: true,
SilenceUsage: true,
Args: cobra.ExactArgs(1),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Enable swapping out stdout/stderr for testing
p.Logger = log.New(cmd.OutOrStderr(), "", 0)
},
RunE: func(cmd *cobra.Command, args []string) error {
secretFetcher, err := p.LoadSecretStore()
if err != nil {
return err
}

secret, err := secretFetcher.GetSecret(cmd.Context(), args[0])
if err != nil {
return err
}

fmt.Fprint(cmd.OutOrStdout(), string(secret))
return nil
},
}

cmd.AddCommand(buildVersionCommand())

return cmd
}
62 changes: 62 additions & 0 deletions cmd/earthly-secret-provider-vault/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"earthly-vault-provider/pkg/provider"
"fmt"
"log"

"github.com/spf13/cobra"
)

func buildRootCommand() *cobra.Command {
p := provider.New()

cmd := &cobra.Command{
Use: cli,
Short: fmt.Sprintf("%s is a secret provider for Earthly that connects to Vault", cli),
Long: fmt.Sprintf(`%[1]s is a secret provider for Earthly that connects to Hashicorp's Vault.
For docs on how to configure this take a look here: https://docs.earthly.dev/docs/earthly-config#secret_provider-experimental.
Since the contract for secret providers is fairly simple you can test this provider by running:
$ %[1]s <vault-path>
This print the secret on stdout.
Generally the CLI will look at ~/.vault-token and ~/.earthly/vault.yml for the configuration.
The token from ~/.vault-token will be used if it exists, otherwise the token from ~/.earthly/vault.yml will be used.
vault.yml should be used to set the Vault address and optionally a lookup secret can be added.
For configuration you can also use the config command:
$ vault login --method=userpass username=test
$ %[1]s config token $(vault print token)
$ %[1]s config address $VAULT_ADDR
To set a config option in the vault.yml file, use the config subcommand.`, cli),
// don't show errors and usage on errors in any RunE function.
SilenceErrors: true,
SilenceUsage: true,
Args: cobra.ExactArgs(1),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Enable swapping out stdout/stderr for testing
p.Logger = log.New(cmd.OutOrStderr(), "", 0)
},
RunE: func(cmd *cobra.Command, args []string) error {
secretFetcher, err := p.LoadSecretStore()
if err != nil {
return err
}

secret, err := secretFetcher.GetSecret(cmd.Context(), args[0])
if err != nil {
return err
}

fmt.Fprint(cmd.OutOrStdout(), string(secret))
return nil
},
}

cmd.AddCommand(buildVersionCommand())
cmd.AddCommand(buildConfigCommand(p))

return cmd
}
63 changes: 58 additions & 5 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package provider

import (
"errors"
"fmt"
"io"
"log"
"net/url"
"os"
"path/filepath"

Expand All @@ -19,22 +22,28 @@ const vaultConfigFile = "vault.yml"

var cfgFilePath = filepath.Join(cliutil.GetEarthlyDir(), vaultConfigFile)

var ErrInvalidConfig = errors.New("invalid config")

type Config struct {
// Token is a token that is used to authenticate with Vault.
Token string `yaml:"token"`
Token string `yaml:"token,omitempty"`
// Address is the address of the Vault server.
Address string `yaml:"address"`
Address string `yaml:"address,omitempty"`
// Prefix will be prepended to any secret that is passed in
Prefix string `yaml:"prefix"`
Prefix string `yaml:"prefix,omitempty"`
}

func (c *Config) Validate() error {
if c.Token == "" {
return errors.New("token is required")
return fmt.Errorf("token is required: %w", ErrInvalidConfig)
}

if c.Address == "" {
return errors.New("address is required")
return fmt.Errorf("address is required: %w", ErrInvalidConfig)
}

if _, err := url.ParseRequestURI(c.Address); err != nil {
return fmt.Errorf("address %q should be a valid URL: %w", c.Address, ErrInvalidConfig)
}

return nil
Expand Down Expand Up @@ -63,6 +72,7 @@ func (p *Provider) LoadSecretStore() (secrets.SecretStore, error) {
if err != nil {
return nil, err
}
defer cfgFile.Close()

if err := yaml.NewDecoder(cfgFile).Decode(&config); err != nil {
return nil, err
Expand All @@ -83,3 +93,46 @@ func (p *Provider) LoadSecretStore() (secrets.SecretStore, error) {

return vault.NewSecretStore(client.Logical(), p.Logger, vault.WithPrefix(config.Prefix)), nil
}

func (p *Provider) SetConfigKey(key, value string) error {
// read the config and create the file if it doesn't exist yet
cfgFile, err := os.OpenFile(cfgFilePath, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return fmt.Errorf("failed opening or creating config file: %w", err)
}
defer cfgFile.Close()

config := Config{}
err = yaml.NewDecoder(cfgFile).Decode(&config)
// if it's an EOF error just proceed since the file probably just got created and is empty
if err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("failed reading config file: %w", err)
}

switch key {
case "token":
config.Token = value
case "address":
config.Address = value
case "prefix":
config.Prefix = value
default:
return fmt.Errorf("key %q is not supported: %w", key, ErrInvalidConfig)
}

// delete file content and reset I/O offset to 0
if err := cfgFile.Truncate(0); err != nil {
return fmt.Errorf("failed truncating config file before write: %w", err)
}

if _, err := cfgFile.Seek(0, 0); err != nil {
return err
}

// write new config to now empty file
if err := yaml.NewEncoder(cfgFile).Encode(&config); err != nil {
return fmt.Errorf("failed writing config file: %w", err)
}

return nil
}

0 comments on commit a5b775f

Please sign in to comment.