2014-06-06 23:40:48 +00:00
|
|
|
/*
|
2015-05-01 16:19:44 +00:00
|
|
|
Copyright 2014 The Kubernetes Authors All rights reserved.
|
2014-06-06 23:40:48 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
2014-06-08 23:10:29 +00:00
|
|
|
|
2014-06-06 23:40:48 +00:00
|
|
|
package kubelet
|
|
|
|
|
|
|
|
import (
|
2015-03-05 21:30:52 +00:00
|
|
|
"crypto/tls"
|
2014-06-19 01:26:23 +00:00
|
|
|
"encoding/json"
|
2014-07-01 21:05:10 +00:00
|
|
|
"errors"
|
2014-06-06 23:40:48 +00:00
|
|
|
"fmt"
|
2014-07-14 21:48:51 +00:00
|
|
|
"io"
|
2014-07-15 20:24:41 +00:00
|
|
|
"net"
|
2014-06-06 23:40:48 +00:00
|
|
|
"net/http"
|
2015-06-04 06:51:14 +00:00
|
|
|
"net/http/pprof"
|
2014-07-01 21:05:10 +00:00
|
|
|
"path"
|
2014-07-15 20:24:41 +00:00
|
|
|
"strconv"
|
2014-07-01 21:05:10 +00:00
|
|
|
"strings"
|
2015-01-08 20:41:38 +00:00
|
|
|
"sync"
|
2014-07-15 20:24:41 +00:00
|
|
|
"time"
|
2014-06-06 23:40:48 +00:00
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
restful "github.com/emicklei/go-restful"
|
2015-08-05 22:05:17 +00:00
|
|
|
"github.com/golang/glog"
|
2015-10-16 03:00:28 +00:00
|
|
|
cadvisorapi "github.com/google/cadvisor/info/v1"
|
2015-08-05 22:05:17 +00:00
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
2015-09-29 03:32:20 +00:00
|
|
|
|
2015-08-05 22:03:47 +00:00
|
|
|
"k8s.io/kubernetes/pkg/api"
|
2015-09-10 03:46:11 +00:00
|
|
|
apierrs "k8s.io/kubernetes/pkg/api/errors"
|
2015-08-05 22:03:47 +00:00
|
|
|
"k8s.io/kubernetes/pkg/api/latest"
|
2015-09-10 03:46:11 +00:00
|
|
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
|
|
|
"k8s.io/kubernetes/pkg/api/v1"
|
|
|
|
"k8s.io/kubernetes/pkg/api/validation"
|
2015-09-29 03:32:20 +00:00
|
|
|
"k8s.io/kubernetes/pkg/auth/authenticator"
|
|
|
|
"k8s.io/kubernetes/pkg/auth/authorizer"
|
2015-10-20 12:21:07 +00:00
|
|
|
"k8s.io/kubernetes/pkg/client/unversioned/remotecommand"
|
2015-08-05 22:03:47 +00:00
|
|
|
"k8s.io/kubernetes/pkg/healthz"
|
|
|
|
"k8s.io/kubernetes/pkg/httplog"
|
|
|
|
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
2015-10-21 20:04:10 +00:00
|
|
|
"k8s.io/kubernetes/pkg/kubelet/dockertools"
|
2015-10-22 02:37:26 +00:00
|
|
|
"k8s.io/kubernetes/pkg/kubelet/portforward"
|
2015-08-05 22:03:47 +00:00
|
|
|
"k8s.io/kubernetes/pkg/types"
|
2015-09-22 20:29:51 +00:00
|
|
|
"k8s.io/kubernetes/pkg/util"
|
2015-08-05 22:03:47 +00:00
|
|
|
"k8s.io/kubernetes/pkg/util/flushwriter"
|
|
|
|
"k8s.io/kubernetes/pkg/util/httpstream"
|
|
|
|
"k8s.io/kubernetes/pkg/util/httpstream/spdy"
|
2015-09-10 03:46:11 +00:00
|
|
|
"k8s.io/kubernetes/pkg/util/limitwriter"
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
"k8s.io/kubernetes/pkg/util/wsstream"
|
2014-06-06 23:40:48 +00:00
|
|
|
)
|
|
|
|
|
2014-07-15 13:54:23 +00:00
|
|
|
// Server is a http.Handler which exposes kubelet functionality over HTTP.
|
|
|
|
type Server struct {
|
2015-09-29 03:32:20 +00:00
|
|
|
auth AuthInterface
|
2015-08-17 01:40:08 +00:00
|
|
|
host HostInterface
|
2015-09-29 03:32:20 +00:00
|
|
|
restfulCont containerInterface
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
|
|
|
|
2015-03-05 21:30:52 +00:00
|
|
|
type TLSOptions struct {
|
|
|
|
Config *tls.Config
|
|
|
|
CertFile string
|
|
|
|
KeyFile string
|
|
|
|
}
|
|
|
|
|
2015-09-29 03:32:20 +00:00
|
|
|
// containerInterface defines the restful.Container functions used on the root container
|
|
|
|
type containerInterface interface {
|
|
|
|
Add(service *restful.WebService) *restful.Container
|
|
|
|
Handle(path string, handler http.Handler)
|
|
|
|
Filter(filter restful.FilterFunction)
|
|
|
|
ServeHTTP(w http.ResponseWriter, r *http.Request)
|
|
|
|
RegisteredWebServices() []*restful.WebService
|
|
|
|
|
|
|
|
// RegisteredHandlePaths returns the paths of handlers registered directly with the container (non-web-services)
|
|
|
|
// Used to test filters are being applied on non-web-service handlers
|
|
|
|
RegisteredHandlePaths() []string
|
|
|
|
}
|
|
|
|
|
|
|
|
// filteringContainer delegates all Handle(...) calls to Container.HandleWithFilter(...),
|
|
|
|
// so we can ensure restful.FilterFunctions are used for all handlers
|
|
|
|
type filteringContainer struct {
|
|
|
|
*restful.Container
|
|
|
|
registeredHandlePaths []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *filteringContainer) Handle(path string, handler http.Handler) {
|
|
|
|
a.HandleWithFilter(path, handler)
|
|
|
|
a.registeredHandlePaths = append(a.registeredHandlePaths, path)
|
|
|
|
}
|
|
|
|
func (a *filteringContainer) RegisteredHandlePaths() []string {
|
|
|
|
return a.registeredHandlePaths
|
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// ListenAndServeKubeletServer initializes a server to respond to HTTP network requests on the Kubelet.
|
2015-09-29 03:32:20 +00:00
|
|
|
func ListenAndServeKubeletServer(host HostInterface, address net.IP, port uint, tlsOptions *TLSOptions, auth AuthInterface, enableDebuggingHandlers bool) {
|
2015-04-08 20:57:19 +00:00
|
|
|
glog.Infof("Starting to listen on %s:%d", address, port)
|
2015-09-29 03:32:20 +00:00
|
|
|
handler := NewServer(host, auth, enableDebuggingHandlers)
|
2014-07-15 20:24:41 +00:00
|
|
|
s := &http.Server{
|
2014-10-04 04:34:30 +00:00
|
|
|
Addr: net.JoinHostPort(address.String(), strconv.FormatUint(uint64(port), 10)),
|
2014-07-15 20:24:41 +00:00
|
|
|
Handler: &handler,
|
|
|
|
MaxHeaderBytes: 1 << 20,
|
|
|
|
}
|
2015-03-05 21:30:52 +00:00
|
|
|
if tlsOptions != nil {
|
|
|
|
s.TLSConfig = tlsOptions.Config
|
|
|
|
glog.Fatal(s.ListenAndServeTLS(tlsOptions.CertFile, tlsOptions.KeyFile))
|
|
|
|
} else {
|
|
|
|
glog.Fatal(s.ListenAndServe())
|
|
|
|
}
|
2014-07-15 20:24:41 +00:00
|
|
|
}
|
|
|
|
|
2015-04-02 04:41:32 +00:00
|
|
|
// ListenAndServeKubeletReadOnlyServer initializes a server to respond to HTTP network requests on the Kubelet.
|
|
|
|
func ListenAndServeKubeletReadOnlyServer(host HostInterface, address net.IP, port uint) {
|
|
|
|
glog.V(1).Infof("Starting to listen read-only on %s:%d", address, port)
|
2015-09-29 03:32:20 +00:00
|
|
|
s := NewServer(host, nil, false)
|
2015-04-02 04:41:32 +00:00
|
|
|
|
|
|
|
server := &http.Server{
|
|
|
|
Addr: net.JoinHostPort(address.String(), strconv.FormatUint(uint64(port), 10)),
|
2015-06-17 22:31:46 +00:00
|
|
|
Handler: &s,
|
2015-04-02 04:41:32 +00:00
|
|
|
MaxHeaderBytes: 1 << 20,
|
|
|
|
}
|
|
|
|
glog.Fatal(server.ListenAndServe())
|
|
|
|
}
|
|
|
|
|
2015-09-29 03:32:20 +00:00
|
|
|
// AuthInterface contains all methods required by the auth filters
|
|
|
|
type AuthInterface interface {
|
|
|
|
authenticator.Request
|
|
|
|
authorizer.RequestAttributesGetter
|
|
|
|
authorizer.Authorizer
|
|
|
|
}
|
|
|
|
|
2014-07-15 20:24:41 +00:00
|
|
|
// HostInterface contains all the kubelet methods required by the server.
|
2014-06-08 23:10:29 +00:00
|
|
|
// For testablitiy.
|
2014-07-15 20:24:41 +00:00
|
|
|
type HostInterface interface {
|
2015-10-16 03:00:28 +00:00
|
|
|
GetContainerInfo(podFullName string, uid types.UID, containerName string, req *cadvisorapi.ContainerInfoRequest) (*cadvisorapi.ContainerInfo, error)
|
2015-04-21 20:02:50 +00:00
|
|
|
GetContainerRuntimeVersion() (kubecontainer.Version, error)
|
2015-10-16 03:00:28 +00:00
|
|
|
GetRawContainerInfo(containerName string, req *cadvisorapi.ContainerInfoRequest, subcontainers bool) (map[string]*cadvisorapi.ContainerInfo, error)
|
|
|
|
GetCachedMachineInfo() (*cadvisorapi.MachineInfo, error)
|
2015-04-03 22:51:50 +00:00
|
|
|
GetPods() []*api.Pod
|
2015-06-23 23:01:12 +00:00
|
|
|
GetRunningPods() ([]*api.Pod, error)
|
2015-03-13 13:19:07 +00:00
|
|
|
GetPodByName(namespace, name string) (*api.Pod, bool)
|
2015-01-14 23:22:21 +00:00
|
|
|
RunInContainer(name string, uid types.UID, container string, cmd []string) ([]byte, error)
|
2015-01-08 20:41:38 +00:00
|
|
|
ExecInContainer(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
2015-07-28 04:48:55 +00:00
|
|
|
AttachContainer(name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
2015-09-10 03:46:11 +00:00
|
|
|
GetKubeletContainerLogs(podFullName, containerName string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) error
|
2014-07-15 07:04:30 +00:00
|
|
|
ServeLogs(w http.ResponseWriter, req *http.Request)
|
2015-01-08 20:41:38 +00:00
|
|
|
PortForward(name string, uid types.UID, port uint16, stream io.ReadWriteCloser) error
|
|
|
|
StreamingConnectionIdleTimeout() time.Duration
|
2015-06-17 22:31:46 +00:00
|
|
|
ResyncInterval() time.Duration
|
2015-02-09 16:40:42 +00:00
|
|
|
GetHostname() string
|
2015-06-17 22:31:46 +00:00
|
|
|
LatestLoopEntryTime() time.Time
|
2014-06-08 23:10:29 +00:00
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// NewServer initializes and configures a kubelet.Server object to handle HTTP requests.
|
2015-09-29 03:32:20 +00:00
|
|
|
func NewServer(host HostInterface, auth AuthInterface, enableDebuggingHandlers bool) Server {
|
2014-08-20 18:24:51 +00:00
|
|
|
server := Server{
|
2015-08-17 01:40:08 +00:00
|
|
|
host: host,
|
2015-09-29 03:32:20 +00:00
|
|
|
auth: auth,
|
|
|
|
restfulCont: &filteringContainer{Container: restful.NewContainer()},
|
|
|
|
}
|
|
|
|
if auth != nil {
|
|
|
|
server.InstallAuthFilter()
|
2014-08-20 18:24:51 +00:00
|
|
|
}
|
|
|
|
server.InstallDefaultHandlers()
|
2014-10-09 23:26:34 +00:00
|
|
|
if enableDebuggingHandlers {
|
|
|
|
server.InstallDebuggingHandlers()
|
|
|
|
}
|
2014-08-20 18:24:51 +00:00
|
|
|
return server
|
|
|
|
}
|
|
|
|
|
2015-09-29 03:32:20 +00:00
|
|
|
// InstallAuthFilter installs authentication filters with the restful Container.
|
|
|
|
func (s *Server) InstallAuthFilter() {
|
|
|
|
s.restfulCont.Filter(func(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
|
|
|
// Authenticate
|
|
|
|
u, ok, err := s.auth.AuthenticateRequest(req.Request)
|
|
|
|
if err != nil {
|
|
|
|
glog.Errorf("Unable to authenticate the request due to an error: %v", err)
|
|
|
|
resp.WriteErrorString(http.StatusUnauthorized, "Unauthorized")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
resp.WriteErrorString(http.StatusUnauthorized, "Unauthorized")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get authorization attributes
|
|
|
|
attrs := s.auth.GetRequestAttributes(u, req.Request)
|
|
|
|
|
|
|
|
// Authorize
|
|
|
|
if err := s.auth.Authorize(attrs); err != nil {
|
|
|
|
msg := fmt.Sprintf("Forbidden (user=%s, verb=%s, namespace=%s, resource=%s)", u.GetName(), attrs.GetVerb(), attrs.GetNamespace(), attrs.GetResource())
|
|
|
|
glog.V(2).Info(msg)
|
|
|
|
resp.WriteErrorString(http.StatusForbidden, msg)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Continue
|
|
|
|
chain.ProcessFilter(req, resp)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
// InstallDefaultHandlers registers the default set of supported HTTP request
|
|
|
|
// patterns with the restful Container.
|
2014-08-20 18:24:51 +00:00
|
|
|
func (s *Server) InstallDefaultHandlers() {
|
2015-08-17 01:40:08 +00:00
|
|
|
healthz.InstallHandler(s.restfulCont,
|
2015-03-19 22:56:29 +00:00
|
|
|
healthz.PingHealthz,
|
|
|
|
healthz.NamedCheck("docker", s.dockerHealthCheck),
|
2015-06-17 22:31:46 +00:00
|
|
|
healthz.NamedCheck("syncloop", s.syncLoopHealthCheck),
|
2015-03-19 22:56:29 +00:00
|
|
|
)
|
2015-08-17 01:40:08 +00:00
|
|
|
var ws *restful.WebService
|
|
|
|
ws = new(restful.WebService)
|
|
|
|
ws.
|
|
|
|
Path("/pods").
|
|
|
|
Produces(restful.MIME_JSON)
|
|
|
|
ws.Route(ws.GET("").
|
|
|
|
To(s.getPods).
|
|
|
|
Operation("getPods"))
|
|
|
|
s.restfulCont.Add(ws)
|
|
|
|
|
|
|
|
s.restfulCont.Handle("/stats/", &httpHandler{f: s.handleStats})
|
2015-09-29 03:32:20 +00:00
|
|
|
s.restfulCont.Handle("/metrics", prometheus.Handler())
|
2015-08-17 01:40:08 +00:00
|
|
|
|
|
|
|
ws = new(restful.WebService)
|
|
|
|
ws.
|
|
|
|
Path("/spec/").
|
|
|
|
Produces(restful.MIME_JSON)
|
|
|
|
ws.Route(ws.GET("").
|
|
|
|
To(s.getSpec).
|
|
|
|
Operation("getSpec").
|
2015-10-16 03:00:28 +00:00
|
|
|
Writes(cadvisorapi.MachineInfo{}))
|
2015-08-17 01:40:08 +00:00
|
|
|
s.restfulCont.Add(ws)
|
2014-10-09 23:26:34 +00:00
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
const pprofBasePath = "/debug/pprof/"
|
|
|
|
|
2014-10-09 23:26:34 +00:00
|
|
|
// InstallDeguggingHandlers registers the HTTP request patterns that serve logs or run commands/containers
|
|
|
|
func (s *Server) InstallDebuggingHandlers() {
|
2015-08-17 01:40:08 +00:00
|
|
|
var ws *restful.WebService
|
|
|
|
|
|
|
|
ws = new(restful.WebService)
|
|
|
|
ws.
|
|
|
|
Path("/run")
|
|
|
|
ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
|
|
|
|
To(s.getRun).
|
|
|
|
Operation("getRun"))
|
|
|
|
ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}/{containerName}").
|
|
|
|
To(s.getRun).
|
|
|
|
Operation("getRun"))
|
|
|
|
s.restfulCont.Add(ws)
|
|
|
|
|
|
|
|
ws = new(restful.WebService)
|
|
|
|
ws.
|
|
|
|
Path("/exec")
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
ws.Route(ws.GET("/{podNamespace}/{podID}/{containerName}").
|
|
|
|
To(s.getExec).
|
|
|
|
Operation("getExec"))
|
2015-08-17 01:40:08 +00:00
|
|
|
ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
|
|
|
|
To(s.getExec).
|
|
|
|
Operation("getExec"))
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
ws.Route(ws.GET("/{podNamespace}/{podID}/{uid}/{containerName}").
|
|
|
|
To(s.getExec).
|
|
|
|
Operation("getExec"))
|
2015-08-17 01:40:08 +00:00
|
|
|
ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}/{containerName}").
|
|
|
|
To(s.getExec).
|
|
|
|
Operation("getExec"))
|
|
|
|
s.restfulCont.Add(ws)
|
|
|
|
|
|
|
|
ws = new(restful.WebService)
|
|
|
|
ws.
|
|
|
|
Path("/attach")
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
ws.Route(ws.GET("/{podNamespace}/{podID}/{containerName}").
|
|
|
|
To(s.getAttach).
|
|
|
|
Operation("getAttach"))
|
2015-08-17 01:40:08 +00:00
|
|
|
ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
|
|
|
|
To(s.getAttach).
|
|
|
|
Operation("getAttach"))
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
ws.Route(ws.GET("/{podNamespace}/{podID}/{uid}/{containerName}").
|
|
|
|
To(s.getAttach).
|
|
|
|
Operation("getAttach"))
|
2015-08-17 01:40:08 +00:00
|
|
|
ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}/{containerName}").
|
|
|
|
To(s.getAttach).
|
|
|
|
Operation("getAttach"))
|
|
|
|
s.restfulCont.Add(ws)
|
|
|
|
|
|
|
|
ws = new(restful.WebService)
|
|
|
|
ws.
|
|
|
|
Path("/portForward")
|
|
|
|
ws.Route(ws.POST("/{podNamespace}/{podID}").
|
|
|
|
To(s.getPortForward).
|
|
|
|
Operation("getPortForward"))
|
|
|
|
ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}").
|
|
|
|
To(s.getPortForward).
|
|
|
|
Operation("getPortForward"))
|
|
|
|
s.restfulCont.Add(ws)
|
|
|
|
|
|
|
|
ws = new(restful.WebService)
|
|
|
|
ws.
|
|
|
|
Path("/logs/")
|
|
|
|
ws.Route(ws.GET("").
|
|
|
|
To(s.getLogs).
|
|
|
|
Operation("getLogs"))
|
2015-10-30 23:19:34 +00:00
|
|
|
ws.Route(ws.GET("/{logpath:*}").
|
|
|
|
To(s.getLogs).
|
|
|
|
Operation("getLogs"))
|
2015-08-17 01:40:08 +00:00
|
|
|
s.restfulCont.Add(ws)
|
|
|
|
|
|
|
|
ws = new(restful.WebService)
|
|
|
|
ws.
|
|
|
|
Path("/containerLogs")
|
|
|
|
ws.Route(ws.GET("/{podNamespace}/{podID}/{containerName}").
|
|
|
|
To(s.getContainerLogs).
|
|
|
|
Operation("getContainerLogs"))
|
|
|
|
s.restfulCont.Add(ws)
|
|
|
|
|
|
|
|
handlePprofEndpoint := func(req *restful.Request, resp *restful.Response) {
|
|
|
|
name := strings.TrimPrefix(req.Request.URL.Path, pprofBasePath)
|
|
|
|
switch name {
|
|
|
|
case "profile":
|
|
|
|
pprof.Profile(resp, req.Request)
|
|
|
|
case "symbol":
|
|
|
|
pprof.Symbol(resp, req.Request)
|
|
|
|
case "cmdline":
|
|
|
|
pprof.Cmdline(resp, req.Request)
|
|
|
|
default:
|
|
|
|
pprof.Index(resp, req.Request)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Setup pporf handlers.
|
|
|
|
ws = new(restful.WebService).Path(pprofBasePath)
|
|
|
|
ws.Route(ws.GET("/{subpath:*}").To(func(req *restful.Request, resp *restful.Response) {
|
|
|
|
handlePprofEndpoint(req, resp)
|
|
|
|
})).Doc("pprof endpoint")
|
|
|
|
s.restfulCont.Add(ws)
|
|
|
|
|
2015-06-23 23:01:12 +00:00
|
|
|
// The /runningpods endpoint is used for testing only.
|
2015-08-17 01:40:08 +00:00
|
|
|
ws = new(restful.WebService)
|
|
|
|
ws.
|
|
|
|
Path("/runningpods/").
|
|
|
|
Produces(restful.MIME_JSON)
|
|
|
|
ws.Route(ws.GET("").
|
|
|
|
To(s.getRunningPods).
|
|
|
|
Operation("getRunningPods"))
|
|
|
|
s.restfulCont.Add(ws)
|
|
|
|
}
|
|
|
|
|
|
|
|
type httpHandler struct {
|
|
|
|
f func(w http.ResponseWriter, r *http.Request)
|
|
|
|
}
|
2015-06-04 06:51:14 +00:00
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
h.f(w, r)
|
2014-08-20 18:24:51 +00:00
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// error serializes an error object into an HTTP response.
|
2014-07-15 13:54:23 +00:00
|
|
|
func (s *Server) error(w http.ResponseWriter, err error) {
|
2015-03-04 14:44:01 +00:00
|
|
|
msg := fmt.Sprintf("Internal Error: %v", err)
|
|
|
|
glog.Infof("HTTP InternalServerError: %s", msg)
|
|
|
|
http.Error(w, msg, http.StatusInternalServerError)
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
|
|
|
|
2015-03-07 22:54:37 +00:00
|
|
|
func (s *Server) dockerHealthCheck(req *http.Request) error {
|
2015-04-21 20:02:50 +00:00
|
|
|
version, err := s.host.GetContainerRuntimeVersion()
|
2015-02-04 17:14:17 +00:00
|
|
|
if err != nil {
|
2015-03-07 22:54:37 +00:00
|
|
|
return errors.New("unknown Docker version")
|
2015-02-04 17:14:17 +00:00
|
|
|
}
|
2015-04-21 20:02:50 +00:00
|
|
|
// Verify the docker version.
|
2015-10-21 20:04:10 +00:00
|
|
|
result, err := version.Compare(dockertools.MinimumDockerAPIVersion)
|
2015-04-21 20:02:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if result < 0 {
|
|
|
|
return fmt.Errorf("Docker version is too old: %q", version.String())
|
2015-02-04 17:14:17 +00:00
|
|
|
}
|
2015-03-07 22:54:37 +00:00
|
|
|
return nil
|
|
|
|
}
|
2015-02-09 16:40:42 +00:00
|
|
|
|
2015-06-17 22:31:46 +00:00
|
|
|
// Checks if kubelet's sync loop that updates containers is working.
|
|
|
|
func (s *Server) syncLoopHealthCheck(req *http.Request) error {
|
|
|
|
duration := s.host.ResyncInterval() * 2
|
|
|
|
minDuration := time.Minute * 5
|
|
|
|
if duration < minDuration {
|
|
|
|
duration = minDuration
|
|
|
|
}
|
|
|
|
enterLoopTime := s.host.LatestLoopEntryTime()
|
|
|
|
if !enterLoopTime.IsZero() && time.Now().After(enterLoopTime.Add(duration)) {
|
|
|
|
return fmt.Errorf("Sync Loop took longer than expected.")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
// getContainerLogs handles containerLogs request against the Kubelet
|
|
|
|
func (s *Server) getContainerLogs(request *restful.Request, response *restful.Response) {
|
|
|
|
podNamespace := request.PathParameter("podNamespace")
|
|
|
|
podID := request.PathParameter("podID")
|
|
|
|
containerName := request.PathParameter("containerName")
|
2014-08-27 19:41:32 +00:00
|
|
|
|
2014-09-17 19:00:09 +00:00
|
|
|
if len(podID) == 0 {
|
2015-08-17 01:40:08 +00:00
|
|
|
// TODO: Why return JSON when the rest return plaintext errors?
|
2015-09-10 03:46:11 +00:00
|
|
|
// TODO: Why return plaintext errors?
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Missing podID."}`))
|
2014-09-17 19:00:09 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(containerName) == 0 {
|
2015-08-17 01:40:08 +00:00
|
|
|
// TODO: Why return JSON when the rest return plaintext errors?
|
|
|
|
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Missing container name."}`))
|
2014-08-27 19:41:32 +00:00
|
|
|
return
|
|
|
|
}
|
2014-10-17 19:51:57 +00:00
|
|
|
if len(podNamespace) == 0 {
|
2015-08-17 01:40:08 +00:00
|
|
|
// TODO: Why return JSON when the rest return plaintext errors?
|
|
|
|
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Missing podNamespace."}`))
|
2014-10-17 19:51:57 +00:00
|
|
|
return
|
|
|
|
}
|
2014-09-24 21:27:10 +00:00
|
|
|
|
2015-09-10 03:46:11 +00:00
|
|
|
query := request.Request.URL.Query()
|
|
|
|
// backwards compatibility for the "tail" query parameter
|
|
|
|
if tail := request.QueryParameter("tail"); len(tail) > 0 {
|
|
|
|
query["tailLines"] = []string{tail}
|
|
|
|
// "all" is the same as omitting tail
|
|
|
|
if tail == "all" {
|
|
|
|
delete(query, "tailLines")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// container logs on the kubelet are locked to v1
|
|
|
|
versioned := &v1.PodLogOptions{}
|
|
|
|
if err := api.Scheme.Convert(&query, versioned); err != nil {
|
|
|
|
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Unable to decode query."}`))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
out, err := api.Scheme.ConvertToVersion(versioned, "")
|
|
|
|
if err != nil {
|
|
|
|
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Unable to convert request query."}`))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
logOptions := out.(*api.PodLogOptions)
|
|
|
|
logOptions.TypeMeta = unversioned.TypeMeta{}
|
|
|
|
if errs := validation.ValidatePodLogOptions(logOptions); len(errs) > 0 {
|
|
|
|
response.WriteError(apierrs.StatusUnprocessableEntity, fmt.Errorf(`{"message": "Invalid request."}`))
|
|
|
|
return
|
|
|
|
}
|
2014-09-15 16:20:01 +00:00
|
|
|
|
2015-01-07 15:18:56 +00:00
|
|
|
pod, ok := s.host.GetPodByName(podNamespace, podID)
|
|
|
|
if !ok {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusNotFound, fmt.Errorf("Pod %q does not exist", podID))
|
2015-02-12 01:03:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
// Check if containerName is valid.
|
|
|
|
containerExists := false
|
|
|
|
for _, container := range pod.Spec.Containers {
|
|
|
|
if container.Name == containerName {
|
|
|
|
containerExists = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !containerExists {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusNotFound, fmt.Errorf("Container %q not found in Pod %q", containerName, podID))
|
2015-01-07 15:18:56 +00:00
|
|
|
return
|
|
|
|
}
|
2014-09-17 19:00:09 +00:00
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
if _, ok := response.ResponseWriter.(http.Flusher); !ok {
|
|
|
|
response.WriteError(http.StatusInternalServerError, fmt.Errorf("unable to convert %v into http.Flusher", response))
|
2015-04-06 15:23:56 +00:00
|
|
|
return
|
2014-08-27 19:41:32 +00:00
|
|
|
}
|
2015-09-11 16:28:31 +00:00
|
|
|
fw := flushwriter.Wrap(response.ResponseWriter)
|
2015-09-10 03:46:11 +00:00
|
|
|
if logOptions.LimitBytes != nil {
|
|
|
|
fw = limitwriter.New(fw, *logOptions.LimitBytes)
|
|
|
|
}
|
2015-08-17 01:40:08 +00:00
|
|
|
response.Header().Set("Transfer-Encoding", "chunked")
|
|
|
|
response.WriteHeader(http.StatusOK)
|
2015-09-10 03:46:11 +00:00
|
|
|
if err := s.host.GetKubeletContainerLogs(kubecontainer.GetPodFullName(pod), containerName, logOptions, fw, fw); err != nil {
|
|
|
|
if err != limitwriter.ErrMaximumWrite {
|
|
|
|
response.WriteError(http.StatusInternalServerError, err)
|
|
|
|
}
|
2014-08-27 19:41:32 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-23 23:01:12 +00:00
|
|
|
// encodePods creates an api.PodList object from pods and returns the encoded
|
|
|
|
// PodList.
|
|
|
|
func encodePods(pods []*api.Pod) (data []byte, err error) {
|
2015-04-03 22:51:50 +00:00
|
|
|
podList := new(api.PodList)
|
|
|
|
for _, pod := range pods {
|
|
|
|
podList.Items = append(podList.Items, *pod)
|
2014-10-22 23:52:38 +00:00
|
|
|
}
|
2015-09-10 19:30:47 +00:00
|
|
|
return latest.GroupOrDie("").Codec.Encode(podList)
|
2015-06-23 23:01:12 +00:00
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
// getPods returns a list of pods bound to the Kubelet and their spec.
|
|
|
|
func (s *Server) getPods(request *restful.Request, response *restful.Response) {
|
2015-06-23 23:01:12 +00:00
|
|
|
pods := s.host.GetPods()
|
|
|
|
data, err := encodePods(pods)
|
|
|
|
if err != nil {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusInternalServerError, err)
|
2015-06-23 23:01:12 +00:00
|
|
|
return
|
|
|
|
}
|
2015-08-17 01:40:08 +00:00
|
|
|
response.Write(data)
|
2015-06-23 23:01:12 +00:00
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
// getRunningPods returns a list of pods running on Kubelet. The list is
|
2015-06-23 23:01:12 +00:00
|
|
|
// provided by the container runtime, and is different from the list returned
|
2015-08-17 01:40:08 +00:00
|
|
|
// by getPods, which is a set of desired pods to run.
|
|
|
|
func (s *Server) getRunningPods(request *restful.Request, response *restful.Response) {
|
2015-06-23 23:01:12 +00:00
|
|
|
pods, err := s.host.GetRunningPods()
|
|
|
|
if err != nil {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusInternalServerError, err)
|
2015-06-23 23:01:12 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
data, err := encodePods(pods)
|
2014-10-22 23:52:38 +00:00
|
|
|
if err != nil {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusInternalServerError, err)
|
2014-10-22 23:52:38 +00:00
|
|
|
return
|
|
|
|
}
|
2015-08-17 01:40:08 +00:00
|
|
|
response.Write(data)
|
2014-10-22 23:52:38 +00:00
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// handleStats handles stats requests against the Kubelet.
|
2014-08-20 18:24:51 +00:00
|
|
|
func (s *Server) handleStats(w http.ResponseWriter, req *http.Request) {
|
|
|
|
s.serveStats(w, req)
|
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
// getLogs handles logs requests against the Kubelet.
|
|
|
|
func (s *Server) getLogs(request *restful.Request, response *restful.Response) {
|
|
|
|
s.host.ServeLogs(response, request.Request)
|
2014-08-20 18:24:51 +00:00
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
// getSpec handles spec requests against the Kubelet.
|
|
|
|
func (s *Server) getSpec(request *restful.Request, response *restful.Response) {
|
2015-09-21 18:06:38 +00:00
|
|
|
info, err := s.host.GetCachedMachineInfo()
|
2014-08-20 18:24:51 +00:00
|
|
|
if err != nil {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusInternalServerError, err)
|
2014-08-20 18:24:51 +00:00
|
|
|
return
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteEntity(info)
|
2014-08-20 18:24:51 +00:00
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
func getContainerCoordinates(request *restful.Request) (namespace, pod string, uid types.UID, container string) {
|
|
|
|
namespace = request.PathParameter("podNamespace")
|
|
|
|
pod = request.PathParameter("podID")
|
|
|
|
if uidStr := request.PathParameter("uid"); uidStr != "" {
|
|
|
|
uid = types.UID(uidStr)
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
2015-08-17 01:40:08 +00:00
|
|
|
container = request.PathParameter("containerName")
|
2015-01-08 20:41:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-09-22 20:29:51 +00:00
|
|
|
const defaultStreamCreationTimeout = 30 * time.Second
|
2015-07-28 04:48:55 +00:00
|
|
|
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
type Closer interface {
|
|
|
|
Close() error
|
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
func (s *Server) getAttach(request *restful.Request, response *restful.Response) {
|
|
|
|
podNamespace, podID, uid, container := getContainerCoordinates(request)
|
2015-07-28 04:48:55 +00:00
|
|
|
pod, ok := s.host.GetPodByName(podNamespace, podID)
|
|
|
|
if !ok {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
|
2015-07-28 04:48:55 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
stdinStream, stdoutStream, stderrStream, errorStream, conn, tty, ok := s.createStreams(request, response)
|
2015-07-28 04:48:55 +00:00
|
|
|
if conn != nil {
|
|
|
|
defer conn.Close()
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
// error is handled in the createStreams function
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
err := s.host.AttachContainer(kubecontainer.GetPodFullName(pod), uid, container, stdinStream, stdoutStream, stderrStream, tty)
|
2015-07-28 04:48:55 +00:00
|
|
|
if err != nil {
|
|
|
|
msg := fmt.Sprintf("Error executing command in container: %v", err)
|
|
|
|
glog.Error(msg)
|
|
|
|
errorStream.Write([]byte(msg))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
// getRun handles requests to run a command inside a container.
|
|
|
|
func (s *Server) getRun(request *restful.Request, response *restful.Response) {
|
|
|
|
podNamespace, podID, uid, container := getContainerCoordinates(request)
|
2015-01-07 15:18:56 +00:00
|
|
|
pod, ok := s.host.GetPodByName(podNamespace, podID)
|
|
|
|
if !ok {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
|
2015-01-07 15:18:56 +00:00
|
|
|
return
|
|
|
|
}
|
2015-08-17 01:40:08 +00:00
|
|
|
command := strings.Split(request.QueryParameter("cmd"), " ")
|
2015-03-23 17:14:30 +00:00
|
|
|
data, err := s.host.RunInContainer(kubecontainer.GetPodFullName(pod), uid, container, command)
|
2014-08-26 19:57:44 +00:00
|
|
|
if err != nil {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusInternalServerError, err)
|
2014-08-26 19:57:44 +00:00
|
|
|
return
|
|
|
|
}
|
2015-08-17 01:40:08 +00:00
|
|
|
response.Write(data)
|
2014-08-26 19:57:44 +00:00
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
// getExec handles requests to run a command inside a container.
|
|
|
|
func (s *Server) getExec(request *restful.Request, response *restful.Response) {
|
|
|
|
podNamespace, podID, uid, container := getContainerCoordinates(request)
|
2015-01-08 20:41:38 +00:00
|
|
|
pod, ok := s.host.GetPodByName(podNamespace, podID)
|
|
|
|
if !ok {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
|
2015-01-08 20:41:38 +00:00
|
|
|
return
|
|
|
|
}
|
2015-08-17 01:40:08 +00:00
|
|
|
stdinStream, stdoutStream, stderrStream, errorStream, conn, tty, ok := s.createStreams(request, response)
|
2015-07-28 04:48:55 +00:00
|
|
|
if conn != nil {
|
|
|
|
defer conn.Close()
|
|
|
|
}
|
|
|
|
if !ok {
|
2015-08-17 01:40:08 +00:00
|
|
|
// error is handled in the createStreams function
|
2015-07-28 04:48:55 +00:00
|
|
|
return
|
|
|
|
}
|
2015-08-17 01:40:08 +00:00
|
|
|
cmd := request.Request.URL.Query()[api.ExecCommandParamm]
|
|
|
|
err := s.host.ExecInContainer(kubecontainer.GetPodFullName(pod), uid, container, cmd, stdinStream, stdoutStream, stderrStream, tty)
|
2015-07-28 04:48:55 +00:00
|
|
|
if err != nil {
|
|
|
|
msg := fmt.Sprintf("Error executing command in container: %v", err)
|
|
|
|
glog.Error(msg)
|
|
|
|
errorStream.Write([]byte(msg))
|
|
|
|
}
|
|
|
|
}
|
2015-01-08 20:41:38 +00:00
|
|
|
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
// standardShellChannels returns the standard channel types for a shell connection (STDIN 0, STDOUT 1, STDERR 2)
|
|
|
|
// along with the approprxate duplex value
|
|
|
|
func standardShellChannels(stdin, stdout, stderr bool) []wsstream.ChannelType {
|
|
|
|
// open three half-duplex channels
|
|
|
|
channels := []wsstream.ChannelType{wsstream.ReadChannel, wsstream.WriteChannel, wsstream.WriteChannel}
|
|
|
|
if !stdin {
|
|
|
|
channels[0] = wsstream.IgnoreChannel
|
|
|
|
}
|
|
|
|
if !stdout {
|
|
|
|
channels[1] = wsstream.IgnoreChannel
|
|
|
|
}
|
|
|
|
if !stderr {
|
|
|
|
channels[2] = wsstream.IgnoreChannel
|
|
|
|
}
|
|
|
|
return channels
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) createStreams(request *restful.Request, response *restful.Response) (io.Reader, io.WriteCloser, io.WriteCloser, io.WriteCloser, Closer, bool, bool) {
|
|
|
|
tty := request.QueryParameter(api.ExecTTYParam) == "1"
|
|
|
|
stdin := request.QueryParameter(api.ExecStdinParam) == "1"
|
|
|
|
stdout := request.QueryParameter(api.ExecStdoutParam) == "1"
|
|
|
|
stderr := request.QueryParameter(api.ExecStderrParam) == "1"
|
|
|
|
if tty && stderr {
|
|
|
|
// TODO: make this an error before we reach this method
|
|
|
|
glog.V(4).Infof("Access to exec with tty and stderr is not supported, bypassing stderr")
|
|
|
|
stderr = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// count the streams client asked for, starting with 1
|
2015-01-08 20:41:38 +00:00
|
|
|
expectedStreams := 1
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
if stdin {
|
2015-01-08 20:41:38 +00:00
|
|
|
expectedStreams++
|
|
|
|
}
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
if stdout {
|
2015-01-08 20:41:38 +00:00
|
|
|
expectedStreams++
|
|
|
|
}
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
if stderr {
|
2015-01-08 20:41:38 +00:00
|
|
|
expectedStreams++
|
|
|
|
}
|
|
|
|
|
|
|
|
if expectedStreams == 1 {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusBadRequest, fmt.Errorf("you must specify at least 1 of stdin, stdout, stderr"))
|
2015-07-28 04:48:55 +00:00
|
|
|
return nil, nil, nil, nil, nil, false, false
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
|
|
|
|
Expose exec and logs via WebSockets
Not all clients and systems can support SPDY protocols. This commit adds
support for two new websocket protocols, one to handle streaming of pod
logs from a pod, and the other to allow exec to be tunneled over
websocket.
Browser support for chunked encoding is still poor, and web consoles
that wish to show pod logs may need to make compromises to display the
output. The /pods/<name>/log endpoint now supports websocket upgrade to
the 'binary.k8s.io' subprotocol, which sends chunks of logs as binary to
the client. Messages are written as logs are streamed from the container
daemon, so flushing should be unaffected.
Browser support for raw communication over SDPY is not possible, and
some languages lack libraries for it and HTTP/2. The Kubelet supports
upgrade to WebSocket instead of SPDY, and will multiplex STDOUT/IN/ERR
over websockets by prepending each binary message with a single byte
representing the channel (0 for IN, 1 for OUT, and 2 for ERR). Because
framing on WebSockets suffers from head-of-line blocking, clients and
other server code should ensure that no particular stream blocks. An
alternative subprotocol 'base64.channel.k8s.io' base64 encodes the body
and uses '0'-'9' to represent the channel for ease of use in browsers.
2015-09-11 20:09:51 +00:00
|
|
|
if wsstream.IsWebSocketRequest(request.Request) {
|
|
|
|
// open the requested channels, and always open the error channel
|
|
|
|
channels := append(standardShellChannels(stdin, stdout, stderr), wsstream.WriteChannel)
|
|
|
|
conn := wsstream.NewConn(channels...)
|
|
|
|
conn.SetIdleTimeout(s.host.StreamingConnectionIdleTimeout())
|
|
|
|
streams, err := conn.Open(httplog.Unlogged(response.ResponseWriter), request.Request)
|
|
|
|
if err != nil {
|
|
|
|
glog.Errorf("Unable to upgrade websocket connection: %v", err)
|
|
|
|
return nil, nil, nil, nil, nil, false, false
|
|
|
|
}
|
|
|
|
// Send an empty message to the lowest writable channel to notify the client the connection is established
|
|
|
|
// TODO: make generic to SDPY and WebSockets and do it outside of this method?
|
|
|
|
switch {
|
|
|
|
case stdout:
|
|
|
|
streams[1].Write([]byte{})
|
|
|
|
case stderr:
|
|
|
|
streams[2].Write([]byte{})
|
|
|
|
default:
|
|
|
|
streams[3].Write([]byte{})
|
|
|
|
}
|
|
|
|
return streams[0], streams[1], streams[2], streams[3], conn, tty, true
|
|
|
|
}
|
|
|
|
|
2015-10-22 00:42:40 +00:00
|
|
|
supportedStreamProtocols := []string{remotecommand.StreamProtocolV2Name, remotecommand.StreamProtocolV1Name}
|
|
|
|
_, err := httpstream.Handshake(request.Request, response.ResponseWriter, supportedStreamProtocols, remotecommand.StreamProtocolV1Name)
|
|
|
|
// negotiated protocol isn't used server side at the moment, but could be in the future
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, nil, nil, nil, false, false
|
|
|
|
}
|
|
|
|
|
2015-01-08 20:41:38 +00:00
|
|
|
streamCh := make(chan httpstream.Stream)
|
|
|
|
|
|
|
|
upgrader := spdy.NewResponseUpgrader()
|
2015-10-22 00:42:40 +00:00
|
|
|
conn := upgrader.UpgradeResponse(response.ResponseWriter, request.Request, func(stream httpstream.Stream) error {
|
2015-01-08 20:41:38 +00:00
|
|
|
streamCh <- stream
|
|
|
|
return nil
|
|
|
|
})
|
2015-08-17 01:40:08 +00:00
|
|
|
// from this point on, we can no longer call methods on response
|
2015-01-08 20:41:38 +00:00
|
|
|
if conn == nil {
|
|
|
|
// The upgrader is responsible for notifying the client of any errors that
|
|
|
|
// occurred during upgrading. All we can do is return here at this point
|
|
|
|
// if we weren't successful in upgrading.
|
2015-07-28 04:48:55 +00:00
|
|
|
return nil, nil, nil, nil, nil, false, false
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
conn.SetIdleTimeout(s.host.StreamingConnectionIdleTimeout())
|
|
|
|
|
|
|
|
// TODO make it configurable?
|
2015-09-22 20:29:51 +00:00
|
|
|
expired := time.NewTimer(defaultStreamCreationTimeout)
|
2015-01-08 20:41:38 +00:00
|
|
|
|
|
|
|
var errorStream, stdinStream, stdoutStream, stderrStream httpstream.Stream
|
|
|
|
receivedStreams := 0
|
|
|
|
WaitForStreams:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case stream := <-streamCh:
|
|
|
|
streamType := stream.Headers().Get(api.StreamType)
|
|
|
|
switch streamType {
|
|
|
|
case api.StreamTypeError:
|
|
|
|
errorStream = stream
|
|
|
|
receivedStreams++
|
|
|
|
case api.StreamTypeStdin:
|
|
|
|
stdinStream = stream
|
|
|
|
receivedStreams++
|
|
|
|
case api.StreamTypeStdout:
|
|
|
|
stdoutStream = stream
|
|
|
|
receivedStreams++
|
|
|
|
case api.StreamTypeStderr:
|
|
|
|
stderrStream = stream
|
|
|
|
receivedStreams++
|
|
|
|
default:
|
|
|
|
glog.Errorf("Unexpected stream type: '%s'", streamType)
|
|
|
|
}
|
|
|
|
if receivedStreams == expectedStreams {
|
|
|
|
break WaitForStreams
|
|
|
|
}
|
|
|
|
case <-expired.C:
|
|
|
|
// TODO find a way to return the error to the user. Maybe use a separate
|
|
|
|
// stream to report errors?
|
|
|
|
glog.Error("Timed out waiting for client to create streams")
|
2015-07-28 04:48:55 +00:00
|
|
|
return nil, nil, nil, nil, nil, false, false
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-28 04:48:55 +00:00
|
|
|
return stdinStream, stdoutStream, stderrStream, errorStream, conn, tty, true
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
|
|
|
|
2015-08-17 01:40:08 +00:00
|
|
|
func getPodCoordinates(request *restful.Request) (namespace, pod string, uid types.UID) {
|
|
|
|
namespace = request.PathParameter("podNamespace")
|
|
|
|
pod = request.PathParameter("podID")
|
|
|
|
if uidStr := request.PathParameter("uid"); uidStr != "" {
|
|
|
|
uid = types.UID(uidStr)
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-09-22 20:29:51 +00:00
|
|
|
// PortForwarder knows how to forward content from a data stream to/from a port
|
|
|
|
// in a pod.
|
|
|
|
type PortForwarder interface {
|
|
|
|
// PortForwarder copies data between a data stream and a port in a pod.
|
|
|
|
PortForward(name string, uid types.UID, port uint16, stream io.ReadWriteCloser) error
|
|
|
|
}
|
|
|
|
|
|
|
|
// getPortForward handles a new restful port forward request. It determines the
|
|
|
|
// pod name and uid and then calls ServePortForward.
|
2015-08-17 01:40:08 +00:00
|
|
|
func (s *Server) getPortForward(request *restful.Request, response *restful.Response) {
|
|
|
|
podNamespace, podID, uid := getPodCoordinates(request)
|
2015-01-08 20:41:38 +00:00
|
|
|
pod, ok := s.host.GetPodByName(podNamespace, podID)
|
|
|
|
if !ok {
|
2015-08-17 01:40:08 +00:00
|
|
|
response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
|
2015-01-08 20:41:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-09-22 20:29:51 +00:00
|
|
|
podName := kubecontainer.GetPodFullName(pod)
|
|
|
|
|
|
|
|
ServePortForward(response.ResponseWriter, request.Request, s.host, podName, uid, s.host.StreamingConnectionIdleTimeout(), defaultStreamCreationTimeout)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ServePortForward handles a port forwarding request. A single request is
|
|
|
|
// kept alive as long as the client is still alive and the connection has not
|
|
|
|
// been timed out due to idleness. This function handles multiple forwarded
|
|
|
|
// connections; i.e., multiple `curl http://localhost:8888/` requests will be
|
|
|
|
// handled by a single invocation of ServePortForward.
|
|
|
|
func ServePortForward(w http.ResponseWriter, req *http.Request, portForwarder PortForwarder, podName string, uid types.UID, idleTimeout time.Duration, streamCreationTimeout time.Duration) {
|
2015-10-22 02:37:26 +00:00
|
|
|
supportedPortForwardProtocols := []string{portforward.PortForwardProtocolV1Name}
|
|
|
|
_, err := httpstream.Handshake(req, w, supportedPortForwardProtocols, portforward.PortForwardProtocolV1Name)
|
2015-10-22 00:42:40 +00:00
|
|
|
// negotiated protocol isn't currently used server side, but could be in the future
|
|
|
|
if err != nil {
|
|
|
|
// Handshake writes the error to the client
|
|
|
|
util.HandleError(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-01-08 20:41:38 +00:00
|
|
|
streamChan := make(chan httpstream.Stream, 1)
|
2015-09-22 20:29:51 +00:00
|
|
|
|
|
|
|
glog.V(5).Infof("Upgrading port forward response")
|
2015-01-08 20:41:38 +00:00
|
|
|
upgrader := spdy.NewResponseUpgrader()
|
2015-10-22 00:42:40 +00:00
|
|
|
conn := upgrader.UpgradeResponse(w, req, portForwardStreamReceived(streamChan))
|
2015-09-22 20:29:51 +00:00
|
|
|
if conn == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
glog.V(5).Infof("(conn=%p) setting port forwarding streaming connection idle timeout to %v", conn, idleTimeout)
|
|
|
|
conn.SetIdleTimeout(idleTimeout)
|
|
|
|
|
|
|
|
h := &portForwardStreamHandler{
|
|
|
|
conn: conn,
|
|
|
|
streamChan: streamChan,
|
|
|
|
streamPairs: make(map[string]*portForwardStreamPair),
|
|
|
|
streamCreationTimeout: streamCreationTimeout,
|
|
|
|
pod: podName,
|
|
|
|
uid: uid,
|
|
|
|
forwarder: portForwarder,
|
|
|
|
}
|
|
|
|
h.run()
|
|
|
|
}
|
|
|
|
|
|
|
|
// portForwardStreamReceived is the httpstream.NewStreamHandler for port
|
|
|
|
// forward streams. It checks each stream's port and stream type headers,
|
|
|
|
// rejecting any streams that with missing or invalid values. Each valid
|
|
|
|
// stream is sent to the streams channel.
|
|
|
|
func portForwardStreamReceived(streams chan httpstream.Stream) func(httpstream.Stream) error {
|
|
|
|
return func(stream httpstream.Stream) error {
|
|
|
|
// make sure it has a valid port header
|
2015-01-08 20:41:38 +00:00
|
|
|
portString := stream.Headers().Get(api.PortHeader)
|
2015-09-22 20:29:51 +00:00
|
|
|
if len(portString) == 0 {
|
|
|
|
return fmt.Errorf("%q header is required", api.PortHeader)
|
|
|
|
}
|
2015-01-08 20:41:38 +00:00
|
|
|
port, err := strconv.ParseUint(portString, 10, 16)
|
|
|
|
if err != nil {
|
2015-09-22 20:29:51 +00:00
|
|
|
return fmt.Errorf("unable to parse %q as a port: %v", portString, err)
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
|
|
|
if port < 1 {
|
2015-09-22 20:29:51 +00:00
|
|
|
return fmt.Errorf("port %q must be > 0", portString)
|
|
|
|
}
|
|
|
|
|
|
|
|
// make sure it has a valid stream type header
|
|
|
|
streamType := stream.Headers().Get(api.StreamType)
|
|
|
|
if len(streamType) == 0 {
|
|
|
|
return fmt.Errorf("%q header is required", api.StreamType)
|
|
|
|
}
|
|
|
|
if streamType != api.StreamTypeError && streamType != api.StreamTypeData {
|
|
|
|
return fmt.Errorf("invalid stream type %q", streamType)
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
2015-09-22 20:29:51 +00:00
|
|
|
|
|
|
|
streams <- stream
|
2015-01-08 20:41:38 +00:00
|
|
|
return nil
|
|
|
|
}
|
2015-09-22 20:29:51 +00:00
|
|
|
}
|
2015-01-08 20:41:38 +00:00
|
|
|
|
2015-09-22 20:29:51 +00:00
|
|
|
// portForwardStreamHandler is capable of processing multiple port forward
|
|
|
|
// requests over a single httpstream.Connection.
|
|
|
|
type portForwardStreamHandler struct {
|
|
|
|
conn httpstream.Connection
|
|
|
|
streamChan chan httpstream.Stream
|
|
|
|
streamPairsLock sync.RWMutex
|
|
|
|
streamPairs map[string]*portForwardStreamPair
|
|
|
|
streamCreationTimeout time.Duration
|
|
|
|
pod string
|
|
|
|
uid types.UID
|
|
|
|
forwarder PortForwarder
|
|
|
|
}
|
2015-01-08 20:41:38 +00:00
|
|
|
|
2015-09-22 20:29:51 +00:00
|
|
|
// getStreamPair returns a portForwardStreamPair for requestID. This creates a
|
|
|
|
// new pair if one does not yet exist for the requestID. The returned bool is
|
|
|
|
// true if the pair was created.
|
|
|
|
func (h *portForwardStreamHandler) getStreamPair(requestID string) (*portForwardStreamPair, bool) {
|
|
|
|
h.streamPairsLock.Lock()
|
|
|
|
defer h.streamPairsLock.Unlock()
|
|
|
|
|
|
|
|
if p, ok := h.streamPairs[requestID]; ok {
|
|
|
|
glog.V(5).Infof("(conn=%p, request=%s) found existing stream pair", h.conn, requestID)
|
|
|
|
return p, false
|
|
|
|
}
|
|
|
|
|
|
|
|
glog.V(5).Infof("(conn=%p, request=%s) creating new stream pair", h.conn, requestID)
|
|
|
|
|
|
|
|
p := newPortForwardPair(requestID)
|
|
|
|
h.streamPairs[requestID] = p
|
|
|
|
|
|
|
|
return p, true
|
|
|
|
}
|
|
|
|
|
|
|
|
// monitorStreamPair waits for the pair to receive both its error and data
|
|
|
|
// streams, or for the timeout to expire (whichever happens first), and then
|
|
|
|
// removes the pair.
|
|
|
|
func (h *portForwardStreamHandler) monitorStreamPair(p *portForwardStreamPair, timeout <-chan time.Time) {
|
|
|
|
select {
|
|
|
|
case <-timeout:
|
|
|
|
err := fmt.Errorf("(conn=%p, request=%s) timed out waiting for streams", h.conn, p.requestID)
|
|
|
|
util.HandleError(err)
|
|
|
|
p.printError(err.Error())
|
|
|
|
case <-p.complete:
|
|
|
|
glog.V(5).Infof("(conn=%p, request=%s) successfully received error and data streams", h.conn, p.requestID)
|
|
|
|
}
|
|
|
|
h.removeStreamPair(p.requestID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// hasStreamPair returns a bool indicating if a stream pair for requestID
|
|
|
|
// exists.
|
|
|
|
func (h *portForwardStreamHandler) hasStreamPair(requestID string) bool {
|
|
|
|
h.streamPairsLock.RLock()
|
|
|
|
defer h.streamPairsLock.RUnlock()
|
|
|
|
|
|
|
|
_, ok := h.streamPairs[requestID]
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
|
|
|
// removeStreamPair removes the stream pair identified by requestID from streamPairs.
|
|
|
|
func (h *portForwardStreamHandler) removeStreamPair(requestID string) {
|
|
|
|
h.streamPairsLock.Lock()
|
|
|
|
defer h.streamPairsLock.Unlock()
|
|
|
|
|
|
|
|
delete(h.streamPairs, requestID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// requestID returns the request id for stream.
|
|
|
|
func (h *portForwardStreamHandler) requestID(stream httpstream.Stream) string {
|
|
|
|
requestID := stream.Headers().Get(api.PortForwardRequestIDHeader)
|
|
|
|
if len(requestID) == 0 {
|
|
|
|
glog.V(5).Infof("(conn=%p) stream received without %s header", h.conn, api.PortForwardRequestIDHeader)
|
|
|
|
// If we get here, it's because the connection came from an older client
|
|
|
|
// that isn't generating the request id header
|
|
|
|
// (https://github.com/kubernetes/kubernetes/blob/843134885e7e0b360eb5441e85b1410a8b1a7a0c/pkg/client/unversioned/portforward/portforward.go#L258-L287)
|
|
|
|
//
|
|
|
|
// This is a best-effort attempt at supporting older clients.
|
|
|
|
//
|
|
|
|
// When there aren't concurrent new forwarded connections, each connection
|
|
|
|
// will have a pair of streams (data, error), and the stream IDs will be
|
|
|
|
// consecutive odd numbers, e.g. 1 and 3 for the first connection. Convert
|
|
|
|
// the stream ID into a pseudo-request id by taking the stream type and
|
|
|
|
// using id = stream.Identifier() when the stream type is error,
|
|
|
|
// and id = stream.Identifier() - 2 when it's data.
|
|
|
|
//
|
|
|
|
// NOTE: this only works when there are not concurrent new streams from
|
|
|
|
// multiple forwarded connections; it's a best-effort attempt at supporting
|
|
|
|
// old clients that don't generate request ids. If there are concurrent
|
|
|
|
// new connections, it's possible that 1 connection gets streams whose IDs
|
|
|
|
// are not consecutive (e.g. 5 and 9 instead of 5 and 7).
|
|
|
|
streamType := stream.Headers().Get(api.StreamType)
|
|
|
|
switch streamType {
|
|
|
|
case api.StreamTypeError:
|
|
|
|
requestID = strconv.Itoa(int(stream.Identifier()))
|
|
|
|
case api.StreamTypeData:
|
|
|
|
requestID = strconv.Itoa(int(stream.Identifier()) - 2)
|
|
|
|
}
|
|
|
|
|
|
|
|
glog.V(5).Infof("(conn=%p) automatically assigning request ID=%q from stream type=%s, stream ID=%d", h.conn, requestID, streamType, stream.Identifier())
|
|
|
|
}
|
|
|
|
return requestID
|
|
|
|
}
|
|
|
|
|
|
|
|
// run is the main loop for the portForwardStreamHandler. It processes new
|
|
|
|
// streams, invoking portForward for each complete stream pair. The loop exits
|
|
|
|
// when the httpstream.Connection is closed.
|
|
|
|
func (h *portForwardStreamHandler) run() {
|
|
|
|
glog.V(5).Infof("(conn=%p) waiting for port forward streams", h.conn)
|
2015-01-08 20:41:38 +00:00
|
|
|
Loop:
|
|
|
|
for {
|
|
|
|
select {
|
2015-09-22 20:29:51 +00:00
|
|
|
case <-h.conn.CloseChan():
|
|
|
|
glog.V(5).Infof("(conn=%p) upgraded connection closed", h.conn)
|
2015-01-08 20:41:38 +00:00
|
|
|
break Loop
|
2015-09-22 20:29:51 +00:00
|
|
|
case stream := <-h.streamChan:
|
|
|
|
requestID := h.requestID(stream)
|
2015-01-08 20:41:38 +00:00
|
|
|
streamType := stream.Headers().Get(api.StreamType)
|
2015-09-22 20:29:51 +00:00
|
|
|
glog.V(5).Infof("(conn=%p, request=%s) received new stream of type %s", h.conn, requestID, streamType)
|
|
|
|
|
|
|
|
p, created := h.getStreamPair(requestID)
|
|
|
|
if created {
|
|
|
|
go h.monitorStreamPair(p, time.After(h.streamCreationTimeout))
|
|
|
|
}
|
|
|
|
if complete, err := p.add(stream); err != nil {
|
|
|
|
msg := fmt.Sprintf("error processing stream for request %s: %v", requestID, err)
|
|
|
|
util.HandleError(errors.New(msg))
|
|
|
|
p.printError(msg)
|
|
|
|
} else if complete {
|
|
|
|
go h.portForward(p)
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-22 20:29:51 +00:00
|
|
|
// portForward invokes the portForwardStreamHandler's forwarder.PortForward
|
|
|
|
// function for the given stream pair.
|
|
|
|
func (h *portForwardStreamHandler) portForward(p *portForwardStreamPair) {
|
|
|
|
defer p.dataStream.Close()
|
|
|
|
defer p.errorStream.Close()
|
2015-01-08 20:41:38 +00:00
|
|
|
|
2015-09-22 20:29:51 +00:00
|
|
|
portString := p.dataStream.Headers().Get(api.PortHeader)
|
|
|
|
port, _ := strconv.ParseUint(portString, 10, 16)
|
2015-01-08 20:41:38 +00:00
|
|
|
|
2015-09-22 20:29:51 +00:00
|
|
|
glog.V(5).Infof("(conn=%p, request=%s) invoking forwarder.PortForward for port %s", h.conn, p.requestID, portString)
|
|
|
|
err := h.forwarder.PortForward(h.pod, h.uid, uint16(port), p.dataStream)
|
|
|
|
glog.V(5).Infof("(conn=%p, request=%s) done invoking forwarder.PortForward for port %s", h.conn, p.requestID, portString)
|
2015-01-08 20:41:38 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2015-09-22 20:29:51 +00:00
|
|
|
msg := fmt.Errorf("error forwarding port %d to pod %s, uid %v: %v", port, h.pod, h.uid, err)
|
|
|
|
util.HandleError(msg)
|
|
|
|
fmt.Fprint(p.errorStream, msg.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// portForwardStreamPair represents the error and data streams for a port
|
|
|
|
// forwarding request.
|
|
|
|
type portForwardStreamPair struct {
|
|
|
|
lock sync.RWMutex
|
|
|
|
requestID string
|
|
|
|
dataStream httpstream.Stream
|
|
|
|
errorStream httpstream.Stream
|
|
|
|
complete chan struct{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// newPortForwardPair creates a new portForwardStreamPair.
|
|
|
|
func newPortForwardPair(requestID string) *portForwardStreamPair {
|
|
|
|
return &portForwardStreamPair{
|
|
|
|
requestID: requestID,
|
|
|
|
complete: make(chan struct{}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add adds the stream to the portForwardStreamPair. If the pair already
|
|
|
|
// contains a stream for the new stream's type, an error is returned. add
|
|
|
|
// returns true if both the data and error streams for this pair have been
|
|
|
|
// received.
|
|
|
|
func (p *portForwardStreamPair) add(stream httpstream.Stream) (bool, error) {
|
|
|
|
p.lock.Lock()
|
|
|
|
defer p.lock.Unlock()
|
|
|
|
|
|
|
|
switch stream.Headers().Get(api.StreamType) {
|
|
|
|
case api.StreamTypeError:
|
|
|
|
if p.errorStream != nil {
|
|
|
|
return false, errors.New("error stream already assigned")
|
|
|
|
}
|
|
|
|
p.errorStream = stream
|
|
|
|
case api.StreamTypeData:
|
|
|
|
if p.dataStream != nil {
|
|
|
|
return false, errors.New("data stream already assigned")
|
|
|
|
}
|
|
|
|
p.dataStream = stream
|
|
|
|
}
|
|
|
|
|
|
|
|
complete := p.errorStream != nil && p.dataStream != nil
|
|
|
|
if complete {
|
|
|
|
close(p.complete)
|
|
|
|
}
|
|
|
|
return complete, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// printError writes s to p.errorStream if p.errorStream has been set.
|
|
|
|
func (p *portForwardStreamPair) printError(s string) {
|
|
|
|
p.lock.RLock()
|
|
|
|
defer p.lock.RUnlock()
|
|
|
|
if p.errorStream != nil {
|
|
|
|
fmt.Fprint(p.errorStream, s)
|
2015-01-08 20:41:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// ServeHTTP responds to HTTP requests on the Kubelet.
|
2014-08-20 18:24:51 +00:00
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
2014-08-21 04:27:19 +00:00
|
|
|
defer httplog.NewLogged(req, &w).StacktraceWhen(
|
2014-08-20 18:24:51 +00:00
|
|
|
httplog.StatusIsNot(
|
|
|
|
http.StatusOK,
|
2014-12-18 02:42:11 +00:00
|
|
|
http.StatusMovedPermanently,
|
|
|
|
http.StatusTemporaryRedirect,
|
2014-08-20 18:24:51 +00:00
|
|
|
http.StatusNotFound,
|
2015-01-08 20:41:38 +00:00
|
|
|
http.StatusSwitchingProtocols,
|
2014-08-20 18:24:51 +00:00
|
|
|
),
|
|
|
|
).Log()
|
2015-08-17 01:40:08 +00:00
|
|
|
s.restfulCont.ServeHTTP(w, req)
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
2014-07-01 21:05:10 +00:00
|
|
|
|
2015-04-23 17:14:08 +00:00
|
|
|
type StatsRequest struct {
|
|
|
|
// The name of the container for which to request stats.
|
|
|
|
// Default: /
|
|
|
|
ContainerName string `json:"containerName,omitempty"`
|
|
|
|
|
|
|
|
// Max number of stats to return.
|
|
|
|
// If start and end time are specified this limit is ignored.
|
|
|
|
// Default: 60
|
|
|
|
NumStats int `json:"num_stats,omitempty"`
|
|
|
|
|
|
|
|
// Start time for which to query information.
|
2015-08-08 21:29:57 +00:00
|
|
|
// If omitted, the beginning of time is assumed.
|
2015-04-23 17:14:08 +00:00
|
|
|
Start time.Time `json:"start,omitempty"`
|
|
|
|
|
|
|
|
// End time for which to query information.
|
2015-08-08 21:29:57 +00:00
|
|
|
// If omitted, current time is assumed.
|
2015-04-23 17:14:08 +00:00
|
|
|
End time.Time `json:"end,omitempty"`
|
|
|
|
|
|
|
|
// Whether to also include information from subcontainers.
|
|
|
|
// Default: false.
|
|
|
|
Subcontainers bool `json:"subcontainers,omitempty"`
|
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// serveStats implements stats logic.
|
2014-07-15 13:54:23 +00:00
|
|
|
func (s *Server) serveStats(w http.ResponseWriter, req *http.Request) {
|
2015-04-23 17:14:08 +00:00
|
|
|
// Stats requests are in the following forms:
|
|
|
|
//
|
|
|
|
// /stats/ : Root container stats
|
|
|
|
// /stats/container/ : Non-Kubernetes container stats (returns a map)
|
|
|
|
// /stats/<pod name>/<container name> : Stats for Kubernetes pod/container
|
|
|
|
// /stats/<namespace>/<pod name>/<uid>/<container name> : Stats for Kubernetes namespace/pod/uid/container
|
2014-07-01 21:05:10 +00:00
|
|
|
components := strings.Split(strings.TrimPrefix(path.Clean(req.URL.Path), "/"), "/")
|
2015-04-23 17:14:08 +00:00
|
|
|
var stats interface{}
|
2014-07-01 21:05:10 +00:00
|
|
|
var err error
|
2015-04-23 17:14:08 +00:00
|
|
|
var query StatsRequest
|
|
|
|
query.NumStats = 60
|
|
|
|
|
2014-07-15 22:40:02 +00:00
|
|
|
err = json.NewDecoder(req.Body).Decode(&query)
|
2014-07-14 21:48:51 +00:00
|
|
|
if err != nil && err != io.EOF {
|
|
|
|
s.error(w, err)
|
|
|
|
return
|
|
|
|
}
|
2015-10-16 03:00:28 +00:00
|
|
|
cadvisorRequest := cadvisorapi.ContainerInfoRequest{
|
2015-04-23 17:14:08 +00:00
|
|
|
NumStats: query.NumStats,
|
|
|
|
Start: query.Start,
|
|
|
|
End: query.End,
|
|
|
|
}
|
|
|
|
|
2014-07-01 21:05:10 +00:00
|
|
|
switch len(components) {
|
|
|
|
case 1:
|
2015-04-23 17:14:08 +00:00
|
|
|
// Root container stats.
|
2015-10-16 03:00:28 +00:00
|
|
|
var statsMap map[string]*cadvisorapi.ContainerInfo
|
2015-04-23 17:14:08 +00:00
|
|
|
statsMap, err = s.host.GetRawContainerInfo("/", &cadvisorRequest, false)
|
|
|
|
stats = statsMap["/"]
|
2014-07-01 21:05:10 +00:00
|
|
|
case 2:
|
2015-04-23 17:14:08 +00:00
|
|
|
// Non-Kubernetes container stats.
|
|
|
|
if components[1] != "container" {
|
|
|
|
http.Error(w, fmt.Sprintf("unknown stats request type %q", components[1]), http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
containerName := path.Join("/", query.ContainerName)
|
|
|
|
stats, err = s.host.GetRawContainerInfo(containerName, &cadvisorRequest, query.Subcontainers)
|
2014-07-01 21:05:10 +00:00
|
|
|
case 3:
|
2015-01-14 21:53:43 +00:00
|
|
|
// Backward compatibility without uid information, does not support namespace
|
2015-01-07 15:18:56 +00:00
|
|
|
pod, ok := s.host.GetPodByName(api.NamespaceDefault, components[1])
|
|
|
|
if !ok {
|
|
|
|
http.Error(w, "Pod does not exist", http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
2015-04-23 17:14:08 +00:00
|
|
|
stats, err = s.host.GetContainerInfo(kubecontainer.GetPodFullName(pod), "", components[2], &cadvisorRequest)
|
2015-01-03 02:17:25 +00:00
|
|
|
case 5:
|
2015-01-07 15:18:56 +00:00
|
|
|
pod, ok := s.host.GetPodByName(components[1], components[2])
|
|
|
|
if !ok {
|
|
|
|
http.Error(w, "Pod does not exist", http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
2015-04-23 17:14:08 +00:00
|
|
|
stats, err = s.host.GetContainerInfo(kubecontainer.GetPodFullName(pod), types.UID(components[3]), components[4], &cadvisorRequest)
|
2014-07-01 21:05:10 +00:00
|
|
|
default:
|
2015-04-23 17:14:08 +00:00
|
|
|
http.Error(w, fmt.Sprintf("Unknown resource: %v", components), http.StatusNotFound)
|
2014-07-01 21:05:10 +00:00
|
|
|
return
|
|
|
|
}
|
2015-02-13 16:54:06 +00:00
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
break
|
2015-04-30 20:35:21 +00:00
|
|
|
case ErrContainerNotFound:
|
2015-02-13 16:54:06 +00:00
|
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
|
|
return
|
|
|
|
default:
|
2014-07-12 17:44:29 +00:00
|
|
|
s.error(w, err)
|
2014-07-01 21:05:10 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if stats == nil {
|
|
|
|
fmt.Fprint(w, "{}")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
data, err := json.Marshal(stats)
|
|
|
|
if err != nil {
|
2014-07-12 17:44:29 +00:00
|
|
|
s.error(w, err)
|
2014-07-01 21:05:10 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
w.Header().Add("Content-type", "application/json")
|
|
|
|
w.Write(data)
|
|
|
|
}
|