Resolve the config path; print help on a bare invocation
Fall back to $XDG_CONFIG_HOME and $HOME/.config before the baked-in /opt/certmanager/config.yaml, and stop failing when called with no args.
This commit is contained in:
+16
-6
@@ -52,11 +52,20 @@ func rootCmd() *cobra.Command {
|
||||
Use: "certmanager <common_name>",
|
||||
Short: "Issue a PKI certificate from HashiCorp Vault",
|
||||
Version: version,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(cfgFile)
|
||||
if len(args) == 0 {
|
||||
// A bare invocation is someone asking what this is; flags
|
||||
// without a common name is a malformed call and must fail.
|
||||
if cmd.Flags().NFlag() == 0 {
|
||||
return cmd.Help()
|
||||
}
|
||||
return fmt.Errorf("a common name is required")
|
||||
}
|
||||
|
||||
cfg, err := config.Load(config.Resolve(cfgFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -79,9 +88,10 @@ func rootCmd() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
// Persistent so the ca subcommand shares it; the positional form keeps
|
||||
// parsing --config exactly as before.
|
||||
cmd.PersistentFlags().StringVar(&cfgFile, "config", config.DefaultPath(), "path to config.yaml")
|
||||
// Persistent so the ca subcommand shares it; empty means "resolve", which
|
||||
// keeps the resolution order out of the usage text and out of --help.
|
||||
cmd.PersistentFlags().StringVar(&cfgFile, "config", "",
|
||||
"path to config.yaml (default: $XDG_CONFIG_HOME or ~/.config, else "+config.FallbackPath()+")")
|
||||
cmd.Flags().StringVarP(&altNames, "alt-names", "a", "", "comma-separated DNS subject alternative names")
|
||||
cmd.Flags().StringVarP(&ipSans, "ip-sans", "i", "", "comma-separated IP subject alternative names")
|
||||
cmd.Flags().IntVarP(&expiryDays, "expiry-days", "e", 365, "certificate validity in days")
|
||||
@@ -106,7 +116,7 @@ func caCmd(cfgFile *string) *cobra.Command {
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
cfg, err := config.LoadCA(*cfgFile)
|
||||
cfg, err := config.LoadCA(config.Resolve(*cfgFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.unkin.net/unkin/certmanager/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -769,3 +771,89 @@ func TestRunCAFailures(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A bare invocation is a human at a terminal, not Puppet: print help and exit
|
||||
// zero rather than failing on the missing positional.
|
||||
func TestRunNoArgsPrintsHelp(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := run(nil, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("exit code = %d, stderr = %q", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Usage:") {
|
||||
t.Errorf("stdout = %q, want the help text", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "certmanager <common_name>") {
|
||||
t.Errorf("help does not document the positional form: %q", stdout.String())
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
t.Errorf("stderr = %q, want empty", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
// seedXDGConfig writes a config where DefaultPath() looks first.
|
||||
func seedXDGConfig(t *testing.T, addr string) {
|
||||
t.Helper()
|
||||
xdg := t.TempDir()
|
||||
dir := filepath.Join(xdg, "certmanager")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
body := "vault:\n addr: " + addr + "\n auth_method: approle\n" +
|
||||
" approle_path: approle\n role_id: test-role-id\n" +
|
||||
" mount_point: pki_int\n role_name: servers_default\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "config.yaml"), []byte(body), 0o600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
}
|
||||
|
||||
// Puppet passes no --config, so the resolved path has to be used.
|
||||
func TestRunResolvesConfigWithoutFlag(t *testing.T) {
|
||||
srv, got := newVaultStub(t, stubOptions{})
|
||||
seedXDGConfig(t, srv.URL)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{"host.example.com", "--alt-names", "host", "--expiry-days", "90", "--json"}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, stderr = %q", code, stderr.String())
|
||||
}
|
||||
if got.issueBody["common_name"] != "host.example.com" {
|
||||
t.Errorf("common_name = %v", got.issueBody["common_name"])
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
t.Errorf("stderr = %q, want empty", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
// The ca subcommand shares the same resolution.
|
||||
func TestRunCAResolvesConfigWithoutFlag(t *testing.T) {
|
||||
srv, got := newVaultStub(t, stubOptions{})
|
||||
seedXDGConfig(t, srv.URL)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := run([]string{"ca"}, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("exit code = %d, stderr = %q", code, stderr.String())
|
||||
}
|
||||
if !got.caRequested {
|
||||
t.Error("the CA chain endpoint was never called")
|
||||
}
|
||||
}
|
||||
|
||||
// With no config anywhere the run fails naming the baked-in path, not a
|
||||
// home-relative one that could never exist under generate().
|
||||
func TestRunWithoutAnyConfigNamesTheBakedPath(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := run([]string{"host.example.com", "--json"}, &stdout, &stderr); code == 0 {
|
||||
t.Fatal("exit code = 0, want non-zero")
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Errorf("stdout = %q, want empty", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), config.FallbackPath()) {
|
||||
t.Errorf("stderr = %q, want it to name %q", stderr.String(), config.FallbackPath())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user