2016-07-11 14:56:35 +00:00
package main
import (
2016-07-11 15:22:05 +00:00
"bytes"
2016-07-11 14:56:35 +00:00
"fmt"
"os"
"strings"
2016-08-23 14:58:14 +00:00
"text/template"
2016-07-11 14:56:35 +00:00
2024-04-07 12:34:19 +00:00
"gopkg.in/yaml.v3"
2016-07-11 14:56:35 +00:00
"github.com/hashicorp/vault/api"
"github.com/mitchellh/go-homedir"
2024-04-07 12:34:19 +00:00
"github.com/sirupsen/logrus"
2019-01-09 15:17:44 +00:00
korvike "github.com/Luzifer/korvike/functions"
2021-09-29 11:19:14 +00:00
"github.com/Luzifer/rconfig/v2"
2016-07-11 14:56:35 +00:00
)
2021-09-29 11:27:35 +00:00
const filePermissionUserWrite = 0 o600
2016-07-11 14:56:35 +00:00
var (
cfg = struct {
2019-01-09 15:28:46 +00:00
File string ` flag:"file,f" default:"vault.yaml" description:"File to import from / export to" validate:"nonzero" `
2016-07-11 14:56:35 +00:00
Import bool ` flag:"import" default:"false" description:"Enable importing data into Vault" `
Export bool ` flag:"export" default:"false" description:"Enable exporting data from Vault" `
ExportPaths [ ] string ` flag:"export-paths" default:"secret" description:"Which paths to export" `
2016-07-19 11:59:55 +00:00
IgnoreErrors bool ` flag:"ignore-errors" default:"false" description:"Do not exit on read/write errors" `
2019-01-09 15:17:44 +00:00
LogLevel string ` flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)" `
2016-07-11 14:56:35 +00:00
VaultAddress string ` flag:"vault-addr" env:"VAULT_ADDR" default:"https://127.0.0.1:8200" description:"Vault API address" `
2019-01-09 15:28:46 +00:00
VaultToken string ` flag:"vault-token" env:"VAULT_TOKEN" vardefault:"vault-token" description:"Specify a token to use instead of app-id auth" validate:"nonzero" `
2016-07-11 14:56:35 +00:00
VersionAndExit bool ` flag:"version" default:"false" description:"Print program version and exit" `
} { }
version = "dev"
)
type importFile struct {
2016-08-08 14:26:11 +00:00
Keys [ ] importField
}
type importField struct {
Key string
2016-10-05 11:21:14 +00:00
State string
2016-08-08 14:26:11 +00:00
Values map [ string ] interface { }
2016-07-11 14:56:35 +00:00
}
type execFunction func ( * api . Client ) error
func vaultTokenFromDisk ( ) string {
vf , err := homedir . Expand ( "~/.vault-token" )
if err != nil {
return ""
}
2024-04-07 12:34:19 +00:00
data , err := os . ReadFile ( vf ) //#nosec G304 // Intended to read file from disk
2016-07-11 14:56:35 +00:00
if err != nil {
return ""
}
return string ( data )
}
2024-04-07 12:34:19 +00:00
func initApp ( ) error {
rconfig . AutoEnv ( true )
2016-07-11 14:56:35 +00:00
rconfig . SetVariableDefaults ( map [ string ] string {
"vault-token" : vaultTokenFromDisk ( ) ,
} )
2019-01-09 15:17:44 +00:00
if err := rconfig . ParseAndValidate ( & cfg ) ; err != nil {
2024-04-07 12:34:19 +00:00
return fmt . Errorf ( "parsing CLI options: %w" , err )
2019-01-09 15:17:44 +00:00
}
2016-07-11 14:56:35 +00:00
2024-04-07 12:34:19 +00:00
l , err := logrus . ParseLevel ( cfg . LogLevel )
if err != nil {
return fmt . Errorf ( "parsing log-level: %w" , err )
2016-07-11 14:56:35 +00:00
}
2024-04-07 12:34:19 +00:00
logrus . SetLevel ( l )
2016-07-11 14:56:35 +00:00
2024-04-07 12:34:19 +00:00
return nil
}
func main ( ) {
var err error
if err = initApp ( ) ; err != nil {
logrus . WithError ( err ) . Fatal ( "initializing app" )
2016-07-11 14:56:35 +00:00
}
2024-04-07 12:34:19 +00:00
if cfg . VersionAndExit {
fmt . Printf ( "yaml-vault %s\n" , version ) //nolint:forbidigo
os . Exit ( 0 )
2016-07-11 14:56:35 +00:00
}
if cfg . Export == cfg . Import {
2024-04-07 12:34:19 +00:00
logrus . Fatal ( "either import or export must be set" )
2016-07-11 14:56:35 +00:00
}
if _ , err := os . Stat ( cfg . File ) ; ( err == nil && cfg . Export ) || ( err != nil && cfg . Import ) {
if cfg . Export {
2024-04-07 12:34:19 +00:00
logrus . Fatal ( "output file exists, stopping now." )
2016-07-11 14:56:35 +00:00
}
2024-04-07 12:34:19 +00:00
logrus . Fatal ( "input file does not exist, stopping now." )
2016-07-11 14:56:35 +00:00
}
client , err := api . NewClient ( & api . Config {
Address : cfg . VaultAddress ,
} )
if err != nil {
2024-04-07 12:34:19 +00:00
logrus . WithError ( err ) . Fatal ( "creating Vault client" )
2016-07-11 14:56:35 +00:00
}
client . SetToken ( cfg . VaultToken )
var ex execFunction
if cfg . Export {
ex = exportFromVault
} else {
ex = importToVault
}
if err = ex ( client ) ; err != nil {
2024-04-07 12:34:19 +00:00
logrus . WithError ( err ) . Fatal ( "executing requested action" )
2016-07-11 14:56:35 +00:00
}
}
func exportFromVault ( client * api . Client ) error {
2016-08-08 14:26:11 +00:00
out := importFile { }
2016-07-11 14:56:35 +00:00
for _ , path := range cfg . ExportPaths {
if path [ 0 ] == '/' {
path = path [ 1 : ]
}
if ! strings . HasSuffix ( path , "/" ) {
2021-09-29 11:27:35 +00:00
path += "/"
2016-07-11 14:56:35 +00:00
}
if err := readRecurse ( client , path , & out ) ; err != nil {
2024-04-07 12:34:19 +00:00
return fmt . Errorf ( "reading from Vault: %w" , err )
2016-07-11 14:56:35 +00:00
}
}
data , err := yaml . Marshal ( out )
if err != nil {
2024-04-07 12:34:19 +00:00
return fmt . Errorf ( "marshalling YAML: %w" , err )
2016-07-11 14:56:35 +00:00
}
2024-04-07 12:34:19 +00:00
if err = os . WriteFile ( cfg . File , data , filePermissionUserWrite ) ; err != nil {
return fmt . Errorf ( "writing file: %w" , err )
2016-07-11 14:56:35 +00:00
}
return nil
}
func importToVault ( client * api . Client ) error {
2024-04-07 12:34:19 +00:00
keysRaw , err := os . ReadFile ( cfg . File )
2016-07-11 14:56:35 +00:00
if err != nil {
2024-04-07 12:34:19 +00:00
return fmt . Errorf ( "reading input-file: %w" , err )
2016-07-11 14:56:35 +00:00
}
2016-07-11 15:22:05 +00:00
keysRaw , err = parseImportFile ( keysRaw )
if err != nil {
2024-04-07 12:34:19 +00:00
return fmt . Errorf ( "parsing input file: %w" , err )
2016-07-11 15:22:05 +00:00
}
2016-07-11 14:56:35 +00:00
var keys importFile
if err := yaml . Unmarshal ( keysRaw , & keys ) ; err != nil {
2024-04-07 12:34:19 +00:00
return fmt . Errorf ( "unmarshalling input file: %w" , err )
2016-07-11 14:56:35 +00:00
}
2016-08-08 14:26:11 +00:00
for _ , field := range keys . Keys {
2024-04-07 12:34:19 +00:00
logger := logrus . WithField ( "path" , field . Key )
2016-10-05 11:21:14 +00:00
if field . State == "absent" {
if _ , err := client . Logical ( ) . Delete ( field . Key ) ; err != nil {
if cfg . IgnoreErrors {
2024-04-07 12:34:19 +00:00
logger . WithError ( err ) . Error ( "deleting key" )
2016-10-05 11:21:14 +00:00
continue
}
2024-04-07 12:34:19 +00:00
return fmt . Errorf ( "deleting path %q: %w" , field . Key , err )
2016-07-19 11:59:55 +00:00
}
2024-04-07 12:34:19 +00:00
logger . Debug ( "deleted key" )
2016-10-05 11:21:14 +00:00
} else {
if _ , err := client . Logical ( ) . Write ( field . Key , field . Values ) ; err != nil {
if cfg . IgnoreErrors {
2024-04-07 12:34:19 +00:00
logger . WithError ( err ) . Error ( "writing data to key" )
2016-10-05 11:21:14 +00:00
continue
}
2024-04-07 12:34:19 +00:00
return fmt . Errorf ( "writing path %q: %w" , field . Key , err )
2016-10-05 11:21:14 +00:00
}
2024-04-07 12:34:19 +00:00
logger . Debug ( "wrote data to key" )
2016-07-11 14:56:35 +00:00
}
}
return nil
}
2016-07-11 15:22:05 +00:00
func parseImportFile ( in [ ] byte ) ( out [ ] byte , err error ) {
2019-01-09 15:17:44 +00:00
t , err := template . New ( "input file" ) . Funcs ( korvike . GetFunctionMap ( ) ) . Parse ( string ( in ) )
2016-07-11 15:22:05 +00:00
if err != nil {
2024-04-07 12:34:19 +00:00
return nil , fmt . Errorf ( "parsing template: %w" , err )
}
buf := new ( bytes . Buffer )
if err = t . Execute ( buf , nil ) ; err != nil {
return nil , fmt . Errorf ( "executing template: %w" , err )
2016-07-11 15:22:05 +00:00
}
2024-04-07 12:34:19 +00:00
return buf . Bytes ( ) , nil
}
func readRecurse ( client * api . Client , path string , out * importFile ) error {
if ! strings . HasSuffix ( path , "/" ) {
secret , err := client . Logical ( ) . Read ( path )
if err != nil {
return fmt . Errorf ( "reading path %q: %w" , path , err )
}
if secret == nil {
if cfg . IgnoreErrors {
logrus . WithField ( "path" , path ) . Info ( "read nil secret" )
return nil
}
return fmt . Errorf ( "read non-existent path %q" , path )
}
out . Keys = append ( out . Keys , importField { Key : path , Values : secret . Data } )
logrus . WithField ( "path" , path ) . Debug ( "read data from key" )
return nil
}
secret , err := client . Logical ( ) . List ( path )
if err != nil {
if cfg . IgnoreErrors {
logrus . WithError ( err ) . WithField ( "path" , path ) . Error ( "reading secret" )
return nil
}
return fmt . Errorf ( "reading path %q: %w" , path , err )
}
if secret != nil && secret . Data [ "keys" ] != nil {
for _ , k := range secret . Data [ "keys" ] . ( [ ] interface { } ) {
if err := readRecurse ( client , path + k . ( string ) , out ) ; err != nil {
return err
}
}
return nil
}
return nil
2016-07-11 15:22:05 +00:00
}