diff --git a/kube/client/client.go b/kube/client/client.go new file mode 100644 index 00000000..8eddc272 --- /dev/null +++ b/kube/client/client.go @@ -0,0 +1,92 @@ +// +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 ( + "context" + "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 + namespace string +} + +// NewKubeClient new kubernetes client +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 + } + + namespace, _, err := config.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil, err + } + + return &KubeClient{ + client: clientset, + namespace: namespace, + }, nil +} + +// GetContainers get containers for a given compose project +func (kc KubeClient) GetContainers(ctx context.Context, projectName string, all bool) ([]compose.ContainerSummary, error) { + fieldSelector := "" + if !all { + fieldSelector = "status.phase=Running" + } + + pods, err := kc.client.CoreV1().Pods(kc.namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", compose.ProjectTag, projectName), + FieldSelector: fieldSelector, + }) + if err != nil { + return nil, err + } + 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 c648950c..1d80aca5 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 @@ -55,8 +57,14 @@ func NewComposeService(ctx context.Context) (compose.Service, error) { if err != nil { return nil, err } + apiClient, err := client.NewKubeClient(config) + if err != nil { + return nil, err + } + return &composeService{ - sdk: actions, + sdk: actions, + client: apiClient, }, nil } @@ -151,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..1960c1ab 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,25 @@ func TestComposeUp(t *testing.T) { res.Assert(t, icmd.Expected{Out: `[{"Name":"compose-kube-demo","Status":"deployed"}]`}) }) + t.Run("compose ps --all", func(t *testing.T) { + getServiceRegx := func(service string) string { + // match output with random hash / spaces like: + // 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) + assert.Equal(t, len(Lines(res.Stdout())), 1, 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