Print Stack API version in version command

* Resolve Stack API using Kubernetes discovering API
* Refactor Kubernetes flags parsing

Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
This commit is contained in:
Silvin Lubecki 2018-02-22 14:55:42 +01:00
parent ef7d8be86c
commit 854aad8927
11 changed files with 242 additions and 66 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

50
kubernetes/check.go Normal file
View File

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

49
kubernetes/check_test.go Normal file
View File

@ -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,
}
}

24
kubernetes/config.go Normal file
View File

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