From 854aad8927e9e61c10be944ced7546f0b7142379 Mon Sep 17 00:00:00 2001 From: Silvin Lubecki Date: Thu, 22 Feb 2018 14:55:42 +0100 Subject: [PATCH] Print Stack API version in version command * Resolve Stack API using Kubernetes discovering API * Refactor Kubernetes flags parsing Signed-off-by: Silvin Lubecki --- cli/command/stack/deploy.go | 2 +- cli/command/stack/kubernetes/check.go | 32 ------------ cli/command/stack/kubernetes/cli.go | 71 ++++++++++++++++---------- cli/command/stack/list.go | 2 +- cli/command/stack/ps.go | 2 +- cli/command/stack/remove.go | 2 +- cli/command/stack/services.go | 2 +- cli/command/system/version.go | 72 +++++++++++++++++++++++++-- kubernetes/check.go | 50 +++++++++++++++++++ kubernetes/check_test.go | 49 ++++++++++++++++++ kubernetes/config.go | 24 +++++++++ 11 files changed, 242 insertions(+), 66 deletions(-) delete mode 100644 cli/command/stack/kubernetes/check.go create mode 100644 kubernetes/check.go create mode 100644 kubernetes/check_test.go create mode 100644 kubernetes/config.go diff --git a/cli/command/stack/deploy.go b/cli/command/stack/deploy.go index 106dd83e8b..a21640d054 100644 --- a/cli/command/stack/deploy.go +++ b/cli/command/stack/deploy.go @@ -20,7 +20,7 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.Namespace = args[0] if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/cli/command/stack/kubernetes/check.go b/cli/command/stack/kubernetes/check.go deleted file mode 100644 index 55411b3dd3..0000000000 --- a/cli/command/stack/kubernetes/check.go +++ /dev/null @@ -1,32 +0,0 @@ -package kubernetes - -import ( - "fmt" - - apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1" - log "github.com/sirupsen/logrus" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" -) - -// APIPresent checks that an API is installed. -func APIPresent(config *rest.Config) error { - log.Debugf("check API present at %s", config.Host) - clients, err := kubernetes.NewForConfig(config) - if err != nil { - return err - } - - groups, err := clients.Discovery().ServerGroups() - if err != nil { - return err - } - - for _, group := range groups.Groups { - if group.Name == apiv1beta1.SchemeGroupVersion.Group { - return nil - } - } - - return fmt.Errorf("could not find %s api. Install it on your cluster first", apiv1beta1.SchemeGroupVersion.Group) -} diff --git a/cli/command/stack/kubernetes/cli.go b/cli/command/stack/kubernetes/cli.go index 37f826272f..5962e0eab6 100644 --- a/cli/command/stack/kubernetes/cli.go +++ b/cli/command/stack/kubernetes/cli.go @@ -1,16 +1,17 @@ package kubernetes import ( - "fmt" "os" "path/filepath" "github.com/docker/cli/cli/command" + "github.com/docker/cli/kubernetes" composev1beta1 "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/typed/compose/v1beta1" "github.com/docker/docker/pkg/homedir" - "github.com/spf13/cobra" + "github.com/pkg/errors" + flag "github.com/spf13/pflag" + kubeclient "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" ) // KubeCli holds kubernetes specifics (client, namespace) with the command.Cli @@ -18,28 +19,38 @@ type KubeCli struct { command.Cli kubeConfig *restclient.Config kubeNamespace string + clientSet *kubeclient.Clientset +} + +// Options contains resolved parameters to initialize kubernetes clients +type Options struct { + Namespace string + Config string +} + +// NewOptions returns an Options initialized with command line flags +func NewOptions(flags *flag.FlagSet) Options { + var opts Options + if namespace, err := flags.GetString("namespace"); err == nil { + opts.Namespace = namespace + } + if kubeConfig, err := flags.GetString("kubeconfig"); err == nil { + opts.Config = kubeConfig + } + return opts } // WrapCli wraps command.Cli with kubernetes specifics -func WrapCli(dockerCli command.Cli, cmd *cobra.Command) (*KubeCli, error) { +func WrapCli(dockerCli command.Cli, opts Options) (*KubeCli, error) { var err error cli := &KubeCli{ Cli: dockerCli, kubeNamespace: "default", } - if cmd.Flags().Changed("namespace") { - cli.kubeNamespace, err = cmd.Flags().GetString("namespace") - if err != nil { - return nil, err - } - } - kubeConfig := "" - if cmd.Flags().Changed("kubeconfig") { - kubeConfig, err = cmd.Flags().GetString("kubeconfig") - if err != nil { - return nil, err - } + if opts.Namespace != "" { + cli.kubeNamespace = opts.Namespace } + kubeConfig := opts.Config if kubeConfig == "" { if config := os.Getenv("KUBECONFIG"); config != "" { kubeConfig = config @@ -47,13 +58,18 @@ func WrapCli(dockerCli command.Cli, cmd *cobra.Command) (*KubeCli, error) { kubeConfig = filepath.Join(homedir.Get(), ".kube/config") } } - - config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) + config, err := kubernetes.NewKubernetesConfig(kubeConfig) if err != nil { - return nil, fmt.Errorf("Failed to load kubernetes configuration file '%s'", kubeConfig) + return nil, err } cli.kubeConfig = config + clientSet, err := kubeclient.NewForConfig(config) + if err != nil { + return nil, err + } + cli.clientSet = clientSet + return cli, nil } @@ -62,15 +78,20 @@ func (c *KubeCli) composeClient() (*Factory, error) { } func (c *KubeCli) stacks() (composev1beta1.StackInterface, error) { - err := APIPresent(c.kubeConfig) + version, err := kubernetes.GetStackAPIVersion(c.clientSet) + if err != nil { return nil, err } - clientSet, err := composev1beta1.NewForConfig(c.kubeConfig) - if err != nil { - return nil, err + switch version { + case kubernetes.StackAPIV1Beta1: + clientSet, err := composev1beta1.NewForConfig(c.kubeConfig) + if err != nil { + return nil, err + } + return clientSet.Stacks(c.kubeNamespace), nil + default: + return nil, errors.Errorf("no supported Stack API version") } - - return clientSet.Stacks(c.kubeNamespace), nil } diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go index 2cb10c1716..b153a8b2f6 100644 --- a/cli/command/stack/list.go +++ b/cli/command/stack/list.go @@ -19,7 +19,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/cli/command/stack/ps.go b/cli/command/stack/ps.go index 8e835201a2..7c003bda80 100644 --- a/cli/command/stack/ps.go +++ b/cli/command/stack/ps.go @@ -20,7 +20,7 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.Namespace = args[0] if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go index e4dd913dbd..25df2a5b61 100644 --- a/cli/command/stack/remove.go +++ b/cli/command/stack/remove.go @@ -20,7 +20,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.Namespaces = args if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/cli/command/stack/services.go b/cli/command/stack/services.go index 7f427c170e..c05d3e51c1 100644 --- a/cli/command/stack/services.go +++ b/cli/command/stack/services.go @@ -20,7 +20,7 @@ func newServicesCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.Namespace = args[0] if dockerCli.ClientInfo().HasKubernetes() { - kli, err := kubernetes.WrapCli(dockerCli, cmd) + kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(cmd.Flags())) if err != nil { return err } diff --git a/cli/command/system/version.go b/cli/command/system/version.go index 8223b6df80..20995dc38a 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -9,10 +9,13 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/kubernetes" "github.com/docker/cli/templates" "github.com/docker/docker/api/types" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/net/context" + kubernetesClient "k8s.io/client-go/kubernetes" ) var versionTemplate = `{{with .Client -}} @@ -48,16 +51,23 @@ Server:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}} {{- end}} {{- end}} {{- end}} + {{- end}}{{- end}} + {{- if .KubernetesOK}}{{with .Kubernetes}} + Kubernetes: + Version: {{.Kubernetes}} + Stack API: {{.StackAPI}} {{- end}}{{end}}` type versionOptions struct { - format string + format string + kubeConfig string } // versionInfo contains version information of both the Client, and Server type versionInfo struct { - Client clientVersion - Server *types.Version + Client clientVersion + Server *types.Version + Kubernetes *kubernetesVersion } type clientVersion struct { @@ -75,12 +85,21 @@ type clientVersion struct { Orchestrator string `json:",omitempty"` } +type kubernetesVersion struct { + Kubernetes string + StackAPI string +} + // ServerOK returns true when the client could connect to the docker server // and parse the information received. It returns false otherwise. func (v versionInfo) ServerOK() bool { return v.Server != nil } +func (v versionInfo) KubernetesOK() bool { + return v.Kubernetes != nil +} + // NewVersionCommand creates a new cobra.Command for `docker version` func NewVersionCommand(dockerCli command.Cli) *cobra.Command { var opts versionOptions @@ -95,8 +114,10 @@ func NewVersionCommand(dockerCli command.Cli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") + flags.StringVarP(&opts.kubeConfig, "kubeconfig", "k", "", "Kubernetes config file") + flags.SetAnnotation("kubeconfig", "kubernetes", nil) + flags.SetAnnotation("kubeconfig", "experimentalCLI", nil) return cmd } @@ -139,6 +160,7 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error { Experimental: dockerCli.ClientInfo().HasExperimental, Orchestrator: string(dockerCli.ClientInfo().Orchestrator), }, + Kubernetes: getKubernetesVersion(dockerCli, opts.kubeConfig), } sv, err := dockerCli.Client().ServerVersion(context.Background()) @@ -189,3 +211,45 @@ func getDetailsOrder(v types.ComponentVersion) []string { sort.Strings(out) return out } + +func getKubernetesVersion(dockerCli command.Cli, kubeConfig string) *kubernetesVersion { + if !dockerCli.ClientInfo().HasKubernetes() { + return nil + } + + version := kubernetesVersion{ + Kubernetes: "Unknown", + StackAPI: "Unknown", + } + config, err := kubernetes.NewKubernetesConfig(kubeConfig) + if err != nil { + logrus.Debugf("failed to get Kubernetes configuration: %s", err) + return &version + } + kubeClient, err := kubernetesClient.NewForConfig(config) + if err != nil { + logrus.Debugf("failed to get Kubernetes client: %s", err) + return &version + } + version.StackAPI = getStackVersion(kubeClient) + version.Kubernetes = getKubernetesServerVersion(kubeClient) + return &version +} + +func getStackVersion(client *kubernetesClient.Clientset) string { + apiVersion, err := kubernetes.GetStackAPIVersion(client) + if err != nil { + logrus.Debugf("failed to get Stack API version: %s", err) + return "Unknown" + } + return string(apiVersion) +} + +func getKubernetesServerVersion(client *kubernetesClient.Clientset) string { + kubeVersion, err := client.DiscoveryClient.ServerVersion() + if err != nil { + logrus.Debugf("failed to get Kubernetes server version: %s", err) + return "Unknown" + } + return kubeVersion.String() +} diff --git a/kubernetes/check.go b/kubernetes/check.go new file mode 100644 index 0000000000..f8dee3d052 --- /dev/null +++ b/kubernetes/check.go @@ -0,0 +1,50 @@ +package kubernetes + +import ( + apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1" + "github.com/pkg/errors" + apimachinerymetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" +) + +// StackVersion represents the detected Compose Component on Kubernetes side. +type StackVersion string + +const ( + // StackAPIV1Beta1 is returned if it's the most recent version available. + StackAPIV1Beta1 = StackVersion("v1beta1") +) + +// GetStackAPIVersion returns the most recent stack API installed. +func GetStackAPIVersion(clientSet *kubernetes.Clientset) (StackVersion, error) { + groups, err := clientSet.Discovery().ServerGroups() + if err != nil { + return "", err + } + + return getAPIVersion(groups) +} + +func getAPIVersion(groups *metav1.APIGroupList) (StackVersion, error) { + switch { + case findVersion(apiv1beta1.SchemeGroupVersion, groups.Groups): + return StackAPIV1Beta1, nil + default: + return "", errors.Errorf("failed to find a Stack API version") + } +} + +func findVersion(stackAPI schema.GroupVersion, groups []apimachinerymetav1.APIGroup) bool { + for _, group := range groups { + if group.Name == stackAPI.Group { + for _, version := range group.Versions { + if version.Version == stackAPI.Version { + return true + } + } + } + } + return false +} diff --git a/kubernetes/check_test.go b/kubernetes/check_test.go new file mode 100644 index 0000000000..0368e05228 --- /dev/null +++ b/kubernetes/check_test.go @@ -0,0 +1,49 @@ +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetStackAPIVersion(t *testing.T) { + var tests = []struct { + description string + groups *metav1.APIGroupList + err bool + expectedStack StackVersion + }{ + {"no stack api", makeGroups(), true, ""}, + {"v1beta1", makeGroups(groupVersion{"compose.docker.com", []string{"v1beta1"}}), false, StackAPIV1Beta1}, + } + + for _, test := range tests { + version, err := getAPIVersion(test.groups) + if test.err { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, test.expectedStack, version) + } +} + +type groupVersion struct { + name string + versions []string +} + +func makeGroups(versions ...groupVersion) *metav1.APIGroupList { + groups := make([]metav1.APIGroup, len(versions)) + for i := range versions { + groups[i].Name = versions[i].name + for _, v := range versions[i].versions { + groups[i].Versions = append(groups[i].Versions, metav1.GroupVersionForDiscovery{Version: v}) + } + } + return &metav1.APIGroupList{ + Groups: groups, + } +} diff --git a/kubernetes/config.go b/kubernetes/config.go new file mode 100644 index 0000000000..3a9fdfc6dc --- /dev/null +++ b/kubernetes/config.go @@ -0,0 +1,24 @@ +package kubernetes + +import ( + "os" + "path/filepath" + + "github.com/docker/docker/pkg/homedir" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// NewKubernetesConfig resolves the path to the desired Kubernetes configuration file, depending +// environment variable and command line flag. +func NewKubernetesConfig(configFlag string) (*restclient.Config, error) { + kubeConfig := configFlag + if kubeConfig == "" { + if config := os.Getenv("KUBECONFIG"); config != "" { + kubeConfig = config + } else { + kubeConfig = filepath.Join(homedir.Get(), ".kube/config") + } + } + return clientcmd.BuildConfigFromFlags("", kubeConfig) +}