Skip to content

Commit

Permalink
Adds a workload status command
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
meyskens committed Dec 20, 2024
1 parent 4ae7e8e commit f911412
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 4 deletions.
174 changes: 172 additions & 2 deletions cmd/cofidectl/cmd/workload/workload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
)
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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.
`
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/trustprovider/trustprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/workload/workload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down

0 comments on commit f911412

Please sign in to comment.