Browse code

Add `oc debug` to make it easy to launch a test pod

Clayton Coleman authored on 2016/02/15 15:49:51
Showing 12 changed files
... ...
@@ -2318,9 +2318,9 @@ _oc_rsync()
2318 2318
     must_have_one_noun=()
2319 2319
 }
2320 2320
 
2321
-_oc_exec()
2321
+_oc_port-forward()
2322 2322
 {
2323
-    last_command="oc_exec"
2323
+    last_command="oc_port-forward"
2324 2324
     commands=()
2325 2325
 
2326 2326
     flags=()
... ...
@@ -2328,12 +2328,72 @@ _oc_exec()
2328 2328
     flags_with_completion=()
2329 2329
     flags_completion=()
2330 2330
 
2331
-    flags+=("--container=")
2332
-    two_word_flags+=("-c")
2333 2331
     flags+=("--pod=")
2334 2332
     two_word_flags+=("-p")
2335
-    flags+=("--stdin")
2336
-    flags+=("-i")
2333
+    flags+=("--api-version=")
2334
+    flags+=("--certificate-authority=")
2335
+    flags_with_completion+=("--certificate-authority")
2336
+    flags_completion+=("_filedir")
2337
+    flags+=("--client-certificate=")
2338
+    flags_with_completion+=("--client-certificate")
2339
+    flags_completion+=("_filedir")
2340
+    flags+=("--client-key=")
2341
+    flags_with_completion+=("--client-key")
2342
+    flags_completion+=("_filedir")
2343
+    flags+=("--cluster=")
2344
+    flags+=("--config=")
2345
+    flags_with_completion+=("--config")
2346
+    flags_completion+=("_filedir")
2347
+    flags+=("--context=")
2348
+    flags+=("--google-json-key=")
2349
+    flags+=("--insecure-skip-tls-verify")
2350
+    flags+=("--log-flush-frequency=")
2351
+    flags+=("--match-server-version")
2352
+    flags+=("--namespace=")
2353
+    two_word_flags+=("-n")
2354
+    flags+=("--server=")
2355
+    flags+=("--token=")
2356
+    flags+=("--user=")
2357
+
2358
+    must_have_one_flag=()
2359
+    must_have_one_noun=()
2360
+}
2361
+
2362
+_oc_debug()
2363
+{
2364
+    last_command="oc_debug"
2365
+    commands=()
2366
+
2367
+    flags=()
2368
+    two_word_flags=()
2369
+    flags_with_completion=()
2370
+    flags_completion=()
2371
+
2372
+    flags+=("--as-root")
2373
+    flags+=("--container=")
2374
+    two_word_flags+=("-c")
2375
+    flags+=("--filename=")
2376
+    flags_with_completion+=("--filename")
2377
+    flags_completion+=("__handle_filename_extension_flag yaml|yml|json")
2378
+    two_word_flags+=("-f")
2379
+    flags_with_completion+=("-f")
2380
+    flags_completion+=("__handle_filename_extension_flag yaml|yml|json")
2381
+    flags+=("--keep-annotations")
2382
+    flags+=("--keep-liveness")
2383
+    flags+=("--keep-readiness")
2384
+    flags+=("--no-headers")
2385
+    flags+=("--no-stdin")
2386
+    flags+=("-I")
2387
+    flags+=("--no-tty")
2388
+    flags+=("-T")
2389
+    flags+=("--node-name=")
2390
+    flags+=("--one-container")
2391
+    flags+=("--output=")
2392
+    two_word_flags+=("-o")
2393
+    flags+=("--output-version=")
2394
+    flags+=("--show-all")
2395
+    flags+=("--sort-by=")
2396
+    flags+=("--template=")
2337 2397
     flags+=("--tty")
2338 2398
     flags+=("-t")
2339 2399
     flags+=("--api-version=")
... ...
@@ -2365,9 +2425,9 @@ _oc_exec()
2365 2365
     must_have_one_noun=()
2366 2366
 }
2367 2367
 
2368
-_oc_port-forward()
2368
+_oc_exec()
2369 2369
 {
2370
-    last_command="oc_port-forward"
2370
+    last_command="oc_exec"
2371 2371
     commands=()
2372 2372
 
2373 2373
     flags=()
... ...
@@ -2375,8 +2435,14 @@ _oc_port-forward()
2375 2375
     flags_with_completion=()
2376 2376
     flags_completion=()
2377 2377
 
2378
+    flags+=("--container=")
2379
+    two_word_flags+=("-c")
2378 2380
     flags+=("--pod=")
2379 2381
     two_word_flags+=("-p")
2382
+    flags+=("--stdin")
2383
+    flags+=("-i")
2384
+    flags+=("--tty")
2385
+    flags+=("-t")
2380 2386
     flags+=("--api-version=")
2381 2387
     flags+=("--certificate-authority=")
2382 2388
     flags_with_completion+=("--certificate-authority")
... ...
@@ -7215,8 +7281,9 @@ _oc()
7215 7215
     commands+=("logs")
7216 7216
     commands+=("rsh")
7217 7217
     commands+=("rsync")
7218
-    commands+=("exec")
7219 7218
     commands+=("port-forward")
7219
+    commands+=("debug")
7220
+    commands+=("exec")
7220 7221
     commands+=("proxy")
7221 7222
     commands+=("attach")
7222 7223
     commands+=("run")
... ...
@@ -5655,9 +5655,9 @@ _openshift_cli_rsync()
5655 5655
     must_have_one_noun=()
5656 5656
 }
5657 5657
 
5658
-_openshift_cli_exec()
5658
+_openshift_cli_port-forward()
5659 5659
 {
5660
-    last_command="openshift_cli_exec"
5660
+    last_command="openshift_cli_port-forward"
5661 5661
     commands=()
5662 5662
 
5663 5663
     flags=()
... ...
@@ -5665,12 +5665,72 @@ _openshift_cli_exec()
5665 5665
     flags_with_completion=()
5666 5666
     flags_completion=()
5667 5667
 
5668
-    flags+=("--container=")
5669
-    two_word_flags+=("-c")
5670 5668
     flags+=("--pod=")
5671 5669
     two_word_flags+=("-p")
5672
-    flags+=("--stdin")
5673
-    flags+=("-i")
5670
+    flags+=("--api-version=")
5671
+    flags+=("--certificate-authority=")
5672
+    flags_with_completion+=("--certificate-authority")
5673
+    flags_completion+=("_filedir")
5674
+    flags+=("--client-certificate=")
5675
+    flags_with_completion+=("--client-certificate")
5676
+    flags_completion+=("_filedir")
5677
+    flags+=("--client-key=")
5678
+    flags_with_completion+=("--client-key")
5679
+    flags_completion+=("_filedir")
5680
+    flags+=("--cluster=")
5681
+    flags+=("--config=")
5682
+    flags_with_completion+=("--config")
5683
+    flags_completion+=("_filedir")
5684
+    flags+=("--context=")
5685
+    flags+=("--google-json-key=")
5686
+    flags+=("--insecure-skip-tls-verify")
5687
+    flags+=("--log-flush-frequency=")
5688
+    flags+=("--match-server-version")
5689
+    flags+=("--namespace=")
5690
+    two_word_flags+=("-n")
5691
+    flags+=("--server=")
5692
+    flags+=("--token=")
5693
+    flags+=("--user=")
5694
+
5695
+    must_have_one_flag=()
5696
+    must_have_one_noun=()
5697
+}
5698
+
5699
+_openshift_cli_debug()
5700
+{
5701
+    last_command="openshift_cli_debug"
5702
+    commands=()
5703
+
5704
+    flags=()
5705
+    two_word_flags=()
5706
+    flags_with_completion=()
5707
+    flags_completion=()
5708
+
5709
+    flags+=("--as-root")
5710
+    flags+=("--container=")
5711
+    two_word_flags+=("-c")
5712
+    flags+=("--filename=")
5713
+    flags_with_completion+=("--filename")
5714
+    flags_completion+=("__handle_filename_extension_flag yaml|yml|json")
5715
+    two_word_flags+=("-f")
5716
+    flags_with_completion+=("-f")
5717
+    flags_completion+=("__handle_filename_extension_flag yaml|yml|json")
5718
+    flags+=("--keep-annotations")
5719
+    flags+=("--keep-liveness")
5720
+    flags+=("--keep-readiness")
5721
+    flags+=("--no-headers")
5722
+    flags+=("--no-stdin")
5723
+    flags+=("-I")
5724
+    flags+=("--no-tty")
5725
+    flags+=("-T")
5726
+    flags+=("--node-name=")
5727
+    flags+=("--one-container")
5728
+    flags+=("--output=")
5729
+    two_word_flags+=("-o")
5730
+    flags+=("--output-version=")
5731
+    flags+=("--show-all")
5732
+    flags+=("--sort-by=")
5733
+    flags+=("--template=")
5674 5734
     flags+=("--tty")
5675 5735
     flags+=("-t")
5676 5736
     flags+=("--api-version=")
... ...
@@ -5702,9 +5762,9 @@ _openshift_cli_exec()
5702 5702
     must_have_one_noun=()
5703 5703
 }
5704 5704
 
