Skip to content

Commit

Permalink
feat: support bi-directional container management
Browse files Browse the repository at this point in the history
  • Loading branch information
deviantony committed Oct 26, 2023
1 parent 5ca3f90 commit 2c21479
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 75 deletions.
4 changes: 4 additions & 0 deletions internal/adapter/container_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,3 +498,7 @@ func (adapter *KubeDockerAdapter) DeployPortainerEdgeAgent(ctx context.Context,

return nil
}

func isContainerInNamespace(container *types.Container, namespace string) bool {
return namespace == "" || container.Labels[k2dtypes.NamespaceNameLabelKey] == namespace
}
4 changes: 4 additions & 0 deletions internal/adapter/namespace_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func isDefaultOrEmptyNamespace(namespace string) bool {
return namespace == "" || namespace == "default"
}

// provisionNamespace provisions a Kubernetes namespace and its corresponding Docker network.
//
// The function performs the following steps:
Expand Down
95 changes: 21 additions & 74 deletions internal/adapter/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@ import (
"io"

"github.com/docker/docker/api/types"
"github.com/portainer/k2d/internal/adapter/errors"
"github.com/portainer/k2d/internal/adapter/filters"
"github.com/portainer/k2d/internal/adapter/naming"
k2dtypes "github.com/portainer/k2d/internal/adapter/types"
"github.com/portainer/k2d/internal/k8s"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apis/core"
)

