cmd

package
v0.0.1-0...-ff937d3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 1, 2023 License: Apache-2.0 Imports: 13 Imported by: 0

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

View Source
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
	},
}
View Source
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
	},
}
View Source
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,
}
View Source
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,
}
View Source
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
	},
}
View Source
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
	},
}
View Source
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
	},
}
View Source
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,
		})
	},
}
View Source
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
	},
}
View Source
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL