VMware vSphere Integrated Containers provider (#206)

* Add Virtual Kubelet provider for VIC

Initial virtual kubelet provider for VMware VIC.  This provider currently
handles creating and starting of a pod VM via the VIC portlayer and persona
server.  Image store handling via the VIC persona server.  This provider
currently requires the feature/wolfpack branch of VIC.

* Added pod stop and delete.  Also added node capacity.

Added the ability to stop and delete pod VMs via VIC.  Also retrieve
node capacity information from the VCH.

* Cleanup and readme file

Some file clean up and added a Readme.md markdown file for the VIC
provider.

* Cleaned up errors, added function comments, moved operation code

1. Cleaned up error handling.  Set standard for creating errors.
2. Added method prototype comments for all interface functions.
3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder.

* Add mocking code and unit tests for podcache, podcreator, and podstarter

Used the unit test framework used in VIC to handle assertions in the provider's
unit test.  Mocking code generated using OSS project mockery, which is compatible
with the testify assertion framework.

* Vendored packages for the VIC provider

Requires feature/wolfpack branch of VIC and a few specific commit sha of
projects used within VIC.

* Implementation of POD Stopper and Deleter unit tests (#4)

* Updated files for initial PR
This commit is contained in:
Loc Nguyen
2018-06-04 15:41:32 -07:00
committed by Ria Bhatia
parent 98a111e8b7
commit 513cebe7b7
6296 changed files with 1123685 additions and 8 deletions

View File

@@ -0,0 +1,454 @@
// Copyright 2016 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 (
"encoding/base64"
"net"
"net/url"
"os/exec"
"testing"
"time"
"github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
// [BEGIN] SLIMMED DOWNED and MODIFIED VERSION of github.com/vmware/vic/lib/metadata
type Common struct {
ExecutionEnvironment string `vic:"0.1" recurse:"depth=0"`
ID string `vic:"0.1" scope:"read-only" key:"id"`
Name string `vic:"0.1" scope:"read-only" key:"name"`
Notes string `vic:"0.1" scope:"read-only" key:"notes"`
}
type ContainerVM struct {
Common `vic:"0.1" scope:"read-only" key:"common"`
Version string `vic:"0.1" scope:"hidden" key:"version"`
Aliases map[string]string `vic:"0.1" recurse:"depth=0"`
Interaction url.URL `vic:"0.1" recurse:"depth=0"`
AgentKey []byte `vic:"0.1" recurse:"depth=0"`
}
type ExecutorConfig struct {
Common `vic:"0.1" scope:"read-only" key:"common"`
Sessions map[string]SessionConfig `vic:"0.1" scope:"hidden" key:"sessions"`
Key string `json:"string"`
}
type ExecutorConfigPointers struct {
Common `vic:"0.1" scope:"read-only" key:"common"`
Sessions map[string]*SessionConfig `vic:"0.1" scope:"hidden" key:"sessions"`
Key string `json:"string"` // will inherit parent vic attributes
}
type Cmd struct {
Path string `vic:"0.1" scope:"hidden" key:"path"`
Args []string `vic:"0.1" scope:"hidden" key:"args"`
Env []string `vic:"0.1" scope:"hidden" key:"env"`
Dir string `vic:"0.1" scope:"hidden" key:"dir"`
Cmd *exec.Cmd `vic:"0.1" scope:"hidden" key:"cmd" recurse:"depth=0"`
}
type SessionConfig struct {
Common `vic:"0.1" scope:"hidden" key:"common" json:"page"`
Cmd Cmd `vic:"0.1" scope:"hidden" key:"cmd"`
Tty bool `vic:"0.1" scope:"hidden" key:"tty"`
}
type ExecutorConfigPointersVisible struct {
Sessions map[string]*VisibleSessionConfig `vic:"0.1" scope:"read-only" key:"sessions"`
}
type VisibleSessionConfig struct {
Cmd Cmd `vic:"0.1" scope:"read-only" key:"cmd"`
Tty bool `vic:"0.1" scope:"read-only" key:"tty"`
}
// [END] SLIMMED VERSION of github.com/vmware/vic/lib/metadata
// make it verbose during testing
func init() {
logger.Level = logrus.DebugLevel
}
func TestBasic(t *testing.T) {
type Type struct {
Int int `vic:"0.1" scope:"read-write" key:"int"`
Bool bool `vic:"0.1" scope:"read-write" key:"bool"`
Float float64 `vic:"0.1" scope:"read-write" key:"float"`
String string `vic:"0.1" scope:"read-write" key:"string"`
}
Struct := Type{
42,
true,
3.14,
"Grrr",
}
encoded := map[string]string{}
Encode(MapSink(encoded), Struct)
expected := map[string]string{
visibleRW("int"): "42",
visibleRW("bool"): "true",
visibleRW("float"): "3.14E+00",
visibleRW("string"): "Grrr",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}
func TestBasicMap(t *testing.T) {
type Type struct {
IntMap map[string]int `vic:"0.1" scope:"read-only" key:"intmap"`
}
// key is not present
var decoded Type
Decode(MapSource(nil), &decoded)
assert.NotNil(t, decoded.IntMap)
assert.Empty(t, decoded.IntMap)
IntMap := Type{
map[string]int{
"1st": 12345,
"2nd": 67890,
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), IntMap)
expected := map[string]string{
visibleRO("intmap" + Separator + "1st"): "12345",
visibleRO("intmap" + Separator + "2nd"): "67890",
visibleRO("intmap"): "1st" + Separator + "2nd",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
// Decode to new variable
decoded = Type{}
Decode(MapSource(encoded), &decoded)
assert.Equal(t, IntMap, decoded, "Encoded and decoded does not match")
// Decode to already existing variable
IntMapOptimusPrime := Type{
map[string]int{
"first": 1,
"second": 2,
"1st": 0,
},
}
Decode(MapSource(encoded), &IntMapOptimusPrime)
// We expect a merge and over-write
expectedOptimusPrime := Type{
map[string]int{
"1st": 12345,
"2nd": 67890,
"first": 1,
"second": 2,
},
}
assert.Equal(t, IntMapOptimusPrime, expectedOptimusPrime, "Decoded and expected does not match")
}
func TestBasicSlice(t *testing.T) {
type Type struct {
IntSlice []int `vic:"0.1" scope:"read-only" key:"intslice"`
}
IntSlice := Type{
[]int{1, 2, 3, 4, 5},
}
encoded := map[string]string{}
Encode(MapSink(encoded), IntSlice)
expected := map[string]string{
visibleRO("intslice~"): "1" + Separator + "2" + Separator + "3" + Separator + "4" + Separator + "5",
visibleRO("intslice"): "4",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
decoded.IntSlice = make([]int, 1)
Decode(MapSource(encoded), &decoded)
assert.Equal(t, IntSlice, decoded, "Encoded and decoded does not match")
}
func TestStruct(t *testing.T) {
type Type struct {
Common Common `vic:"0.1" scope:"read-only" key:"common"`
}
Struct := Type{
Common: Common{
ID: "0xDEADBEEF",
Name: "Struct",
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), Struct)
expected := map[string]string{
visibleRO("common/id"): "0xDEADBEEF",
visibleRO("common/name"): "Struct",
visibleRO("common/notes"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}
func TestTime(t *testing.T) {
type Type struct {
Time time.Time `vic:"0.1" scope:"read-only" key:"time"`
}
Time := Type{
Time: time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC),
}
encoded := map[string]string{}
Encode(MapSink(encoded), Time)
expected := map[string]string{
visibleRO("time"): "2009-11-10 23:00:00 +0000 UTC",
}
assert.Equal(t, encoded, expected, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Time, decoded, "Encoded and decoded does not match")
}
func TestNet(t *testing.T) {
type Type struct {
Net net.IPNet `vic:"0.1" scope:"read-only" key:"net"`
}
// 127.0.0.1/8
n := net.IPNet{IP: net.IP{0x7f, 0x0, 0x0, 0x1}, Mask: net.IPMask{0xff, 0x0, 0x0, 0x0}}
Net := Type{
Net: n,
}
encoded := map[string]string{}
Encode(MapSink(encoded), Net)
expected := map[string]string{
visibleRO("net/IP"): base64.StdEncoding.EncodeToString(n.IP),
visibleRO("net/Mask"): base64.StdEncoding.EncodeToString(n.Mask),
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Net, decoded, "Encoded and decoded does not match")
}
func TestNilNetPointer(t *testing.T) {
type Type struct {
Net *net.IPNet `vic:"0.1" scope:"read-only" key:"net"`
}
Net := Type{
Net: nil,
}
// Net should be nil - pointers are supposed to be nil if the referenced tree is zero valued
encoded := map[string]string{}
Encode(MapSink(encoded), Net)
expected := map[string]string{}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Net, decoded, "Encoded and decoded does not match")
}
func TestPointer(t *testing.T) {
type Type struct {
Pointer *ContainerVM `vic:"0.1" scope:"hidden" key:"pointer"`
PointerOmitnested *ContainerVM `vic:"0.1" scope:"non-persistent" key:"pointeromitnested" recurse:"depth=0"`
}
Pointer := Type{
Pointer: &ContainerVM{Version: "0.1"},
}
encoded := map[string]string{}
Encode(MapSink(encoded), Pointer)
expected := map[string]string{
visibleRO("pointer/common/id"): "",
visibleRO("pointer/common/name"): "",
visibleRO("pointer/common/notes"): "",
"pointer/version": "0.1",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Pointer, decoded, "Encoded and decoded does not match")
}
func TestInheritenceOfNonPersistence(t *testing.T) {
type CommonPersistence struct {
ExecutionEnvironment string `vic:"0.1" recurse:"depth=0"`
ID string `vic:"0.1" scope:"read-only" key:"id"`
Name string `vic:"0.1" scope:"read-only" key:"name"`
Notes string `vic:"0.1" scope:"hidden" key:"notes"`
}
type Type struct {
Common CommonPersistence `vic:"0.1" scope:"read-write,non-persistent" key:"common"`
}
Struct := Type{
Common: CommonPersistence{
ID: "0xDEADBEEF",
Name: "Struct",
},
}
encoded := map[string]string{}
filterSink := ScopeFilterSink(NonPersistent|Hidden, MapSink(encoded))
Encode(filterSink, Struct)
expected := map[string]string{
visibleRONonpersistent("common/id"): "0xDEADBEEF",
visibleRONonpersistent("common/name"): "Struct",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}
func TestInheritenceOfNonPersistenceWithPointer(t *testing.T) {
type Persistence struct {
ExecutorConfigPointersVisible `vic:"0.1" scope:"read-only,non-persistent" key:"pointers"`
}
Struct := Persistence{
ExecutorConfigPointersVisible: ExecutorConfigPointersVisible{
Sessions: map[string]*VisibleSessionConfig{
"primary": {
Tty: true,
},
},
},
}
encoded := map[string]string{}
filterSink := ScopeFilterSink(NonPersistent|Hidden, MapSink(encoded))
Encode(filterSink, Struct)
expected := map[string]string{
visibleRONonpersistent("pointers/sessions"): "primary",
visibleRONonpersistent("pointers/sessions" + Separator + "primary/tty"): "true",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Persistence
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}
func TestFilterSink(t *testing.T) {
type CommonPersistence struct {
ExecutionEnvironment string `vic:"0.1" recurse:"depth=0"`
ID string `vic:"0.1" scope:"read-only" key:"id"`
Name string `vic:"0.1" scope:"read-only,non-persistent" key:"name"`
Notes string `vic:"0.1" scope:"read-only" key:"notes"`
}
type Type struct {
Common CommonPersistence `vic:"0.1" scope:"read-write" key:"common"`
}
Struct := Type{
Common: CommonPersistence{
ID: "0xDEADBEEF",
Name: "Struct",
},
}
encoded := map[string]string{}
filterSink := ScopeFilterSink(NonPersistent|Hidden, MapSink(encoded))
Encode(filterSink, Struct)
expected := map[string]string{
visibleRONonpersistent("common/name"): "Struct",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
// strip ID as that would be filtered out
Struct.Common.ID = ""
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}

View File

@@ -0,0 +1,537 @@
// Copyright 2016 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 (
"encoding/base64"
"fmt"
"net"
"testing"
"time"
"github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
// make it verbose during testing
func init() {
logger.Level = logrus.DebugLevel
}
func TestEmbedded(t *testing.T) {
type Type struct {
Common `vic:"0.1" scope:"read-only" key:"common"`
}
Embedded := Type{
Common: Common{
ID: "0xDEADBEEF",
Name: "Embedded",
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), Embedded)
expected := map[string]string{
visibleRO("common/id"): "0xDEADBEEF",
visibleRO("common/name"): "Embedded",
visibleRO("common/notes"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Embedded, decoded, "Encoded and decoded does not match")
}
func TestNetPointer(t *testing.T) {
type Type struct {
Net *net.IPNet `vic:"0.1" scope:"read-only" key:"net"`
}
// 127.0.0.1/8
n := net.IPNet{IP: net.IP{0x7f, 0x0, 0x0, 0x1}, Mask: net.IPMask{0xff, 0x0, 0x0, 0x0}}
Net := Type{
Net: &n,
}
encoded := map[string]string{}
Encode(MapSink(encoded), Net)
expected := map[string]string{
visibleRO("net/IP"): base64.StdEncoding.EncodeToString(n.IP),
visibleRO("net/Mask"): base64.StdEncoding.EncodeToString(n.Mask),
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Net, decoded, "Encoded and decoded does not match")
}
func TestTimePointer(t *testing.T) {
d := time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC)
type Type struct {
Time *time.Time `vic:"0.1" scope:"read-only" key:"time"`
}
Time := Type{
Time: &d,
}
encoded := map[string]string{}
Encode(MapSink(encoded), Time)
expected := map[string]string{
visibleRO("time"): "2009-11-10 23:00:00 +0000 UTC",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Time, decoded, "Encoded and decoded does not match")
}
func TestStructMap(t *testing.T) {
type Type struct {
StructMap map[string]Common `vic:"0.1" scope:"read-only" key:"map"`
}
StructMap := Type{
map[string]Common{
"Key1": {
ID: "0xDEADBEEF",
Name: "beef",
},
"Key2": {
ID: "0x8BADF00D",
Name: "food",
},
"Key3": {
ID: "0xDEADF00D",
Name: "dead",
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), StructMap)
expected := map[string]string{
visibleRO("map" + Separator + "Key1/id"): "0xDEADBEEF",
visibleRO("map" + Separator + "Key1/name"): "beef",
visibleRO("map" + Separator + "Key1/notes"): "",
visibleRO("map" + Separator + "Key2/id"): "0x8BADF00D",
visibleRO("map" + Separator + "Key2/name"): "food",
visibleRO("map" + Separator + "Key2/notes"): "",
visibleRO("map" + Separator + "Key3/id"): "0xDEADF00D",
visibleRO("map" + Separator + "Key3/name"): "dead",
visibleRO("map" + Separator + "Key3/notes"): "",
visibleRO("map"): "Key1" + Separator + "Key2" + Separator + "Key3",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, StructMap, decoded, "Encoded and decoded does not match")
}
func TestIntStructMap(t *testing.T) {
type Type struct {
StructMap map[int]Common `vic:"0.1" scope:"read-only" key:"map"`
}
StructMap := Type{
map[int]Common{
1: {
ID: "0xDEADBEEF",
Name: "beef",
},
2: {
ID: "0x8BADF00D",
Name: "food",
},
3: {
ID: "0xDEADF00D",
Name: "dead",
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), StructMap)
expected := map[string]string{
visibleRO("map" + Separator + "1/id"): "0xDEADBEEF",
visibleRO("map" + Separator + "1/name"): "beef",
visibleRO("map" + Separator + "1/notes"): "",
visibleRO("map" + Separator + "2/id"): "0x8BADF00D",
visibleRO("map" + Separator + "2/name"): "food",
visibleRO("map" + Separator + "2/notes"): "",
visibleRO("map" + Separator + "3/id"): "0xDEADF00D",
visibleRO("map" + Separator + "3/name"): "dead",
visibleRO("map" + Separator + "3/notes"): "",
visibleRO("map"): "1" + Separator + "2" + Separator + "3",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, StructMap, decoded, "Encoded and decoded does not match")
}
func TestStructSlice(t *testing.T) {
type Type struct {
StructSlice []Common `vic:"0.1" scope:"read-only" key:"slice"`
}
StructSlice := Type{
[]Common{
{
ID: "0xDEADFEED",
Name: "feed",
},
{
ID: "0xFACEFEED",
Name: "face",
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), StructSlice)
expected := map[string]string{
visibleRO("slice"): "1",
visibleRO("slice" + Separator + "0/id"): "0xDEADFEED",
visibleRO("slice" + Separator + "0/name"): "feed",
visibleRO("slice" + Separator + "0/notes"): "",
visibleRO("slice" + Separator + "1/id"): "0xFACEFEED",
visibleRO("slice" + Separator + "1/name"): "face",
visibleRO("slice" + Separator + "1/notes"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, StructSlice, decoded, "Encoded and decoded does not match")
}
func TestMultipleScope(t *testing.T) {
MultipleScope := struct {
MultipleScope string `vic:"0.1" scope:"read-only,hidden,non-persistent" key:"multiscope"`
}{
"MultipleScope",
}
encoded := map[string]string{}
Encode(MapSink(encoded), MultipleScope)
expected := map[string]string{}
assert.Equal(t, expected, encoded, "Not equal")
}
func TestUnknownScope(t *testing.T) {
UnknownScope := struct {
UnknownScope int `vic:"0.1" scope:"unknownscope" key:"unknownscope"`
}{
42,
}
encoded := map[string]string{}
Encode(MapSink(encoded), UnknownScope)
expected := map[string]string{}
assert.Equal(t, encoded, expected, "Not equal")
}
func TestUnknownProperty(t *testing.T) {
UnknownProperty := struct {
UnknownProperty int `vic:"0.1" scope:"hidden" key:"unknownproperty" recurse:"unknownproperty"`
}{
42,
}
encoded := map[string]string{}
Encode(MapSink(encoded), UnknownProperty)
expected := map[string]string{
hidden("unknownproperty"): "42",
}
assert.Equal(t, expected, encoded, "Not equal")
}
func TestOmitNested(t *testing.T) {
OmitNested := struct {
Time time.Time `vic:"0.1" scope:"volatile" key:"time" recurse:"depth=0"`
CurrentTime time.Time `vic:"0.1" scope:"volatile" key:"time"`
}{
Time: time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC),
CurrentTime: time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC),
}
encoded := map[string]string{}
Encode(MapSink(encoded), OmitNested)
expected := map[string]string{
visibleRO("time"): "2009-11-10 23:00:00 +0000 UTC",
}
assert.Equal(t, expected, encoded, "Encoded and decoded does not match")
}
func TestComplex(t *testing.T) {
type Type struct {
ExecutorConfig ExecutorConfig `vic:"0.1" scope:"hidden" key:"executorconfig"`
}
ExecutorConfig := Type{
ExecutorConfig{
Sessions: map[string]SessionConfig{
"Session1": {
Common: Common{
ID: "SessionID",
Name: "SessionName",
},
Tty: true,
Cmd: Cmd{
Path: "/vmware",
Args: []string{"/bin/imagec", "-standalone"},
Env: []string{"PATH=/bin", "USER=imagec"},
Dir: "/",
},
},
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), ExecutorConfig)
expected := map[string]string{
visibleRO("executorconfig/common/id"): "",
visibleRO("executorconfig/common/name"): "",
visibleRO("executorconfig/common/notes"): "",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/id"): "SessionID",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/name"): "SessionName",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/notes"): "",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/path"): "/vmware",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args~"): "/bin/imagec" + Separator + "-standalone",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args"): "1",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env~"): "PATH=/bin" + Separator + "USER=imagec",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env"): "1",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/dir"): "/",
hidden("executorconfig/sessions" + Separator + "Session1/tty"): "true",
hidden("executorconfig/sessions"): "Session1",
hidden("executorconfig/Key"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, ExecutorConfig, decoded, "Encoded and decoded does not match")
}
func TestComplexPointer(t *testing.T) {
type Type struct {
ExecutorConfig *ExecutorConfig `vic:"0.1" scope:"hidden" key:"executorconfig"`
}
ExecutorConfig := Type{
&ExecutorConfig{
Sessions: map[string]SessionConfig{
"Session1": {
Common: Common{
ID: "SessionID",
Name: "SessionName",
},
Tty: true,
Cmd: Cmd{
Path: "/vmware",
Args: []string{"/bin/imagec", "-standalone"},
Env: []string{"PATH=/bin", "USER=imagec"},
Dir: "/",
},
},
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), ExecutorConfig)
expected := map[string]string{
visibleRO("executorconfig/common/id"): "",
visibleRO("executorconfig/common/name"): "",
visibleRO("executorconfig/common/notes"): "",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/id"): "SessionID",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/name"): "SessionName",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/notes"): "",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/path"): "/vmware",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args~"): "/bin/imagec" + Separator + "-standalone",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args"): "1",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env~"): "PATH=/bin" + Separator + "USER=imagec",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env"): "1",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/dir"): "/",
hidden("executorconfig/sessions" + Separator + "Session1/tty"): "true",
hidden("executorconfig/sessions"): "Session1",
hidden("executorconfig/Key"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, ExecutorConfig, decoded, "Encoded and decoded does not match")
}
// TestPointerDecode tests the translation from a type where the sessions are direct values to
// one where they are pointers
func TestPointerDecode(t *testing.T) {
reference := ExecutorConfig{
Sessions: map[string]SessionConfig{
"Session1": {
Common: Common{
ID: "SessionID",
Name: "SessionName",
},
Tty: true,
Cmd: Cmd{
Path: "/vmware",
Args: []string{"/bin/imagec", "-standalone"},
Env: []string{"PATH=/bin", "USER=imagec"},
Dir: "/",
},
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), reference)
expected := map[string]string{
visibleRO("common/id"): "",
visibleRO("common/name"): "",
visibleRO("common/notes"): "",
visibleRO("sessions" + Separator + "Session1/common/id"): "SessionID",
visibleRO("sessions" + Separator + "Session1/common/name"): "SessionName",
visibleRO("sessions" + Separator + "Session1/common/notes"): "",
hidden("sessions" + Separator + "Session1/cmd/path"): "/vmware",
hidden("sessions" + Separator + "Session1/cmd/args~"): "/bin/imagec" + Separator + "-standalone",
hidden("sessions" + Separator + "Session1/cmd/args"): "1",
hidden("sessions" + Separator + "Session1/cmd/env~"): "PATH=/bin" + Separator + "USER=imagec",
hidden("sessions" + Separator + "Session1/cmd/env"): "1",
hidden("sessions" + Separator + "Session1/cmd/dir"): "/",
hidden("sessions" + Separator + "Session1/tty"): "true",
hidden("sessions"): "Session1",
hidden("Key"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded ExecutorConfigPointers
Decode(MapSource(encoded), &decoded)
// cannot assert equality at a high level because of the different structure types, but we can test the
// common structure fragments
assert.Equal(t, reference.Sessions["Session1"], *decoded.Sessions["Session1"], "Encoded and decoded sessions do not match")
}
func TestInsideOutside(t *testing.T) {
type Inside struct {
ID string `vic:"0.1" scope:"read-write" key:"id"`
Name string `vic:"0.1" scope:"read-write" key:"name"`
}
type Outside struct {
Inside Inside `vic:"0.1" scope:"read-only" key:"inside"`
ID string `vic:"0.1" scope:"read-write" key:"id"`
Name string `vic:"0.1" scope:"read-write" key:"name"`
}
outside := Outside{
Inside: Inside{
ID: "inside",
Name: "Inside",
},
ID: "outside",
Name: "Outside",
}
encoded := map[string]string{}
Encode(MapSink(encoded), outside)
expected := map[string]string{
visibleRW("inside.id"): "inside",
visibleRW("inside.name"): "Inside",
visibleRW("id"): "outside",
visibleRW("name"): "Outside",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Outside
Decode(MapSource(encoded), &decoded)
assert.Equal(t, outside, decoded, "Encoded and decoded does not match")
}
func TestIPSlice(t *testing.T) {
type Slice struct {
Slice []net.IP `vic:"0.1" scope:"read-only" key:"slice"`
}
ips := []net.IP{
net.ParseIP("10.10.10.10"),
net.ParseIP("10.10.10.1"),
}
encodedIPs := make([]string, len(ips))
for i := range ips {
Encode(func(key, value string) error {
encodedIPs[i] = value
return nil
}, ips[i])
}
s := Slice{
Slice: ips,
}
encoded := make(map[string]string)
Encode(MapSink(encoded), s)
expected := map[string]string{
visibleRO("slice"): fmt.Sprintf("%d", len(ips)-1),
}
for i := range encodedIPs {
expected[visibleRO(fmt.Sprintf("slice"+Separator+"%d", i))] = encodedIPs[i]
}
assert.Equal(t, expected, encoded, "Encoded and expected do not match")
var decoded Slice
Decode(MapSource(encoded), &decoded)
assert.Equal(t, s, decoded, "Encoded and decoded do not match")
}

View File

@@ -0,0 +1,477 @@
// Copyright 2016 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 (
"encoding/base64"
"fmt"
"reflect"
"strconv"
"strings"
"time"
)
var (
nilValue = reflect.ValueOf(nil)
)
type decoder func(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error)
var (
kindDecoders map[reflect.Kind]decoder
intfDecoders map[reflect.Type]decoder
)
func init() {
kindDecoders = map[reflect.Kind]decoder{
reflect.String: decodeString,
reflect.Struct: decodeStruct,
reflect.Slice: decodeSlice,
reflect.Array: decodeSlice,
reflect.Map: decodeMap,
reflect.Ptr: decodePtr,
reflect.Int: decodePrimitive,
reflect.Int8: decodePrimitive,
reflect.Int16: decodePrimitive,
reflect.Int32: decodePrimitive,
reflect.Int64: decodePrimitive,
reflect.Bool: decodePrimitive,
reflect.Float32: decodePrimitive,
reflect.Float64: decodePrimitive,
}
intfDecoders = map[reflect.Type]decoder{
reflect.TypeOf(time.Time{}): decodeTime,
}
}
// decode is the generic switcher that decides which decoder to use for a field
func decode(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// if depth has reached zero, we skip decoding entirely
if depth.depth == 0 {
return dest, nil
}
depth.depth--
// obtain the handler from the map, checking for the more specific interfaces first
dec, ok := intfDecoders[dest.Type()]
if ok {
return dec(src, dest, prefix, depth)
}
dec, ok = kindDecoders[dest.Kind()]
if ok {
return dec(src, dest, prefix, depth)
}
logger.Debugf("Skipping unsupported field, interface: %T, kind %s", dest, dest.Kind())
return dest, nil
}
// decodeString is the degenerative case where what we get is what we need
func decodeString(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
v, err := src(prefix)
if err != nil {
logger.Debugf("No value found in data source for string at key %q", prefix)
return nilValue, err
}
return reflect.ValueOf(v), nil
}
// decodePrimitive wraps the fromString primitive decoding in a manner that can be called via decode
func decodePrimitive(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
var this reflect.Value
if !dest.CanAddr() {
logger.Debugf("Making new primitive for %s", prefix)
ptr := reflect.New(dest.Type())
this = ptr.Elem()
} else {
logger.Debugf("Reusing existing struct for %s", prefix)
this = dest
}
// see if there's a value to decode
v, err := src(prefix)
if err != nil {
logger.Debugf("No value available for key to primitive %s", prefix)
return nilValue, err
}
t := this.Type()
this.Set(fromString(reflect.Zero(t), v))
return this, nil
}
func decodePtr(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// if we're not following pointers, then return immediately
if !depth.follow {
return dest, nil
}
// value representing the run-time data
logger.Debugf("Decoding pointer into object: %#v", dest)
// if the pointer is nil we need to create the destination type
target := dest
if dest.IsNil() {
target = reflect.New(dest.Type().Elem())
}
// check to see if the resulting object is not nil
// If it is nil, then there was nothing to decode and the pointer remains nil
result, err := decode(src, target.Elem(), prefix, depth)
logger.Debugf("target is now %#v, %+q ", target, target.Type())
if !result.IsValid() || err == ErrKeyNotFound {
// leave the pointer as nil if the result is zero type or invalid
return dest, nil
}
// neither pointer, nor zero
// NOTE: if the returned result is not addressable this can panic - that generally
// indicates an incorrect implementation of a decodeX method... those should always
// return addressable Values. See decodeByteSlice as an example - this uses make([]byte)
// rather than built in string(bytes) conversion specifically to get an addressable return
if dest.IsNil() {
dest = target
}
dest.Elem().Set(result)
return dest, nil
}
var typeType = reflect.TypeOf((*reflect.Type)(nil)).Elem()
func decodeStruct(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// value representing the run-time data
logger.Debugf("Decoding struct into object: %#v, type: %s", dest, dest.Type().Name())
var this reflect.Value
if !dest.CanAddr() {
logger.Debugf("Making new struct for %s", prefix)
ptr := reflect.New(dest.Type())
this = ptr.Elem()
} else {
logger.Debugf("Reusing existing struct for %s", prefix)
this = dest
}
// do we have any data for this struct at all
var valid bool
var err error
noKeysFound := true
// iterate through every field in the struct
for i := 0; i < this.NumField(); i++ {
field := this.Field(i)
key, fdepth := calculateKeyFromField(this.Type().Field(i), prefix, depth)
if key == "" {
// this is either a malformed key or explicitly skipped
continue
}
// Dump what we have so far
logger.Debugf("Key: %s, Kind: %s Value: %s", key, field.Kind(), field.String())
// check to see if the resulting object is not nil
// If it is nil, then there was nothing to decode
var result reflect.Value
result, err = decode(src, field, key, fdepth)
if result.IsValid() {
logger.Debugf("Setting field %s to %#v", this.Type().Field(i).Name, result)
field.Set(result)
valid = true
if err != ErrKeyNotFound {
noKeysFound = false
}
} else {
logger.Debugf("Invalid result for field %s", this.Type().Field(i).Name)
}
}
if !valid || noKeysFound {
logger.Debugf("No valid result, returning nil value")
return nilValue, err
}
logger.Debugf("Return decoded structure for %s: %#v", prefix, this)
return this, nil
}
func decodeByteSlice(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
logger.Debugf("Converting string to []byte")
base, err := src(prefix)
if err != nil {
logger.Debugf("No value found in data source for []byte %q", prefix)
return nilValue, err
}
bytes, err := base64.StdEncoding.DecodeString(base)
if err != nil {
logger.Debugf("Expected base64 encoded string for []byte %q: %s", prefix, err)
return nilValue, err
}
length := len(bytes)
// we don't even try to merge byte arrays - no idea how to get append behaviour
// correct with reflection
// use make([]byte) rather than built in string(bytes) conversion to get an addressable return value
logger.Debugf("Making new slice for %s", prefix)
this := make([]byte, length, length)
copy(this, bytes)
return reflect.ValueOf(this), nil
}
func decodeSlice(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// value representing the run-time data
logger.Debugf("Decoding struct into object: %#v", dest)
kind := dest.Type().Elem().Kind()
if kind == reflect.Uint8 {
return decodeByteSlice(src, dest, prefix, depth)
}
// do we have any data for this struct at all
length := 0
curLen := 0
// get the length of the array
len, err := src(prefix)
if err != nil || len == "" {
logger.Debugf("No value available for key %s - will create empty array if needed", prefix)
} else {
// if there's any data at all then we can assume we need to be extant
lengthValue := fromString(reflect.ValueOf(0), len)
length = int(lengthValue.Int()) + 1
}
var this reflect.Value
if !dest.IsValid() || dest.IsNil() || length > dest.Cap() {
logger.Debugf("Making new slice for %s", prefix)
this = reflect.MakeSlice(dest.Type(), length, length)
} else {
this = dest
this.SetLen(length)
curLen = this.Len()
}
// determine the key given the array type
if kind == reflect.Struct || isEncodableSliceElemType(dest.Type().Elem()) {
for i := 0; i < length; i++ {
// convert key to name|index format
key := appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
// if there's already a struct in the array at this index then we pass that as the current
// value
var cur reflect.Value
if i < curLen {
cur = this.Index(i)
} else {
cur = reflect.Zero(dest.Type().Elem())
}
var result reflect.Value
// #nosec: Errors unhandled.
result, _ = decode(src, cur, key, depth)
if result.IsValid() {
this.Index(i).Set(result)
}
continue
}
return this, nil
}
// convert key to name|index format
key := appendToPrefix(prefix, "", "~")
kval, err := src(key)
if err != nil {
logger.Debugf("No value found in data source for key %q", key)
return this, err
}
// lookup the key and split it
values := strings.Split(kval, Separator)
for i := 0; i < length; i++ {
v := values[i]
t := this.Type().Elem()
k := fromString(reflect.Zero(t), v)
// set the i'th slice item
this.Index(i).Set(k)
}
return this, nil
}
func decodeMap(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// value representing the run-time data
logger.Debugf("Decoding struct into object: %#v", dest)
// if the value is the zero type, we have to create ourselves
var this reflect.Value
if !dest.IsValid() || dest.IsNil() {
logger.Debugf("Making new maps for %s", prefix)
this = reflect.MakeMap(dest.Type())
} else {
this = dest
}
mapkeys, err := src(prefix)
if mapkeys == "" || err != nil {
logger.Debugf("No value found in data source for maps keys %q", prefix)
return this, err
}
keytype := this.Type().Key()
valtype := this.Type().Elem()
// split the list of map keys and iterate
for _, value := range strings.Split(mapkeys, Separator) {
k := fromString(reflect.Zero(keytype), value)
target := this.MapIndex(k)
if !target.IsValid() {
target = reflect.Zero(valtype)
}
key := appendToPrefix(prefix, Separator, value)
// check to see if the resulting object is not nil
// If it is nil, then there was nothing to decode and the pointer remains nil
// #nosec: Errors unhandled.
result, _ := decode(src, target, key, depth)
if result.IsValid() {
this.SetMapIndex(k, result)
}
}
return this, nil
}
func decodeTime(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
v, err := src(prefix)
if err != nil {
logger.Debugf("No value found in data source for time %q", prefix)
return nilValue, err
}
t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", v)
if err != nil {
logger.Debugf("Failed to convert value %q to time", v)
}
return reflect.ValueOf(t), nil
}
// fromString converts string representation of a basic type to basic type
func fromString(field reflect.Value, value string) reflect.Value {
// handle the zero value
// TODO: can probably handle this more efficiently with a nil pointer return
// as whatever we're populating with primitives will already have their zero
// value.
if value == "" {
return reflect.Zero(field.Type())
}
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
s, err := strconv.ParseInt(value, 10, 64)
if err != nil {
logger.Errorf("Failed to convert value %#v (%s) to int: %s", value, field.Kind(), err.Error())
return field
}
return reflect.ValueOf(s).Convert(field.Type())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
s, err := strconv.ParseUint(value, 10, 64)
if err != nil {
logger.Errorf("Failed to convert value %#v (%s) to uint: %s", value, field.Kind(), err.Error())
return field
}
return reflect.ValueOf(s).Convert(field.Type())
case reflect.Bool:
s, err := strconv.ParseBool(value)
if err != nil {
logger.Errorf("Failed to convert value %#v (%s) to bool: %s", value, field.Kind(), err.Error())
return field
}
return reflect.ValueOf(s)
case reflect.String:
return reflect.ValueOf(value)
case reflect.Float32, reflect.Float64:
s, err := strconv.ParseFloat(value, 64)
if err != nil {
logger.Errorf("Failed to convert value %#v (%s) to float: %s", value, field.Kind(), err.Error())
return field
}
return reflect.ValueOf(s)
}
logger.Debugf("Invalid Kind: %s (%#v)", field.Kind(), value)
return field
}
// DataSource provides a function that, give a key will return a value
// this is to be used during extraConfig decode to obtain values. Should
// return ErrKeyNotFound if the key does not exist in the data source.
type DataSource func(string) (string, error)
// Decode populates a destination with data from the supplied data source
func Decode(src DataSource, dest interface{}) interface{} {
if src == nil {
logger.Warnf("Decode source is nil - unable to continue")
return dest
}
// #nosec: Errors unhandled.
value, _ := decode(src, reflect.ValueOf(dest), DefaultPrefix, Unbounded)
return value.Interface()
}
// DecodeWithPrefix populates a destination with data from the supplied data source, using
// the specified prefix - this allows for decode into substructres.
func DecodeWithPrefix(src DataSource, dest interface{}, prefix string) interface{} {
if src == nil {
logger.Warnf("Decode source is nil - unable to continue")
return dest
}
// #nosec: Errors unhandled.
value, _ := decode(src, reflect.ValueOf(dest), prefix, Unbounded)
return value.Interface()
}
// MapSource takes a key/value map and uses that as the datasource for decoding into
// target structures
func MapSource(src map[string]string) DataSource {
return func(key string) (string, error) {
val, ok := src[key]
if !ok {
return "", ErrKeyNotFound
}
return val, nil
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2016 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 "errors"
// GuestInfoSource uses the rpcvmx mechanism to access the guestinfo key/value map as
// the datasource for decoding into target structures
func GuestInfoSource() (DataSource, error) {
return GuestInfoSourceWithPrefix("")
}
// GuestInfoSourceWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSourceWithPrefix(prefix string) (DataSource, error) {
return nil, errors.New("Not implemented on OSX")
}

View File

@@ -0,0 +1,63 @@
// 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"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vmw-guestinfo/rpcvmx"
"github.com/vmware/vmw-guestinfo/vmcheck"
)
// GuestInfoSource uses the rpcvmx mechanism to access the guestinfo key/value map as
// the datasource for decoding into target structures
func GuestInfoSource() (DataSource, error) {
return GuestInfoSourceWithPrefix("")
}
// GuestInfoSourceWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSourceWithPrefix(prefix string) (DataSource, error) {
// Check we're using a vcpu (which doesn't assume this is UID 0).
if !vmcheck.IsVirtualCPU() {
return nil, fmt.Errorf("not in a virtual world")
}
guestinfo := rpcvmx.NewConfig()
source := func(key string) (string, error) {
if key != GuestInfoSecretKey {
key = addPrefixToKey(DefaultGuestInfoPrefix, prefix, key)
}
value, err := guestinfo.String(key, "")
if value == "" {
err = ErrKeyNotFound
} else if value == "<nil>" {
value = ""
}
if key != GuestInfoSecretKey { // don't log the secret key
log.Debugf("GuestInfoSource: key: %s, value: %#v, error: %s", key, value, err)
}
return value, err
}
return new(SecretKey).Source(source), nil
}

View File

@@ -0,0 +1,32 @@
// Copyright 2016 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 (
"errors"
)
// GuestInfoSource uses the rpcvmx mechanism to access the guestinfo key/value map as
// the datasource for decoding into target structures
func GuestInfoSource() (DataSource, error) {
return GuestInfoSourceWithPrefix("")
}
// GuestInfoSourceWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSourceWithPrefix(prefix string) (DataSource, error) {
return nil, errors.New("Not implemented on Windows")
}

View File

@@ -0,0 +1,47 @@
// Copyright 2016 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
/*
Package extraconfig provides Encode/Decode methods to convert data between Go structs and VMware Extraconfig values.
The implementation understands the following set of annotations and map the fields to appropriate extraConfig keys - in the case where the key describes a boolean state, omitting the annotation implies the opposite:
hidden - hidden from GuestOS
read-only - value can only be modified via vSphere APIs
read-write - value can be modified
non-persistent - value will be lost on VM reboot
volatile - field is not exported directly, but via a function that freshens the value each time)
The struct fields are required to be annotated with the "vic" tag, otherwise extraconfig package simply skips them. Scope and key tags are also required.
Scope tag can contain multiple values (comma separated)
Key tag can contain extra properties (comma separated) but the first element has to the name of the key.
type Example struct {
// skipped - does not contain any tag
Note string
// skipped - does not contain scope and key
ID string `vic:"0.1"`
// valid - extraconfig will encode this using a read-only key (as instructed by scope)
Name string `vic:"0.1" scope:"read-only" key:"name"`
// valid - but extraconfig won't nest into the struct (so it's value will be type's zero value)
Time time.Time `vic:"0.1" scope:"volatile" key:"time,omitnested"`
}
*/

View File

@@ -0,0 +1,300 @@
// Copyright 2016 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 (
"encoding/base64"
"errors"
"fmt"
"net"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
)
var (
ErrKeyNotFound = errors.New("key not found")
)
type encoder func(sink DataSink, src reflect.Value, prefix string, depth recursion)
var kindEncoders map[reflect.Kind]encoder
var intfEncoders map[reflect.Type]encoder
func init() {
kindEncoders = map[reflect.Kind]encoder{
reflect.String: encodeString,
reflect.Struct: encodeStruct,
reflect.Slice: encodeSlice,
reflect.Array: encodeSlice,
reflect.Map: encodeMap,
reflect.Ptr: encodePtr,
reflect.Int: encodePrimitive,
reflect.Int8: encodePrimitive,
reflect.Int16: encodePrimitive,
reflect.Int32: encodePrimitive,
reflect.Int64: encodePrimitive,
reflect.Bool: encodePrimitive,
reflect.Float32: encodePrimitive,
reflect.Float64: encodePrimitive,
}
intfEncoders = map[reflect.Type]encoder{
reflect.TypeOf(time.Time{}): encodeTime,
}
}
// decode is the generic switcher that decides which decoder to use for a field
func encode(sink DataSink, src reflect.Value, prefix string, depth recursion) {
// if depth has reached zero, we skip encoding entirely
if depth.depth == 0 {
return
}
depth.depth--
// obtain the handler from the map, checking for the more specific interfaces first
enc, ok := intfEncoders[src.Type()]
if ok {
enc(sink, src, prefix, depth)
return
}
enc, ok = kindEncoders[src.Kind()]
if ok {
enc(sink, src, prefix, depth)
return
}
logger.Debugf("Skipping unsupported field, interface: %T, kind %s", src, src.Kind())
}
// encodeString is the degenerative case where what we get is what we need
func encodeString(sink DataSink, src reflect.Value, prefix string, depth recursion) {
err := sink(prefix, src.String())
if err != nil {
logger.Errorf("Failed to encode string for key %s: %s", prefix, err)
}
}
// encodePrimitive wraps the toString primitive encoding in a manner that can be called via encode
func encodePrimitive(sink DataSink, src reflect.Value, prefix string, depth recursion) {
err := sink(prefix, toString(src))
if err != nil {
logger.Errorf("Failed to encode primitive for key %s: %s", prefix, err)
}
}
func encodePtr(sink DataSink, src reflect.Value, prefix string, depth recursion) {
// if we're not following pointers, return immediately
if !depth.follow {
return
}
logger.Debugf("Encoding object: %#v", src)
if src.IsNil() {
// no need to attempt anything
return
}
encode(sink, src.Elem(), prefix, depth)
}
func encodeStruct(sink DataSink, src reflect.Value, prefix string, depth recursion) {
logger.Debugf("Encoding object: %#v", src)
// iterate through every field in the struct
for i := 0; i < src.NumField(); i++ {
field := src.Field(i)
key, fdepth := calculateKeyFromField(src.Type().Field(i), prefix, depth)
if key == "" {
logger.Debugf("Skipping field %s with empty computed key", src.Type().Field(i).Name)
continue
}
// Dump what we have so far
logger.Debugf("Key: %s, Kind: %s Value: %s", key, field.Kind(), field.String())
encode(sink, field, key, fdepth)
}
}
func isEncodableSliceElemType(t reflect.Type) bool {
switch t {
case reflect.TypeOf((net.IP)(nil)):
return true
}
return false
}
func encodeSlice(sink DataSink, src reflect.Value, prefix string, depth recursion) {
logger.Debugf("Encoding object: %#v", src)
length := src.Len()
if length == 0 {
logger.Debug("Skipping empty slice")
return
}
// determine the key given the array type
kind := src.Type().Elem().Kind()
if kind == reflect.Uint8 {
// special []byte array handling
logger.Debugf("Converting []byte to base64 string")
str := base64.StdEncoding.EncodeToString(src.Bytes())
encode(sink, reflect.ValueOf(str), prefix, depth)
return
} else if kind == reflect.Struct || isEncodableSliceElemType(src.Type().Elem()) {
for i := 0; i < length; i++ {
// convert key to name|index format
key := appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
encode(sink, src.Index(i), key, depth)
}
} else {
// else assume it's primitive - we'll panic/recover and continue it not
defer func() {
if err := recover(); err != nil {
logger.Errorf("unable to encode %s (slice) for %s: %s", src.Type(), prefix, err)
}
}()
values := make([]string, length)
for i := 0; i < length; i++ {
values[i] = toString(src.Index(i))
}
// convert key to name|index format
key := appendToPrefix(prefix, "", "~")
err := sink(key, strings.Join(values, Separator))
if err != nil {
logger.Errorf("Failed to encode slice data for key %s: %s", key, err)
}
}
// prefix contains the length of the array
// seems insane calling toString(ValueOf(..)) but it means we're using the same path for everything
err := sink(prefix, toString(reflect.ValueOf(length-1)))
if err != nil {
logger.Errorf("Failed to encode slice length for key %s: %s", prefix, err)
}
}
func encodeMap(sink DataSink, src reflect.Value, prefix string, depth recursion) {
logger.Debugf("Encoding object: %#v", src)
// iterate over keys and recurse
mkeys := src.MapKeys()
length := len(mkeys)
if length == 0 {
logger.Debug("Skipping empty map")
return
}
logger.Debugf("Encoding map entries based off prefix: %s", prefix)
keys := make([]string, length)
for i, v := range mkeys {
keys[i] = toString(v)
key := appendToPrefix(prefix, Separator, keys[i])
encode(sink, src.MapIndex(v), key, depth)
}
// sort the keys before joining - purely to make testing viable
sort.Strings(keys)
err := sink(prefix, strings.Join(keys, Separator))
if err != nil {
logger.Errorf("Failed to encode map keys for key %s: %s", prefix, err)
}
}
func encodeTime(sink DataSink, src reflect.Value, prefix string, depth recursion) {
err := sink(prefix, src.Interface().(time.Time).String())
if err != nil {
logger.Errorf("Failed to encode time for key %s: %s", prefix, err)
}
}
// toString converts a basic type to its string representation
func toString(field reflect.Value) string {
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(field.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(field.Uint(), 10)
case reflect.Bool:
return strconv.FormatBool(field.Bool())
case reflect.String:
return field.String()
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(field.Float(), 'E', -1, 64)
default:
panic(field.Type().String() + " is an unhandled type")
}
}
// DataSink provides a function that, given a key/value will persist that
// in some manner suited for later retrieval
type DataSink func(string, string) error
// Encode serializes the given type to the supplied data sink
func Encode(sink DataSink, src interface{}) {
encode(sink, reflect.ValueOf(src), DefaultPrefix, Unbounded)
}
// EncodeWithPrefix serializes the given type to the supplied data sink, using
// the supplied prefix - this allows for serialization of subsections of a
// struct
func EncodeWithPrefix(sink DataSink, src interface{}, prefix string) {
encode(sink, reflect.ValueOf(src), prefix, Unbounded)
}
// MapSink takes a map and populates it with key/value pairs from the encode
func MapSink(sink map[string]string) DataSink {
// this is a very basic mechanism of allowing serialized updates to a sink
// a more involved approach is necessary if wanting to do concurrent read/write
mutex := sync.Mutex{}
return func(key, value string) error {
mutex.Lock()
defer mutex.Unlock()
sink[key] = value
return nil
}
}
// ScopeFilterSink will create a DataSink that only stores entries where the key scope
// matches one or more scopes in the filter.
// The filter is a bitwise composion of scope flags
func ScopeFilterSink(filter uint, sink DataSink) DataSink {
return func(key, value string) error {
logger.Debugf("Filtering encode of %s with scopes: %v", key, calculateScopeFromKey(key))
scope := calculateScope(calculateScopeFromKey(key))
if scope&filter != 0 {
sink(key, value)
} else {
logger.Debugf("Skipping encode of %s with scopes that do not match filter: %v", key, calculateScopeFromKey(key))
}
return nil
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2016 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 "errors"
// GuestInfoSink uses the rpcvmx mechanism to update the guestinfo key/value map as
// the datasink for encoding target structures
func GuestInfoSink() (DataSink, error) {
return GuestInfoSinkWithPrefix("")
}
// GuestInfoSinkWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSinkWithPrefix(prefix string) (DataSink, error) {
return nil, errors.New("Not implemented on OSX")
}

View File

@@ -0,0 +1,63 @@
// Copyright 2016 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"
"strings"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vmw-guestinfo/rpcvmx"
"github.com/vmware/vmw-guestinfo/vmcheck"
)
// GuestInfoSink uses the rpcvmx mechanism to update the guestinfo key/value map as
// the datasink for encoding target structures
func GuestInfoSink() (DataSink, error) {
return GuestInfoSinkWithPrefix("")
}
// GuestInfoSinkWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSinkWithPrefix(prefix string) (DataSink, error) {
// Check we're using a vcpu (which doesn't assume this is UID 0).
if !vmcheck.IsVirtualCPU() {
return nil, fmt.Errorf("not in a virtual world")
}
guestinfo := rpcvmx.NewConfig()
return func(key, value string) error {
if strings.Contains(key, "/") {
// quietly skip if it's a read-only key
return nil
}
key = addPrefixToKey(DefaultGuestInfoPrefix, prefix, key)
if value == "" {
value = "<nil>"
}
log.Debugf("GuestInfoSink: setting key: %s, value: %#v", key, value)
err := guestinfo.SetString(key, value)
if err != nil {
log.Errorf("GuestInfoSink: error: %#v", err)
}
return err
}, nil
}

View File

@@ -0,0 +1,30 @@
// Copyright 2016 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 "errors"
// GuestInfoSink uses the rpcvmx mechanism to update the guestinfo key/value map as
// the datasink for encoding target structures
func GuestInfoSink() (DataSink, error) {
return GuestInfoSinkWithPrefix("")
}
// GuestInfoSinkWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSinkWithPrefix(prefix string) (DataSink, error) {
return nil, errors.New("Not implemented on Windows")
}

View File

@@ -0,0 +1,21 @@
// Copyright 2016 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 "testing"
func TestExistingBasic(t *testing.T) {
t.Skip("decode into existing target tests not yet implemented")
}

View File

@@ -0,0 +1,510 @@
// 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 (
// DefaultTagName value
DefaultTagName = "vic"
// DefaultPrefix value
DefaultPrefix = ""
// DefaultGuestInfoPrefix value
DefaultGuestInfoPrefix = "guestinfo.vice."
//Separator for slice values and map keys
Separator = "|"
// suffix separator character
suffixSeparator = "@"
// secret suffix
secretSuffix = "secret"
// non-persistent suffix
nonpersistentSuffix = "non-persistent"
)
const (
// Invalid value
Invalid = 1 << iota
// Hidden value
Hidden
// ReadOnly value
ReadOnly
// 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
}
// 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 "hidden":
scope |= Hidden
case "read-only":
scope |= ReadOnly
case "read-write":
scope |= ReadWrite
case nonpersistentSuffix:
scope |= NonPersistent
case "volatile":
scope |= Volatile
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, DefaultGuestInfoPrefix) {
scopes = append(scopes, "hidden")
}
if strings.Contains(key, "/") {
scopes = append(scopes, "read-only")
} else {
scopes = append(scopes, "read-write")
}
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("scope"), ",")
logger.Debugf("Scopes: %#v", scopes)
// get the keys and split properties from it
key = tags.Get("key")
logger.Debugf("Key specified: %s", key)
// get the keys and split properties from it
recurse := tags.Get("recurse")
if recurse != "" {
props := strings.Split(recurse, ",")
// process properties
for _, prop := range props {
// determine recursion depth
if strings.HasPrefix(prop, "depth") {
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 == "nofollow" {
fdepth.follow = false
} else if prop == "follow" {
fdepth.follow = 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, DefaultGuestInfoPrefix)
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)
}

View File

@@ -0,0 +1,269 @@
// Copyright 2016 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 (
"testing"
"strings"
"github.com/stretchr/testify/assert"
)
func visibleRO(key string) string {
return calculateKey(calculateScope([]string{"read-only"}), "", key)
}
func visibleRONonpersistent(key string) string {
return calculateKey(calculateScope([]string{"read-only", "non-persistent"}), "", key)
}
func visibleRW(key string) string {
return calculateKey(calculateScope([]string{"read-write"}), "", key)
}
func hidden(key string) string {
return calculateKey(calculateScope([]string{"hidden"}), "", key)
}
func TestHidden(t *testing.T) {
scopes := []string{"hidden"}
key := calculateKey(calculateScope(scopes), "a/b", "c")
assert.Equal(t, "a/b/c", key, "Key should remain hidden")
}
func TestHide(t *testing.T) {
scopes := []string{"hidden"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+"/a/b", "c")
assert.Equal(t, "a/b/c", key, "Key should be hidden")
}
func TestReveal(t *testing.T) {
scopes := []string{"read-only"}
key := calculateKey(calculateScope(scopes), "a/b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+"/a/b/c", key, "Key should be exposed")
}
func TestVisibleReadOnly(t *testing.T) {
scopes := []string{"read-only"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+"/a/b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+"/a/b/c", key, "Key should be remain visible and read-only")
}
func TestVisibleReadWrite(t *testing.T) {
scopes := []string{"read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c", key, "Key should be remain visible and read-write")
}
func TestTopLevelReadOnly(t *testing.T) {
scopes := []string{"read-only"}
key := calculateKey(calculateScope(scopes), "", "a")
assert.Equal(t, DefaultGuestInfoPrefix+"/a", key, "Key should be visible and read-only")
}
func TestReadOnlyToReadWrite(t *testing.T) {
scopes := []string{"read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+"/a/b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c", key, "Key should be visible and change to read-write")
}
func TestReadWriteToReadOnly(t *testing.T) {
scopes := []string{"read-only"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+"/a/b/c", key, "Key should be visible and change to read-only")
}
func TestCompoundKey(t *testing.T) {
scopes := []string{"read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a", "b/c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c", key, "Key should be visible and read-write")
}
func TestNoScopes(t *testing.T) {
scopes := []string{}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a/b", "c")
assert.Equal(t, "a/b/c", key, "Key should be completely proscriptive")
key = calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, "a.b/c", key, "Key should be hidden")
key = calculateKey(calculateScope(scopes), "a.b", "c")
assert.Equal(t, "a.b/c", key, "Key should remain hidden")
}
func TestSecret(t *testing.T) {
scopes := []string{"secret", "read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c"+suffixSeparator+secretSuffix, key, "Key should have secret suffix")
}
func TestNonpersistent(t *testing.T) {
scopes := []string{"non-persistent", "read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c"+suffixSeparator+nonpersistentSuffix, key, "Key should have non-persistent suffix")
}
func TestMultipleSuffixes(t *testing.T) {
scopes := []string{"non-persistent", "secret", "read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.True(t, strings.Contains(key, suffixSeparator+secretSuffix) && strings.Contains(key, suffixSeparator+nonpersistentSuffix), "Key should contain both secret and non-persistent suffix")
}
func TestCalculateKeys(t *testing.T) {
type AStruct struct {
I int
}
type Type struct {
ExecutorConfig ExecutorConfig `vic:"0.1" scope:"hidden" key:"executorconfig"`
Array []AStruct `vic:"0.1" scope:"read-write" key:"array"`
Ptr *AStruct `vic:"0.1" scope:"read-only" key:"ptr"`
Str string `vic:"0.1" scope:"read-only" key:"str"`
Bytes []uint8 `vic:"0.1" scope:"read-write" key:"bytes"`
}
ec := Type{
ExecutorConfig: ExecutorConfig{
Sessions: map[string]SessionConfig{
"Session1": {
Common: Common{
ID: "SessionID",
Name: "SessionName",
},
Tty: true,
Cmd: Cmd{
Path: "/vmware",
Args: []string{"/bin/imagec", "-standalone"},
Env: []string{"PATH=/bin", "USER=imagec"},
Dir: "/",
},
},
},
},
Array: []AStruct{
{I: 0},
},
Ptr: &AStruct{
I: 1,
},
Str: "foo",
Bytes: []byte{0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf},
}
var tests = []struct {
in string
out []string
}{
{
"ExecutorConfig.*",
[]string{
visibleRO("executorconfig/common"),
hidden("executorconfig/sessions"),
"executorconfig/Key",
},
},
{
"ExecutorConfig.Sessions.*",
[]string{"executorconfig/sessions" + Separator + "Session1"},
},
{
"ExecutorConfig.Sessions.Session1.Cmd.Args",
[]string{"executorconfig/sessions" + Separator + "Session1/cmd/args"},
},
{
"ExecutorConfig.Sessions.*.Cmd.Args.*",
[]string{"executorconfig/sessions" + Separator + "Session1/cmd/args~"},
},
{
"ExecutorConfig.Sessions.*.Cmd.Args.0",
[]string{"executorconfig/sessions" + Separator + "Session1/cmd/args~"},
},
{
"Array.0.I",
[]string{visibleRW("array" + Separator + "0/I")},
},
{
"Array.*",
[]string{visibleRW("array" + Separator + "0")},
},
{
"Ptr.I",
[]string{visibleRO("ptr/I")},
},
{
"Str",
[]string{visibleRO("str")},
},
{
"Bytes",
[]string{visibleRW("bytes")},
},
{
"Bytes.0",
[]string{visibleRW("bytes")},
},
{
"Bytes.*",
[]string{visibleRW("bytes")},
},
}
for _, te := range tests {
keys := CalculateKeys(ec, te.in, "")
assert.Equal(t, te.out, keys)
}
panicTests := []string{
"Array.1.I",
"Array.0.i",
"Array.f.i",
"ExecutorConfig.foo",
"foo",
"ExecutorConfig.Sessions.foo",
"Str.*",
"Str.foo",
}
for _, te := range panicTests {
assert.Panics(t, func() {
CalculateKeys(ec, te, "")
})
}
}

View File

@@ -0,0 +1,127 @@
// 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 (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
log "github.com/Sirupsen/logrus"
"golang.org/x/crypto/nacl/secretbox"
)
// The value of this key is hidden from API requests, but visible within the guest
// #nosec: Potential hardcoded credentials
const GuestInfoSecretKey = "guestinfo.ovfEnv"
// SecretKey provides helpers to encrypt/decrypt extraconfig values
type SecretKey struct {
key [32]byte
}
// NewSecretKey generates a new secret key
func NewSecretKey() (*SecretKey, error) {
s := new(SecretKey)
if _, err := rand.Read(s.key[:]); err != nil {
return nil, err
}
return s, nil
}
// FromString base64 decodes an existing SecretKey
func (s *SecretKey) FromString(key string) error {
b, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return err
}
if len(b) != 32 {
return errors.New("invalid secret key")
}
copy(s.key[:], b)
return nil
}
// String base64 encodes a SecretKey
func (s *SecretKey) String() string {
return base64.StdEncoding.EncodeToString(s.key[:])
}
// Source wraps the given DataSource, decrypting any secret values
func (s *SecretKey) Source(ds DataSource) DataSource {
// If GuestInfoSecretKey has a value, it should be our secret key.
// #nosec: Errors unhandled.
if val, _ := ds(GuestInfoSecretKey); val != "" {
if err := s.FromString(val); err != nil {
log.Errorf("failed to decode %s: %s", GuestInfoSecretKey, err)
} else {
log.Debugf("secret key decoded from %s", GuestInfoSecretKey)
}
}
return func(key string) (string, error) {
val, err := ds(key)
if err == nil && isSecret(key) {
b, err := base64.StdEncoding.DecodeString(val)
if err != nil {
return "", err
}
var nonce [24]byte
copy(nonce[:], b[:24])
plaintext, ok := secretbox.Open([]byte{}, b[24:], &nonce, &s.key)
if !ok {
return "", fmt.Errorf("failed to decrypt value for %s", key)
}
val = string(plaintext)
}
return val, err
}
}
// Sink wraps the given DataSink, encrypting any secret values
func (s *SecretKey) Sink(ds DataSink) DataSink {
// Store our secret key.
if err := ds(GuestInfoSecretKey, s.String()); err != nil {
log.Errorf("failed to store %s: %s", GuestInfoSecretKey, err)
}
return func(key, value string) error {
if isSecret(key) {
var nonce [24]byte
if _, err := rand.Read(nonce[:]); err != nil {
return err
}
ciphertext := secretbox.Seal(nonce[:], []byte(value), &nonce, &s.key)
value = base64.StdEncoding.EncodeToString(ciphertext)
}
return ds(key, value)
}
}

View File

@@ -0,0 +1,67 @@
// 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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSecretFields(t *testing.T) {
type tell struct {
Who string `vic:"0.1" scope:"secret" key:"who"`
}
type stuff struct {
Username string `vic:"0.1" scope:"read-only" key:"username"`
Password string `vic:"0.1" scope:"secret" key:"password"`
Tell tell
}
config := stuff{
Username: "root",
Password: "super-s@fe-passw0rd",
Tell: tell{"noone"},
}
out, err := NewSecretKey()
if err != nil {
t.Fatal(err)
}
encoded := map[string]string{}
Encode(out.Sink(MapSink(encoded)), config)
password := encoded["guestinfo.vice./password"+suffixSeparator+secretSuffix]
assert.NotEmpty(t, password, "encrypted password")
assert.NotEqual(t, password, config.Password, "encrypted password")
for _, expectEq := range []bool{true, false} {
var in SecretKey
var decoded stuff
Decode(in.Source(MapSource(encoded)), &decoded)
if expectEq {
assert.Equal(t, config, decoded, "Encoded and decoded does not match")
} else {
assert.NotEqual(t, config, decoded, "Encoded and decoded should not not match")
}
// second time should fail to decrypt w/o GuestInfoSecretKey
delete(encoded, GuestInfoSecretKey)
}
}