5705
-_openshift_cli_port-forward()
5705
+_openshift_cli_exec()
5706 5706
 {
5707
-    last_command="openshift_cli_port-forward"
5707
+    last_command="openshift_cli_exec"
5708 5708
     commands=()
5709 5709
 
5710 5710
     flags=()
... ...
@@ -5712,8 +5772,14 @@ _openshift_cli_port-forward()
5712 5712
     flags_with_completion=()
5713 5713
     flags_completion=()
5714 5714
 
5715
+    flags+=("--container=")
5716
+    two_word_flags+=("-c")
5715 5717
     flags+=("--pod=")
5716 5718
     two_word_flags+=("-p")
5719
+    flags+=("--stdin")
5720
+    flags+=("-i")
5721
+    flags+=("--tty")
5722
+    flags+=("-t")
5717 5723
     flags+=("--api-version=")
5718 5724
     flags+=("--certificate-authority=")
5719 5725
     flags_with_completion+=("--certificate-authority")
... ...
@@ -10552,8 +10618,9 @@ _openshift_cli()
10552 10552
     commands+=("logs")
10553 10553
     commands+=("rsh")
10554 10554
     commands+=("rsync")
10555
-    commands+=("exec")
10556 10555
     commands+=("port-forward")
10556
+    commands+=("debug")
10557
+    commands+=("exec")
10557 10558
     commands+=("proxy")
10558 10559
     commands+=("attach")
10559 10560
     commands+=("run")
... ...
@@ -841,6 +841,26 @@ Create a secret from a local file, directory or literal value.
841 841
 ====
842 842
 
843 843
 
844
+== oc debug
845
+Launch a new instance of a pod for debugging
846
+
847
+====
848
+
849
+[options="nowrap"]
850
+----
851
+
852
+  # Debug a currently running deployment
853
+  $ oc debug dc/test
854
+
855
+  # Debug a specific failing container by running the env command in the 'second' container
856
+  $ oc debug dc/test -c second -- /bin/env
857
+
858
+  # See the pod that would be created to debug
859
+  $ oc debug dc/test -o yaml
860
+----
861
+====
862
+
863
+
844 864
 == oc delete
845 865
 Delete one or more resources
846 866
 
... ...
@@ -130,8 +130,9 @@ func NewCommandCLI(name, fullName string, in io.Reader, out, errout io.Writer) *
130 130
 				cmd.NewCmdLogs(cmd.LogsRecommendedName, fullName, f, out),
131 131
 				cmd.NewCmdRsh(cmd.RshRecommendedName, fullName, f, in, out, errout),
132 132
 				rsync.NewCmdRsync(rsync.RsyncRecommendedName, fullName, f, out, errout),
133
-				cmd.NewCmdExec(fullName, f, in, out, errout),
134 133
 				cmd.NewCmdPortForward(fullName, f),
134
+				cmd.NewCmdDebug(fullName, f, in, out, errout),
135
+				cmd.NewCmdExec(fullName, f, in, out, errout),
135 136
 				cmd.NewCmdProxy(fullName, f, out),
136 137
 				cmd.NewCmdAttach(fullName, f, in, out, errout),
137 138
 				cmd.NewCmdRun(fullName, f, in, out, errout),
138 139
new file mode 100644
... ...
@@ -0,0 +1,575 @@
0
+package cmd
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"os"
6
+	"strings"
7
+	"time"
8
+
9
+	"github.com/golang/glog"
10
+	"github.com/spf13/cobra"
11
+
12
+	kapi "k8s.io/kubernetes/pkg/api"
13
+	kapierrors "k8s.io/kubernetes/pkg/api/errors"
14
+	"k8s.io/kubernetes/pkg/api/unversioned"
15
+	kclient "k8s.io/kubernetes/pkg/client/unversioned"
16
+	"k8s.io/kubernetes/pkg/fields"
17
+	kcmd "k8s.io/kubernetes/pkg/kubectl/cmd"
18
+	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
19
+	"k8s.io/kubernetes/pkg/kubectl/resource"
20
+	"k8s.io/kubernetes/pkg/util/interrupt"
21
+	"k8s.io/kubernetes/pkg/util/term"
22
+	"k8s.io/kubernetes/pkg/util/wait"
23
+	"k8s.io/kubernetes/pkg/watch"
24
+
25
+	cmdutil "github.com/openshift/origin/pkg/cmd/util"
26
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
27
+	imageapi "github.com/openshift/origin/pkg/image/api"
28
+	"k8s.io/kubernetes/pkg/runtime"
29
+)
30
+
31
+type DebugOptions struct {
32
+	Attach kcmd.AttachOptions
33
+
34
+	Print         func(pod *kapi.Pod, w io.Writer) error
35
+	LogsForObject func(object, options runtime.Object) (*kclient.Request, error)
36
+
37
+	NoStdin    bool
38
+	ForceTTY   bool
39
+	DisableTTY bool
40
+	Filename   string
41
+	Timeout    time.Duration
42
+
43
+	Command         []string
44
+	Annotations     map[string]string
45
+	AsRoot          bool
46
+	KeepLabels      bool // TODO: evaluate selecting the right labels automatically
47
+	KeepAnnotations bool
48
+	KeepLiveness    bool
49
+	KeepReadiness   bool
50
+	OneContainer    bool
51
+	NodeName        string
52
+	AddEnv          []kapi.EnvVar
53
+	RemoveEnv       []string
54
+}
55
+
56
+const (
57
+	debugLong = `
58
+Launch a command shell to debug a running application
59
+
60
+When debugging images and setup problems, it's useful to get an exact copy of a running
61
+pod configuration and troubleshoot with a shell. Since a pod that is failing may not be
62
+started and not accessible to 'rsh' or 'exec', the 'debug' command makes it easy to
63
+create a carbon copy of that setup.
64
+
65
+The default mode is to start a shell inside of the first container of the referenced pod,
66
+replication controller, or deployment config. The started pod will be a copy of your
67
+source pod, with labels stripped, the command changed to '/bin/sh', and readiness and
68
+liveness checks disabled. If you just want to run a command, add '--' and a command to
69
+run. Passing a command will not create a TTY or send STDIN by default. Other flags are
70
+supported for altering the container or pod in common ways.
71
+
72
+The debug pod is deleted when the the remote command completes or the user interrupts
73
+the shell.`
74
+
75
+	debugExample = `
76
+  # Debug a currently running deployment
77
+  $ %[1]s dc/test
78
+
79
+  # Debug a specific failing container by running the env command in the 'second' container
80
+  $ %[1]s dc/test -c second -- /bin/env
81
+
82
+  # See the pod that would be created to debug
83
+  $ %[1]s dc/test -o yaml`
84
+
85
+	debugPodLabelName = "debug.openshift.io/name"
86
+
87
+	debugPodAnnotationSourceContainer = "debug.openshift.io/source-container"
88
+	debugPodAnnotationSourceResource  = "debug.openshift.io/source-resource"
89
+)
90
+
91
+// NewCmdDebug creates a command for debugging pods.
92
+func NewCmdDebug(fullName string, f *clientcmd.Factory, in io.Reader, out, errout io.Writer) *cobra.Command {
93
+	options := &DebugOptions{
94
+		Timeout: 30 * time.Second,
95
+		Command: []string{"/bin/sh"},
96
+		Attach: kcmd.AttachOptions{
97
+			In:    in,
98
+			Out:   out,
99
+			Err:   errout,
100
+			TTY:   true,
101
+			Stdin: true,
102
+
103
+			Attach: &kcmd.DefaultRemoteAttach{},
104
+		},
105
+		LogsForObject: f.LogsForObject,
106
+	}
107
+
108
+	cmd := &cobra.Command{
109
+		Use:     "debug RESOURCE/NAME [ENV1=VAL1 ...] [-c CONTAINER] [-- COMMAND]",
110
+		Short:   "Launch a new instance of a pod for debugging",
111
+		Long:    debugLong,
112
+		Example: fmt.Sprintf(debugExample, fmt.Sprintf("%s debug", fullName)),
113
+		Run: func(cmd *cobra.Command, args []string) {
114
+			kcmdutil.CheckErr(options.Complete(cmd, f, args, in, out, errout))
115
+			kcmdutil.CheckErr(options.Validate())
116
+			kcmdutil.CheckErr(options.Debug())
117
+		},
118
+	}
119
+
120
+	// TODO: when T is deprecated use the printer, but keep these hidden
121
+	cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml|wide|name|go-template=...|go-template-file=...|jsonpath=...|jsonpath-file=... See golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [http://releases.k8s.io/HEAD/docs/user-guide/jsonpath.md].")
122
+	cmd.Flags().String("output-version", "", "Output the formatted object with the given version (default api-version).")
123
+	cmd.Flags().String("template", "", "Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].")
124
+	cmd.Flags().Bool("no-headers", false, "When using the default output, don't print headers.")
125
+	cmd.Flags().MarkHidden("no-headers")
126
+	cmd.Flags().String("sort-by", "", "If non-empty, sort list types using this field specification.  The field specification is expressed as a JSONPath expression (e.g. 'ObjectMeta.Name'). The field in the API resource specified by this JSONPath expression must be an integer or a string.")
127
+	cmd.Flags().MarkHidden("sort-by")
128
+	cmd.Flags().Bool("show-all", true, "When printing, show all resources (default hide terminated pods.)")
129
+	cmd.Flags().MarkHidden("show-all")
130
+
131
+	cmd.Flags().BoolVarP(&options.NoStdin, "no-stdin", "I", options.NoStdin, "Bypasses passing STDIN to the container, defaults to true if no command specified")
132
+	cmd.Flags().BoolVarP(&options.ForceTTY, "tty", "t", false, "Force a pseudo-terminal to be allocated")
133
+	cmd.Flags().BoolVarP(&options.DisableTTY, "no-tty", "T", false, "Disable pseudo-terminal allocation")
134
+
135
+	cmd.Flags().StringVarP(&options.Attach.ContainerName, "container", "c", "", "Container name; defaults to first container")
136
+	cmd.Flags().BoolVar(&options.KeepAnnotations, "keep-annotations", false, "Keep the original pod annotations")
137
+	cmd.Flags().BoolVar(&options.KeepLiveness, "keep-liveness", false, "Keep the original pod liveness probes")
138
+	cmd.Flags().BoolVar(&options.KeepReadiness, "keep-readiness", false, "Keep the original pod readiness probes")
139
+	cmd.Flags().BoolVar(&options.OneContainer, "one-container", false, "Run only the selected container, remove all others")
140
+	cmd.Flags().StringVar(&options.NodeName, "node-name", "", "Set a specific node to run on - by default the pod will run on any valid node")
141
+	cmd.Flags().BoolVar(&options.AsRoot, "as-root", false, "Try to run the container as the root user")
142
+
143
+	cmd.Flags().StringVarP(&options.Filename, "filename", "f", "", "Filename or URL to file to read a template")
144
+	cmd.MarkFlagFilename("filename", "yaml", "yml", "json")
145
+
146
+	return cmd
147
+}
148
+
149
+func (o *DebugOptions) Complete(cmd *cobra.Command, f *clientcmd.Factory, args []string, in io.Reader, out, errout io.Writer) error {
150
+	if i := cmd.ArgsLenAtDash(); i != -1 && i < len(args) {
151
+		o.Command = args[i:]
152
+		args = args[:i]
153
+	}
154
+	resources, envArgs, ok := cmdutil.SplitEnvironmentFromResources(args)
155
+	if !ok {
156
+		return kcmdutil.UsageError(cmd, "all resources must be specified before environment changes: %s", strings.Join(args, " "))
157
+	}
158
+
159
+	switch {
160
+	case o.ForceTTY && o.NoStdin:
161
+		return kcmdutil.UsageError(cmd, "you may not specify -I and -t together")
162
+	case o.ForceTTY && o.DisableTTY:
163
+		return kcmdutil.UsageError(cmd, "you may not specify -t and -T together")
164
+	case o.ForceTTY:
165
+		o.Attach.TTY = true
166
+	case o.DisableTTY:
167
+		o.Attach.TTY = false
168
+	// don't default TTY to true if a command is passed
169
+	case len(o.Command) > 0:
170
+		o.Attach.TTY = false
171
+		o.Attach.Stdin = false
172
+	default:
173
+		o.Attach.TTY = term.IsTerminal(in)
174
+	}
175
+	if o.NoStdin {
176
+		o.Attach.TTY = false
177
+		o.Attach.Stdin = false
178
+	}
179
+
180
+	if o.Annotations == nil {
181
+		o.Annotations = make(map[string]string)
182
+	}
183
+
184
+	cmdNamespace, explicit, err := f.DefaultNamespace()
185
+	if err != nil {
186
+		return err
187
+	}
188
+
189
+	mapper, typer := f.Object()
190
+	b := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), kapi.Codecs.UniversalDecoder()).
191
+		NamespaceParam(cmdNamespace).DefaultNamespace().
192
+		SingleResourceType().
193
+		ResourceTypeOrNameArgs(true, resources...).
194
+		Flatten()
195
+	if len(o.Filename) > 0 {
196
+		b.FilenameParam(explicit, o.Filename)
197
+	}
198
+
199
+	o.AddEnv, o.RemoveEnv, err = cmdutil.ParseEnv(envArgs, nil)
200
+	if err != nil {
201
+		return err
202
+	}
203
+
204
+	one := false
205
+	infos, err := b.Do().IntoSingular(&one).Infos()
206
+	if err != nil {
207
+		return err
208
+	}
209
+	if !one {
210
+		return fmt.Errorf("you must identify a resource with a pod template to debug")
211
+	}
212
+
213
+	template, err := f.ApproximatePodTemplateForObject(infos[0].Object)
214
+	if err != nil || template == nil {
215
+		return fmt.Errorf("cannot debug %s: %v", infos[0].Name, err)
216
+	}
217
+	pod := &kapi.Pod{
218
+		ObjectMeta: template.ObjectMeta,
219
+		Spec:       template.Spec,
220
+	}
221
+	pod.Name, pod.Namespace = infos[0].Name, infos[0].Namespace
222
+	o.Attach.Pod = pod
223
+
224
+	if len(o.Attach.ContainerName) == 0 && len(pod.Spec.Containers) > 0 {
225
+		glog.V(4).Infof("defaulting container name to %s", pod.Spec.Containers[0].Name)
226
+		o.Attach.ContainerName = pod.Spec.Containers[0].Name
227
+	}
228
+
229
+	o.Annotations[debugPodAnnotationSourceResource] = fmt.Sprintf("%s/%s", infos[0].Mapping.Resource, infos[0].Name)
230
+	o.Annotations[debugPodAnnotationSourceContainer] = o.Attach.ContainerName
231
+
232
+	output := kcmdutil.GetFlagString(cmd, "output")
233
+	if len(output) != 0 {
234
+		o.Print = func(pod *kapi.Pod, out io.Writer) error {
235
+			return f.PrintObject(cmd, pod, out)
236
+		}
237
+	}
238
+
239
+	config, err := f.ClientConfig()
240
+	if err != nil {
241
+		return err
242
+	}
243
+	o.Attach.Config = config
244
+
245
+	_, kc, err := f.Clients()
246
+	if err != nil {
247
+		return err
248
+	}
249
+	o.Attach.Client = kc
250
+	return nil
251
+}
252
+func (o DebugOptions) Validate() error {
253
+	names := containerNames(o.Attach.Pod)
254
+	if len(names) == 0 {
255
+		return fmt.Errorf("the provided pod must have at least one container")
256
+	}
257
+	if len(o.Attach.ContainerName) == 0 {
258
+		return fmt.Errorf("you must provide a container name to debug")
259
+	}
260
+	if containerForName(o.Attach.Pod, o.Attach.ContainerName) == nil {
261
+		return fmt.Errorf("the container %q is not a valid container name; must be one of %v", o.Attach.ContainerName, names)
262
+	}
263
+	return nil
264
+}
265
+
266
+// WatchConditionFunc returns true if the condition has been reached, false if it has not been reached yet,
267
+// or an error if the condition cannot be checked and should terminate.
268
+type WatchConditionFunc func(event watch.Event) (bool, error)
269
+
270
+// Until reads items from the watch until each provided condition succeeds, and then returns the last watch
271
+// encountered. The first condition that returns an error terminates the watch (and the event is also returned).
272
+// If no event has been received, the returned event will be nil.
273
+// TODO: move to pkg/watch upstream
274
+func Until(timeout time.Duration, watcher watch.Interface, conditions ...WatchConditionFunc) (*watch.Event, error) {
275
+	ch := watcher.ResultChan()
276
+	defer watcher.Stop()
277
+	var after <-chan time.Time
278
+	if timeout > 0 {
279
+		after = time.After(timeout)
280
+	} else {
281
+		ch := make(chan time.Time)
282
+		close(ch)
283
+		after = ch
284
+	}
285
+	var lastEvent *watch.Event
286
+	for _, condition := range conditions {
287
+		for {
288
+			select {
289
+			case event, ok := <-ch:
290
+				if !ok {
291
+					return lastEvent, wait.ErrWaitTimeout
292
+				}
293
+				lastEvent = &event
294
+				// TODO: check for watch expired error and retry watch from latest point?
295
+				done, err := condition(event)
296
+				if err != nil {
297
+					return lastEvent, err
298
+				}
299
+				if done {
300
+					return lastEvent, nil
301
+				}
302
+			case <-after:
303
+				return lastEvent, wait.ErrWaitTimeout
304
+			}
305
+		}
306
+	}
307
+	return lastEvent, wait.ErrWaitTimeout
308
+}
309
+
310
+// ErrPodCompleted is returned by PodRunning or PodContainerRunning to indicate that
311
+// the pod has already reached completed state.
312
+var ErrPodCompleted = fmt.Errorf("pod ran to completion")
313
+
314
+// TODO: move to pkg/client/conditions.go upstream
315
+//
316
+// Example of a running condition, will be used elsewhere
317
+//
318
+// PodRunning returns true if the pod is running, false if the pod has not yet reached running state,
319
+// returns ErrPodCompleted if the pod has run to completion, or an error in any other case.
320
+// func PodRunning(event watch.Event) (bool, error) {
321
+// 	switch event.Type {
322
+// 	case watch.Deleted:
323
+// 		return false, kapierrors.NewNotFound(unversioned.GroupResource{Resource: "pods"}, "")
324
+// 	}
325
+// 	switch t := event.Object.(type) {
326
+// 	case *kapi.Pod:
327
+// 		switch t.Status.Phase {
328
+// 		case kapi.PodRunning:
329
+// 			return true, nil
330
+// 		case kapi.PodFailed, kapi.PodSucceeded:
331
+// 			return false, ErrPodCompleted
332
+// 		}
333
+// 	}
334
+// 	return false, nil
335
+// }
336
+
337
+// PodContainerRunning returns false until the named container has ContainerStatus running (at least once),
338
+// and will return an error if the pod is deleted, runs to completion, or the container pod is not available.
339
+func PodContainerRunning(containerName string) WatchConditionFunc {
340
+	return func(event watch.Event) (bool, error) {
341
+		switch event.Type {
342
+		case watch.Deleted:
343
+			return false, kapierrors.NewNotFound(unversioned.GroupResource{Resource: "pods"}, "")
344
+		}
345
+		switch t := event.Object.(type) {
346
+		case *kapi.Pod:
347
+			switch t.Status.Phase {
348
+			case kapi.PodRunning, kapi.PodPending:
349
+			case kapi.PodFailed, kapi.PodSucceeded:
350
+				return false, ErrPodCompleted
351
+			default:
352
+				return false, nil
353
+			}
354
+			for _, s := range t.Status.ContainerStatuses {
355
+				if s.Name != containerName {
356
+					continue
357
+				}
358
+				return s.State.Running != nil, nil
359
+			}
360
+			return false, nil
361
+		}
362
+		return false, nil
363
+	}
364
+}
365
+
366
+// SingleObject returns a ListOptions for watching a single object.
367
+// TODO: move to pkg/api/helpers.go upstream.
368
+func SingleObject(meta kapi.ObjectMeta) kapi.ListOptions {
369
+	return kapi.ListOptions{
370
+		FieldSelector:   fields.OneTermEqualSelector("metadata.name", meta.Name),
371
+		ResourceVersion: meta.ResourceVersion,
372
+	}
373
+}
374
+
375
+// Debug creates and runs a debugging pod.
376
+func (o *DebugOptions) Debug() error {
377
+	pod, originalCommand := o.transformPodForDebug(o.Annotations)
378
+	var commandString string
379
+	switch {
380
+	case len(originalCommand) > 0:
381
+		commandString = strings.Join(originalCommand, " ")
382
+	default:
383
+		commandString = "<image entrypoint>"
384
+	}
385
+
386
+	if o.Print != nil {
387
+		return o.Print(pod, o.Attach.Out)
388
+	}
389
+
390
+	glog.V(5).Infof("Creating pod: %#v", pod)
391
+	fmt.Fprintf(o.Attach.Err, "Debugging with pod/%s, original command: %s\n", pod.Name, commandString)
392
+	pod, err := o.createPod(pod)
393
+	if err != nil {
394
+		return err
395
+	}
396
+
397
+	// ensure the pod is cleaned up on shutdown
398
+	o.Attach.InterruptParent = interrupt.New(
399
+		func(os.Signal) { os.Exit(1) },
400
+		func() {
401
+			fmt.Fprintf(o.Attach.Err, "\nRemoving debug pod ...\n")
402
+			if err := o.Attach.Client.Pods(pod.Namespace).Delete(pod.Name, kapi.NewDeleteOptions(0)); err != nil {
403
+				fmt.Fprintf(o.Attach.Err, "error: unable to delete the debug pod %q: %v", pod.Name, err)
404
+			}
405
+		},
406
+	)
407
+	glog.V(5).Infof("Created attach arguments: %#v", o.Attach)
408
+	return o.Attach.InterruptParent.Run(func() error {
409
+		w, err := o.Attach.Client.Pods(pod.Namespace).Watch(SingleObject(pod.ObjectMeta))
410
+		if err != nil {
411
+			return err
412
+		}
413
+		fmt.Fprintf(o.Attach.Err, "Waiting for pod to start ...\n")
414
+		switch _, err := Until(o.Timeout, w, PodContainerRunning(o.Attach.ContainerName)); {
415
+		// switch to logging output
416
+		case err == ErrPodCompleted, !o.Attach.Stdin:
417
+			_, err := kcmd.LogsOptions{
418
+				Object: pod,
419
+				Options: &kapi.PodLogOptions{
420
+					Container: o.Attach.ContainerName,
421
+					Follow:    true,
422
+				},
423
+				Out: o.Attach.Out,
424
+
425
+				LogsForObject: o.LogsForObject,
426
+			}.RunLogs()
427
+			return err
428
+		case err != nil:
429
+			return err
430
+		default:
431
+			// TODO: attach can race with pod completion, allow attach to switch to logs
432
+			return o.Attach.Run()
433
+		}
434
+	})
435
+}
436
+
437
+// transformPodForDebug alters the input pod to be debuggable
438
+func (o *DebugOptions) transformPodForDebug(annotations map[string]string) (*kapi.Pod, []string) {
439
+	pod := o.Attach.Pod
440
+
441
+	// reset the container
442
+	container := containerForName(pod, o.Attach.ContainerName)
443
+
444
+	// identify the command to be run
445
+	originalCommand := append(container.Command, container.Args...)
446
+	container.Command = o.Command
447
+	if len(originalCommand) == 0 {
448
+		if cmd, ok := imageapi.ContainerImageEntrypointByAnnotation(pod.Annotations, o.Attach.ContainerName); ok {
449
+			originalCommand = cmd
450
+		}
451
+	}
452
+	container.Args = nil
453
+
454
+	container.TTY = o.Attach.Stdin && o.Attach.TTY
455
+	container.Stdin = o.Attach.Stdin
456
+	container.StdinOnce = o.Attach.Stdin
457
+
458
+	if !o.KeepReadiness {
459
+		container.ReadinessProbe = nil
460
+	}
461
+	if !o.KeepLiveness {
462
+		container.LivenessProbe = nil
463
+	}
464
+
465
+	var newEnv []kapi.EnvVar
466
+	if len(o.RemoveEnv) > 0 {
467
+		for i := range container.Env {
468
+			skip := false
469
+			for _, name := range o.RemoveEnv {
470
+				if name == container.Env[i].Name {
471
+					skip = true
472
+					break
473
+				}
474
+			}
475
+			if skip {
476
+				continue
477
+			}
478
+			newEnv = append(newEnv, container.Env[i])
479
+		}
480
+	} else {
481
+		newEnv = container.Env
482
+	}
483
+	for _, env := range o.AddEnv {
484
+		newEnv = append(newEnv, env)
485
+	}
486
+	container.Env = newEnv
487
+
488
+	if o.AsRoot {
489
+		if container.SecurityContext == nil {
490
+			container.SecurityContext = &kapi.SecurityContext{}
491
+		}
492
+		container.SecurityContext.RunAsNonRoot = nil
493
+		zero := int64(0)
494
+		container.SecurityContext.RunAsUser = &zero
495
+	}
496
+
497
+	if o.OneContainer {
498
+		pod.Spec.Containers = []kapi.Container{*container}
499
+	}
500
+
501
+	// reset the pod
502
+	if pod.Annotations == nil || !o.KeepAnnotations {
503
+		pod.Annotations = make(map[string]string)
504
+	}
505
+	for k, v := range annotations {
506
+		pod.Annotations[k] = v
507
+	}
508
+	if o.KeepLabels {
509
+		if pod.Labels == nil {
510
+			pod.Labels = make(map[string]string)
511
+		}
512
+		pod.Labels[debugPodLabelName] = pod.Name
513
+	} else {
514
+		pod.Labels = map[string]string{debugPodLabelName: pod.Name}
515
+	}
516
+	// always clear the NodeName
517
+	pod.Spec.NodeName = o.NodeName
518
+
519
+	pod.ResourceVersion = ""
520
+	pod.Spec.RestartPolicy = kapi.RestartPolicyNever
521
+	// TODO: shorten segments, make incrementing?
522
+	pod.Name = fmt.Sprintf("debug-%s", pod.Name)
523
+	pod.Status = kapi.PodStatus{}
524
+	pod.UID = ""
525
+	pod.CreationTimestamp = unversioned.Time{}
526
+	pod.SelfLink = ""
527
+
528
+	return pod, originalCommand
529
+}
530
+
531
+// createPod creates the debug pod, and will attempt to delete an existing debug
532
+// pod with the same name, but will return an error in any other case.
533
+func (o *DebugOptions) createPod(pod *kapi.Pod) (*kapi.Pod, error) {
534
+	namespace, name := pod.Namespace, pod.Name
535
+
536
+	// create the pod
537
+	created, err := o.Attach.Client.Pods(namespace).Create(pod)
538
+	if err == nil || !kapierrors.IsAlreadyExists(err) {
539
+		return created, err
540
+	}
541
+
542
+	// only continue if the pod has the right annotations
543
+	existing, err := o.Attach.Client.Pods(namespace).Get(name)
544
+	if err != nil {
545
+		return nil, err
546
+	}
547
+	if existing.Annotations[debugPodAnnotationSourceResource] != o.Annotations[debugPodAnnotationSourceResource] {
548
+		return nil, fmt.Errorf("a pod already exists named %q, please delete it before running debug", name)
549
+	}
550
+
551
+	// delete the existing pod
552
+	if err := o.Attach.Client.Pods(namespace).Delete(name, kapi.NewDeleteOptions(0)); err != nil && !kapierrors.IsNotFound(err) {
553
+		return nil, fmt.Errorf("unable to delete existing debug pod %q: %v", name, err)
554
+	}
555
+	return o.Attach.Client.Pods(namespace).Create(pod)
556
+}
557
+
558
+func containerForName(pod *kapi.Pod, name string) *kapi.Container {
559
+	for i, c := range pod.Spec.Containers {
560
+		if c.Name != name {
561
+			continue
562
+		}
563
+		return &pod.Spec.Containers[i]
564
+	}
565
+	return nil
566
+}
567
+
568
+func containerNames(pod *kapi.Pod) []string {
569
+	var names []string
570
+	for _, c := range pod.Spec.Containers {
571
+		names = append(names, c.Name)
572
+	}
573
+	return names
574
+}
... ...
@@ -101,21 +101,9 @@ func validateNoOverwrites(meta *kapi.ObjectMeta, labels map[string]string) error
101 101
 
