package command import ( "errors" "fmt" "io/fs" "os" "regexp" "strings" "github.com/muesli/reflow/wordwrap" "github.com/posener/complete" ) const ( // DefaultHclVarInitName is the default name we use when initializing the // example var file in HCL format DefaultHclVarInitName = "spec.nv.hcl" // DefaultHclVarInitName is the default name we use when initializing the // example var file in JSON format DefaultJsonVarInitName = "spec.nv.json" ) // VarInitCommand generates a new variable specification type VarInitCommand struct { Meta } func (c *VarInitCommand) Help() string { helpText := ` Usage: nomad var init Creates an example variable specification file that can be used as a starting point to customize further. When no filename is supplied, a default filename of "spec.nv.hcl" or "spec.nv.json" will be used depending on the output format. Init Options: -out (hcl | json) Format of generated variable specification. Defaults to "hcl". ` return strings.TrimSpace(helpText) } func (c *VarInitCommand) Synopsis() string { return "Create an example variable specification file" } func (c *VarInitCommand) AutocompleteFlags() complete.Flags { return complete.Flags{ "-out": complete.PredictSet("hcl", "json"), } } func (c *VarInitCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing } func (c *VarInitCommand) Name() string { return "var init" } func (c *VarInitCommand) Run(args []string) int { var outFmt string var quiet bool flags := c.Meta.FlagSet(c.Name(), FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } flags.StringVar(&outFmt, "out", "hcl", "") if err := flags.Parse(args); err != nil { return 1 } // Check that we get no arguments args = flags.Args() if l := len(args); l > 1 { c.Ui.Error("This command takes no arguments or one: ") c.Ui.Error(commandErrorText(c)) return 1 } var fileName, fileContent string switch outFmt { case "hcl": fileName = DefaultHclVarInitName fileContent = defaultHclVarSpec case "json": fileName = DefaultJsonVarInitName fileContent = defaultJsonVarSpec } if len(args) == 1 { fileName = args[0] } // Check if the file already exists _, err := os.Stat(fileName) if err == nil { c.Ui.Error(fmt.Sprintf("File %q already exists", fileName)) return 1 } if err != nil && !errors.Is(err, fs.ErrNotExist) { c.Ui.Error(fmt.Sprintf("Failed to stat %q: %v", fileName, err)) return 1 } // Write out the example err = os.WriteFile(fileName, []byte(fileContent), 0660) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to write %q: %v", fileName, err)) return 1 } // Success if !quiet { if outFmt == "json" { c.Ui.Info(wrapString(tidyRawString(strings.ReplaceAll(msgOnlyItemsRequired, "items", "Items")), 70)) c.Ui.Warn(wrapString(tidyRawString(strings.ReplaceAll(msgWarnKeys, "items", "Items")), 70)) } c.Ui.Output(fmt.Sprintf("Example variable specification written to %s", fileName)) } return 0 } const ( msgWarnKeys = ` REMINDER: While keys in the items map can contain dots, using them in templates is easier when they do not. As a best practice, avoid dotted keys when possible.` msgOnlyItemsRequired = ` The items map is the only strictly required part of a variable specification, since path and namespace can be set via other means. It contains the sensitive material to encrypt and store as a Nomad variable. The entire items map is encrypted and decrypted as a single unit.` ) var defaultHclVarSpec = strings.TrimSpace(` # A variable path can be specified in the specification file # and will be used when writing the variable without specifying a # path in the command or when writing JSON directly to the `+"`/var/`"+` # HTTP API endpoint # path = "path/to/variable" # The Namespace to write the variable can be included in the specification. This # value can be overridden by specifying the "-namespace" flag on the "put" # command. # namespace = "default" `+makeHCLComment(msgOnlyItemsRequired)+` `+makeHCLComment(msgWarnKeys)+` items { key1 = "value 1" key2 = "value 2" } `) + "\n" var defaultJsonVarSpec = strings.TrimSpace(` { "Namespace": "default", "Path": "path/to/variable", "Items": { "key1": "value 1", "key2": "value 2" } } `) + "\n" // makeHCLComment is a helper function that will take the contents of a raw // string, tidy them, wrap them to 68 characters and add a leading comment // marker plus a space. func makeHCLComment(in string) string { return wrapAndPrepend(tidyRawString(in), 70, "# ") } // wrapString is a convenience func to abstract away the word wrapping // implementation func wrapString(input string, lineLen int) string { return wordwrap.String(input, lineLen) } // wrapAndPrepend will word wrap the input string to lineLen characters and // prepend the provided prefix to every line. The total length of each returned // line will be at most len(input[line])+len(prefix) func wrapAndPrepend(input string, lineLen int, prefix string) string { ss := strings.Split(wrapString(input, lineLen-len(prefix)), "\n") prefixStringList(ss, prefix) return strings.Join(ss, "\n") } // tidyRawString will convert a wrapped and indented raw string into a single // long string suitable for rewrapping with another tool. It trims leading and // trailing whitespace and then consume groups of tabs, newlines, and spaces // replacing them with a single space func tidyRawString(raw string) string { re := regexp.MustCompile("[\t\n ]+") return re.ReplaceAllString(strings.TrimSpace(raw), " ") } // prefixStringList is a helper function that prepends each item in a slice of // string with a provided prefix. func prefixStringList(ss []string, prefix string) []string { for i, s := range ss { ss[i] = prefix + s } return ss }