Merge pull request #3537 from brendandburns/cli

Add inline JSON patching to kubectl update ...
pull/6/head
bgrant0607 2015-01-15 17:08:51 -08:00
commit 52cd620da8
5 changed files with 222 additions and 18 deletions

View File

@ -236,6 +236,9 @@ Examples:
$ cat pod.json | kubectl update -f -
<update a pod based on the json passed into stdin>
$ kubectl update pods my-pod --patch='{ "apiVersion": "v1beta1", "desiredState": { "manifest": [{ "cpu": 100 }]}}'
<update a pod by downloading it, applying the patch, then updating, requires apiVersion be specified>
Usage:
```
@ -261,6 +264,7 @@ Usage:
--match-server-version=false: Require server version to match client version
-n, --namespace="": If present, the namespace scope for this CLI request.
--ns-path="/home/username/.kubernetes_ns": Path to the namespace info file that holds the namespace context to use for CLI requests.
--patch="": A JSON document to override the existing resource. The resource is downloaded, then patched with the JSON, the updated
-s, --server="": The address of the Kubernetes API server
--stderrthreshold=2: logs at or above this threshold go to stderr
--token="": Bearer token for authentication to the API server.
@ -882,10 +886,13 @@ Examples:
$ kubectl run-container nginx --image=dockerfile/nginx --dry-run
<just print the corresponding API objects, don't actually send them to the apiserver>
$ kubectl run-container nginx --image=dockerfile/nginx --overrides='{ "apiVersion": "v1beta1", "desiredState": { ... } }'
<start a single instance of nginx, but overload the desired state with a partial set of values parsed from JSON
Usage:
```
kubectl run-container <name> --image=<image> [--replicas=replicas] [--dry-run=<bool>] [flags]
kubectl run-container <name> --image=<image> [--replicas=replicas] [--dry-run=<bool>] [--overrides=<inline-json>] [flags]
Available Flags:
--alsologtostderr=false: log to standard error as well as files
@ -913,6 +920,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. Requires that the object supply a valid apiVersion field.
-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

View File

@ -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)
dataMap["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)
}

View File

@ -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")
}
}
}

View File

@ -27,7 +27,7 @@ import (
func (f *Factory) NewCmdRunContainer(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "run-container <name> --image=<image> [--replicas=replicas] [--dry-run=<bool>]",
Use: "run-container <name> --image=<image> [--replicas=replicas] [--dry-run=<bool>] [--overrides=<inline-json>]",
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)
@ -40,7 +40,10 @@ Examples:
<starts a replicated instance of nginx>
$ kubectl run-container nginx --image=dockerfile/nginx --dry-run
<just print the corresponding API objects, don't actually send them to the apiserver>`,
<just print the corresponding API objects, don't actually send them to the apiserver>
$ kubectl run-container nginx --image=dockerfile/nginx --overrides='{ "apiVersion": "v1beta1", "desiredState": { ... } }'
<start a single instance of nginx, but overload the desired state with a partial set of values parsed from JSON`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
usageError(cmd, "<name> is required for run")
@ -65,6 +68,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))
@ -81,5 +89,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. Requires that the object supply a valid apiVersion field.")
return cmd
}

View File

@ -37,27 +37,68 @@ Examples:
<update a pod using the data in pod.json>
$ cat pod.json | kubectl update -f -
<update a pod based on the json passed into stdin>`,
<update a pod based on the json passed into stdin>
$ kubectl update pods my-pod --patch='{ "apiVersion": "v1beta1", "desiredState": { "manifest": [{ "cpu": 100 }]}}'
<update a pod by downloading it, applying the patch, then updating, requires apiVersion be specified>`,
Run: func(cmd *cobra.Command, args []string) {
filename := GetFlagString(cmd, "filename")
if len(filename) == 0 {
usageError(cmd, "Must specify filename to update")
patch := GetFlagString(cmd, "patch")
if len(filename) == 0 && len(patch) == 0 {
usageError(cmd, "Must specify --filename or --patch to update")
}
if len(filename) != 0 && len(patch) != 0 {
usageError(cmd, "Can not specify both --filename and --patch")
}
var name string
if len(filename) > 0 {
name = updateWithFile(cmd, f, filename)
} else {
name = updateWithPatch(cmd, args, f, patch)
}
schema, err := f.Validator(cmd)
checkErr(err)
mapper, typer := f.Object(cmd)
mapping, namespace, name, data := ResourceFromFile(cmd, filename, typer, mapper, schema)
client, err := f.RESTClient(cmd, mapping)
checkErr(err)
err = CompareNamespaceFromFile(cmd, namespace)
checkErr(err)
err = resource.NewHelper(client, mapping).Update(namespace, name, true, data)
checkErr(err)
fmt.Fprintf(out, "%s\n", name)
},
}
cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to update the resource")
cmd.Flags().String("patch", "", "A JSON document to override the existing resource. The resource is downloaded, then patched with the JSON, the updated")
return cmd
}
func updateWithPatch(cmd *cobra.Command, args []string, f *Factory, patch string) string {
mapper, _ := f.Object(cmd)
mapping, namespace, name := ResourceFromArgs(cmd, args, mapper)
client, err := f.RESTClient(cmd, mapping)
checkErr(err)
helper := resource.NewHelper(client, mapping)
obj, err := helper.Get(namespace, name)
checkErr(err)
Merge(obj, patch, mapping.Kind)
data, err := helper.Codec.Encode(obj)
checkErr(err)
err = helper.Update(namespace, name, true, data)
checkErr(err)
return name
}
func updateWithFile(cmd *cobra.Command, f *Factory, filename string) string {
schema, err := f.Validator(cmd)
checkErr(err)
mapper, typer := f.Object(cmd)
mapping, namespace, name, data := ResourceFromFile(cmd, filename, typer, mapper, schema)
client, err := f.RESTClient(cmd, mapping)
checkErr(err)
err = CompareNamespaceFromFile(cmd, namespace)
checkErr(err)
err = resource.NewHelper(client, mapping).Update(namespace, name, true, data)
checkErr(err)
return name
}