github.com/hernad/nomad@v1.6.112/command/namespace_apply.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "io" 11 "os" 12 "strings" 13 14 "github.com/hashicorp/hcl" 15 "github.com/hashicorp/hcl/hcl/ast" 16 "github.com/hernad/nomad/api" 17 flaghelper "github.com/hernad/nomad/helper/flags" 18 "github.com/mitchellh/mapstructure" 19 "github.com/posener/complete" 20 ) 21 22 type NamespaceApplyCommand struct { 23 Meta 24 } 25 26 func (c *NamespaceApplyCommand) Help() string { 27 helpText := ` 28 Usage: nomad namespace apply [options] <input> 29 30 Apply is used to create or update a namespace. The specification file 31 will be read from stdin by specifying "-", otherwise a path to the file is 32 expected. 33 34 Instead of a file, you may instead pass the namespace name to create 35 or update as the only argument. 36 37 If ACLs are enabled, this command requires a management ACL token. In 38 federated clusters, the namespace will be created in the authoritative region 39 and will be replicated to all federated regions. 40 41 General Options: 42 43 ` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + ` 44 45 Apply Options: 46 47 -quota 48 The quota to attach to the namespace. 49 50 -description 51 An optional description for the namespace. 52 53 -json 54 Parse the input as a JSON namespace specification. 55 ` 56 return strings.TrimSpace(helpText) 57 } 58 59 func (c *NamespaceApplyCommand) AutocompleteFlags() complete.Flags { 60 return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), 61 complete.Flags{ 62 "-description": complete.PredictAnything, 63 "-quota": QuotaPredictor(c.Meta.Client), 64 "-json": complete.PredictNothing, 65 }) 66 } 67 68 func (c *NamespaceApplyCommand) AutocompleteArgs() complete.Predictor { 69 return complete.PredictOr( 70 NamespacePredictor(c.Meta.Client, nil), 71 complete.PredictFiles("*.hcl"), 72 complete.PredictFiles("*.json"), 73 ) 74 } 75 76 func (c *NamespaceApplyCommand) Synopsis() string { 77 return "Create or update a namespace" 78 } 79 80 func (c *NamespaceApplyCommand) Name() string { return "namespace apply" } 81 82 func (c *NamespaceApplyCommand) Run(args []string) int { 83 var jsonInput bool 84 var description, quota *string 85 86 flags := c.Meta.FlagSet(c.Name(), FlagSetClient) 87 flags.Usage = func() { c.Ui.Output(c.Help()) } 88 flags.Var((flaghelper.FuncVar)(func(s string) error { 89 description = &s 90 return nil 91 }), "description", "") 92 flags.Var((flaghelper.FuncVar)(func(s string) error { 93 quota = &s 94 return nil 95 }), "quota", "") 96 flags.BoolVar(&jsonInput, "json", false, "") 97 98 if err := flags.Parse(args); err != nil { 99 return 1 100 } 101 102 // Check that we get exactly one argument 103 args = flags.Args() 104 if l := len(args); l != 1 { 105 c.Ui.Error("This command takes one argument: <input>") 106 c.Ui.Error(commandErrorText(c)) 107 return 1 108 } 109 110 file := args[0] 111 var rawNamespace []byte 112 var err error 113 var namespace *api.Namespace 114 115 // Get the HTTP client 116 client, err := c.Meta.Client() 117 if err != nil { 118 c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) 119 return 1 120 } 121 122 if fi, err := os.Stat(file); (file == "-" || err == nil) && !fi.IsDir() { 123 if quota != nil || description != nil { 124 c.Ui.Warn("Flags are ignored when a file is specified!") 125 } 126 127 if file == "-" { 128 rawNamespace, err = io.ReadAll(os.Stdin) 129 if err != nil { 130 c.Ui.Error(fmt.Sprintf("Failed to read stdin: %v", err)) 131 return 1 132 } 133 } else { 134 rawNamespace, err = os.ReadFile(file) 135 if err != nil { 136 c.Ui.Error(fmt.Sprintf("Failed to read file: %v", err)) 137 return 1 138 } 139 } 140 if jsonInput { 141 var jsonSpec api.Namespace 142 dec := json.NewDecoder(bytes.NewBuffer(rawNamespace)) 143 if err := dec.Decode(&jsonSpec); err != nil { 144 c.Ui.Error(fmt.Sprintf("Failed to parse quota: %v", err)) 145 return 1 146 } 147 namespace = &jsonSpec 148 } else { 149 hclSpec, err := parseNamespaceSpec(rawNamespace) 150 if err != nil { 151 c.Ui.Error(fmt.Sprintf("Error parsing quota specification: %s", err)) 152 return 1 153 } 154 155 namespace = hclSpec 156 } 157 } else { 158 name := args[0] 159 160 // Validate we have at-least a name 161 if name == "" { 162 c.Ui.Error("Namespace name required") 163 return 1 164 } 165 166 // Lookup the given namespace 167 namespace, _, err = client.Namespaces().Info(name, nil) 168 if err != nil && !strings.Contains(err.Error(), "404") { 169 c.Ui.Error(fmt.Sprintf("Error looking up namespace: %s", err)) 170 return 1 171 } 172 173 if namespace == nil { 174 namespace = &api.Namespace{ 175 Name: name, 176 } 177 } 178 179 // Add what is set 180 if description != nil { 181 namespace.Description = *description 182 } 183 if quota != nil { 184 namespace.Quota = *quota 185 } 186 } 187 _, err = client.Namespaces().Register(namespace, nil) 188 if err != nil { 189 c.Ui.Error(fmt.Sprintf("Error applying namespace: %s", err)) 190 return 1 191 } 192 193 c.Ui.Output(fmt.Sprintf("Successfully applied namespace %q!", namespace.Name)) 194 195 return 0 196 } 197 198 // parseNamespaceSpec is used to parse the namespace specification from HCL 199 func parseNamespaceSpec(input []byte) (*api.Namespace, error) { 200 root, err := hcl.ParseBytes(input) 201 if err != nil { 202 return nil, err 203 } 204 205 // Top-level item should be a list 206 list, ok := root.Node.(*ast.ObjectList) 207 if !ok { 208 return nil, fmt.Errorf("error parsing: root should be an object") 209 } 210 211 var spec api.Namespace 212 if err := parseNamespaceSpecImpl(&spec, list); err != nil { 213 return nil, err 214 } 215 216 return &spec, nil 217 } 218 219 // parseNamespaceSpec parses the quota namespace taking as input the AST tree 220 func parseNamespaceSpecImpl(result *api.Namespace, list *ast.ObjectList) error { 221 // Decode the full thing into a map[string]interface for ease 222 var m map[string]interface{} 223 if err := hcl.DecodeObject(&m, list); err != nil { 224 return err 225 } 226 227 delete(m, "capabilities") 228 delete(m, "meta") 229 delete(m, "node_pool_config") 230 231 // Decode the rest 232 if err := mapstructure.WeakDecode(m, result); err != nil { 233 return err 234 } 235 236 cObj := list.Filter("capabilities") 237 if len(cObj.Items) > 0 { 238 for _, o := range cObj.Elem().Items { 239 ot, ok := o.Val.(*ast.ObjectType) 240 if !ok { 241 break 242 } 243 var opts *api.NamespaceCapabilities 244 if err := hcl.DecodeObject(&opts, ot.List); err != nil { 245 return err 246 } 247 result.Capabilities = opts 248 break 249 } 250 } 251 252 npObj := list.Filter("node_pool_config") 253 if len(npObj.Items) > 0 { 254 for _, o := range npObj.Elem().Items { 255 ot, ok := o.Val.(*ast.ObjectType) 256 if !ok { 257 break 258 } 259 var npConfig *api.NamespaceNodePoolConfiguration 260 if err := hcl.DecodeObject(&npConfig, ot.List); err != nil { 261 return err 262 } 263 result.NodePoolConfiguration = npConfig 264 break 265 } 266 } 267 268 if metaO := list.Filter("meta"); len(metaO.Items) > 0 { 269 for _, o := range metaO.Elem().Items { 270 var m map[string]interface{} 271 if err := hcl.DecodeObject(&m, o.Val); err != nil { 272 return err 273 } 274 if err := mapstructure.WeakDecode(m, &result.Meta); err != nil { 275 return err 276 } 277 } 278 } 279 280 return nil 281 }