diff --git a/README.md b/README.md index aa6bda16e..ead516d7d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ InfraKit ======== -[![Circle -CI](https://circleci.com/gh/docker/infrakit.png?style=shield&circle-token=50d2063f283f98b7d94746416c979af3102275b5)](https://circleci.com/gh/docker/infrakit) +[![Circle CI](https://circleci.com/gh/docker/infrakit.png?style=shield&circle-token=50d2063f283f98b7d94746416c979af3102275b5)](https://circleci.com/gh/docker/infrakit) +[![Go Report Card](https://goreportcard.com/badge/github.com/docker/infrakit)](https://goreportcard.com/report/github.com/docker/infrakit) [![codecov.io](https://codecov.io/github/docker/infrakit/coverage.svg?branch=master&token=z08ZKeIJfA)](https://codecov.io/github/docker/infrakit?branch=master) _InfraKit_ is a toolkit for creating and managing declarative, self-healing infrastructure. diff --git a/cmd/cli/plugin.go b/cmd/cli/plugin.go index bc3e3a30c..fc5f9da70 100644 --- a/cmd/cli/plugin.go +++ b/cmd/cli/plugin.go @@ -9,6 +9,7 @@ import ( "github.com/docker/infrakit/pkg/launch/os" "github.com/docker/infrakit/pkg/plugin" "github.com/docker/infrakit/pkg/template" + "github.com/docker/infrakit/pkg/types" "github.com/spf13/cobra" ) @@ -63,10 +64,10 @@ func pluginCommand(plugins func() discovery.Plugins) *cobra.Command { return err } - configs := launch.Config([]byte(view)) + configs := types.AnyString(view) parsedRules := []launch.Rule{} - err = configs.Unmarshal(&parsedRules) + err = configs.Decode(&parsedRules) if err != nil { return err } @@ -107,11 +108,11 @@ func pluginCommand(plugins func() discovery.Plugins) *cobra.Command { name := pluginToStart ch <- launch.StartPlugin{ Plugin: plugin.Name(name), - Started: func(config *launch.Config) { + Started: func(config *types.Any) { fmt.Println(name, "started.") wait.Done() }, - Error: func(config *launch.Config, err error) { + Error: func(config *types.Any, err error) { fmt.Println("Error starting", name, "err=", err) wait.Done() }, diff --git a/docs/plugins/group.md b/docs/plugins/group.md index b0b81b6dc..4c7352dc4 100644 --- a/docs/plugins/group.md +++ b/docs/plugins/group.md @@ -1,6 +1,6 @@ # Group plugin API - + ## API diff --git a/pkg/discovery/dir_test.go b/pkg/discovery/dir_test.go index 6ac7ddc5b..f857dbf40 100644 --- a/pkg/discovery/dir_test.go +++ b/pkg/discovery/dir_test.go @@ -68,10 +68,10 @@ func TestDirDiscovery(t *testing.T) { blockWhileFileExists(path2) - p, err = discover.Find(plugin.Name(name1)) + _, err = discover.Find(plugin.Name(name1)) require.Error(t, err) - p, err = discover.Find(plugin.Name(name2)) + _, err = discover.Find(plugin.Name(name2)) require.Error(t, err) list, err := discover.List() diff --git a/pkg/example/flavor/combo/flavor.go b/pkg/example/flavor/combo/flavor.go index c2ac9f2c6..65ba8417a 100644 --- a/pkg/example/flavor/combo/flavor.go +++ b/pkg/example/flavor/combo/flavor.go @@ -154,12 +154,7 @@ func (f flavorCombo) Prepare( return inst, err } - var props json.RawMessage - if pluginSpec.Properties != nil { - props = *pluginSpec.Properties - } - - flavorOutput, err := plugin.Prepare(props, clone, allocation) + flavorOutput, err := plugin.Prepare(types.RawMessage(pluginSpec.Properties), clone, allocation) if err != nil { return inst, err } diff --git a/pkg/launch/config.go b/pkg/launch/config.go deleted file mode 100644 index dd562c445..000000000 --- a/pkg/launch/config.go +++ /dev/null @@ -1,45 +0,0 @@ -package launch - -import ( - "encoding/json" -) - -// Config is the raw configuration on how to launch the plugin. -type Config json.RawMessage - -// Unmarshal decodes the raw config container in this object to the typed struct. -func (c *Config) Unmarshal(typed interface{}) error { - if c == nil || len([]byte(*c)) == 0 { - return nil // no effect on typed - } - return json.Unmarshal([]byte(*c), typed) -} - -// Marshal populates this raw message with a decoded form of the input struct. -func (c *Config) Marshal(typed interface{}) error { - buff, err := json.MarshalIndent(typed, "", " ") - if err != nil { - return err - } - *c = Config(json.RawMessage(buff)) - return nil -} - -// String returns the string representation. -func (c *Config) String() string { - return string([]byte(*c)) -} - -// MarshalJSON implements the json Marshaler interface -func (c *Config) MarshalJSON() ([]byte, error) { - if c == nil { - return nil, nil - } - return []byte(*c), nil -} - -// UnmarshalJSON implements the json Unmarshaler interface -func (c *Config) UnmarshalJSON(data []byte) error { - *c = Config(json.RawMessage(data)) - return nil -} diff --git a/pkg/launch/exec.go b/pkg/launch/exec.go index f46250d0c..8ead4951f 100644 --- a/pkg/launch/exec.go +++ b/pkg/launch/exec.go @@ -1,5 +1,9 @@ package launch +import ( + "github.com/docker/infrakit/pkg/types" +) + // Exec is a service that is able to start plugins based on different // mechanisms from running local binary to pulling and running docker containers or engine plugins type Exec interface { @@ -14,5 +18,5 @@ type Exec interface { // status of the plugin. // The client can receive and block on the returned channel // and add optional timeout in its own select statement. - Exec(name string, config *Config) (<-chan error, error) + Exec(name string, config *types.Any) (<-chan error, error) } diff --git a/pkg/launch/monitor.go b/pkg/launch/monitor.go index 913795ee8..f7cc9334e 100644 --- a/pkg/launch/monitor.go +++ b/pkg/launch/monitor.go @@ -6,16 +6,17 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/infrakit/pkg/plugin" + "github.com/docker/infrakit/pkg/types" ) -var errNoConfig = errors.New("no-counfig") +var errNoConfig = errors.New("no-config") // ExecRule encapsulates what's required to exec a plugin type ExecRule struct { // Exec is the name of the exec to use to start the plugin Exec string // Properties is the properties for the executor - Properties *Config + Properties *types.Any } // Rule provides the instructions on starting the plugin @@ -58,17 +59,17 @@ func NewMonitor(l Exec, rules []Rule) *Monitor { // StartPlugin is the command to start a plugin type StartPlugin struct { Plugin plugin.Name - Started func(*Config) - Error func(*Config, error) + Started func(*types.Any) + Error func(*types.Any, error) } -func (s StartPlugin) reportError(config *Config, e error) { +func (s StartPlugin) reportError(config *types.Any, e error) { if s.Error != nil { go s.Error(config, e) } } -func (s StartPlugin) reportSuccess(config *Config) { +func (s StartPlugin) reportSuccess(config *types.Any) { if s.Started != nil { go s.Started(config) } @@ -111,7 +112,7 @@ func (m *Monitor) Start() (chan<- StartPlugin, error) { continue loop } - configCopy := &Config{} + configCopy := types.AnyBytes(nil) if r.Launch.Properties != nil { *configCopy = *r.Launch.Properties } diff --git a/pkg/launch/monitor_test.go b/pkg/launch/monitor_test.go index 65d295b9b..de05f0f02 100644 --- a/pkg/launch/monitor_test.go +++ b/pkg/launch/monitor_test.go @@ -3,6 +3,7 @@ package launch import ( "testing" + "github.com/docker/infrakit/pkg/types" "github.com/stretchr/testify/require" ) @@ -14,16 +15,16 @@ type testConfig struct { type testLauncher struct { name string t *testing.T - callback func(*Config) + callback func(*types.Any) } func (l *testLauncher) Name() string { return l.name } -func (l *testLauncher) Exec(name string, config *Config) (<-chan error, error) { +func (l *testLauncher) Exec(name string, config *types.Any) (<-chan error, error) { rule := testConfig{} - err := config.Unmarshal(&rule) + err := config.Decode(&rule) if err != nil { return nil, err } @@ -47,7 +48,7 @@ func TestMonitorLoopNoRules(t *testing.T) { input <- StartPlugin{ Plugin: "test", - Error: func(config *Config, e error) { + Error: func(config *types.Any, e error) { errChan <- e }, } @@ -60,28 +61,23 @@ func TestMonitorLoopNoRules(t *testing.T) { func TestMonitorLoopValidRule(t *testing.T) { - raw := &Config{} config := &testConfig{ Cmd: "hello", Args: []string{"world", "hello"}, } - rawErr := raw.Marshal(config) - require.NoError(t, rawErr) - require.True(t, len([]byte(*raw)) > 0) - - var receivedArgs *Config + var receivedArgs *types.Any rule := Rule{ Plugin: "hello", Launch: ExecRule{ Exec: "test", - Properties: raw, + Properties: types.AnyValueMust(config), }, } monitor := NewMonitor(&testLauncher{ name: "test", t: t, - callback: func(c *Config) { + callback: func(c *types.Any) { receivedArgs = c }, }, []Rule{rule}) @@ -93,45 +89,38 @@ func TestMonitorLoopValidRule(t *testing.T) { started := make(chan interface{}) input <- StartPlugin{ Plugin: "hello", - Started: func(config *Config) { + Started: func(config *types.Any) { close(started) }, } <-started - expected := &Config{} - err = expected.Marshal(config) - require.NoError(t, err) - + expected := types.AnyValueMust(config) require.Equal(t, *expected, *receivedArgs) monitor.Stop() } func TestMonitorLoopRuleLookupBehavior(t *testing.T) { - raw := &Config{} + config := &testConfig{ Cmd: "hello", Args: []string{"world", "hello"}, } - rawErr := raw.Marshal(config) - require.NoError(t, rawErr) - require.True(t, len([]byte(*raw)) > 0) - - var receivedArgs *Config + var receivedArgs *types.Any rule := Rule{ Plugin: "hello", Launch: ExecRule{ Exec: "test", - Properties: raw, + Properties: types.AnyValueMust(config), }, } monitor := NewMonitor(&testLauncher{ name: "test", t: t, - callback: func(c *Config) { + callback: func(c *types.Any) { receivedArgs = c }, }, []Rule{rule}) @@ -143,17 +132,14 @@ func TestMonitorLoopRuleLookupBehavior(t *testing.T) { started := make(chan interface{}) input <- StartPlugin{ Plugin: "hello", - Started: func(config *Config) { + Started: func(config *types.Any) { close(started) }, } <-started - expected := &Config{} - err = expected.Marshal(config) - require.NoError(t, err) - + expected := types.AnyValueMust(config) require.Equal(t, *expected, *receivedArgs) monitor.Stop() diff --git a/pkg/launch/noop.go b/pkg/launch/noop.go index 48349f7d2..5e171e579 100644 --- a/pkg/launch/noop.go +++ b/pkg/launch/noop.go @@ -2,6 +2,7 @@ package launch import ( log "github.com/Sirupsen/logrus" + "github.com/docker/infrakit/pkg/types" ) type noOp int @@ -17,7 +18,7 @@ func (n noOp) Name() string { } // Launch starts the plugin given the name -func (n noOp) Exec(name string, config *Config) (<-chan error, error) { +func (n noOp) Exec(name string, config *types.Any) (<-chan error, error) { log.Infoln("NO-OP Exec: not automatically starting plugin", name, "args=", config) starting := make(chan error) diff --git a/pkg/launch/os/os.go b/pkg/launch/os/os.go index 5b0ad60e3..2ab5adc02 100644 --- a/pkg/launch/os/os.go +++ b/pkg/launch/os/os.go @@ -3,7 +3,7 @@ package os import ( "sync" - "github.com/docker/infrakit/pkg/launch" + "github.com/docker/infrakit/pkg/types" ) // LaunchConfig is the rule for how to start up a os process. @@ -45,10 +45,10 @@ func (l *Launcher) Name() string { // The command is run in the background / asynchronously. The returned read channel // stops blocking as soon as the command completes (which uses shell to run the real task in // background). -func (l *Launcher) Exec(name string, config *launch.Config) (<-chan error, error) { +func (l *Launcher) Exec(name string, config *types.Any) (<-chan error, error) { launchConfig := &LaunchConfig{} - if err := config.Unmarshal(launchConfig); err != nil { + if err := config.Decode(launchConfig); err != nil { return nil, err } diff --git a/pkg/launch/os/os_test.go b/pkg/launch/os/os_test.go index 061c2e2d1..f56a8a97c 100644 --- a/pkg/launch/os/os_test.go +++ b/pkg/launch/os/os_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/docker/infrakit/pkg/launch" + "github.com/docker/infrakit/pkg/types" "github.com/stretchr/testify/require" ) @@ -17,13 +17,9 @@ func TestLaunchOSCommand(t *testing.T) { launcher, err := NewLauncher() require.NoError(t, err) - raw := &launch.Config{} - - err = raw.Marshal(&LaunchConfig{ + starting, err := launcher.Exec("sleepPlugin", types.AnyValueMust(&LaunchConfig{ Cmd: "sleep 100", - }) - require.NoError(t, err) - starting, err := launcher.Exec("sleepPlugin", raw) + })) require.NoError(t, err) <-starting @@ -37,14 +33,10 @@ func TestLaunchWithLog(t *testing.T) { launcher, err := NewLauncher() require.NoError(t, err) - raw := &launch.Config{} - - err = raw.Marshal(&LaunchConfig{ + starting, err := launcher.Exec("echoPlugin", types.AnyValueMust(&LaunchConfig{ Cmd: fmt.Sprintf("echo hello > %s 2>&1", logfile), SamePgID: true, - }) - require.NoError(t, err) - starting, err := launcher.Exec("echoPlugin", raw) + })) require.NoError(t, err) <-starting diff --git a/pkg/manager/manager_test.go b/pkg/manager/manager_test.go index f61d576fd..588fe0e36 100644 --- a/pkg/manager/manager_test.go +++ b/pkg/manager/manager_test.go @@ -15,6 +15,7 @@ import ( group_rpc "github.com/docker/infrakit/pkg/rpc/group" "github.com/docker/infrakit/pkg/rpc/server" "github.com/docker/infrakit/pkg/spi/group" + "github.com/docker/infrakit/pkg/types" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) @@ -94,10 +95,9 @@ func testDiscoveryDir(t *testing.T) string { } func testBuildGroupSpec(groupID, properties string) group.Spec { - raw := json.RawMessage([]byte(properties)) return group.Spec{ ID: group.ID(groupID), - Properties: &raw, + Properties: types.AnyString(properties), } } @@ -115,9 +115,9 @@ func testBuildGlobalSpec(t *testing.T, gs group.Spec) GlobalSpec { } } -func testToStruct(m *json.RawMessage) interface{} { +func testToStruct(m *types.Any) interface{} { o := map[string]interface{}{} - json.Unmarshal([]byte(*m), &o) + m.Decode(&o) return &o } diff --git a/pkg/plugin/group/integration_test.go b/pkg/plugin/group/integration_test.go index 0da8776f0..2d025fa52 100644 --- a/pkg/plugin/group/integration_test.go +++ b/pkg/plugin/group/integration_test.go @@ -9,10 +9,11 @@ import ( "time" plugin_base "github.com/docker/infrakit/pkg/plugin" - "github.com/docker/infrakit/pkg/plugin/group/types" + group_types "github.com/docker/infrakit/pkg/plugin/group/types" "github.com/docker/infrakit/pkg/spi/flavor" "github.com/docker/infrakit/pkg/spi/group" "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/types" "github.com/stretchr/testify/require" ) @@ -39,8 +40,8 @@ func flavorPluginLookup(_ plugin_base.Name) (flavor.Plugin, error) { return &testFlavor{}, nil } -func minionProperties(instances int, instanceData string, flavorInit string) *json.RawMessage { - r := json.RawMessage(fmt.Sprintf(`{ +func minionProperties(instances int, instanceData string, flavorInit string) *types.Any { + return types.AnyString(fmt.Sprintf(`{ "Allocation": { "Size": %d }, @@ -58,16 +59,15 @@ func minionProperties(instances int, instanceData string, flavorInit string) *js } } }`, instances, instanceData, flavorInit)) - return &r } -func leaderProperties(logicalIDs []instance.LogicalID, data string) *json.RawMessage { +func leaderProperties(logicalIDs []instance.LogicalID, data string) *types.Any { idsValue, err := json.Marshal(logicalIDs) if err != nil { panic(err) } - r := json.RawMessage(fmt.Sprintf(`{ + return types.AnyString(fmt.Sprintf(`{ "Allocation": { "LogicalIDs": %s }, @@ -84,7 +84,6 @@ func leaderProperties(logicalIDs []instance.LogicalID, data string) *json.RawMes } } }`, idsValue, data)) - return &r } func pluginLookup(pluginName string, plugin instance.Plugin) InstancePluginLookup { @@ -112,7 +111,7 @@ func memberTags(id group.ID) map[string]string { func provisionTags(config group.Spec) map[string]string { tags := memberTags(config.ID) - tags[configTag] = types.MustParse(types.ParseProperties(config)).InstanceHash() + tags[configTag] = group_types.MustParse(group_types.ParseProperties(config)).InstanceHash() return tags } @@ -437,7 +436,7 @@ func TestInstanceAndFlavorChange(t *testing.T) { require.Equal(t, "updated init", inst.Init) properties := map[string]string{} - err = json.Unmarshal(types.RawMessage(inst.Properties), &properties) + err = types.AnyBytes([]byte(*inst.Properties)).Decode(&properties) require.NoError(t, err) require.Equal(t, "data2", properties["OpaqueValue"]) } diff --git a/pkg/plugin/group/scaled.go b/pkg/plugin/group/scaled.go index 4b228899d..368196cfc 100644 --- a/pkg/plugin/group/scaled.go +++ b/pkg/plugin/group/scaled.go @@ -61,7 +61,7 @@ func (s *scaledGroup) CreateOne(logicalID *instance.LogicalID) { spec := instance.Spec{ Tags: tags, LogicalID: logicalID, - Properties: settings.config.Instance.Properties, + Properties: types.RawMessagePtr(settings.config.Instance.Properties), } spec, err := settings.flavorPlugin.Prepare( diff --git a/pkg/plugin/group/types/types.go b/pkg/plugin/group/types/types.go index 35f72f758..1997dea08 100644 --- a/pkg/plugin/group/types/types.go +++ b/pkg/plugin/group/types/types.go @@ -9,6 +9,7 @@ import ( "github.com/docker/infrakit/pkg/plugin" "github.com/docker/infrakit/pkg/spi/group" "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/types" ) // Spec is the configuration schema for the plugin, provided in group.Spec.Properties @@ -27,20 +28,22 @@ type AllocationMethod struct { // InstancePlugin is the structure that describes an instance plugin. type InstancePlugin struct { Plugin plugin.Name - Properties *json.RawMessage // this will be the Spec of the plugin + Properties *types.Any // this will be the Spec of the plugin } // FlavorPlugin describes the flavor configuration type FlavorPlugin struct { Plugin plugin.Name - Properties *json.RawMessage // this will be the Spec of the plugin + Properties *types.Any // this will be the Spec of the plugin } // ParseProperties parses the group plugin properties JSON document in a group configuration. func ParseProperties(config group.Spec) (Spec, error) { parsed := Spec{} - if err := json.Unmarshal([]byte(RawMessage(config.Properties)), &parsed); err != nil { - return parsed, fmt.Errorf("Invalid properties: %s", err) + if config.Properties != nil { + if err := config.Properties.Decode(&parsed); err != nil { + return parsed, fmt.Errorf("Invalid properties: %s", err) + } } return parsed, nil } @@ -48,12 +51,11 @@ func ParseProperties(config group.Spec) (Spec, error) { // UnparseProperties composes group.spec from id and props func UnparseProperties(id string, props Spec) (group.Spec, error) { unparsed := group.Spec{ID: group.ID(id)} - b, err := json.Marshal(props) + any, err := types.AnyValue(props) if err != nil { - return unparsed, fmt.Errorf("Invalid properties: %s", err) + return unparsed, err } - rawMessage := json.RawMessage(b) - unparsed.Properties = &rawMessage + unparsed.Properties = any return unparsed, nil } @@ -103,9 +105,18 @@ func (c Spec) InstanceHash() string { // an empty raw message. This is useful for structs where fields are json.RawMessage pointers for bi-directional // marshal and unmarshal (value receivers will encode base64 instead of raw json when marshaled), so bi-directional // structs should use pointer fields. -func RawMessage(r *json.RawMessage) (raw json.RawMessage) { +func RawMessage(r *types.Any) (raw json.RawMessage) { if r != nil { - raw = *r + raw = json.RawMessage(r.Bytes()) } return } + +// RawMessagePtr makes a copy of the Any and make it available as a pointer to a JSON raw message +func RawMessagePtr(r *types.Any) *json.RawMessage { + if r == nil { + return nil + } + raw := json.RawMessage(r.Bytes()) + return &raw +} diff --git a/pkg/rpc/group/rpc_test.go b/pkg/rpc/group/rpc_test.go index 05438861d..7a4553601 100644 --- a/pkg/rpc/group/rpc_test.go +++ b/pkg/rpc/group/rpc_test.go @@ -1,13 +1,13 @@ package group import ( - "encoding/json" "errors" "testing" rpc_server "github.com/docker/infrakit/pkg/rpc/server" "github.com/docker/infrakit/pkg/spi/group" "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/types" "github.com/stretchr/testify/require" "io/ioutil" "path" @@ -49,11 +49,10 @@ func tempSocket() string { func TestGroupPluginCommitGroup(t *testing.T) { socketPath := tempSocket() - raw := json.RawMessage([]byte(`{"foo":"bar"}`)) groupSpecActual := make(chan group.Spec, 1) groupSpec := group.Spec{ ID: group.ID("group"), - Properties: &raw, + Properties: types.AnyString(`{"foo":"bar"}`), } server, err := rpc_server.StartPluginAtPath(socketPath, PluginServer(&testPlugin{ @@ -76,11 +75,10 @@ func TestGroupPluginCommitGroup(t *testing.T) { func TestGroupPluginCommitGroupError(t *testing.T) { socketPath := tempSocket() - raw := json.RawMessage([]byte(`{"foo":"bar"}`)) groupSpecActual := make(chan group.Spec, 1) groupSpec := group.Spec{ ID: group.ID("group"), - Properties: &raw, + Properties: types.AnyString(`{"foo":"bar"}`), } server, err := rpc_server.StartPluginAtPath(socketPath, PluginServer(&testPlugin{ diff --git a/pkg/spi/group/spi.go b/pkg/spi/group/spi.go index 4bc86b2ae..98638b90e 100644 --- a/pkg/spi/group/spi.go +++ b/pkg/spi/group/spi.go @@ -1,9 +1,9 @@ package group import ( - "encoding/json" "github.com/docker/infrakit/pkg/spi" "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/types" ) // InterfaceSpec is the current name and version of the Group API. @@ -35,10 +35,10 @@ type Spec struct { ID ID // Properties is the configuration for the group. - // The schema for the raw JSON can be found as the *.Spec of the plugin used. + // The schema for the raw Any can be found as the *.Spec of the plugin used. // For instance, if the default group plugin is used, the value here will be - // a JSON representation of github.com/docker/infrakit/plugin/group/types.Spec - Properties *json.RawMessage + // an Any / encoded representation of github.com/docker/infrakit/plugin/group/types.Spec + Properties *types.Any } // Description is a placeholder for the reported state of a Group. diff --git a/pkg/types/any.go b/pkg/types/any.go new file mode 100644 index 000000000..d5e87decb --- /dev/null +++ b/pkg/types/any.go @@ -0,0 +1,84 @@ +package types + +import ( + "encoding/json" +) + +// Any is the raw configuration for the plugin +type Any json.RawMessage + +// AnyString returns an Any from a string that represents the marshaled/encoded data +func AnyString(s string) *Any { + return AnyBytes([]byte(s)) +} + +// AnyBytes returns an Any from the encoded message bytes +func AnyBytes(data []byte) *Any { + any := &Any{} + *any = data + return any +} + +// AnyValue returns an Any from a value by marshaling / encoding the input +func AnyValue(v interface{}) (*Any, error) { + if v == nil { + return nil, nil // So that any omitempty will see an empty/zero value + } + any := &Any{} + err := any.marshal(v) + return any, err +} + +// AnyValueMust returns an Any from a value by marshaling / encoding the input. It panics if there's error. +func AnyValueMust(v interface{}) *Any { + any, err := AnyValue(v) + if err != nil { + panic(err) + } + return any +} + +// Decode decodes the any into the input typed struct +func (c *Any) Decode(typed interface{}) error { + if c == nil || len([]byte(*c)) == 0 { + return nil // no effect on typed + } + return json.Unmarshal([]byte(*c), typed) +} + +// marshal populates this raw message with a decoded form of the input struct. +func (c *Any) marshal(typed interface{}) error { + buff, err := json.MarshalIndent(typed, "", " ") + if err != nil { + return err + } + *c = Any(json.RawMessage(buff)) + return nil +} + +// Bytes returns the encoded bytes +func (c *Any) Bytes() []byte { + if c == nil { + return nil + } + return []byte(*c) +} + +// String returns the string representation. +func (c *Any) String() string { + return string([]byte(*c)) +} + +// MarshalJSON implements the json Marshaler interface +func (c *Any) MarshalJSON() ([]byte, error) { + if c == nil { + return nil, nil + } + return []byte(*c), nil +} + +// UnmarshalJSON implements the json Unmarshaler interface +func (c *Any) UnmarshalJSON(data []byte) error { + *c = Any(json.RawMessage(data)) + return nil +} diff --git a/pkg/types/any_test.go b/pkg/types/any_test.go new file mode 100644 index 000000000..cb3a6b0e1 --- /dev/null +++ b/pkg/types/any_test.go @@ -0,0 +1,80 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +type testSpec2 struct { + Properties *Any `json:"properties,omitempty"` +} + +type testSpec struct { + Properties *Any `json:"properties,omitempty"` + Nested testSpec2 `json:"nested"` +} + +type testCantMarshal struct { + Private func() error +} + +func TestMarshalUnmarshalAny(t *testing.T) { + + config := AnyBytes([]byte(`{"name":"config"}`)) + configCopy := AnyString(`{"name":"config"}`) + require.Equal(t, config.String(), configCopy.String()) + + config1, err := AnyValue(map[string]interface{}{"name": "config1"}) + require.NoError(t, err) + config2, err := AnyValue(map[string]interface{}{"name": "config2"}) + require.NoError(t, err) + + spec := testSpec{ + Properties: config1, + Nested: testSpec2{ + Properties: config2, + }, + } + + any, err := AnyValue(spec) + require.NoError(t, err) + + // now take the encoded buffer and use Any to parse it into a typed struct + parsedSpec := testSpec{} + any2 := AnyBytes(any.Bytes()) + err = any2.Decode(&parsedSpec) + require.NoError(t, err) + require.Equal(t, any, any2) + + buff1, err := json.MarshalIndent(spec, "", " ") + require.NoError(t, err) + buff2, err := json.MarshalIndent(parsedSpec, "", " ") + require.NoError(t, err) + require.Equal(t, buff1, buff2) + + caughtErr := make(chan interface{}, 1) + var notHere chan interface{} + func() { + defer func() { + if r := recover(); r != nil { + caughtErr <- r + } + }() + spec = testSpec{ + Properties: AnyValueMust(testCantMarshal{Private: func() error { return nil }}), + Nested: testSpec2{ + Properties: AnyValueMust(nil), + }, + } + + notHere <- 1 // don't expect to come here; here will make this test hang because writing to nil channel blocks + + any3 := AnyValueMust(spec) + t.Log(any3.String()) + }() + + <-caughtErr // will be stuck fi we didn't get a value + +}