github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/var_init.go (about)

     1  package command
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/fs"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"github.com/muesli/reflow/wordwrap"
    12  	"github.com/posener/complete"
    13  )
    14  
    15  const (
    16  	// DefaultHclVarInitName is the default name we use when initializing the
    17  	// example var file in HCL format
    18  	DefaultHclVarInitName = "spec.nv.hcl"
    19  
    20  	// DefaultHclVarInitName is the default name we use when initializing the
    21  	// example var file in JSON format
    22  	DefaultJsonVarInitName = "spec.nv.json"
    23  )
    24  
    25  // VarInitCommand generates a new variable specification
    26  type VarInitCommand struct {
    27  	Meta
    28  }
    29  
    30  func (c *VarInitCommand) Help() string {
    31  	helpText := `
    32  Usage: nomad var init <filename>
    33  
    34    Creates an example variable specification file that can be used as a starting
    35    point to customize further. When no filename is supplied, a default filename
    36    of "spec.nv.hcl" or "spec.nv.json" will be used depending on the output
    37    format.
    38  
    39  Init Options:
    40  
    41    -out (hcl | json)
    42      Format of generated variable specification. Defaults to "hcl".
    43  
    44  `
    45  	return strings.TrimSpace(helpText)
    46  }
    47  
    48  func (c *VarInitCommand) Synopsis() string {
    49  	return "Create an example variable specification file"
    50  }
    51  
    52  func (c *VarInitCommand) AutocompleteFlags() complete.Flags {
    53  	return complete.Flags{
    54  		"-out": complete.PredictSet("hcl", "json"),
    55  	}
    56  }
    57  
    58  func (c *VarInitCommand) AutocompleteArgs() complete.Predictor {
    59  	return complete.PredictNothing
    60  }
    61  
    62  func (c *VarInitCommand) Name() string { return "var init" }
    63  
    64  func (c *VarInitCommand) Run(args []string) int {
    65  	var outFmt string
    66  	var quiet bool
    67  
    68  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
    69  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    70  	flags.StringVar(&outFmt, "out", "hcl", "")
    71  
    72  	if err := flags.Parse(args); err != nil {
    73  		return 1
    74  	}
    75  
    76  	// Check that we get no arguments
    77  	args = flags.Args()
    78  	if l := len(args); l > 1 {
    79  		c.Ui.Error("This command takes no arguments or one: <filename>")
    80  		c.Ui.Error(commandErrorText(c))
    81  		return 1
    82  	}
    83  	var fileName, fileContent string
    84  	switch outFmt {
    85  	case "hcl":
    86  		fileName = DefaultHclVarInitName
    87  		fileContent = defaultHclVarSpec
    88  	case "json":
    89  		fileName = DefaultJsonVarInitName
    90  		fileContent = defaultJsonVarSpec
    91  	}
    92  
    93  	if len(args) == 1 {
    94  		fileName = args[0]
    95  	}
    96  
    97  	// Check if the file already exists
    98  	_, err := os.Stat(fileName)
    99  	if err == nil {
   100  		c.Ui.Error(fmt.Sprintf("File %q already exists", fileName))
   101  		return 1
   102  	}
   103  	if err != nil && !errors.Is(err, fs.ErrNotExist) {
   104  		c.Ui.Error(fmt.Sprintf("Failed to stat %q: %v", fileName, err))
   105  		return 1
   106  	}
   107  
   108  	// Write out the example
   109  	err = os.WriteFile(fileName, []byte(fileContent), 0660)
   110  	if err != nil {
   111  		c.Ui.Error(fmt.Sprintf("Failed to write %q: %v", fileName, err))
   112  		return 1
   113  	}
   114  
   115  	// Success
   116  	if !quiet {
   117  		if outFmt == "json" {
   118  			c.Ui.Info(wrapString(tidyRawString(strings.ReplaceAll(msgOnlyItemsRequired, "items", "Items")), 70))
   119  			c.Ui.Warn(wrapString(tidyRawString(strings.ReplaceAll(msgWarnKeys, "items", "Items")), 70))
   120  		}
   121  		c.Ui.Output(fmt.Sprintf("Example variable specification written to %s", fileName))
   122  	}
   123  	return 0
   124  }
   125  
   126  const (
   127  	msgWarnKeys = `
   128  	REMINDER: While keys in the items map can contain dots, using them in
   129  	templates is easier when they do not. As a best practice, avoid dotted
   130  	keys when possible.`
   131  	msgOnlyItemsRequired = `
   132  	The items map is the only strictly required part of a variable
   133  	specification, since path and namespace can be set via other means. It
   134  	contains the sensitive material to encrypt and store as a Nomad variable.
   135  	The entire items map is encrypted and decrypted as a single unit.`
   136  )
   137  
   138  var defaultHclVarSpec = strings.TrimSpace(`
   139  # A variable path can be specified in the specification file
   140  # and will be used when writing the variable without specifying a
   141  # path in the command or when writing JSON directly to the `+"`/var/`"+`
   142  # HTTP API endpoint
   143  # path = "path/to/variable"
   144  
   145  # The Namespace to write the variable can be included in the specification. This
   146  # value can be overridden by specifying the "-namespace" flag on the "put"
   147  # command.
   148  # namespace = "default"
   149  
   150  `+makeHCLComment(msgOnlyItemsRequired)+`
   151  
   152  `+makeHCLComment(msgWarnKeys)+`
   153  items {
   154    key1 = "value 1"
   155    key2 = "value 2"
   156  }
   157  `) + "\n"
   158  
   159  var defaultJsonVarSpec = strings.TrimSpace(`
   160  {
   161    "Namespace": "default",
   162    "Path": "path/to/variable",
   163    "Items": {
   164      "key1": "value 1",
   165      "key2": "value 2"
   166    }
   167  }
   168  `) + "\n"
   169  
   170  // makeHCLComment is a helper function that will take the contents of a raw
   171  // string, tidy them, wrap them to 68 characters and add a leading comment
   172  // marker plus a space.
   173  func makeHCLComment(in string) string {
   174  	return wrapAndPrepend(tidyRawString(in), 70, "# ")
   175  }
   176  
   177  // wrapString is a convenience func to abstract away the word wrapping
   178  // implementation
   179  func wrapString(input string, lineLen int) string {
   180  	return wordwrap.String(input, lineLen)
   181  }
   182  
   183  // wrapAndPrepend will word wrap the input string to lineLen characters and
   184  // prepend the provided prefix to every line. The total length of each returned
   185  // line will be at most len(input[line])+len(prefix)
   186  func wrapAndPrepend(input string, lineLen int, prefix string) string {
   187  	ss := strings.Split(wrapString(input, lineLen-len(prefix)), "\n")
   188  	prefixStringList(ss, prefix)
   189  	return strings.Join(ss, "\n")
   190  }
   191  
   192  // tidyRawString will convert a wrapped and indented raw string into a single
   193  // long string suitable for rewrapping with another tool. It trims leading and
   194  // trailing whitespace and then consume groups of tabs, newlines, and spaces
   195  // replacing them with a single space
   196  func tidyRawString(raw string) string {
   197  	re := regexp.MustCompile("[\t\n ]+")
   198  	return re.ReplaceAllString(strings.TrimSpace(raw), " ")
   199  }
   200  
   201  // prefixStringList is a helper function that prepends each item in a slice of
   202  // string with a provided prefix.
   203  func prefixStringList(ss []string, prefix string) []string {
   204  	for i, s := range ss {
   205  		ss[i] = prefix + s
   206  	}
   207  	return ss
   208  }