View File

@@ -0,0 +1,56 @@
// Copyright 2016 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 (
"sync"
)
// Store provides combined DataSource and DataSink.
type Store interface {
Get(string) (string, error)
Put(string, string) error
}
type MapStore struct {
mutex sync.Mutex
store map[string]string
}
func New() *MapStore {
return &MapStore{
store: make(map[string]string),
}
}
func (t *MapStore) Get(key string) (string, error) {
t.mutex.Lock()
defer t.mutex.Unlock()
val, ok := t.store[key]
if !ok {
return "", ErrKeyNotFound
}
return val, nil
}
func (t *MapStore) Put(key, value string) error {
t.mutex.Lock()
defer t.mutex.Unlock()
t.store[key] = value
return nil
}

View File

@@ -0,0 +1,55 @@
// Copyright 2016 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 vmomi
import (
"testing"
"reflect"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/vim25/types"
)
func TestDelta(t *testing.T) {
new := map[string]string{
"hello": "goodbye",
"cruel": "world",
"is": "not",
"enough": "already",
}
existing := []types.BaseOptionValue{
&types.OptionValue{Key: "hello", Value: "goodbye"},
&types.OptionValue{Key: "is", Value: "always"},
&types.OptionValue{Key: "present", Value: "regardless"},
}
updatesSlice := OptionValueUpdatesFromMap(existing, new)
expected := map[string]string{
"enough": "already", // added
"cruel": "world", // added
"is": "not", // changed
}
// turn them back into maps for equality check
updates := OptionValueMap(updatesSlice)
if !assert.True(t, reflect.DeepEqual(expected, updates), "DeepEqual says they do not match") {
t.Fatalf("Expected: %+q \nActual: %+q\n", expected, updates)
}
}

