diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 575fccaccb0..65c88197bfd 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -27,12 +27,20 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/golang/glog" "github.com/spf13/cobra" ) +type Factory struct { + Mapper meta.RESTMapper + Typer runtime.ObjectTyper + Client func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) +} + func RunKubectl(out io.Writer) { // Parent command to which all subcommands are added. cmds := &cobra.Command{ @@ -44,6 +52,15 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, Run: runHelp, } + factory := &Factory{ + Mapper: latest.NewDefaultRESTMapper(), + Typer: api.Scheme, + Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { + // Will handle all resources defined by the command + return getKubeClient(cmd), nil + }, + } + // Globally persistent flags across all subcommands. // TODO Change flag names to consts to allow safer lookup from subcommands. // TODO Add a verbose flag that turns on glog logging. Probably need a way @@ -63,9 +80,11 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(NewCmdProxy(out)) cmds.AddCommand(NewCmdGet(out)) cmds.AddCommand(NewCmdDescribe(out)) - cmds.AddCommand(NewCmdCreate(out)) - cmds.AddCommand(NewCmdUpdate(out)) - cmds.AddCommand(NewCmdDelete(out)) + + cmds.AddCommand(factory.NewCmdCreate(out)) + cmds.AddCommand(factory.NewCmdUpdate(out)) + cmds.AddCommand(factory.NewCmdDelete(out)) + cmds.AddCommand(NewCmdNamespace(out)) cmds.AddCommand(NewCmdLog(out)) cmds.AddCommand(NewCmdCreateAll(out)) diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 1d63e508853..ae5094befd7 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -17,13 +17,14 @@ limitations under the License. package cmd import ( + "fmt" "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/spf13/cobra" ) -func NewCmdCreate(out io.Writer) *cobra.Command { +func (f *Factory) NewCmdCreate(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "create -f filename", Short: "Create a resource by filename or stdin", @@ -40,13 +41,15 @@ Examples: Run: func(cmd *cobra.Command, args []string) { filename := getFlagString(cmd, "filename") if len(filename) == 0 { - usageError(cmd, "Must pass a filename to update") + usageError(cmd, "Must specify filename to create") } - data, err := readConfigData(filename) + mapping, namespace, name, data := ResourceFromFile(filename, f.Typer, f.Mapper) + client, err := f.Client(cmd, mapping) checkErr(err) - err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, getKubeNamespace(cmd), kubectl.ModifyCreate, data) + err = kubectl.NewRESTModifier(client, mapping).Create(namespace, data) checkErr(err) + fmt.Fprintf(out, "%s\n", name) }, } cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to create the resource") diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index f4ca5a9408f..d0923ff9eaa 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -17,13 +17,14 @@ limitations under the License. package cmd import ( + "fmt" "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/spf13/cobra" ) -func NewCmdDelete(out io.Writer) *cobra.Command { +func (f *Factory) NewCmdDelete(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "delete ([-f filename] | ( ))", Short: "Delete a resource by filename, stdin or resource and id", @@ -48,31 +49,14 @@ Examples: $ kubectl delete pod 1234-56-7890-234234-456456 `, Run: func(cmd *cobra.Command, args []string) { - // If command line args are passed in, use those preferentially. - if len(args) > 0 && len(args) != 2 { - usageError(cmd, "If passing in command line parameters, must be resource and name") - } - - var data []byte - var err error - - if len(args) == 2 { - data, err = kubectl.CreateResource(args[0], args[1]) - } else { - filename := getFlagString(cmd, "filename") - if len(filename) > 0 { - data, err = readConfigData(getFlagString(cmd, "filename")) - } - } + filename := getFlagString(cmd, "filename") + mapping, namespace, name := ResourceFromArgsOrFile(cmd, args, filename, f.Typer, f.Mapper) + client, err := f.Client(cmd, mapping) checkErr(err) - if len(data) == 0 { - usageError(cmd, "Must specify filename or command line params") - } - - // TODO Add ability to require a resource-version check for delete. - err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, getKubeNamespace(cmd), kubectl.ModifyDelete, data) + err = kubectl.NewRESTModifier(client, mapping).Delete(namespace, name) checkErr(err) + fmt.Fprintf(out, "%s\n", name) }, } cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to delete the resource") diff --git a/pkg/kubectl/cmd/resource.go b/pkg/kubectl/cmd/resource.go new file mode 100644 index 00000000000..8b3435b1d01 --- /dev/null +++ b/pkg/kubectl/cmd/resource.go @@ -0,0 +1,92 @@ +/* +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 ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// ResourceFromArgsOrFile expects two arguments or a valid file with a given type, and extracts +// the fields necessary to uniquely locate a resource. Displays a usageError if that contract is +// not satisfied, or a generic error if any other problems occur. +func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string, typer runtime.ObjectTyper, mapper meta.RESTMapper) (mapping *meta.RESTMapping, namespace, name string) { + // If command line args are passed in, use those preferentially. + if len(args) > 0 && len(args) != 2 { + usageError(cmd, "If passing in command line parameters, must be resource and name") + } + + if len(args) == 2 { + resource := args[0] + namespace = api.NamespaceDefault + name = args[1] + if len(name) == 0 || len(resource) == 0 { + usageError(cmd, "Must specify filename or command line params") + } + + version, kind, err := mapper.VersionAndKindForResource(resource) + checkErr(err) + + mapping, err = mapper.RESTMapping(version, kind) + checkErr(err) + return + } + + if len(filename) == 0 { + usageError(cmd, "Must specify filename or command line params") + } + + mapping, namespace, name, _ = ResourceFromFile(filename, typer, mapper) + if len(name) == 0 { + checkErr(fmt.Errorf("The resource in the provided file has no name (or ID) defined")) + } + + return +} + +func ResourceFromFile(filename string, typer runtime.ObjectTyper, mapper meta.RESTMapper) (mapping *meta.RESTMapping, namespace, name string, data []byte) { + configData, err := readConfigData(filename) + checkErr(err) + data = configData + + version, kind, err := typer.DataVersionAndKind(data) + checkErr(err) + + // TODO: allow unversioned objects? + if len(version) == 0 { + checkErr(fmt.Errorf("The resource in the provided file has no apiVersion defined")) + } + + mapping, err = mapper.RESTMapping(version, kind) + checkErr(err) + + obj, err := mapping.Codec.Decode(data) + checkErr(err) + + meta := mapping.MetadataAccessor + namespace, err = meta.Namespace(obj) + checkErr(err) + name, err = meta.Name(obj) + checkErr(err) + + return +} diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index 5873637c965..a4f565097e3 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -17,13 +17,14 @@ limitations under the License. package cmd import ( + "fmt" "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/spf13/cobra" ) -func NewCmdUpdate(out io.Writer) *cobra.Command { +func (f *Factory) NewCmdUpdate(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "update -f filename", Short: "Update a resource by filename or stdin", @@ -40,14 +41,15 @@ Examples: Run: func(cmd *cobra.Command, args []string) { filename := getFlagString(cmd, "filename") if len(filename) == 0 { - usageError(cmd, "Must pass a filename to update") + usageError(cmd, "Must specify filename to update") } - - data, err := readConfigData(filename) + mapping, namespace, name, data := ResourceFromFile(filename, f.Typer, f.Mapper) + client, err := f.Client(cmd, mapping) checkErr(err) - err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, getKubeNamespace(cmd), kubectl.ModifyUpdate, data) + err = kubectl.NewRESTModifier(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") diff --git a/pkg/kubectl/interfaces.go b/pkg/kubectl/interfaces.go new file mode 100644 index 00000000000..443eb8a57c1 --- /dev/null +++ b/pkg/kubectl/interfaces.go @@ -0,0 +1,30 @@ +/* +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 kubectl + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +// RESTClient is a client helper for dealing with RESTful resources +// in a generic way. +type RESTClient interface { + Get() *client.Request + Post() *client.Request + Delete() *client.Request + Put() *client.Request +} diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 46c9873f59d..7f113fa991b 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -27,12 +27,11 @@ import ( "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/version" + "gopkg.in/v1/yaml" ) @@ -160,21 +159,6 @@ func makeImageList(manifest api.ContainerManifest) string { return strings.Join(images, ",") } -// Takes input 'data' as either json or yaml and attemps to decode it into the -// supplied object. -func dataToObject(data []byte) (runtime.Object, error) { - // This seems hacky but we can't get the codec from kubeClient. - versionInterfaces, err := latest.InterfacesFor(apiVersionToUse) - if err != nil { - return nil, err - } - obj, err := versionInterfaces.Codec.Decode(data) - if err != nil { - return nil, err - } - return obj, nil -} - const ( resolveToPath = "path" resolveToKind = "kind" diff --git a/pkg/kubectl/modify.go b/pkg/kubectl/modify.go index 40efec4f485..5eda8de1c11 100644 --- a/pkg/kubectl/modify.go +++ b/pkg/kubectl/modify.go @@ -17,150 +17,76 @@ limitations under the License. package kubectl import ( - "fmt" - "io" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) -type ModifyAction string +// RESTModifier provides methods for mutating a known or unknown +// RESTful resource. +type RESTModifier struct { + Resource string + // A RESTClient capable of mutating this resource + RESTClient RESTClient + // A codec for decoding and encoding objects of this resource type. + Codec runtime.Codec + // An interface for reading or writing the resource version of this + // type. + Versioner runtime.ResourceVersioner +} -const ( - ModifyCreate = ModifyAction("create") - ModifyUpdate = ModifyAction("update") - ModifyDelete = ModifyAction("delete") -) - -func Modify(w io.Writer, c *client.RESTClient, namespace string, action ModifyAction, data []byte) error { - if action != ModifyCreate && action != ModifyUpdate && action != ModifyDelete { - return fmt.Errorf("Action not recognized") +// NewRESTModifier creates a RESTModifier from a RESTMapping +func NewRESTModifier(client RESTClient, mapping *meta.RESTMapping) *RESTModifier { + return &RESTModifier{ + RESTClient: client, + Resource: mapping.Resource, + Codec: mapping.Codec, + Versioner: mapping.MetadataAccessor, } +} - // TODO Support multiple API versions. - version, kind, err := versionAndKind(data) +func (m *RESTModifier) Delete(namespace, name string) error { + return m.RESTClient.Delete().Path(m.Resource).Path(name).Do().Error() +} + +func (m *RESTModifier) Create(namespace string, data []byte) error { + return m.RESTClient.Post().Path(m.Resource).Body(data).Do().Error() +} + +func (m *RESTModifier) Update(namespace, name string, overwrite bool, data []byte) error { + c := m.RESTClient + + obj, err := m.Codec.Decode(data) if err != nil { - return err + // We don't know how to handle this object, but update it anyway + return c.Put().Path(m.Resource).Path(name).Body(data).Do().Error() } - if version != apiVersionToUse { - return fmt.Errorf("Only supporting API version '%s' for now (version '%s' specified)", apiVersionToUse, version) - } - - obj, err := dataToObject(data) + // Attempt to version the object based on client logic. + version, err := m.Versioner.ResourceVersion(obj) if err != nil { - if err.Error() == "No type '' for version ''" { - return fmt.Errorf("Object could not be decoded. Make sure it has the Kind field defined.") + // We don't know how to version this object, so send it to the server as is + return c.Put().Path(m.Resource).Path(name).Body(data).Do().Error() + } + if version == "" && overwrite { + // Retrieve the current version of the object to overwrite the server object + serverObj, err := c.Get().Path(m.Resource).Path(name).Do().Get() + if err != nil { + // The object does not exist, but we want it to be created + return c.Put().Path(m.Resource).Path(name).Body(data).Do().Error() } - return err + serverVersion, err := m.Versioner.ResourceVersion(serverObj) + if err != nil { + return err + } + if err := m.Versioner.SetResourceVersion(obj, serverVersion); err != nil { + return err + } + newData, err := m.Codec.Encode(obj) + if err != nil { + return err + } + data = newData } - resource, err := resolveKindToResource(kind) - if err != nil { - return err - } - - var id string - switch action { - case "create": - id, err = doCreate(c, namespace, resource, data) - case "update": - id, err = doUpdate(c, namespace, resource, obj) - case "delete": - id, err = doDelete(c, namespace, resource, obj) - } - - if err != nil { - return err - } - - fmt.Fprintf(w, "%s\n", id) - return nil -} - -// Creates the object then returns the ID of the newly created object. -func doCreate(c *client.RESTClient, namespace string, resource string, data []byte) (string, error) { - obj, err := c.Post().Namespace(namespace).Path(resource).Body(data).Do().Get() - if err != nil { - return "", err - } - return getIDFromObj(obj) -} - -// Creates the object then returns the ID of the newly created object. -func doUpdate(c *client.RESTClient, namespace string, resource string, obj runtime.Object) (string, error) { - // Figure out the ID of the object to update by introspecting into the - // object. - id, err := getIDFromObj(obj) - if err != nil { - return "", fmt.Errorf("Name not retrievable from object for update: %v", err) - } - - // Get the object from the server to find out its current resource - // version to prevent race conditions in updating the object. - serverObj, err := c.Get().Namespace(namespace).Path(resource).Path(id).Do().Get() - if err != nil { - return "", fmt.Errorf("Item Name %s does not exist for update: %v", id, err) - } - version, err := getResourceVersionFromObj(serverObj) - if err != nil { - return "", err - } - - // Update the object we are trying to send to the server with the - // correct resource version. - meta, err := meta.Accessor(obj) - if err != nil { - return "", err - } - meta.SetResourceVersion(version) - - // Convert object with updated resourceVersion to data for PUT. - data, err := c.Codec.Encode(obj) - if err != nil { - return "", err - } - - // Do the update. - err = c.Put().Namespace(namespace).Path(resource).Path(id).Body(data).Do().Error() - fmt.Printf("r: %q, i: %q, d: %s", resource, id, data) - if err != nil { - return "", err - } - - return id, nil -} - -func doDelete(c *client.RESTClient, namespace string, resource string, obj runtime.Object) (string, error) { - id, err := getIDFromObj(obj) - if err != nil { - return "", fmt.Errorf("Name not retrievable from object for delete: %v", err) - } - if id == "" { - return "", fmt.Errorf("The supplied resource has no Name and cannot be deleted") - } - - err = c.Delete().Namespace(namespace).Path(resource).Path(id).Do().Error() - if err != nil { - return "", err - } - - return id, nil -} - -func getIDFromObj(obj runtime.Object) (string, error) { - meta, err := meta.Accessor(obj) - if err != nil { - return "", err - } - return meta.Name(), nil -} - -func getResourceVersionFromObj(obj runtime.Object) (string, error) { - meta, err := meta.Accessor(obj) - if err != nil { - return "", err - } - return meta.ResourceVersion(), nil + return c.Put().Path(m.Resource).Path(name).Body(data).Do().Error() } diff --git a/pkg/kubectl/modify_test.go b/pkg/kubectl/modify_test.go new file mode 100644 index 00000000000..8be729d0d44 --- /dev/null +++ b/pkg/kubectl/modify_test.go @@ -0,0 +1,63 @@ +/* +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 kubectl + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +type FakeRESTClient struct{} + +func (c *FakeRESTClient) Get() *client.Request { + return &client.Request{} +} +func (c *FakeRESTClient) Put() *client.Request { + return &client.Request{} +} +func (c *FakeRESTClient) Post() *client.Request { + return &client.Request{} +} +func (c *FakeRESTClient) Delete() *client.Request { + return &client.Request{} +} + +func TestRESTModifierDelete(t *testing.T) { + tests := []struct { + Err bool + }{ + /*{ + Err: true, + },*/ + } + for _, test := range tests { + client := &FakeRESTClient{} + modifier := &RESTModifier{ + RESTClient: client, + } + err := modifier.Delete("bar", "foo") + switch { + case err == nil && test.Err: + t.Errorf("Unexpected non-error") + continue + case err != nil && !test.Err: + t.Errorf("Unexpected error: %v", err) + continue + } + } +} diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 1d9763396c4..7e6f26e40ac 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -176,7 +176,7 @@ func (s *Scheme) KnownTypes(version string) map[string]reflect.Type { } // DataVersionAndKind will return the APIVersion and Kind of the given wire-format -// enconding of an API Object, or an error. +// encoding of an API Object, or an error. func (s *Scheme) DataVersionAndKind(data []byte) (version, kind string, err error) { return s.raw.DataVersionAndKind(data) }