Documentation ¶
Overview ¶
Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx> SPDX-License-Identifier: Apache-2.0
Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx> SPDX-License-Identifier: Apache-2.0
Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx> SPDX-License-Identifier: Apache-2.0
Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx> SPDX-License-Identifier: Apache-2.0
Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx> SPDX-License-Identifier: Apache-2.0
Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx> SPDX-License-Identifier: Apache-2.0
Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx> SPDX-License-Identifier: Apache-2.0
Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx> SPDX-License-Identifier: Apache-2.0
Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx> SPDX-License-Identifier: Apache-2.0
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Diff = &command.Command{ Path: []string{"diff"}, Summary: "Shows differences between local and remote configs", Description: `Fetches remote and compares against local, ignoring comments but respecting order. The diff output shows what would happen upon running ﹅joao fetch﹅. Specify ﹅--remote﹅ to show what would happen upon ﹅joao flush﹅`, Arguments: command.Arguments{ { Name: "config", Description: "The configuration file(s) to diff", Required: false, Variadic: true, Values: &command.ValueSource{ Files: &fileExtensions, }, }, }, Options: command.Options{ "output": { Description: "How to format the differences", Type: command.ValueTypeString, Default: "auto", Values: &command.ValueSource{ Static: &[]string{ "auto", "patch", "exit-code", "short", }, }, }, "remote": { Description: "Shows what would happen on `flush` instead of `fetch`", Type: command.ValueTypeBoolean, Default: false, }, "redacted": { Description: "Compare redacted versions", Type: command.ValueTypeBoolean, Default: false, }, }, Action: func(cmd *command.Command) error { paths := cmd.Arguments[0].ToValue().([]string) redacted := cmd.Options["redacted"].ToValue().(bool) remote := cmd.Options["remote"].ToValue().(bool) for _, path := range paths { local, err := config.Load(path, false) if err != nil { return err } if err := local.DiffRemote(path, redacted, remote, cmd.Cobra.OutOrStdout(), cmd.Cobra.OutOrStderr()); err != nil { return err } } logrus.Info("Done") return nil }, }
var Fetch = &command.Command{ Path: []string{"fetch"}, Summary: "fetches configuration values from 1Password", Description: `Fetches secrets for local ﹅CONFIG﹅ files from 1Password.`, Arguments: command.Arguments{ { Name: "config", Description: "The configuration file(s) to fetch", Required: false, Variadic: true, Values: &command.ValueSource{ Files: &fileExtensions, }, }, }, Options: command.Options{ "dry-run": { Description: "Don't persist to the filesystem", Type: "bool", }, }, Action: func(cmd *command.Command) error { paths := cmd.Arguments[0].ToValue().([]string) for _, path := range paths { local, err := config.Load(path, false) if err != nil { return err } if dryRun := cmd.Options["dry-run"].ToValue().(bool); dryRun { logrus.Warnf("dry-run: comparing %s to %s", local.OPURL(), path) stdout := cmd.Cobra.OutOrStdout() stderr := cmd.Cobra.OutOrStderr() if err := local.DiffRemote(path, false, true, stdout, stderr); err != nil { return err } logrus.Warnf("dry-run: did not update %s", path) continue } remote, err := config.Load(path, true) if err != nil { return err } if err = local.Merge(remote); err != nil { return err } if err := local.AsFile(path); err != nil { return err } logrus.Infof("Fetched %s => %s", remote.OPURL(), path) } logrus.Info("Done") return nil }, }
var FilterClean = &command.Command{ Path: []string{"git-filter", "clean"}, Summary: "a filter for git to call when a file is checked in", Description: `see ﹅joao git-filter﹅ for instructions to install this filter Use ﹅--flush﹅ to save changes to 1password before redacting file.`, Arguments: command.Arguments{ { Name: "path", Description: "The git staged path to read from", Required: true, Values: &command.ValueSource{ Files: &fileExtensions, }, }, }, Options: command.Options{ "flush": { Description: "Save to 1Password after before redacting", Type: "bool", }, }, Action: redactedData, }
var FilterDiff = &command.Command{ Path: []string{"git-filter", "diff"}, Summary: "a filter for git to call during `git diff`", Description: `see ﹅joao git-filter﹅ for instructions to install this filter`, Arguments: command.Arguments{ { Name: "path", Description: "The git staged path to read from", Required: true, Values: &command.ValueSource{ Files: &fileExtensions, }, }, }, Options: command.Options{}, Action: redactedData, }
var FilterGroup = &command.Command{ Path: []string{"git-filter"}, Summary: "Subcommands used by `git` as filters", Description: `In order to store configuration files within a git repository while keeping secrets off remote copies, ﹅joao﹅ provides git filters. To install them, **every collaborator** would need to run: ﹅﹅﹅sh # setup filters in your local copy of the repo: # this runs when you check in a file (i.e. about to commit a config file) # it will flush secrets to 1password before removing secrets from the file on disk git config filter.joao.clean "joao git-filter clean --flush %f" # this step runs after checkout (i.e. pulling changes) # it simply outputs the file as-is on disk git config filter.joao.smudge cat # let's enforce these filters git config filter.joao.required true # optionally, configure a diff filter to show changes as would be committed to git # this does not modify the original file on disk git config diff.joao.textconv "joao git-filter diff" ﹅﹅﹅ Then, **only once**, we need to specify which files to apply the filters and diff commands to: ﹅﹅﹅sh # adds diff and filter attributes for config files ending with .joao.yaml echo '**/*.joao.yaml filter=joao diff=joao' >> .gitattributes # finally, commit and push these attributes git add .gitattributes git commit -m "installing joao attributes" git push origin main ﹅﹅﹅ See: - https://git-scm.com/docs/gitattributes#_filter - https://git-scm.com/docs/gitattributes#_diff`, Arguments: command.Arguments{}, Options: command.Options{}, Action: func(cmd *command.Command) error { data, err := cmd.ShowHelp(command.Root.Options, os.Args) if err != nil { return err } _, err = cmd.Cobra.OutOrStderr().Write(data) return err }, }
var Flush = &command.Command{ Path: []string{"flush"}, Summary: "flush configuration values to 1Password", Description: `Creates or updates existing items for every ﹅CONFIG﹅ file provided. Does not delete 1Password items.`, Arguments: command.Arguments{ { Name: "config", Description: "The configuration file(s) to flush", Required: false, Variadic: true, Values: &command.ValueSource{ Files: &fileExtensions, }, }, }, Options: command.Options{ "dry-run": { Description: "Don't persist to 1Password", Type: "bool", }, "redact": { Description: "Redact local file after flushing", Type: "bool", }, }, Action: func(cmd *command.Command) error { paths := cmd.Arguments[0].ToValue().([]string) dryRun := cmd.Options["dry-run"].ToValue().(bool) if dryRun { opclient.Use(&opclient.CLI{DryRun: true}) } for _, path := range paths { cfg, err := config.Load(path, false) if err != nil { return err } if dryRun { logrus.Warnf("dry-run: comparing %s to %s", path, cfg.OPURL()) if err := cfg.DiffRemote(path, false, false, cmd.Cobra.OutOrStdout(), cmd.Cobra.OutOrStderr()); err != nil { return err } logrus.Warnf("dry-run: did not update %s", cfg.OPURL()) continue } if err := opclient.Update(cfg.Vault, cfg.Name, cfg.ToOP()); err != nil { return fmt.Errorf("could not flush to 1password: %w", err) } if cmd.Options["redact"].ToValue().(bool) { if err := cfg.AsFile(path, config.OutputModeRedacted); err != nil { return err } } logrus.Infof("Flushed %s to %s", path, cfg.OPURL()) } logrus.Info("Done") return nil }, }
var Get = &command.Command{ Path: []string{"get"}, Summary: "retrieves configuration", Description: ` looks at the filesystem or remotely, using 1password (over the CLI if available, or 1password-connect, if configured). ` + "`--output`" + ` can be one of: - **raw**: - when querying for scalar values this will return a non-quoted version of the values - when querying for trees or lists, this will output JSON - **yaml**: formats the value at the given path as YAML - **json**: formats the value at the given path as JSON - **op**: formats the whole configuration as a 1Password item`, Arguments: command.Arguments{ { Name: "config", Description: "The configuration to get from", Required: true, Values: &command.ValueSource{ Files: &fileExtensions, }, }, { Name: "path", Default: ".", Description: "A dot-delimited path to extract from CONFIG", Values: &command.ValueSource{ Func: config.AutocompleteKeysAndParents, }, }, }, Options: command.Options{ "output": { ShortName: "o", Description: "the format to use for rendering output", Default: "raw", Values: &command.ValueSource{ Static: &[]string{"raw", "json", "yaml", "diff-yaml", "op"}, }, }, "redacted": { Description: "Do not print secret values", Type: "bool", Default: false, }, "remote": { Description: "Get values from 1password", Type: "bool", Default: false, }, }, Action: func(cmd *command.Command) error { path := cmd.Arguments[0].ToValue().(string) query := cmd.Arguments[1].ToValue().(string) remote := cmd.Options["remote"].ToValue().(bool) format := cmd.Options["output"].ToValue().(string) redacted := cmd.Options["redacted"].ToValue().(bool) cfg, err := config.Load(path, remote) if err != nil { return err } if query == "" || query == "." { var bytes []byte switch format { case "yaml", "raw", "diff-yaml": modes := []config.OutputMode{} if redacted { modes = append(modes, config.OutputModeRedacted) } if format == "diff-yaml" { modes = append(modes, config.OutputModeNoComments, config.OutputModeSorted) } bytes, err = cfg.AsYAML(modes...) case "json", "op": bytes, err = cfg.AsJSON(redacted, format == "op") default: return fmt.Errorf("unknown format %s", format) } if err != nil { return err } _, err = cmd.Cobra.OutOrStdout().Write(bytes) return err } parts := strings.Split(query, ".") entry := cfg.Tree for _, part := range parts { entry = entry.ChildNamed(part) if entry == nil { return fmt.Errorf("value not found at %s of %s", part, query) } } var bytes []byte if len(entry.Content) > 0 { val := entry.AsMap() if format == "yaml" { enc := yaml.NewEncoder(cmd.Cobra.OutOrStdout()) enc.SetIndent(2) return enc.Encode(val) } bytes, err = json.Marshal(val) if err != nil { return err } } else { bytes = []byte(entry.String()) } _, err = cmd.Cobra.OutOrStdout().Write(bytes) return err }, }
var GitFilters = []*command.Command{ FilterDiff, FilterClean, FilterGroup, }
var Plugin = &command.Command{ Path: []string{"vault-plugin"}, Summary: "Starts a vault-joao-plugin server", Description: `﹅joao﹅ can run as a plugin to Hashicorp Vault, and make whole configuration entries available—secrets and all—through the Vault API. To install, download ﹅joao﹅ to the machine running ﹅vault﹅ at the ﹅plugin_directory﹅, as specified by vault's config. The installed ﹅joao﹅ executable needs to be executable for the user running vault only. ### Configuration ﹅﹅﹅sh export VAULT_PLUGIN_DIR=/var/lib/vault/plugins chmod 700 "$VAULT_PLUGIN_DIR/joao" export PLUGIN_SHA="$(openssl dgst -sha256 -hex "$VAULT_PLUGIN_DIR/joao" | awk '{print $2}')" export VERSION="$($VAULT_PLUGIN_DIR/joao --version)" # register vault plugin register -sha256="$PLUGIN_SHA" -command=joao -args="vault-plugin" -version="$VERSION" secret joao # configure, add ﹅vault﹅ to set a default vault for querying vault write config/1password "host=$OP_CONNECT_HOST" "token=$OP_CONNECT_TOKEN" # vault=my-default-vault if !(vault plugin list secret | grep -c -m1 '^joao ' >/dev/null); then # first time, let's enable the secrets backend vault secrets enable -path=config joao else # updating from a previous version vault secrets tune -plugin-version="$VERSION" config/ vault plugin reload -plugin joao fi ﹅﹅﹅ ### Vault API ﹅﹅﹅sh # VAULT is optional if configured with a default ﹅vault﹅. See above # vault read config/tree/[VAULT/]ITEM vault read config/tree/service:api vault read config/tree/prod/service:api # vault list config/trees/[VAULT/] vault list config/trees vault list config/trees/prod ﹅﹅﹅ See: - https://developer.hashicorp.com/vault/docs/plugins `, Options: command.Options{ "sigh0": { ShortName: "c", Default: "", }, "sigh1": { ShortName: "t", Default: "", }, }, Action: func(cmd *command.Command) error { apiClientMeta := &api.PluginAPIClientMeta{} flags := apiClientMeta.FlagSet() err := flags.Parse(os.Args[2:]) if err != nil { logrus.Warnf("Could not parse flags: %s", err) } tlsConfig := apiClientMeta.GetTLSConfig() tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) return plugin.ServeMultiplex(&plugin.ServeOpts{ BackendFactoryFunc: vault.Factory, TLSProviderFunc: tlsProviderFunc, }) }, }
var Redact = &command.Command{ Path: []string{"redact"}, Summary: "removes secrets from configuration", Description: `Removes secret values (not the keys) from existing items for every ﹅CONFIG﹅ file provided.`, Arguments: command.Arguments{ { Name: "config", Description: "The configuration file(s) to redact", Required: false, Variadic: true, Values: &command.ValueSource{ Files: &fileExtensions, }, }, }, Action: func(cmd *command.Command) error { paths := cmd.Arguments[0].ToValue().([]string) for _, path := range paths { cfg, err := config.Load(path, false) if err != nil { return err } if err := cfg.AsFile(path, config.OutputModeRedacted); err != nil { return err } } logrus.Info("Done") return nil }, }
var Set = &command.Command{ Path: []string{"set"}, Summary: "updates configuration values", Description: ` Updates the value at ﹅PATH﹅ in a local ﹅CONFIG﹅ file. Specify ﹅--secret﹅ to keep the value secret, or ﹅--delete﹅ to delete the key at PATH. Will read values from stdin (or ﹅--from﹅ a file) and store it at the ﹅PATH﹅ of ﹅CONFIG﹅, optionally ﹅--flush﹅ing to 1Password. `, Arguments: command.Arguments{ { Name: "config", Description: "The configuration file to modify", Required: true, Values: &command.ValueSource{ Files: &fileExtensions, }, }, { Name: "path", Required: true, Description: "A dot-delimited path to set in CONFIG", Values: &command.ValueSource{ SuggestRaw: true, Suggestion: true, Func: config.AutocompleteKeys, }, }, }, Options: command.Options{ "input": { ShortName: "i", Description: "the file to read input from", Default: "/dev/stdin", Values: &command.ValueSource{ Files: &[]string{}, }, }, "secret": { Description: "Store value as a secret string", Type: "bool", }, "delete": { Description: "Delete the value at the given PATH", Type: "bool", }, "json": { Description: "Treat input as JSON-encoded", Type: "bool", }, "flush": { Description: "Save to 1Password after saving to PATH", Type: "bool", }, }, Action: func(cmd *command.Command) error { path := cmd.Arguments[0].ToValue().(string) query := cmd.Arguments[1].ToValue().(string) var cfg *config.Config var err error secret := cmd.Options["secret"].ToValue().(bool) delete := cmd.Options["delete"].ToValue().(bool) input := cmd.Options["input"].ToValue().(string) parseJSON := cmd.Options["json"].ToValue().(bool) flush := cmd.Options["flush"].ToValue().(bool) if secret && delete { return fmt.Errorf("cannot --delete and set a --secret at the same time") } if secret && parseJSON { return fmt.Errorf("cannot set a --secret that is JSON encoded, encode individual values instead") } if delete && input != "/dev/stdin" { logrus.Warn("Ignoring --input while deleting") } cfg, err = config.Load(path, false) if err != nil { return err } parts := strings.Split(query, ".") if delete { if err := cfg.Delete(parts); err != nil { return err } } else { var valueBytes []byte if input == "/dev/stdin" { valueBytes, err = io.ReadAll(cmd.Cobra.InOrStdin()) } else { valueBytes, err = os.ReadFile(input) } if err != nil { return err } if err := cfg.Set(parts, valueBytes, secret, parseJSON); err != nil { return err } } if err := cfg.AsFile(path); err != nil { return err } if flush { if err := opclient.Update(cfg.Vault, cfg.Name, cfg.ToOP()); err != nil { return fmt.Errorf("could not flush to 1password: %w", err) } } logrus.Info("Done") return err }, }
Functions ¶
This section is empty.
Types ¶
This section is empty.