View File

@@ -0,0 +1,140 @@
// 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 vmomi is in a separate package to avoid the transitive inclusion of govmomi
// as a fundamental dependency of the main extraconfig
package vmomi
import (
"fmt"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
)
// OptionValueMap returns a map from array of OptionValues
func OptionValueMap(src []types.BaseOptionValue) map[string]string {
// create the key/value store from the extraconfig slice for lookups
kv := make(map[string]string)
for i := range src {
k := src[i].GetOptionValue().Key
v := src[i].GetOptionValue().Value.(string)
kv[k] = unescapeNil(v)
}
return kv
}
// OptionValueSource is a convenience method to generate a MapSource source from
// and array of OptionValue's
func OptionValueSource(src []types.BaseOptionValue) extraconfig.DataSource {
kv := OptionValueMap(src)
return extraconfig.MapSource(kv)
}
// OptionValueFromMap is a convenience method to convert a map into a BaseOptionValue array
// escapeNil - if true a nil string is replaced with "<nil>". Allows us to distinguish between
// deletion and nil as a value
func OptionValueFromMap(data map[string]string, escape bool) []types.BaseOptionValue {
if len(data) == 0 {
return nil
}
array := make([]types.BaseOptionValue, len(data))
i := 0
for k, v := range data {
if escape {
v = escapeNil(v)
}
array[i] = &types.OptionValue{Key: k, Value: v}
i++
}
return array
}
// OptionValueArrayToString translates the options array in to a Go formatted structure dump
func OptionValueArrayToString(options []types.BaseOptionValue) string {
// create the key/value store from the extraconfig slice for lookups
kv := make(map[string]string)
for i := range options {
k := options[i].GetOptionValue().Key
v := options[i].GetOptionValue().Value.(string)
kv[k] = v
}
return fmt.Sprintf("%#v", kv)
}
// OptionValueUpdatesFromMap generates an optionValue array for those entries in the map that do not
// already exist, are changed from the reference array, or a removed
// A removed entry will have a nil string for the value
// NOTE: DOES NOT CURRENTLY SUPPORT DELETION OF KEYS - KEYS MISSING FROM NEW MAP ARE IGNORED
func OptionValueUpdatesFromMap(existing []types.BaseOptionValue, new map[string]string) []types.BaseOptionValue {
e := len(existing)
if e == 0 {
return OptionValueFromMap(new, true)
}
n := len(new)
updates := make(map[string]string, n+e)
unchanged := make(map[string]struct{}, n+e)
// first the existing keys
for i := range existing {
v := existing[i].GetOptionValue()
if nV, ok := new[v.Key]; ok && nV == v.Value.(string) {
unchanged[v.Key] = struct{}{}
// no change
continue
} else if ok {
// changed
updates[v.Key] = escapeNil(nV)
} else {
// deletion
// NOTE: ignored as this also deletes non VIC entries currently
// there's no prefix for the non-guestinfo keys so cannot easily filter
// updates[v.Key] = ""
}
}
// now the new keys
for k, v := range new {
if _, ok := unchanged[k]; ok {
continue
}
if _, ok := updates[k]; !ok {
updates[k] = escapeNil(v)
}
}
return OptionValueFromMap(updates, false)
}
func escapeNil(input string) string {
if input == "" {
return "<nil>"
}
return input
}
func unescapeNil(input string) string {
if input == "<nil>" {
return ""
}
return input
}