mirror of https://github.com/docker/cli.git
Merge pull request #898 from silvin-lubecki/add-stack-to-version-command
Print Stack API and Kubernetes versions in version command
This commit is contained in:
commit
2851c007fe
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue