Skip to content

Commit

Permalink
Add builtin func to exec into K8s pods (#272)
Browse files Browse the repository at this point in the history
* Add  builtin func to exec into K8s pods

* format code

---------

Co-authored-by: Atanas Todorov <[email protected]>
  • Loading branch information
nasioman and Atanas Todorov authored Jan 15, 2025
1 parent 792a2bd commit 8c4dc9d
Show file tree
Hide file tree
Showing 11 changed files with 447 additions and 6 deletions.
37 changes: 31 additions & 6 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,41 @@ This configuration function declares and stores configuration needed to connect
#### Output
`kube_config()` returns a struct with the following fields.

| Field | Description |
| --------| --------- |
| `path` | The path to the local Kubernetes config that was set |
| `cluster_context` | The name of a context that was set for the cluster |
| `capi_provider`|A provider that was set for Cluster-API usage|

#### Example
```python
kube_config(path=args.kube_conf, cluster_context="my-cluster")
```

### `kube_exec()`
This function executes an arbitrary command inside a K8s pod

#### Parameters

| Param | Description | Required |
|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
| `namespace` | Namespace of the target pod. The default value is 'default'. | No |
| `pod` | The name of the target pod. | Yes |
| `container` | Container name. If omitted, use the kubectl.kubernetes.io/default-container annotation for selecting the container to be attached or the first container in the pod will be chosen. | No |
| `cmd` | The command to be executed inside the container. | Yes |
| `workdir` | A parent directory where the result file from the executed command will be saved. Defaults to `crashd_config.workdir` or if `crashd_config.workdir` doesn't exist, it defaults to `/tmp/crashd` | No |
| `kube_config` | A struct with Kubernetes configuration.If not provided defaults to Kubernetes config returned by `kube_config() | No |
| `timeout_in_seconds`| The maximum duration (in seconds) to wait for the command to complete. If not specified, the default is 120 seconds. | No |
| `output_file` | The file (relative to the working directory) where the command output will be streamed. If not specified, the output is appended /workdir/<pod-name>.out | No |


#### Output
`kube_exec()` returns a struct with the following fields.

| Field | Description |
|-----------------|------------------------------------------------------------|
| `file` | The path to a file where the command result was redirected |
| `error` | An error message if one was encountered |

#### Example
```python
kube_exec(pod="nginx", output_file="nginx_version.txt",container="nginx", cmd=["nginx", "-v"])
```

### `ssh_config()`
This function creates configuration that can be used to connect via SSH to remote machines.

Expand Down
14 changes: 14 additions & 0 deletions examples/kube_exec.crsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
work_dir = args.workdir if hasattr(args, "workdir") else fail("Error: workdir argument is required but not provided.")
conf = crashd_config(workdir=work_dir)
kube_config_path = args.kubeconfig
set_defaults(kube_config(path=kube_config_path))

# Exec into pod and run a long-running command. The command timeout period is controlled via timeout_in_seconds
#Output is appended in file under work_dir/<pod name>.out
kube_exec(namespace=args.namespace,pod="nginx", timeout_in_seconds=3, cmd=["sh", "-c" ,"while true; do echo 'Running'; sleep 1; done"])

# Exec into pod and run short-lived command. The output will be appended in work_dir/<pod name>.out
kube_exec(pod="nginx", cmd=["ls"])

# Exec into pod and run short-lived command. The output will be stored into file: work_dir/nginx_version.txt
kube_exec(pod="nginx", output_file="nginx_version.txt",container="nginx", cmd=["nginx", "-v"])
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
Expand Down Expand Up @@ -52,6 +54,9 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/Z
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
Expand All @@ -74,6 +79,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand All @@ -83,6 +90,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
Expand Down
109 changes: 109 additions & 0 deletions k8s/executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2019 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package k8s

import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/remotecommand"
"os"
"time"
)

// Executor is a struct that facilitates the execution of commands in Kubernetes pods.
// It uses the SPDYExecutor to stream command
type Executor struct {
Executor remotecommand.Executor
}

type ExecOptions struct {
Namespace string
Command []string
Podname string
ContainerName string
Config *Config
Timeout time.Duration
}

func NewExecutor(kubeconfig string, clusterCtxName string, opts ExecOptions) (*Executor, error) {
restCfg, err := restConfig(kubeconfig, clusterCtxName)
if err != nil {
return nil, err
}
setCoreDefaultConfig(restCfg)
restc, err := rest.RESTClientFor(restCfg)
if err != nil {
return nil, err
}

request := restc.Post().
Namespace(opts.Namespace).
Resource("pods").
Name(opts.Podname).
SubResource("exec").
VersionedParams(&corev1.PodExecOptions{
Container: opts.ContainerName,
Command: opts.Command,
Stdout: true,
Stderr: true,
TTY: false,
}, scheme.ParameterCodec)
executor, err := remotecommand.NewSPDYExecutor(restCfg, "POST", request.URL())
if err != nil {
return nil, err

}
return &Executor{Executor: executor}, nil
}

// makeRESTConfig creates a new *rest.Config with a k8s context name if one is provided.
func restConfig(fileName, contextName string) (*rest.Config, error) {
if fileName == "" {
return nil, fmt.Errorf("kubeconfig file path required")
}

if contextName != "" {
// create the config object from k8s config path and context
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: fileName},
&clientcmd.ConfigOverrides{
CurrentContext: contextName,
}).ClientConfig()
}

return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: fileName},
&clientcmd.ConfigOverrides{},
).ClientConfig()
}

// ExecCommand executes a command inside a specified Kubernetes pod using the SPDYExecutor.
func (k8sc *Executor) ExecCommand(ctx context.Context, outputFilePath string, execOptions ExecOptions) error {
ctx, cancel := context.WithTimeout(ctx, execOptions.Timeout)
defer cancel()

file, err := os.OpenFile(outputFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("error creating output file: %v", err)
}
defer file.Close()

// Execute the command and stream the stdout and stderr to the file. Some commands are using stderr.
err = k8sc.Executor.StreamWithContext(ctx, remotecommand.StreamOptions{
Stdout: file,
Stderr: file,
})
if err != nil {
if err == context.DeadlineExceeded {
return fmt.Errorf("command execution timed out. command:%s", execOptions.Command)
}
return err
}

return nil
}
96 changes: 96 additions & 0 deletions starlark/kube_exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) 2020 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package starlark

import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/vmware-tanzu/crash-diagnostics/k8s"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
"path/filepath"
"time"
)

// KubeExecFn is a starlark built-in for executing command in target K8s pods
func KubeExecFn(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var namespace, pod, container, workdir, outputfile string
var timeout int
var command *starlark.List
var kubeConfig *starlarkstruct.Struct

if err := starlark.UnpackArgs(
identifiers.kubeExec, args, kwargs,
"namespace?", &namespace,
"pod", &pod,
"container?", &container,
"cmd", &command,
"workdir?", &workdir,
"output_file?", &outputfile,
"kube_config?", &kubeConfig,
"timeout_in_seconds?", &timeout,
); err != nil {
return starlark.None, errors.Wrap(err, "failed to read args")
}

if namespace == "" {
namespace = "default"
}
if timeout == 0 {
//Default timeout if not specified is 2 Minutes
timeout = 120
}

if len(workdir) == 0 {
//Defaults to crashd_config.workdir or /tmp/crashd
if dir, err := getWorkdirFromThread(thread); err == nil {
workdir = dir
}
}

ctx, ok := thread.Local(identifiers.scriptCtx).(context.Context)
if !ok || ctx == nil {
return starlark.None, fmt.Errorf("script context not found")
}

if kubeConfig == nil {
kubeConfig = thread.Local(identifiers.kubeCfg).(*starlarkstruct.Struct)
}
path, err := getKubeConfigPathFromStruct(kubeConfig)
if err != nil {
return starlark.None, errors.Wrap(err, "failed to get kubeconfig")
}
clusterCtxName := getKubeConfigContextNameFromStruct(kubeConfig)

execOpts := k8s.ExecOptions{
Namespace: namespace,
Podname: pod,
ContainerName: container,
Command: toSlice(command),
Timeout: time.Duration(timeout) * time.Second,
}
executor, err := k8s.NewExecutor(path, clusterCtxName, execOpts)
if err != nil {
return starlark.None, errors.Wrap(err, "could not initialize search client")
}

outputFilePath := filepath.Join(trimQuotes(workdir), outputfile)
if outputfile == "" {
outputFilePath = filepath.Join(trimQuotes(workdir), pod+".out")
}
err = executor.ExecCommand(ctx, outputFilePath, execOpts)

return starlarkstruct.FromStringDict(
starlark.String(identifiers.kubeCapture),
starlark.StringDict{
"file": starlark.String(outputFilePath),
"error": func() starlark.String {
if err != nil {
return starlark.String(err.Error())
}
return ""
}(),
}), nil
}
Loading

0 comments on commit 8c4dc9d

Please sign in to comment.