Skip to content

Commit

Permalink
adds workload status command - wip
Browse files Browse the repository at this point in the history
add initial implementation to inject a debug container and run it and attach to stdout

update debug container name in the pod

add workload as root command

mark flags as required

update implementation to use logs instead of exec

add spinner

remove the existing container check so it can be run again

remove the existing container check so it can be run again

generate a new unique debug container each time the cmd is used

add the spiffe workload API volume mount

disables container selectors

Signed-off-by: Maartje Eyskens <[email protected]>
  • Loading branch information
mattbates authored and meyskens committed Nov 16, 2024
1 parent 536cd1c commit ae18958
Showing 1 changed file with 192 additions and 2 deletions.
194 changes: 192 additions & 2 deletions cmd/cofidectl/cmd/workload/workload.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@
package workload

import (
"bytes"
"context"
"fmt"
"io"
"os"
"time"

trust_zone_proto "github.com/cofide/cofide-api-sdk/gen/go/proto/trust_zone/v1alpha1"
cmdcontext "github.com/cofide/cofidectl/cmd/cofidectl/cmd/context"
"github.com/cofide/cofidectl/internal/pkg/workload"
"github.com/olekukonko/tablewriter"

"github.com/briandowns/spinner"
kubeutil "github.com/cofide/cofidectl/internal/pkg/kube"
"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 @@ -29,6 +38,13 @@ var workloadRootCmdDesc = `
This command consists of multiple sub-commands to interact with workloads.
`

type Opts struct {
workloadName string // used to pass arg[0]
podName string
namespace string
trustZone string
}

func (c *WorkloadCommand) GetRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "workload list|discover [ARGS]",
Expand All @@ -40,6 +56,7 @@ func (c *WorkloadCommand) GetRootCommand() *cobra.Command {
cmd.AddCommand(
c.GetListCommand(),
c.GetDiscoverCommand(),
c.GetStatusCommand(),
)

return cmd
Expand Down Expand Up @@ -87,22 +104,73 @@ func (w *WorkloadCommand) GetListCommand() *cobra.Command {
return fmt.Errorf("no trust zones have been configured")
}

return nil
},
}

return cmd
}

var workloadStatusCmdDesc = `
This command will display the status of workloads in the Cofide configuration state.
`

func (w *WorkloadCommand) GetStatusCommand() *cobra.Command {
opts := Opts{}
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")
}

ds, err := w.cmdCtx.PluginManager.GetDataSource()
if err != nil {
return err
}

var trustZones []*trust_zone_proto.TrustZone

if opts.trustZone != "" {
trustZone, err := ds.GetTrustZone(opts.trustZone)
if err != nil {
return err
}

trustZones = append(trustZones, trustZone)
} else {
trustZones, err = ds.ListTrustZones()
if err != nil {
return err
}
}

if len(trustZones) == 0 {
return fmt.Errorf("no trust zones have been configured")
}

err = renderRegisteredWorkloads(cmd.Context(), kubeConfig, trustZones)
if err != nil {
return err
}

return nil
opts.workloadName = args[0]
return w.status(cmd.Context(), kubeConfig, opts)
},
}

f := cmd.Flags()
f.StringVar(&opts.trustZone, "trust-zone", "", "list the registered workloads in a specific trust zone")
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
}
Expand Down Expand Up @@ -137,6 +205,48 @@ func renderRegisteredWorkloads(ctx context.Context, kubeConfig string, trustZone
return nil
}

const debugContainerNamePrefix = "cofidectl-debug"
const debugContainerImage = "cofidectl-debug:latest"

func (w *WorkloadCommand) status(ctx context.Context, kubeConfig string, opts Opts) error {
ds, err := w.cmdCtx.PluginManager.GetDataSource()
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
}

var workloadDiscoverCmdDesc = `
This command will discover all of the unregistered workloads.
`
Expand Down Expand Up @@ -237,3 +347,83 @@ func renderUnregisteredWorkloads(ctx context.Context, kubeConfig string, trustZo

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 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
}

0 comments on commit ae18958

Please sign in to comment.