From 4209bd4888c2e0d7806898975761cbf8866b273d Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 14 Jan 2015 19:29:13 -0800 Subject: [PATCH] Add add a utility for merging JSON fragments, and use it in run-container. --- docs/kubectl.md | 3 +- pkg/kubectl/cmd/helpers.go | 38 +++++++++++ pkg/kubectl/cmd/helpers_test.go | 108 ++++++++++++++++++++++++++++++++ pkg/kubectl/cmd/run.go | 8 ++- 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 pkg/kubectl/cmd/helpers_test.go diff --git a/docs/kubectl.md b/docs/kubectl.md index fb3978e6a2b..7042f3a00b6 100644 --- a/docs/kubectl.md +++ b/docs/kubectl.md @@ -885,7 +885,7 @@ Examples: Usage: ``` - kubectl run-container --image= [--replicas=replicas] [--dry-run=] [flags] + kubectl run-container --image= [--replicas=replicas] [--dry-run=] [--overrides=] [flags] Available Flags: --alsologtostderr=false: log to standard error as well as files @@ -913,6 +913,7 @@ Usage: --ns-path="/home/username/.kubernetes_ns": Path to the namespace info file that holds the namespace context to use for CLI requests. -o, --output="": Output format: json|yaml|template|templatefile --output-version="": Output the formatted object with the given version (default api-version) + --overrides="": An inline JSON override for the generated object. If this is non-empty, it is parsed used to override the generated object -r, --replicas=1: Number of replicas to create for this container. Default 1 -s, --server="": The address of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr diff --git a/pkg/kubectl/cmd/helpers.go b/pkg/kubectl/cmd/helpers.go index 942c95e33e8..8827cc6250b 100644 --- a/pkg/kubectl/cmd/helpers.go +++ b/pkg/kubectl/cmd/helpers.go @@ -17,6 +17,7 @@ limitations under the License. package cmd import ( + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -26,7 +27,10 @@ import ( "strings" "time" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/golang/glog" + "github.com/imdario/mergo" "github.com/spf13/cobra" ) @@ -171,3 +175,37 @@ func ReadConfigDataFromLocation(location string) ([]byte, error) { return data, nil } } + +func Merge(dst runtime.Object, fragment, kind string) error { + // Ok, this is a little hairy, we'd rather not force the user to specify a kind for their JSON + // So we pull it into a map, add the Kind field, and then reserialize. + // We also pull the apiVersion for proper parsing + var intermediate interface{} + if err := json.Unmarshal([]byte(fragment), &intermediate); err != nil { + return err + } + dataMap, ok := intermediate.(map[string]interface{}) + if !ok { + return fmt.Errorf("Expected a map, found something else: %s", fragment) + } + version, found := dataMap["apiVersion"] + if !found { + return fmt.Errorf("Inline JSON requires an apiVersion field") + } + versionString, ok := version.(string) + if !ok { + return fmt.Errorf("apiVersion must be a string") + } + codec := runtime.CodecFor(api.Scheme, versionString) + + intermediate.(map[string]interface{})["kind"] = kind + data, err := json.Marshal(intermediate) + if err != nil { + return err + } + src, err := codec.Decode(data) + if err != nil { + return err + } + return mergo.Merge(dst, src) +} diff --git a/pkg/kubectl/cmd/helpers_test.go b/pkg/kubectl/cmd/helpers_test.go new file mode 100644 index 00000000000..57c229539ea --- /dev/null +++ b/pkg/kubectl/cmd/helpers_test.go @@ -0,0 +1,108 @@ +/* +Copyright 2014 Google 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 cmd + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func TestMerge(t *testing.T) { + tests := []struct { + obj runtime.Object + fragment string + expected runtime.Object + expectErr bool + }{ + { + obj: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + }, + fragment: "{ \"apiVersion\": \"v1beta1\" }", + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + }, + }, + { + obj: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + }, + fragment: "{ \"apiVersion\": \"v1beta1\", \"id\": \"baz\", \"desiredState\": { \"host\": \"bar\" } }", + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "baz", + }, + Spec: api.PodSpec{ + Host: "bar", + }, + }, + }, + { + obj: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + }, + fragment: "{ \"apiVersion\": \"v1beta3\", \"spec\": { \"volumes\": [ {\"name\": \"v1\"}, {\"name\": \"v2\"} ] } }", + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + { + Name: "v1", + }, + { + Name: "v2", + }, + }, + }, + }, + }, + { + obj: &api.Pod{}, + fragment: "invalid json", + expected: &api.Pod{}, + expectErr: true, + }, + } + + for _, test := range tests { + err := Merge(test.obj, test.fragment, "Pod") + if !test.expectErr { + if err != nil { + t.Errorf("unexpected error: %v", err) + } else if !reflect.DeepEqual(test.obj, test.expected) { + t.Errorf("\nexpected:\n%v\nsaw:\n%v", test.expected, test.obj) + } + } + if test.expectErr && err == nil { + t.Errorf("unexpected non-error") + } + } + +} diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index 914b43fdbd2..9fc44236dc9 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -27,7 +27,7 @@ import ( func (f *Factory) NewCmdRunContainer(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "run-container --image= [--replicas=replicas] [--dry-run=]", + Use: "run-container --image= [--replicas=replicas] [--dry-run=] [--overrides=]", Short: "Run a particular image on the cluster.", Long: `Create and run a particular image, possibly replicated. Creates a replication controller to manage the created container(s) @@ -65,6 +65,11 @@ Examples: controller, err := generator.Generate(params) checkErr(err) + inline := GetFlagString(cmd, "overrides") + if len(inline) > 0 { + Merge(controller, inline, "ReplicationController") + } + // TODO: extract this flag to a central location, when such a location exists. if !GetFlagBool(cmd, "dry-run") { controller, err = client.ReplicationControllers(namespace).Create(controller.(*api.ReplicationController)) @@ -79,5 +84,6 @@ Examples: cmd.Flags().IntP("replicas", "r", 1, "Number of replicas to create for this container. Default 1") cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, don't actually do anything") cmd.Flags().StringP("labels", "l", "", "Labels to apply to the pod(s) created by this call to run.") + cmd.Flags().String("overrides", "", "An inline JSON override for the generated object. If this is non-empty, it is parsed used to override the generated object") return cmd }