type PodLogOptions struct {
Expand Down Expand Up @@ -44,31 +39,29 @@ func (adapter *KubeDockerAdapter) CreateContainerFromPod(ctx context.Context, po
return adapter.createContainerFromPodSpec(ctx, opts)
}

func (adapter *KubeDockerAdapter) DeletePod(ctx context.Context, podName string, namespace string) error {
container, err := adapter.findContainerFromPodAndNamespace(ctx, podName, namespace)
if err != nil {
return fmt.Errorf("unable to find container associated to the pod %s/%s: %w", namespace, podName, err)
}

err = adapter.cli.ContainerRemove(ctx, container.Names[0], types.ContainerRemoveOptions{Force: true})
if err != nil {
adapter.logger.Warnf("unable to remove container: %s", err)
}

return nil
}

// The GetPod implementation is using a filtered list approach as the Docker API provide different response types
// when inspecting a container and listing containers.
// The logic used to build a pod from a container is based on the type returned by the list operation (types.Container)
// and not the inspect operation (types.ContainerJSON).
// This is because using the inspect operation everywhere would be more expensive overall.
func (adapter *KubeDockerAdapter) GetPod(ctx context.Context, podName string, namespace string) (*corev1.Pod, error) {
filter := filters.ByPod(namespace, podName)
containers, err := adapter.cli.ContainerList(ctx, types.ContainerListOptions{All: true, Filters: filter})
container, err := adapter.findContainerFromPodAndNamespace(ctx, podName, namespace)
if err != nil {
return nil, fmt.Errorf("unable to list containers: %w", err)
}

var container *types.Container

containerName := naming.BuildContainerName(podName, namespace)
for _, cntr := range containers {
if cntr.Names[0] == "/"+containerName {
container = &cntr
break
}
}

if container == nil {
adapter.logger.Errorf("unable to find container for pod %s in namespace %s", podName, namespace)
return nil, errors.ErrResourceNotFound
return nil, fmt.Errorf("unable to find container associated to the pod %s/%s: %w", namespace, podName, err)
}

pod, err := adapter.buildPodFromContainer(*container)
Expand All @@ -83,7 +76,7 @@ func (adapter *KubeDockerAdapter) GetPod(ctx context.Context, podName string, na
},
}

err = adapter.ConvertK8SResource(pod, &versionedPod)
err = adapter.ConvertK8SResource(&pod, &versionedPod)
if err != nil {
return nil, fmt.Errorf("unable to convert internal object to versioned object: %w", err)
}
Expand All @@ -92,10 +85,9 @@ func (adapter *KubeDockerAdapter) GetPod(ctx context.Context, podName string, na
}

func (adapter *KubeDockerAdapter) GetPodLogs(ctx context.Context, namespace string, podName string, opts PodLogOptions) (io.ReadCloser, error) {
containerName := naming.BuildContainerName(podName, namespace)
container, err := adapter.cli.ContainerInspect(ctx, containerName)
container, err := adapter.findContainerFromPodAndNamespace(ctx, podName, namespace)
if err != nil {
return nil, fmt.Errorf("unable to inspect container: %w", err)
return nil, fmt.Errorf("unable to find container associated to the pod %s/%s: %w", namespace, podName, err)
}

return adapter.cli.ContainerLogs(ctx, container.ID, types.ContainerLogsOptions{
Expand All @@ -108,7 +100,7 @@ func (adapter *KubeDockerAdapter) GetPodLogs(ctx context.Context, namespace stri
}

func (adapter *KubeDockerAdapter) GetPodTable(ctx context.Context, namespace string) (*metav1.Table, error) {
podList, err := adapter.listPods(ctx, namespace)
podList, err := adapter.getPodListFromContainers(ctx, namespace)
if err != nil {
return &metav1.Table{}, fmt.Errorf("unable to list pods: %w", err)
}
Expand All @@ -117,7 +109,7 @@ func (adapter *KubeDockerAdapter) GetPodTable(ctx context.Context, namespace str
}

func (adapter *KubeDockerAdapter) ListPods(ctx context.Context, namespace string) (corev1.PodList, error) {
podList, err := adapter.listPods(ctx, namespace)
podList, err := adapter.getPodListFromContainers(ctx, namespace)
if err != nil {
return corev1.PodList{}, fmt.Errorf("unable to list pods: %w", err)
}
Expand All @@ -136,48 +128,3 @@ func (adapter *KubeDockerAdapter) ListPods(ctx context.Context, namespace string

return versionedPodList, nil
}

func (adapter *KubeDockerAdapter) buildPodFromContainer(container types.Container) (*core.Pod, error) {
pod := adapter.converter.ConvertContainerToPod(container)

if container.Labels[k2dtypes.PodLastAppliedConfigLabelKey] != "" {
internalPodSpecData := container.Labels[k2dtypes.PodLastAppliedConfigLabelKey]
podSpec := core.PodSpec{}

err := json.Unmarshal([]byte(internalPodSpecData), &podSpec)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal pod spec: %w", err)
}

pod.Spec = podSpec
}

return &pod, nil
}

func (adapter *KubeDockerAdapter) listPods(ctx context.Context, namespace string) (core.PodList, error) {
filter := filters.ByNamespace(namespace)
containers, err := adapter.cli.ContainerList(ctx, types.ContainerListOptions{All: true, Filters: filter})
if err != nil {
return core.PodList{}, fmt.Errorf("unable to list containers: %w", err)
}

pods := []core.Pod{}

for _, container := range containers {
pod, err := adapter.buildPodFromContainer(container)
if err != nil {
return core.PodList{}, fmt.Errorf("unable to get pods: %w", err)
}

pods = append(pods, *pod)
}

return core.PodList{
TypeMeta: metav1.TypeMeta{
Kind: "PodList",
APIVersion: "v1",
},
Items: pods,
}, nil
}
184 changes: 184 additions & 0 deletions internal/adapter/pod_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package adapter

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/docker/docker/api/types"
"github.com/portainer/k2d/internal/adapter/errors"
"github.com/portainer/k2d/internal/adapter/filters"
"github.com/portainer/k2d/internal/adapter/naming"
k2dtypes "github.com/portainer/k2d/internal/adapter/types"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apis/core"
)

// buildPodFromContainer converts a Docker container into a Kubernetes Pod object.
// The function leverages an internal converter to map the basic attributes of a container
// to a Pod. Additionally, it attempts to extract the last-applied PodSpec configuration
// (if available) from the container labels and sets it to the Pod's Spec field.
//
// Parameters:
// - container: The Docker container that needs to be converted into a Pod.
//
// Returns:
// - core.Pod: The converted Pod object.
// - error: An error object if any error occurs during the conversion.
func (adapter *KubeDockerAdapter) buildPodFromContainer(container types.Container) (core.Pod, error) {
pod := adapter.converter.ConvertContainerToPod(container)

if container.Labels[k2dtypes.PodLastAppliedConfigLabelKey] != "" {
internalPodSpecData := container.Labels[k2dtypes.PodLastAppliedConfigLabelKey]
podSpec := core.PodSpec{}

err := json.Unmarshal([]byte(internalPodSpecData), &podSpec)
if err != nil {
return core.Pod{}, fmt.Errorf("unable to unmarshal pod spec: %w", err)
}

pod.Spec = podSpec
}

return pod, nil
}

// findContainerFromPodAndNamespace searches for a Docker container based on a given Pod name and namespace.
// It lists all the containers and filters them based on the Pod and namespace information.
// If the namespace is neither 'default' nor empty, it adds specific filters to pinpoint the search.
//
// Parameters:
// - ctx: The context within which the function operates.
// - podName: The name of the Pod for which to find the container.
// - namespace: The Kubernetes namespace where the Pod resides.
//
// Returns:
// - *types.Container: A pointer to the matching Docker container.
// - error: An error object if the container is not found or any other error occurs.
func (adapter *KubeDockerAdapter) findContainerFromPodAndNamespace(ctx context.Context, podName string, namespace string) (*types.Container, error) {
var container *types.Container

listOptions := types.ContainerListOptions{All: true}
containerName := podName

if !isDefaultOrEmptyNamespace(namespace) {
listOptions.Filters = filters.ByPod(namespace, podName)
containerName = naming.BuildContainerName(podName, namespace)
}

containers, err := adapter.cli.ContainerList(ctx, listOptions)
if err != nil {
return nil, fmt.Errorf("unable to list containers: %w", err)
}

for _, cntr := range containers {
updateDefaultPodLabels(&cntr)

if cntr.Names[0] == "/"+containerName {
container = &cntr
break
}
}

if container == nil {
adapter.logger.Errorf("unable to find container for pod %s in namespace %s", podName, namespace)
return nil, errors.ErrResourceNotFound
}

return container, nil
}

// getPodListFromContainers is responsible for retrieving a list of Kubernetes Pod objects
// based on the Docker containers running in a specific namespace. The function performs the following steps:
//
// 1. Prepares Docker container listing options. If the namespace is neither 'default' nor empty,
// it adds a filter to only include containers that are part of the given Kubernetes namespace.
//
// 2. Calls the Docker API to list all containers that match the prepared listing options.
//
// 3. Invokes buildPodList to convert the list of Docker containers into a list of Kubernetes Pod objects.
// During this conversion, each container's metadata and spec are translated to the corresponding fields in a Pod object.
//
// 4. Returns a PodList object, which is a collection of the generated Pod objects, wrapped with metadata.
//
// Parameters:
// - ctx: The context within which the function should operate. This is used for timeouts and cancellations.
// - namespace: The Kubernetes namespace in which to look for Pods. An empty or 'default' namespace applies special handling.
//
// Returns:
// - core.PodList: A list of Kubernetes Pods encapsulated in a PodList object, along with Kubernetes metadata.
// - error: An error object which could contain various types of errors including API call failures, JSON unmarshalling errors, etc.
func (adapter *KubeDockerAdapter) getPodListFromContainers(ctx context.Context, namespace string) (core.PodList, error) {
listOptions := types.ContainerListOptions{All: true}
if !isDefaultOrEmptyNamespace(namespace) {
listOptions.Filters = filters.ByNamespace(namespace)
}

containers, err := adapter.cli.ContainerList(ctx, listOptions)
if err != nil {
return core.PodList{}, err
}

pods, err := adapter.buildPodList(containers, namespace)
if err != nil {
return core.PodList{}, err
}

return core.PodList{
TypeMeta: metav1.TypeMeta{
Kind: "PodList",
APIVersion: "v1",
},
Items: pods,
}, nil
}

// buildPodList is responsible for creating a list of Kubernetes Pod objects based on a given list of Docker containers.
// The function operates by filtering and converting Docker containers to Pods.
// If the specified namespace is neither 'default' nor empty, it will decorate existing containers with the namespace and workload labels if they are missing.
// This will allow support for containers that were created outside of k2d.
// Parameters:
// - containers: A list of Docker containers from which the Pods will be created.
// - namespace: The Kubernetes namespace to which the list of Pods should be restricted.
// If the namespace is empty or 'default', special label handling will be applied.
//
// Returns:
// - []core.Pod: A list of Kubernetes Pods constructed from the filtered list of Docker containers.
// - error: An error object that may contain information about any error occurring during the conversion process,
// such as issues in invoking the Docker API or converting the container attributes to Pod fields.
func (adapter *KubeDockerAdapter) buildPodList(containers []types.Container, namespace string) ([]core.Pod, error) {
var pods []core.Pod

for _, container := range containers {
if isDefaultOrEmptyNamespace(namespace) {
updateDefaultPodLabels(&container)
}

if !isContainerInNamespace(&container, namespace) {
continue
}

pod, err := adapter.buildPodFromContainer(container)
if err != nil {
return nil, fmt.Errorf("unable to get pods: %w", err)
}
pods = append(pods, pod)
}

return pods, nil
}

// updateDefaultPodLabels is a utility function that sets the default pod labels associated to a Docker container
// if they are not already set. This is used for containers that were created outside of k2d.
// It sets the namespace label to 'default' if it is missing,
// and extracts the workload name from the Docker container's name.
//
// Parameters:
// - container: A pointer to the Docker container whose labels need to be updated.
func updateDefaultPodLabels(container *types.Container) {
if _, exists := container.Labels[k2dtypes.NamespaceNameLabelKey]; !exists {
container.Labels[k2dtypes.NamespaceNameLabelKey] = "default"
container.Labels[k2dtypes.WorkloadNameLabelKey] = strings.TrimPrefix(container.Names[0], "/")
}
}
2 changes: 1 addition & 1 deletion internal/api/core/v1/pods/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func (svc PodService) DeletePod(r *restful.Request, w *restful.Response) {
namespace := utils.GetNamespaceFromRequest(r)

podName := r.PathParameter("name")
svc.adapter.DeleteContainer(r.Request.Context(), podName, namespace)
svc.adapter.DeletePod(r.Request.Context(), podName, namespace)

w.WriteAsJson(metav1.Status{
TypeMeta: metav1.TypeMeta{
Expand Down

0 comments on commit 2c21479

Please sign in to comment.