2020-08-13 11:20:22 -04:00
package common
import (
2021-09-26 11:48:43 -04:00
"bytes"
"encoding/json"
"errors"
2020-08-13 11:20:22 -04:00
"fmt"
2021-09-26 11:48:43 -04:00
"regexp"
"strconv"
"strings"
2020-08-13 11:20:22 -04:00
"github.com/hashicorp/boundary/internal/cmd/base"
2021-09-26 11:48:43 -04:00
"github.com/hashicorp/go-secure-stdlib/parseutil"
2020-09-08 13:27:51 -07:00
"github.com/posener/complete"
2020-08-13 11:20:22 -04:00
)
2021-06-02 13:05:10 -05:00
func PopulateCommonFlags ( c * base . Command , f * base . FlagSet , resourceType string , flagNames map [ string ][] string , command string ) {
for _ , name := range flagNames [ command ] {
2020-08-13 11:20:22 -04:00
switch name {
2020-09-08 13:27:51 -07:00
case "scope-id" :
f . StringVar ( & base . StringVar {
Name : "scope-id" ,
Target : & c . FlagScopeId ,
2020-10-02 19:20:30 -04:00
EnvVar : "BOUNDARY_SCOPE_ID" ,
2020-09-08 13:27:51 -07:00
Default : "global" ,
Completion : complete . PredictAnything ,
2020-10-22 08:40:00 -04:00
Usage : `Scope in which to make the request.` ,
})
case "scope-name" :
f . StringVar ( & base . StringVar {
Name : "scope-name" ,
Target : & c . FlagScopeName ,
EnvVar : "BOUNDARY_SCOPE_NAME" ,
Completion : complete . PredictAnything ,
Usage : `Scope in which to make the request, identified by name.` ,
2020-09-08 13:27:51 -07:00
})
2020-08-13 11:20:22 -04:00
case "id" :
f . StringVar ( & base . StringVar {
Name : "id" ,
Target : & c . FlagId ,
2020-10-22 08:40:00 -04:00
Usage : fmt . Sprintf ( "ID of the %s on which to operate." , resourceType ),
2020-08-13 11:20:22 -04:00
})
case "name" :
f . StringVar ( & base . StringVar {
Name : "name" ,
Target : & c . FlagName ,
2020-10-22 08:40:00 -04:00
Usage : fmt . Sprintf ( "Name to set on the %s." , resourceType ),
2020-08-13 11:20:22 -04:00
})
case "description" :
f . StringVar ( & base . StringVar {
Name : "description" ,
Target : & c . FlagDescription ,
2020-10-22 08:40:00 -04:00
Usage : fmt . Sprintf ( "Description to set on the %s." , resourceType ),
2020-08-13 11:20:22 -04:00
})
case "version" :
f . IntVar ( & base . IntVar {
Name : "version" ,
Target : & c . FlagVersion ,
2020-08-18 17:46:34 -04:00
Usage : fmt . Sprintf ( "The version of the %s against which to perform an update operation. If not specified, the command will perform a check-and-set automatically." , resourceType ),
2020-08-13 11:20:22 -04:00
})
2020-10-02 19:20:30 -04:00
case "auth-method-id" :
f . StringVar ( & base . StringVar {
Name : "auth-method-id" ,
EnvVar : "BOUNDARY_AUTH_METHOD_ID" ,
Target : & c . FlagAuthMethodId ,
2020-10-22 08:40:00 -04:00
Usage : "The auth-method resource to use for the operation." ,
2020-10-02 19:20:30 -04:00
})
case "host-catalog-id" :
f . StringVar ( & base . StringVar {
Name : "host-catalog-id" ,
EnvVar : "BOUNDARY_HOST_CATALOG_ID" ,
Target : & c . FlagHostCatalogId ,
2020-10-22 08:40:00 -04:00
Usage : "The host-catalog resource to use for the operation." ,
2020-10-02 19:20:30 -04:00
})
2021-06-22 22:26:26 -04:00
case "credential-store-id" :
f . StringVar ( & base . StringVar {
Name : "credential-store-id" ,
EnvVar : "BOUNDARY_CREDENTIAL_STORE_ID" ,
Target : & c . FlagCredentialStoreId ,
Usage : "The credential-store resource to use for the operation." ,
})
2021-01-23 13:39:04 -05:00
case "recursive" :
f . BoolVar ( & base . BoolVar {
Name : "recursive" ,
Target : & c . FlagRecursive ,
Usage : "If set, the list operation will be applied recursively into child scopes, if supported by the type." ,
})
2021-06-02 13:05:10 -05:00
}
}
if command == "list" {
for _ , name := range flagNames [ command ] {
switch name {
case "filter" :
f . StringVar ( & base . StringVar {
Name : "filter" ,
Target : & c . FlagFilter ,
Usage : "If set, the list operation will be filtered before being returned. The filter operates against each item in the list. Using single quotes is recommended as filters contain double quotes. See https://www.boundaryproject.io/docs/concepts/filtering/resource-listing for details." ,
})
}
2020-08-13 11:20:22 -04:00
}
}
}
2021-09-26 11:48:43 -04:00
func PopulateAttributeFlags ( c * base . Command , f * base . FlagSet , flagNames map [ string ][] string , command string ) {
keyDelimiter := "."
for _ , name := range flagNames [ command ] {
switch name {
case "attributes" :
f . StringVar ( & base . StringVar {
Name : "attributes" ,
Target : & c . FlagAttributes ,
Usage : `A JSON map value to use as the entirety of the request's attribute map. Usually this will be soured from a file via "file://" syntax.` ,
})
case "attr" :
f . CombinationSliceVar ( & base . CombinationSliceVar {
Name : "attr" ,
Target : & c . FlagAttrs ,
KvSplit : true ,
KeyDelimiter : & keyDelimiter ,
ProtoCompatKey : true ,
Usage : `A key=value attribute to add to the request's attribute map. The type is automatically inferred. Use -string-attr, -bool-attr, or -num-attr if the type needs to be overridden. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"` ,
})
case "string-attr" :
f . CombinationSliceVar ( & base . CombinationSliceVar {
Name : "string-attr" ,
Target : & c . FlagAttrs ,
KvSplit : true ,
KeyDelimiter : & keyDelimiter ,
ProtoCompatKey : true ,
Usage : `A key=value string attribute to add to the request's attribute map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"` ,
})
case "bool-attr" :
f . CombinationSliceVar ( & base . CombinationSliceVar {
Name : "bool-attr" ,
Target : & c . FlagAttrs ,
KvSplit : true ,
KeyDelimiter : & keyDelimiter ,
ProtoCompatKey : true ,
Usage : `A key=value bool attribute to add to the request's attribute map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"` ,
})
case "num-attr" :
f . CombinationSliceVar ( & base . CombinationSliceVar {
Name : "num-attr" ,
Target : & c . FlagAttrs ,
KvSplit : true ,
KeyDelimiter : & keyDelimiter ,
ProtoCompatKey : true ,
Usage : `A key=value numeric attribute to add to the request's attribute map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"` ,
})
}
}
}
// From https://stackoverflow.com/a/13340826, modified to remove exponents
var jsonNumberRegex = regexp . MustCompile ( `^-?(?:0|[1-9]\d*)(?:\.\d+)?$` )
// HandleAttributeFlags takes in a command and a func to call for default (that
// is, set to nil) and non-default values. Suffix can be used to allow this
// logic to be used for various needs, e.g. -attr vs -secret.
func HandleAttributeFlags ( c * base . Command , suffix string , defaultFunc func (), setFunc func ( map [ string ] interface {})) error {
// If we were given -attributes, use that as-is
switch c . FlagAttributes {
case "" :
// Nothing, continue on
case "null" :
defaultFunc ()
return nil
default :
parsedString , err := parseutil . ParsePath ( c . FlagAttributes )
if err != nil && ! errors . Is ( err , parseutil . ErrNotAUrl ) {
return fmt . Errorf ( "error parsing attributes flag as a URL: %w" , err )
}
// We should be able to parse the string as a JSON object
var setMap map [ string ] interface {}
if err := json . Unmarshal ([] byte ( parsedString ), & setMap ); err != nil {
return fmt . Errorf ( "error parsing attributes flag as JSON: %w" , err )
}
setFunc ( setMap )
return nil
}
setMap := map [ string ] interface {}{}
for _ , attr := range c . FlagAttrs {
if len ( attr . Keys ) == 0 {
// No idea why this would happen, but skip it
continue
}
var val interface {}
var err error
// First, perform any needed parsing if we are given the type
switch attr . Name {
case "num-" + suffix :
// JSON treats all numbers equally, however, we will try to be a
// little better so that we don't include decimals if we don't need
// to (and don't have to worry about precision if not necessary)
if strings . Contains ( attr . Value , "." ) {
val , err = strconv . ParseFloat ( attr . Value , 64 )
if err != nil {
return fmt . Errorf ( "error parsing attribute %q as a float: %w" , attr . Value , err )
}
} else {
val , err = strconv . ParseInt ( attr . Value , 10 , 64 )
if err != nil {
return fmt . Errorf ( "error parsing attribute %q as an integer: %w" , attr . Value , err )
}
}
case "string-" + suffix :
val = strings . Trim ( attr . Value , `"` )
case "bool-" + suffix :
switch attr . Value {
case "true" :
val = true
case "false" :
val = false
default :
return fmt . Errorf ( "error parsing attribute %q as a bool" , attr . Value )
}
case suffix :
// In this case, use heuristics to just do the right thing the vast
// majority of the time
switch {
case attr . Value == "null" : // Explicit null, we want to set to a null value to clear it
val = nil
case attr . Value == "true" : // bool true
val = true
case attr . Value == "false" : // bool false
val = false
case strings . HasPrefix ( attr . Value , `"` ): // explicitly quoted string
val = strings . Trim ( attr . Value , `"` )
case jsonNumberRegex . Match ([] byte ( strings . Trim ( attr . Value , `"` ))): // number
// Same logic as above
if strings . Contains ( attr . Value , "." ) {
val , err = strconv . ParseFloat ( attr . Value , 64 )
if err != nil {
return fmt . Errorf ( "error parsing attribute %q as a float: %w" , attr . Value , err )
}
} else {
val , err = strconv . ParseInt ( attr . Value , 10 , 64 )
if err != nil {
return fmt . Errorf ( "error parsing attribute %q as an integer: %w" , attr . Value , err )
}
}
case strings . HasPrefix ( attr . Value , "[" ): // serialized JSON array
var s [] interface {}
u := json . NewDecoder ( bytes . NewBufferString ( attr . Value ))
u . UseNumber ()
if err := u . Decode ( & s ); err != nil {
return fmt . Errorf ( "error parsing attribute %q as a json array: %w" , attr . Value , err )
}
val = s
case strings . HasPrefix ( attr . Value , "{" ): // serialized JSON map
var m map [ string ] interface {}
u := json . NewDecoder ( bytes . NewBufferString ( attr . Value ))
u . UseNumber ()
if err := u . Decode ( & m ); err != nil {
return fmt . Errorf ( "error parsing attribute %q as a json map: %w" , attr . Value , err )
}
val = m
default :
// Default is to treat as a string value
val = attr . Value
}
default :
return fmt . Errorf ( "unknown attribute flag %q" , attr . Name )
}
// Now we have to insert it in the right position in the attribute map
currMap := setMap
for i , segment := range attr . Keys {
if segment == "" {
return fmt . Errorf ( "key segment %q for value %q is empty" , segment , attr . Value )
}
switch {
case i == len ( attr . Keys ) - 1 :
// If we get an explicit "null" override whatever is currently
// there
if val == nil {
currMap [ segment ] = nil
break
}
// We're at the last hop, do the actual insertion
switch t := currMap [ segment ].( type ) {
case nil :
// Nothing currently exists
currMap [ segment ] = val
case [] interface {}:
// It's already a slice, so just append
currMap [ segment ] = append ( t , val )
default :
// It's not a slice, so create a new slice with the
// exisitng and new values
currMap [ segment ] = [] interface {}{ t , val }
}
default :
// We need to keep traversing
switch t := currMap [ segment ].( type ) {
case nil :
// We haven't hit this segment before, so create a new
// object leading off of it and set it to current
newMap := map [ string ] interface {}{}
currMap [ segment ] = newMap
currMap = newMap
case map [ string ] interface {}:
// We've seen this before and already have a map so just set
// that as our new location
currMap = t
default :
// We should only ever be seeing maps if we're not at the
// final location
return fmt . Errorf ( "unexpected type for key segment %q: %T" , segment , t )
}
}
}
}
if len ( setMap ) > 0 {
setFunc ( setMap )
}
return nil
}