diff --git a/cmd/kops/root.go b/cmd/kops/root.go index fd7827791e..5af0677ce0 100644 --- a/cmd/kops/root.go +++ b/cmd/kops/root.go @@ -100,6 +100,7 @@ func NewCmdRoot(f *util.Factory, out io.Writer) *cobra.Command { cmd.AddCommand(NewCmdEdit(f, out)) cmd.AddCommand(NewCmdUpdate(f, out)) cmd.AddCommand(NewCmdReplace(f, out)) + cmd.AddCommand(NewCmdToolbox(f, out)) cmd.AddCommand(NewCmdValidate(f, out)) return cmd diff --git a/cmd/kops/toolbox.go b/cmd/kops/toolbox.go index d7df1215f7..24943a64c4 100644 --- a/cmd/kops/toolbox.go +++ b/cmd/kops/toolbox.go @@ -18,14 +18,18 @@ package main import ( "github.com/spf13/cobra" + "io" + "k8s.io/kops/cmd/kops/util" ) -// toolboxCmd represents the toolbox command -var toolboxCmd = &cobra.Command{ - Use: "toolbox", - Short: "Misc infrequently used commands", -} +func NewCmdToolbox(f *util.Factory, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "toolbox", + Short: "Misc infrequently used commands", + } -func init() { - rootCommand.AddCommand(toolboxCmd) + cmd.AddCommand(NewCmdToolboxConvertImported(f, out)) + cmd.AddCommand(NewCmdToolboxDump(f, out)) + + return cmd } diff --git a/cmd/kops/toolbox_convert_imported.go b/cmd/kops/toolbox_convert_imported.go index 02334d6166..97a2dde892 100644 --- a/cmd/kops/toolbox_convert_imported.go +++ b/cmd/kops/toolbox_convert_imported.go @@ -19,46 +19,64 @@ package main import ( "fmt" "github.com/spf13/cobra" + "io" + "k8s.io/kops/cmd/kops/util" api "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/upup/pkg/fi/cloudup/awsup" "k8s.io/kops/upup/pkg/kutil" k8sapi "k8s.io/kubernetes/pkg/api" ) -type ConvertImportedCmd struct { +type ToolboxConvertImportedOptions struct { NewClusterName string // Channel is the location of the api.Channel to use for our defaults Channel string + + ClusterName string } -var convertImported ConvertImportedCmd +func (o *ToolboxConvertImportedOptions) InitDefaults() { + o.Channel = api.DefaultChannel +} + +func NewCmdToolboxConvertImported(f *util.Factory, out io.Writer) *cobra.Command { + options := &ToolboxConvertImportedOptions{} -func init() { cmd := &cobra.Command{ Use: "convert-imported", Short: "Convert an imported cluster into a kops cluster", Run: func(cmd *cobra.Command, args []string) { - err := convertImported.Run() + if err := rootCommand.ProcessArgs(args); err != nil { + exitWithError(err) + } + + options.ClusterName = rootCommand.ClusterName() + + err := RunToolboxConvertImported(f, out, options) if err != nil { exitWithError(err) } }, } - toolboxCmd.AddCommand(cmd) + cmd.Flags().StringVar(&options.NewClusterName, "newname", options.NewClusterName, "new cluster name") + cmd.Flags().StringVar(&options.Channel, "channel", options.Channel, "Channel to use for upgrade") - cmd.Flags().StringVar(&convertImported.NewClusterName, "newname", "", "new cluster name") - cmd.Flags().StringVar(&convertImported.Channel, "channel", api.DefaultChannel, "Channel to use for upgrade") + return cmd } -func (c *ConvertImportedCmd) Run() error { - cluster, err := rootCommand.Cluster() +func RunToolboxConvertImported(f *util.Factory, out io.Writer, options *ToolboxConvertImportedOptions) error { + clientset, err := f.Clientset() if err != nil { return err } - clientset, err := rootCommand.Clientset() + if options.ClusterName == "" { + return fmt.Errorf("ClusterName is required") + } + + cluster, err := clientset.Clusters().Get(options.ClusterName) if err != nil { return err } @@ -76,7 +94,7 @@ func (c *ConvertImportedCmd) Run() error { return fmt.Errorf("cluster %q does not appear to be a cluster imported using kops import", cluster.ObjectMeta.Name) } - if c.NewClusterName == "" { + if options.NewClusterName == "" { return fmt.Errorf("--newname is required for converting an imported cluster") } @@ -110,13 +128,13 @@ func (c *ConvertImportedCmd) Run() error { return fmt.Errorf("error initializing AWS client: %v", err) } - channel, err := api.LoadChannel(c.Channel) + channel, err := api.LoadChannel(options.Channel) if err != nil { return err } d := &kutil.ConvertKubeupCluster{ - NewClusterName: c.NewClusterName, + NewClusterName: options.NewClusterName, OldClusterName: oldClusterName, Cloud: cloud, ClusterConfig: cluster, diff --git a/cmd/kops/toolbox_dump.go b/cmd/kops/toolbox_dump.go new file mode 100644 index 0000000000..623f2bf861 --- /dev/null +++ b/cmd/kops/toolbox_dump.go @@ -0,0 +1,146 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 main + +import ( + "encoding/json" + "fmt" + "github.com/golang/glog" + "github.com/spf13/cobra" + "io" + "k8s.io/kops/cmd/kops/util" + "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/upup/pkg/fi/cloudup" + "k8s.io/kops/upup/pkg/kutil" +) + +type ToolboxDumpOptions struct { + Output string + + ClusterName string +} + +func (o *ToolboxDumpOptions) InitDefaults() { + o.Output = OutputYaml +} + +func NewCmdToolboxDump(f *util.Factory, out io.Writer) *cobra.Command { + options := &ToolboxDumpOptions{} + options.InitDefaults() + + cmd := &cobra.Command{ + Use: "dump", + Short: "Dump information about a cluster", + Run: func(cmd *cobra.Command, args []string) { + if err := rootCommand.ProcessArgs(args); err != nil { + exitWithError(err) + } + + options.ClusterName = rootCommand.ClusterName() + + err := RunToolboxDump(f, out, options) + if err != nil { + exitWithError(err) + } + }, + } + + // TODO: Push up to top-level command? + cmd.Flags().StringVarP(&options.Output, "output", "o", options.Output, "output format. One of: yaml, json") + + return cmd +} + +func RunToolboxDump(f *util.Factory, out io.Writer, options *ToolboxDumpOptions) error { + clientset, err := f.Clientset() + if err != nil { + return err + } + + if options.ClusterName == "" { + return fmt.Errorf("ClusterName is required") + } + + cluster, err := clientset.Clusters().Get(options.ClusterName) + if err != nil { + return err + } + + if cluster == nil { + return fmt.Errorf("cluster not found %q", options.ClusterName) + } + + cloud, err := cloudup.BuildCloud(cluster) + if err != nil { + return err + } + + d := &kutil.DeleteCluster{} + d.ClusterName = options.ClusterName + d.Cloud = cloud + + resources, err := d.ListResources() + if err != nil { + return err + } + + data := make(map[string]interface{}) + + dumpedResources := []interface{}{} + for k, r := range resources { + if r.Dumper == nil { + glog.V(8).Infof("skipping dump of %q (no Dumper)", k) + continue + } + + o, err := r.Dumper(r) + if err != nil { + return fmt.Errorf("error dumping %q: %v", k, err) + } + if o != nil { + dumpedResources = append(dumpedResources, o) + } + } + data["resources"] = dumpedResources + + switch options.Output { + case OutputYaml: + b, err := kops.ToRawYaml(data) + if err != nil { + return fmt.Errorf("error marshaling yaml: %v", err) + } + _, err = out.Write(b) + if err != nil { + return fmt.Errorf("error writing to stdout: %v", err) + } + return nil + + case OutputJSON: + b, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("error marshaling json: %v", err) + } + _, err = out.Write(b) + if err != nil { + return fmt.Errorf("error writing to stdout: %v", err) + } + return nil + + default: + return fmt.Errorf("Unsupported output format: %q", options.Output) + } +} diff --git a/upup/pkg/kutil/delete_cluster.go b/upup/pkg/kutil/delete_cluster.go index f1e0e62a00..4b476d900b 100644 --- a/upup/pkg/kutil/delete_cluster.go +++ b/upup/pkg/kutil/delete_cluster.go @@ -65,6 +65,8 @@ type ResourceTracker struct { groupKey string groupDeleter func(cloud fi.Cloud, trackers []*ResourceTracker) error + Dumper func(r *ResourceTracker) (interface{}, error) + obj interface{} } @@ -484,8 +486,10 @@ func ListInstances(cloud fi.Cloud, clusterName string) ([]*ResourceTracker, erro tracker := &ResourceTracker{ Name: FindName(instance.Tags), ID: id, - Type: "instance", + Type: ec2.ResourceTypeInstance, deleter: DeleteInstance, + Dumper: DumpInstance, + obj: instance, } var blocks []string @@ -518,6 +522,14 @@ func ListInstances(cloud fi.Cloud, clusterName string) ([]*ResourceTracker, erro return trackers, nil } +func DumpInstance(r *ResourceTracker) (interface{}, error) { + data := make(map[string]interface{}) + data["id"] = r.ID + data["type"] = ec2.ResourceTypeInstance + data["raw"] = r.obj + return data, nil +} + func DeleteSecurityGroup(cloud fi.Cloud, t *ResourceTracker) error { c := cloud.(awsup.AWSCloud) @@ -1236,6 +1248,14 @@ func DeleteVPC(cloud fi.Cloud, r *ResourceTracker) error { return nil } +func DumpVPC(r *ResourceTracker) (interface{}, error) { + data := make(map[string]interface{}) + data["id"] = r.ID + data["type"] = ec2.ResourceTypeVpc + data["raw"] = r.obj + return data, nil +} + func DescribeVPCs(cloud fi.Cloud) ([]*ec2.Vpc, error) { c := cloud.(awsup.AWSCloud) @@ -1262,8 +1282,10 @@ func ListVPCs(cloud fi.Cloud, clusterName string) ([]*ResourceTracker, error) { tracker := &ResourceTracker{ Name: FindName(v.Tags), ID: aws.StringValue(v.VpcId), - Type: "vpc", + Type: ec2.ResourceTypeVpc, deleter: DeleteVPC, + Dumper: DumpVPC, + obj: v, } var blocks []string