573 lines
15 KiB
Go
573 lines
15 KiB
Go
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package extraconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
|
|
"github.com/vmware/vic/pkg/log"
|
|
)
|
|
|
|
const (
|
|
// GuestInfoPrefix is dictated by vSphere
|
|
GuestInfoPrefix = "guestinfo."
|
|
|
|
// ScopeTag is the tag name used for declaring scopes for a field
|
|
ScopeTag = "scope"
|
|
// KeyTag is the tag name by which to override default key naming based on field name
|
|
KeyTag = "key"
|
|
// RecurseTag is the tag name with which different recursion properties are declared
|
|
RecurseTag = "recurse"
|
|
|
|
// HiddenScope means the key is hidden from the guest and will not have a GuestInfoPrefix
|
|
HiddenScope = "hidden"
|
|
// ReadOnlyScope means the key is read-only from the guest
|
|
ReadOnlyScope = "read-only"
|
|
// ReadWriteScope means the key may be read and modified by the guest
|
|
ReadWriteScope = "read-write"
|
|
// VolatileScope means that the value is expected to change and should be refreshed on use
|
|
VolatileScope = "volatile"
|
|
|
|
// SecretSuffix means the value should be encrypted in the vmx.
|
|
SecretSuffix = "secret"
|
|
// NonPersistentSuffix means the key should only be written if the key will be deleted on guest power off.
|
|
NonPersistentSuffix = "non-persistent"
|
|
|
|
// RecurseDepthProperty controls how deep to recuse into a structure field from this level. A value of zero
|
|
// prevents both encode and decode of that field. This is provided to control recursion into unannotated structures.
|
|
// This is unbounded if not specified.
|
|
RecurseDepthProperty = "depth"
|
|
// RecurseFollowProperty instructs encode and decode to follow pointers.
|
|
RecurseFollowProperty = "follow"
|
|
// RecurseNoFollowProperty instructs encode and decode not to follow pointers.
|
|
RecurseNoFollowProperty = "nofollow"
|
|
// RecurseSkipEncodeProperty causes the marked field and subfields to be skipped when encoding.
|
|
RecurseSkipEncodeProperty = "skip-encode"
|
|
// RecurseSkipDecodeProperty causes the marked field and subfields to be skipped when decoding.
|
|
RecurseSkipDecodeProperty = "skip-decode"
|
|
)
|
|
|
|
// TODO: this entire section of variables should be turned into a config struct
|
|
// that can be passed to Encode and Decode, or that Encode and Decode are method on
|
|
var (
|
|
// DefaultTagName is the annotation tag name we use for basic semantic version. Not currently used.
|
|
DefaultTagName = "vic"
|
|
|
|
// DefaultPrefix is prepended to generated key paths for basic namespacing
|
|
DefaultPrefix = "vice."
|
|
|
|
//Separator for slice values and map keys
|
|
Separator = "|"
|
|
|
|
// suffix separator character
|
|
suffixSeparator = "@"
|
|
)
|
|
|
|
func defaultGuestInfoPrefix() string {
|
|
return GuestInfoPrefix + DefaultPrefix
|
|
}
|
|
|
|
const (
|
|
// Invalid value
|
|
Invalid = 1 << iota
|
|
// Hidden value
|
|
Hidden
|
|
// ReadOnly value
|
|
ReadOnly
|
|
// WriteOnly value
|
|
WriteOnly
|
|
// ReadWrite value
|
|
ReadWrite
|
|
// NonPersistent value
|
|
NonPersistent
|
|
// Volatile value
|
|
Volatile
|
|
// Secret value
|
|
Secret
|
|
)
|
|
|
|
type recursion struct {
|
|
// depth is a recursion depth, 0 equating to skip field
|
|
depth int
|
|
// follow controls whether we follow pointers
|
|
follow bool
|
|
// set to skip decode of a field but still allow encode
|
|
skipDecode bool
|
|
// set to skip encode of a field but still allow decode
|
|
skipEncode bool
|
|
}
|
|
|
|
// Unbounded is the value used for unbounded recursion
|
|
var Unbounded = recursion{depth: -1, follow: true}
|
|
|
|
var logger = &logrus.Logger{
|
|
Out: os.Stderr,
|
|
// We're using our own text formatter to skip the \n and \t escaping logrus
|
|
// was doing on non TTY Out (we redirect to a file) descriptors.
|
|
Formatter: log.NewTextFormatter(),
|
|
Hooks: make(logrus.LevelHooks),
|
|
Level: logrus.InfoLevel,
|
|
}
|
|
|
|
// SetLogLevel for the extraconfig package
|
|
func SetLogLevel(level logrus.Level) {
|
|
logger.Level = level
|
|
}
|
|
|
|
// calculateScope returns the uint representation of scope tag
|
|
func calculateScope(scopes []string) uint {
|
|
var scope uint
|
|
|
|
empty := true
|
|
for i := range scopes {
|
|
if scopes[i] != "" {
|
|
empty = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if empty {
|
|
return Hidden | ReadOnly
|
|
}
|
|
|
|
for _, v := range scopes {
|
|
switch v {
|
|
case HiddenScope:
|
|
scope |= Hidden
|
|
case ReadOnlyScope:
|
|
scope |= ReadOnly
|
|
case ReadWriteScope:
|
|
scope |= ReadWrite
|
|
case VolatileScope:
|
|
scope |= Volatile
|
|
case NonPersistentSuffix:
|
|
scope |= NonPersistent
|
|
case SecretSuffix:
|
|
scope |= Secret | ReadOnly
|
|
default:
|
|
return Invalid
|
|
}
|
|
}
|
|
return scope
|
|
}
|
|
|
|
func isSecret(key string) bool {
|
|
suffix := strings.Split(key, suffixSeparator)
|
|
if len(suffix) < 2 {
|
|
// no @ separator
|
|
return false
|
|
}
|
|
|
|
for i := range suffix[1:] {
|
|
if suffix[i+1] == SecretSuffix {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func isNonPersistent(key string) bool {
|
|
suffix := strings.Split(key, suffixSeparator)
|
|
if len(suffix) < 2 {
|
|
// no @ separator
|
|
return false
|
|
}
|
|
|
|
for i := range suffix[1:] {
|
|
if suffix[i+1] == NonPersistentSuffix {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func calculateScopeFromKey(key string) []string {
|
|
scopes := []string{}
|
|
|
|
if !strings.HasPrefix(key, GuestInfoPrefix) {
|
|
scopes = append(scopes, HiddenScope)
|
|
}
|
|
|
|
if strings.Contains(key, "/") {
|
|
scopes = append(scopes, ReadOnlyScope)
|
|
} else {
|
|
scopes = append(scopes, ReadWriteScope)
|
|
}
|
|
|
|
if isSecret(key) {
|
|
scopes = append(scopes, SecretSuffix)
|
|
}
|
|
|
|
if isNonPersistent(key) {
|
|
scopes = append(scopes, NonPersistentSuffix)
|
|
}
|
|
|
|
return scopes
|
|
}
|
|
|
|
func calculateKeyFromField(field reflect.StructField, prefix string, depth recursion) (string, recursion) {
|
|
skip := recursion{}
|
|
//skip unexported fields
|
|
if field.PkgPath != "" {
|
|
logger.Debugf("Skipping %s (not exported)", field.Name)
|
|
return "", skip
|
|
}
|
|
|
|
// get the annotations
|
|
tags := field.Tag
|
|
logger.Debugf("Tags: %#v", tags)
|
|
|
|
var key string
|
|
var scopes []string
|
|
var scope uint
|
|
|
|
fdepth := depth
|
|
|
|
prefixScopes := calculateScopeFromKey(prefix)
|
|
prefixScope := calculateScope(prefixScopes)
|
|
|
|
// do we have DefaultTagName?
|
|
if tags.Get(DefaultTagName) != "" {
|
|
// get the scopes
|
|
scopes = strings.Split(tags.Get(ScopeTag), ",")
|
|
logger.Debugf("Scopes: %#v", scopes)
|
|
|
|
// get the keys and split properties from it
|
|
key = tags.Get(KeyTag)
|
|
logger.Debugf("Key specified: %s", key)
|
|
|
|
// get the keys and split properties from it
|
|
recurse := tags.Get(RecurseTag)
|
|
if recurse != "" {
|
|
props := strings.Split(recurse, ",")
|
|
// process properties
|
|
for _, prop := range props {
|
|
// determine recursion depth
|
|
if strings.HasPrefix(prop, RecurseDepthProperty) {
|
|
parts := strings.Split(prop, "=")
|
|
if len(parts) != 2 {
|
|
logger.Warnf("Skipping field with incorrect recurse property: %s", prop)
|
|
return "", skip
|
|
}
|
|
|
|
val, err := strconv.ParseInt(parts[1], 10, 64)
|
|
if err != nil {
|
|
logger.Warnf("Skipping field with incorrect recurse value: %s", parts[1])
|
|
return "", skip
|
|
}
|
|
fdepth.depth = int(val)
|
|
} else if prop == RecurseNoFollowProperty {
|
|
fdepth.follow = false
|
|
} else if prop == RecurseFollowProperty {
|
|
fdepth.follow = true
|
|
} else if prop == RecurseSkipDecodeProperty {
|
|
fdepth.skipDecode = true
|
|
} else if prop == RecurseSkipEncodeProperty {
|
|
fdepth.skipEncode = true
|
|
} else {
|
|
logger.Warnf("Ignoring unknown recurse property %s (%s)", key, prop)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
logger.Debugf("%s not tagged - inheriting parent scope", field.Name)
|
|
scopes = prefixScopes
|
|
}
|
|
|
|
if key == "" {
|
|
logger.Debugf("%s does not specify key - defaulting to fieldname", field.Name)
|
|
key = field.Name
|
|
}
|
|
|
|
scope = calculateScope(scopes)
|
|
|
|
// non-persistent is inherited, even if other scopes are specified
|
|
if prefixScope&NonPersistent != 0 {
|
|
scope |= NonPersistent
|
|
}
|
|
|
|
// re-calculate the key based on the scope and prefix
|
|
if key = calculateKey(scope, prefix, key); key == "" {
|
|
logger.Debugf("Skipping %s (unknown scope %s)", field.Name, scopes)
|
|
return "", skip
|
|
}
|
|
|
|
return key, fdepth
|
|
}
|
|
|
|
// calculateKey calculates the key based on the scope and current prefix
|
|
func calculateKey(scope uint, prefix string, key string) string {
|
|
if scope&Invalid != 0 {
|
|
logger.Debugf("invalid scope")
|
|
return ""
|
|
}
|
|
|
|
newSep := "/"
|
|
oldSep := "."
|
|
key = strings.TrimSpace(key)
|
|
|
|
hide := scope&Hidden != 0
|
|
write := scope&ReadWrite != 0
|
|
visible := strings.HasPrefix(prefix, GuestInfoPrefix)
|
|
|
|
if !hide && write {
|
|
oldSep = "/"
|
|
newSep = "."
|
|
}
|
|
|
|
// strip any existing suffix from the prefix - it'll be re-added if still applicable
|
|
suffix := strings.Index(prefix, suffixSeparator)
|
|
if suffix != -1 {
|
|
prefix = prefix[:suffix]
|
|
}
|
|
|
|
// assemble the actual keypath with appropriate separators
|
|
out := key
|
|
if prefix != "" {
|
|
out = strings.Join([]string{prefix, key}, newSep)
|
|
}
|
|
|
|
if scope&Secret != 0 {
|
|
out += suffixSeparator + SecretSuffix
|
|
}
|
|
|
|
if scope&NonPersistent != 0 {
|
|
if hide {
|
|
logger.Debugf("Unable to combine non-persistent and hidden scopes")
|
|
return ""
|
|
}
|
|
out += suffixSeparator + NonPersistentSuffix
|
|
}
|
|
|
|
// we don't care about existing separators when hiden
|
|
if hide {
|
|
if !visible {
|
|
return out
|
|
}
|
|
|
|
// strip the prefix and the leading r/w signifier
|
|
return out[len(defaultGuestInfoPrefix())+1:]
|
|
}
|
|
|
|
// ensure that separators are correct
|
|
out = strings.Replace(out, oldSep, newSep, -1)
|
|
|
|
// Assemble the base that controls key publishing in guest
|
|
if !visible {
|
|
return defaultGuestInfoPrefix() + newSep + out
|
|
}
|
|
|
|
// prefix will have been mangled by strings.Replace
|
|
return defaultGuestInfoPrefix() + out[len(defaultGuestInfoPrefix()):]
|
|
}
|
|
|
|
// utility function to allow adding of arbitrary prefix into key
|
|
// header is a leading segment that is preserved, prefix is injected after that
|
|
func addPrefixToKey(header, prefix, key string) string {
|
|
if prefix == "" {
|
|
return key
|
|
}
|
|
|
|
base := strings.TrimPrefix(key, header)
|
|
separator := base[0]
|
|
|
|
var modifiedPrefix string
|
|
if separator == '.' {
|
|
modifiedPrefix = strings.Replace(prefix, "/", ".", -1)
|
|
} else {
|
|
modifiedPrefix = strings.Replace(prefix, ".", "/", -1)
|
|
}
|
|
|
|
// we assume (given usage comment for WithPrefix) that there's no leading or trailing separator
|
|
// on the prefix. base has a leading separator
|
|
// guestinfoPrefix is const so adding it to the format string directly
|
|
return fmt.Sprintf(header+"%c%s%s", separator, modifiedPrefix, base)
|
|
}
|
|
|
|
// appendToPrefix will join the value to the prefix with the separator (if any) while ensuring that
|
|
// any suffixes are moved to the end of the key
|
|
func appendToPrefix(prefix, separator, value string) string {
|
|
// strip any existing suffix from the prefix - it'll be re-added if still applicable
|
|
index := strings.Index(prefix, suffixSeparator)
|
|
suffix := ""
|
|
if index != -1 {
|
|
suffix = prefix[index:]
|
|
prefix = prefix[:index]
|
|
}
|
|
|
|
// suffix wil still include the suffix separator if present
|
|
key := fmt.Sprintf("%s%s%s%s", prefix, separator, value, suffix)
|
|
|
|
return key
|
|
}
|
|
|
|
func calculateKeys(v reflect.Value, field string, prefix string) []string {
|
|
logger.Debugf("v=%#v, field=%#v, prefix=%#v", v, field, prefix)
|
|
if v.Kind() == reflect.Ptr {
|
|
return calculateKeys(v.Elem(), field, prefix)
|
|
}
|
|
|
|
if field == "" {
|
|
return []string{prefix}
|
|
}
|
|
|
|
s := strings.SplitN(field, ".", 2)
|
|
field = ""
|
|
iterate := false
|
|
if s[0] == "*" {
|
|
iterate = true
|
|
}
|
|
|
|
if len(s) > 1 {
|
|
field = s[1]
|
|
}
|
|
|
|
if !iterate {
|
|
switch v.Kind() {
|
|
case reflect.Map:
|
|
found := false
|
|
for _, k := range v.MapKeys() {
|
|
sk := k.Convert(reflect.TypeOf(""))
|
|
if sk.String() == s[0] {
|
|
v = v.MapIndex(k)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
panic(fmt.Sprintf("could not find map key %s", s[0]))
|
|
}
|
|
prefix = appendToPrefix(prefix, Separator, s[0])
|
|
case reflect.Array, reflect.Slice:
|
|
i, err := strconv.Atoi(s[0])
|
|
if err != nil {
|
|
panic(fmt.Sprintf("bad array index %s: %s", s[0], err))
|
|
}
|
|
switch v.Type().Elem().Kind() {
|
|
case reflect.Struct:
|
|
prefix = appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
|
|
case reflect.Uint8:
|
|
return []string{prefix}
|
|
default:
|
|
prefix = appendToPrefix(prefix, "", "~")
|
|
}
|
|
v = v.Index(i)
|
|
case reflect.Struct:
|
|
f, found := v.Type().FieldByName(s[0])
|
|
if !found {
|
|
panic(fmt.Sprintf("could not find field %s", s[0]))
|
|
}
|
|
prefix, _ = calculateKeyFromField(f, prefix, recursion{})
|
|
v = v.FieldByIndex(f.Index)
|
|
default:
|
|
panic(fmt.Sprintf("cannot get field from type %s", v.Type()))
|
|
}
|
|
|
|
return calculateKeys(v, field, prefix)
|
|
}
|
|
|
|
var out []string
|
|
switch v.Kind() {
|
|
case reflect.Map:
|
|
for _, k := range v.MapKeys() {
|
|
sk := k.Convert(reflect.TypeOf(""))
|
|
prefix := appendToPrefix(prefix, Separator, sk.String())
|
|
out = append(out, calculateKeys(v.MapIndex(k), field, prefix)...)
|
|
}
|
|
case reflect.Array, reflect.Slice:
|
|
switch v.Type().Elem().Kind() {
|
|
case reflect.Struct:
|
|
for i := 0; i < v.Len(); i++ {
|
|
prefix := appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
|
|
out = append(out, calculateKeys(v.Index(i), field, prefix)...)
|
|
}
|
|
case reflect.Uint8:
|
|
return []string{prefix}
|
|
default:
|
|
return []string{appendToPrefix(prefix, "", "~")}
|
|
}
|
|
case reflect.Struct:
|
|
for i := 0; i < v.NumField(); i++ {
|
|
prefix, _ := calculateKeyFromField(v.Type().Field(i), prefix, recursion{})
|
|
out = append(out, calculateKeys(v.Field(i), field, prefix)...)
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("can't iterate type %s", v.Type().String()))
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
// CalculateKeys gets the keys in extraconfig corresponding to the field
|
|
// specification passed in for obj. Examples:
|
|
//
|
|
// type struct A {
|
|
// I int `vic:"0.1" scope:"read-only" key:"i"`
|
|
// Str string `vic:"0.1" scope:"read-only" key:"str"`
|
|
// }
|
|
//
|
|
// type struct B {
|
|
// A A `vic:"0.1" scope:"read-only" key:"a"`
|
|
// Array []A `vic:"0.1" scope:"read-only" key:"array"`
|
|
// Map map[string]string `vic:"0.1" scope:"read-only" key:"map"`
|
|
// }
|
|
//
|
|
// b := B{}
|
|
// b.Array = []A{A{}}
|
|
// b.Map = map[string]string{"foo": "", "bar": ""}
|
|
// // returns []string{"a/str"}
|
|
// CalculateKeys(b, "A.Str", "")
|
|
//
|
|
// // returns []string{"array|0"}
|
|
// CalculateKeys(b, "Array.0", "")
|
|
//
|
|
// // returns []string{"array|0"}
|
|
// CalculateKeys(b, "Array.*", "")
|
|
//
|
|
// // returns []string{"map|foo", "map|bar"}
|
|
// CalculateKeys(b, "Map.*", "")
|
|
//
|
|
// // returns []string{"map|foo"}
|
|
// CalculateKeys(b, "Map.foo", "")
|
|
//
|
|
// // returns []string{"map|foo/str"}
|
|
// CalculateKeys(b, "Map.foo.str", "")
|
|
//
|
|
func CalculateKeys(obj interface{}, field string, prefix string) []string {
|
|
return calculateKeys(reflect.ValueOf(obj), field, prefix)
|
|
}
|
|
|
|
// CalculateKey is a specific case of CalculateKeys that will panic if more than one key
|
|
// matches the field pattern passed in.
|
|
func CalculateKey(obj interface{}, field string, prefix string) string {
|
|
keys := calculateKeys(reflect.ValueOf(obj), field, prefix)
|
|
if len(keys) != 1 {
|
|
panic("CalculateKey should only ever return one key")
|
|
}
|
|
|
|
return keys[0]
|
|
}
|