-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add builtin func to exec into K8s pods (#272)
* Add builtin func to exec into K8s pods * format code --------- Co-authored-by: Atanas Todorov <[email protected]>
- Loading branch information
Showing
11 changed files
with
447 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.