From e8e756a719db275b81a2ec09e135c93b7945ab52 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Tue, 28 Jul 2015 15:56:27 -0700 Subject: [PATCH 1/2] Add pod/attach to the APIServer. --- api/swagger-spec/v1.json | 146 +++++++++++++++++++++++++++++ pkg/api/deep_copy_generated.go | 13 +++ pkg/api/register.go | 2 + pkg/api/types.go | 21 +++++ pkg/api/v1/conversion_generated.go | 32 +++++++ pkg/api/v1/deep_copy_generated.go | 13 +++ pkg/api/v1/register.go | 2 + pkg/api/v1/types.go | 22 +++++ pkg/kubelet/dockertools/manager.go | 1 + pkg/master/master.go | 1 + pkg/registry/pod/etcd/etcd.go | 39 ++++++++ pkg/registry/pod/rest.go | 67 +++++++++---- 12 files changed, 341 insertions(+), 18 deletions(-) diff --git a/api/swagger-spec/v1.json b/api/swagger-spec/v1.json index 48d730c51f1..8b361eafe6d 100644 --- a/api/swagger-spec/v1.json +++ b/api/swagger-spec/v1.json @@ -5637,6 +5637,152 @@ } ] }, + { + "path": "/api/v1/namespaces/{namespace}/pods/{name}/attach", + "description": "API at /api/v1 version v1", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to attach of Pod", + "nickname": "connectGetNamespacedPodAttach", + "parameters": [ + { + "type": "boolean", + "paramType": "query", + "name": "stdin", + "description": "redirect the standard input stream of the pod for this call; defaults to false", + "required": false, + "allowMultiple": false + }, + { + "type": "boolean", + "paramType": "query", + "name": "stdout", + "description": "redirect the standard output stream of the pod for this call; defaults to true", + "required": false, + "allowMultiple": false + }, + { + "type": "boolean", + "paramType": "query", + "name": "stderr", + "description": "redirect the standard error stream of the pod for this call; defaults to true", + "required": false, + "allowMultiple": false + }, + { + "type": "boolean", + "paramType": "query", + "name": "tty", + "description": "allocate a terminal for this attach call; defaults to false", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "container", + "description": "the container in which to execute the command. Defaults to only container if there is only one container in the pod.", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespace", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "name", + "description": "name of the Pod", + "required": true, + "allowMultiple": false + } + ], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "POST", + "summary": "connect POST requests to attach of Pod", + "nickname": "connectPostNamespacedPodAttach", + "parameters": [ + { + "type": "boolean", + "paramType": "query", + "name": "stdin", + "description": "redirect the standard input stream of the pod for this call; defaults to false", + "required": false, + "allowMultiple": false + }, + { + "type": "boolean", + "paramType": "query", + "name": "stdout", + "description": "redirect the standard output stream of the pod for this call; defaults to true", + "required": false, + "allowMultiple": false + }, + { + "type": "boolean", + "paramType": "query", + "name": "stderr", + "description": "redirect the standard error stream of the pod for this call; defaults to true", + "required": false, + "allowMultiple": false + }, + { + "type": "boolean", + "paramType": "query", + "name": "tty", + "description": "allocate a terminal for this attach call; defaults to false", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "container", + "description": "the container in which to execute the command. Defaults to only container if there is only one container in the pod.", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespace", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "name", + "description": "name of the Pod", + "required": true, + "allowMultiple": false + } + ], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/api/v1/namespaces/{namespace}/pods/{name}/binding", "description": "API at /api/v1 version v1", diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index c4e083c068e..5edf631e181 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -1200,6 +1200,18 @@ func deepCopy_api_Pod(in Pod, out *Pod, c *conversion.Cloner) error { return nil } +func deepCopy_api_PodAttachOptions(in PodAttachOptions, out *PodAttachOptions, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + out.Stdin = in.Stdin + out.Stdout = in.Stdout + out.Stderr = in.Stderr + out.TTY = in.TTY + out.Container = in.Container + return nil +} + func deepCopy_api_PodCondition(in PodCondition, out *PodCondition, c *conversion.Cloner) error { out.Type = in.Type out.Status = in.Status @@ -2144,6 +2156,7 @@ func init() { deepCopy_api_PersistentVolumeSpec, deepCopy_api_PersistentVolumeStatus, deepCopy_api_Pod, + deepCopy_api_PodAttachOptions, deepCopy_api_PodCondition, deepCopy_api_PodExecOptions, deepCopy_api_PodList, diff --git a/pkg/api/register.go b/pkg/api/register.go index e0ecfb38e18..675f9e5e618 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -59,6 +59,7 @@ func init() { &PersistentVolumeClaimList{}, &DeleteOptions{}, &ListOptions{}, + &PodAttachOptions{}, &PodLogOptions{}, &PodExecOptions{}, &PodProxyOptions{}, @@ -106,6 +107,7 @@ func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {} func (*DeleteOptions) IsAnAPIObject() {} func (*ListOptions) IsAnAPIObject() {} +func (*PodAttachOptions) IsAnAPIObject() {} func (*PodLogOptions) IsAnAPIObject() {} func (*PodExecOptions) IsAnAPIObject() {} func (*PodProxyOptions) IsAnAPIObject() {} diff --git a/pkg/api/types.go b/pkg/api/types.go index 744b2100c17..9de223317c6 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1517,6 +1517,27 @@ type PodLogOptions struct { Previous bool } +// PodAttachOptions is the query options to a Pod's remote attach call +// TODO: merge w/ PodExecOptions below for stdin, stdout, etc +type PodAttachOptions struct { + TypeMeta `json:",inline"` + + // Stdin if true indicates that stdin is to be redirected for the attach call + Stdin bool `json:"stdin,omitempty" description:"redirect the standard input stream of the pod for this call; defaults to false"` + + // Stdout if true indicates that stdout is to be redirected for the attach call + Stdout bool `json:"stdout,omitempty" description:"redirect the standard output stream of the pod for this call; defaults to true"` + + // Stderr if true indicates that stderr is to be redirected for the attach call + Stderr bool `json:"stderr,omitempty" description:"redirect the standard error stream of the pod for this call; defaults to true"` + + // TTY if true indicates that a tty will be allocated for the attach call + TTY bool `json:"tty,omitempty" description:"allocate a terminal for this attach call; defaults to false"` + + // Container to attach to. + Container string `json:"container,omitempty" description:"the container in which to execute the command. Defaults to only container if there is only one container in the pod."` +} + // PodExecOptions is the query options to a Pod's remote exec call type PodExecOptions struct { TypeMeta diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index 52cdbe805f6..8fa007c32f3 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -1392,6 +1392,21 @@ func convert_api_Pod_To_v1_Pod(in *api.Pod, out *Pod, s conversion.Scope) error return nil } +func convert_api_PodAttachOptions_To_v1_PodAttachOptions(in *api.PodAttachOptions, out *PodAttachOptions, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.PodAttachOptions))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + out.Stdin = in.Stdin + out.Stdout = in.Stdout + out.Stderr = in.Stderr + out.TTY = in.TTY + out.Container = in.Container + return nil +} + func convert_api_PodCondition_To_v1_PodCondition(in *api.PodCondition, out *PodCondition, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*api.PodCondition))(in) @@ -3642,6 +3657,21 @@ func convert_v1_Pod_To_api_Pod(in *Pod, out *api.Pod, s conversion.Scope) error return nil } +func convert_v1_PodAttachOptions_To_api_PodAttachOptions(in *PodAttachOptions, out *api.PodAttachOptions, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*PodAttachOptions))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + out.Stdin = in.Stdin + out.Stdout = in.Stdout + out.Stderr = in.Stderr + out.TTY = in.TTY + out.Container = in.Container + return nil +} + func convert_v1_PodCondition_To_api_PodCondition(in *PodCondition, out *api.PodCondition, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*PodCondition))(in) @@ -4595,6 +4625,7 @@ func init() { convert_api_PersistentVolumeSpec_To_v1_PersistentVolumeSpec, convert_api_PersistentVolumeStatus_To_v1_PersistentVolumeStatus, convert_api_PersistentVolume_To_v1_PersistentVolume, + convert_api_PodAttachOptions_To_v1_PodAttachOptions, convert_api_PodCondition_To_v1_PodCondition, convert_api_PodExecOptions_To_v1_PodExecOptions, convert_api_PodList_To_v1_PodList, @@ -4707,6 +4738,7 @@ func init() { convert_v1_PersistentVolumeSpec_To_api_PersistentVolumeSpec, convert_v1_PersistentVolumeStatus_To_api_PersistentVolumeStatus, convert_v1_PersistentVolume_To_api_PersistentVolume, + convert_v1_PodAttachOptions_To_api_PodAttachOptions, convert_v1_PodCondition_To_api_PodCondition, convert_v1_PodExecOptions_To_api_PodExecOptions, convert_v1_PodList_To_api_PodList, diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index 91c9b8d3f68..3575a7c25b5 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -1203,6 +1203,18 @@ func deepCopy_v1_Pod(in Pod, out *Pod, c *conversion.Cloner) error { return nil } +func deepCopy_v1_PodAttachOptions(in PodAttachOptions, out *PodAttachOptions, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + out.Stdin = in.Stdin + out.Stdout = in.Stdout + out.Stderr = in.Stderr + out.TTY = in.TTY + out.Container = in.Container + return nil +} + func deepCopy_v1_PodCondition(in PodCondition, out *PodCondition, c *conversion.Cloner) error { out.Type = in.Type out.Status = in.Status @@ -2152,6 +2164,7 @@ func init() { deepCopy_v1_PersistentVolumeSpec, deepCopy_v1_PersistentVolumeStatus, deepCopy_v1_Pod, + deepCopy_v1_PodAttachOptions, deepCopy_v1_PodCondition, deepCopy_v1_PodExecOptions, deepCopy_v1_PodList, diff --git a/pkg/api/v1/register.go b/pkg/api/v1/register.go index 082ac87897d..e497f6ed4a3 100644 --- a/pkg/api/v1/register.go +++ b/pkg/api/v1/register.go @@ -74,6 +74,7 @@ func addKnownTypes() { &PersistentVolumeClaimList{}, &DeleteOptions{}, &ListOptions{}, + &PodAttachOptions{}, &PodLogOptions{}, &PodExecOptions{}, &PodProxyOptions{}, @@ -121,6 +122,7 @@ func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaimList) IsAnAPIObject() {} func (*DeleteOptions) IsAnAPIObject() {} func (*ListOptions) IsAnAPIObject() {} +func (*PodAttachOptions) IsAnAPIObject() {} func (*PodLogOptions) IsAnAPIObject() {} func (*PodExecOptions) IsAnAPIObject() {} func (*PodProxyOptions) IsAnAPIObject() {} diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 7b115f490e1..99c92a6d5ce 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -1475,6 +1475,28 @@ type PodLogOptions struct { Previous bool `json:"previous,omitempty" description:"return previous terminated container logs; defaults to false"` } +// PodAttachOptions is the query options to a Pod's remote attach call +// TODO: merge w/ PodExecOptions below for stdin, stdout, etc +type PodAttachOptions struct { + TypeMeta `json:",inline"` + + // Stdin if true indicates that stdin is to be redirected for the attach call + Stdin bool `json:"stdin,omitempty" description:"redirect the standard input stream of the pod for this call; defaults to false"` + + // Stdout if true indicates that stdout is to be redirected for the attach call + Stdout bool `json:"stdout,omitempty" description:"redirect the standard output stream of the pod for this call; defaults to true"` + + // Stderr if true indicates that stderr is to be redirected for the attach call + Stderr bool `json:"stderr,omitempty" description:"redirect the standard error stream of the pod for this call; defaults to true"` + + // TTY if true indicates that a tty will be allocated for the attach call, this is passed through to the container runtime so the tty + // is allocated on the worker node by the container runtime. + TTY bool `json:"tty,omitempty" description:"allocate a terminal for this attach call; defaults to false"` + + // Container to attach to. + Container string `json:"container,omitempty" description:"the container in which to execute the command. Defaults to only container if there is only one container in the pod."` +} + // PodExecOptions is the query options to a Pod's remote exec call type PodExecOptions struct { TypeMeta `json:",inline"` diff --git a/pkg/kubelet/dockertools/manager.go b/pkg/kubelet/dockertools/manager.go index c7a062f74c7..609345537cb 100644 --- a/pkg/kubelet/dockertools/manager.go +++ b/pkg/kubelet/dockertools/manager.go @@ -1012,6 +1012,7 @@ func (dm *DockerManager) AttachContainer(containerId string, stdin io.Reader, st InputStream: stdin, OutputStream: stdout, ErrorStream: stderr, + Stream: true, Logs: true, Stdin: stdin != nil, Stdout: stdout != nil, diff --git a/pkg/master/master.go b/pkg/master/master.go index d7cf999a78f..80f0af25355 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -470,6 +470,7 @@ func (m *Master) init(c *Config) { // TODO: Factor out the core API registration m.storage = map[string]rest.Storage{ "pods": podStorage.Pod, + "pods/attach": podStorage.Attach, "pods/status": podStorage.Status, "pods/log": podStorage.Log, "pods/exec": podStorage.Exec, diff --git a/pkg/registry/pod/etcd/etcd.go b/pkg/registry/pod/etcd/etcd.go index bb7b4e2f716..35f0f93c253 100644 --- a/pkg/registry/pod/etcd/etcd.go +++ b/pkg/registry/pod/etcd/etcd.go @@ -47,6 +47,7 @@ type PodStorage struct { Log *LogREST Proxy *ProxyREST Exec *ExecREST + Attach *AttachREST PortForward *PortForwardREST } @@ -96,6 +97,7 @@ func NewStorage(s tools.StorageInterface, k client.ConnectionInfoGetter) PodStor Log: &LogREST{store: store, kubeletConn: k}, Proxy: &ProxyREST{store: store}, Exec: &ExecREST{store: store, kubeletConn: k}, + Attach: &AttachREST{store: store, kubeletConn: k}, PortForward: &PortForwardREST{store: store, kubeletConn: k}, } } @@ -284,6 +286,43 @@ func (r *ProxyREST) Connect(ctx api.Context, id string, opts runtime.Object) (re // Support both GET and POST methods. Over time, we want to move all clients to start using POST and then stop supporting GET. var upgradeableMethods = []string{"GET", "POST"} +// AttachREST implements the attach subresource for a Pod +type AttachREST struct { + store *etcdgeneric.Etcd + kubeletConn client.ConnectionInfoGetter +} + +// Implement Connecter +var _ = rest.Connecter(&AttachREST{}) + +// New creates a new Pod object +func (r *AttachREST) New() runtime.Object { + return &api.Pod{} +} + +// Connect returns a handler for the pod exec proxy +func (r *AttachREST) Connect(ctx api.Context, name string, opts runtime.Object) (rest.ConnectHandler, error) { + attachOpts, ok := opts.(*api.PodAttachOptions) + if !ok { + return nil, fmt.Errorf("Invalid options object: %#v", opts) + } + location, transport, err := pod.AttachLocation(r.store, r.kubeletConn, ctx, name, attachOpts) + if err != nil { + return nil, err + } + return genericrest.NewUpgradeAwareProxyHandler(location, transport, true), nil +} + +// NewConnectOptions returns the versioned object that represents exec parameters +func (r *AttachREST) NewConnectOptions() (runtime.Object, bool, string) { + return &api.PodAttachOptions{}, false, "" +} + +// ConnectMethods returns the methods supported by exec +func (r *AttachREST) ConnectMethods() []string { + return upgradeableMethods +} + // ExecREST implements the exec subresource for a Pod type ExecREST struct { store *etcdgeneric.Etcd diff --git a/pkg/registry/pod/rest.go b/pkg/registry/pod/rest.go index d35e8302eda..e5bcd5807db 100644 --- a/pkg/registry/pod/rest.go +++ b/pkg/registry/pod/rest.go @@ -188,7 +188,6 @@ func ResourceLocation(getter ResourceGetter, ctx api.Context, id string) (*url.U // LogLocation returns a the log URL for a pod container. If opts.Container is blank // and only one container is present in the pod, that container is used. func LogLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, ctx api.Context, name string, opts *api.PodLogOptions) (*url.URL, http.RoundTripper, error) { - pod, err := getPod(getter, ctx, name) if err != nil { return nil, nil, err @@ -228,17 +227,62 @@ func LogLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, ct return loc, nodeTransport, nil } +func streamParams(params url.Values, opts runtime.Object) error { + switch opts := opts.(type) { + case *api.PodExecOptions: + if opts.Stdin { + params.Add(api.ExecStdinParam, "1") + } + if opts.Stdout { + params.Add(api.ExecStdoutParam, "1") + } + if opts.Stderr { + params.Add(api.ExecStderrParam, "1") + } + if opts.TTY { + params.Add(api.ExecTTYParam, "1") + } + for _, c := range opts.Command { + params.Add("command", c) + } + case *api.PodAttachOptions: + if opts.Stdin { + params.Add(api.ExecStdinParam, "1") + } + if opts.Stdout { + params.Add(api.ExecStdoutParam, "1") + } + if opts.Stderr { + params.Add(api.ExecStderrParam, "1") + } + if opts.TTY { + params.Add(api.ExecTTYParam, "1") + } + default: + return fmt.Errorf("Unknown object for streaming: %v", opts) + } + return nil +} + +// AttachLocation returns the attach URL for a pod container. If opts.Container is blank +// and only one container is present in the pod, that container is used. +func AttachLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, ctx api.Context, name string, opts *api.PodAttachOptions) (*url.URL, http.RoundTripper, error) { + return streamLocation(getter, connInfo, ctx, name, opts, opts.Container, "attach") +} + // ExecLocation returns the exec URL for a pod container. If opts.Container is blank // and only one container is present in the pod, that container is used. func ExecLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, ctx api.Context, name string, opts *api.PodExecOptions) (*url.URL, http.RoundTripper, error) { + return streamLocation(getter, connInfo, ctx, name, opts, opts.Container, "exec") +} +func streamLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, ctx api.Context, name string, opts runtime.Object, container, path string) (*url.URL, http.RoundTripper, error) { pod, err := getPod(getter, ctx, name) if err != nil { return nil, nil, err } // Try to figure out a container - container := opts.Container if container == "" { if len(pod.Spec.Containers) == 1 { container = pod.Spec.Containers[0].Name @@ -256,25 +300,13 @@ func ExecLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, c return nil, nil, err } params := url.Values{} - if opts.Stdin { - params.Add(api.ExecStdinParam, "1") - } - if opts.Stdout { - params.Add(api.ExecStdoutParam, "1") - } - if opts.Stderr { - params.Add(api.ExecStderrParam, "1") - } - if opts.TTY { - params.Add(api.ExecTTYParam, "1") - } - for _, c := range opts.Command { - params.Add("command", c) + if err := streamParams(params, opts); err != nil { + return nil, nil, err } loc := &url.URL{ Scheme: nodeScheme, Host: fmt.Sprintf("%s:%d", nodeHost, nodePort), - Path: fmt.Sprintf("/exec/%s/%s/%s", pod.Namespace, name, container), + Path: fmt.Sprintf("/%s/%s/%s/%s", path, pod.Namespace, name, container), RawQuery: params.Encode(), } return loc, nodeTransport, nil @@ -282,7 +314,6 @@ func ExecLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, c // PortForwardLocation returns a the port-forward URL for a pod. func PortForwardLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, ctx api.Context, name string) (*url.URL, http.RoundTripper, error) { - pod, err := getPod(getter, ctx, name) if err != nil { return nil, nil, err From 97cb1cd071e56baed345ec156b70c31236a552a0 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 29 Jul 2015 16:19:09 -0700 Subject: [PATCH 2/2] Add support for attach to kubectl --- contrib/completions/bash/kubectl | 24 ++ docs/man/man1/.files_generated | 1 + docs/man/man1/kubectl-attach.1 | 161 +++++++++++++ docs/man/man1/kubectl.1 | 2 +- docs/user-guide/docker-cli-to-kubectl.md | 62 ++++- docs/user-guide/kubectl/.files_generated | 1 + docs/user-guide/kubectl/kubectl.md | 3 +- docs/user-guide/kubectl/kubectl_attach.md | 108 +++++++++ pkg/client/remotecommand/remotecommand.go | 92 ++++++-- .../remotecommand/remotecommand_test.go | 77 +++++++ pkg/kubectl/cmd/attach.go | 211 ++++++++++++++++++ pkg/kubectl/cmd/attach_test.go | 198 ++++++++++++++++ pkg/kubectl/cmd/cmd.go | 1 + 13 files changed, 916 insertions(+), 25 deletions(-) create mode 100644 docs/man/man1/kubectl-attach.1 create mode 100644 docs/user-guide/kubectl/kubectl_attach.md create mode 100644 pkg/kubectl/cmd/attach.go create mode 100644 pkg/kubectl/cmd/attach_test.go diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 0100d09c7a4..ddcb4a502a7 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -505,6 +505,29 @@ _kubectl_scale() must_have_one_noun=() } +_kubectl_attach() +{ + last_command="kubectl_attach" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--container=") + two_word_flags+=("-c") + flags+=("--help") + flags+=("-h") + flags+=("--stdin") + flags+=("-i") + flags+=("--tty") + flags+=("-t") + + must_have_one_flag=() + must_have_one_noun=() +} + _kubectl_exec() { last_command="kubectl_exec" @@ -946,6 +969,7 @@ _kubectl() commands+=("logs") commands+=("rolling-update") commands+=("scale") + commands+=("attach") commands+=("exec") commands+=("port-forward") commands+=("proxy") diff --git a/docs/man/man1/.files_generated b/docs/man/man1/.files_generated index 4156725b069..76219f56480 100644 --- a/docs/man/man1/.files_generated +++ b/docs/man/man1/.files_generated @@ -1,4 +1,5 @@ kubectl-api-versions.1 +kubectl-attach.1 kubectl-cluster-info.1 kubectl-config-set-cluster.1 kubectl-config-set-context.1 diff --git a/docs/man/man1/kubectl-attach.1 b/docs/man/man1/kubectl-attach.1 new file mode 100644 index 00000000000..861cfb8b2a7 --- /dev/null +++ b/docs/man/man1/kubectl-attach.1 @@ -0,0 +1,161 @@ +.TH "KUBERNETES" "1" " kubernetes User Manuals" "Eric Paris" "Jan 2015" "" + + +.SH NAME +.PP +kubectl attach \- Attach to a running container. + + +.SH SYNOPSIS +.PP +\fBkubectl attach\fP [OPTIONS] + + +.SH DESCRIPTION +.PP +Attach to a a process that is already running inside an existing container. + + +.SH OPTIONS +.PP +\fB\-c\fP, \fB\-\-container\fP="" + Container name + +.PP +\fB\-h\fP, \fB\-\-help\fP=false + help for attach + +.PP +\fB\-i\fP, \fB\-\-stdin\fP=false + Pass stdin to the container + +.PP +\fB\-t\fP, \fB\-\-tty\fP=false + Stdin is a TTY + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +.PP +\fB\-\-alsologtostderr\fP=false + log to standard error as well as files + +.PP +\fB\-\-api\-version\fP="" + The API version to use when talking to the server + +.PP +\fB\-\-certificate\-authority\fP="" + Path to a cert. file for the certificate authority. + +.PP +\fB\-\-client\-certificate\fP="" + Path to a client key file for TLS. + +.PP +\fB\-\-client\-key\fP="" + Path to a client key file for TLS. + +.PP +\fB\-\-cluster\fP="" + The name of the kubeconfig cluster to use + +.PP +\fB\-\-context\fP="" + The name of the kubeconfig context to use + +.PP +\fB\-\-insecure\-skip\-tls\-verify\fP=false + If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure. + +.PP +\fB\-\-kubeconfig\fP="" + Path to the kubeconfig file to use for CLI requests. + +.PP +\fB\-\-log\-backtrace\-at\fP=:0 + when logging hits line file:N, emit a stack trace + +.PP +\fB\-\-log\-dir\fP="" + If non\-empty, write log files in this directory + +.PP +\fB\-\-log\-flush\-frequency\fP=5s + Maximum number of seconds between log flushes + +.PP +\fB\-\-logtostderr\fP=true + log to standard error instead of files + +.PP +\fB\-\-match\-server\-version\fP=false + Require server version to match client version + +.PP +\fB\-\-namespace\fP="" + If present, the namespace scope for this CLI request. + +.PP +\fB\-\-password\fP="" + Password for basic authentication to the API server. + +.PP +\fB\-s\fP, \fB\-\-server\fP="" + The address and port of the Kubernetes API server + +.PP +\fB\-\-stderrthreshold\fP=2 + logs at or above this threshold go to stderr + +.PP +\fB\-\-token\fP="" + Bearer token for authentication to the API server. + +.PP +\fB\-\-user\fP="" + The name of the kubeconfig user to use + +.PP +\fB\-\-username\fP="" + Username for basic authentication to the API server. + +.PP +\fB\-\-v\fP=0 + log level for V logs + +.PP +\fB\-\-validate\fP=false + If true, use a schema to validate the input before sending it + +.PP +\fB\-\-vmodule\fP= + comma\-separated list of pattern=N settings for file\-filtered logging + + +.SH EXAMPLE +.PP +.RS + +.nf +// get output from running pod 123456\-7890, using the first container by default +$ kubectl attach 123456\-7890 + +// get output from ruby\-container from pod 123456\-7890 +$ kubectl attach 123456\-7890 \-c ruby\-container date + +// switch to raw terminal mode, sends stdin to 'bash' in ruby\-container from pod 123456\-780 +// and sends stdout/stderr from 'bash' back to the client +$ kubectl attach 123456\-7890 \-c ruby\-container \-i \-t + +.fi +.RE + + +.SH SEE ALSO +.PP +\fBkubectl(1)\fP, + + +.SH HISTORY +.PP +January 2015, Originally compiled by Eric Paris (eparis at redhat dot com) based on the kubernetes source material, but hopefully they have been automatically generated since! diff --git a/docs/man/man1/kubectl.1 b/docs/man/man1/kubectl.1 index f6969202a60..e2dd746bd6d 100644 --- a/docs/man/man1/kubectl.1 +++ b/docs/man/man1/kubectl.1 @@ -124,7 +124,7 @@ Find more information at .SH SEE ALSO .PP -\fBkubectl\-get(1)\fP, \fBkubectl\-describe(1)\fP, \fBkubectl\-create(1)\fP, \fBkubectl\-replace(1)\fP, \fBkubectl\-patch(1)\fP, \fBkubectl\-delete(1)\fP, \fBkubectl\-namespace(1)\fP, \fBkubectl\-logs(1)\fP, \fBkubectl\-rolling\-update(1)\fP, \fBkubectl\-scale(1)\fP, \fBkubectl\-exec(1)\fP, \fBkubectl\-port\-forward(1)\fP, \fBkubectl\-proxy(1)\fP, \fBkubectl\-run(1)\fP, \fBkubectl\-stop(1)\fP, \fBkubectl\-expose(1)\fP, \fBkubectl\-label(1)\fP, \fBkubectl\-config(1)\fP, \fBkubectl\-cluster\-info(1)\fP, \fBkubectl\-api\-versions(1)\fP, \fBkubectl\-version(1)\fP, +\fBkubectl\-get(1)\fP, \fBkubectl\-describe(1)\fP, \fBkubectl\-create(1)\fP, \fBkubectl\-replace(1)\fP, \fBkubectl\-patch(1)\fP, \fBkubectl\-delete(1)\fP, \fBkubectl\-namespace(1)\fP, \fBkubectl\-logs(1)\fP, \fBkubectl\-rolling\-update(1)\fP, \fBkubectl\-scale(1)\fP, \fBkubectl\-attach(1)\fP, \fBkubectl\-exec(1)\fP, \fBkubectl\-port\-forward(1)\fP, \fBkubectl\-proxy(1)\fP, \fBkubectl\-run(1)\fP, \fBkubectl\-stop(1)\fP, \fBkubectl\-expose(1)\fP, \fBkubectl\-label(1)\fP, \fBkubectl\-config(1)\fP, \fBkubectl\-cluster\-info(1)\fP, \fBkubectl\-api\-versions(1)\fP, \fBkubectl\-version(1)\fP, .SH HISTORY diff --git a/docs/user-guide/docker-cli-to-kubectl.md b/docs/user-guide/docker-cli-to-kubectl.md index 4ecc73d4caf..ebdae76c707 100644 --- a/docs/user-guide/docker-cli-to-kubectl.md +++ b/docs/user-guide/docker-cli-to-kubectl.md @@ -41,6 +41,7 @@ In this doc, we introduce the Kubernetes command line to for interacting with th - [kubectl for docker users](#kubectl-for-docker-users) - [docker run](#docker-run) - [docker ps](#docker-ps) + - [docker attach](#docker-attach) - [docker exec](#docker-exec) - [docker logs](#docker-logs) - [docker stop and docker rm](#docker-stop-and-docker-rm) @@ -99,9 +100,9 @@ NAME READY STATUS RESTARTS AGE nginx-app-5jyvm 1/1 Running 0 1h ``` -#### docker exec +#### docker attach -How do I execute a command in a container? Checkout [kubectl exec](kubectl/kubectl_exec.md). +How do I attach to a process that is already running in a container? Checkout [kubectl attach](kubectl/kubectl_attach.md) With docker: @@ -109,18 +110,47 @@ With docker: $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a9ec34d98787 nginx "nginx -g 'daemon of 8 minutes ago Up 8 minutes 0.0.0.0:80->80/tcp, 443/tcp nginx-app -$ docker exec a9ec34d98787 cat /etc/hostname -a9ec34d98787 +$ docker attach -it a9ec34d98787 +... ``` With kubectl: ```console +$ kubectl get pods +NAME READY STATUS RESTARTS AGE +nginx-app-5jyvm 1/1 Running 0 10m +$ kubectl attach -it nginx-app-5jyvm +... + +``` + +#### docker exec + +How do I execute a command in a container? Checkout [kubectl exec](kubectl/kubectl_exec.md). + +With docker: + +```console + +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +a9ec34d98787 nginx "nginx -g 'daemon of 8 minutes ago Up 8 minutes 0.0.0.0:80->80/tcp, 443/tcp nginx-app +$ docker exec a9ec34d98787 cat /etc/hostname +a9ec34d98787 + +``` + +With kubectl: + +```console + $ kubectl get po NAME READY STATUS RESTARTS AGE nginx-app-5jyvm 1/1 Running 0 10m $ kubectl exec nginx-app-5jyvm -- cat /etc/hostname nginx-app-5jyvm + ``` What about interactive commands? @@ -129,15 +159,21 @@ What about interactive commands? With docker: ```console + $ docker exec -ti a9ec34d98787 /bin/sh + # exit + ``` With kubectl: ```console + $ kubectl exec -ti nginx-app-5jyvm -- /bin/sh + # exit + ``` For more information see [Getting into containers](getting-into-containers.md). @@ -150,25 +186,31 @@ How do I follow stdout/stderr of a running process? Checkout [kubectl logs](kube With docker: ```console + $ docker logs -f a9e 192.168.9.1 - - [14/Jul/2015:01:04:02 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.35.0" "-" 192.168.9.1 - - [14/Jul/2015:01:04:03 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.35.0" "-" + ``` With kubectl: ```console + $ kubectl logs -f nginx-app-zibvs 10.240.63.110 - - [14/Jul/2015:01:09:01 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.26.0" "-" 10.240.63.110 - - [14/Jul/2015:01:09:02 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.26.0" "-" + ``` Now's a good time to mention slight difference between pods and containers; by default pods will not terminate if their processes exit. Instead it will restart the process. This is similar to the docker run option `--restart=always` with one major difference. In docker, the output for each invocation of the process is concatenated but for Kubernetes, each invokation is separate. To see the output from a prevoius run in Kubernetes, do this: ```console + $ kubectl logs --previous nginx-app-zibvs 10.240.63.110 - - [14/Jul/2015:01:09:01 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.26.0" "-" 10.240.63.110 - - [14/Jul/2015:01:09:02 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.26.0" "-" + ``` See [Logging](logging.md) for more information. @@ -180,6 +222,7 @@ How do I stop and delete a running process? Checkout [kubectl delete](kubectl/ku With docker ```console + $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a9ec34d98787 nginx "nginx -g 'daemon of 22 hours ago Up 22 hours 0.0.0.0:80->80/tcp, 443/tcp nginx-app @@ -187,11 +230,13 @@ $ docker stop a9ec34d98787 a9ec34d98787 $ docker rm a9ec34d98787 a9ec34d98787 + ``` With kubectl: ```console + $ kubectl get rc nginx-app CONTROLLER CONTAINER(S) IMAGE(S) SELECTOR REPLICAS nginx-app nginx-app nginx run=nginx-app 1 @@ -203,6 +248,7 @@ NAME READY STATUS RESTARTS AGE nginx-app-aualv 1/1 Running 0 16s $ kubectl get po NAME READY STATUS RESTARTS AGE + ``` Notice that we don't delete the pod directly. With kubectl we want to delete the replication controller that owns the pod. If we delete the pod directly, the replication controller will recreate the pod. @@ -218,6 +264,7 @@ How do I get the version of my client and server? Checkout [kubectl version](kub With docker: ```console + $ docker version Client version: 1.7.0 Client API version: 1.19 @@ -229,14 +276,17 @@ Server API version: 1.19 Go version (server): go1.4.2 Git commit (server): 0baf609 OS/Arch (server): linux/amd64 + ``` With kubectl: ```console + $ kubectl version Client Version: version.Info{Major:"0", Minor:"20.1", GitVersion:"v0.20.1", GitCommit:"", GitTreeState:"not a git tree"} Server Version: version.Info{Major:"0", Minor:"21+", GitVersion:"v0.21.1-411-g32699e873ae1ca-dirty", GitCommit:"32699e873ae1caa01812e41de7eab28df4358ee4", GitTreeState:"dirty"} + ``` #### docker info @@ -246,6 +296,7 @@ How do I get miscellaneous info about my environment and configuration? Checkout With docker: ```console + $ docker info Containers: 40 Images: 168 @@ -263,11 +314,13 @@ Total Memory: 31.32 GiB Name: k8s-is-fun.mtv.corp.google.com ID: ADUV:GCYR:B3VJ:HMPO:LNPQ:KD5S:YKFQ:76VN:IANZ:7TFV:ZBF4:BYJO WARNING: No swap limit support + ``` With kubectl: ```console + $ kubectl cluster-info Kubernetes master is running at https://108.59.85.141 KubeDNS is running at https://108.59.85.141/api/v1/proxy/namespaces/kube-system/services/kube-dns @@ -275,6 +328,7 @@ KubeUI is running at https://108.59.85.141/api/v1/proxy/namespaces/kube-system/s Grafana is running at https://108.59.85.141/api/v1/proxy/namespaces/kube-system/services/monitoring-grafana Heapster is running at https://108.59.85.141/api/v1/proxy/namespaces/kube-system/services/monitoring-heapster InfluxDB is running at https://108.59.85.141/api/v1/proxy/namespaces/kube-system/services/monitoring-influxdb + ``` diff --git a/docs/user-guide/kubectl/.files_generated b/docs/user-guide/kubectl/.files_generated index 71996881a42..73e9a345778 100644 --- a/docs/user-guide/kubectl/.files_generated +++ b/docs/user-guide/kubectl/.files_generated @@ -1,5 +1,6 @@ kubectl.md kubectl_api-versions.md +kubectl_attach.md kubectl_cluster-info.md kubectl_config.md kubectl_config_set-cluster.md diff --git a/docs/user-guide/kubectl/kubectl.md b/docs/user-guide/kubectl/kubectl.md index 5cf0e4d5c4b..5a5ce37db74 100644 --- a/docs/user-guide/kubectl/kubectl.md +++ b/docs/user-guide/kubectl/kubectl.md @@ -79,6 +79,7 @@ kubectl ### SEE ALSO * [kubectl api-versions](kubectl_api-versions.md) - Print available API versions. +* [kubectl attach](kubectl_attach.md) - Attach to a running container. * [kubectl cluster-info](kubectl_cluster-info.md) - Display cluster info * [kubectl config](kubectl_config.md) - config modifies kubeconfig files * [kubectl create](kubectl_create.md) - Create a resource by filename or stdin @@ -100,7 +101,7 @@ kubectl * [kubectl stop](kubectl_stop.md) - Deprecated: Gracefully shut down a resource by name or filename. * [kubectl version](kubectl_version.md) - Print the client and server version information. -###### Auto generated by spf13/cobra at 2015-07-29 09:18:59.541696918 +0000 UTC +###### Auto generated by spf13/cobra at 2015-07-30 03:45:17.319803488 +0000 UTC diff --git a/docs/user-guide/kubectl/kubectl_attach.md b/docs/user-guide/kubectl/kubectl_attach.md new file mode 100644 index 00000000000..a1e51cc6386 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_attach.md @@ -0,0 +1,108 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +If you are using a released version of Kubernetes, you should +refer to the docs that go with that version. + + +The latest 1.0.x release of this document can be found +[here](http://releases.k8s.io/release-1.0/docs/user-guide/kubectl/kubectl_attach.md). + +Documentation for other releases can be found at +[releases.k8s.io](http://releases.k8s.io). + +-- + + + + + +## kubectl attach + +Attach to a running container. + +### Synopsis + + +Attach to a a process that is already running inside an existing container. + +``` +kubectl attach POD -c CONTAINER +``` + +### Examples + +``` +// get output from running pod 123456-7890, using the first container by default +$ kubectl attach 123456-7890 + +// get output from ruby-container from pod 123456-7890 +$ kubectl attach 123456-7890 -c ruby-container date + +// switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod 123456-780 +// and sends stdout/stderr from 'bash' back to the client +$ kubectl attach 123456-7890 -c ruby-container -i -t +``` + +### Options + +``` + -c, --container="": Container name + -h, --help=false: help for attach + -i, --stdin=false: Pass stdin to the container + -t, --tty=false: Stdin is a TTY +``` + +### Options inherited from parent commands + +``` + --alsologtostderr=false: log to standard error as well as files + --api-version="": The API version to use when talking to the server + --certificate-authority="": Path to a cert. file for the certificate authority. + --client-certificate="": Path to a client key file for TLS. + --client-key="": Path to a client key file for TLS. + --cluster="": The name of the kubeconfig cluster to use + --context="": The name of the kubeconfig context to use + --insecure-skip-tls-verify=false: If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure. + --kubeconfig="": Path to the kubeconfig file to use for CLI requests. + --log-backtrace-at=:0: when logging hits line file:N, emit a stack trace + --log-dir=: If non-empty, write log files in this directory + --log-flush-frequency=5s: Maximum number of seconds between log flushes + --logtostderr=true: log to standard error instead of files + --match-server-version=false: Require server version to match client version + --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. + -s, --server="": The address and port of the Kubernetes API server + --stderrthreshold=2: logs at or above this threshold go to stderr + --token="": Bearer token for authentication to the API server. + --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. + --v=0: log level for V logs + --validate=false: If true, use a schema to validate the input before sending it + --vmodule=: comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO + +* [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager + +###### Auto generated by spf13/cobra at 2015-07-30 17:45:25.860905122 +0000 UTC + + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_attach.md?pixel)]() + diff --git a/pkg/client/remotecommand/remotecommand.go b/pkg/client/remotecommand/remotecommand.go index 09ce4317ff3..666b7065142 100644 --- a/pkg/client/remotecommand/remotecommand.go +++ b/pkg/client/remotecommand/remotecommand.go @@ -25,6 +25,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion/queryparams" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/httpstream" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/httpstream/spdy" "github.com/golang/glog" @@ -40,32 +41,74 @@ func (u *defaultUpgrader) upgrade(req *client.Request, config *client.Config) (h return req.Upgrade(config, spdy.NewRoundTripper) } -// Executor executes a command on a pod container -type Executor struct { - req *client.Request - config *client.Config - command []string - stdin io.Reader - stdout io.Writer - stderr io.Writer - tty bool +type Streamer struct { + req *client.Request + config *client.Config + stdin io.Reader + stdout io.Writer + stderr io.Writer + tty bool upgrader upgrader } +// Executor executes a command on a pod container +type Executor struct { + Streamer + command []string +} + // New creates a new RemoteCommandExecutor func New(req *client.Request, config *client.Config, command []string, stdin io.Reader, stdout, stderr io.Writer, tty bool) *Executor { return &Executor{ - req: req, - config: config, command: command, - stdin: stdin, - stdout: stdout, - stderr: stderr, - tty: tty, + Streamer: Streamer{ + req: req, + config: config, + stdin: stdin, + stdout: stdout, + stderr: stderr, + tty: tty, + }, } } +type Attach struct { + Streamer +} + +// NewAttach creates a new RemoteAttach +func NewAttach(req *client.Request, config *client.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool) *Attach { + return &Attach{ + Streamer: Streamer{ + req: req, + config: config, + stdin: stdin, + stdout: stdout, + stderr: stderr, + tty: tty, + }, + } +} + +// Execute sends a remote command execution request, upgrading the +// connection and creating streams to represent stdin/stdout/stderr. Data is +// copied between these streams and the supplied stdin/stdout/stderr parameters. +func (e *Attach) Execute() error { + opts := api.PodAttachOptions{ + Stdin: (e.stdin != nil), + Stdout: (e.stdout != nil), + Stderr: (!e.tty && e.stderr != nil), + TTY: e.tty, + } + + if err := e.setupRequestParameters(&opts); err != nil { + return err + } + + return e.doStream() +} + // Execute sends a remote command execution request, upgrading the // connection and creating streams to represent stdin/stdout/stderr. Data is // copied between these streams and the supplied stdin/stdout/stderr parameters. @@ -78,7 +121,15 @@ func (e *Executor) Execute() error { Command: e.command, } - versioned, err := api.Scheme.ConvertToVersion(&opts, e.config.Version) + if err := e.setupRequestParameters(&opts); err != nil { + return err + } + + return e.doStream() +} + +func (e *Streamer) setupRequestParameters(obj runtime.Object) error { + versioned, err := api.Scheme.ConvertToVersion(obj, e.config.Version) if err != nil { return err } @@ -91,7 +142,10 @@ func (e *Executor) Execute() error { e.req.Param(k, vv) } } + return nil +} +func (e *Streamer) doStream() error { if e.upgrader == nil { e.upgrader = &defaultUpgrader{} } @@ -134,7 +188,7 @@ func (e *Executor) Execute() error { }() defer errorStream.Reset() - if opts.Stdin { + if e.stdin != nil { headers.Set(api.StreamType, api.StreamTypeStdin) remoteStdin, err := conn.CreateStream(headers) if err != nil { @@ -151,7 +205,7 @@ func (e *Executor) Execute() error { waitCount := 0 completedStreams := 0 - if opts.Stdout { + if e.stdout != nil { waitCount++ headers.Set(api.StreamType, api.StreamTypeStdout) remoteStdout, err := conn.CreateStream(headers) @@ -162,7 +216,7 @@ func (e *Executor) Execute() error { go cp(api.StreamTypeStdout, e.stdout, remoteStdout) } - if opts.Stderr && !e.tty { + if e.stderr != nil && !e.tty { waitCount++ headers.Set(api.StreamType, api.StreamTypeStderr) remoteStderr, err := conn.CreateStream(headers) diff --git a/pkg/client/remotecommand/remotecommand_test.go b/pkg/client/remotecommand/remotecommand_test.go index 72f96d44b23..9e247fe37d2 100644 --- a/pkg/client/remotecommand/remotecommand_test.go +++ b/pkg/client/remotecommand/remotecommand_test.go @@ -190,3 +190,80 @@ func TestRequestExecuteRemoteCommand(t *testing.T) { server.Close() } } + +// TODO: this test is largely cut and paste, refactor to share code +func TestRequestAttachRemoteCommand(t *testing.T) { + testCases := []struct { + Stdin string + Stdout string + Stderr string + Error string + Tty bool + }{ + { + Error: "bail", + }, + { + Stdin: "a", + Stdout: "b", + Stderr: "c", + }, + { + Stdin: "a", + Stdout: "b", + Tty: true, + }, + } + + for i, testCase := range testCases { + localOut := &bytes.Buffer{} + localErr := &bytes.Buffer{} + + server := httptest.NewServer(fakeExecServer(t, i, testCase.Stdin, testCase.Stdout, testCase.Stderr, testCase.Error, testCase.Tty)) + + url, _ := url.ParseRequestURI(server.URL) + c := client.NewRESTClient(url, "x", nil, -1, -1) + req := c.Post().Resource("testing") + + conf := &client.Config{ + Host: server.URL, + } + e := NewAttach(req, conf, strings.NewReader(testCase.Stdin), localOut, localErr, testCase.Tty) + //e.upgrader = testCase.Upgrader + err := e.Execute() + hasErr := err != nil + + if len(testCase.Error) > 0 { + if !hasErr { + t.Errorf("%d: expected an error", i) + } else { + if e, a := testCase.Error, err.Error(); !strings.Contains(a, e) { + t.Errorf("%d: expected error stream read '%v', got '%v'", i, e, a) + } + } + + server.Close() + continue + } + + if hasErr { + t.Errorf("%d: unexpected error: %v", i, err) + server.Close() + continue + } + + if len(testCase.Stdout) > 0 { + if e, a := testCase.Stdout, localOut; e != a.String() { + t.Errorf("%d: expected stdout data '%s', got '%s'", i, e, a) + } + } + + if testCase.Stderr != "" { + if e, a := testCase.Stderr, localErr; e != a.String() { + t.Errorf("%d: expected stderr data '%s', got '%s'", i, e, a) + } + } + + server.Close() + } +} diff --git a/pkg/kubectl/cmd/attach.go b/pkg/kubectl/cmd/attach.go new file mode 100644 index 00000000000..17e9e50290f --- /dev/null +++ b/pkg/kubectl/cmd/attach.go @@ -0,0 +1,211 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "io" + "os" + "os/signal" + "syscall" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/remotecommand" + cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" + "github.com/docker/docker/pkg/term" + "github.com/golang/glog" + "github.com/spf13/cobra" +) + +const ( + attach_example = `// get output from running pod 123456-7890, using the first container by default +$ kubectl attach 123456-7890 + +// get output from ruby-container from pod 123456-7890 +$ kubectl attach 123456-7890 -c ruby-container date + +// switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod 123456-780 +// and sends stdout/stderr from 'bash' back to the client +$ kubectl attach 123456-7890 -c ruby-container -i -t` +) + +func NewCmdAttach(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command { + options := &AttachOptions{ + In: cmdIn, + Out: cmdOut, + Err: cmdErr, + + Attach: &DefaultRemoteAttach{}, + } + cmd := &cobra.Command{ + Use: "attach POD -c CONTAINER", + Short: "Attach to a running container.", + Long: "Attach to a a process that is already running inside an existing container.", + Example: attach_example, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(options.Complete(f, cmd, args)) + cmdutil.CheckErr(options.Validate()) + cmdutil.CheckErr(options.Run()) + }, + } + // TODO support UID + cmd.Flags().StringVarP(&options.ContainerName, "container", "c", "", "Container name") + cmd.Flags().BoolVarP(&options.Stdin, "stdin", "i", false, "Pass stdin to the container") + cmd.Flags().BoolVarP(&options.TTY, "tty", "t", false, "Stdin is a TTY") + return cmd +} + +// RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing +type RemoteAttach interface { + Attach(req *client.Request, config *client.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool) error +} + +// DefaultRemoteAttach is the standard implementation of attaching +type DefaultRemoteAttach struct{} + +func (*DefaultRemoteAttach) Attach(req *client.Request, config *client.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool) error { + attach := remotecommand.NewAttach(req, config, stdin, stdout, stderr, tty) + return attach.Execute() +} + +// AttachOptions declare the arguments accepted by the Exec command +type AttachOptions struct { + Namespace string + PodName string + ContainerName string + Stdin bool + TTY bool + + In io.Reader + Out io.Writer + Err io.Writer + + Attach RemoteAttach + Client *client.Client + Config *client.Config +} + +// Complete verifies command line arguments and loads data from the command environment +func (p *AttachOptions) Complete(f *cmdutil.Factory, cmd *cobra.Command, argsIn []string) error { + if len(argsIn) == 0 { + return cmdutil.UsageError(cmd, "POD is required for attach") + } + if len(argsIn) > 1 { + return cmdutil.UsageError(cmd, fmt.Sprintf("expected a single argument: POD, saw %d: %s", len(argsIn), argsIn)) + } + p.PodName = argsIn[0] + + namespace, _, err := f.DefaultNamespace() + if err != nil { + return err + } + p.Namespace = namespace + + config, err := f.ClientConfig() + if err != nil { + return err + } + p.Config = config + + client, err := f.Client() + if err != nil { + return err + } + p.Client = client + + return nil +} + +// Validate checks that the provided attach options are specified. +func (p *AttachOptions) Validate() error { + if len(p.PodName) == 0 { + return fmt.Errorf("pod name must be specified") + } + if p.Out == nil || p.Err == nil { + return fmt.Errorf("both output and error output must be provided") + } + if p.Attach == nil || p.Client == nil || p.Config == nil { + return fmt.Errorf("client, client config, and attach must be provided") + } + return nil +} + +// Run executes a validated remote execution against a pod. +func (p *AttachOptions) Run() error { + pod, err := p.Client.Pods(p.Namespace).Get(p.PodName) + if err != nil { + return err + } + + if pod.Status.Phase != api.PodRunning { + return fmt.Errorf("pod %s is not running and cannot be attached to; current phase is %s", p.PodName, pod.Status.Phase) + } + + containerName := p.ContainerName + if len(containerName) == 0 { + glog.V(4).Infof("defaulting container name to %s", pod.Spec.Containers[0].Name) + containerName = pod.Spec.Containers[0].Name + } + + // TODO: refactor with terminal helpers from the edit utility once that is merged + var stdin io.Reader + tty := p.TTY + if p.Stdin { + stdin = p.In + if tty { + if file, ok := stdin.(*os.File); ok { + inFd := file.Fd() + if term.IsTerminal(inFd) { + oldState, err := term.SetRawTerminal(inFd) + if err != nil { + glog.Fatal(err) + } + // this handles a clean exit, where the command finished + defer term.RestoreTerminal(inFd, oldState) + + // SIGINT is handled by term.SetRawTerminal (it runs a goroutine that listens + // for SIGINT and restores the terminal before exiting) + + // this handles SIGTERM + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGTERM) + go func() { + <-sigChan + term.RestoreTerminal(inFd, oldState) + os.Exit(0) + }() + } else { + fmt.Fprintln(p.Err, "STDIN is not a terminal") + } + } else { + tty = false + fmt.Fprintln(p.Err, "Unable to use a TTY - input is not the right kind of file") + } + } + } + + // TODO: consider abstracting into a client invocation or client helper + req := p.Client.RESTClient.Post(). + Resource("pods"). + Name(pod.Name). + Namespace(pod.Namespace). + SubResource("attach"). + Param("container", containerName) + + return p.Attach.Attach(req, p.Config, stdin, p.Out, p.Err, tty) +} diff --git a/pkg/kubectl/cmd/attach_test.go b/pkg/kubectl/cmd/attach_test.go new file mode 100644 index 00000000000..ab2f610ca49 --- /dev/null +++ b/pkg/kubectl/cmd/attach_test.go @@ -0,0 +1,198 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bytes" + "fmt" + "io" + "net/http" + "testing" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +type fakeRemoteAttach struct { + req *client.Request + attachErr error +} + +func (f *fakeRemoteAttach) Attach(req *client.Request, config *client.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool) error { + f.req = req + return f.attachErr +} + +func TestPodAndContainerAttach(t *testing.T) { + tests := []struct { + args []string + p *AttachOptions + name string + expectError bool + expectedPod string + expectedContainer string + }{ + { + p: &AttachOptions{}, + expectError: true, + name: "empty", + }, + { + p: &AttachOptions{}, + args: []string{"foo", "bar"}, + expectError: true, + name: "too many args", + }, + { + p: &AttachOptions{}, + args: []string{"foo"}, + expectedPod: "foo", + name: "no container, no flags", + }, + { + p: &AttachOptions{ContainerName: "bar"}, + args: []string{"foo"}, + expectedPod: "foo", + expectedContainer: "bar", + name: "container in flag", + }, + } + for _, test := range tests { + f, tf, codec := NewAPIFactory() + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { return nil, nil }), + } + tf.Namespace = "test" + tf.ClientConfig = &client.Config{} + + cmd := &cobra.Command{} + options := test.p + err := options.Complete(f, cmd, test.args) + if test.expectError && err == nil { + t.Errorf("unexpected non-error (%s)", test.name) + } + if !test.expectError && err != nil { + t.Errorf("unexpected error: %v (%s)", err, test.name) + } + if err != nil { + continue + } + if options.PodName != test.expectedPod { + t.Errorf("expected: %s, got: %s (%s)", test.expectedPod, options.PodName, test.name) + } + if options.ContainerName != test.expectedContainer { + t.Errorf("expected: %s, got: %s (%s)", test.expectedContainer, options.ContainerName, test.name) + } + } +} + +func TestAttach(t *testing.T) { + version := testapi.Version() + tests := []struct { + name, version, podPath, attachPath, container string + pod *api.Pod + attachErr bool + }{ + { + name: "pod attach", + version: version, + podPath: "/api/" + version + "/namespaces/test/pods/foo", + attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach", + pod: attachPod(), + }, + { + name: "pod attach error", + version: version, + podPath: "/api/" + version + "/namespaces/test/pods/foo", + attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach", + pod: attachPod(), + attachErr: true, + }, + } + for _, test := range tests { + f, tf, codec := NewAPIFactory() + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == test.podPath && m == "GET": + body := objBody(codec, test.pod) + return &http.Response{StatusCode: 200, Body: body}, nil + default: + // Ensures no GET is performed when deleting by name + t.Errorf("%s: unexpected request: %s %#v\n%#v", test.name, req.Method, req.URL, req) + return nil, fmt.Errorf("unexpected request") + } + }), + } + tf.Namespace = "test" + tf.ClientConfig = &client.Config{Version: test.version} + bufOut := bytes.NewBuffer([]byte{}) + bufErr := bytes.NewBuffer([]byte{}) + bufIn := bytes.NewBuffer([]byte{}) + ex := &fakeRemoteAttach{} + if test.attachErr { + ex.attachErr = fmt.Errorf("attach error") + } + params := &AttachOptions{ + ContainerName: "bar", + In: bufIn, + Out: bufOut, + Err: bufErr, + Attach: ex, + } + cmd := &cobra.Command{} + if err := params.Complete(f, cmd, []string{"foo"}); err != nil { + t.Fatal(err) + } + err := params.Run() + if test.attachErr && err != ex.attachErr { + t.Errorf("%s: Unexpected exec error: %v", test.name, err) + continue + } + if !test.attachErr && err != nil { + t.Errorf("%s: Unexpected error: %v", test.name, err) + continue + } + if !test.attachErr && ex.req.URL().Path != test.attachPath { + t.Errorf("%s: Did not get expected path for exec request", test.name) + continue + } + } +} + +func attachPod() *api.Pod { + return &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"}, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{ + { + Name: "bar", + }, + }, + }, + Status: api.PodStatus{ + Phase: api.PodRunning, + }, + } +} diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index f08e96514c3..b856849d658 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -135,6 +135,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(NewCmdRollingUpdate(f, out)) cmds.AddCommand(NewCmdScale(f, out)) + cmds.AddCommand(NewCmdAttach(f, in, out, err)) cmds.AddCommand(NewCmdExec(f, in, out, err)) cmds.AddCommand(NewCmdPortForward(f)) cmds.AddCommand(NewCmdProxy(f, out))