mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-20 10:20:51 +00:00
Merge pull request #96058 from verb/1.20-cli-debug-image-mutations
Add tests and set-image option to kubectl debug
This commit is contained in:
commit
1ba27096ae
@ -249,6 +249,7 @@ package_group(
|
|||||||
"//staging/src/k8s.io/kubectl/pkg/cmd/config",
|
"//staging/src/k8s.io/kubectl/pkg/cmd/config",
|
||||||
"//staging/src/k8s.io/kubectl/pkg/cmd/cp",
|
"//staging/src/k8s.io/kubectl/pkg/cmd/cp",
|
||||||
"//staging/src/k8s.io/kubectl/pkg/cmd/create",
|
"//staging/src/k8s.io/kubectl/pkg/cmd/create",
|
||||||
|
"//staging/src/k8s.io/kubectl/pkg/cmd/debug",
|
||||||
"//staging/src/k8s.io/kubectl/pkg/cmd/delete",
|
"//staging/src/k8s.io/kubectl/pkg/cmd/delete",
|
||||||
"//staging/src/k8s.io/kubectl/pkg/cmd/describe",
|
"//staging/src/k8s.io/kubectl/pkg/cmd/describe",
|
||||||
"//staging/src/k8s.io/kubectl/pkg/cmd/drain",
|
"//staging/src/k8s.io/kubectl/pkg/cmd/drain",
|
||||||
|
@ -58,7 +58,10 @@ go_test(
|
|||||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library",
|
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library",
|
||||||
|
"//staging/src/k8s.io/kubectl/pkg/cmd/testing:go_default_library",
|
||||||
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
|
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
|
||||||
|
"//vendor/github.com/google/go-cmp/cmp/cmpopts:go_default_library",
|
||||||
|
"//vendor/github.com/spf13/cobra:go_default_library",
|
||||||
"//vendor/k8s.io/utils/pointer:go_default_library",
|
"//vendor/k8s.io/utils/pointer:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -55,7 +55,7 @@ var (
|
|||||||
Debug cluster resources using interactive debugging containers.
|
Debug cluster resources using interactive debugging containers.
|
||||||
|
|
||||||
'debug' provides automation for common debugging tasks for cluster objects identified by
|
'debug' provides automation for common debugging tasks for cluster objects identified by
|
||||||
resource and name. Pods will be used by default if resource is not specified.
|
resource and name. Pods will be used by default if no resource is specified.
|
||||||
|
|
||||||
The action taken by 'debug' varies depending on what resource is specified. Supported
|
The action taken by 'debug' varies depending on what resource is specified. Supported
|
||||||
actions include:
|
actions include:
|
||||||
@ -66,8 +66,7 @@ var (
|
|||||||
debugging utilities without restarting the pod.
|
debugging utilities without restarting the pod.
|
||||||
* Node: Create a new pod that runs in the node's host namespaces and can access
|
* Node: Create a new pod that runs in the node's host namespaces and can access
|
||||||
the node's filesystem.
|
the node's filesystem.
|
||||||
|
`))
|
||||||
Alpha disclaimer: command line flags may change`))
|
|
||||||
|
|
||||||
debugExample = templates.Examples(i18n.T(`
|
debugExample = templates.Examples(i18n.T(`
|
||||||
# Create an interactive debugging session in pod mypod and immediately attach to it.
|
# Create an interactive debugging session in pod mypod and immediately attach to it.
|
||||||
@ -78,11 +77,17 @@ var (
|
|||||||
# (requires the EphemeralContainers feature to be enabled in the cluster)
|
# (requires the EphemeralContainers feature to be enabled in the cluster)
|
||||||
kubectl alpha debug --image=myproj/debug-tools -c debugger mypod
|
kubectl alpha debug --image=myproj/debug-tools -c debugger mypod
|
||||||
|
|
||||||
# Create a debug container as a copy of the original Pod and attach to it
|
# Create a copy of mypod adding a debug container and attach to it
|
||||||
kubectl alpha debug mypod -it --image=busybox --copy-to=my-debugger
|
kubectl alpha debug mypod -it --image=busybox --copy-to=my-debugger
|
||||||
|
|
||||||
# Create a copy of mypod named my-debugger with my-container's image changed to busybox
|
# Create a copy of mypod changing the command of mycontainer
|
||||||
kubectl alpha debug mypod --image=busybox --container=my-container --copy-to=my-debugger -- sleep 1d
|
kubectl alpha debug mypod -it --copy-to=my-debugger --container=mycontainer -- sh
|
||||||
|
|
||||||
|
# Create a copy of mypod changing all container images to busybox
|
||||||
|
kubectl alpha debug mypod --copy-to=my-debugger --set-image=*=busybox
|
||||||
|
|
||||||
|
# Create a copy of mypod adding a debug container and changing container images
|
||||||
|
kubectl alpha debug mypod -it --copy-to=my-debugger --image=debian --set-image=app=app:debug,sidecar=sidecar:debug
|
||||||
|
|
||||||
# Create an interactive debugging session on a node and immediately attach to it.
|
# Create an interactive debugging session on a node and immediately attach to it.
|
||||||
# The container will run in the host namespaces and the host's filesystem will be mounted at /host
|
# The container will run in the host namespaces and the host's filesystem will be mounted at /host
|
||||||
@ -108,13 +113,13 @@ type DebugOptions struct {
|
|||||||
PullPolicy corev1.PullPolicy
|
PullPolicy corev1.PullPolicy
|
||||||
Quiet bool
|
Quiet bool
|
||||||
SameNode bool
|
SameNode bool
|
||||||
|
SetImages map[string]string
|
||||||
ShareProcesses bool
|
ShareProcesses bool
|
||||||
Target string
|
TargetContainer string
|
||||||
TTY bool
|
TTY bool
|
||||||
|
|
||||||
shareProcessedChanged bool
|
shareProcessedChanged bool
|
||||||
|
|
||||||
builder *resource.Builder
|
|
||||||
podClient corev1client.PodsGetter
|
podClient corev1client.PodsGetter
|
||||||
|
|
||||||
genericclioptions.IOStreams
|
genericclioptions.IOStreams
|
||||||
@ -135,9 +140,9 @@ func NewCmdDebug(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.
|
|||||||
o := NewDebugOptions(streams)
|
o := NewDebugOptions(streams)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "debug (POD | TYPE[[.VERSION].GROUP]/NAME) --image=image [ -- COMMAND [args...] ]",
|
Use: "debug (POD | TYPE[[.VERSION].GROUP]/NAME) [ -- COMMAND [args...] ]",
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Short: i18n.T("Attach a debug container to a running pod"),
|
Short: i18n.T("Create debugging sessions for troubleshooting workloads and nodes"),
|
||||||
Long: debugLong,
|
Long: debugLong,
|
||||||
Example: debugExample,
|
Example: debugExample,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
@ -156,16 +161,16 @@ func addDebugFlags(cmd *cobra.Command, opt *DebugOptions) {
|
|||||||
cmd.Flags().BoolVar(&opt.Attach, "attach", opt.Attach, i18n.T("If true, wait for the container to start running, and then attach as if 'kubectl attach ...' were called. Default false, unless '-i/--stdin' is set, in which case the default is true."))
|
cmd.Flags().BoolVar(&opt.Attach, "attach", opt.Attach, i18n.T("If true, wait for the container to start running, and then attach as if 'kubectl attach ...' were called. Default false, unless '-i/--stdin' is set, in which case the default is true."))
|
||||||
cmd.Flags().StringVarP(&opt.Container, "container", "c", opt.Container, i18n.T("Container name to use for debug container."))
|
cmd.Flags().StringVarP(&opt.Container, "container", "c", opt.Container, i18n.T("Container name to use for debug container."))
|
||||||
cmd.Flags().StringVar(&opt.CopyTo, "copy-to", opt.CopyTo, i18n.T("Create a copy of the target Pod with this name."))
|
cmd.Flags().StringVar(&opt.CopyTo, "copy-to", opt.CopyTo, i18n.T("Create a copy of the target Pod with this name."))
|
||||||
cmd.Flags().BoolVar(&opt.Replace, "replace", opt.Replace, i18n.T("When used with '--copy-to', delete the original Pod"))
|
cmd.Flags().BoolVar(&opt.Replace, "replace", opt.Replace, i18n.T("When used with '--copy-to', delete the original Pod."))
|
||||||
cmd.Flags().StringToString("env", nil, i18n.T("Environment variables to set in the container."))
|
cmd.Flags().StringToString("env", nil, i18n.T("Environment variables to set in the container."))
|
||||||
cmd.Flags().StringVar(&opt.Image, "image", opt.Image, i18n.T("Container image to use for debug container."))
|
cmd.Flags().StringVar(&opt.Image, "image", opt.Image, i18n.T("Container image to use for debug container."))
|
||||||
cmd.MarkFlagRequired("image")
|
cmd.Flags().StringToStringVar(&opt.SetImages, "set-image", opt.SetImages, i18n.T("When used with '--copy-to', a list of name=image pairs for changing container images, similar to how 'kubectl set image' works."))
|
||||||
cmd.Flags().String("image-pull-policy", "", i18n.T("The image pull policy for the container. If left empty, this value will not be specified by the client and defaulted by the server"))
|
cmd.Flags().String("image-pull-policy", "", i18n.T("The image pull policy for the container. If left empty, this value will not be specified by the client and defaulted by the server."))
|
||||||
cmd.Flags().BoolVarP(&opt.Interactive, "stdin", "i", opt.Interactive, i18n.T("Keep stdin open on the container(s) in the pod, even if nothing is attached."))
|
cmd.Flags().BoolVarP(&opt.Interactive, "stdin", "i", opt.Interactive, i18n.T("Keep stdin open on the container(s) in the pod, even if nothing is attached."))
|
||||||
cmd.Flags().BoolVar(&opt.Quiet, "quiet", opt.Quiet, i18n.T("If true, suppress informational messages."))
|
cmd.Flags().BoolVar(&opt.Quiet, "quiet", opt.Quiet, i18n.T("If true, suppress informational messages."))
|
||||||
cmd.Flags().BoolVar(&opt.SameNode, "same-node", opt.SameNode, i18n.T("When used with '--copy-to', schedule the copy of target Pod on the same node."))
|
cmd.Flags().BoolVar(&opt.SameNode, "same-node", opt.SameNode, i18n.T("When used with '--copy-to', schedule the copy of target Pod on the same node."))
|
||||||
cmd.Flags().BoolVar(&opt.ShareProcesses, "share-processes", opt.ShareProcesses, i18n.T("When used with '--copy-to', enable process namespace sharing in the copy."))
|
cmd.Flags().BoolVar(&opt.ShareProcesses, "share-processes", opt.ShareProcesses, i18n.T("When used with '--copy-to', enable process namespace sharing in the copy."))
|
||||||
cmd.Flags().StringVar(&opt.Target, "target", "", i18n.T("When debugging a pod, target processes in this container name."))
|
cmd.Flags().StringVar(&opt.TargetContainer, "target", "", i18n.T("When using an ephemeral container, target processes in this container name."))
|
||||||
cmd.Flags().BoolVarP(&opt.TTY, "tty", "t", opt.TTY, i18n.T("Allocate a TTY for the debugging container."))
|
cmd.Flags().BoolVarP(&opt.TTY, "tty", "t", opt.TTY, i18n.T("Allocate a TTY for the debugging container."))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +178,6 @@ func addDebugFlags(cmd *cobra.Command, opt *DebugOptions) {
|
|||||||
func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
o.builder = f.NewBuilder()
|
|
||||||
o.PullPolicy = corev1.PullPolicy(cmdutil.GetFlagString(cmd, "image-pull-policy"))
|
o.PullPolicy = corev1.PullPolicy(cmdutil.GetFlagString(cmd, "image-pull-policy"))
|
||||||
|
|
||||||
// Arguments
|
// Arguments
|
||||||
@ -205,13 +209,6 @@ func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clientset
|
|
||||||
clientset, err := f.KubernetesClientSet()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("internal error getting clientset: %v", err)
|
|
||||||
}
|
|
||||||
o.podClient = clientset.CoreV1()
|
|
||||||
|
|
||||||
// Share processes
|
// Share processes
|
||||||
o.shareProcessedChanged = cmd.Flags().Changed("share-processes")
|
o.shareProcessedChanged = cmd.Flags().Changed("share-processes")
|
||||||
|
|
||||||
@ -220,12 +217,31 @@ func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st
|
|||||||
|
|
||||||
// Validate checks that the provided debug options are specified.
|
// Validate checks that the provided debug options are specified.
|
||||||
func (o *DebugOptions) Validate(cmd *cobra.Command) error {
|
func (o *DebugOptions) Validate(cmd *cobra.Command) error {
|
||||||
// Image
|
// CopyTo
|
||||||
if len(o.Image) == 0 {
|
if len(o.CopyTo) > 0 {
|
||||||
return fmt.Errorf("--image is required")
|
if len(o.Image) == 0 && len(o.SetImages) == 0 && len(o.Args) == 0 {
|
||||||
|
return fmt.Errorf("you must specify --image, --set-image or command arguments.")
|
||||||
}
|
}
|
||||||
if !reference.ReferenceRegexp.MatchString(o.Image) {
|
if len(o.Args) > 0 && len(o.Container) == 0 && len(o.Image) == 0 {
|
||||||
return fmt.Errorf("Invalid image name %q: %v", o.Image, reference.ErrReferenceInvalidFormat)
|
return fmt.Errorf("you must specify an existing container or a new image when specifying args.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// These flags are exclusive to --copy-to
|
||||||
|
switch {
|
||||||
|
case o.Replace:
|
||||||
|
return fmt.Errorf("--replace may only be used with --copy-to.")
|
||||||
|
case o.SameNode:
|
||||||
|
return fmt.Errorf("--same-node may only be used with --copy-to.")
|
||||||
|
case len(o.SetImages) > 0:
|
||||||
|
return fmt.Errorf("--set-image may only be used with --copy-to.")
|
||||||
|
case len(o.Image) == 0:
|
||||||
|
return fmt.Errorf("you must specify --image when not using --copy-to.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image
|
||||||
|
if len(o.Image) > 0 && !reference.ReferenceRegexp.MatchString(o.Image) {
|
||||||
|
return fmt.Errorf("invalid image name %q: %v", o.Image, reference.ErrReferenceInvalidFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
@ -241,8 +257,15 @@ func (o *DebugOptions) Validate(cmd *cobra.Command) error {
|
|||||||
return fmt.Errorf("invalid image pull policy: %s", o.PullPolicy)
|
return fmt.Errorf("invalid image pull policy: %s", o.PullPolicy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target
|
// SetImages
|
||||||
if len(o.Target) > 0 && len(o.CopyTo) > 0 {
|
for name, image := range o.SetImages {
|
||||||
|
if !reference.ReferenceRegexp.MatchString(image) {
|
||||||
|
return fmt.Errorf("invalid image name %q for container %q: %v", image, name, reference.ErrReferenceInvalidFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetContainer
|
||||||
|
if len(o.TargetContainer) > 0 && len(o.CopyTo) > 0 {
|
||||||
return fmt.Errorf("--target is incompatible with --copy-to. Use --share-processes instead.")
|
return fmt.Errorf("--target is incompatible with --copy-to. Use --share-processes instead.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +281,13 @@ func (o *DebugOptions) Validate(cmd *cobra.Command) error {
|
|||||||
func (o *DebugOptions) Run(f cmdutil.Factory, cmd *cobra.Command) error {
|
func (o *DebugOptions) Run(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
r := o.builder.
|
clientset, err := f.KubernetesClientSet()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("internal error getting clientset: %v", err)
|
||||||
|
}
|
||||||
|
o.podClient = clientset.CoreV1()
|
||||||
|
|
||||||
|
r := f.NewBuilder().
|
||||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||||
NamespaceParam(o.Namespace).DefaultNamespace().ResourceNames("pods", o.TargetNames...).
|
NamespaceParam(o.Namespace).DefaultNamespace().ResourceNames("pods", o.TargetNames...).
|
||||||
Do()
|
Do()
|
||||||
@ -266,7 +295,7 @@ func (o *DebugOptions) Run(f cmdutil.Factory, cmd *cobra.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.Visit(func(info *resource.Info, err error) error {
|
err = r.Visit(func(info *resource.Info, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO(verb): configurable early return
|
// TODO(verb): configurable early return
|
||||||
return err
|
return err
|
||||||
@ -369,8 +398,11 @@ func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev
|
|||||||
|
|
||||||
// debugByCopy runs a copy of the target Pod with a debug container added or an original container modified
|
// debugByCopy runs a copy of the target Pod with a debug container added or an original container modified
|
||||||
func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) {
|
func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) {
|
||||||
copied, dc := o.generatePodCopyWithDebugContainer(pod)
|
copied, dc, err := o.generatePodCopyWithDebugContainer(pod)
|
||||||
copied, err := o.podClient.Pods(copied.Namespace).Create(ctx, copied, metav1.CreateOptions{})
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
created, err := o.podClient.Pods(copied.Namespace).Create(ctx, copied, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@ -380,7 +412,7 @@ func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev
|
|||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return copied, dc, nil
|
return created, dc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateDebugContainer returns an EphemeralContainer suitable for use as a debug container
|
// generateDebugContainer returns an EphemeralContainer suitable for use as a debug container
|
||||||
@ -398,7 +430,7 @@ func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) *corev1.Ephemeral
|
|||||||
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
||||||
TTY: o.TTY,
|
TTY: o.TTY,
|
||||||
},
|
},
|
||||||
TargetContainerName: o.Target,
|
TargetContainerName: o.TargetContainer,
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.ArgsOnly {
|
if o.ArgsOnly {
|
||||||
@ -475,8 +507,8 @@ func (o *DebugOptions) generateNodeDebugPod(node string) *corev1.Pod {
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
// generatePodCopy takes a Pod and returns a copy and the debug container name of that copy
|
// generatePodCopyWithDebugContainer takes a Pod and returns a copy and the debug container name of that copy
|
||||||
func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*corev1.Pod, string) {
|
func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*corev1.Pod, string, error) {
|
||||||
copied := &corev1.Pod{
|
copied := &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: o.CopyTo,
|
Name: o.CopyTo,
|
||||||
@ -495,30 +527,59 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core
|
|||||||
copied.Spec.NodeName = ""
|
copied.Spec.NodeName = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
containerByName := containerNameToRef(copied)
|
// Apply image mutations
|
||||||
c, containerExists := containerByName[o.Container]
|
for i, c := range copied.Spec.Containers {
|
||||||
// Add a new container if the specified container does not exist
|
override := o.SetImages["*"]
|
||||||
if !containerExists {
|
if img, ok := o.SetImages[c.Name]; ok {
|
||||||
name := o.computeDebugContainerName(copied)
|
override = img
|
||||||
c = &corev1.Container{Name: name}
|
|
||||||
// envs are customizable when adding new container
|
|
||||||
c.Env = o.Env
|
|
||||||
}
|
}
|
||||||
c.Image = o.Image
|
if len(override) > 0 {
|
||||||
c.ImagePullPolicy = o.PullPolicy
|
copied.Spec.Containers[i].Image = override
|
||||||
c.Stdin = o.Interactive
|
}
|
||||||
c.TerminationMessagePolicy = corev1.TerminationMessageReadFile
|
}
|
||||||
c.TTY = o.TTY
|
|
||||||
|
containerByName := containerNameToRef(copied)
|
||||||
|
name := o.Container
|
||||||
|
if len(name) == 0 {
|
||||||
|
name = o.computeDebugContainerName(copied)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, ok := containerByName[name]
|
||||||
|
if !ok {
|
||||||
|
// Adding a new debug container
|
||||||
|
if len(o.Image) == 0 {
|
||||||
|
return nil, "", fmt.Errorf("you must specify image when creating new container")
|
||||||
|
}
|
||||||
|
c = &corev1.Container{
|
||||||
|
Name: name,
|
||||||
|
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
copied.Spec.Containers = append(copied.Spec.Containers, *c)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(o.Args) > 0 {
|
||||||
if o.ArgsOnly {
|
if o.ArgsOnly {
|
||||||
c.Args = o.Args
|
c.Args = o.Args
|
||||||
} else {
|
} else {
|
||||||
c.Command = o.Args
|
c.Command = o.Args
|
||||||
c.Args = nil
|
c.Args = nil
|
||||||
}
|
}
|
||||||
if !containerExists {
|
|
||||||
copied.Spec.Containers = append(copied.Spec.Containers, *c)
|
|
||||||
}
|
}
|
||||||
return copied, c.Name
|
if len(o.Env) > 0 {
|
||||||
|
c.Env = o.Env
|
||||||
|
}
|
||||||
|
if len(o.Image) > 0 {
|
||||||
|
c.Image = o.Image
|
||||||
|
}
|
||||||
|
if len(o.PullPolicy) > 0 {
|
||||||
|
c.ImagePullPolicy = o.PullPolicy
|
||||||
|
}
|
||||||
|
c.Stdin = o.Interactive
|
||||||
|
c.TTY = o.TTY
|
||||||
|
|
||||||
|
return copied, name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *DebugOptions) computeDebugContainerName(pod *corev1.Pod) string {
|
func (o *DebugOptions) computeDebugContainerName(pod *corev1.Pod) string {
|
||||||
@ -624,7 +685,7 @@ func handleAttachPod(ctx context.Context, f cmdutil.Factory, podClient corev1cli
|
|||||||
status := getContainerStatusByName(pod, containerName)
|
status := getContainerStatusByName(pod, containerName)
|
||||||
if status == nil {
|
if status == nil {
|
||||||
// impossible path
|
// impossible path
|
||||||
return fmt.Errorf("Error get container status of %s: %+v", containerName, err)
|
return fmt.Errorf("error getting container status of container name %q: %+v", containerName, err)
|
||||||
}
|
}
|
||||||
if status.State.Terminated != nil {
|
if status.State.Terminated != nil {
|
||||||
klog.V(1).Info("Ephemeral container terminated, falling back to logs")
|
klog.V(1).Info("Ephemeral container terminated, falling back to logs")
|
||||||
|
@ -18,13 +18,18 @@ package debug
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -65,7 +70,7 @@ func TestGenerateDebugContainer(t *testing.T) {
|
|||||||
Container: "debugger",
|
Container: "debugger",
|
||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
Target: "myapp",
|
TargetContainer: "myapp",
|
||||||
},
|
},
|
||||||
expected: &corev1.EphemeralContainer{
|
expected: &corev1.EphemeralContainer{
|
||||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||||
@ -247,8 +252,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
opts *DebugOptions
|
opts *DebugOptions
|
||||||
pod *corev1.Pod
|
havePod, wantPod *corev1.Pod
|
||||||
expected *corev1.Pod
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "basic",
|
name: "basic",
|
||||||
@ -258,7 +262,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -271,7 +275,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
NodeName: "node-1",
|
NodeName: "node-1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -281,7 +285,6 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -296,7 +299,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
SameNode: true,
|
SameNode: true,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -309,7 +312,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
NodeName: "node-1",
|
NodeName: "node-1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -319,7 +322,6 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NodeName: "node-1",
|
NodeName: "node-1",
|
||||||
@ -334,7 +336,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
@ -354,7 +356,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
@ -367,7 +369,6 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -381,7 +382,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -393,7 +394,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -424,7 +425,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Value: "test",
|
Value: "test",
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -436,7 +437,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -468,7 +469,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -480,7 +481,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -510,7 +511,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -522,7 +523,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -548,10 +549,9 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
CopyTo: "debugger",
|
CopyTo: "debugger",
|
||||||
Container: "debugger",
|
Container: "debugger",
|
||||||
Args: []string{"sleep", "1d"},
|
Args: []string{"sleep", "1d"},
|
||||||
Image: "busybox",
|
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -560,12 +560,14 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
Command: []string{"echo"},
|
Command: []string{"echo"},
|
||||||
|
Image: "app",
|
||||||
Args: []string{"one", "two", "three"},
|
Args: []string{"one", "two", "three"},
|
||||||
|
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -573,7 +575,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
Image: "busybox",
|
Image: "app",
|
||||||
Command: []string{"sleep", "1d"},
|
Command: []string{"sleep", "1d"},
|
||||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
||||||
@ -589,7 +591,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -601,7 +603,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -627,7 +629,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -639,7 +641,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -665,7 +667,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -685,7 +687,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -719,7 +721,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
PullPolicy: corev1.PullIfNotPresent,
|
PullPolicy: corev1.PullIfNotPresent,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -743,7 +745,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -772,7 +774,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
ShareProcesses: true,
|
ShareProcesses: true,
|
||||||
shareProcessedChanged: true,
|
shareProcessedChanged: true,
|
||||||
},
|
},
|
||||||
pod: &corev1.Pod{
|
havePod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "target",
|
Name: "target",
|
||||||
},
|
},
|
||||||
@ -780,12 +782,14 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
|
ImagePullPolicy: corev1.PullAlways,
|
||||||
|
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NodeName: "node-1",
|
NodeName: "node-1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &corev1.Pod{
|
wantPod: &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "debugger",
|
Name: "debugger",
|
||||||
},
|
},
|
||||||
@ -802,17 +806,215 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Change image for a named container",
|
||||||
|
opts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
CopyTo: "myapp-copy",
|
||||||
|
Container: "app",
|
||||||
|
Image: "busybox",
|
||||||
|
TargetNames: []string{"myapp"},
|
||||||
|
},
|
||||||
|
havePod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "myapp"},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "appimage"},
|
||||||
|
{Name: "sidecar", Image: "sidecarimage"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "myapp-copy"},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "busybox"},
|
||||||
|
{Name: "sidecar", Image: "sidecarimage"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Change image for a named container with set-image",
|
||||||
|
opts: &DebugOptions{
|
||||||
|
CopyTo: "myapp-copy",
|
||||||
|
Container: "app",
|
||||||
|
SetImages: map[string]string{"app": "busybox"},
|
||||||
|
},
|
||||||
|
havePod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "myapp",
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "appimage"},
|
||||||
|
{Name: "sidecar", Image: "sidecarimage"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "myapp-copy",
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "busybox"},
|
||||||
|
{Name: "sidecar", Image: "sidecarimage"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Change image for all containers with set-image",
|
||||||
|
opts: &DebugOptions{
|
||||||
|
CopyTo: "myapp-copy",
|
||||||
|
Container: "app",
|
||||||
|
SetImages: map[string]string{"*": "busybox"},
|
||||||
|
},
|
||||||
|
havePod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "myapp",
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "appimage"},
|
||||||
|
{Name: "sidecar", Image: "sidecarimage"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "myapp-copy",
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "busybox"},
|
||||||
|
{Name: "sidecar", Image: "busybox"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Change image for multiple containers with set-image",
|
||||||
|
opts: &DebugOptions{
|
||||||
|
CopyTo: "myapp-copy",
|
||||||
|
Container: "app",
|
||||||
|
SetImages: map[string]string{"*": "busybox", "app": "app-debugger"},
|
||||||
|
},
|
||||||
|
havePod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "myapp",
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "appimage"},
|
||||||
|
{Name: "sidecar", Image: "sidecarimage"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "myapp-copy",
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "app-debugger"},
|
||||||
|
{Name: "sidecar", Image: "busybox"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add interactive debug container minimal args",
|
||||||
|
opts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Attach: true,
|
||||||
|
CopyTo: "my-debugger",
|
||||||
|
Image: "busybox",
|
||||||
|
Interactive: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
havePod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "mypod"},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "appimage"},
|
||||||
|
{Name: "sidecar", Image: "sidecarimage"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "my-debugger"},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "appimage"},
|
||||||
|
{Name: "sidecar", Image: "sidecarimage"},
|
||||||
|
{
|
||||||
|
Name: "debugger-1",
|
||||||
|
Image: "busybox",
|
||||||
|
Stdin: true,
|
||||||
|
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: add container and also mutate images",
|
||||||
|
opts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Attach: true,
|
||||||
|
CopyTo: "my-debugger",
|
||||||
|
Image: "debian",
|
||||||
|
Interactive: true,
|
||||||
|
Namespace: "default",
|
||||||
|
SetImages: map[string]string{
|
||||||
|
"app": "app:debug",
|
||||||
|
"sidecar": "sidecar:debug",
|
||||||
|
},
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
havePod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "mypod"},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "appimage"},
|
||||||
|
{Name: "sidecar", Image: "sidecarimage"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPod: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "my-debugger"},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{Name: "app", Image: "app:debug"},
|
||||||
|
{Name: "sidecar", Image: "sidecar:debug"},
|
||||||
|
{
|
||||||
|
Name: "debugger-1",
|
||||||
|
Image: "debian",
|
||||||
|
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
|
||||||
|
Stdin: true,
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard()
|
tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard()
|
||||||
suffixCounter = 0
|
suffixCounter = 0
|
||||||
|
|
||||||
if tc.pod == nil {
|
if tc.havePod == nil {
|
||||||
tc.pod = &corev1.Pod{}
|
tc.havePod = &corev1.Pod{}
|
||||||
}
|
}
|
||||||
pod, _ := tc.opts.generatePodCopyWithDebugContainer(tc.pod)
|
gotPod, _, _ := tc.opts.generatePodCopyWithDebugContainer(tc.havePod)
|
||||||
if diff := cmp.Diff(tc.expected, pod); diff != "" {
|
if diff := cmp.Diff(tc.wantPod, gotPod); diff != "" {
|
||||||
t.Error("unexpected diff in generated object: (-want +got):\n", diff)
|
t.Error("TestGeneratePodCopyWithDebugContainer: diff in generated object: (-want +got):\n", diff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -976,3 +1178,345 @@ func TestGenerateNodeDebugPod(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompleteAndValidate(t *testing.T) {
|
||||||
|
tf := cmdtesting.NewTestFactory()
|
||||||
|
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams()
|
||||||
|
cmpFilter := cmp.FilterPath(func(p cmp.Path) bool {
|
||||||
|
switch p.String() {
|
||||||
|
// IOStreams contains unexported fields
|
||||||
|
case "IOStreams":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, cmp.Ignore())
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name, args string
|
||||||
|
wantOpts *DebugOptions
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No targets",
|
||||||
|
args: "--image=image",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid environment variables",
|
||||||
|
args: "--image=busybox --env=FOO mypod",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid image name",
|
||||||
|
args: "--image=image:label@deadbeef mypod",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid pull policy",
|
||||||
|
args: "--image=image --image-pull-policy=whenever-you-feel-like-it",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TTY without stdin",
|
||||||
|
args: "--image=image --tty",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Set image pull policy",
|
||||||
|
args: "--image=busybox --image-pull-policy=Always mypod",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Image: "busybox",
|
||||||
|
Namespace: "default",
|
||||||
|
PullPolicy: corev1.PullPolicy("Always"),
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple targets",
|
||||||
|
args: "--image=busybox mypod1 mypod2",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Image: "busybox",
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod1", "mypod2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Arguments with dash",
|
||||||
|
args: "--image=busybox mypod1 mypod2 -- echo 1 2",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{"echo", "1", "2"},
|
||||||
|
Image: "busybox",
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod1", "mypod2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Interactive no attach",
|
||||||
|
args: "-ti --image=busybox --attach=false mypod",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Attach: false,
|
||||||
|
Image: "busybox",
|
||||||
|
Interactive: true,
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Set environment variables",
|
||||||
|
args: "--image=busybox --env=FOO=BAR mypod",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Env: []v1.EnvVar{{Name: "FOO", Value: "BAR"}},
|
||||||
|
Image: "busybox",
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ephemeral container: interactive session minimal args",
|
||||||
|
args: "mypod -it --image=busybox",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Attach: true,
|
||||||
|
Image: "busybox",
|
||||||
|
Interactive: true,
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ephemeral container: non-interactive debugger with image and name",
|
||||||
|
args: "--image=myproj/debug-tools --image-pull-policy=Always -c debugger mypod",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Container: "debugger",
|
||||||
|
Image: "myproj/debug-tools",
|
||||||
|
Namespace: "default",
|
||||||
|
PullPolicy: corev1.PullPolicy("Always"),
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ephemeral container: no image specified",
|
||||||
|
args: "mypod",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ephemeral container: no image but args",
|
||||||
|
args: "mypod -- echo 1 2",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ephemeral container: replace not allowed",
|
||||||
|
args: "--replace --image=busybox mypod",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ephemeral container: same-node not allowed",
|
||||||
|
args: "--same-node --image=busybox mypod",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ephemeral container: incompatible with --set-image",
|
||||||
|
args: "--set-image=*=busybox mypod",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: interactive debug container minimal args",
|
||||||
|
args: "mypod -it --image=busybox --copy-to=my-debugger",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Attach: true,
|
||||||
|
CopyTo: "my-debugger",
|
||||||
|
Image: "busybox",
|
||||||
|
Interactive: true,
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: non-interactive with debug container, image name and command",
|
||||||
|
args: "mypod --image=busybox --container=my-container --copy-to=my-debugger -- sleep 1d",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{"sleep", "1d"},
|
||||||
|
Container: "my-container",
|
||||||
|
CopyTo: "my-debugger",
|
||||||
|
Image: "busybox",
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: replace single image of existing container",
|
||||||
|
args: "mypod --image=busybox --container=my-container --copy-to=my-debugger",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Container: "my-container",
|
||||||
|
CopyTo: "my-debugger",
|
||||||
|
Image: "busybox",
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: mutate existing container images",
|
||||||
|
args: "mypod --set-image=*=busybox,app=app-debugger --copy-to=my-debugger",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
CopyTo: "my-debugger",
|
||||||
|
Namespace: "default",
|
||||||
|
SetImages: map[string]string{
|
||||||
|
"*": "busybox",
|
||||||
|
"app": "app-debugger",
|
||||||
|
},
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: add container and also mutate images",
|
||||||
|
args: "mypod -it --copy-to=my-debugger --image=debian --set-image=app=app:debug,sidecar=sidecar:debug",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Attach: true,
|
||||||
|
CopyTo: "my-debugger",
|
||||||
|
Image: "debian",
|
||||||
|
Interactive: true,
|
||||||
|
Namespace: "default",
|
||||||
|
SetImages: map[string]string{
|
||||||
|
"app": "app:debug",
|
||||||
|
"sidecar": "sidecar:debug",
|
||||||
|
},
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: change command",
|
||||||
|
args: "mypod -it --copy-to=my-debugger --container=mycontainer -- sh",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Attach: true,
|
||||||
|
Args: []string{"sh"},
|
||||||
|
Container: "mycontainer",
|
||||||
|
CopyTo: "my-debugger",
|
||||||
|
Interactive: true,
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"mypod"},
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: no image specified",
|
||||||
|
args: "mypod -it --copy-to=my-debugger",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: args but no image specified",
|
||||||
|
args: "mypod --copy-to=my-debugger -- echo milo",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: --target not allowed",
|
||||||
|
args: "mypod --target --image=busybox --copy-to=my-debugger",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pod copy: invalid --set-image",
|
||||||
|
args: "mypod --set-image=*=SUPERGOODIMAGE#1!!!! --copy-to=my-debugger",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Node: interactive session minimal args",
|
||||||
|
args: "node/mynode -it --image=busybox",
|
||||||
|
wantOpts: &DebugOptions{
|
||||||
|
Args: []string{},
|
||||||
|
Attach: true,
|
||||||
|
Image: "busybox",
|
||||||
|
Interactive: true,
|
||||||
|
Namespace: "default",
|
||||||
|
ShareProcesses: true,
|
||||||
|
TargetNames: []string{"node/mynode"},
|
||||||
|
TTY: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Node: no image specified",
|
||||||
|
args: "node/mynode -it",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Node: --replace not allowed",
|
||||||
|
args: "--image=busybox --replace node/mynode",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Node: --same-node not allowed",
|
||||||
|
args: "--image=busybox --same-node node/mynode",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Node: --set-image not allowed",
|
||||||
|
args: "--image=busybox --set-image=*=busybox node/mynode",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Node: --target not allowed",
|
||||||
|
args: "node/mynode --target --image=busybox",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
opts := NewDebugOptions(ioStreams)
|
||||||
|
var gotError error
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
gotError = opts.Complete(tf, cmd, args)
|
||||||
|
if gotError != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gotError = opts.Validate(cmd)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.SetArgs(strings.Split(tc.args, " "))
|
||||||
|
addDebugFlags(cmd, opts)
|
||||||
|
|
||||||
|
cmdError := cmd.Execute()
|
||||||
|
|
||||||
|
if tc.wantError {
|
||||||
|
if cmdError != nil || gotError != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("CompleteAndValidate got nil errors but wantError: %v", tc.wantError)
|
||||||
|
} else if cmdError != nil {
|
||||||
|
t.Fatalf("cmd.Execute got error '%v' executing test cobra.Command, wantError: %v", cmdError, tc.wantError)
|
||||||
|
} else if gotError != nil {
|
||||||
|
t.Fatalf("CompleteAndValidate got error: '%v', wantError: %v", gotError, tc.wantError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.wantOpts, opts, cmpFilter, cmpopts.IgnoreUnexported(DebugOptions{})); diff != "" {
|
||||||
|
t.Error("CompleteAndValidate unexpected diff in generated object: (-want +got):\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user