Skip to content

Commit

Permalink
Introduce glob logic for pod-exec
Browse files Browse the repository at this point in the history
Allow the usage of globs like `*` in the pod-exec command to specify
that `havener` should look-up potential targets to match them against
the user provided target.

Refactor `ListNamespaces` to be a pointer receiver function.

Introduce context at main havener handle to use just one context.

Introduce concurrency when looking up pods in multiple namespaces.
  • Loading branch information
HeavyWombat committed May 14, 2024
1 parent 38506de commit 60cad27
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 93 deletions.
2 changes: 1 addition & 1 deletion internal/cmd/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func init() {
}

func retrieveClusterEvents(hvnr havener.Havener) error {
namespaces, err := havener.ListNamespaces(hvnr.Client())
namespaces, err := hvnr.ListNamespaces()
if err != nil {
return fmt.Errorf("failed to get a list of namespaces: %w", err)
}
Expand Down
164 changes: 93 additions & 71 deletions internal/cmd/pexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,29 @@
package cmd

import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/gonvenience/term"
"github.com/homeport/havener/pkg/havener"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/client-go/kubernetes"
)

const podDefaultCommand = "/bin/sh"

type target struct {
namespace string
podName string
containerName string
}

var (
podExecNoTty bool
podExecBlock bool
Expand Down Expand Up @@ -103,20 +107,20 @@ func execInClusterPods(hvnr havener.Havener, args []string) error {
switch {
case len(args) >= 2: // pod and command is given
input, command = args[0], args[1:]
podMap, err = lookupPodsByName(hvnr.Client(), input)
podMap, err = lookupPodsByName(hvnr, input)
if err != nil {
return err
}

case len(args) == 1: // only pod is given
input, command = args[0], []string{podDefaultCommand}
podMap, err = lookupPodsByName(hvnr.Client(), input)
podMap, err = lookupPodsByName(hvnr, input)
if err != nil {
return err
}

default:
return availablePodsError(hvnr.Client(), "no pod name specified")
return availablePodsError(hvnr, "no pod name specified")
}

// Count number of containers from all pods
Expand Down Expand Up @@ -209,111 +213,129 @@ func execInClusterPods(hvnr havener.Havener, args []string) error {
return nil
}

func lookupPodContainers(client kubernetes.Interface, p *corev1.Pod) (containerList []string, err error) {
for _, c := range p.Spec.Containers {
containerList = append(containerList, c.Name)
func containerNames(pod *corev1.Pod) []string {
var result []string
for _, container := range pod.Spec.Containers {
result = append(result, container.Name)
}
return containerList, err

return result
}

func (t target) String() string {
return fmt.Sprintf("%s/%s/%s",
t.namespace,
t.podName,
t.containerName,
)
}

func lookupAllPods(client kubernetes.Interface, namespaces []string) (map[*corev1.Pod][]string, error) {
var podLists = make(map[*corev1.Pod][]string)
for _, namespace := range namespaces {
podsPerNs, err := client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
func lookupPodsByName(h havener.Havener, input string) (map[*corev1.Pod][]string, error) {
var targets = map[*corev1.Pod][]string{}

// In case special term `all` is used, immediately return the full list of all pod containers
if input == "all" {
list, err := h.ListPods()
if err != nil {
return nil, err
}

for i := range podsPerNs.Items {
listOfContainers, err := lookupPodContainers(client, &(podsPerNs.Items[i]))
if err != nil {
return nil, err
}
podLists[&(podsPerNs.Items[i])] = listOfContainers
for _, pod := range list {
targets[pod] = containerNames(pod)
}
}
return podLists, nil
}

func lookupPodsByName(client kubernetes.Interface, input string) (map[*corev1.Pod][]string, error) {
inputList := strings.Split(input, ",")
return targets, nil
}

podList := make(map[*corev1.Pod][]string, len(inputList))
for _, podName := range inputList {
splited := strings.Split(podName, "/")
var keys, candidates []target
var lookUp = map[target]*corev1.Pod{}

for _, str := range strings.Split(input, ",") {
var splited = strings.Split(str, "/")
switch len(splited) {
case 1: // only the pod name is given
namespaces, err := havener.ListNamespaces(client)
if err != nil {
namespace, podName, containerName := "*", splited[0], "*"
candidates = append(candidates, target{namespace, podName, containerName})
if err := updateLookUps(h, &keys, lookUp, namespace); err != nil {
return nil, err
}

if input == "all" {
return lookupAllPods(client, namespaces)
case 2: // namespace, and pod name is given
namespace, podName, containerName := splited[0], splited[1], "*"
candidates = append(candidates, target{namespace, podName, containerName})
if err := updateLookUps(h, &keys, lookUp, namespace); err != nil {
return nil, err
}

pods := []*corev1.Pod{}
for _, namespace := range namespaces {
if pod, err := client.CoreV1().Pods(namespace).Get(context.TODO(), input, metav1.GetOptions{}); err == nil {
pods = append(pods, pod)
}
case 3: // namespace, pod, and container name is given
namespace, podName, containerName := splited[0], splited[1], splited[2]
candidates = append(candidates, target{namespace, podName, containerName})
if err := updateLookUps(h, &keys, lookUp, namespace); err != nil {
return nil, err
}

switch {
case len(pods) < 1:
return nil, availablePodsError(client, fmt.Sprintf("unable to find a pod named %s", input))
default:
return nil, fmt.Errorf("unsupported naming schema, it needs to be [namespace/]pod[/container]")
}
}

case len(pods) > 1:
return nil, fmt.Errorf("more than one pod named %s found, please specify a namespace", input)
for _, candidate := range candidates {
for _, key := range keys {
match, err := filepath.Match(candidate.String(), key.String())
if err != nil {
return nil, err
}

podList[pods[0]] = []string{pods[0].Spec.Containers[0].Name}

case 2: // namespace, and pod name is given
namespace, podName := splited[0], splited[1]
pod, err := client.CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{})
if err != nil {
return nil, availablePodsError(client, fmt.Sprintf("pod %s not found", input))
if match {
pod := lookUp[key]
targets[pod] = append(targets[pod], key.containerName)
}
}
}

podList[pod] = []string{pod.Spec.Containers[0].Name}
return targets, nil
}

case 3: // namespace, pod, and container name is given
namespace, podName, container := splited[0], splited[1], splited[2]
pod, err := client.CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{})
if err != nil {
return nil, availablePodsError(client, fmt.Sprintf("pod %s not found", input))
}
func updateLookUps(h havener.Havener, keys *[]target, lookUp map[target]*corev1.Pod, namespace string) error {
var namespaces []string
if namespace != "*" {
namespaces = append(namespaces, namespace)
}

podList[pod] = []string{container}
list, err := h.ListPods(namespaces...)
if err != nil {
return err
}

default:
return nil, fmt.Errorf("unsupported naming schema, it needs to be [namespace/]pod[/container]")
for i, pod := range list {
for _, containerName := range containerNames(pod) {
key := target{pod.Namespace, pod.Name, containerName}
*keys = append(*keys, key)
lookUp[key] = list[i]
}
}

return podList, nil
return nil
}

func availablePodsError(client kubernetes.Interface, title string) error {
pods, err := havener.ListPods(client)
func availablePodsError(h havener.Havener, format string, a ...any) error {
pods, err := h.ListPods()
if err != nil {
return fmt.Errorf("failed to list all pods in cluster: %w", err)
}
podList := []string{}

var targets []string
for _, pod := range pods {
for i := range pod.Spec.Containers {
podList = append(podList, fmt.Sprintf("%s/%s/%s",
pod.ObjectMeta.Namespace,
pod.Name,
pod.Spec.Containers[i].Name,
))
for _, container := range pod.Spec.Containers {
target := target{pod.Namespace, pod.Name, container.Name}
targets = append(targets, target.String())
}
}

return fmt.Errorf("%s: %w",
title,
fmt.Errorf("> Usage:\npod-exec [flags] <pod> <command>\n> List of available pods:\n%s", strings.Join(podList, "\n")),
fmt.Sprintf(format, a...),
fmt.Errorf("List of available pods:\n%s",
strings.Join(targets, "\n"),
),
)
}
16 changes: 16 additions & 0 deletions pkg/havener/havener.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"fmt"
"io"
"os"
"strconv"
"time"

"golang.org/x/sync/syncmap"
Expand All @@ -52,6 +53,16 @@ import (

var m = new(syncmap.Map)

var concurrency = func() int {
if input, ok := os.LookupEnv("HAVENER_CONCURRENCY"); ok {
if val, err := strconv.Atoi(input); err == nil && val > 0 {
return val
}
}

return 5 // default
}()

// AddShutdownFunction adds a function to be called in case GracefulShutdown is
// called, for example to clean up resources.
func AddShutdownFunction(f func()) {
Expand Down Expand Up @@ -83,10 +94,12 @@ type Hvnr struct {
// Havener is an interface to work with a cluster through the havener
// abstraction layer
type Havener interface {
Context() context.Context
Client() kubernetes.Interface
RESTConfig() *rest.Config
ClusterName() string

ListNamespaces() ([]string, error)
ListPods(namespaces ...string) ([]*corev1.Pod, error)
ListNodes() ([]corev1.Node, error)
ListSecrets(namespaces ...string) ([]*corev1.Secret, error)
Expand Down Expand Up @@ -171,6 +184,9 @@ func NewHavener(opts ...Option) (hvnr *Hvnr, err error) {
return hvnr, nil
}

// Context returns the context of the Havener handle
func (h *Hvnr) Context() context.Context { return h.ctx }

// ClusterName returns the name of the currently configured cluster
func (h *Hvnr) ClusterName() string {
return h.clusterName
Expand Down
Loading

0 comments on commit 60cad27

Please sign in to comment.