mirror of
https://github.com/ceph/ceph-csi.git
synced 2025-06-13 10:33:35 +00:00
vendor files
This commit is contained in:
106
vendor/k8s.io/kubernetes/pkg/kubelet/server/BUILD
generated
vendored
Normal file
106
vendor/k8s.io/kubernetes/pkg/kubelet/server/BUILD
generated
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"auth.go",
|
||||
"doc.go",
|
||||
"server.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server",
|
||||
deps = [
|
||||
"//pkg/api/legacyscheme:go_default_library",
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/core/v1/validation:go_default_library",
|
||||
"//pkg/kubelet/container:go_default_library",
|
||||
"//pkg/kubelet/server/portforward:go_default_library",
|
||||
"//pkg/kubelet/server/remotecommand:go_default_library",
|
||||
"//pkg/kubelet/server/stats:go_default_library",
|
||||
"//pkg/kubelet/server/streaming:go_default_library",
|
||||
"//pkg/kubelet/types:go_default_library",
|
||||
"//pkg/util/configz:go_default_library",
|
||||
"//pkg/util/limitwriter:go_default_library",
|
||||
"//vendor/github.com/emicklei/go-restful:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/github.com/google/cadvisor/info/v1:go_default_library",
|
||||
"//vendor/github.com/google/cadvisor/metrics:go_default_library",
|
||||
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
|
||||
"//vendor/github.com/prometheus/client_golang/prometheus/promhttp:go_default_library",
|
||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/remotecommand:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/healthz:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/httplog:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/flushwriter:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/remotecommand:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"auth_test.go",
|
||||
"server_test.go",
|
||||
"server_websocket_test.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server",
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/core/install:go_default_library",
|
||||
"//pkg/kubelet/apis/stats/v1alpha1:go_default_library",
|
||||
"//pkg/kubelet/cm:go_default_library",
|
||||
"//pkg/kubelet/container:go_default_library",
|
||||
"//pkg/kubelet/container/testing:go_default_library",
|
||||
"//pkg/kubelet/server/portforward:go_default_library",
|
||||
"//pkg/kubelet/server/remotecommand:go_default_library",
|
||||
"//pkg/kubelet/server/stats:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//vendor/github.com/google/cadvisor/info/v1:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/require:go_default_library",
|
||||
"//vendor/golang.org/x/net/websocket:go_default_library",
|
||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/httpstream:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/remotecommand:go_default_library",
|
||||
"//vendor/k8s.io/client-go/util/testing:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//pkg/kubelet/server/portforward:all-srcs",
|
||||
"//pkg/kubelet/server/remotecommand:all-srcs",
|
||||
"//pkg/kubelet/server/stats:all-srcs",
|
||||
"//pkg/kubelet/server/streaming:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
114
vendor/k8s.io/kubernetes/pkg/kubelet/server/auth.go
generated
vendored
Normal file
114
vendor/k8s.io/kubernetes/pkg/kubelet/server/auth.go
generated
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
// KubeletAuth implements AuthInterface
|
||||
type KubeletAuth struct {
|
||||
// authenticator identifies the user for requests to the Kubelet API
|
||||
authenticator.Request
|
||||
// authorizerAttributeGetter builds authorization.Attributes for a request to the Kubelet API
|
||||
authorizer.RequestAttributesGetter
|
||||
// authorizer determines whether a given authorization.Attributes is allowed
|
||||
authorizer.Authorizer
|
||||
}
|
||||
|
||||
// NewKubeletAuth returns a kubelet.AuthInterface composed of the given authenticator, attribute getter, and authorizer
|
||||
func NewKubeletAuth(authenticator authenticator.Request, authorizerAttributeGetter authorizer.RequestAttributesGetter, authorizer authorizer.Authorizer) AuthInterface {
|
||||
return &KubeletAuth{authenticator, authorizerAttributeGetter, authorizer}
|
||||
}
|
||||
|
||||
func NewNodeAuthorizerAttributesGetter(nodeName types.NodeName) authorizer.RequestAttributesGetter {
|
||||
return nodeAuthorizerAttributesGetter{nodeName: nodeName}
|
||||
}
|
||||
|
||||
type nodeAuthorizerAttributesGetter struct {
|
||||
nodeName types.NodeName
|
||||
}
|
||||
|
||||
func isSubpath(subpath, path string) bool {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
return subpath == path || (strings.HasPrefix(subpath, path) && subpath[len(path)] == '/')
|
||||
}
|
||||
|
||||
// GetRequestAttributes populates authorizer attributes for the requests to the kubelet API.
|
||||
// Default attributes are: {apiVersion=v1,verb=<http verb from request>,resource=nodes,name=<node name>,subresource=proxy}
|
||||
// More specific verb/resource is set for the following request patterns:
|
||||
// /stats/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=stats
|
||||
// /metrics/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=metrics
|
||||
// /logs/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=log
|
||||
// /spec/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=spec
|
||||
func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *http.Request) authorizer.Attributes {
|
||||
|
||||
apiVerb := ""
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
apiVerb = "create"
|
||||
case "GET":
|
||||
apiVerb = "get"
|
||||
case "PUT":
|
||||
apiVerb = "update"
|
||||
case "PATCH":
|
||||
apiVerb = "patch"
|
||||
case "DELETE":
|
||||
apiVerb = "delete"
|
||||
}
|
||||
|
||||
requestPath := r.URL.Path
|
||||
|
||||
// Default attributes mirror the API attributes that would allow this access to the kubelet API
|
||||
attrs := authorizer.AttributesRecord{
|
||||
User: u,
|
||||
Verb: apiVerb,
|
||||
Namespace: "",
|
||||
APIGroup: "",
|
||||
APIVersion: "v1",
|
||||
Resource: "nodes",
|
||||
Subresource: "proxy",
|
||||
Name: string(n.nodeName),
|
||||
ResourceRequest: true,
|
||||
Path: requestPath,
|
||||
}
|
||||
|
||||
// Override subresource for specific paths
|
||||
// This allows subdividing access to the kubelet API
|
||||
switch {
|
||||
case isSubpath(requestPath, statsPath):
|
||||
attrs.Subresource = "stats"
|
||||
case isSubpath(requestPath, metricsPath):
|
||||
attrs.Subresource = "metrics"
|
||||
case isSubpath(requestPath, logsPath):
|
||||
// "log" to match other log subresources (pods/log, etc)
|
||||
attrs.Subresource = "log"
|
||||
case isSubpath(requestPath, specPath):
|
||||
attrs.Subresource = "spec"
|
||||
}
|
||||
|
||||
glog.V(5).Infof("Node request attributes: attrs=%#v", attrs)
|
||||
|
||||
return attrs
|
||||
}
|
53
vendor/k8s.io/kubernetes/pkg/kubelet/server/auth_test.go
generated
vendored
Normal file
53
vendor/k8s.io/kubernetes/pkg/kubelet/server/auth_test.go
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsSubPath(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
subpath string
|
||||
path string
|
||||
expected bool
|
||||
}{
|
||||
"empty": {subpath: "", path: "", expected: true},
|
||||
|
||||
"match 1": {subpath: "foo", path: "foo", expected: true},
|
||||
"match 2": {subpath: "/foo", path: "/foo", expected: true},
|
||||
"match 3": {subpath: "/foo/", path: "/foo/", expected: true},
|
||||
"match 4": {subpath: "/foo/bar", path: "/foo/bar", expected: true},
|
||||
|
||||
"subpath of root 1": {subpath: "/foo", path: "/", expected: true},
|
||||
"subpath of root 2": {subpath: "/foo/", path: "/", expected: true},
|
||||
"subpath of root 3": {subpath: "/foo/bar", path: "/", expected: true},
|
||||
|
||||
"subpath of path 1": {subpath: "/foo", path: "/foo", expected: true},
|
||||
"subpath of path 2": {subpath: "/foo/", path: "/foo", expected: true},
|
||||
"subpath of path 3": {subpath: "/foo/bar", path: "/foo", expected: true},
|
||||
|
||||
"mismatch 1": {subpath: "/foo", path: "/bar", expected: false},
|
||||
"mismatch 2": {subpath: "/foo", path: "/foobar", expected: false},
|
||||
"mismatch 3": {subpath: "/foobar", path: "/foo", expected: false},
|
||||
}
|
||||
|
||||
for k, tc := range testcases {
|
||||
result := isSubpath(tc.subpath, tc.path)
|
||||
if result != tc.expected {
|
||||
t.Errorf("%s: expected %v, got %v", k, tc.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
18
vendor/k8s.io/kubernetes/pkg/kubelet/server/doc.go
generated
vendored
Normal file
18
vendor/k8s.io/kubernetes/pkg/kubelet/server/doc.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package server contains functions related to serving Kubelet's external interface.
|
||||
package server // import "k8s.io/kubernetes/pkg/kubelet/server"
|
55
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/BUILD
generated
vendored
Normal file
55
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/BUILD
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"constants.go",
|
||||
"httpstream.go",
|
||||
"portforward.go",
|
||||
"websocket.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server/portforward",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/httpstream:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/httplog:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/wsstream:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"httpstream_test.go",
|
||||
"websocket_test.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server/portforward",
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/httpstream:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
23
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/constants.go
generated
vendored
Normal file
23
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/constants.go
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// package portforward contains server-side logic for handling port forwarding requests.
|
||||
package portforward
|
||||
|
||||
// The subprotocol "portforward.k8s.io" is used for port forwarding.
|
||||
const ProtocolV1Name = "portforward.k8s.io"
|
||||
|
||||
var SupportedProtocols = []string{ProtocolV1Name}
|
309
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/httpstream.go
generated
vendored
Normal file
309
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/httpstream.go
generated
vendored
Normal file
@ -0,0 +1,309 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
func handleHttpStreams(req *http.Request, w http.ResponseWriter, portForwarder PortForwarder, podName string, uid types.UID, supportedPortForwardProtocols []string, idleTimeout, streamCreationTimeout time.Duration) error {
|
||||
_, err := httpstream.Handshake(req, w, supportedPortForwardProtocols)
|
||||
// negotiated protocol isn't currently used server side, but could be in the future
|
||||
if err != nil {
|
||||
// Handshake writes the error to the client
|
||||
return err
|
||||
}
|
||||
streamChan := make(chan httpstream.Stream, 1)
|
||||
|
||||
glog.V(5).Infof("Upgrading port forward response")
|
||||
upgrader := spdy.NewResponseUpgrader()
|
||||
conn := upgrader.UpgradeResponse(w, req, httpStreamReceived(streamChan))
|
||||
if conn == nil {
|
||||
return errors.New("Unable to upgrade websocket connection")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
glog.V(5).Infof("(conn=%p) setting port forwarding streaming connection idle timeout to %v", conn, idleTimeout)
|
||||
conn.SetIdleTimeout(idleTimeout)
|
||||
|
||||
h := &httpStreamHandler{
|
||||
conn: conn,
|
||||
streamChan: streamChan,
|
||||
streamPairs: make(map[string]*httpStreamPair),
|
||||
streamCreationTimeout: streamCreationTimeout,
|
||||
pod: podName,
|
||||
uid: uid,
|
||||
forwarder: portForwarder,
|
||||
}
|
||||
h.run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// httpStreamReceived 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 httpStreamReceived(streams chan httpstream.Stream) func(httpstream.Stream, <-chan struct{}) error {
|
||||
return func(stream httpstream.Stream, replySent <-chan struct{}) error {
|
||||
// make sure it has a valid port header
|
||||
portString := stream.Headers().Get(api.PortHeader)
|
||||
if len(portString) == 0 {
|
||||
return fmt.Errorf("%q header is required", api.PortHeader)
|
||||
}
|
||||
port, err := strconv.ParseUint(portString, 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse %q as a port: %v", portString, err)
|
||||
}
|
||||
if port < 1 {
|
||||
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)
|
||||
}
|
||||
|
||||
streams <- stream
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// httpStreamHandler is capable of processing multiple port forward
|
||||
// requests over a single httpstream.Connection.
|
||||
type httpStreamHandler struct {
|
||||
conn httpstream.Connection
|
||||
streamChan chan httpstream.Stream
|
||||
streamPairsLock sync.RWMutex
|
||||
streamPairs map[string]*httpStreamPair
|
||||
streamCreationTimeout time.Duration
|
||||
pod string
|
||||
uid types.UID
|
||||
forwarder PortForwarder
|
||||
}
|
||||
|
||||
// getStreamPair returns a httpStreamPair 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 *httpStreamHandler) getStreamPair(requestID string) (*httpStreamPair, 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 *httpStreamHandler) monitorStreamPair(p *httpStreamPair, timeout <-chan time.Time) {
|
||||
select {
|
||||
case <-timeout:
|
||||
err := fmt.Errorf("(conn=%v, request=%s) timed out waiting for streams", h.conn, p.requestID)
|
||||
utilruntime.HandleError(err)
|
||||
p.printError(err.Error())
|
||||
case <-p.complete:
|
||||
glog.V(5).Infof("(conn=%v, 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 *httpStreamHandler) 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 *httpStreamHandler) removeStreamPair(requestID string) {
|
||||
h.streamPairsLock.Lock()
|
||||
defer h.streamPairsLock.Unlock()
|
||||
|
||||
delete(h.streamPairs, requestID)
|
||||
}
|
||||
|
||||
// requestID returns the request id for stream.
|
||||
func (h *httpStreamHandler) 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 httpStreamHandler. It processes new
|
||||
// streams, invoking portForward for each complete stream pair. The loop exits
|
||||
// when the httpstream.Connection is closed.
|
||||
func (h *httpStreamHandler) run() {
|
||||
glog.V(5).Infof("(conn=%p) waiting for port forward streams", h.conn)
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-h.conn.CloseChan():
|
||||
glog.V(5).Infof("(conn=%p) upgraded connection closed", h.conn)
|
||||
break Loop
|
||||
case stream := <-h.streamChan:
|
||||
requestID := h.requestID(stream)
|
||||
streamType := stream.Headers().Get(api.StreamType)
|
||||
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)
|
||||
utilruntime.HandleError(errors.New(msg))
|
||||
p.printError(msg)
|
||||
} else if complete {
|
||||
go h.portForward(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// portForward invokes the httpStreamHandler's forwarder.PortForward
|
||||
// function for the given stream pair.
|
||||
func (h *httpStreamHandler) portForward(p *httpStreamPair) {
|
||||
defer p.dataStream.Close()
|
||||
defer p.errorStream.Close()
|
||||
|
||||
portString := p.dataStream.Headers().Get(api.PortHeader)
|
||||
port, _ := strconv.ParseInt(portString, 10, 32)
|
||||
|
||||
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, int32(port), p.dataStream)
|
||||
glog.V(5).Infof("(conn=%p, request=%s) done invoking forwarder.PortForward for port %s", h.conn, p.requestID, portString)
|
||||
|
||||
if err != nil {
|
||||
msg := fmt.Errorf("error forwarding port %d to pod %s, uid %v: %v", port, h.pod, h.uid, err)
|
||||
utilruntime.HandleError(msg)
|
||||
fmt.Fprint(p.errorStream, msg.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// httpStreamPair represents the error and data streams for a port
|
||||
// forwarding request.
|
||||
type httpStreamPair struct {
|
||||
lock sync.RWMutex
|
||||
requestID string
|
||||
dataStream httpstream.Stream
|
||||
errorStream httpstream.Stream
|
||||
complete chan struct{}
|
||||
}
|
||||
|
||||
// newPortForwardPair creates a new httpStreamPair.
|
||||
func newPortForwardPair(requestID string) *httpStreamPair {
|
||||
return &httpStreamPair{
|
||||
requestID: requestID,
|
||||
complete: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// add adds the stream to the httpStreamPair. 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 *httpStreamPair) 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 *httpStreamPair) printError(s string) {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
if p.errorStream != nil {
|
||||
fmt.Fprint(p.errorStream, s)
|
||||
}
|
||||
}
|
246
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/httpstream_test.go
generated
vendored
Normal file
246
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/httpstream_test.go
generated
vendored
Normal file
@ -0,0 +1,246 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
func TestHTTPStreamReceived(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
port string
|
||||
streamType string
|
||||
expectedError string
|
||||
}{
|
||||
"missing port": {
|
||||
expectedError: `"port" header is required`,
|
||||
},
|
||||
"unable to parse port": {
|
||||
port: "abc",
|
||||
expectedError: `unable to parse "abc" as a port: strconv.ParseUint: parsing "abc": invalid syntax`,
|
||||
},
|
||||
"negative port": {
|
||||
port: "-1",
|
||||
expectedError: `unable to parse "-1" as a port: strconv.ParseUint: parsing "-1": invalid syntax`,
|
||||
},
|
||||
"missing stream type": {
|
||||
port: "80",
|
||||
expectedError: `"streamType" header is required`,
|
||||
},
|
||||
"valid port with error stream": {
|
||||
port: "80",
|
||||
streamType: "error",
|
||||
},
|
||||
"valid port with data stream": {
|
||||
port: "80",
|
||||
streamType: "data",
|
||||
},
|
||||
"invalid stream type": {
|
||||
port: "80",
|
||||
streamType: "foo",
|
||||
expectedError: `invalid stream type "foo"`,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
streams := make(chan httpstream.Stream, 1)
|
||||
f := httpStreamReceived(streams)
|
||||
stream := newFakeHttpStream()
|
||||
if len(test.port) > 0 {
|
||||
stream.headers.Set("port", test.port)
|
||||
}
|
||||
if len(test.streamType) > 0 {
|
||||
stream.headers.Set("streamType", test.streamType)
|
||||
}
|
||||
replySent := make(chan struct{})
|
||||
err := f(stream, replySent)
|
||||
close(replySent)
|
||||
if len(test.expectedError) > 0 {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected err=%q, but it was nil", name, test.expectedError)
|
||||
}
|
||||
if e, a := test.expectedError, err.Error(); e != a {
|
||||
t.Errorf("%s: expected err=%q, got %q", name, e, a)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", name, err)
|
||||
continue
|
||||
}
|
||||
if s := <-streams; s != stream {
|
||||
t.Errorf("%s: expected stream %#v, got %#v", name, stream, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStreamPair(t *testing.T) {
|
||||
timeout := make(chan time.Time)
|
||||
|
||||
h := &httpStreamHandler{
|
||||
streamPairs: make(map[string]*httpStreamPair),
|
||||
}
|
||||
|
||||
// test adding a new entry
|
||||
p, created := h.getStreamPair("1")
|
||||
if p == nil {
|
||||
t.Fatalf("unexpected nil pair")
|
||||
}
|
||||
if !created {
|
||||
t.Fatal("expected created=true")
|
||||
}
|
||||
if p.dataStream != nil {
|
||||
t.Errorf("unexpected non-nil data stream")
|
||||
}
|
||||
if p.errorStream != nil {
|
||||
t.Errorf("unexpected non-nil error stream")
|
||||
}
|
||||
|
||||
// start the monitor for this pair
|
||||
monitorDone := make(chan struct{})
|
||||
go func() {
|
||||
h.monitorStreamPair(p, timeout)
|
||||
close(monitorDone)
|
||||
}()
|
||||
|
||||
if !h.hasStreamPair("1") {
|
||||
t.Fatal("This should still be true")
|
||||
}
|
||||
|
||||
// make sure we can retrieve an existing entry
|
||||
p2, created := h.getStreamPair("1")
|
||||
if created {
|
||||
t.Fatal("expected created=false")
|
||||
}
|
||||
if p != p2 {
|
||||
t.Fatalf("retrieving an existing pair: expected %#v, got %#v", p, p2)
|
||||
}
|
||||
|
||||
// removed via complete
|
||||
dataStream := newFakeHttpStream()
|
||||
dataStream.headers.Set(api.StreamType, api.StreamTypeData)
|
||||
complete, err := p.add(dataStream)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding data stream to pair: %v", err)
|
||||
}
|
||||
if complete {
|
||||
t.Fatalf("unexpected complete")
|
||||
}
|
||||
|
||||
errorStream := newFakeHttpStream()
|
||||
errorStream.headers.Set(api.StreamType, api.StreamTypeError)
|
||||
complete, err = p.add(errorStream)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error adding error stream to pair: %v", err)
|
||||
}
|
||||
if !complete {
|
||||
t.Fatal("unexpected incomplete")
|
||||
}
|
||||
|
||||
// make sure monitorStreamPair completed
|
||||
<-monitorDone
|
||||
|
||||
// make sure the pair was removed
|
||||
if h.hasStreamPair("1") {
|
||||
t.Fatal("expected removal of pair after both data and error streams received")
|
||||
}
|
||||
|
||||
// removed via timeout
|
||||
p, created = h.getStreamPair("2")
|
||||
if !created {
|
||||
t.Fatal("expected created=true")
|
||||
}
|
||||
if p == nil {
|
||||
t.Fatal("expected p not to be nil")
|
||||
}
|
||||
monitorDone = make(chan struct{})
|
||||
go func() {
|
||||
h.monitorStreamPair(p, timeout)
|
||||
close(monitorDone)
|
||||
}()
|
||||
// cause the timeout
|
||||
close(timeout)
|
||||
// make sure monitorStreamPair completed
|
||||
<-monitorDone
|
||||
if h.hasStreamPair("2") {
|
||||
t.Fatal("expected stream pair to be removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestID(t *testing.T) {
|
||||
h := &httpStreamHandler{}
|
||||
|
||||
s := newFakeHttpStream()
|
||||
s.headers.Set(api.StreamType, api.StreamTypeError)
|
||||
s.id = 1
|
||||
if e, a := "1", h.requestID(s); e != a {
|
||||
t.Errorf("expected %q, got %q", e, a)
|
||||
}
|
||||
|
||||
s.headers.Set(api.StreamType, api.StreamTypeData)
|
||||
s.id = 3
|
||||
if e, a := "1", h.requestID(s); e != a {
|
||||
t.Errorf("expected %q, got %q", e, a)
|
||||
}
|
||||
|
||||
s.id = 7
|
||||
s.headers.Set(api.PortForwardRequestIDHeader, "2")
|
||||
if e, a := "2", h.requestID(s); e != a {
|
||||
t.Errorf("expected %q, got %q", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeHttpStream struct {
|
||||
headers http.Header
|
||||
id uint32
|
||||
}
|
||||
|
||||
func newFakeHttpStream() *fakeHttpStream {
|
||||
return &fakeHttpStream{
|
||||
headers: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
var _ httpstream.Stream = &fakeHttpStream{}
|
||||
|
||||
func (s *fakeHttpStream) Read(data []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *fakeHttpStream) Write(data []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *fakeHttpStream) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeHttpStream) Reset() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeHttpStream) Headers() http.Header {
|
||||
return s.headers
|
||||
}
|
||||
|
||||
func (s *fakeHttpStream) Identifier() uint32 {
|
||||
return s.id
|
||||
}
|
53
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/portforward.go
generated
vendored
Normal file
53
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/portforward.go
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/util/wsstream"
|
||||
)
|
||||
|
||||
// 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 int32, stream io.ReadWriteCloser) error
|
||||
}
|
||||
|
||||
// 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, portForwardOptions *V4Options, idleTimeout time.Duration, streamCreationTimeout time.Duration, supportedProtocols []string) {
|
||||
var err error
|
||||
if wsstream.IsWebSocketRequest(req) {
|
||||
err = handleWebSocketStreams(req, w, portForwarder, podName, uid, portForwardOptions, supportedProtocols, idleTimeout, streamCreationTimeout)
|
||||
} else {
|
||||
err = handleHttpStreams(req, w, portForwarder, podName, uid, supportedProtocols, idleTimeout, streamCreationTimeout)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
runtime.HandleError(err)
|
||||
return
|
||||
}
|
||||
}
|
197
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/websocket.go
generated
vendored
Normal file
197
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/websocket.go
generated
vendored
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/server/httplog"
|
||||
"k8s.io/apiserver/pkg/util/wsstream"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
const (
|
||||
dataChannel = iota
|
||||
errorChannel
|
||||
|
||||
v4BinaryWebsocketProtocol = "v4." + wsstream.ChannelWebSocketProtocol
|
||||
v4Base64WebsocketProtocol = "v4." + wsstream.Base64ChannelWebSocketProtocol
|
||||
)
|
||||
|
||||
// options contains details about which streams are required for
|
||||
// port forwarding.
|
||||
// All fields incldued in V4Options need to be expressed explicilty in the
|
||||
// CRI (pkg/kubelet/apis/cri/{version}/api.proto) PortForwardRequest.
|
||||
type V4Options struct {
|
||||
Ports []int32
|
||||
}
|
||||
|
||||
// newOptions creates a new options from the Request.
|
||||
func NewV4Options(req *http.Request) (*V4Options, error) {
|
||||
if !wsstream.IsWebSocketRequest(req) {
|
||||
return &V4Options{}, nil
|
||||
}
|
||||
|
||||
portStrings := req.URL.Query()[api.PortHeader]
|
||||
if len(portStrings) == 0 {
|
||||
return nil, fmt.Errorf("query parameter %q is required", api.PortHeader)
|
||||
}
|
||||
|
||||
ports := make([]int32, 0, len(portStrings))
|
||||
for _, portString := range portStrings {
|
||||
if len(portString) == 0 {
|
||||
return nil, fmt.Errorf("query parameter %q cannot be empty", api.PortHeader)
|
||||
}
|
||||
for _, p := range strings.Split(portString, ",") {
|
||||
port, err := strconv.ParseUint(p, 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse %q as a port: %v", portString, err)
|
||||
}
|
||||
if port < 1 {
|
||||
return nil, fmt.Errorf("port %q must be > 0", portString)
|
||||
}
|
||||
ports = append(ports, int32(port))
|
||||
}
|
||||
}
|
||||
|
||||
return &V4Options{
|
||||
Ports: ports,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildV4Options returns a V4Options based on the given information.
|
||||
func BuildV4Options(ports []int32) (*V4Options, error) {
|
||||
return &V4Options{Ports: ports}, nil
|
||||
}
|
||||
|
||||
// handleWebSocketStreams handles requests to forward ports to a pod via
|
||||
// a PortForwarder. A pair of streams are created per port (DATA n,
|
||||
// ERROR n+1). The associated port is written to each stream as a unsigned 16
|
||||
// bit integer in little endian format.
|
||||
func handleWebSocketStreams(req *http.Request, w http.ResponseWriter, portForwarder PortForwarder, podName string, uid types.UID, opts *V4Options, supportedPortForwardProtocols []string, idleTimeout, streamCreationTimeout time.Duration) error {
|
||||
channels := make([]wsstream.ChannelType, 0, len(opts.Ports)*2)
|
||||
for i := 0; i < len(opts.Ports); i++ {
|
||||
channels = append(channels, wsstream.ReadWriteChannel, wsstream.WriteChannel)
|
||||
}
|
||||
conn := wsstream.NewConn(map[string]wsstream.ChannelProtocolConfig{
|
||||
"": {
|
||||
Binary: true,
|
||||
Channels: channels,
|
||||
},
|
||||
v4BinaryWebsocketProtocol: {
|
||||
Binary: true,
|
||||
Channels: channels,
|
||||
},
|
||||
v4Base64WebsocketProtocol: {
|
||||
Binary: false,
|
||||
Channels: channels,
|
||||
},
|
||||
})
|
||||
conn.SetIdleTimeout(idleTimeout)
|
||||
_, streams, err := conn.Open(httplog.Unlogged(w), req)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Unable to upgrade websocket connection: %v", err)
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
streamPairs := make([]*websocketStreamPair, len(opts.Ports))
|
||||
for i := range streamPairs {
|
||||
streamPair := websocketStreamPair{
|
||||
port: opts.Ports[i],
|
||||
dataStream: streams[i*2+dataChannel],
|
||||
errorStream: streams[i*2+errorChannel],
|
||||
}
|
||||
streamPairs[i] = &streamPair
|
||||
|
||||
portBytes := make([]byte, 2)
|
||||
// port is always positive so conversion is allowable
|
||||
binary.LittleEndian.PutUint16(portBytes, uint16(streamPair.port))
|
||||
streamPair.dataStream.Write(portBytes)
|
||||
streamPair.errorStream.Write(portBytes)
|
||||
}
|
||||
h := &websocketStreamHandler{
|
||||
conn: conn,
|
||||
streamPairs: streamPairs,
|
||||
pod: podName,
|
||||
uid: uid,
|
||||
forwarder: portForwarder,
|
||||
}
|
||||
h.run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// websocketStreamPair represents the error and data streams for a port
|
||||
// forwarding request.
|
||||
type websocketStreamPair struct {
|
||||
port int32
|
||||
dataStream io.ReadWriteCloser
|
||||
errorStream io.WriteCloser
|
||||
}
|
||||
|
||||
// websocketStreamHandler is capable of processing a single port forward
|
||||
// request over a websocket connection
|
||||
type websocketStreamHandler struct {
|
||||
conn *wsstream.Conn
|
||||
streamPairs []*websocketStreamPair
|
||||
pod string
|
||||
uid types.UID
|
||||
forwarder PortForwarder
|
||||
}
|
||||
|
||||
// run invokes the websocketStreamHandler's forwarder.PortForward
|
||||
// function for the given stream pair.
|
||||
func (h *websocketStreamHandler) run() {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(h.streamPairs))
|
||||
|
||||
for _, pair := range h.streamPairs {
|
||||
p := pair
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
h.portForward(p)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (h *websocketStreamHandler) portForward(p *websocketStreamPair) {
|
||||
defer p.dataStream.Close()
|
||||
defer p.errorStream.Close()
|
||||
|
||||
glog.V(5).Infof("(conn=%p) invoking forwarder.PortForward for port %d", h.conn, p.port)
|
||||
err := h.forwarder.PortForward(h.pod, h.uid, p.port, p.dataStream)
|
||||
glog.V(5).Infof("(conn=%p) done invoking forwarder.PortForward for port %d", h.conn, p.port)
|
||||
|
||||
if err != nil {
|
||||
msg := fmt.Errorf("error forwarding port %d to pod %s, uid %v: %v", p.port, h.pod, h.uid, err)
|
||||
runtime.HandleError(msg)
|
||||
fmt.Fprint(p.errorStream, msg.Error())
|
||||
}
|
||||
}
|
101
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/websocket_test.go
generated
vendored
Normal file
101
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/websocket_test.go
generated
vendored
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestV4Options(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
url string
|
||||
websocket bool
|
||||
expectedOpts *V4Options
|
||||
expectedError string
|
||||
}{
|
||||
"non-ws request": {
|
||||
url: "http://example.com",
|
||||
expectedOpts: &V4Options{},
|
||||
},
|
||||
"missing port": {
|
||||
url: "http://example.com",
|
||||
websocket: true,
|
||||
expectedError: `query parameter "port" is required`,
|
||||
},
|
||||
"unable to parse port": {
|
||||
url: "http://example.com?port=abc",
|
||||
websocket: true,
|
||||
expectedError: `unable to parse "abc" as a port: strconv.ParseUint: parsing "abc": invalid syntax`,
|
||||
},
|
||||
"negative port": {
|
||||
url: "http://example.com?port=-1",
|
||||
websocket: true,
|
||||
expectedError: `unable to parse "-1" as a port: strconv.ParseUint: parsing "-1": invalid syntax`,
|
||||
},
|
||||
"one port": {
|
||||
url: "http://example.com?port=80",
|
||||
websocket: true,
|
||||
expectedOpts: &V4Options{
|
||||
Ports: []int32{80},
|
||||
},
|
||||
},
|
||||
"multiple ports": {
|
||||
url: "http://example.com?port=80,90,100",
|
||||
websocket: true,
|
||||
expectedOpts: &V4Options{
|
||||
Ports: []int32{80, 90, 100},
|
||||
},
|
||||
},
|
||||
"multiple port": {
|
||||
url: "http://example.com?port=80&port=90",
|
||||
websocket: true,
|
||||
expectedOpts: &V4Options{
|
||||
Ports: []int32{80, 90},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
req, err := http.NewRequest(http.MethodGet, test.url, nil)
|
||||
if err != nil {
|
||||
t.Errorf("%s: invalid url %q err=%q", name, test.url, err)
|
||||
continue
|
||||
}
|
||||
if test.websocket {
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
}
|
||||
opts, err := NewV4Options(req)
|
||||
if len(test.expectedError) > 0 {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected err=%q, but it was nil", name, test.expectedError)
|
||||
}
|
||||
if e, a := test.expectedError, err.Error(); e != a {
|
||||
t.Errorf("%s: expected err=%q, got %q", name, e, a)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", name, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(test.expectedOpts, opts) {
|
||||
t.Errorf("%s: expected options %#v, got %#v", name, test.expectedOpts, err)
|
||||
}
|
||||
}
|
||||
}
|
46
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/BUILD
generated
vendored
Normal file
46
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/BUILD
generated
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"attach.go",
|
||||
"doc.go",
|
||||
"exec.go",
|
||||
"httpstream.go",
|
||||
"websocket.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server/remotecommand",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/httpstream:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/remotecommand:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/httplog:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/wsstream:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/remotecommand:go_default_library",
|
||||
"//vendor/k8s.io/utils/exec:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
59
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/attach.go
generated
vendored
Normal file
59
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/attach.go
generated
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package remotecommand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
)
|
||||
|
||||
// Attacher knows how to attach to a running container in a pod.
|
||||
type Attacher interface {
|
||||
// AttachContainer attaches to the running container in the pod, copying data between in/out/err
|
||||
// and the container's stdin/stdout/stderr.
|
||||
AttachContainer(name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error
|
||||
}
|
||||
|
||||
// ServeAttach handles requests to attach to a container. After creating/receiving the required
|
||||
// streams, it delegates the actual attaching to attacher.
|
||||
func ServeAttach(w http.ResponseWriter, req *http.Request, attacher Attacher, podName string, uid types.UID, container string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) {
|
||||
ctx, ok := createStreams(req, w, streamOpts, supportedProtocols, idleTimeout, streamCreationTimeout)
|
||||
if !ok {
|
||||
// error is handled by createStreams
|
||||
return
|
||||
}
|
||||
defer ctx.conn.Close()
|
||||
|
||||
err := attacher.AttachContainer(podName, uid, container, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error attaching to container: %v", err)
|
||||
runtime.HandleError(err)
|
||||
ctx.writeStatus(apierrors.NewInternalError(err))
|
||||
} else {
|
||||
ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{
|
||||
Status: metav1.StatusSuccess,
|
||||
}})
|
||||
}
|
||||
}
|
18
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/doc.go
generated
vendored
Normal file
18
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/doc.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// package remotecommand contains functions related to executing commands in and attaching to pods.
|
||||
package remotecommand // import "k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
|
79
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/exec.go
generated
vendored
Normal file
79
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/exec.go
generated
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package remotecommand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
utilexec "k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
// Executor knows how to execute a command in a container in a pod.
|
||||
type Executor interface {
|
||||
// ExecInContainer executes a command in a container in the pod, copying data
|
||||
// between in/out/err and the container's stdin/stdout/stderr.
|
||||
ExecInContainer(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error
|
||||
}
|
||||
|
||||
// ServeExec handles requests to execute a command in a container. After
|
||||
// creating/receiving the required streams, it delegates the actual execution
|
||||
// to the executor.
|
||||
func ServeExec(w http.ResponseWriter, req *http.Request, executor Executor, podName string, uid types.UID, container string, cmd []string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) {
|
||||
ctx, ok := createStreams(req, w, streamOpts, supportedProtocols, idleTimeout, streamCreationTimeout)
|
||||
if !ok {
|
||||
// error is handled by createStreams
|
||||
return
|
||||
}
|
||||
defer ctx.conn.Close()
|
||||
|
||||
err := executor.ExecInContainer(podName, uid, container, cmd, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan, 0)
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(utilexec.ExitError); ok && exitErr.Exited() {
|
||||
rc := exitErr.ExitStatus()
|
||||
ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Reason: remotecommandconsts.NonZeroExitCodeReason,
|
||||
Details: &metav1.StatusDetails{
|
||||
Causes: []metav1.StatusCause{
|
||||
{
|
||||
Type: remotecommandconsts.ExitCodeCauseType,
|
||||
Message: fmt.Sprintf("%d", rc),
|
||||
},
|
||||
},
|
||||
},
|
||||
Message: fmt.Sprintf("command terminated with non-zero exit code: %v", exitErr),
|
||||
}})
|
||||
} else {
|
||||
err = fmt.Errorf("error executing command in container: %v", err)
|
||||
runtime.HandleError(err)
|
||||
ctx.writeStatus(apierrors.NewInternalError(err))
|
||||
}
|
||||
} else {
|
||||
ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{
|
||||
Status: metav1.StatusSuccess,
|
||||
}})
|
||||
}
|
||||
}
|
447
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/httpstream.go
generated
vendored
Normal file
447
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/httpstream.go
generated
vendored
Normal file
@ -0,0 +1,447 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package remotecommand
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
|
||||
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/util/wsstream"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Options contains details about which streams are required for
|
||||
// remote command execution.
|
||||
type Options struct {
|
||||
Stdin bool
|
||||
Stdout bool
|
||||
Stderr bool
|
||||
TTY bool
|
||||
}
|
||||
|
||||
// NewOptions creates a new Options from the Request.
|
||||
func NewOptions(req *http.Request) (*Options, error) {
|
||||
tty := req.FormValue(api.ExecTTYParam) == "1"
|
||||
stdin := req.FormValue(api.ExecStdinParam) == "1"
|
||||
stdout := req.FormValue(api.ExecStdoutParam) == "1"
|
||||
stderr := req.FormValue(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
|
||||
}
|
||||
|
||||
if !stdin && !stdout && !stderr {
|
||||
return nil, fmt.Errorf("you must specify at least 1 of stdin, stdout, stderr")
|
||||
}
|
||||
|
||||
return &Options{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
TTY: tty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// context contains the connection and streams used when
|
||||
// forwarding an attach or execute session into a container.
|
||||
type context struct {
|
||||
conn io.Closer
|
||||
stdinStream io.ReadCloser
|
||||
stdoutStream io.WriteCloser
|
||||
stderrStream io.WriteCloser
|
||||
writeStatus func(status *apierrors.StatusError) error
|
||||
resizeStream io.ReadCloser
|
||||
resizeChan chan remotecommand.TerminalSize
|
||||
tty bool
|
||||
}
|
||||
|
||||
// streamAndReply holds both a Stream and a channel that is closed when the stream's reply frame is
|
||||
// enqueued. Consumers can wait for replySent to be closed prior to proceeding, to ensure that the
|
||||
// replyFrame is enqueued before the connection's goaway frame is sent (e.g. if a stream was
|
||||
// received and right after, the connection gets closed).
|
||||
type streamAndReply struct {
|
||||
httpstream.Stream
|
||||
replySent <-chan struct{}
|
||||
}
|
||||
|
||||
// waitStreamReply waits until either replySent or stop is closed. If replySent is closed, it sends
|
||||
// an empty struct to the notify channel.
|
||||
func waitStreamReply(replySent <-chan struct{}, notify chan<- struct{}, stop <-chan struct{}) {
|
||||
select {
|
||||
case <-replySent:
|
||||
notify <- struct{}{}
|
||||
case <-stop:
|
||||
}
|
||||
}
|
||||
|
||||
func createStreams(req *http.Request, w http.ResponseWriter, opts *Options, supportedStreamProtocols []string, idleTimeout, streamCreationTimeout time.Duration) (*context, bool) {
|
||||
var ctx *context
|
||||
var ok bool
|
||||
if wsstream.IsWebSocketRequest(req) {
|
||||
ctx, ok = createWebSocketStreams(req, w, opts, idleTimeout)
|
||||
} else {
|
||||
ctx, ok = createHttpStreamStreams(req, w, opts, supportedStreamProtocols, idleTimeout, streamCreationTimeout)
|
||||
}
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if ctx.resizeStream != nil {
|
||||
ctx.resizeChan = make(chan remotecommand.TerminalSize)
|
||||
go handleResizeEvents(ctx.resizeStream, ctx.resizeChan)
|
||||
}
|
||||
|
||||
return ctx, true
|
||||
}
|
||||
|
||||
func createHttpStreamStreams(req *http.Request, w http.ResponseWriter, opts *Options, supportedStreamProtocols []string, idleTimeout, streamCreationTimeout time.Duration) (*context, bool) {
|
||||
protocol, err := httpstream.Handshake(req, w, supportedStreamProtocols)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprint(w, err.Error())
|
||||
return nil, false
|
||||
}
|
||||
|
||||
streamCh := make(chan streamAndReply)
|
||||
|
||||
upgrader := spdy.NewResponseUpgrader()
|
||||
conn := upgrader.UpgradeResponse(w, req, func(stream httpstream.Stream, replySent <-chan struct{}) error {
|
||||
streamCh <- streamAndReply{Stream: stream, replySent: replySent}
|
||||
return nil
|
||||
})
|
||||
// from this point on, we can no longer call methods on response
|
||||
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.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
conn.SetIdleTimeout(idleTimeout)
|
||||
|
||||
var handler protocolHandler
|
||||
switch protocol {
|
||||
case remotecommandconsts.StreamProtocolV4Name:
|
||||
handler = &v4ProtocolHandler{}
|
||||
case remotecommandconsts.StreamProtocolV3Name:
|
||||
handler = &v3ProtocolHandler{}
|
||||
case remotecommandconsts.StreamProtocolV2Name:
|
||||
handler = &v2ProtocolHandler{}
|
||||
case "":
|
||||
glog.V(4).Infof("Client did not request protocol negotiaion. Falling back to %q", remotecommandconsts.StreamProtocolV1Name)
|
||||
fallthrough
|
||||
case remotecommandconsts.StreamProtocolV1Name:
|
||||
handler = &v1ProtocolHandler{}
|
||||
}
|
||||
|
||||
// count the streams client asked for, starting with 1
|
||||
expectedStreams := 1
|
||||
if opts.Stdin {
|
||||
expectedStreams++
|
||||
}
|
||||
if opts.Stdout {
|
||||
expectedStreams++
|
||||
}
|
||||
if opts.Stderr {
|
||||
expectedStreams++
|
||||
}
|
||||
if opts.TTY && handler.supportsTerminalResizing() {
|
||||
expectedStreams++
|
||||
}
|
||||
|
||||
expired := time.NewTimer(streamCreationTimeout)
|
||||
defer expired.Stop()
|
||||
|
||||
ctx, err := handler.waitForStreams(streamCh, expectedStreams, expired.C)
|
||||
if err != nil {
|
||||
runtime.HandleError(err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ctx.conn = conn
|
||||
ctx.tty = opts.TTY
|
||||
|
||||
return ctx, true
|
||||
}
|
||||
|
||||
type protocolHandler interface {
|
||||
// waitForStreams waits for the expected streams or a timeout, returning a
|
||||
// remoteCommandContext if all the streams were received, or an error if not.
|
||||
waitForStreams(streams <-chan streamAndReply, expectedStreams int, expired <-chan time.Time) (*context, error)
|
||||
// supportsTerminalResizing returns true if the protocol handler supports terminal resizing
|
||||
supportsTerminalResizing() bool
|
||||
}
|
||||
|
||||
// v4ProtocolHandler implements the V4 protocol version for streaming command execution. It only differs
|
||||
// in from v3 in the error stream format using an json-marshaled metav1.Status which carries
|
||||
// the process' exit code.
|
||||
type v4ProtocolHandler struct{}
|
||||
|
||||
func (*v4ProtocolHandler) waitForStreams(streams <-chan streamAndReply, expectedStreams int, expired <-chan time.Time) (*context, error) {
|
||||
ctx := &context{}
|
||||
receivedStreams := 0
|
||||
replyChan := make(chan struct{})
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
WaitForStreams:
|
||||
for {
|
||||
select {
|
||||
case stream := <-streams:
|
||||
streamType := stream.Headers().Get(api.StreamType)
|
||||
switch streamType {
|
||||
case api.StreamTypeError:
|
||||
ctx.writeStatus = v4WriteStatusFunc(stream) // write json errors
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStdin:
|
||||
ctx.stdinStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStdout:
|
||||
ctx.stdoutStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStderr:
|
||||
ctx.stderrStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeResize:
|
||||
ctx.resizeStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
default:
|
||||
runtime.HandleError(fmt.Errorf("Unexpected stream type: %q", streamType))
|
||||
}
|
||||
case <-replyChan:
|
||||
receivedStreams++
|
||||
if receivedStreams == expectedStreams {
|
||||
break WaitForStreams
|
||||
}
|
||||
case <-expired:
|
||||
// TODO find a way to return the error to the user. Maybe use a separate
|
||||
// stream to report errors?
|
||||
return nil, errors.New("timed out waiting for client to create streams")
|
||||
}
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// supportsTerminalResizing returns true because v4ProtocolHandler supports it
|
||||
func (*v4ProtocolHandler) supportsTerminalResizing() bool { return true }
|
||||
|
||||
// v3ProtocolHandler implements the V3 protocol version for streaming command execution.
|
||||
type v3ProtocolHandler struct{}
|
||||
|
||||
func (*v3ProtocolHandler) waitForStreams(streams <-chan streamAndReply, expectedStreams int, expired <-chan time.Time) (*context, error) {
|
||||
ctx := &context{}
|
||||
receivedStreams := 0
|
||||
replyChan := make(chan struct{})
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
WaitForStreams:
|
||||
for {
|
||||
select {
|
||||
case stream := <-streams:
|
||||
streamType := stream.Headers().Get(api.StreamType)
|
||||
switch streamType {
|
||||
case api.StreamTypeError:
|
||||
ctx.writeStatus = v1WriteStatusFunc(stream)
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStdin:
|
||||
ctx.stdinStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStdout:
|
||||
ctx.stdoutStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStderr:
|
||||
ctx.stderrStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeResize:
|
||||
ctx.resizeStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
default:
|
||||
runtime.HandleError(fmt.Errorf("Unexpected stream type: %q", streamType))
|
||||
}
|
||||
case <-replyChan:
|
||||
receivedStreams++
|
||||
if receivedStreams == expectedStreams {
|
||||
break WaitForStreams
|
||||
}
|
||||
case <-expired:
|
||||
// TODO find a way to return the error to the user. Maybe use a separate
|
||||
// stream to report errors?
|
||||
return nil, errors.New("timed out waiting for client to create streams")
|
||||
}
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// supportsTerminalResizing returns true because v3ProtocolHandler supports it
|
||||
func (*v3ProtocolHandler) supportsTerminalResizing() bool { return true }
|
||||
|
||||
// v2ProtocolHandler implements the V2 protocol version for streaming command execution.
|
||||
type v2ProtocolHandler struct{}
|
||||
|
||||
func (*v2ProtocolHandler) waitForStreams(streams <-chan streamAndReply, expectedStreams int, expired <-chan time.Time) (*context, error) {
|
||||
ctx := &context{}
|
||||
receivedStreams := 0
|
||||
replyChan := make(chan struct{})
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
WaitForStreams:
|
||||
for {
|
||||
select {
|
||||
case stream := <-streams:
|
||||
streamType := stream.Headers().Get(api.StreamType)
|
||||
switch streamType {
|
||||
case api.StreamTypeError:
|
||||
ctx.writeStatus = v1WriteStatusFunc(stream)
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStdin:
|
||||
ctx.stdinStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStdout:
|
||||
ctx.stdoutStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStderr:
|
||||
ctx.stderrStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
default:
|
||||
runtime.HandleError(fmt.Errorf("Unexpected stream type: %q", streamType))
|
||||
}
|
||||
case <-replyChan:
|
||||
receivedStreams++
|
||||
if receivedStreams == expectedStreams {
|
||||
break WaitForStreams
|
||||
}
|
||||
case <-expired:
|
||||
// TODO find a way to return the error to the user. Maybe use a separate
|
||||
// stream to report errors?
|
||||
return nil, errors.New("timed out waiting for client to create streams")
|
||||
}
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// supportsTerminalResizing returns false because v2ProtocolHandler doesn't support it.
|
||||
func (*v2ProtocolHandler) supportsTerminalResizing() bool { return false }
|
||||
|
||||
// v1ProtocolHandler implements the V1 protocol version for streaming command execution.
|
||||
type v1ProtocolHandler struct{}
|
||||
|
||||
func (*v1ProtocolHandler) waitForStreams(streams <-chan streamAndReply, expectedStreams int, expired <-chan time.Time) (*context, error) {
|
||||
ctx := &context{}
|
||||
receivedStreams := 0
|
||||
replyChan := make(chan struct{})
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
WaitForStreams:
|
||||
for {
|
||||
select {
|
||||
case stream := <-streams:
|
||||
streamType := stream.Headers().Get(api.StreamType)
|
||||
switch streamType {
|
||||
case api.StreamTypeError:
|
||||
ctx.writeStatus = v1WriteStatusFunc(stream)
|
||||
|
||||
// This defer statement shouldn't be here, but due to previous refactoring, it ended up in
|
||||
// here. This is what 1.0.x kubelets do, so we're retaining that behavior. This is fixed in
|
||||
// the v2ProtocolHandler.
|
||||
defer stream.Reset()
|
||||
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStdin:
|
||||
ctx.stdinStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStdout:
|
||||
ctx.stdoutStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
case api.StreamTypeStderr:
|
||||
ctx.stderrStream = stream
|
||||
go waitStreamReply(stream.replySent, replyChan, stop)
|
||||
default:
|
||||
runtime.HandleError(fmt.Errorf("Unexpected stream type: %q", streamType))
|
||||
}
|
||||
case <-replyChan:
|
||||
receivedStreams++
|
||||
if receivedStreams == expectedStreams {
|
||||
break WaitForStreams
|
||||
}
|
||||
case <-expired:
|
||||
// TODO find a way to return the error to the user. Maybe use a separate
|
||||
// stream to report errors?
|
||||
return nil, errors.New("timed out waiting for client to create streams")
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.stdinStream != nil {
|
||||
ctx.stdinStream.Close()
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// supportsTerminalResizing returns false because v1ProtocolHandler doesn't support it.
|
||||
func (*v1ProtocolHandler) supportsTerminalResizing() bool { return false }
|
||||
|
||||
func handleResizeEvents(stream io.Reader, channel chan<- remotecommand.TerminalSize) {
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
decoder := json.NewDecoder(stream)
|
||||
for {
|
||||
size := remotecommand.TerminalSize{}
|
||||
if err := decoder.Decode(&size); err != nil {
|
||||
break
|
||||
}
|
||||
channel <- size
|
||||
}
|
||||
}
|
||||
|
||||
func v1WriteStatusFunc(stream io.Writer) func(status *apierrors.StatusError) error {
|
||||
return func(status *apierrors.StatusError) error {
|
||||
if status.Status().Status == metav1.StatusSuccess {
|
||||
return nil // send error messages
|
||||
}
|
||||
_, err := stream.Write([]byte(status.Error()))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// v4WriteStatusFunc returns a WriteStatusFunc that marshals a given api Status
|
||||
// as json in the error channel.
|
||||
func v4WriteStatusFunc(stream io.Writer) func(status *apierrors.StatusError) error {
|
||||
return func(status *apierrors.StatusError) error {
|
||||
bs, err := json.Marshal(status.Status())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stream.Write(bs)
|
||||
return err
|
||||
}
|
||||
}
|
132
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/websocket.go
generated
vendored
Normal file
132
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/websocket.go
generated
vendored
Normal file
@ -0,0 +1,132 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package remotecommand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/server/httplog"
|
||||
"k8s.io/apiserver/pkg/util/wsstream"
|
||||
)
|
||||
|
||||
const (
|
||||
stdinChannel = iota
|
||||
stdoutChannel
|
||||
stderrChannel
|
||||
errorChannel
|
||||
resizeChannel
|
||||
|
||||
preV4BinaryWebsocketProtocol = wsstream.ChannelWebSocketProtocol
|
||||
preV4Base64WebsocketProtocol = wsstream.Base64ChannelWebSocketProtocol
|
||||
v4BinaryWebsocketProtocol = "v4." + wsstream.ChannelWebSocketProtocol
|
||||
v4Base64WebsocketProtocol = "v4." + wsstream.Base64ChannelWebSocketProtocol
|
||||
)
|
||||
|
||||
// createChannels returns the standard channel types for a shell connection (STDIN 0, STDOUT 1, STDERR 2)
|
||||
// along with the approximate duplex value. It also creates the error (3) and resize (4) channels.
|
||||
func createChannels(opts *Options) []wsstream.ChannelType {
|
||||
// open the requested channels, and always open the error channel
|
||||
channels := make([]wsstream.ChannelType, 5)
|
||||
channels[stdinChannel] = readChannel(opts.Stdin)
|
||||
channels[stdoutChannel] = writeChannel(opts.Stdout)
|
||||
channels[stderrChannel] = writeChannel(opts.Stderr)
|
||||
channels[errorChannel] = wsstream.WriteChannel
|
||||
channels[resizeChannel] = wsstream.ReadChannel
|
||||
return channels
|
||||
}
|
||||
|
||||
// readChannel returns wsstream.ReadChannel if real is true, or wsstream.IgnoreChannel.
|
||||
func readChannel(real bool) wsstream.ChannelType {
|
||||
if real {
|
||||
return wsstream.ReadChannel
|
||||
}
|
||||
return wsstream.IgnoreChannel
|
||||
}
|
||||
|
||||
// writeChannel returns wsstream.WriteChannel if real is true, or wsstream.IgnoreChannel.
|
||||
func writeChannel(real bool) wsstream.ChannelType {
|
||||
if real {
|
||||
return wsstream.WriteChannel
|
||||
}
|
||||
return wsstream.IgnoreChannel
|
||||
}
|
||||
|
||||
// createWebSocketStreams returns a context containing the websocket connection and
|
||||
// streams needed to perform an exec or an attach.
|
||||
func createWebSocketStreams(req *http.Request, w http.ResponseWriter, opts *Options, idleTimeout time.Duration) (*context, bool) {
|
||||
channels := createChannels(opts)
|
||||
conn := wsstream.NewConn(map[string]wsstream.ChannelProtocolConfig{
|
||||
"": {
|
||||
Binary: true,
|
||||
Channels: channels,
|
||||
},
|
||||
preV4BinaryWebsocketProtocol: {
|
||||
Binary: true,
|
||||
Channels: channels,
|
||||
},
|
||||
preV4Base64WebsocketProtocol: {
|
||||
Binary: false,
|
||||
Channels: channels,
|
||||
},
|
||||
v4BinaryWebsocketProtocol: {
|
||||
Binary: true,
|
||||
Channels: channels,
|
||||
},
|
||||
v4Base64WebsocketProtocol: {
|
||||
Binary: false,
|
||||
Channels: channels,
|
||||
},
|
||||
})
|
||||
conn.SetIdleTimeout(idleTimeout)
|
||||
negotiatedProtocol, streams, err := conn.Open(httplog.Unlogged(w), req)
|
||||
if err != nil {
|
||||
runtime.HandleError(fmt.Errorf("Unable to upgrade websocket connection: %v", err))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Send an empty message to the lowest writable channel to notify the client the connection is established
|
||||
// TODO: make generic to SPDY and WebSockets and do it outside of this method?
|
||||
switch {
|
||||
case opts.Stdout:
|
||||
streams[stdoutChannel].Write([]byte{})
|
||||
case opts.Stderr:
|
||||
streams[stderrChannel].Write([]byte{})
|
||||
default:
|
||||
streams[errorChannel].Write([]byte{})
|
||||
}
|
||||
|
||||
ctx := &context{
|
||||
conn: conn,
|
||||
stdinStream: streams[stdinChannel],
|
||||
stdoutStream: streams[stdoutChannel],
|
||||
stderrStream: streams[stderrChannel],
|
||||
tty: opts.TTY,
|
||||
resizeStream: streams[resizeChannel],
|
||||
}
|
||||
|
||||
switch negotiatedProtocol {
|
||||
case v4BinaryWebsocketProtocol, v4Base64WebsocketProtocol:
|
||||
ctx.writeStatus = v4WriteStatusFunc(streams[errorChannel])
|
||||
default:
|
||||
ctx.writeStatus = v1WriteStatusFunc(streams[errorChannel])
|
||||
}
|
||||
|
||||
return ctx, true
|
||||
}
|
846
vendor/k8s.io/kubernetes/pkg/kubelet/server/server.go
generated
vendored
Normal file
846
vendor/k8s.io/kubernetes/pkg/kubelet/server/server.go
generated
vendored
Normal file
@ -0,0 +1,846 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/url"
|
||||
"reflect"
|
||||
goruntime "runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"github.com/golang/glog"
|
||||
cadvisorapi "github.com/google/cadvisor/info/v1"
|
||||
"github.com/google/cadvisor/metrics"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/httplog"
|
||||
"k8s.io/apiserver/pkg/util/flushwriter"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/core/v1/validation"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/portforward"
|
||||
remotecommandserver "k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/stats"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/streaming"
|
||||
kubelettypes "k8s.io/kubernetes/pkg/kubelet/types"
|
||||
"k8s.io/kubernetes/pkg/util/configz"
|
||||
"k8s.io/kubernetes/pkg/util/limitwriter"
|
||||
)
|
||||
|
||||
const (
|
||||
metricsPath = "/metrics"
|
||||
cadvisorMetricsPath = "/metrics/cadvisor"
|
||||
specPath = "/spec/"
|
||||
statsPath = "/stats/"
|
||||
logsPath = "/logs/"
|
||||
)
|
||||
|
||||
// Server is a http.Handler which exposes kubelet functionality over HTTP.
|
||||
type Server struct {
|
||||
auth AuthInterface
|
||||
host HostInterface
|
||||
restfulCont containerInterface
|
||||
resourceAnalyzer stats.ResourceAnalyzer
|
||||
runtime kubecontainer.Runtime
|
||||
}
|
||||
|
||||
type TLSOptions struct {
|
||||
Config *tls.Config
|
||||
CertFile string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ListenAndServeKubeletServer initializes a server to respond to HTTP network requests on the Kubelet.
|
||||
func ListenAndServeKubeletServer(
|
||||
host HostInterface,
|
||||
resourceAnalyzer stats.ResourceAnalyzer,
|
||||
address net.IP,
|
||||
port uint,
|
||||
tlsOptions *TLSOptions,
|
||||
auth AuthInterface,
|
||||
enableDebuggingHandlers,
|
||||
enableContentionProfiling bool,
|
||||
runtime kubecontainer.Runtime,
|
||||
criHandler http.Handler) {
|
||||
glog.Infof("Starting to listen on %s:%d", address, port)
|
||||
handler := NewServer(host, resourceAnalyzer, auth, enableDebuggingHandlers, enableContentionProfiling, runtime, criHandler)
|
||||
s := &http.Server{
|
||||
Addr: net.JoinHostPort(address.String(), strconv.FormatUint(uint64(port), 10)),
|
||||
Handler: &handler,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
if tlsOptions != nil {
|
||||
s.TLSConfig = tlsOptions.Config
|
||||
// Passing empty strings as the cert and key files means no
|
||||
// cert/keys are specified and GetCertificate in the TLSConfig
|
||||
// should be called instead.
|
||||
glog.Fatal(s.ListenAndServeTLS(tlsOptions.CertFile, tlsOptions.KeyFile))
|
||||
} else {
|
||||
glog.Fatal(s.ListenAndServe())
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServeKubeletReadOnlyServer initializes a server to respond to HTTP network requests on the Kubelet.
|
||||
func ListenAndServeKubeletReadOnlyServer(host HostInterface, resourceAnalyzer stats.ResourceAnalyzer, address net.IP, port uint, runtime kubecontainer.Runtime) {
|
||||
glog.V(1).Infof("Starting to listen read-only on %s:%d", address, port)
|
||||
s := NewServer(host, resourceAnalyzer, nil, false, false, runtime, nil)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: net.JoinHostPort(address.String(), strconv.FormatUint(uint64(port), 10)),
|
||||
Handler: &s,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
glog.Fatal(server.ListenAndServe())
|
||||
}
|
||||
|
||||
// AuthInterface contains all methods required by the auth filters
|
||||
type AuthInterface interface {
|
||||
authenticator.Request
|
||||
authorizer.RequestAttributesGetter
|
||||
authorizer.Authorizer
|
||||
}
|
||||
|
||||
// HostInterface contains all the kubelet methods required by the server.
|
||||
// For testability.
|
||||
type HostInterface interface {
|
||||
stats.StatsProvider
|
||||
GetVersionInfo() (*cadvisorapi.VersionInfo, error)
|
||||
GetCachedMachineInfo() (*cadvisorapi.MachineInfo, error)
|
||||
GetRunningPods() ([]*v1.Pod, error)
|
||||
RunInContainer(name string, uid types.UID, container string, cmd []string) ([]byte, error)
|
||||
ExecInContainer(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error
|
||||
AttachContainer(name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error
|
||||
GetKubeletContainerLogs(podFullName, containerName string, logOptions *v1.PodLogOptions, stdout, stderr io.Writer) error
|
||||
ServeLogs(w http.ResponseWriter, req *http.Request)
|
||||
PortForward(name string, uid types.UID, port int32, stream io.ReadWriteCloser) error
|
||||
StreamingConnectionIdleTimeout() time.Duration
|
||||
ResyncInterval() time.Duration
|
||||
GetHostname() string
|
||||
LatestLoopEntryTime() time.Time
|
||||
GetExec(podFullName string, podUID types.UID, containerName string, cmd []string, streamOpts remotecommandserver.Options) (*url.URL, error)
|
||||
GetAttach(podFullName string, podUID types.UID, containerName string, streamOpts remotecommandserver.Options) (*url.URL, error)
|
||||
GetPortForward(podName, podNamespace string, podUID types.UID, portForwardOpts portforward.V4Options) (*url.URL, error)
|
||||
}
|
||||
|
||||
// NewServer initializes and configures a kubelet.Server object to handle HTTP requests.
|
||||
func NewServer(
|
||||
host HostInterface,
|
||||
resourceAnalyzer stats.ResourceAnalyzer,
|
||||
auth AuthInterface,
|
||||
enableDebuggingHandlers,
|
||||
enableContentionProfiling bool,
|
||||
runtime kubecontainer.Runtime,
|
||||
criHandler http.Handler) Server {
|
||||
server := Server{
|
||||
host: host,
|
||||
resourceAnalyzer: resourceAnalyzer,
|
||||
auth: auth,
|
||||
restfulCont: &filteringContainer{Container: restful.NewContainer()},
|
||||
runtime: runtime,
|
||||
}
|
||||
if auth != nil {
|
||||
server.InstallAuthFilter()
|
||||
}
|
||||
server.InstallDefaultHandlers()
|
||||
if enableDebuggingHandlers {
|
||||
server.InstallDebuggingHandlers(criHandler)
|
||||
if enableContentionProfiling {
|
||||
goruntime.SetBlockProfileRate(1)
|
||||
}
|
||||
} else {
|
||||
server.InstallDebuggingDisabledHandlers()
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
// 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
|
||||
decision, _, err := s.auth.Authorize(attrs)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Authorization error (user=%s, verb=%s, resource=%s, subresource=%s)", u.GetName(), attrs.GetVerb(), attrs.GetResource(), attrs.GetSubresource())
|
||||
glog.Errorf(msg, err)
|
||||
resp.WriteErrorString(http.StatusInternalServerError, msg)
|
||||
return
|
||||
}
|
||||
if decision != authorizer.DecisionAllow {
|
||||
msg := fmt.Sprintf("Forbidden (user=%s, verb=%s, resource=%s, subresource=%s)", u.GetName(), attrs.GetVerb(), attrs.GetResource(), attrs.GetSubresource())
|
||||
glog.V(2).Info(msg)
|
||||
resp.WriteErrorString(http.StatusForbidden, msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Continue
|
||||
chain.ProcessFilter(req, resp)
|
||||
})
|
||||
}
|
||||
|
||||
// InstallDefaultHandlers registers the default set of supported HTTP request
|
||||
// patterns with the restful Container.
|
||||
func (s *Server) InstallDefaultHandlers() {
|
||||
healthz.InstallHandler(s.restfulCont,
|
||||
healthz.PingHealthz,
|
||||
healthz.NamedCheck("syncloop", s.syncLoopHealthCheck),
|
||||
)
|
||||
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.Add(stats.CreateHandlers(statsPath, s.host, s.resourceAnalyzer))
|
||||
s.restfulCont.Handle(metricsPath, prometheus.Handler())
|
||||
|
||||
// cAdvisor metrics are exposed under the secured handler as well
|
||||
r := prometheus.NewRegistry()
|
||||
r.MustRegister(metrics.NewPrometheusCollector(prometheusHostAdapter{s.host}, containerPrometheusLabels))
|
||||
s.restfulCont.Handle(cadvisorMetricsPath,
|
||||
promhttp.HandlerFor(r, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}),
|
||||
)
|
||||
|
||||
ws = new(restful.WebService)
|
||||
ws.
|
||||
Path(specPath).
|
||||
Produces(restful.MIME_JSON)
|
||||
ws.Route(ws.GET("").
|
||||
To(s.getSpec).
|
||||
Operation("getSpec").
|
||||
Writes(cadvisorapi.MachineInfo{}))
|
||||
s.restfulCont.Add(ws)
|
||||
}
|
||||
|
||||
const pprofBasePath = "/debug/pprof/"
|
||||
|
||||
// InstallDebuggingHandlers registers the HTTP request patterns that serve logs or run commands/containers
|
||||
func (s *Server) InstallDebuggingHandlers(criHandler http.Handler) {
|
||||
glog.Infof("Adding debug handlers to kubelet server.")
|
||||
|
||||
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")
|
||||
ws.Route(ws.GET("/{podNamespace}/{podID}/{containerName}").
|
||||
To(s.getExec).
|
||||
Operation("getExec"))
|
||||
ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
|
||||
To(s.getExec).
|
||||
Operation("getExec"))
|
||||
ws.Route(ws.GET("/{podNamespace}/{podID}/{uid}/{containerName}").
|
||||
To(s.getExec).
|
||||
Operation("getExec"))
|
||||
ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}/{containerName}").
|
||||
To(s.getExec).
|
||||
Operation("getExec"))
|
||||
s.restfulCont.Add(ws)
|
||||
|
||||
ws = new(restful.WebService)
|
||||
ws.
|
||||
Path("/attach")
|
||||
ws.Route(ws.GET("/{podNamespace}/{podID}/{containerName}").
|
||||
To(s.getAttach).
|
||||
Operation("getAttach"))
|
||||
ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
|
||||
To(s.getAttach).
|
||||
Operation("getAttach"))
|
||||
ws.Route(ws.GET("/{podNamespace}/{podID}/{uid}/{containerName}").
|
||||
To(s.getAttach).
|
||||
Operation("getAttach"))
|
||||
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.GET("/{podNamespace}/{podID}").
|
||||
To(s.getPortForward).
|
||||
Operation("getPortForward"))
|
||||
ws.Route(ws.POST("/{podNamespace}/{podID}").
|
||||
To(s.getPortForward).
|
||||
Operation("getPortForward"))
|
||||
ws.Route(ws.GET("/{podNamespace}/{podID}/{uid}").
|
||||
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(logsPath)
|
||||
ws.Route(ws.GET("").
|
||||
To(s.getLogs).
|
||||
Operation("getLogs"))
|
||||
ws.Route(ws.GET("/{logpath:*}").
|
||||
To(s.getLogs).
|
||||
Operation("getLogs").
|
||||
Param(ws.PathParameter("logpath", "path to the log").DataType("string")))
|
||||
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)
|
||||
|
||||
configz.InstallHandler(s.restfulCont)
|
||||
|
||||
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)
|
||||
case "trace":
|
||||
pprof.Trace(resp, req.Request)
|
||||
default:
|
||||
pprof.Index(resp, req.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup pprof 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)
|
||||
|
||||
// The /runningpods endpoint is used for testing only.
|
||||
ws = new(restful.WebService)
|
||||
ws.
|
||||
Path("/runningpods/").
|
||||
Produces(restful.MIME_JSON)
|
||||
ws.Route(ws.GET("").
|
||||
To(s.getRunningPods).
|
||||
Operation("getRunningPods"))
|
||||
s.restfulCont.Add(ws)
|
||||
|
||||
if criHandler != nil {
|
||||
s.restfulCont.Handle("/cri/", criHandler)
|
||||
}
|
||||
}
|
||||
|
||||
// InstallDebuggingDisabledHandlers registers the HTTP request patterns that provide better error message
|
||||
func (s *Server) InstallDebuggingDisabledHandlers() {
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Debug endpoints are disabled.", http.StatusMethodNotAllowed)
|
||||
})
|
||||
|
||||
paths := []string{
|
||||
"/run/", "/exec/", "/attach/", "/portForward/", "/containerLogs/",
|
||||
"/runningpods/", pprofBasePath, logsPath}
|
||||
for _, p := range paths {
|
||||
s.restfulCont.Handle(p, h)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
if len(podID) == 0 {
|
||||
// TODO: Why return JSON when the rest return plaintext errors?
|
||||
// TODO: Why return plaintext errors?
|
||||
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Missing podID."}`))
|
||||
return
|
||||
}
|
||||
if len(containerName) == 0 {
|
||||
// TODO: Why return JSON when the rest return plaintext errors?
|
||||
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Missing container name."}`))
|
||||
return
|
||||
}
|
||||
if len(podNamespace) == 0 {
|
||||
// TODO: Why return JSON when the rest return plaintext errors?
|
||||
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Missing podNamespace."}`))
|
||||
return
|
||||
}
|
||||
|
||||
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 the v1 API version of PodLogOptions
|
||||
logOptions := &v1.PodLogOptions{}
|
||||
if err := legacyscheme.ParameterCodec.DecodeParameters(query, v1.SchemeGroupVersion, logOptions); err != nil {
|
||||
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Unable to decode query."}`))
|
||||
return
|
||||
}
|
||||
logOptions.TypeMeta = metav1.TypeMeta{}
|
||||
if errs := validation.ValidatePodLogOptions(logOptions); len(errs) > 0 {
|
||||
response.WriteError(http.StatusUnprocessableEntity, fmt.Errorf(`{"message": "Invalid request."}`))
|
||||
return
|
||||
}
|
||||
|
||||
pod, ok := s.host.GetPodByName(podNamespace, podID)
|
||||
if !ok {
|
||||
response.WriteError(http.StatusNotFound, fmt.Errorf("pod %q does not exist\n", podID))
|
||||
return
|
||||
}
|
||||
// Check if containerName is valid.
|
||||
containerExists := false
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if container.Name == containerName {
|
||||
containerExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !containerExists {
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
if container.Name == containerName {
|
||||
containerExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !containerExists {
|
||||
response.WriteError(http.StatusNotFound, fmt.Errorf("container %q not found in pod %q\n", containerName, podID))
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := response.ResponseWriter.(http.Flusher); !ok {
|
||||
response.WriteError(http.StatusInternalServerError, fmt.Errorf("unable to convert %v into http.Flusher, cannot show logs\n", reflect.TypeOf(response)))
|
||||
return
|
||||
}
|
||||
fw := flushwriter.Wrap(response.ResponseWriter)
|
||||
// Byte limit logic is already implemented in kuberuntime. However, we still need this for
|
||||
// old runtime integration.
|
||||
// TODO(random-liu): Remove this once we switch to CRI integration.
|
||||
if logOptions.LimitBytes != nil {
|
||||
fw = limitwriter.New(fw, *logOptions.LimitBytes)
|
||||
}
|
||||
response.Header().Set("Transfer-Encoding", "chunked")
|
||||
if err := s.host.GetKubeletContainerLogs(kubecontainer.GetPodFullName(pod), containerName, logOptions, fw, fw); err != nil {
|
||||
if err != limitwriter.ErrMaximumWrite {
|
||||
response.WriteError(http.StatusBadRequest, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// encodePods creates an v1.PodList object from pods and returns the encoded
|
||||
// PodList.
|
||||
func encodePods(pods []*v1.Pod) (data []byte, err error) {
|
||||
podList := new(v1.PodList)
|
||||
for _, pod := range pods {
|
||||
podList.Items = append(podList.Items, *pod)
|
||||
}
|
||||
// TODO: this needs to be parameterized to the kubelet, not hardcoded. Depends on Kubelet
|
||||
// as API server refactor.
|
||||
// TODO: Locked to v1, needs to be made generic
|
||||
codec := legacyscheme.Codecs.LegacyCodec(schema.GroupVersion{Group: v1.GroupName, Version: "v1"})
|
||||
return runtime.Encode(codec, podList)
|
||||
}
|
||||
|
||||
// getPods returns a list of pods bound to the Kubelet and their spec.
|
||||
func (s *Server) getPods(request *restful.Request, response *restful.Response) {
|
||||
pods := s.host.GetPods()
|
||||
data, err := encodePods(pods)
|
||||
if err != nil {
|
||||
response.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
writeJsonResponse(response, data)
|
||||
}
|
||||
|
||||
// getRunningPods returns a list of pods running on Kubelet. The list is
|
||||
// provided by the container runtime, and is different from the list returned
|
||||
// by getPods, which is a set of desired pods to run.
|
||||
func (s *Server) getRunningPods(request *restful.Request, response *restful.Response) {
|
||||
pods, err := s.host.GetRunningPods()
|
||||
if err != nil {
|
||||
response.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
data, err := encodePods(pods)
|
||||
if err != nil {
|
||||
response.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
writeJsonResponse(response, data)
|
||||
}
|
||||
|
||||
// getLogs handles logs requests against the Kubelet.
|
||||
func (s *Server) getLogs(request *restful.Request, response *restful.Response) {
|
||||
s.host.ServeLogs(response, request.Request)
|
||||
}
|
||||
|
||||
// getSpec handles spec requests against the Kubelet.
|
||||
func (s *Server) getSpec(request *restful.Request, response *restful.Response) {
|
||||
info, err := s.host.GetCachedMachineInfo()
|
||||
if err != nil {
|
||||
response.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
response.WriteEntity(info)
|
||||
}
|
||||
|
||||
type execRequestParams struct {
|
||||
podNamespace string
|
||||
podName string
|
||||
podUID types.UID
|
||||
containerName string
|
||||
cmd []string
|
||||
}
|
||||
|
||||
func getExecRequestParams(req *restful.Request) execRequestParams {
|
||||
return execRequestParams{
|
||||
podNamespace: req.PathParameter("podNamespace"),
|
||||
podName: req.PathParameter("podID"),
|
||||
podUID: types.UID(req.PathParameter("uid")),
|
||||
containerName: req.PathParameter("containerName"),
|
||||
cmd: req.Request.URL.Query()[api.ExecCommandParam],
|
||||
}
|
||||
}
|
||||
|
||||
type portForwardRequestParams struct {
|
||||
podNamespace string
|
||||
podName string
|
||||
podUID types.UID
|
||||
}
|
||||
|
||||
func getPortForwardRequestParams(req *restful.Request) portForwardRequestParams {
|
||||
return portForwardRequestParams{
|
||||
podNamespace: req.PathParameter("podNamespace"),
|
||||
podName: req.PathParameter("podID"),
|
||||
podUID: types.UID(req.PathParameter("uid")),
|
||||
}
|
||||
}
|
||||
|
||||
// getAttach handles requests to attach to a container.
|
||||
func (s *Server) getAttach(request *restful.Request, response *restful.Response) {
|
||||
params := getExecRequestParams(request)
|
||||
streamOpts, err := remotecommandserver.NewOptions(request.Request)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
response.WriteError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
pod, ok := s.host.GetPodByName(params.podNamespace, params.podName)
|
||||
if !ok {
|
||||
response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
podFullName := kubecontainer.GetPodFullName(pod)
|
||||
redirect, err := s.host.GetAttach(podFullName, params.podUID, params.containerName, *streamOpts)
|
||||
if err != nil {
|
||||
streaming.WriteError(err, response.ResponseWriter)
|
||||
return
|
||||
}
|
||||
if redirect != nil {
|
||||
http.Redirect(response.ResponseWriter, request.Request, redirect.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
remotecommandserver.ServeAttach(response.ResponseWriter,
|
||||
request.Request,
|
||||
s.host,
|
||||
podFullName,
|
||||
params.podUID,
|
||||
params.containerName,
|
||||
streamOpts,
|
||||
s.host.StreamingConnectionIdleTimeout(),
|
||||
remotecommandconsts.DefaultStreamCreationTimeout,
|
||||
remotecommandconsts.SupportedStreamingProtocols)
|
||||
}
|
||||
|
||||
// getExec handles requests to run a command inside a container.
|
||||
func (s *Server) getExec(request *restful.Request, response *restful.Response) {
|
||||
params := getExecRequestParams(request)
|
||||
streamOpts, err := remotecommandserver.NewOptions(request.Request)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
response.WriteError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
pod, ok := s.host.GetPodByName(params.podNamespace, params.podName)
|
||||
if !ok {
|
||||
response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
podFullName := kubecontainer.GetPodFullName(pod)
|
||||
redirect, err := s.host.GetExec(podFullName, params.podUID, params.containerName, params.cmd, *streamOpts)
|
||||
if err != nil {
|
||||
streaming.WriteError(err, response.ResponseWriter)
|
||||
return
|
||||
}
|
||||
if redirect != nil {
|
||||
http.Redirect(response.ResponseWriter, request.Request, redirect.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
remotecommandserver.ServeExec(response.ResponseWriter,
|
||||
request.Request,
|
||||
s.host,
|
||||
podFullName,
|
||||
params.podUID,
|
||||
params.containerName,
|
||||
params.cmd,
|
||||
streamOpts,
|
||||
s.host.StreamingConnectionIdleTimeout(),
|
||||
remotecommandconsts.DefaultStreamCreationTimeout,
|
||||
remotecommandconsts.SupportedStreamingProtocols)
|
||||
}
|
||||
|
||||
// getRun handles requests to run a command inside a container.
|
||||
func (s *Server) getRun(request *restful.Request, response *restful.Response) {
|
||||
params := getExecRequestParams(request)
|
||||
pod, ok := s.host.GetPodByName(params.podNamespace, params.podName)
|
||||
if !ok {
|
||||
response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
// For legacy reasons, run uses different query param than exec.
|
||||
params.cmd = strings.Split(request.QueryParameter("cmd"), " ")
|
||||
data, err := s.host.RunInContainer(kubecontainer.GetPodFullName(pod), params.podUID, params.containerName, params.cmd)
|
||||
if err != nil {
|
||||
response.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
writeJsonResponse(response, data)
|
||||
}
|
||||
|
||||
// Derived from go-restful writeJSON.
|
||||
func writeJsonResponse(response *restful.Response, data []byte) {
|
||||
if data == nil {
|
||||
response.WriteHeader(http.StatusOK)
|
||||
// do not write a nil representation
|
||||
return
|
||||
}
|
||||
response.Header().Set(restful.HEADER_ContentType, restful.MIME_JSON)
|
||||
response.WriteHeader(http.StatusOK)
|
||||
if _, err := response.Write(data); err != nil {
|
||||
glog.Errorf("Error writing response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// getPortForward handles a new restful port forward request. It determines the
|
||||
// pod name and uid and then calls ServePortForward.
|
||||
func (s *Server) getPortForward(request *restful.Request, response *restful.Response) {
|
||||
params := getPortForwardRequestParams(request)
|
||||
|
||||
portForwardOptions, err := portforward.NewV4Options(request.Request)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
response.WriteError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
pod, ok := s.host.GetPodByName(params.podNamespace, params.podName)
|
||||
if !ok {
|
||||
response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
|
||||
return
|
||||
}
|
||||
if len(params.podUID) > 0 && pod.UID != params.podUID {
|
||||
response.WriteError(http.StatusNotFound, fmt.Errorf("pod not found"))
|
||||
return
|
||||
}
|
||||
|
||||
redirect, err := s.host.GetPortForward(pod.Name, pod.Namespace, pod.UID, *portForwardOptions)
|
||||
if err != nil {
|
||||
streaming.WriteError(err, response.ResponseWriter)
|
||||
return
|
||||
}
|
||||
if redirect != nil {
|
||||
http.Redirect(response.ResponseWriter, request.Request, redirect.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
portforward.ServePortForward(response.ResponseWriter,
|
||||
request.Request,
|
||||
s.host,
|
||||
kubecontainer.GetPodFullName(pod),
|
||||
params.podUID,
|
||||
portForwardOptions,
|
||||
s.host.StreamingConnectionIdleTimeout(),
|
||||
remotecommandconsts.DefaultStreamCreationTimeout,
|
||||
portforward.SupportedProtocols)
|
||||
}
|
||||
|
||||
// ServeHTTP responds to HTTP requests on the Kubelet.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
defer httplog.NewLogged(req, &w).StacktraceWhen(
|
||||
httplog.StatusIsNot(
|
||||
http.StatusOK,
|
||||
http.StatusFound,
|
||||
http.StatusMovedPermanently,
|
||||
http.StatusTemporaryRedirect,
|
||||
http.StatusBadRequest,
|
||||
http.StatusNotFound,
|
||||
http.StatusSwitchingProtocols,
|
||||
),
|
||||
).Log()
|
||||
s.restfulCont.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// prometheusHostAdapter adapts the HostInterface to the interface expected by the
|
||||
// cAdvisor prometheus collector.
|
||||
type prometheusHostAdapter struct {
|
||||
host HostInterface
|
||||
}
|
||||
|
||||
func (a prometheusHostAdapter) SubcontainersInfo(containerName string, query *cadvisorapi.ContainerInfoRequest) ([]*cadvisorapi.ContainerInfo, error) {
|
||||
all, err := a.host.GetRawContainerInfo(containerName, query, true)
|
||||
items := make([]*cadvisorapi.ContainerInfo, 0, len(all))
|
||||
for _, v := range all {
|
||||
items = append(items, v)
|
||||
}
|
||||
return items, err
|
||||
}
|
||||
func (a prometheusHostAdapter) GetVersionInfo() (*cadvisorapi.VersionInfo, error) {
|
||||
return a.host.GetVersionInfo()
|
||||
}
|
||||
func (a prometheusHostAdapter) GetMachineInfo() (*cadvisorapi.MachineInfo, error) {
|
||||
return a.host.GetCachedMachineInfo()
|
||||
}
|
||||
|
||||
// containerPrometheusLabels maps cAdvisor labels to prometheus labels.
|
||||
func containerPrometheusLabels(c *cadvisorapi.ContainerInfo) map[string]string {
|
||||
// Prometheus requires that all metrics in the same family have the same labels,
|
||||
// so we arrange to supply blank strings for missing labels
|
||||
var name, image, podName, namespace, containerName string
|
||||
if len(c.Aliases) > 0 {
|
||||
name = c.Aliases[0]
|
||||
}
|
||||
image = c.Spec.Image
|
||||
if v, ok := c.Spec.Labels[kubelettypes.KubernetesPodNameLabel]; ok {
|
||||
podName = v
|
||||
}
|
||||
if v, ok := c.Spec.Labels[kubelettypes.KubernetesPodNamespaceLabel]; ok {
|
||||
namespace = v
|
||||
}
|
||||
if v, ok := c.Spec.Labels[kubelettypes.KubernetesContainerNameLabel]; ok {
|
||||
containerName = v
|
||||
}
|
||||
set := map[string]string{
|
||||
metrics.LabelID: c.Name,
|
||||
metrics.LabelName: name,
|
||||
metrics.LabelImage: image,
|
||||
"pod_name": podName,
|
||||
"namespace": namespace,
|
||||
"container_name": containerName,
|
||||
}
|
||||
return set
|
||||
}
|
1697
vendor/k8s.io/kubernetes/pkg/kubelet/server/server_test.go
generated
vendored
Normal file
1697
vendor/k8s.io/kubernetes/pkg/kubelet/server/server_test.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
331
vendor/k8s.io/kubernetes/pkg/kubelet/server/server_websocket_test.go
generated
vendored
Normal file
331
vendor/k8s.io/kubernetes/pkg/kubelet/server/server_websocket_test.go
generated
vendored
Normal file
@ -0,0 +1,331 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
const (
|
||||
dataChannel = iota
|
||||
errorChannel
|
||||
)
|
||||
|
||||
func TestServeWSPortForward(t *testing.T) {
|
||||
tests := []struct {
|
||||
port string
|
||||
uid bool
|
||||
clientData string
|
||||
containerData string
|
||||
shouldError bool
|
||||
}{
|
||||
{port: "", shouldError: true},
|
||||
{port: "abc", shouldError: true},
|
||||
{port: "-1", shouldError: true},
|
||||
{port: "65536", shouldError: true},
|
||||
{port: "0", shouldError: true},
|
||||
{port: "1", shouldError: false},
|
||||
{port: "8000", shouldError: false},
|
||||
{port: "8000", clientData: "client data", containerData: "container data", shouldError: false},
|
||||
{port: "65535", shouldError: false},
|
||||
{port: "65535", uid: true, shouldError: false},
|
||||
}
|
||||
|
||||
podNamespace := "other"
|
||||
podName := "foo"
|
||||
expectedPodName := getPodName(podName, podNamespace)
|
||||
expectedUid := "9b01b80f-8fb4-11e4-95ab-4200af06647"
|
||||
|
||||
for i, test := range tests {
|
||||
fw := newServerTest()
|
||||
defer fw.testHTTPServer.Close()
|
||||
|
||||
fw.fakeKubelet.streamingConnectionIdleTimeoutFunc = func() time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
||||
portForwardFuncDone := make(chan struct{})
|
||||
|
||||
fw.fakeKubelet.portForwardFunc = func(name string, uid types.UID, port int32, stream io.ReadWriteCloser) error {
|
||||
defer close(portForwardFuncDone)
|
||||
|
||||
if e, a := expectedPodName, name; e != a {
|
||||
t.Fatalf("%d: pod name: expected '%v', got '%v'", i, e, a)
|
||||
}
|
||||
|
||||
if e, a := expectedUid, uid; test.uid && e != string(a) {
|
||||
t.Fatalf("%d: uid: expected '%v', got '%v'", i, e, a)
|
||||
}
|
||||
|
||||
p, err := strconv.ParseInt(test.port, 10, 32)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: error parsing port string '%s': %v", i, test.port, err)
|
||||
}
|
||||
if e, a := int32(p), port; e != a {
|
||||
t.Fatalf("%d: port: expected '%v', got '%v'", i, e, a)
|
||||
}
|
||||
|
||||
if test.clientData != "" {
|
||||
fromClient := make([]byte, 32)
|
||||
n, err := stream.Read(fromClient)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: error reading client data: %v", i, err)
|
||||
}
|
||||
if e, a := test.clientData, string(fromClient[0:n]); e != a {
|
||||
t.Fatalf("%d: client data: expected to receive '%v', got '%v'", i, e, a)
|
||||
}
|
||||
}
|
||||
|
||||
if test.containerData != "" {
|
||||
_, err := stream.Write([]byte(test.containerData))
|
||||
if err != nil {
|
||||
t.Fatalf("%d: error writing container data: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var url string
|
||||
if test.uid {
|
||||
url = fmt.Sprintf("ws://%s/portForward/%s/%s/%s?port=%s", fw.testHTTPServer.Listener.Addr().String(), podNamespace, podName, expectedUid, test.port)
|
||||
} else {
|
||||
url = fmt.Sprintf("ws://%s/portForward/%s/%s?port=%s", fw.testHTTPServer.Listener.Addr().String(), podNamespace, podName, test.port)
|
||||
}
|
||||
|
||||
ws, err := websocket.Dial(url, "", "http://127.0.0.1/")
|
||||
if test.shouldError {
|
||||
if err == nil {
|
||||
t.Fatalf("%d: websocket dial expected err", i)
|
||||
}
|
||||
continue
|
||||
} else if err != nil {
|
||||
t.Fatalf("%d: websocket dial unexpected err: %v", i, err)
|
||||
}
|
||||
|
||||
defer ws.Close()
|
||||
|
||||
p, err := strconv.ParseUint(test.port, 10, 16)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: error parsing port string '%s': %v", i, test.port, err)
|
||||
}
|
||||
p16 := uint16(p)
|
||||
|
||||
channel, data, err := wsRead(ws)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: read failed: expected no error: got %v", i, err)
|
||||
}
|
||||
if channel != dataChannel {
|
||||
t.Fatalf("%d: wrong channel: got %q: expected %q", i, channel, dataChannel)
|
||||
}
|
||||
if len(data) != binary.Size(p16) {
|
||||
t.Fatalf("%d: wrong data size: got %q: expected %d", i, data, binary.Size(p16))
|
||||
}
|
||||
if e, a := p16, binary.LittleEndian.Uint16(data); e != a {
|
||||
t.Fatalf("%d: wrong data: got %q: expected %s", i, data, test.port)
|
||||
}
|
||||
|
||||
channel, data, err = wsRead(ws)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: read succeeded: expected no error: got %v", i, err)
|
||||
}
|
||||
if channel != errorChannel {
|
||||
t.Fatalf("%d: wrong channel: got %q: expected %q", i, channel, errorChannel)
|
||||
}
|
||||
if len(data) != binary.Size(p16) {
|
||||
t.Fatalf("%d: wrong data size: got %q: expected %d", i, data, binary.Size(p16))
|
||||
}
|
||||
if e, a := p16, binary.LittleEndian.Uint16(data); e != a {
|
||||
t.Fatalf("%d: wrong data: got %q: expected %s", i, data, test.port)
|
||||
}
|
||||
|
||||
if test.clientData != "" {
|
||||
println("writing the client data")
|
||||
err := wsWrite(ws, dataChannel, []byte(test.clientData))
|
||||
if err != nil {
|
||||
t.Fatalf("%d: unexpected error writing client data: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
if test.containerData != "" {
|
||||
_, data, err = wsRead(ws)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: unexpected error reading container data: %v", i, err)
|
||||
}
|
||||
if e, a := test.containerData, string(data); e != a {
|
||||
t.Fatalf("%d: expected to receive '%v' from container, got '%v'", i, e, a)
|
||||
}
|
||||
}
|
||||
|
||||
<-portForwardFuncDone
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeWSMultiplePortForward(t *testing.T) {
|
||||
portsText := []string{"7000,8000", "9000"}
|
||||
ports := []uint16{7000, 8000, 9000}
|
||||
podNamespace := "other"
|
||||
podName := "foo"
|
||||
expectedPodName := getPodName(podName, podNamespace)
|
||||
|
||||
fw := newServerTest()
|
||||
defer fw.testHTTPServer.Close()
|
||||
|
||||
fw.fakeKubelet.streamingConnectionIdleTimeoutFunc = func() time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
||||
portForwardWG := sync.WaitGroup{}
|
||||
portForwardWG.Add(len(ports))
|
||||
|
||||
portsMutex := sync.Mutex{}
|
||||
portsForwarded := map[int32]struct{}{}
|
||||
|
||||
fw.fakeKubelet.portForwardFunc = func(name string, uid types.UID, port int32, stream io.ReadWriteCloser) error {
|
||||
defer portForwardWG.Done()
|
||||
|
||||
if e, a := expectedPodName, name; e != a {
|
||||
t.Fatalf("%d: pod name: expected '%v', got '%v'", port, e, a)
|
||||
}
|
||||
|
||||
portsMutex.Lock()
|
||||
portsForwarded[port] = struct{}{}
|
||||
portsMutex.Unlock()
|
||||
|
||||
fromClient := make([]byte, 32)
|
||||
n, err := stream.Read(fromClient)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: error reading client data: %v", port, err)
|
||||
}
|
||||
if e, a := fmt.Sprintf("client data on port %d", port), string(fromClient[0:n]); e != a {
|
||||
t.Fatalf("%d: client data: expected to receive '%v', got '%v'", port, e, a)
|
||||
}
|
||||
|
||||
_, err = stream.Write([]byte(fmt.Sprintf("container data on port %d", port)))
|
||||
if err != nil {
|
||||
t.Fatalf("%d: error writing container data: %v", port, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("ws://%s/portForward/%s/%s?", fw.testHTTPServer.Listener.Addr().String(), podNamespace, podName)
|
||||
for _, port := range portsText {
|
||||
url = url + fmt.Sprintf("port=%s&", port)
|
||||
}
|
||||
|
||||
ws, err := websocket.Dial(url, "", "http://127.0.0.1/")
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial unexpected err: %v", err)
|
||||
}
|
||||
|
||||
defer ws.Close()
|
||||
|
||||
for i, port := range ports {
|
||||
channel, data, err := wsRead(ws)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: read failed: expected no error: got %v", i, err)
|
||||
}
|
||||
if int(channel) != i*2+dataChannel {
|
||||
t.Fatalf("%d: wrong channel: got %q: expected %q", i, channel, i*2+dataChannel)
|
||||
}
|
||||
if len(data) != binary.Size(port) {
|
||||
t.Fatalf("%d: wrong data size: got %q: expected %d", i, data, binary.Size(port))
|
||||
}
|
||||
if e, a := port, binary.LittleEndian.Uint16(data); e != a {
|
||||
t.Fatalf("%d: wrong data: got %q: expected %d", i, data, port)
|
||||
}
|
||||
|
||||
channel, data, err = wsRead(ws)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: read succeeded: expected no error: got %v", i, err)
|
||||
}
|
||||
if int(channel) != i*2+errorChannel {
|
||||
t.Fatalf("%d: wrong channel: got %q: expected %q", i, channel, i*2+errorChannel)
|
||||
}
|
||||
if len(data) != binary.Size(port) {
|
||||
t.Fatalf("%d: wrong data size: got %q: expected %d", i, data, binary.Size(port))
|
||||
}
|
||||
if e, a := port, binary.LittleEndian.Uint16(data); e != a {
|
||||
t.Fatalf("%d: wrong data: got %q: expected %d", i, data, port)
|
||||
}
|
||||
}
|
||||
|
||||
for i, port := range ports {
|
||||
println("writing the client data", port)
|
||||
err := wsWrite(ws, byte(i*2+dataChannel), []byte(fmt.Sprintf("client data on port %d", port)))
|
||||
if err != nil {
|
||||
t.Fatalf("%d: unexpected error writing client data: %v", i, err)
|
||||
}
|
||||
|
||||
channel, data, err := wsRead(ws)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: unexpected error reading container data: %v", i, err)
|
||||
}
|
||||
|
||||
if int(channel) != i*2+dataChannel {
|
||||
t.Fatalf("%d: wrong channel: got %q: expected %q", port, channel, i*2+dataChannel)
|
||||
}
|
||||
if e, a := fmt.Sprintf("container data on port %d", port), string(data); e != a {
|
||||
t.Fatalf("%d: expected to receive '%v' from container, got '%v'", i, e, a)
|
||||
}
|
||||
}
|
||||
|
||||
portForwardWG.Wait()
|
||||
|
||||
portsMutex.Lock()
|
||||
defer portsMutex.Unlock()
|
||||
if len(ports) != len(portsForwarded) {
|
||||
t.Fatalf("expected to forward %d ports; got %v", len(ports), portsForwarded)
|
||||
}
|
||||
}
|
||||
func wsWrite(conn *websocket.Conn, channel byte, data []byte) error {
|
||||
frame := make([]byte, len(data)+1)
|
||||
frame[0] = channel
|
||||
copy(frame[1:], data)
|
||||
err := websocket.Message.Send(conn, frame)
|
||||
return err
|
||||
}
|
||||
|
||||
func wsRead(conn *websocket.Conn) (byte, []byte, error) {
|
||||
for {
|
||||
var data []byte
|
||||
err := websocket.Message.Receive(conn, &data)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
channel := data[0]
|
||||
data = data[1:]
|
||||
|
||||
return channel, data, err
|
||||
}
|
||||
}
|
68
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/BUILD
generated
vendored
Normal file
68
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/BUILD
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"fs_resource_analyzer.go",
|
||||
"handler.go",
|
||||
"resource_analyzer.go",
|
||||
"summary.go",
|
||||
"volume_stat_calculator.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server/stats",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/kubelet/apis/stats/v1alpha1:go_default_library",
|
||||
"//pkg/kubelet/cm:go_default_library",
|
||||
"//pkg/kubelet/container:go_default_library",
|
||||
"//pkg/kubelet/metrics:go_default_library",
|
||||
"//pkg/kubelet/util/format:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//vendor/github.com/emicklei/go-restful:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/github.com/google/cadvisor/info/v1:go_default_library",
|
||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"summary_test.go",
|
||||
"volume_stat_calculator_test.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server/stats",
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//pkg/kubelet/apis/stats/v1alpha1:go_default_library",
|
||||
"//pkg/kubelet/cm:go_default_library",
|
||||
"//pkg/kubelet/server/stats/testing:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//vendor/github.com/google/gofuzz:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//pkg/kubelet/server/stats/testing:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
20
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/doc.go
generated
vendored
Normal file
20
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/doc.go
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package stats handles exporting Kubelet and container stats.
|
||||
// NOTE: We intend to move this functionality into a standalone pod, so this package should be very
|
||||
// loosely coupled to the rest of the Kubelet.
|
||||
package stats // import "k8s.io/kubernetes/pkg/kubelet/server/stats"
|
107
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/fs_resource_analyzer.go
generated
vendored
Normal file
107
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/fs_resource_analyzer.go
generated
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Map to PodVolumeStats pointers since the addresses for map values are not constant and can cause pain
|
||||
// if we need ever to get a pointer to one of the values (e.g. you can't)
|
||||
type Cache map[types.UID]*volumeStatCalculator
|
||||
|
||||
// fsResourceAnalyzerInterface is for embedding fs functions into ResourceAnalyzer
|
||||
type fsResourceAnalyzerInterface interface {
|
||||
GetPodVolumeStats(uid types.UID) (PodVolumeStats, bool)
|
||||
}
|
||||
|
||||
// fsResourceAnalyzer provides stats about fs resource usage
|
||||
type fsResourceAnalyzer struct {
|
||||
statsProvider StatsProvider
|
||||
calcPeriod time.Duration
|
||||
cachedVolumeStats atomic.Value
|
||||
startOnce sync.Once
|
||||
}
|
||||
|
||||
var _ fsResourceAnalyzerInterface = &fsResourceAnalyzer{}
|
||||
|
||||
// newFsResourceAnalyzer returns a new fsResourceAnalyzer implementation
|
||||
func newFsResourceAnalyzer(statsProvider StatsProvider, calcVolumePeriod time.Duration) *fsResourceAnalyzer {
|
||||
r := &fsResourceAnalyzer{
|
||||
statsProvider: statsProvider,
|
||||
calcPeriod: calcVolumePeriod,
|
||||
}
|
||||
r.cachedVolumeStats.Store(make(Cache))
|
||||
return r
|
||||
}
|
||||
|
||||
// Start eager background caching of volume stats.
|
||||
func (s *fsResourceAnalyzer) Start() {
|
||||
s.startOnce.Do(func() {
|
||||
if s.calcPeriod <= 0 {
|
||||
glog.Info("Volume stats collection disabled.")
|
||||
return
|
||||
}
|
||||
glog.Info("Starting FS ResourceAnalyzer")
|
||||
go wait.Forever(func() { s.updateCachedPodVolumeStats() }, s.calcPeriod)
|
||||
})
|
||||
}
|
||||
|
||||
// updateCachedPodVolumeStats calculates and caches the PodVolumeStats for every Pod known to the kubelet.
|
||||
func (s *fsResourceAnalyzer) updateCachedPodVolumeStats() {
|
||||
oldCache := s.cachedVolumeStats.Load().(Cache)
|
||||
newCache := make(Cache)
|
||||
|
||||
// Copy existing entries to new map, creating/starting new entries for pods missing from the cache
|
||||
for _, pod := range s.statsProvider.GetPods() {
|
||||
if value, found := oldCache[pod.GetUID()]; !found {
|
||||
newCache[pod.GetUID()] = newVolumeStatCalculator(s.statsProvider, s.calcPeriod, pod).StartOnce()
|
||||
} else {
|
||||
newCache[pod.GetUID()] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Stop entries for pods that have been deleted
|
||||
for uid, entry := range oldCache {
|
||||
if _, found := newCache[uid]; !found {
|
||||
entry.StopOnce()
|
||||
}
|
||||
}
|
||||
|
||||
// Update the cache reference
|
||||
s.cachedVolumeStats.Store(newCache)
|
||||
}
|
||||
|
||||
// GetPodVolumeStats returns the PodVolumeStats for a given pod. Results are looked up from a cache that
|
||||
// is eagerly populated in the background, and never calculated on the fly.
|
||||
func (s *fsResourceAnalyzer) GetPodVolumeStats(uid types.UID) (PodVolumeStats, bool) {
|
||||
cache := s.cachedVolumeStats.Load().(Cache)
|
||||
if statCalc, found := cache[uid]; !found {
|
||||
// TODO: Differentiate between stats being empty
|
||||
// See issue #20679
|
||||
return PodVolumeStats{}, false
|
||||
} else {
|
||||
return statCalc.GetLatest()
|
||||
}
|
||||
}
|
279
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/handler.go
generated
vendored
Normal file
279
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/handler.go
generated
vendored
Normal file
@ -0,0 +1,279 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"github.com/golang/glog"
|
||||
cadvisorapi "github.com/google/cadvisor/info/v1"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
statsapi "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
)
|
||||
|
||||
// Host methods required by stats handlers.
|
||||
type StatsProvider interface {
|
||||
// The following stats are provided by either CRI or cAdvisor.
|
||||
//
|
||||
// ListPodStats returns the stats of all the containers managed by pods.
|
||||
ListPodStats() ([]statsapi.PodStats, error)
|
||||
// ImageFsStats returns the stats of the image filesystem.
|
||||
ImageFsStats() (*statsapi.FsStats, error)
|
||||
|
||||
// The following stats are provided by cAdvisor.
|
||||
//
|
||||
// GetCgroupStats returns the stats and the networking usage of the cgroup
|
||||
// with the specified cgroupName.
|
||||
GetCgroupStats(cgroupName string) (*statsapi.ContainerStats, *statsapi.NetworkStats, error)
|
||||
// RootFsStats returns the stats of the node root filesystem.
|
||||
RootFsStats() (*statsapi.FsStats, error)
|
||||
|
||||
// The following stats are provided by cAdvisor for legacy usage.
|
||||
//
|
||||
// GetContainerInfo returns the information of the container with the
|
||||
// containerName managed by the pod with the uid.
|
||||
GetContainerInfo(podFullName string, uid types.UID, containerName string, req *cadvisorapi.ContainerInfoRequest) (*cadvisorapi.ContainerInfo, error)
|
||||
// GetRawContainerInfo returns the information of the container with the
|
||||
// containerName. If subcontainers is true, this function will return the
|
||||
// information of all the sub-containers as well.
|
||||
GetRawContainerInfo(containerName string, req *cadvisorapi.ContainerInfoRequest, subcontainers bool) (map[string]*cadvisorapi.ContainerInfo, error)
|
||||
|
||||
// The following information is provided by Kubelet.
|
||||
//
|
||||
// GetPodByName returns the spec of the pod with the name in the specified
|
||||
// namespace.
|
||||
GetPodByName(namespace, name string) (*v1.Pod, bool)
|
||||
// GetNode returns the spec of the local node.
|
||||
GetNode() (*v1.Node, error)
|
||||
// GetNodeConfig returns the configuration of the local node.
|
||||
GetNodeConfig() cm.NodeConfig
|
||||
// ListVolumesForPod returns the stats of the volume used by the pod with
|
||||
// the podUID.
|
||||
ListVolumesForPod(podUID types.UID) (map[string]volume.Volume, bool)
|
||||
// GetPods returns the specs of all the pods running on this node.
|
||||
GetPods() []*v1.Pod
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
provider StatsProvider
|
||||
summaryProvider SummaryProvider
|
||||
}
|
||||
|
||||
func CreateHandlers(rootPath string, provider StatsProvider, summaryProvider SummaryProvider) *restful.WebService {
|
||||
h := &handler{provider, summaryProvider}
|
||||
|
||||
ws := &restful.WebService{}
|
||||
ws.Path(rootPath).
|
||||
Produces(restful.MIME_JSON)
|
||||
|
||||
endpoints := []struct {
|
||||
path string
|
||||
handler restful.RouteFunction
|
||||
}{
|
||||
{"", h.handleStats},
|
||||
{"/summary", h.handleSummary},
|
||||
{"/container", h.handleSystemContainer},
|
||||
{"/{podName}/{containerName}", h.handlePodContainer},
|
||||
{"/{namespace}/{podName}/{uid}/{containerName}", h.handlePodContainer},
|
||||
}
|
||||
|
||||
for _, e := range endpoints {
|
||||
for _, method := range []string{"GET", "POST"} {
|
||||
ws.Route(ws.
|
||||
Method(method).
|
||||
Path(e.path).
|
||||
To(e.handler))
|
||||
}
|
||||
}
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
type StatsRequest struct {
|
||||
// The name of the container for which to request stats.
|
||||
// Default: /
|
||||
// +optional
|
||||
ContainerName string `json:"containerName,omitempty"`
|
||||
|
||||
// Max number of stats to return.
|
||||
// If start and end time are specified this limit is ignored.
|
||||
// Default: 60
|
||||
// +optional
|
||||
NumStats int `json:"num_stats,omitempty"`
|
||||
|
||||
// Start time for which to query information.
|
||||
// If omitted, the beginning of time is assumed.
|
||||
// +optional
|
||||
Start time.Time `json:"start,omitempty"`
|
||||
|
||||
// End time for which to query information.
|
||||
// If omitted, current time is assumed.
|
||||
// +optional
|
||||
End time.Time `json:"end,omitempty"`
|
||||
|
||||
// Whether to also include information from subcontainers.
|
||||
// Default: false.
|
||||
// +optional
|
||||
Subcontainers bool `json:"subcontainers,omitempty"`
|
||||
}
|
||||
|
||||
func (r *StatsRequest) cadvisorRequest() *cadvisorapi.ContainerInfoRequest {
|
||||
return &cadvisorapi.ContainerInfoRequest{
|
||||
NumStats: r.NumStats,
|
||||
Start: r.Start,
|
||||
End: r.End,
|
||||
}
|
||||
}
|
||||
|
||||
func parseStatsRequest(request *restful.Request) (StatsRequest, error) {
|
||||
// Default request.
|
||||
query := StatsRequest{
|
||||
NumStats: 60,
|
||||
}
|
||||
|
||||
err := json.NewDecoder(request.Request.Body).Decode(&query)
|
||||
if err != nil && err != io.EOF {
|
||||
return query, err
|
||||
}
|
||||
return query, nil
|
||||
}
|
||||
|
||||
// Handles root container stats requests to /stats
|
||||
func (h *handler) handleStats(request *restful.Request, response *restful.Response) {
|
||||
query, err := parseStatsRequest(request)
|
||||
if err != nil {
|
||||
handleError(response, "/stats", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Root container stats.
|
||||
statsMap, err := h.provider.GetRawContainerInfo("/", query.cadvisorRequest(), false)
|
||||
if err != nil {
|
||||
handleError(response, fmt.Sprintf("/stats %v", query), err)
|
||||
return
|
||||
}
|
||||
writeResponse(response, statsMap["/"])
|
||||
}
|
||||
|
||||
// Handles stats summary requests to /stats/summary
|
||||
func (h *handler) handleSummary(request *restful.Request, response *restful.Response) {
|
||||
summary, err := h.summaryProvider.Get()
|
||||
if err != nil {
|
||||
handleError(response, "/stats/summary", err)
|
||||
} else {
|
||||
writeResponse(response, summary)
|
||||
}
|
||||
}
|
||||
|
||||
// Handles non-kubernetes container stats requests to /stats/container/
|
||||
func (h *handler) handleSystemContainer(request *restful.Request, response *restful.Response) {
|
||||
query, err := parseStatsRequest(request)
|
||||
if err != nil {
|
||||
handleError(response, "/stats/container", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Non-Kubernetes container stats.
|
||||
containerName := path.Join("/", query.ContainerName)
|
||||
stats, err := h.provider.GetRawContainerInfo(
|
||||
containerName, query.cadvisorRequest(), query.Subcontainers)
|
||||
if err != nil {
|
||||
if _, ok := stats[containerName]; ok {
|
||||
// If the failure is partial, log it and return a best-effort response.
|
||||
glog.Errorf("Partial failure issuing GetRawContainerInfo(%v): %v", query, err)
|
||||
} else {
|
||||
handleError(response, fmt.Sprintf("/stats/container %v", query), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
writeResponse(response, stats)
|
||||
}
|
||||
|
||||
// Handles kubernetes pod/container stats requests to:
|
||||
// /stats/<pod name>/<container name>
|
||||
// /stats/<namespace>/<pod name>/<uid>/<container name>
|
||||
func (h *handler) handlePodContainer(request *restful.Request, response *restful.Response) {
|
||||
query, err := parseStatsRequest(request)
|
||||
if err != nil {
|
||||
handleError(response, request.Request.URL.String(), err)
|
||||
return
|
||||
}
|
||||
|
||||
// Default parameters.
|
||||
params := map[string]string{
|
||||
"namespace": metav1.NamespaceDefault,
|
||||
"uid": "",
|
||||
}
|
||||
for k, v := range request.PathParameters() {
|
||||
params[k] = v
|
||||
}
|
||||
|
||||
if params["podName"] == "" || params["containerName"] == "" {
|
||||
response.WriteErrorString(http.StatusBadRequest,
|
||||
fmt.Sprintf("Invalid pod container request: %v", params))
|
||||
return
|
||||
}
|
||||
|
||||
pod, ok := h.provider.GetPodByName(params["namespace"], params["podName"])
|
||||
if !ok {
|
||||
glog.V(4).Infof("Container not found: %v", params)
|
||||
response.WriteError(http.StatusNotFound, kubecontainer.ErrContainerNotFound)
|
||||
return
|
||||
}
|
||||
stats, err := h.provider.GetContainerInfo(
|
||||
kubecontainer.GetPodFullName(pod),
|
||||
types.UID(params["uid"]),
|
||||
params["containerName"],
|
||||
query.cadvisorRequest())
|
||||
|
||||
if err != nil {
|
||||
handleError(response, fmt.Sprintf("%s %v", request.Request.URL.String(), query), err)
|
||||
return
|
||||
}
|
||||
writeResponse(response, stats)
|
||||
}
|
||||
|
||||
func writeResponse(response *restful.Response, stats interface{}) {
|
||||
if err := response.WriteAsJson(stats); err != nil {
|
||||
glog.Errorf("Error writing response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleError serializes an error object into an HTTP response.
|
||||
// request is provided for logging.
|
||||
func handleError(response *restful.Response, request string, err error) {
|
||||
switch err {
|
||||
case kubecontainer.ErrContainerNotFound:
|
||||
response.WriteError(http.StatusNotFound, err)
|
||||
default:
|
||||
msg := fmt.Sprintf("Internal Error: %v", err)
|
||||
glog.Errorf("HTTP InternalServerError serving %s: %s", request, msg)
|
||||
response.WriteErrorString(http.StatusInternalServerError, msg)
|
||||
}
|
||||
}
|
49
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/resource_analyzer.go
generated
vendored
Normal file
49
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/resource_analyzer.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ResourceAnalyzer provides statistics on node resource consumption
|
||||
type ResourceAnalyzer interface {
|
||||
Start()
|
||||
|
||||
fsResourceAnalyzerInterface
|
||||
SummaryProvider
|
||||
}
|
||||
|
||||
// resourceAnalyzer implements ResourceAnalyzer
|
||||
type resourceAnalyzer struct {
|
||||
*fsResourceAnalyzer
|
||||
SummaryProvider
|
||||
}
|
||||
|
||||
var _ ResourceAnalyzer = &resourceAnalyzer{}
|
||||
|
||||
// NewResourceAnalyzer returns a new ResourceAnalyzer
|
||||
func NewResourceAnalyzer(statsProvider StatsProvider, calVolumeFrequency time.Duration) ResourceAnalyzer {
|
||||
fsAnalyzer := newFsResourceAnalyzer(statsProvider, calVolumeFrequency)
|
||||
summaryProvider := NewSummaryProvider(statsProvider)
|
||||
return &resourceAnalyzer{fsAnalyzer, summaryProvider}
|
||||
}
|
||||
|
||||
// Start starts background functions necessary for the ResourceAnalyzer to function
|
||||
func (ra *resourceAnalyzer) Start() {
|
||||
ra.fsResourceAnalyzer.Start()
|
||||
}
|
106
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/summary.go
generated
vendored
Normal file
106
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/summary.go
generated
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
statsapi "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
|
||||
)
|
||||
|
||||
type SummaryProvider interface {
|
||||
Get() (*statsapi.Summary, error)
|
||||
}
|
||||
|
||||
// summaryProviderImpl implements the SummaryProvider interface.
|
||||
type summaryProviderImpl struct {
|
||||
provider StatsProvider
|
||||
}
|
||||
|
||||
var _ SummaryProvider = &summaryProviderImpl{}
|
||||
|
||||
// NewSummaryProvider returns a SummaryProvider using the stats provided by the
|
||||
// specified statsProvider.
|
||||
func NewSummaryProvider(statsProvider StatsProvider) SummaryProvider {
|
||||
return &summaryProviderImpl{statsProvider}
|
||||
}
|
||||
|
||||
// Get provides a new Summary with the stats from Kubelet.
|
||||
func (sp *summaryProviderImpl) Get() (*statsapi.Summary, error) {
|
||||
// TODO(timstclair): Consider returning a best-effort response if any of
|
||||
// the following errors occur.
|
||||
node, err := sp.provider.GetNode()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get node info: %v", err)
|
||||
}
|
||||
nodeConfig := sp.provider.GetNodeConfig()
|
||||
rootStats, networkStats, err := sp.provider.GetCgroupStats("/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get root cgroup stats: %v", err)
|
||||
}
|
||||
rootFsStats, err := sp.provider.RootFsStats()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rootFs stats: %v", err)
|
||||
}
|
||||
imageFsStats, err := sp.provider.ImageFsStats()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get imageFs stats: %v", err)
|
||||
}
|
||||
podStats, err := sp.provider.ListPodStats()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list pod stats: %v", err)
|
||||
}
|
||||
|
||||
nodeStats := statsapi.NodeStats{
|
||||
NodeName: node.Name,
|
||||
CPU: rootStats.CPU,
|
||||
Memory: rootStats.Memory,
|
||||
Network: networkStats,
|
||||
StartTime: rootStats.StartTime,
|
||||
Fs: rootFsStats,
|
||||
Runtime: &statsapi.RuntimeStats{ImageFs: imageFsStats},
|
||||
}
|
||||
|
||||
systemContainers := map[string]string{
|
||||
statsapi.SystemContainerKubelet: nodeConfig.KubeletCgroupsName,
|
||||
statsapi.SystemContainerRuntime: nodeConfig.RuntimeCgroupsName,
|
||||
statsapi.SystemContainerMisc: nodeConfig.SystemCgroupsName,
|
||||
}
|
||||
for sys, name := range systemContainers {
|
||||
// skip if cgroup name is undefined (not all system containers are required)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
s, _, err := sp.provider.GetCgroupStats(name)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to get system container stats for %q: %v", name, err)
|
||||
continue
|
||||
}
|
||||
// System containers don't have a filesystem associated with them.
|
||||
s.Logs, s.Rootfs = nil, nil
|
||||
s.Name = sys
|
||||
nodeStats.SystemContainers = append(nodeStats.SystemContainers, *s)
|
||||
}
|
||||
|
||||
summary := statsapi.Summary{
|
||||
Node: nodeStats,
|
||||
Pods: podStats,
|
||||
}
|
||||
return &summary, nil
|
||||
}
|
143
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/summary_test.go
generated
vendored
Normal file
143
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/summary_test.go
generated
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
statsapi "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
statstest "k8s.io/kubernetes/pkg/kubelet/server/stats/testing"
|
||||
)
|
||||
|
||||
func TestSummaryProvider(t *testing.T) {
|
||||
var (
|
||||
podStats = []statsapi.PodStats{
|
||||
{
|
||||
PodRef: statsapi.PodReference{Name: "test-pod", Namespace: "test-namespace", UID: "UID_test-pod"},
|
||||
StartTime: metav1.NewTime(time.Now()),
|
||||
Containers: []statsapi.ContainerStats{*getContainerStats()},
|
||||
Network: getNetworkStats(),
|
||||
VolumeStats: []statsapi.VolumeStats{*getVolumeStats()},
|
||||
},
|
||||
}
|
||||
imageFsStats = getFsStats()
|
||||
rootFsStats = getFsStats()
|
||||
node = &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-node"}}
|
||||
nodeConfig = cm.NodeConfig{
|
||||
RuntimeCgroupsName: "/runtime",
|
||||
SystemCgroupsName: "/misc",
|
||||
KubeletCgroupsName: "/kubelet",
|
||||
}
|
||||
cgroupStatsMap = map[string]struct {
|
||||
cs *statsapi.ContainerStats
|
||||
ns *statsapi.NetworkStats
|
||||
}{
|
||||
"/": {cs: getContainerStats(), ns: getNetworkStats()},
|
||||
"/runtime": {cs: getContainerStats(), ns: getNetworkStats()},
|
||||
"/misc": {cs: getContainerStats(), ns: getNetworkStats()},
|
||||
"/kubelet": {cs: getContainerStats(), ns: getNetworkStats()},
|
||||
}
|
||||
)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
mockStatsProvider := new(statstest.StatsProvider)
|
||||
mockStatsProvider.
|
||||
On("GetNode").Return(node, nil).
|
||||
On("GetNodeConfig").Return(nodeConfig).
|
||||
On("ListPodStats").Return(podStats, nil).
|
||||
On("ImageFsStats").Return(imageFsStats, nil).
|
||||
On("RootFsStats").Return(rootFsStats, nil).
|
||||
On("GetCgroupStats", "/").Return(cgroupStatsMap["/"].cs, cgroupStatsMap["/"].ns, nil).
|
||||
On("GetCgroupStats", "/runtime").Return(cgroupStatsMap["/runtime"].cs, cgroupStatsMap["/runtime"].ns, nil).
|
||||
On("GetCgroupStats", "/misc").Return(cgroupStatsMap["/misc"].cs, cgroupStatsMap["/misc"].ns, nil).
|
||||
On("GetCgroupStats", "/kubelet").Return(cgroupStatsMap["/kubelet"].cs, cgroupStatsMap["/kubelet"].ns, nil)
|
||||
|
||||
provider := NewSummaryProvider(mockStatsProvider)
|
||||
summary, err := provider.Get()
|
||||
assert.NoError(err)
|
||||
|
||||
assert.Equal(summary.Node.NodeName, "test-node")
|
||||
assert.Equal(summary.Node.StartTime, cgroupStatsMap["/"].cs.StartTime)
|
||||
assert.Equal(summary.Node.CPU, cgroupStatsMap["/"].cs.CPU)
|
||||
assert.Equal(summary.Node.Memory, cgroupStatsMap["/"].cs.Memory)
|
||||
assert.Equal(summary.Node.Network, cgroupStatsMap["/"].ns)
|
||||
assert.Equal(summary.Node.Fs, rootFsStats)
|
||||
assert.Equal(summary.Node.Runtime, &statsapi.RuntimeStats{ImageFs: imageFsStats})
|
||||
|
||||
assert.Equal(len(summary.Node.SystemContainers), 3)
|
||||
assert.Contains(summary.Node.SystemContainers, statsapi.ContainerStats{
|
||||
Name: "kubelet",
|
||||
StartTime: cgroupStatsMap["/kubelet"].cs.StartTime,
|
||||
CPU: cgroupStatsMap["/kubelet"].cs.CPU,
|
||||
Memory: cgroupStatsMap["/kubelet"].cs.Memory,
|
||||
Accelerators: cgroupStatsMap["/kubelet"].cs.Accelerators,
|
||||
UserDefinedMetrics: cgroupStatsMap["/kubelet"].cs.UserDefinedMetrics,
|
||||
})
|
||||
assert.Contains(summary.Node.SystemContainers, statsapi.ContainerStats{
|
||||
Name: "misc",
|
||||
StartTime: cgroupStatsMap["/misc"].cs.StartTime,
|
||||
CPU: cgroupStatsMap["/misc"].cs.CPU,
|
||||
Memory: cgroupStatsMap["/misc"].cs.Memory,
|
||||
Accelerators: cgroupStatsMap["/misc"].cs.Accelerators,
|
||||
UserDefinedMetrics: cgroupStatsMap["/misc"].cs.UserDefinedMetrics,
|
||||
})
|
||||
assert.Contains(summary.Node.SystemContainers, statsapi.ContainerStats{
|
||||
Name: "runtime",
|
||||
StartTime: cgroupStatsMap["/runtime"].cs.StartTime,
|
||||
CPU: cgroupStatsMap["/runtime"].cs.CPU,
|
||||
Memory: cgroupStatsMap["/runtime"].cs.Memory,
|
||||
Accelerators: cgroupStatsMap["/runtime"].cs.Accelerators,
|
||||
UserDefinedMetrics: cgroupStatsMap["/runtime"].cs.UserDefinedMetrics,
|
||||
})
|
||||
assert.Equal(summary.Pods, podStats)
|
||||
}
|
||||
|
||||
func getFsStats() *statsapi.FsStats {
|
||||
f := fuzz.New().NilChance(0)
|
||||
v := &statsapi.FsStats{}
|
||||
f.Fuzz(v)
|
||||
return v
|
||||
}
|
||||
|
||||
func getContainerStats() *statsapi.ContainerStats {
|
||||
f := fuzz.New().NilChance(0)
|
||||
v := &statsapi.ContainerStats{}
|
||||
f.Fuzz(v)
|
||||
return v
|
||||
}
|
||||
|
||||
func getVolumeStats() *statsapi.VolumeStats {
|
||||
f := fuzz.New().NilChance(0)
|
||||
v := &statsapi.VolumeStats{}
|
||||
f.Fuzz(v)
|
||||
return v
|
||||
}
|
||||
|
||||
func getNetworkStats() *statsapi.NetworkStats {
|
||||
f := fuzz.New().NilChance(0)
|
||||
v := &statsapi.NetworkStats{}
|
||||
f.Fuzz(v)
|
||||
return v
|
||||
}
|
31
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/testing/BUILD
generated
vendored
Normal file
31
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/testing/BUILD
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["mock_stats_provider.go"],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server/stats/testing",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/kubelet/apis/stats/v1alpha1:go_default_library",
|
||||
"//pkg/kubelet/cm:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//vendor/github.com/google/cadvisor/info/v1:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/mock:go_default_library",
|
||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
280
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/testing/mock_stats_provider.go
generated
vendored
Normal file
280
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/testing/mock_stats_provider.go
generated
vendored
Normal file
@ -0,0 +1,280 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package testing
|
||||
|
||||
import cm "k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
|
||||
import types "k8s.io/apimachinery/pkg/types"
|
||||
import v1 "github.com/google/cadvisor/info/v1"
|
||||
import v1alpha1 "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
|
||||
import volume "k8s.io/kubernetes/pkg/volume"
|
||||
|
||||
// DO NOT EDIT
|
||||
// GENERATED BY mockery
|
||||
|
||||
// StatsProvider is an autogenerated mock type for the StatsProvider type
|
||||
type StatsProvider struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetCgroupStats provides a mock function with given fields: cgroupName
|
||||
func (_m *StatsProvider) GetCgroupStats(cgroupName string) (*v1alpha1.ContainerStats, *v1alpha1.NetworkStats, error) {
|
||||
ret := _m.Called(cgroupName)
|
||||
|
||||
var r0 *v1alpha1.ContainerStats
|
||||
if rf, ok := ret.Get(0).(func(string) *v1alpha1.ContainerStats); ok {
|
||||
r0 = rf(cgroupName)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1alpha1.ContainerStats)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *v1alpha1.NetworkStats
|
||||
if rf, ok := ret.Get(1).(func(string) *v1alpha1.NetworkStats); ok {
|
||||
r1 = rf(cgroupName)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*v1alpha1.NetworkStats)
|
||||
}
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(string) error); ok {
|
||||
r2 = rf(cgroupName)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// GetContainerInfo provides a mock function with given fields: podFullName, uid, containerName, req
|
||||
func (_m *StatsProvider) GetContainerInfo(podFullName string, uid types.UID, containerName string, req *v1.ContainerInfoRequest) (*v1.ContainerInfo, error) {
|
||||
ret := _m.Called(podFullName, uid, containerName, req)
|
||||
|
||||
var r0 *v1.ContainerInfo
|
||||
if rf, ok := ret.Get(0).(func(string, types.UID, string, *v1.ContainerInfoRequest) *v1.ContainerInfo); ok {
|
||||
r0 = rf(podFullName, uid, containerName, req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1.ContainerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, types.UID, string, *v1.ContainerInfoRequest) error); ok {
|
||||
r1 = rf(podFullName, uid, containerName, req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetNode provides a mock function with given fields:
|
||||
func (_m *StatsProvider) GetNode() (*corev1.Node, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *corev1.Node
|
||||
if rf, ok := ret.Get(0).(func() *corev1.Node); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*corev1.Node)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetNodeConfig provides a mock function with given fields:
|
||||
func (_m *StatsProvider) GetNodeConfig() cm.NodeConfig {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 cm.NodeConfig
|
||||
if rf, ok := ret.Get(0).(func() cm.NodeConfig); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(cm.NodeConfig)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetPodByName provides a mock function with given fields: namespace, name
|
||||
func (_m *StatsProvider) GetPodByName(namespace string, name string) (*corev1.Pod, bool) {
|
||||
ret := _m.Called(namespace, name)
|
||||
|
||||
var r0 *corev1.Pod
|
||||
if rf, ok := ret.Get(0).(func(string, string) *corev1.Pod); ok {
|
||||
r0 = rf(namespace, name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*corev1.Pod)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 bool
|
||||
if rf, ok := ret.Get(1).(func(string, string) bool); ok {
|
||||
r1 = rf(namespace, name)
|
||||
} else {
|
||||
r1 = ret.Get(1).(bool)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetPods provides a mock function with given fields:
|
||||
func (_m *StatsProvider) GetPods() []*corev1.Pod {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*corev1.Pod
|
||||
if rf, ok := ret.Get(0).(func() []*corev1.Pod); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*corev1.Pod)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetRawContainerInfo provides a mock function with given fields: containerName, req, subcontainers
|
||||
func (_m *StatsProvider) GetRawContainerInfo(containerName string, req *v1.ContainerInfoRequest, subcontainers bool) (map[string]*v1.ContainerInfo, error) {
|
||||
ret := _m.Called(containerName, req, subcontainers)
|
||||
|
||||
var r0 map[string]*v1.ContainerInfo
|
||||
if rf, ok := ret.Get(0).(func(string, *v1.ContainerInfoRequest, bool) map[string]*v1.ContainerInfo); ok {
|
||||
r0 = rf(containerName, req, subcontainers)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]*v1.ContainerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *v1.ContainerInfoRequest, bool) error); ok {
|
||||
r1 = rf(containerName, req, subcontainers)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ImageFsStats provides a mock function with given fields:
|
||||
func (_m *StatsProvider) ImageFsStats() (*v1alpha1.FsStats, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *v1alpha1.FsStats
|
||||
if rf, ok := ret.Get(0).(func() *v1alpha1.FsStats); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1alpha1.FsStats)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListPodStats provides a mock function with given fields:
|
||||
func (_m *StatsProvider) ListPodStats() ([]v1alpha1.PodStats, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []v1alpha1.PodStats
|
||||
if rf, ok := ret.Get(0).(func() []v1alpha1.PodStats); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]v1alpha1.PodStats)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListVolumesForPod provides a mock function with given fields: podUID
|
||||
func (_m *StatsProvider) ListVolumesForPod(podUID types.UID) (map[string]volume.Volume, bool) {
|
||||
ret := _m.Called(podUID)
|
||||
|
||||
var r0 map[string]volume.Volume
|
||||
if rf, ok := ret.Get(0).(func(types.UID) map[string]volume.Volume); ok {
|
||||
r0 = rf(podUID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]volume.Volume)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 bool
|
||||
if rf, ok := ret.Get(1).(func(types.UID) bool); ok {
|
||||
r1 = rf(podUID)
|
||||
} else {
|
||||
r1 = ret.Get(1).(bool)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RootFsStats provides a mock function with given fields:
|
||||
func (_m *StatsProvider) RootFsStats() (*v1alpha1.FsStats, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *v1alpha1.FsStats
|
||||
if rf, ok := ret.Get(0).(func() *v1alpha1.FsStats); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1alpha1.FsStats)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
175
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/volume_stat_calculator.go
generated
vendored
Normal file
175
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/volume_stat_calculator.go
generated
vendored
Normal file
@ -0,0 +1,175 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
stats "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/kubelet/metrics"
|
||||
"k8s.io/kubernetes/pkg/kubelet/util/format"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// volumeStatCalculator calculates volume metrics for a given pod periodically in the background and caches the result
|
||||
type volumeStatCalculator struct {
|
||||
statsProvider StatsProvider
|
||||
jitterPeriod time.Duration
|
||||
pod *v1.Pod
|
||||
stopChannel chan struct{}
|
||||
startO sync.Once
|
||||
stopO sync.Once
|
||||
latest atomic.Value
|
||||
}
|
||||
|
||||
// PodVolumeStats encapsulates the VolumeStats for a pod.
|
||||
// It consists of two lists, for local ephemeral volumes, and for persistent volumes respectively.
|
||||
type PodVolumeStats struct {
|
||||
EphemeralVolumes []stats.VolumeStats
|
||||
PersistentVolumes []stats.VolumeStats
|
||||
}
|
||||
|
||||
// newVolumeStatCalculator creates a new VolumeStatCalculator
|
||||
func newVolumeStatCalculator(statsProvider StatsProvider, jitterPeriod time.Duration, pod *v1.Pod) *volumeStatCalculator {
|
||||
return &volumeStatCalculator{
|
||||
statsProvider: statsProvider,
|
||||
jitterPeriod: jitterPeriod,
|
||||
pod: pod,
|
||||
stopChannel: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// StartOnce starts pod volume calc that will occur periodically in the background until s.StopOnce is called
|
||||
func (s *volumeStatCalculator) StartOnce() *volumeStatCalculator {
|
||||
s.startO.Do(func() {
|
||||
go wait.JitterUntil(func() {
|
||||
s.calcAndStoreStats()
|
||||
}, s.jitterPeriod, 1.0, true, s.stopChannel)
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
// StopOnce stops background pod volume calculation. Will not stop a currently executing calculations until
|
||||
// they complete their current iteration.
|
||||
func (s *volumeStatCalculator) StopOnce() *volumeStatCalculator {
|
||||
s.stopO.Do(func() {
|
||||
close(s.stopChannel)
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
// getLatest returns the most recent PodVolumeStats from the cache
|
||||
func (s *volumeStatCalculator) GetLatest() (PodVolumeStats, bool) {
|
||||
if result := s.latest.Load(); result == nil {
|
||||
return PodVolumeStats{}, false
|
||||
} else {
|
||||
return result.(PodVolumeStats), true
|
||||
}
|
||||
}
|
||||
|
||||
// calcAndStoreStats calculates PodVolumeStats for a given pod and writes the result to the s.latest cache.
|
||||
// If the pod references PVCs, the prometheus metrics for those are updated with the result.
|
||||
func (s *volumeStatCalculator) calcAndStoreStats() {
|
||||
// Find all Volumes for the Pod
|
||||
volumes, found := s.statsProvider.ListVolumesForPod(s.pod.UID)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
// Get volume specs for the pod - key'd by volume name
|
||||
volumesSpec := make(map[string]v1.Volume)
|
||||
for _, v := range s.pod.Spec.Volumes {
|
||||
volumesSpec[v.Name] = v
|
||||
}
|
||||
|
||||
// Call GetMetrics on each Volume and copy the result to a new VolumeStats.FsStats
|
||||
ephemeralStats := []stats.VolumeStats{}
|
||||
persistentStats := []stats.VolumeStats{}
|
||||
for name, v := range volumes {
|
||||
metric, err := v.GetMetrics()
|
||||
if err != nil {
|
||||
// Expected for Volumes that don't support Metrics
|
||||
if !volume.IsNotSupported(err) {
|
||||
glog.V(4).Infof("Failed to calculate volume metrics for pod %s volume %s: %+v", format.Pod(s.pod), name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Lookup the volume spec and add a 'PVCReference' for volumes that reference a PVC
|
||||
volSpec := volumesSpec[name]
|
||||
var pvcRef *stats.PVCReference
|
||||
if pvcSource := volSpec.PersistentVolumeClaim; pvcSource != nil {
|
||||
pvcRef = &stats.PVCReference{
|
||||
Name: pvcSource.ClaimName,
|
||||
Namespace: s.pod.GetNamespace(),
|
||||
}
|
||||
// Set the PVC's prometheus metrics
|
||||
s.setPVCMetrics(pvcRef, metric)
|
||||
}
|
||||
volumeStats := s.parsePodVolumeStats(name, pvcRef, metric, volSpec)
|
||||
if isVolumeEphemeral(volSpec) {
|
||||
ephemeralStats = append(ephemeralStats, volumeStats)
|
||||
} else {
|
||||
persistentStats = append(persistentStats, volumeStats)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Store the new stats
|
||||
s.latest.Store(PodVolumeStats{EphemeralVolumes: ephemeralStats,
|
||||
PersistentVolumes: persistentStats})
|
||||
}
|
||||
|
||||
// parsePodVolumeStats converts (internal) volume.Metrics to (external) stats.VolumeStats structures
|
||||
func (s *volumeStatCalculator) parsePodVolumeStats(podName string, pvcRef *stats.PVCReference, metric *volume.Metrics, volSpec v1.Volume) stats.VolumeStats {
|
||||
available := uint64(metric.Available.Value())
|
||||
capacity := uint64(metric.Capacity.Value())
|
||||
used := uint64(metric.Used.Value())
|
||||
inodes := uint64(metric.Inodes.Value())
|
||||
inodesFree := uint64(metric.InodesFree.Value())
|
||||
inodesUsed := uint64(metric.InodesUsed.Value())
|
||||
|
||||
return stats.VolumeStats{
|
||||
Name: podName,
|
||||
PVCRef: pvcRef,
|
||||
FsStats: stats.FsStats{Time: metric.Time, AvailableBytes: &available, CapacityBytes: &capacity,
|
||||
UsedBytes: &used, Inodes: &inodes, InodesFree: &inodesFree, InodesUsed: &inodesUsed},
|
||||
}
|
||||
}
|
||||
|
||||
func isVolumeEphemeral(volume v1.Volume) bool {
|
||||
if (volume.EmptyDir != nil && volume.EmptyDir.Medium == v1.StorageMediumDefault) ||
|
||||
volume.ConfigMap != nil || volume.GitRepo != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// setPVCMetrics sets the given PVC's prometheus metrics to match the given volume.Metrics
|
||||
func (s *volumeStatCalculator) setPVCMetrics(pvcRef *stats.PVCReference, metric *volume.Metrics) {
|
||||
metrics.VolumeStatsAvailableBytes.WithLabelValues(pvcRef.Namespace, pvcRef.Name).Set(float64(metric.Available.Value()))
|
||||
metrics.VolumeStatsCapacityBytes.WithLabelValues(pvcRef.Namespace, pvcRef.Name).Set(float64(metric.Capacity.Value()))
|
||||
metrics.VolumeStatsUsedBytes.WithLabelValues(pvcRef.Namespace, pvcRef.Name).Set(float64(metric.Used.Value()))
|
||||
metrics.VolumeStatsInodes.WithLabelValues(pvcRef.Namespace, pvcRef.Name).Set(float64(metric.Inodes.Value()))
|
||||
metrics.VolumeStatsInodesFree.WithLabelValues(pvcRef.Namespace, pvcRef.Name).Set(float64(metric.InodesFree.Value()))
|
||||
metrics.VolumeStatsInodesUsed.WithLabelValues(pvcRef.Namespace, pvcRef.Name).Set(float64(metric.InodesUsed.Value()))
|
||||
}
|
143
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/volume_stat_calculator_test.go
generated
vendored
Normal file
143
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/volume_stat_calculator_test.go
generated
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
k8sv1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kubestats "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
|
||||
statstest "k8s.io/kubernetes/pkg/kubelet/server/stats/testing"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace0 = "test0"
|
||||
pName0 = "pod0"
|
||||
capacity = int64(10000000)
|
||||
available = int64(5000000)
|
||||
inodesTotal = int64(2000)
|
||||
inodesFree = int64(1000)
|
||||
|
||||
vol0 = "vol0"
|
||||
vol1 = "vol1"
|
||||
pvcClaimName = "pvc-fake"
|
||||
)
|
||||
|
||||
func TestPVCRef(t *testing.T) {
|
||||
// Create pod spec to test against
|
||||
podVolumes := []k8sv1.Volume{
|
||||
{
|
||||
Name: vol0,
|
||||
VolumeSource: k8sv1.VolumeSource{
|
||||
GCEPersistentDisk: &k8sv1.GCEPersistentDiskVolumeSource{
|
||||
PDName: "fake-device1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: vol1,
|
||||
VolumeSource: k8sv1.VolumeSource{
|
||||
PersistentVolumeClaim: &k8sv1.PersistentVolumeClaimVolumeSource{
|
||||
ClaimName: pvcClaimName,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakePod := &k8sv1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: pName0,
|
||||
Namespace: namespace0,
|
||||
UID: "UID" + pName0,
|
||||
},
|
||||
Spec: k8sv1.PodSpec{
|
||||
Volumes: podVolumes,
|
||||
},
|
||||
}
|
||||
|
||||
// Setup mock stats provider
|
||||
mockStats := new(statstest.StatsProvider)
|
||||
volumes := map[string]volume.Volume{vol0: &fakeVolume{}, vol1: &fakeVolume{}}
|
||||
mockStats.On("ListVolumesForPod", fakePod.UID).Return(volumes, true)
|
||||
|
||||
// Calculate stats for pod
|
||||
statsCalculator := newVolumeStatCalculator(mockStats, time.Minute, fakePod)
|
||||
statsCalculator.calcAndStoreStats()
|
||||
vs, _ := statsCalculator.GetLatest()
|
||||
|
||||
assert.Len(t, append(vs.EphemeralVolumes, vs.PersistentVolumes...), 2)
|
||||
// Verify 'vol0' doesn't have a PVC reference
|
||||
assert.Contains(t, append(vs.EphemeralVolumes, vs.PersistentVolumes...), kubestats.VolumeStats{
|
||||
Name: vol0,
|
||||
FsStats: expectedFSStats(),
|
||||
})
|
||||
// Verify 'vol1' has a PVC reference
|
||||
assert.Contains(t, append(vs.EphemeralVolumes, vs.PersistentVolumes...), kubestats.VolumeStats{
|
||||
Name: vol1,
|
||||
PVCRef: &kubestats.PVCReference{
|
||||
Name: pvcClaimName,
|
||||
Namespace: namespace0,
|
||||
},
|
||||
FsStats: expectedFSStats(),
|
||||
})
|
||||
}
|
||||
|
||||
// Fake volume/metrics provider
|
||||
var _ volume.Volume = &fakeVolume{}
|
||||
|
||||
type fakeVolume struct{}
|
||||
|
||||
func (v *fakeVolume) GetPath() string { return "" }
|
||||
|
||||
func (v *fakeVolume) GetMetrics() (*volume.Metrics, error) {
|
||||
return expectedMetrics(), nil
|
||||
}
|
||||
|
||||
func expectedMetrics() *volume.Metrics {
|
||||
return &volume.Metrics{
|
||||
Available: resource.NewQuantity(available, resource.BinarySI),
|
||||
Capacity: resource.NewQuantity(capacity, resource.BinarySI),
|
||||
Used: resource.NewQuantity(available-capacity, resource.BinarySI),
|
||||
Inodes: resource.NewQuantity(inodesTotal, resource.BinarySI),
|
||||
InodesFree: resource.NewQuantity(inodesFree, resource.BinarySI),
|
||||
InodesUsed: resource.NewQuantity(inodesTotal-inodesFree, resource.BinarySI),
|
||||
}
|
||||
}
|
||||
|
||||
func expectedFSStats() kubestats.FsStats {
|
||||
metric := expectedMetrics()
|
||||
available := uint64(metric.Available.Value())
|
||||
capacity := uint64(metric.Capacity.Value())
|
||||
used := uint64(metric.Used.Value())
|
||||
inodes := uint64(metric.Inodes.Value())
|
||||
inodesFree := uint64(metric.InodesFree.Value())
|
||||
inodesUsed := uint64(metric.InodesUsed.Value())
|
||||
return kubestats.FsStats{
|
||||
AvailableBytes: &available,
|
||||
CapacityBytes: &capacity,
|
||||
UsedBytes: &used,
|
||||
Inodes: &inodes,
|
||||
InodesFree: &inodesFree,
|
||||
InodesUsed: &inodesUsed,
|
||||
}
|
||||
}
|
63
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/BUILD
generated
vendored
Normal file
63
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/BUILD
generated
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"errors.go",
|
||||
"request_cache.go",
|
||||
"server.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server/streaming",
|
||||
deps = [
|
||||
"//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library",
|
||||
"//pkg/kubelet/server/portforward:go_default_library",
|
||||
"//pkg/kubelet/server/remotecommand:go_default_library",
|
||||
"//vendor/github.com/emicklei/go-restful:go_default_library",
|
||||
"//vendor/google.golang.org/grpc:go_default_library",
|
||||
"//vendor/google.golang.org/grpc/codes:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/remotecommand:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/remotecommand:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"request_cache_test.go",
|
||||
"server_test.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubelet/server/streaming",
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library",
|
||||
"//pkg/kubelet/server/portforward:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/require:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library",
|
||||
"//vendor/k8s.io/client-go/rest:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/remotecommand:go_default_library",
|
||||
"//vendor/k8s.io/client-go/transport/spdy:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
55
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/errors.go
generated
vendored
Normal file
55
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/errors.go
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func ErrorStreamingDisabled(method string) error {
|
||||
return grpc.Errorf(codes.NotFound, fmt.Sprintf("streaming method %s disabled", method))
|
||||
}
|
||||
|
||||
// The error returned when the maximum number of in-flight requests is exceeded.
|
||||
func ErrorTooManyInFlight() error {
|
||||
return grpc.Errorf(codes.ResourceExhausted, "maximum number of in-flight requests exceeded")
|
||||
}
|
||||
|
||||
// Translates a CRI streaming error into an appropriate HTTP response.
|
||||
func WriteError(err error, w http.ResponseWriter) error {
|
||||
var status int
|
||||
switch grpc.Code(err) {
|
||||
case codes.NotFound:
|
||||
status = http.StatusNotFound
|
||||
case codes.ResourceExhausted:
|
||||
// We only expect to hit this if there is a DoS, so we just wait the full TTL.
|
||||
// If this is ever hit in steady-state operations, consider increasing the MaxInFlight requests,
|
||||
// or plumbing through the time to next expiration.
|
||||
w.Header().Set("Retry-After", strconv.Itoa(int(CacheTTL.Seconds())))
|
||||
status = http.StatusTooManyRequests
|
||||
default:
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
_, writeErr := w.Write([]byte(err.Error()))
|
||||
return writeErr
|
||||
}
|
146
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/request_cache.go
generated
vendored
Normal file
146
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/request_cache.go
generated
vendored
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
)
|
||||
|
||||
var (
|
||||
// Timeout after which tokens become invalid.
|
||||
CacheTTL = 1 * time.Minute
|
||||
// The maximum number of in-flight requests to allow.
|
||||
MaxInFlight = 1000
|
||||
// Length of the random base64 encoded token identifying the request.
|
||||
TokenLen = 8
|
||||
)
|
||||
|
||||
// requestCache caches streaming (exec/attach/port-forward) requests and generates a single-use
|
||||
// random token for their retrieval. The requestCache is used for building streaming URLs without
|
||||
// the need to encode every request parameter in the URL.
|
||||
type requestCache struct {
|
||||
// clock is used to obtain the current time
|
||||
clock clock.Clock
|
||||
|
||||
// tokens maps the generate token to the request for fast retrieval.
|
||||
tokens map[string]*list.Element
|
||||
// ll maintains an age-ordered request list for faster garbage collection of expired requests.
|
||||
ll *list.List
|
||||
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// Type representing an *ExecRequest, *AttachRequest, or *PortForwardRequest.
|
||||
type request interface{}
|
||||
|
||||
type cacheEntry struct {
|
||||
token string
|
||||
req request
|
||||
expireTime time.Time
|
||||
}
|
||||
|
||||
func newRequestCache() *requestCache {
|
||||
return &requestCache{
|
||||
clock: clock.RealClock{},
|
||||
ll: list.New(),
|
||||
tokens: make(map[string]*list.Element),
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the given request into the cache and returns the token used for fetching it out.
|
||||
func (c *requestCache) Insert(req request) (token string, err error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// Remove expired entries.
|
||||
c.gc()
|
||||
// If the cache is full, reject the request.
|
||||
if c.ll.Len() == MaxInFlight {
|
||||
return "", ErrorTooManyInFlight()
|
||||
}
|
||||
token, err = c.uniqueToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ele := c.ll.PushFront(&cacheEntry{token, req, c.clock.Now().Add(CacheTTL)})
|
||||
|
||||
c.tokens[token] = ele
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Consume the token (remove it from the cache) and return the cached request, if found.
|
||||
func (c *requestCache) Consume(token string) (req request, found bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
ele, ok := c.tokens[token]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
c.ll.Remove(ele)
|
||||
delete(c.tokens, token)
|
||||
|
||||
entry := ele.Value.(*cacheEntry)
|
||||
if c.clock.Now().After(entry.expireTime) {
|
||||
// Entry already expired.
|
||||
return nil, false
|
||||
}
|
||||
return entry.req, true
|
||||
}
|
||||
|
||||
// uniqueToken generates a random URL-safe token and ensures uniqueness.
|
||||
func (c *requestCache) uniqueToken() (string, error) {
|
||||
const maxTries = 10
|
||||
// Number of bytes to be TokenLen when base64 encoded.
|
||||
tokenSize := math.Ceil(float64(TokenLen) * 6 / 8)
|
||||
rawToken := make([]byte, int(tokenSize))
|
||||
for i := 0; i < maxTries; i++ {
|
||||
if _, err := rand.Read(rawToken); err != nil {
|
||||
return "", err
|
||||
}
|
||||
encoded := base64.RawURLEncoding.EncodeToString(rawToken)
|
||||
token := encoded[:TokenLen]
|
||||
// If it's unique, return it. Otherwise retry.
|
||||
if _, exists := c.tokens[encoded]; !exists {
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to generate unique token")
|
||||
}
|
||||
|
||||
// Must be write-locked prior to calling.
|
||||
func (c *requestCache) gc() {
|
||||
now := c.clock.Now()
|
||||
for c.ll.Len() > 0 {
|
||||
oldest := c.ll.Back()
|
||||
entry := oldest.Value.(*cacheEntry)
|
||||
if !now.After(entry.expireTime) {
|
||||
return
|
||||
}
|
||||
|
||||
// Oldest value is expired; remove it.
|
||||
c.ll.Remove(oldest)
|
||||
delete(c.tokens, entry.token)
|
||||
}
|
||||
}
|
221
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/request_cache_test.go
generated
vendored
Normal file
221
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/request_cache_test.go
generated
vendored
Normal file
@ -0,0 +1,221 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
)
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
c, _ := newTestCache()
|
||||
|
||||
// Insert normal
|
||||
oldestTok, err := c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, oldestTok, TokenLen)
|
||||
assertCacheSize(t, c, 1)
|
||||
|
||||
// Insert until full
|
||||
for i := 0; i < MaxInFlight-2; i++ {
|
||||
tok, err := c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tok, TokenLen)
|
||||
}
|
||||
assertCacheSize(t, c, MaxInFlight-1)
|
||||
|
||||
newestReq := nextRequest()
|
||||
newestTok, err := c.Insert(newestReq)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, newestTok, TokenLen)
|
||||
assertCacheSize(t, c, MaxInFlight)
|
||||
require.Contains(t, c.tokens, oldestTok, "oldest request should still be cached")
|
||||
|
||||
// Consume newest token.
|
||||
req, ok := c.Consume(newestTok)
|
||||
assert.True(t, ok, "newest request should still be cached")
|
||||
assert.Equal(t, newestReq, req)
|
||||
require.Contains(t, c.tokens, oldestTok, "oldest request should still be cached")
|
||||
|
||||
// Insert again (still full)
|
||||
tok, err := c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tok, TokenLen)
|
||||
assertCacheSize(t, c, MaxInFlight)
|
||||
|
||||
// Insert again (should evict)
|
||||
_, err = c.Insert(nextRequest())
|
||||
assert.Error(t, err, "should reject further requests")
|
||||
errResponse := httptest.NewRecorder()
|
||||
require.NoError(t, WriteError(err, errResponse))
|
||||
assert.Equal(t, errResponse.Code, http.StatusTooManyRequests)
|
||||
assert.Equal(t, strconv.Itoa(int(CacheTTL.Seconds())), errResponse.HeaderMap.Get("Retry-After"))
|
||||
|
||||
assertCacheSize(t, c, MaxInFlight)
|
||||
_, ok = c.Consume(oldestTok)
|
||||
assert.True(t, ok, "oldest request should be valid")
|
||||
}
|
||||
|
||||
func TestConsume(t *testing.T) {
|
||||
c, clock := newTestCache()
|
||||
|
||||
{ // Insert & consume.
|
||||
req := nextRequest()
|
||||
tok, err := c.Insert(req)
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 1)
|
||||
|
||||
cachedReq, ok := c.Consume(tok)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, req, cachedReq)
|
||||
assertCacheSize(t, c, 0)
|
||||
}
|
||||
|
||||
{ // Insert & consume out of order
|
||||
req1 := nextRequest()
|
||||
tok1, err := c.Insert(req1)
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 1)
|
||||
|
||||
req2 := nextRequest()
|
||||
tok2, err := c.Insert(req2)
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 2)
|
||||
|
||||
cachedReq2, ok := c.Consume(tok2)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, req2, cachedReq2)
|
||||
assertCacheSize(t, c, 1)
|
||||
|
||||
cachedReq1, ok := c.Consume(tok1)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, req1, cachedReq1)
|
||||
assertCacheSize(t, c, 0)
|
||||
}
|
||||
|
||||
{ // Consume a second time
|
||||
req := nextRequest()
|
||||
tok, err := c.Insert(req)
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 1)
|
||||
|
||||
cachedReq, ok := c.Consume(tok)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, req, cachedReq)
|
||||
assertCacheSize(t, c, 0)
|
||||
|
||||
_, ok = c.Consume(tok)
|
||||
assert.False(t, ok)
|
||||
assertCacheSize(t, c, 0)
|
||||
}
|
||||
|
||||
{ // Consume without insert
|
||||
_, ok := c.Consume("fooBAR")
|
||||
assert.False(t, ok)
|
||||
assertCacheSize(t, c, 0)
|
||||
}
|
||||
|
||||
{ // Consume expired
|
||||
tok, err := c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 1)
|
||||
|
||||
clock.Step(2 * CacheTTL)
|
||||
|
||||
_, ok := c.Consume(tok)
|
||||
assert.False(t, ok)
|
||||
assertCacheSize(t, c, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGC(t *testing.T) {
|
||||
c, clock := newTestCache()
|
||||
|
||||
// When empty
|
||||
c.gc()
|
||||
assertCacheSize(t, c, 0)
|
||||
|
||||
tok1, err := c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 1)
|
||||
clock.Step(10 * time.Second)
|
||||
tok2, err := c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 2)
|
||||
|
||||
// expired: tok1, tok2
|
||||
// non-expired: tok3, tok4
|
||||
clock.Step(2 * CacheTTL)
|
||||
tok3, err := c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 1)
|
||||
clock.Step(10 * time.Second)
|
||||
tok4, err := c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 2)
|
||||
|
||||
_, ok := c.Consume(tok1)
|
||||
assert.False(t, ok)
|
||||
_, ok = c.Consume(tok2)
|
||||
assert.False(t, ok)
|
||||
_, ok = c.Consume(tok3)
|
||||
assert.True(t, ok)
|
||||
_, ok = c.Consume(tok4)
|
||||
assert.True(t, ok)
|
||||
|
||||
// When full, nothing is expired.
|
||||
for i := 0; i < MaxInFlight; i++ {
|
||||
_, err := c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assertCacheSize(t, c, MaxInFlight)
|
||||
|
||||
// When everything is expired
|
||||
clock.Step(2 * CacheTTL)
|
||||
_, err = c.Insert(nextRequest())
|
||||
require.NoError(t, err)
|
||||
assertCacheSize(t, c, 1)
|
||||
}
|
||||
|
||||
func newTestCache() (*requestCache, *clock.FakeClock) {
|
||||
c := newRequestCache()
|
||||
fakeClock := clock.NewFakeClock(time.Now())
|
||||
c.clock = fakeClock
|
||||
return c, fakeClock
|
||||
}
|
||||
|
||||
func assertCacheSize(t *testing.T, cache *requestCache, expectedSize int) {
|
||||
tokenLen := len(cache.tokens)
|
||||
llLen := cache.ll.Len()
|
||||
assert.Equal(t, tokenLen, llLen, "inconsistent cache size! len(tokens)=%d; len(ll)=%d", tokenLen, llLen)
|
||||
assert.Equal(t, expectedSize, tokenLen, "unexpected cache size!")
|
||||
}
|
||||
|
||||
var requestUID = 0
|
||||
|
||||
func nextRequest() interface{} {
|
||||
requestUID++
|
||||
return requestUID
|
||||
}
|
374
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/server.go
generated
vendored
Normal file
374
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/server.go
generated
vendored
Normal file
@ -0,0 +1,374 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/portforward"
|
||||
remotecommandserver "k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
|
||||
)
|
||||
|
||||
// The library interface to serve the stream requests.
|
||||
type Server interface {
|
||||
http.Handler
|
||||
|
||||
// Get the serving URL for the requests.
|
||||
// Requests must not be nil. Responses may be nil iff an error is returned.
|
||||
GetExec(*runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error)
|
||||
GetAttach(req *runtimeapi.AttachRequest) (*runtimeapi.AttachResponse, error)
|
||||
GetPortForward(*runtimeapi.PortForwardRequest) (*runtimeapi.PortForwardResponse, error)
|
||||
|
||||
// Start the server.
|
||||
// addr is the address to serve on (address:port) stayUp indicates whether the server should
|
||||
// listen until Stop() is called, or automatically stop after all expected connections are
|
||||
// closed. Calling Get{Exec,Attach,PortForward} increments the expected connection count.
|
||||
// Function does not return until the server is stopped.
|
||||
Start(stayUp bool) error
|
||||
// Stop the server, and terminate any open connections.
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// The interface to execute the commands and provide the streams.
|
||||
type Runtime interface {
|
||||
Exec(containerID string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error
|
||||
Attach(containerID string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error
|
||||
PortForward(podSandboxID string, port int32, stream io.ReadWriteCloser) error
|
||||
}
|
||||
|
||||
// Config defines the options used for running the stream server.
|
||||
type Config struct {
|
||||
// The host:port address the server will listen on.
|
||||
Addr string
|
||||
// The optional base URL for constructing streaming URLs. If empty, the baseURL will be
|
||||
// constructed from the serve address.
|
||||
BaseURL *url.URL
|
||||
|
||||
// How long to leave idle connections open for.
|
||||
StreamIdleTimeout time.Duration
|
||||
// How long to wait for clients to create streams. Only used for SPDY streaming.
|
||||
StreamCreationTimeout time.Duration
|
||||
|
||||
// The streaming protocols the server supports (understands and permits). See
|
||||
// k8s.io/kubernetes/pkg/kubelet/server/remotecommand/constants.go for available protocols.
|
||||
// Only used for SPDY streaming.
|
||||
SupportedRemoteCommandProtocols []string
|
||||
|
||||
// The streaming protocols the server supports (understands and permits). See
|
||||
// k8s.io/kubernetes/pkg/kubelet/server/portforward/constants.go for available protocols.
|
||||
// Only used for SPDY streaming.
|
||||
SupportedPortForwardProtocols []string
|
||||
|
||||
// The config for serving over TLS. If nil, TLS will not be used.
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// DefaultConfig provides default values for server Config. The DefaultConfig is partial, so
|
||||
// some fields like Addr must still be provided.
|
||||
var DefaultConfig = Config{
|
||||
StreamIdleTimeout: 4 * time.Hour,
|
||||
StreamCreationTimeout: remotecommandconsts.DefaultStreamCreationTimeout,
|
||||
SupportedRemoteCommandProtocols: remotecommandconsts.SupportedStreamingProtocols,
|
||||
SupportedPortForwardProtocols: portforward.SupportedProtocols,
|
||||
}
|
||||
|
||||
// TODO(tallclair): Add auth(n/z) interface & handling.
|
||||
func NewServer(config Config, runtime Runtime) (Server, error) {
|
||||
s := &server{
|
||||
config: config,
|
||||
runtime: &criAdapter{runtime},
|
||||
cache: newRequestCache(),
|
||||
}
|
||||
|
||||
if s.config.BaseURL == nil {
|
||||
s.config.BaseURL = &url.URL{
|
||||
Scheme: "http",
|
||||
Host: s.config.Addr,
|
||||
}
|
||||
if s.config.TLSConfig != nil {
|
||||
s.config.BaseURL.Scheme = "https"
|
||||
}
|
||||
}
|
||||
|
||||
ws := &restful.WebService{}
|
||||
endpoints := []struct {
|
||||
path string
|
||||
handler restful.RouteFunction
|
||||
}{
|
||||
{"/exec/{token}", s.serveExec},
|
||||
{"/attach/{token}", s.serveAttach},
|
||||
{"/portforward/{token}", s.servePortForward},
|
||||
}
|
||||
// If serving relative to a base path, set that here.
|
||||
pathPrefix := path.Dir(s.config.BaseURL.Path)
|
||||
for _, e := range endpoints {
|
||||
for _, method := range []string{"GET", "POST"} {
|
||||
ws.Route(ws.
|
||||
Method(method).
|
||||
Path(path.Join(pathPrefix, e.path)).
|
||||
To(e.handler))
|
||||
}
|
||||
}
|
||||
handler := restful.NewContainer()
|
||||
handler.Add(ws)
|
||||
s.handler = handler
|
||||
s.server = &http.Server{
|
||||
Addr: s.config.Addr,
|
||||
Handler: s.handler,
|
||||
TLSConfig: s.config.TLSConfig,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type server struct {
|
||||
config Config
|
||||
runtime *criAdapter
|
||||
handler http.Handler
|
||||
cache *requestCache
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func validateExecRequest(req *runtimeapi.ExecRequest) error {
|
||||
if req.ContainerId == "" {
|
||||
return grpc.Errorf(codes.InvalidArgument, "missing required container_id")
|
||||
}
|
||||
if req.Tty && req.Stderr {
|
||||
// If TTY is set, stderr cannot be true because multiplexing is not
|
||||
// supported.
|
||||
return grpc.Errorf(codes.InvalidArgument, "tty and stderr cannot both be true")
|
||||
}
|
||||
if !req.Stdin && !req.Stdout && !req.Stderr {
|
||||
return grpc.Errorf(codes.InvalidArgument, "one of stdin, stdout, or stderr must be set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) GetExec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
|
||||
if err := validateExecRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := s.cache.Insert(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &runtimeapi.ExecResponse{
|
||||
Url: s.buildURL("exec", token),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateAttachRequest(req *runtimeapi.AttachRequest) error {
|
||||
if req.ContainerId == "" {
|
||||
return grpc.Errorf(codes.InvalidArgument, "missing required container_id")
|
||||
}
|
||||
if req.Tty && req.Stderr {
|
||||
// If TTY is set, stderr cannot be true because multiplexing is not
|
||||
// supported.
|
||||
return grpc.Errorf(codes.InvalidArgument, "tty and stderr cannot both be true")
|
||||
}
|
||||
if !req.Stdin && !req.Stdout && !req.Stderr {
|
||||
return grpc.Errorf(codes.InvalidArgument, "one of stdin, stdout, and stderr must be set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) GetAttach(req *runtimeapi.AttachRequest) (*runtimeapi.AttachResponse, error) {
|
||||
if err := validateAttachRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := s.cache.Insert(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &runtimeapi.AttachResponse{
|
||||
Url: s.buildURL("attach", token),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) GetPortForward(req *runtimeapi.PortForwardRequest) (*runtimeapi.PortForwardResponse, error) {
|
||||
if req.PodSandboxId == "" {
|
||||
return nil, grpc.Errorf(codes.InvalidArgument, "missing required pod_sandbox_id")
|
||||
}
|
||||
token, err := s.cache.Insert(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &runtimeapi.PortForwardResponse{
|
||||
Url: s.buildURL("portforward", token),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) Start(stayUp bool) error {
|
||||
if !stayUp {
|
||||
// TODO(tallclair): Implement this.
|
||||
return errors.New("stayUp=false is not yet implemented")
|
||||
}
|
||||
|
||||
if s.config.TLSConfig != nil {
|
||||
return s.server.ListenAndServeTLS("", "") // Use certs from TLSConfig.
|
||||
} else {
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) Stop() error {
|
||||
return s.server.Close()
|
||||
}
|
||||
|
||||
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *server) buildURL(method, token string) string {
|
||||
return s.config.BaseURL.ResolveReference(&url.URL{
|
||||
Path: path.Join(method, token),
|
||||
}).String()
|
||||
}
|
||||
|
||||
func (s *server) serveExec(req *restful.Request, resp *restful.Response) {
|
||||
token := req.PathParameter("token")
|
||||
cachedRequest, ok := s.cache.Consume(token)
|
||||
if !ok {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
return
|
||||
}
|
||||
exec, ok := cachedRequest.(*runtimeapi.ExecRequest)
|
||||
if !ok {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
return
|
||||
}
|
||||
|
||||
streamOpts := &remotecommandserver.Options{
|
||||
Stdin: exec.Stdin,
|
||||
Stdout: exec.Stdout,
|
||||
Stderr: exec.Stderr,
|
||||
TTY: exec.Tty,
|
||||
}
|
||||
|
||||
remotecommandserver.ServeExec(
|
||||
resp.ResponseWriter,
|
||||
req.Request,
|
||||
s.runtime,
|
||||
"", // unused: podName
|
||||
"", // unusued: podUID
|
||||
exec.ContainerId,
|
||||
exec.Cmd,
|
||||
streamOpts,
|
||||
s.config.StreamIdleTimeout,
|
||||
s.config.StreamCreationTimeout,
|
||||
s.config.SupportedRemoteCommandProtocols)
|
||||
}
|
||||
|
||||
func (s *server) serveAttach(req *restful.Request, resp *restful.Response) {
|
||||
token := req.PathParameter("token")
|
||||
cachedRequest, ok := s.cache.Consume(token)
|
||||
if !ok {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
return
|
||||
}
|
||||
attach, ok := cachedRequest.(*runtimeapi.AttachRequest)
|
||||
if !ok {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
return
|
||||
}
|
||||
|
||||
streamOpts := &remotecommandserver.Options{
|
||||
Stdin: attach.Stdin,
|
||||
Stdout: attach.Stdout,
|
||||
Stderr: attach.Stderr,
|
||||
TTY: attach.Tty,
|
||||
}
|
||||
remotecommandserver.ServeAttach(
|
||||
resp.ResponseWriter,
|
||||
req.Request,
|
||||
s.runtime,
|
||||
"", // unused: podName
|
||||
"", // unusued: podUID
|
||||
attach.ContainerId,
|
||||
streamOpts,
|
||||
s.config.StreamIdleTimeout,
|
||||
s.config.StreamCreationTimeout,
|
||||
s.config.SupportedRemoteCommandProtocols)
|
||||
}
|
||||
|
||||
func (s *server) servePortForward(req *restful.Request, resp *restful.Response) {
|
||||
token := req.PathParameter("token")
|
||||
cachedRequest, ok := s.cache.Consume(token)
|
||||
if !ok {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
return
|
||||
}
|
||||
pf, ok := cachedRequest.(*runtimeapi.PortForwardRequest)
|
||||
if !ok {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
return
|
||||
}
|
||||
|
||||
portForwardOptions, err := portforward.BuildV4Options(pf.Port)
|
||||
if err != nil {
|
||||
resp.WriteError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
portforward.ServePortForward(
|
||||
resp.ResponseWriter,
|
||||
req.Request,
|
||||
s.runtime,
|
||||
pf.PodSandboxId,
|
||||
"", // unused: podUID
|
||||
portForwardOptions,
|
||||
s.config.StreamIdleTimeout,
|
||||
s.config.StreamCreationTimeout,
|
||||
s.config.SupportedPortForwardProtocols)
|
||||
}
|
||||
|
||||
// criAdapter wraps the Runtime functions to conform to the remotecommand interfaces.
|
||||
// The adapter binds the container ID to the container name argument, and the pod sandbox ID to the pod name.
|
||||
type criAdapter struct {
|
||||
Runtime
|
||||
}
|
||||
|
||||
var _ remotecommandserver.Executor = &criAdapter{}
|
||||
var _ remotecommandserver.Attacher = &criAdapter{}
|
||||
var _ portforward.PortForwarder = &criAdapter{}
|
||||
|
||||
func (a *criAdapter) ExecInContainer(podName string, podUID types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error {
|
||||
return a.Runtime.Exec(container, cmd, in, out, err, tty, resize)
|
||||
}
|
||||
|
||||
func (a *criAdapter) AttachContainer(podName string, podUID types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
|
||||
return a.Runtime.Attach(container, in, out, err, tty, resize)
|
||||
}
|
||||
|
||||
func (a *criAdapter) PortForward(podName string, podUID types.UID, port int32, stream io.ReadWriteCloser) error {
|
||||
return a.Runtime.PortForward(podName, port, stream)
|
||||
}
|
469
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/server_test.go
generated
vendored
Normal file
469
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/server_test.go
generated
vendored
Normal file
@ -0,0 +1,469 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package streaming
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
"k8s.io/client-go/transport/spdy"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
|
||||
kubeletportforward "k8s.io/kubernetes/pkg/kubelet/server/portforward"
|
||||
)
|
||||
|
||||
const (
|
||||
testAddr = "localhost:12345"
|
||||
testContainerID = "container789"
|
||||
testPodSandboxID = "pod0987"
|
||||
)
|
||||
|
||||
func TestGetExec(t *testing.T) {
|
||||
serv, err := NewServer(Config{
|
||||
Addr: testAddr,
|
||||
}, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
tlsServer, err := NewServer(Config{
|
||||
Addr: testAddr,
|
||||
TLSConfig: &tls.Config{},
|
||||
}, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
const pathPrefix = "cri/shim"
|
||||
prefixServer, err := NewServer(Config{
|
||||
Addr: testAddr,
|
||||
BaseURL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: testAddr,
|
||||
Path: "/" + pathPrefix + "/",
|
||||
},
|
||||
}, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assertRequestToken := func(expectedReq *runtimeapi.ExecRequest, cache *requestCache, token string) {
|
||||
req, ok := cache.Consume(token)
|
||||
require.True(t, ok, "token %s not found!", token)
|
||||
assert.Equal(t, expectedReq, req)
|
||||
}
|
||||
request := &runtimeapi.ExecRequest{
|
||||
ContainerId: testContainerID,
|
||||
Cmd: []string{"echo", "foo"},
|
||||
Tty: true,
|
||||
Stdin: true,
|
||||
}
|
||||
{ // Non-TLS
|
||||
resp, err := serv.GetExec(request)
|
||||
assert.NoError(t, err)
|
||||
expectedURL := "http://" + testAddr + "/exec/"
|
||||
assert.Contains(t, resp.Url, expectedURL)
|
||||
token := strings.TrimPrefix(resp.Url, expectedURL)
|
||||
assertRequestToken(request, serv.(*server).cache, token)
|
||||
}
|
||||
|
||||
{ // TLS
|
||||
resp, err := tlsServer.GetExec(request)
|
||||
assert.NoError(t, err)
|
||||
expectedURL := "https://" + testAddr + "/exec/"
|
||||
assert.Contains(t, resp.Url, expectedURL)
|
||||
token := strings.TrimPrefix(resp.Url, expectedURL)
|
||||
assertRequestToken(request, tlsServer.(*server).cache, token)
|
||||
}
|
||||
|
||||
{ // Path prefix
|
||||
resp, err := prefixServer.GetExec(request)
|
||||
assert.NoError(t, err)
|
||||
expectedURL := "http://" + testAddr + "/" + pathPrefix + "/exec/"
|
||||
assert.Contains(t, resp.Url, expectedURL)
|
||||
token := strings.TrimPrefix(resp.Url, expectedURL)
|
||||
assertRequestToken(request, prefixServer.(*server).cache, token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExecAttachRequest(t *testing.T) {
|
||||
type config struct {
|
||||
tty bool
|
||||
stdin bool
|
||||
stdout bool
|
||||
stderr bool
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
configs []config
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
desc: "at least one stream must be true",
|
||||
expectErr: true,
|
||||
configs: []config{
|
||||
{false, false, false, false},
|
||||
{true, false, false, false}},
|
||||
},
|
||||
{
|
||||
desc: "tty and stderr cannot both be true",
|
||||
expectErr: true,
|
||||
configs: []config{
|
||||
{true, false, false, true},
|
||||
{true, false, true, true},
|
||||
{true, true, false, true},
|
||||
{true, true, true, true},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "a valid config should pass",
|
||||
expectErr: false,
|
||||
configs: []config{
|
||||
{false, false, false, true},
|
||||
{false, false, true, false},
|
||||
{false, false, true, true},
|
||||
{false, true, false, false},
|
||||
{false, true, false, true},
|
||||
{false, true, true, false},
|
||||
{false, true, true, true},
|
||||
{true, false, true, false},
|
||||
{true, true, false, false},
|
||||
{true, true, true, false},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
for _, c := range tc.configs {
|
||||
// validate the exec request.
|
||||
execReq := &runtimeapi.ExecRequest{
|
||||
ContainerId: testContainerID,
|
||||
Cmd: []string{"date"},
|
||||
Tty: c.tty,
|
||||
Stdin: c.stdin,
|
||||
Stdout: c.stdout,
|
||||
Stderr: c.stderr,
|
||||
}
|
||||
err := validateExecRequest(execReq)
|
||||
assert.Equal(t, tc.expectErr, err != nil, "config: %v, err: %v", c, err)
|
||||
|
||||
// validate the attach request.
|
||||
attachReq := &runtimeapi.AttachRequest{
|
||||
ContainerId: testContainerID,
|
||||
Tty: c.tty,
|
||||
Stdin: c.stdin,
|
||||
Stdout: c.stdout,
|
||||
Stderr: c.stderr,
|
||||
}
|
||||
err = validateAttachRequest(attachReq)
|
||||
assert.Equal(t, tc.expectErr, err != nil, "config: %v, err: %v", c, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAttach(t *testing.T) {
|
||||
serv, err := NewServer(Config{
|
||||
Addr: testAddr,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
tlsServer, err := NewServer(Config{
|
||||
Addr: testAddr,
|
||||
TLSConfig: &tls.Config{},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertRequestToken := func(expectedReq *runtimeapi.AttachRequest, cache *requestCache, token string) {
|
||||
req, ok := cache.Consume(token)
|
||||
require.True(t, ok, "token %s not found!", token)
|
||||
assert.Equal(t, expectedReq, req)
|
||||
}
|
||||
|
||||
request := &runtimeapi.AttachRequest{
|
||||
ContainerId: testContainerID,
|
||||
Stdin: true,
|
||||
Tty: true,
|
||||
}
|
||||
{ // Non-TLS
|
||||
resp, err := serv.GetAttach(request)
|
||||
assert.NoError(t, err)
|
||||
expectedURL := "http://" + testAddr + "/attach/"
|
||||
assert.Contains(t, resp.Url, expectedURL)
|
||||
token := strings.TrimPrefix(resp.Url, expectedURL)
|
||||
assertRequestToken(request, serv.(*server).cache, token)
|
||||
}
|
||||
|
||||
{ // TLS
|
||||
resp, err := tlsServer.GetAttach(request)
|
||||
assert.NoError(t, err)
|
||||
expectedURL := "https://" + testAddr + "/attach/"
|
||||
assert.Contains(t, resp.Url, expectedURL)
|
||||
token := strings.TrimPrefix(resp.Url, expectedURL)
|
||||
assertRequestToken(request, tlsServer.(*server).cache, token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPortForward(t *testing.T) {
|
||||
podSandboxID := testPodSandboxID
|
||||
request := &runtimeapi.PortForwardRequest{
|
||||
PodSandboxId: podSandboxID,
|
||||
Port: []int32{1, 2, 3, 4},
|
||||
}
|
||||
|
||||
{ // Non-TLS
|
||||
serv, err := NewServer(Config{
|
||||
Addr: testAddr,
|
||||
}, nil)
|
||||
assert.NoError(t, err)
|
||||
resp, err := serv.GetPortForward(request)
|
||||
assert.NoError(t, err)
|
||||
expectedURL := "http://" + testAddr + "/portforward/"
|
||||
assert.True(t, strings.HasPrefix(resp.Url, expectedURL))
|
||||
token := strings.TrimPrefix(resp.Url, expectedURL)
|
||||
req, ok := serv.(*server).cache.Consume(token)
|
||||
require.True(t, ok, "token %s not found!", token)
|
||||
assert.Equal(t, testPodSandboxID, req.(*runtimeapi.PortForwardRequest).PodSandboxId)
|
||||
}
|
||||
|
||||
{ // TLS
|
||||
tlsServer, err := NewServer(Config{
|
||||
Addr: testAddr,
|
||||
TLSConfig: &tls.Config{},
|
||||
}, nil)
|
||||
assert.NoError(t, err)
|
||||
resp, err := tlsServer.GetPortForward(request)
|
||||
assert.NoError(t, err)
|
||||
expectedURL := "https://" + testAddr + "/portforward/"
|
||||
assert.True(t, strings.HasPrefix(resp.Url, expectedURL))
|
||||
token := strings.TrimPrefix(resp.Url, expectedURL)
|
||||
req, ok := tlsServer.(*server).cache.Consume(token)
|
||||
require.True(t, ok, "token %s not found!", token)
|
||||
assert.Equal(t, testPodSandboxID, req.(*runtimeapi.PortForwardRequest).PodSandboxId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeExec(t *testing.T) {
|
||||
runRemoteCommandTest(t, "exec")
|
||||
}
|
||||
|
||||
func TestServeAttach(t *testing.T) {
|
||||
runRemoteCommandTest(t, "attach")
|
||||
}
|
||||
|
||||
func TestServePortForward(t *testing.T) {
|
||||
s, testServer := startTestServer(t)
|
||||
defer testServer.Close()
|
||||
|
||||
resp, err := s.GetPortForward(&runtimeapi.PortForwardRequest{
|
||||
PodSandboxId: testPodSandboxID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
reqURL, err := url.Parse(resp.Url)
|
||||
require.NoError(t, err)
|
||||
|
||||
transport, upgrader, err := spdy.RoundTripperFor(&restclient.Config{})
|
||||
require.NoError(t, err)
|
||||
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", reqURL)
|
||||
streamConn, _, err := dialer.Dial(kubeletportforward.ProtocolV1Name)
|
||||
require.NoError(t, err)
|
||||
defer streamConn.Close()
|
||||
|
||||
// Create the streams.
|
||||
headers := http.Header{}
|
||||
// Error stream is required, but unused in this test.
|
||||
headers.Set(api.StreamType, api.StreamTypeError)
|
||||
headers.Set(api.PortHeader, strconv.Itoa(testPort))
|
||||
_, err = streamConn.CreateStream(headers)
|
||||
require.NoError(t, err)
|
||||
// Setup the data stream.
|
||||
headers.Set(api.StreamType, api.StreamTypeData)
|
||||
headers.Set(api.PortHeader, strconv.Itoa(testPort))
|
||||
stream, err := streamConn.CreateStream(headers)
|
||||
require.NoError(t, err)
|
||||
|
||||
doClientStreams(t, "portforward", stream, stream, nil)
|
||||
}
|
||||
|
||||
//
|
||||
// Run the remote command test.
|
||||
// commandType is either "exec" or "attach".
|
||||
func runRemoteCommandTest(t *testing.T, commandType string) {
|
||||
s, testServer := startTestServer(t)
|
||||
defer testServer.Close()
|
||||
|
||||
var reqURL *url.URL
|
||||
stdin, stdout, stderr := true, true, true
|
||||
containerID := testContainerID
|
||||
switch commandType {
|
||||
case "exec":
|
||||
resp, err := s.GetExec(&runtimeapi.ExecRequest{
|
||||
ContainerId: containerID,
|
||||
Cmd: []string{"echo"},
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
reqURL, err = url.Parse(resp.Url)
|
||||
require.NoError(t, err)
|
||||
case "attach":
|
||||
resp, err := s.GetAttach(&runtimeapi.AttachRequest{
|
||||
ContainerId: containerID,
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
reqURL, err = url.Parse(resp.Url)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
stdinR, stdinW := io.Pipe()
|
||||
stdoutR, stdoutW := io.Pipe()
|
||||
stderrR, stderrW := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
exec, err := remotecommand.NewSPDYExecutor(&restclient.Config{}, "POST", reqURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := remotecommand.StreamOptions{
|
||||
Stdin: stdinR,
|
||||
Stdout: stdoutW,
|
||||
Stderr: stderrW,
|
||||
Tty: false,
|
||||
}
|
||||
require.NoError(t, exec.Stream(opts))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
doClientStreams(t, commandType, stdinW, stdoutR, stderrR)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Repeat request with the same URL should be a 404.
|
||||
resp, err := http.Get(reqURL.String())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
}
|
||||
|
||||
func startTestServer(t *testing.T) (Server, *httptest.Server) {
|
||||
var s Server
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
s.ServeHTTP(w, r)
|
||||
}))
|
||||
cleanup := true
|
||||
defer func() {
|
||||
if cleanup {
|
||||
testServer.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
testURL, err := url.Parse(testServer.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
rt := newFakeRuntime(t)
|
||||
config := DefaultConfig
|
||||
config.BaseURL = testURL
|
||||
s, err = NewServer(config, rt)
|
||||
require.NoError(t, err)
|
||||
|
||||
cleanup = false // Caller must close the test server.
|
||||
return s, testServer
|
||||
}
|
||||
|
||||
const (
|
||||
testInput = "abcdefg"
|
||||
testOutput = "fooBARbaz"
|
||||
testErr = "ERROR!!!"
|
||||
testPort = 12345
|
||||
)
|
||||
|
||||
func newFakeRuntime(t *testing.T) *fakeRuntime {
|
||||
return &fakeRuntime{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRuntime struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (f *fakeRuntime) Exec(containerID string, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
|
||||
assert.Equal(f.t, testContainerID, containerID)
|
||||
doServerStreams(f.t, "exec", stdin, stdout, stderr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuntime) Attach(containerID string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
|
||||
assert.Equal(f.t, testContainerID, containerID)
|
||||
doServerStreams(f.t, "attach", stdin, stdout, stderr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRuntime) PortForward(podSandboxID string, port int32, stream io.ReadWriteCloser) error {
|
||||
assert.Equal(f.t, testPodSandboxID, podSandboxID)
|
||||
assert.EqualValues(f.t, testPort, port)
|
||||
doServerStreams(f.t, "portforward", stream, stream, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send & receive expected input/output. Must be the inverse of doClientStreams.
|
||||
// Function will block until the expected i/o is finished.
|
||||
func doServerStreams(t *testing.T, prefix string, stdin io.Reader, stdout, stderr io.Writer) {
|
||||
if stderr != nil {
|
||||
writeExpected(t, "server stderr", stderr, prefix+testErr)
|
||||
}
|
||||
readExpected(t, "server stdin", stdin, prefix+testInput)
|
||||
writeExpected(t, "server stdout", stdout, prefix+testOutput)
|
||||
}
|
||||
|
||||
// Send & receive expected input/output. Must be the inverse of doServerStreams.
|
||||
// Function will block until the expected i/o is finished.
|
||||
func doClientStreams(t *testing.T, prefix string, stdin io.Writer, stdout, stderr io.Reader) {
|
||||
if stderr != nil {
|
||||
readExpected(t, "client stderr", stderr, prefix+testErr)
|
||||
}
|
||||
writeExpected(t, "client stdin", stdin, prefix+testInput)
|
||||
readExpected(t, "client stdout", stdout, prefix+testOutput)
|
||||
}
|
||||
|
||||
// Read and verify the expected string from the stream.
|
||||
func readExpected(t *testing.T, streamName string, r io.Reader, expected string) {
|
||||
result := make([]byte, len(expected))
|
||||
_, err := io.ReadAtLeast(r, result, len(expected))
|
||||
assert.NoError(t, err, "stream %s", streamName)
|
||||
assert.Equal(t, expected, string(result), "stream %s", streamName)
|
||||
}
|
||||
|
||||
// Write and verify success of the data over the stream.
|
||||
func writeExpected(t *testing.T, streamName string, w io.Writer, data string) {
|
||||
n, err := io.WriteString(w, data)
|
||||
assert.NoError(t, err, "stream %s", streamName)
|
||||
assert.Equal(t, len(data), n, "stream %s", streamName)
|
||||
}
|
Reference in New Issue
Block a user