From 5a7aced7b0af327d0b538a216ba7eecb94d32eaa Mon Sep 17 00:00:00 2001 From: derekwaynecarr Date: Tue, 21 Oct 2014 14:11:53 -0400 Subject: [PATCH] Kubectl namespace support Add unit test for load namespace info Different message on display of namespace versus setting namespace --- pkg/kubectl/cmd/cmd.go | 21 +++++++++++++ pkg/kubectl/cmd/create.go | 2 +- pkg/kubectl/cmd/delete.go | 2 +- pkg/kubectl/cmd/get.go | 2 +- pkg/kubectl/cmd/namespace.go | 60 ++++++++++++++++++++++++++++++++++++ pkg/kubectl/cmd/update.go | 2 +- pkg/kubectl/get.go | 4 +-- pkg/kubectl/kubectl.go | 34 ++++++++++++++++++++ pkg/kubectl/kubectl_test.go | 52 +++++++++++++++++++++++++++++++ pkg/kubectl/modify.go | 22 ++++++------- 10 files changed, 184 insertions(+), 17 deletions(-) create mode 100644 pkg/kubectl/cmd/namespace.go diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index fae4d6e173a..951983969e2 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -25,6 +25,7 @@ import ( "strconv" "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/kubectl" @@ -55,6 +56,8 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.PersistentFlags().String("client-certificate", "", "Path to a client certificate for TLS.") cmds.PersistentFlags().String("client-key", "", "Path to a client key file for TLS.") cmds.PersistentFlags().Bool("insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.") + cmds.PersistentFlags().String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.") + cmds.PersistentFlags().StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.") cmds.AddCommand(NewCmdVersion(out)) cmds.AddCommand(NewCmdProxy(out)) @@ -63,6 +66,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(NewCmdCreate(out)) cmds.AddCommand(NewCmdUpdate(out)) cmds.AddCommand(NewCmdDelete(out)) + cmds.AddCommand(NewCmdNamespace(out)) if err := cmds.Execute(); err != nil { os.Exit(1) @@ -138,6 +142,23 @@ func getFlagInt(cmd *cobra.Command, flag string) int { return v } +func getKubeNamespace(cmd *cobra.Command) string { + result := api.NamespaceDefault + if ns := getFlagString(cmd, "namespace"); len(ns) > 0 { + result = ns + glog.V(2).Infof("Using namespace from -ns flag") + } else { + nsPath := getFlagString(cmd, "ns-path") + nsInfo, err := kubectl.LoadNamespaceInfo(nsPath) + if err != nil { + glog.Fatalf("Error loading current namespace: %v", err) + } + result = nsInfo.Namespace + } + glog.V(2).Infof("Using namespace %s", result) + return result +} + func getKubeConfig(cmd *cobra.Command) *client.Config { config := &client.Config{} diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 95bf92522cf..1d63e508853 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -45,7 +45,7 @@ Examples: data, err := readConfigData(filename) checkErr(err) - err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, kubectl.ModifyCreate, data) + err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, getKubeNamespace(cmd), kubectl.ModifyCreate, data) checkErr(err) }, } diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index 86c3b15cb04..f4ca5a9408f 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -71,7 +71,7 @@ Examples: } // TODO Add ability to require a resource-version check for delete. - err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, kubectl.ModifyDelete, data) + err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, getKubeNamespace(cmd), kubectl.ModifyDelete, data) checkErr(err) }, } diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index f4d4bba2cfc..85c769486fd 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -57,7 +57,7 @@ Examples: outputFormat := getFlagString(cmd, "output") templateFile := getFlagString(cmd, "template") selector := getFlagString(cmd, "selector") - err := kubectl.Get(out, getKubeClient(cmd).RESTClient, resource, id, selector, outputFormat, getFlagBool(cmd, "no-headers"), templateFile) + err := kubectl.Get(out, getKubeClient(cmd).RESTClient, getKubeNamespace(cmd), resource, id, selector, outputFormat, getFlagBool(cmd, "no-headers"), templateFile) checkErr(err) }, } diff --git a/pkg/kubectl/cmd/namespace.go b/pkg/kubectl/cmd/namespace.go new file mode 100644 index 00000000000..e008bd9077a --- /dev/null +++ b/pkg/kubectl/cmd/namespace.go @@ -0,0 +1,60 @@ +/* +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 ( + "io" + + "fmt" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/spf13/cobra" +) + +func NewCmdNamespace(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "namespace []", + Short: "Set and view the current Kubernetes namespace", + Long: `Set and view the current Kubernetes namespace scope for command line requests. + +A Kubernetes namespace subdivides the cluster into groups of logically related pods, services, and replication controllers. + +Examples: + $ kubectl namespace + Using namespace default + + $ kubectl namespace other + Set current namespace to other`, + Run: func(cmd *cobra.Command, args []string) { + nsPath := getFlagString(cmd, "ns-path") + var err error + var ns *kubectl.NamespaceInfo + switch len(args) { + case 0: + ns, err = kubectl.LoadNamespaceInfo(nsPath) + fmt.Printf("Using namespace %s\n", ns.Namespace) + case 1: + ns = &kubectl.NamespaceInfo{Namespace: args[0]} + err = kubectl.SaveNamespaceInfo(nsPath, ns) + fmt.Printf("Set current namespace to %s\n", ns.Namespace) + default: + usageError(cmd, "kubectl namespace []") + } + checkErr(err) + }, + } + return cmd +} diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index 08907a9afc2..5873637c965 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -46,7 +46,7 @@ Examples: data, err := readConfigData(filename) checkErr(err) - err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, kubectl.ModifyUpdate, data) + err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, getKubeNamespace(cmd), kubectl.ModifyUpdate, data) checkErr(err) }, } diff --git a/pkg/kubectl/get.go b/pkg/kubectl/get.go index bd54cb650a4..13d5612dedf 100644 --- a/pkg/kubectl/get.go +++ b/pkg/kubectl/get.go @@ -23,13 +23,13 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client" ) -func Get(w io.Writer, c *client.RESTClient, resource string, id string, selector string, format string, noHeaders bool, templateFile string) error { +func Get(w io.Writer, c *client.RESTClient, namespace string, resource string, id string, selector string, format string, noHeaders bool, templateFile string) error { path, err := resolveResource(resolveToPath, resource) if err != nil { return err } - r := c.Verb("GET").Path(path) + r := c.Verb("GET").Namespace(namespace).Path(path) if len(id) > 0 { r.Path(id) } diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 566b8a0a4dc..46c9873f59d 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -31,6 +31,7 @@ import ( "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" ) @@ -68,6 +69,39 @@ type AuthInfo struct { Insecure *bool } +type NamespaceInfo struct { + Namespace string +} + +// LoadNamespaceInfo parses a NamespaceInfo object from a file path. It creates a file at the specified path if it doesn't exist with the default namespace. +func LoadNamespaceInfo(path string) (*NamespaceInfo, error) { + var ns NamespaceInfo + if _, err := os.Stat(path); os.IsNotExist(err) { + ns.Namespace = api.NamespaceDefault + err = SaveNamespaceInfo(path, &ns) + return &ns, err + } + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + err = json.Unmarshal(data, &ns) + if err != nil { + return nil, err + } + return &ns, err +} + +// SaveNamespaceInfo saves a NamespaceInfo object at the specified file path. +func SaveNamespaceInfo(path string, ns *NamespaceInfo) error { + if !util.IsDNSLabel(ns.Namespace) { + return fmt.Errorf("Namespace %s is not a valid DNS Label", ns.Namespace) + } + data, err := json.Marshal(ns) + err = ioutil.WriteFile(path, data, 0600) + return err +} + // LoadAuthInfo parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. func LoadAuthInfo(path string, r io.Reader) (*AuthInfo, error) { var auth AuthInfo diff --git a/pkg/kubectl/kubectl_test.go b/pkg/kubectl/kubectl_test.go index 5ce1f38317a..f2ee55f31f6 100644 --- a/pkg/kubectl/kubectl_test.go +++ b/pkg/kubectl/kubectl_test.go @@ -33,6 +33,58 @@ func validateAction(expectedAction, actualAction client.FakeAction, t *testing.T } } +func TestLoadNamespaceInfo(t *testing.T) { + loadNamespaceInfoTests := []struct { + nsData string + nsInfo *NamespaceInfo + }{ + { + `{"Namespace":"test"}`, + &NamespaceInfo{Namespace: "test"}, + }, + { + "", nil, + }, + { + "missing", + &NamespaceInfo{Namespace: "default"}, + }, + } + for _, loadNamespaceInfoTest := range loadNamespaceInfoTests { + tt := loadNamespaceInfoTest + nsfile, err := ioutil.TempFile("", "testNamespaceInfo") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if tt.nsData != "missing" { + defer os.Remove(nsfile.Name()) + defer nsfile.Close() + _, err := nsfile.WriteString(tt.nsData) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } else { + nsfile.Close() + os.Remove(nsfile.Name()) + } + nsInfo, err := LoadNamespaceInfo(nsfile.Name()) + if len(tt.nsData) == 0 && tt.nsData != "missing" { + if err == nil { + t.Error("LoadNamespaceInfo didn't fail on an empty file") + } + continue + } + if tt.nsData != "missing" { + if err != nil { + t.Errorf("Unexpected error: %v, %v", tt.nsData, err) + } + if !reflect.DeepEqual(nsInfo, tt.nsInfo) { + t.Errorf("Expected %v, got %v", tt.nsInfo, nsInfo) + } + } + } +} + func TestLoadAuthInfo(t *testing.T) { loadAuthInfoTests := []struct { authData string diff --git a/pkg/kubectl/modify.go b/pkg/kubectl/modify.go index eeeda63cd1f..40efec4f485 100644 --- a/pkg/kubectl/modify.go +++ b/pkg/kubectl/modify.go @@ -33,7 +33,7 @@ const ( ModifyDelete = ModifyAction("delete") ) -func Modify(w io.Writer, c *client.RESTClient, action ModifyAction, data []byte) error { +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") } @@ -64,11 +64,11 @@ func Modify(w io.Writer, c *client.RESTClient, action ModifyAction, data []byte) var id string switch action { case "create": - id, err = doCreate(c, resource, data) + id, err = doCreate(c, namespace, resource, data) case "update": - id, err = doUpdate(c, resource, obj) + id, err = doUpdate(c, namespace, resource, obj) case "delete": - id, err = doDelete(c, resource, obj) + id, err = doDelete(c, namespace, resource, obj) } if err != nil { @@ -80,8 +80,8 @@ func Modify(w io.Writer, c *client.RESTClient, action ModifyAction, data []byte) } // Creates the object then returns the ID of the newly created object. -func doCreate(c *client.RESTClient, resource string, data []byte) (string, error) { - obj, err := c.Post().Path(resource).Body(data).Do().Get() +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 } @@ -89,7 +89,7 @@ func doCreate(c *client.RESTClient, resource string, data []byte) (string, error } // Creates the object then returns the ID of the newly created object. -func doUpdate(c *client.RESTClient, resource string, obj runtime.Object) (string, error) { +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) @@ -99,7 +99,7 @@ func doUpdate(c *client.RESTClient, resource string, obj runtime.Object) (string // 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().Path(resource).Path(id).Do().Get() + 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) } @@ -123,7 +123,7 @@ func doUpdate(c *client.RESTClient, resource string, obj runtime.Object) (string } // Do the update. - err = c.Put().Path(resource).Path(id).Body(data).Do().Error() + 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 @@ -132,7 +132,7 @@ func doUpdate(c *client.RESTClient, resource string, obj runtime.Object) (string return id, nil } -func doDelete(c *client.RESTClient, resource string, obj runtime.Object) (string, error) { +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) @@ -141,7 +141,7 @@ func doDelete(c *client.RESTClient, resource string, obj runtime.Object) (string return "", fmt.Errorf("The supplied resource has no Name and cannot be deleted") } - err = c.Delete().Path(resource).Path(id).Do().Error() + err = c.Delete().Namespace(namespace).Path(resource).Path(id).Do().Error() if err != nil { return "", err }