102 102
 // RunEnv contains all the necessary functionality for the OpenShift cli env command
103 103
 func RunEnv(f *clientcmd.Factory, in io.Reader, out io.Writer, cmd *cobra.Command, args []string, envParams, filenames []string) error {
104
-	resources, envArgs := []string{}, []string{}
105
-	first := true
106
-	for _, s := range args {
107
-		isEnv := strings.Contains(s, "=") || strings.HasSuffix(s, "-")
108
-		switch {
109
-		case first && isEnv:
110
-			first = false
111
-			fallthrough
112
-		case !first && isEnv:
113
-			envArgs = append(envArgs, s)
114
-		case first && !isEnv:
115
-			resources = append(resources, s)
116
-		case !first && !isEnv:
117
-			return kcmdutil.UsageError(cmd, "all resources must be specified before environment changes: %s", s)
118
-		}
104
+	resources, envArgs, ok := cmdutil.SplitEnvironmentFromResources(args)
105
+	if !ok {
106
+		return kcmdutil.UsageError(cmd, "all resources must be specified before environment changes: %s", strings.Join(args, " "))
119 107
 	}
120 108
 	if len(filenames) == 0 && len(resources) < 1 {
121 109
 		return kcmdutil.UsageError(cmd, "one or more resources must be specified as <resource> <name> or <resource>/<name>")
... ...
@@ -418,9 +418,10 @@ func NewFactory(clientConfig kclientcmd.ClientConfig) *Factory {
418 418
 				}
419 419
 			}
420 420
 			var oldestPod *api.Pod
421
-			for _, pod := range pods.Items {
421
+			for i := range pods.Items {
422
+				pod := &pods.Items[i]
422 423
 				if oldestPod == nil || pod.CreationTimestamp.Before(oldestPod.CreationTimestamp) {
423
-					oldestPod = &pod
424
+					oldestPod = pod
424 425
 				}
425 426
 			}
426 427
 			return oldestPod, nil
... ...
@@ -484,6 +485,55 @@ func (f *Factory) UpdatePodSpecForObject(obj runtime.Object, fn func(*api.PodSpe
484 484
 	}
485 485
 }
486 486
 
487
+// ApproximatePodTemplateForObject returns a pod template object for the provided source.
488
+// It may return both an error and a object. It attempt to return the best possible template
489
+// avaliable at the current time.
490
+func (w *Factory) ApproximatePodTemplateForObject(object runtime.Object) (*api.PodTemplateSpec, error) {
491
+	switch t := object.(type) {
492
+	case *deployapi.DeploymentConfig:
493
+		fallback := t.Spec.Template
494
+
495
+		_, kc, err := w.Clients()
496
+		if err != nil {
497
+			return fallback, err
498
+		}
499
+
500
+		latestDeploymentName := deployutil.LatestDeploymentNameForConfig(t)
501
+		deployment, err := kc.ReplicationControllers(t.Namespace).Get(latestDeploymentName)
502
+		if err != nil {
503
+			return fallback, err
504
+		}
505
+
506
+		fallback = deployment.Spec.Template
507
+
508
+		pods, err := kc.Pods(deployment.Namespace).List(api.ListOptions{LabelSelector: labels.SelectorFromSet(deployment.Spec.Selector)})
509
+		if err != nil {
510
+			return fallback, err
511
+		}
512
+
513
+		for i := range pods.Items {
514
+			pod := &pods.Items[i]
515
+			if fallback == nil || pod.CreationTimestamp.Before(fallback.CreationTimestamp) {
516
+				fallback = &api.PodTemplateSpec{
517
+					ObjectMeta: pod.ObjectMeta,
518
+					Spec:       pod.Spec,
519
+				}
520
+			}
521
+		}
522
+		return fallback, nil
523
+
524
+	default:
525
+		pod, err := w.AttachablePodForObject(object)
526
+		if pod != nil {
527
+			return &api.PodTemplateSpec{
528
+				ObjectMeta: pod.ObjectMeta,
529
+				Spec:       pod.Spec,
530
+			}, err
531
+		}
532
+		return nil, err
533
+	}
534
+}
535
+
487 536
 // Clients returns an OpenShift and Kubernetes client.
488 537
 func (f *Factory) Clients() (*client.Client, *kclient.Client, error) {
489 538
 	kClient, err := f.Client()
... ...
@@ -47,6 +47,26 @@ func IsEnvironmentArgument(s string) bool {
47 47
 	return argumentEnvironment.MatchString(s)
48 48
 }
49 49
 
50
+func SplitEnvironmentFromResources(args []string) (resources, envArgs []string, ok bool) {
51
+	first := true
52
+	for _, s := range args {
53
+		// this method also has to understand env removal syntax, i.e. KEY-
54
+		isEnv := IsEnvironmentArgument(s) || strings.HasSuffix(s, "-")
55
+		switch {
56
+		case first && isEnv:
57
+			first = false
58
+			fallthrough
59
+		case !first && isEnv:
60
+			envArgs = append(envArgs, s)
61
+		case first && !isEnv:
62
+			resources = append(resources, s)
63
+		case !first && !isEnv:
64
+			return nil, nil, false
65
+		}
66
+	}
67
+	return resources, envArgs, true
68
+}
69
+
50 70
 func ParseEnvironmentArguments(s []string) (Environment, []string, []error) {
51 71
 	errs := []error{}
52 72
 	duplicates := []string{}
... ...
@@ -18,6 +18,7 @@ import (
18 18
 	buildapi "github.com/openshift/origin/pkg/build/api"
19 19
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
20 20
 	"github.com/openshift/origin/pkg/generate/git"
21
+	imageapi "github.com/openshift/origin/pkg/image/api"
21 22
 	"github.com/openshift/origin/pkg/util"
22 23
 )
23 24
 
... ...
@@ -306,6 +307,8 @@ func (r *DeploymentConfigRef) DeploymentConfig() (*deployapi.DeploymentConfig, e
306 306
 		},
307 307
 	}
308 308
 
309
+	annotations := make(map[string]string)
310
+
309 311
 	template := kapi.PodSpec{}
310 312
 	for i := range r.Images {
311 313
 		c, containerTriggers, err := r.Images[i].DeployableContainer()
... ...
@@ -314,6 +317,9 @@ func (r *DeploymentConfigRef) DeploymentConfig() (*deployapi.DeploymentConfig, e
314 314
 		}
315 315
 		triggers = append(triggers, containerTriggers...)
316 316
 		template.Containers = append(template.Containers, *c)
317
+		if cmd, ok := r.Images[i].Command(); ok {
318
+			imageapi.SetContainerImageEntrypointAnnotation(annotations, c.Name, cmd)
319
+		}
317 320
 	}
318 321
 
319 322
 	// Create EmptyDir volumes for all container volume mounts
... ...
@@ -342,7 +348,8 @@ func (r *DeploymentConfigRef) DeploymentConfig() (*deployapi.DeploymentConfig, e
342 342
 			Selector: selector,
343 343
 			Template: &kapi.PodTemplateSpec{
344 344
 				ObjectMeta: kapi.ObjectMeta{
345
-					Labels: selector,
345
+					Labels:      selector,
346
+					Annotations: annotations,
346 347
 				},
347 348
 				Spec: template,
348 349
 			},
... ...
@@ -230,6 +230,21 @@ func (r *ImageRef) SuggestName() (string, bool) {
230 230
 	return "", false
231 231
 }
232 232
 
233
+// Command returns the command the image invokes by default, or false if no such command has been defined.
234
+func (r *ImageRef) Command() (cmd []string, ok bool) {
235
+	if r == nil || r.Info == nil || r.Info.Config == nil {
236
+		return nil, false
237
+	}
238
+	config := r.Info.Config
239
+	switch {
240
+	case len(config.Entrypoint) > 0:
241
+		cmd = append(config.Entrypoint, config.Cmd...)
242
+	case len(config.Cmd) > 0:
243
+		cmd = config.Cmd
244
+	}
245
+	return cmd, len(cmd) > 0
246
+}
247
+
233 248
 // BuildOutput returns the BuildOutput of an image reference
234 249
 func (r *ImageRef) BuildOutput() (*buildapi.BuildOutput, error) {
235 250
 	if r == nil {
... ...
@@ -27,6 +27,10 @@ const (
27 27
 	DockerDefaultV1Registry = "index." + DockerDefaultRegistry
28 28
 	// DockerDefaultV2Registry is the host name of the default v2 registry
29 29
 	DockerDefaultV2Registry = "registry-1." + DockerDefaultRegistry
30
+
31
+	// containerImageEntrypointAnnotationFormatKey is a format used to identify the entrypoint of a particular
32
+	// container in a pod template. It is a JSON array of strings.
33
+	containerImageEntrypointAnnotationFormatKey = "openshift.io/container.%s.image.entrypoint"
30 34
 )
31 35
 
32 36
 // TODO remove (base, tag, id)
... ...
@@ -811,3 +815,25 @@ func PrioritizeTags(tags []string) {
811 811
 	}
812 812
 	copy(tags, finalTags)
813 813
 }
814
+
815
+func ContainerImageEntrypointByAnnotation(annotations map[string]string, containerName string) ([]string, bool) {
816
+	s, ok := annotations[fmt.Sprintf(containerImageEntrypointAnnotationFormatKey, containerName)]
817
+	if !ok {
818
+		return nil, false
819
+	}
820
+	var arr []string
821
+	if err := json.Unmarshal([]byte(s), &arr); err != nil {
822
+		return nil, false
823
+	}
824
+	return arr, true
825
+}
826
+
827
+func SetContainerImageEntrypointAnnotation(annotations map[string]string, containerName string, cmd []string) {
828
+	key := fmt.Sprintf(containerImageEntrypointAnnotationFormatKey, containerName)
829
+	if len(cmd) == 0 {
830
+		delete(annotations, key)
831
+		return
832
+	}
833
+	s, _ := json.Marshal(cmd)
834
+	annotations[key] = string(s)
835
+}
814 836
new file mode 100755
... ...
@@ -0,0 +1,34 @@
0
+#!/bin/bash
1
+
2
+set -o errexit
3
+set -o nounset
4
+set -o pipefail
5
+
6
+OS_ROOT=$(dirname "${BASH_SOURCE}")/../..
7
+source "${OS_ROOT}/hack/util.sh"
8
+source "${OS_ROOT}/hack/cmd_util.sh"
9
+os::log::install_errexit
10
+
11
+# Cleanup cluster resources created by this test
12
+(
13
+  set +e
14
+  oc delete all,templates --all
15
+  exit 0
16
+) &>/dev/null
17
+
18
+
19
+# This test validates the debug command
20
+os::cmd::expect_success 'oc create -f test/integration/fixtures/test-deployment-config.yaml'
21
+os::cmd::expect_success_and_text "oc debug dc/test-deployment-config -o yaml" '\- /bin/sh'
22
+os::cmd::expect_success_and_text "oc debug dc/test-deployment-config --keep-annotations -o yaml" 'annotations:'
23
+os::cmd::expect_success_and_text "oc debug dc/test-deployment-config --as-root -o yaml" 'runAsUser: 0'
24
+os::cmd::expect_success_and_text "oc debug dc/test-deployment-config --keep-liveness --keep-readiness -o yaml" ''
25
+os::cmd::expect_success_and_text "oc debug dc/test-deployment-config -o yaml -- /bin/env" '\- /bin/env'
26
+os::cmd::expect_success_and_not_text "oc debug dc/test-deployment-config -o yaml -- /bin/env" 'stdin'
27
+os::cmd::expect_success_and_not_text "oc debug dc/test-deployment-config -o yaml -- /bin/env" 'tty'
28
+# Does not require a real resource on the server
29
+os::cmd::expect_success_and_text "oc debug -f examples/hello-openshift/hello-pod.json --keep-liveness --keep-readiness -o yaml" ''
30
+os::cmd::expect_success_and_text "oc debug -f examples/hello-openshift/hello-pod.json -o yaml -- /bin/env" '\- /bin/env'
31
+os::cmd::expect_success_and_not_text "oc debug -f examples/hello-openshift/hello-pod.json -o yaml -- /bin/env" 'stdin'
32
+os::cmd::expect_success_and_not_text "oc debug -f examples/hello-openshift/hello-pod.json -o yaml -- /bin/env" 'tty'
33
+echo "debug: ok"