From f911412618d9f0931963e55fb06849969850878b Mon Sep 17 00:00:00 2001 From: Maartje Eyskens Date: Fri, 20 Dec 2024 11:11:09 +0100 Subject: [PATCH] Adds a workload status command Initially, this command will take a workload name, with pod name, namespace and trust-zone as flags, and use Maartje's "attest-me" implementation via a debug container to return human-readable cert information with the CLI. As a follow-up, I'd like to make it simpler to reference the workload (ideally a single argument) and for pod and cluster info be inferred, but that'll depend on how we handle 'workloads' and will need some additional thought. Signed-off-by: Maartje Eyskens --- cmd/cofidectl/cmd/workload/workload.go | 174 +++++++++++++++++++- internal/pkg/trustprovider/trustprovider.go | 2 +- internal/pkg/workload/workload.go | 2 +- 3 files changed, 174 insertions(+), 4 deletions(-) diff --git a/cmd/cofidectl/cmd/workload/workload.go b/cmd/cofidectl/cmd/workload/workload.go index 400594d..fd3ced2 100644 --- a/cmd/cofidectl/cmd/workload/workload.go +++ b/cmd/cofidectl/cmd/workload/workload.go @@ -4,16 +4,24 @@ package workload import ( + "bytes" "context" "fmt" + "io" "os" + "time" + "github.com/briandowns/spinner" trust_zone_proto "github.com/cofide/cofide-api-sdk/gen/go/proto/trust_zone/v1alpha1" "github.com/cofide/cofidectl/internal/pkg/workload" cmdcontext "github.com/cofide/cofidectl/pkg/cmd/context" + kubeutil "github.com/cofide/cofidectl/pkg/kube" "github.com/cofide/cofidectl/pkg/provider/helm" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" ) type WorkloadCommand struct { @@ -32,13 +40,14 @@ This command consists of multiple sub-commands to interact with workloads. func (c *WorkloadCommand) GetRootCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "workload list|discover [ARGS]", - Short: "List workloads in a trust zone or discover candidate workloads", + Use: "workload list|discover|status [ARGS]", + Short: "List or introspect the status of workloads in a trust zone or discover candidate workloads", Long: workloadRootCmdDesc, Args: cobra.NoArgs, } cmd.AddCommand( + c.GetStatusCommand(), c.GetListCommand(), c.GetDiscoverCommand(), ) @@ -108,6 +117,148 @@ func (w *WorkloadCommand) GetListCommand() *cobra.Command { return cmd } +var workloadStatusCmdDesc = ` +This command will display the status of workloads in the Cofide configuration state. +` + +type StatusOpts struct { + podName string + namespace string + trustZone string +} + +func (w *WorkloadCommand) GetStatusCommand() *cobra.Command { + opts := StatusOpts{} + cmd := &cobra.Command{ + Use: "status [NAME]", + Short: "Display workload status", + Long: workloadStatusCmdDesc, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + kubeConfig, err := cmd.Flags().GetString("kube-config") + if err != nil { + return fmt.Errorf("failed to retrieve the kubeconfig file location") + } + + return w.status(cmd.Context(), kubeConfig, opts) + }, + } + + f := cmd.Flags() + f.StringVar(&opts.podName, "pod-name", "", "Pod name for the workload") + f.StringVar(&opts.namespace, "namespace", "", "Namespace for the workload") + f.StringVar(&opts.trustZone, "trust-zone", "", "Trust zone for the workload") + + cobra.CheckErr(cmd.MarkFlagRequired("pod-name")) + cobra.CheckErr(cmd.MarkFlagRequired("namespace")) + cobra.CheckErr(cmd.MarkFlagRequired("trust-zone")) + + return cmd +} + +const debugContainerNamePrefix = "cofidectl-debug" +const debugContainerImage = "ghcr.io/cofide/cofidectl-debug-container/cmd:v0.1.0" + +func (w *WorkloadCommand) status(ctx context.Context, kubeConfig string, opts StatusOpts) error { + ds, err := w.cmdCtx.PluginManager.GetDataSource(ctx) + if err != nil { + return err + } + + trustZone, err := ds.GetTrustZone(opts.trustZone) + if err != nil { + return err + } + + client, err := kubeutil.NewKubeClientFromSpecifiedContext(kubeConfig, *trustZone.KubernetesContext) + if err != nil { + return err + } + + // Create a spinner to display whilst the debug container is created and executed and logs retrieved + s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) + s.Start() + defer s.Stop() + s.Suffix = "Starting debug container" + + pod, container, err := createDebugContainer(ctx, client, opts.podName, opts.namespace) + if err != nil { + return fmt.Errorf("could not create ephemeral debug container: %s", err) + } + + s.Suffix = "Retrieving workload status" + + workload, err := getWorkloadStatus(ctx, client, pod, container) + if err != nil { + return fmt.Errorf("could not retrieve logs of the ephemeral debug container: %w", err) + } + + fmt.Println(workload) + + return nil +} + +func createDebugContainer(ctx context.Context, client *kubeutil.Client, podName string, namespace string) (*corev1.Pod, string, error) { + pod, err := client.Clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + return nil, "", fmt.Errorf("error getting pod: %v", err) + } + + debugContainerName := fmt.Sprintf("%s-%s", debugContainerNamePrefix, rand.String(5)) + + debugContainer := corev1.EphemeralContainer{ + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: debugContainerName, + Image: debugContainerImage, + ImagePullPolicy: corev1.PullIfNotPresent, + TTY: true, + Stdin: true, + VolumeMounts: []corev1.VolumeMount{ + { + ReadOnly: true, + Name: "spiffe-workload-api", + MountPath: "/spiffe-workload-api", + }}, + }, + TargetContainerName: pod.Spec.Containers[0].Name, + } + + pod.Spec.EphemeralContainers = append(pod.Spec.EphemeralContainers, debugContainer) + + _, err = client.Clientset.CoreV1().Pods(namespace).UpdateEphemeralContainers( + ctx, + pod.Name, + pod, + metav1.UpdateOptions{}, + ) + if err != nil { + return nil, "", fmt.Errorf("error creating debug container: %v", err) + } + + // Wait for the debug container to complete + waitCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() + for { + pod, err := client.Clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + return nil, "", fmt.Errorf("error getting pod status: %v", err) + } + + for _, status := range pod.Status.EphemeralContainerStatuses { + if status.Name == debugContainerName && status.State.Terminated != nil { + return pod, debugContainerName, nil + } + } + + select { + case <-waitCtx.Done(): + return nil, "", fmt.Errorf("timeout waiting for debug container to complete") + default: + continue + } + } +} + func renderRegisteredWorkloads(ctx context.Context, kubeConfig string, trustZones []*trust_zone_proto.TrustZone) error { data := make([][]string, 0, len(trustZones)) @@ -144,6 +295,25 @@ func renderRegisteredWorkloads(ctx context.Context, kubeConfig string, trustZone return nil } +func getWorkloadStatus(ctx context.Context, client *kubeutil.Client, pod *corev1.Pod, container string) (string, error) { + logs, err := client.Clientset.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &corev1.PodLogOptions{ + Container: container, + }).Stream(ctx) + if err != nil { + return "", err + } + defer logs.Close() + + // Read the logs + buf := new(bytes.Buffer) + _, err = io.Copy(buf, logs) + if err != nil { + return "", err + } + + return buf.String(), nil +} + var workloadDiscoverCmdDesc = ` This command will discover all of the unregistered workloads. ` diff --git a/internal/pkg/trustprovider/trustprovider.go b/internal/pkg/trustprovider/trustprovider.go index 5c543ee..3a460a0 100644 --- a/internal/pkg/trustprovider/trustprovider.go +++ b/internal/pkg/trustprovider/trustprovider.go @@ -41,7 +41,7 @@ func (tp *TrustProvider) GetValues() error { WorkloadAttestorConfig: map[string]any{ "enabled": true, "skipKubeletVerification": true, - "disableContainerSelectors": false, + "disableContainerSelectors": true, "useNewContainerLocator": false, "verboseContainerLocatorLogs": false, }, diff --git a/internal/pkg/workload/workload.go b/internal/pkg/workload/workload.go index 1224ea3..9740a27 100644 --- a/internal/pkg/workload/workload.go +++ b/internal/pkg/workload/workload.go @@ -8,8 +8,8 @@ import ( "fmt" "time" - "github.com/cofide/cofidectl/pkg/spire" kubeutil "github.com/cofide/cofidectl/pkg/kube" + "github.com/cofide/cofidectl/pkg/spire" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" )