From e8e756a719db275b81a2ec09e135c93b7945ab52 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Tue, 28 Jul 2015 15:56:27 -0700 Subject: [PATCH] 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