From 73e94b4ba50acaf35bc5a9f52b44b8e662dd985f Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Thu, 4 Feb 2021 15:36:57 +0100 Subject: [PATCH 1/4] Add Kube client Signed-off-by: Guillaume Tardif --- kube/client/client.go | 43 +++++++++++++++++++++++++++++++++++++++++++ kube/compose.go | 9 +++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 kube/client/client.go diff --git a/kube/client/client.go b/kube/client/client.go new file mode 100644 index 00000000..da97872a --- /dev/null +++ b/kube/client/client.go @@ -0,0 +1,43 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI 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 client + +import ( + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" +) + +type KubeClient struct { + client *kubernetes.Clientset +} + +func NewKubeClient(config genericclioptions.RESTClientGetter) (*KubeClient, error) { + restConfig, err := config.ToRESTConfig() + if err != nil { + return nil, err + } + + clientset, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return nil, err + } + return &KubeClient{ + client: clientset, + }, nil +} diff --git a/kube/compose.go b/kube/compose.go index c648950c..27266da7 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -30,12 +30,14 @@ import ( "github.com/docker/compose-cli/api/context/store" "github.com/docker/compose-cli/api/errdefs" "github.com/docker/compose-cli/api/progress" + "github.com/docker/compose-cli/kube/client" "github.com/docker/compose-cli/kube/helm" "github.com/docker/compose-cli/kube/resources" ) type composeService struct { - sdk *helm.Actions + sdk *helm.Actions + client *client.KubeClient } // NewComposeService create a kubernetes implementation of the compose.Service API @@ -52,11 +54,14 @@ func NewComposeService(ctx context.Context) (compose.Service, error) { return nil, err } actions, err := helm.NewActions(config) + apiClient, err := client.NewKubeClient(config) if err != nil { return nil, err } + return &composeService{ - sdk: actions, + sdk: actions, + client: apiClient, }, nil } From f368036dddea81b16cdeddc088f2be2023eb311c Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Thu, 4 Feb 2021 16:57:06 +0100 Subject: [PATCH 2/4] Add compose ps listing pod statuses (retrieve pods from compose project label) This does not yet retrieve port sharing data (need to reconcile ports with kube services) Signed-off-by: Guillaume Tardif --- kube/client/client.go | 38 ++++++++++++++++++++++++++ kube/client/client_test.go | 56 ++++++++++++++++++++++++++++++++++++++ kube/compose.go | 5 +++- kube/e2e/compose_test.go | 13 +++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 kube/client/client_test.go diff --git a/kube/client/client.go b/kube/client/client.go index da97872a..1cdf8821 100644 --- a/kube/client/client.go +++ b/kube/client/client.go @@ -19,14 +19,24 @@ package client import ( + "context" + "encoding/json" + "fmt" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes" + + "github.com/docker/compose-cli/api/compose" ) +// KubeClient API to access kube objects type KubeClient struct { client *kubernetes.Clientset } +// NewKubeClient new kubernetes client func NewKubeClient(config genericclioptions.RESTClientGetter) (*KubeClient, error) { restConfig, err := config.ToRESTConfig() if err != nil { @@ -41,3 +51,31 @@ func NewKubeClient(config genericclioptions.RESTClientGetter) (*KubeClient, erro client: clientset, }, nil } + +// GetContainers get containers for a given compose project +func (kc KubeClient) GetContainers(ctx context.Context, projectName string, all bool) ([]compose.ContainerSummary, error) { + pods, err := kc.client.CoreV1().Pods("").List(ctx, metav1.ListOptions{LabelSelector: fmt.Sprintf("%s=%s", compose.ProjectTag, projectName)}) + + if err != nil { + return nil, err + } + json, _ := json.MarshalIndent(pods, "", " ") + fmt.Println(string(json)) + fmt.Printf("containers: %d\n", len(pods.Items)) + result := []compose.ContainerSummary{} + for _, pod := range pods.Items { + result = append(result, podToContainerSummary(pod)) + } + + return result, nil +} + +func podToContainerSummary(pod v1.Pod) compose.ContainerSummary { + return compose.ContainerSummary{ + ID: pod.GetObjectMeta().GetName(), + Name: pod.GetObjectMeta().GetName(), + Service: pod.GetObjectMeta().GetLabels()[compose.ServiceTag], + State: string(pod.Status.Phase), + Project: pod.GetObjectMeta().GetLabels()[compose.ProjectTag], + } +} diff --git a/kube/client/client_test.go b/kube/client/client_test.go new file mode 100644 index 00000000..bd063dfa --- /dev/null +++ b/kube/client/client_test.go @@ -0,0 +1,56 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI 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 client + +import ( + "testing" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "gotest.tools/v3/assert" + + "github.com/docker/compose-cli/api/compose" +) + +func TestPodToContainerSummary(t *testing.T) { + pod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "c1-123", + Labels: map[string]string{ + compose.ProjectTag: "myproject", + compose.ServiceTag: "service1", + }, + }, + Status: v1.PodStatus{ + Phase: "Running", + }, + } + + container := podToContainerSummary(pod) + + expected := compose.ContainerSummary{ + ID: "c1-123", + Name: "c1-123", + Project: "myproject", + Service: "service1", + State: "Running", + } + assert.DeepEqual(t, container, expected) +} diff --git a/kube/compose.go b/kube/compose.go index 27266da7..1d80aca5 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -54,6 +54,9 @@ func NewComposeService(ctx context.Context) (compose.Service, error) { return nil, err } actions, err := helm.NewActions(config) + if err != nil { + return nil, err + } apiClient, err := client.NewKubeClient(config) if err != nil { return nil, err @@ -156,7 +159,7 @@ func (s *composeService) Logs(ctx context.Context, projectName string, consumer // Ps executes the equivalent to a `compose ps` func (s *composeService) Ps(ctx context.Context, projectName string, options compose.PsOptions) ([]compose.ContainerSummary, error) { - return nil, errdefs.ErrNotImplemented + return s.client.GetContainers(ctx, projectName, options.All) } // Convert translate compose model into backend's native format diff --git a/kube/e2e/compose_test.go b/kube/e2e/compose_test.go index 0d14c8f7..0813f5a3 100644 --- a/kube/e2e/compose_test.go +++ b/kube/e2e/compose_test.go @@ -24,6 +24,7 @@ import ( "testing" "time" + testify "github.com/stretchr/testify/assert" "gotest.tools/v3/assert" "gotest.tools/v3/icmd" @@ -78,6 +79,18 @@ func TestComposeUp(t *testing.T) { res.Assert(t, icmd.Expected{Out: `[{"Name":"compose-kube-demo","Status":"deployed"}]`}) }) + t.Run("compose ps", func(t *testing.T) { + getServiceRegx := func(project string, service string) string { + // match output with random hash / spaces like: + // myproject-db-698f4dd798-jd9gw db Running + return fmt.Sprintf("%s-%s-.*\\s+%s\\s+Pending\\s+", project, service, service) + } + res := c.RunDockerCmd("compose", "ps", "-p", projectName) + testify.Regexp(t, getServiceRegx(projectName, "db"), res.Stdout()) + testify.Regexp(t, getServiceRegx(projectName, "words"), res.Stdout()) + testify.Regexp(t, getServiceRegx(projectName, "web"), res.Stdout()) + }) + t.Run("check running project", func(t *testing.T) { // Docker Desktop kube cluster automatically exposes ports on the host, this is not the case with kind on Desktop, //we need to connect to the clusterIP, from the kind container From 15c0b883fef33963316c5e4987d27ab962f12b26 Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Fri, 5 Feb 2021 09:36:37 +0100 Subject: [PATCH 3/4] hide non running containers if no --all option Signed-off-by: Guillaume Tardif --- kube/client/client.go | 13 ++++++++----- kube/e2e/compose_test.go | 21 ++++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/kube/client/client.go b/kube/client/client.go index 1cdf8821..cdb0cb01 100644 --- a/kube/client/client.go +++ b/kube/client/client.go @@ -20,7 +20,6 @@ package client import ( "context" - "encoding/json" "fmt" v1 "k8s.io/api/core/v1" @@ -54,14 +53,18 @@ func NewKubeClient(config genericclioptions.RESTClientGetter) (*KubeClient, erro // GetContainers get containers for a given compose project func (kc KubeClient) GetContainers(ctx context.Context, projectName string, all bool) ([]compose.ContainerSummary, error) { - pods, err := kc.client.CoreV1().Pods("").List(ctx, metav1.ListOptions{LabelSelector: fmt.Sprintf("%s=%s", compose.ProjectTag, projectName)}) + fieldSelector := "" + if !all { + fieldSelector = "status.phase=Running" + } + pods, err := kc.client.CoreV1().Pods("").List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", compose.ProjectTag, projectName), + FieldSelector: fieldSelector, + }) if err != nil { return nil, err } - json, _ := json.MarshalIndent(pods, "", " ") - fmt.Println(string(json)) - fmt.Printf("containers: %d\n", len(pods.Items)) result := []compose.ContainerSummary{} for _, pod := range pods.Items { result = append(result, podToContainerSummary(pod)) diff --git a/kube/e2e/compose_test.go b/kube/e2e/compose_test.go index 0813f5a3..1960c1ab 100644 --- a/kube/e2e/compose_test.go +++ b/kube/e2e/compose_test.go @@ -79,16 +79,23 @@ func TestComposeUp(t *testing.T) { res.Assert(t, icmd.Expected{Out: `[{"Name":"compose-kube-demo","Status":"deployed"}]`}) }) - t.Run("compose ps", func(t *testing.T) { - getServiceRegx := func(project string, service string) string { + t.Run("compose ps --all", func(t *testing.T) { + getServiceRegx := func(service string) string { // match output with random hash / spaces like: - // myproject-db-698f4dd798-jd9gw db Running - return fmt.Sprintf("%s-%s-.*\\s+%s\\s+Pending\\s+", project, service, service) + // db-698f4dd798-jd9gw db Running + return fmt.Sprintf("%s-.*\\s+%s\\s+Pending\\s+", service, service) } + res := c.RunDockerCmd("compose", "ps", "-p", projectName, "--all") + testify.Regexp(t, getServiceRegx("db"), res.Stdout()) + testify.Regexp(t, getServiceRegx("words"), res.Stdout()) + testify.Regexp(t, getServiceRegx("web"), res.Stdout()) + + assert.Equal(t, len(Lines(res.Stdout())), 4, res.Stdout()) + }) + + t.Run("compose ps hides non running containers", func(t *testing.T) { res := c.RunDockerCmd("compose", "ps", "-p", projectName) - testify.Regexp(t, getServiceRegx(projectName, "db"), res.Stdout()) - testify.Regexp(t, getServiceRegx(projectName, "words"), res.Stdout()) - testify.Regexp(t, getServiceRegx(projectName, "web"), res.Stdout()) + assert.Equal(t, len(Lines(res.Stdout())), 1, res.Stdout()) }) t.Run("check running project", func(t *testing.T) { From 677bfc9ce5f5f849f082e91fc8b07ec0e6ba2dd6 Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Fri, 5 Feb 2021 11:09:06 +0100 Subject: [PATCH 4/4] Set kube namespace properly Signed-off-by: Guillaume Tardif --- kube/client/client.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/kube/client/client.go b/kube/client/client.go index cdb0cb01..8eddc272 100644 --- a/kube/client/client.go +++ b/kube/client/client.go @@ -32,7 +32,8 @@ import ( // KubeClient API to access kube objects type KubeClient struct { - client *kubernetes.Clientset + client *kubernetes.Clientset + namespace string } // NewKubeClient new kubernetes client @@ -46,8 +47,15 @@ func NewKubeClient(config genericclioptions.RESTClientGetter) (*KubeClient, erro if err != nil { return nil, err } + + namespace, _, err := config.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil, err + } + return &KubeClient{ - client: clientset, + client: clientset, + namespace: namespace, }, nil } @@ -58,7 +66,7 @@ func (kc KubeClient) GetContainers(ctx context.Context, projectName string, all fieldSelector = "status.phase=Running" } - pods, err := kc.client.CoreV1().Pods("").List(ctx, metav1.ListOptions{ + pods, err := kc.client.CoreV1().Pods(kc.namespace).List(ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=%s", compose.ProjectTag, projectName), FieldSelector: fieldSelector, })