Switch to github.com/golang/dep for vendoring
Signed-off-by: Mrunal Patel <mrunalp@gmail.com>
This commit is contained in:
parent
d6ab91be27
commit
8e5b17cf13
15431 changed files with 3971413 additions and 8881 deletions
104
vendor/k8s.io/kubernetes/pkg/kubelet/server/BUILD
generated
vendored
Normal file
104
vendor/k8s.io/kubernetes/pkg/kubelet/server/BUILD
generated
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
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",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/api/v1:go_default_library",
|
||||
"//pkg/api/v1/validation:go_default_library",
|
||||
"//pkg/kubelet/cm: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/util/configz:go_default_library",
|
||||
"//pkg/util/limitwriter:go_default_library",
|
||||
"//pkg/util/term:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//vendor:github.com/emicklei/go-restful",
|
||||
"//vendor:github.com/golang/glog",
|
||||
"//vendor:github.com/google/cadvisor/info/v1",
|
||||
"//vendor:github.com/google/cadvisor/info/v2",
|
||||
"//vendor:github.com/prometheus/client_golang/prometheus",
|
||||
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
|
||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/runtime",
|
||||
"//vendor:k8s.io/apiserver/pkg/authentication/authenticator",
|
||||
"//vendor:k8s.io/apiserver/pkg/authentication/user",
|
||||
"//vendor:k8s.io/apiserver/pkg/authorization/authorizer",
|
||||
"//vendor:k8s.io/apiserver/pkg/healthz",
|
||||
"//vendor:k8s.io/apiserver/pkg/httplog",
|
||||
"//vendor:k8s.io/apiserver/pkg/util/flushwriter",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"auth_test.go",
|
||||
"server_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/api/v1: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/remotecommand:go_default_library",
|
||||
"//pkg/kubelet/server/stats:go_default_library",
|
||||
"//pkg/kubelet/types:go_default_library",
|
||||
"//pkg/util/httpstream:go_default_library",
|
||||
"//pkg/util/httpstream/spdy:go_default_library",
|
||||
"//pkg/util/term:go_default_library",
|
||||
"//pkg/util/testing:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//vendor:github.com/google/cadvisor/info/v1",
|
||||
"//vendor:github.com/google/cadvisor/info/v2",
|
||||
"//vendor:github.com/stretchr/testify/assert",
|
||||
"//vendor:github.com/stretchr/testify/require",
|
||||
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/sets",
|
||||
"//vendor:k8s.io/apiserver/pkg/authentication/user",
|
||||
"//vendor:k8s.io/apiserver/pkg/authorization/authorizer",
|
||||
],
|
||||
)
|
||||
|
||||
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"
|
50
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/BUILD
generated
vendored
Normal file
50
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/BUILD
generated
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"constants.go",
|
||||
"portforward.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/util/httpstream:go_default_library",
|
||||
"//pkg/util/httpstream/spdy:go_default_library",
|
||||
"//vendor:github.com/golang/glog",
|
||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/runtime",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["portforward_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//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"],
|
||||
)
|
21
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/constants.go
generated
vendored
Normal file
21
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/constants.go
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
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 PortForwardProtocolV1Name = "portforward.k8s.io"
|
323
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/portforward.go
generated
vendored
Normal file
323
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/portforward.go
generated
vendored
Normal file
|
@ -0,0 +1,323 @@
|
|||
/*
|
||||
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"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/util/httpstream"
|
||||
"k8s.io/kubernetes/pkg/util/httpstream/spdy"
|
||||
)
|
||||
|
||||
// PortForwarder knows how to forward content from a data stream to/from a port
|
||||
// in a pod.
|
||||
type PortForwarder interface {
|
||||
// PortForwarder copies data between a data stream and a port in a pod.
|
||||
PortForward(name string, uid types.UID, port uint16, stream io.ReadWriteCloser) error
|
||||
}
|
||||
|
||||
// ServePortForward handles a port forwarding request. A single request is
|
||||
// kept alive as long as the client is still alive and the connection has not
|
||||
// been timed out due to idleness. This function handles multiple forwarded
|
||||
// connections; i.e., multiple `curl http://localhost:8888/` requests will be
|
||||
// handled by a single invocation of ServePortForward.
|
||||
func ServePortForward(w http.ResponseWriter, req *http.Request, portForwarder PortForwarder, podName string, uid types.UID, idleTimeout time.Duration, streamCreationTimeout time.Duration) {
|
||||
supportedPortForwardProtocols := []string{PortForwardProtocolV1Name}
|
||||
_, 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
|
||||
utilruntime.HandleError(err)
|
||||
return
|
||||
}
|
||||
|
||||
streamChan := make(chan httpstream.Stream, 1)
|
||||
|
||||
glog.V(5).Infof("Upgrading port forward response")
|
||||
upgrader := spdy.NewResponseUpgrader()
|
||||
conn := upgrader.UpgradeResponse(w, req, portForwardStreamReceived(streamChan))
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
glog.V(5).Infof("(conn=%p) setting port forwarding streaming connection idle timeout to %v", conn, idleTimeout)
|
||||
conn.SetIdleTimeout(idleTimeout)
|
||||
|
||||
h := &portForwardStreamHandler{
|
||||
conn: conn,
|
||||
streamChan: streamChan,
|
||||
streamPairs: make(map[string]*portForwardStreamPair),
|
||||
streamCreationTimeout: streamCreationTimeout,
|
||||
pod: podName,
|
||||
uid: uid,
|
||||
forwarder: portForwarder,
|
||||
}
|
||||
h.run()
|
||||
}
|
||||
|
||||
// portForwardStreamReceived is the httpstream.NewStreamHandler for port
|
||||
// forward streams. It checks each stream's port and stream type headers,
|
||||
// rejecting any streams that with missing or invalid values. Each valid
|
||||
// stream is sent to the streams channel.
|
||||
func portForwardStreamReceived(streams chan httpstream.Stream) func(httpstream.Stream, <-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
|
||||
}
|
||||
}
|
||||
|
||||
// portForwardStreamHandler is capable of processing multiple port forward
|
||||
// requests over a single httpstream.Connection.
|
||||
type portForwardStreamHandler struct {
|
||||
conn httpstream.Connection
|
||||
streamChan chan httpstream.Stream
|
||||
streamPairsLock sync.RWMutex
|
||||
streamPairs map[string]*portForwardStreamPair
|
||||
streamCreationTimeout time.Duration
|
||||
pod string
|
||||
uid types.UID
|
||||
forwarder PortForwarder
|
||||
}
|
||||
|
||||
// getStreamPair returns a portForwardStreamPair for requestID. This creates a
|
||||
// new pair if one does not yet exist for the requestID. The returned bool is
|
||||
// true if the pair was created.
|
||||
func (h *portForwardStreamHandler) getStreamPair(requestID string) (*portForwardStreamPair, bool) {
|
||||
h.streamPairsLock.Lock()
|
||||
defer h.streamPairsLock.Unlock()
|
||||
|
||||
if p, ok := h.streamPairs[requestID]; ok {
|
||||
glog.V(5).Infof("(conn=%p, request=%s) found existing stream pair", h.conn, requestID)
|
||||
return p, false
|
||||
}
|
||||
|
||||
glog.V(5).Infof("(conn=%p, request=%s) creating new stream pair", h.conn, requestID)
|
||||
|
||||
p := newPortForwardPair(requestID)
|
||||
h.streamPairs[requestID] = p
|
||||
|
||||
return p, true
|
||||
}
|
||||
|
||||
// monitorStreamPair waits for the pair to receive both its error and data
|
||||
// streams, or for the timeout to expire (whichever happens first), and then
|
||||
// removes the pair.
|
||||
func (h *portForwardStreamHandler) monitorStreamPair(p *portForwardStreamPair, timeout <-chan time.Time) {
|
||||
select {
|
||||
case <-timeout:
|
||||
err := fmt.Errorf("(conn=%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 *portForwardStreamHandler) hasStreamPair(requestID string) bool {
|
||||
h.streamPairsLock.RLock()
|
||||
defer h.streamPairsLock.RUnlock()
|
||||
|
||||
_, ok := h.streamPairs[requestID]
|
||||
return ok
|
||||
}
|
||||
|
||||
// removeStreamPair removes the stream pair identified by requestID from streamPairs.
|
||||
func (h *portForwardStreamHandler) removeStreamPair(requestID string) {
|
||||
h.streamPairsLock.Lock()
|
||||
defer h.streamPairsLock.Unlock()
|
||||
|
||||
delete(h.streamPairs, requestID)
|
||||
}
|
||||
|
||||
// requestID returns the request id for stream.
|
||||
func (h *portForwardStreamHandler) requestID(stream httpstream.Stream) string {
|
||||
requestID := stream.Headers().Get(api.PortForwardRequestIDHeader)
|
||||
if len(requestID) == 0 {
|
||||
glog.V(5).Infof("(conn=%p) stream received without %s header", h.conn, api.PortForwardRequestIDHeader)
|
||||
// If we get here, it's because the connection came from an older client
|
||||
// that isn't generating the request id header
|
||||
// (https://github.com/kubernetes/kubernetes/blob/843134885e7e0b360eb5441e85b1410a8b1a7a0c/pkg/client/unversioned/portforward/portforward.go#L258-L287)
|
||||
//
|
||||
// This is a best-effort attempt at supporting older clients.
|
||||
//
|
||||
// When there aren't concurrent new forwarded connections, each connection
|
||||
// will have a pair of streams (data, error), and the stream IDs will be
|
||||
// consecutive odd numbers, e.g. 1 and 3 for the first connection. Convert
|
||||
// the stream ID into a pseudo-request id by taking the stream type and
|
||||
// using id = stream.Identifier() when the stream type is error,
|
||||
// and id = stream.Identifier() - 2 when it's data.
|
||||
//
|
||||
// NOTE: this only works when there are not concurrent new streams from
|
||||
// multiple forwarded connections; it's a best-effort attempt at supporting
|
||||
// old clients that don't generate request ids. If there are concurrent
|
||||
// new connections, it's possible that 1 connection gets streams whose IDs
|
||||
// are not consecutive (e.g. 5 and 9 instead of 5 and 7).
|
||||
streamType := stream.Headers().Get(api.StreamType)
|
||||
switch streamType {
|
||||
case api.StreamTypeError:
|
||||
requestID = strconv.Itoa(int(stream.Identifier()))
|
||||
case api.StreamTypeData:
|
||||
requestID = strconv.Itoa(int(stream.Identifier()) - 2)
|
||||
}
|
||||
|
||||
glog.V(5).Infof("(conn=%p) automatically assigning request ID=%q from stream type=%s, stream ID=%d", h.conn, requestID, streamType, stream.Identifier())
|
||||
}
|
||||
return requestID
|
||||
}
|
||||
|
||||
// run is the main loop for the portForwardStreamHandler. It processes new
|
||||
// streams, invoking portForward for each complete stream pair. The loop exits
|
||||
// when the httpstream.Connection is closed.
|
||||
func (h *portForwardStreamHandler) run() {
|
||||
glog.V(5).Infof("(conn=%p) waiting for port forward streams", h.conn)
|
||||
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 portForwardStreamHandler's forwarder.PortForward
|
||||
// function for the given stream pair.
|
||||
func (h *portForwardStreamHandler) portForward(p *portForwardStreamPair) {
|
||||
defer p.dataStream.Close()
|
||||
defer p.errorStream.Close()
|
||||
|
||||
portString := p.dataStream.Headers().Get(api.PortHeader)
|
||||
port, _ := strconv.ParseUint(portString, 10, 16)
|
||||
|
||||
glog.V(5).Infof("(conn=%p, request=%s) invoking forwarder.PortForward for port %s", h.conn, p.requestID, portString)
|
||||
err := h.forwarder.PortForward(h.pod, h.uid, uint16(port), p.dataStream)
|
||||
glog.V(5).Infof("(conn=%p, request=%s) done invoking forwarder.PortForward for port %s", h.conn, p.requestID, portString)
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// portForwardStreamPair represents the error and data streams for a port
|
||||
// forwarding request.
|
||||
type portForwardStreamPair struct {
|
||||
lock sync.RWMutex
|
||||
requestID string
|
||||
dataStream httpstream.Stream
|
||||
errorStream httpstream.Stream
|
||||
complete chan struct{}
|
||||
}
|
||||
|
||||
// newPortForwardPair creates a new portForwardStreamPair.
|
||||
func newPortForwardPair(requestID string) *portForwardStreamPair {
|
||||
return &portForwardStreamPair{
|
||||
requestID: requestID,
|
||||
complete: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// add adds the stream to the portForwardStreamPair. If the pair already
|
||||
// contains a stream for the new stream's type, an error is returned. add
|
||||
// returns true if both the data and error streams for this pair have been
|
||||
// received.
|
||||
func (p *portForwardStreamPair) add(stream httpstream.Stream) (bool, error) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
switch stream.Headers().Get(api.StreamType) {
|
||||
case api.StreamTypeError:
|
||||
if p.errorStream != nil {
|
||||
return false, errors.New("error stream already assigned")
|
||||
}
|
||||
p.errorStream = stream
|
||||
case api.StreamTypeData:
|
||||
if p.dataStream != nil {
|
||||
return false, errors.New("data stream already assigned")
|
||||
}
|
||||
p.dataStream = stream
|
||||
}
|
||||
|
||||
complete := p.errorStream != nil && p.dataStream != nil
|
||||
if complete {
|
||||
close(p.complete)
|
||||
}
|
||||
return complete, nil
|
||||
}
|
||||
|
||||
// printError writes s to p.errorStream if p.errorStream has been set.
|
||||
func (p *portForwardStreamPair) printError(s string) {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
if p.errorStream != nil {
|
||||
fmt.Fprint(p.errorStream, s)
|
||||
}
|
||||
}
|
246
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/portforward_test.go
generated
vendored
Normal file
246
vendor/k8s.io/kubernetes/pkg/kubelet/server/portforward/portforward_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/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/util/httpstream"
|
||||
)
|
||||
|
||||
func TestPortForwardStreamReceived(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 := portForwardStreamReceived(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func TestGetStreamPair(t *testing.T) {
|
||||
timeout := make(chan time.Time)
|
||||
|
||||
h := &portForwardStreamHandler{
|
||||
streamPairs: make(map[string]*portForwardStreamPair),
|
||||
}
|
||||
|
||||
// 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 := &portForwardStreamHandler{}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
48
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/BUILD
generated
vendored
Normal file
48
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/BUILD
generated
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"attach.go",
|
||||
"constants.go",
|
||||
"doc.go",
|
||||
"exec.go",
|
||||
"httpstream.go",
|
||||
"websocket.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/util/exec:go_default_library",
|
||||
"//pkg/util/httpstream:go_default_library",
|
||||
"//pkg/util/httpstream/spdy:go_default_library",
|
||||
"//pkg/util/term:go_default_library",
|
||||
"//vendor:github.com/golang/glog",
|
||||
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/runtime",
|
||||
"//vendor:k8s.io/apiserver/pkg/httplog",
|
||||
"//vendor:k8s.io/apiserver/pkg/util/wsstream",
|
||||
],
|
||||
)
|
||||
|
||||
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/kubernetes/pkg/util/term"
|
||||
)
|
||||
|
||||
// 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 term.Size) 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,
|
||||
}})
|
||||
}
|
||||
}
|
46
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/constants.go
generated
vendored
Normal file
46
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/constants.go
generated
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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 "time"
|
||||
|
||||
const (
|
||||
DefaultStreamCreationTimeout = 30 * time.Second
|
||||
|
||||
// The SPDY subprotocol "channel.k8s.io" is used for remote command
|
||||
// attachment/execution. This represents the initial unversioned subprotocol,
|
||||
// which has the known bugs http://issues.k8s.io/13394 and
|
||||
// http://issues.k8s.io/13395.
|
||||
StreamProtocolV1Name = "channel.k8s.io"
|
||||
|
||||
// The SPDY subprotocol "v2.channel.k8s.io" is used for remote command
|
||||
// attachment/execution. It is the second version of the subprotocol and
|
||||
// resolves the issues present in the first version.
|
||||
StreamProtocolV2Name = "v2.channel.k8s.io"
|
||||
|
||||
// The SPDY subprotocol "v3.channel.k8s.io" is used for remote command
|
||||
// attachment/execution. It is the third version of the subprotocol and
|
||||
// adds support for resizing container terminals.
|
||||
StreamProtocolV3Name = "v3.channel.k8s.io"
|
||||
|
||||
// The SPDY subprotocol "v4.channel.k8s.io" is used for remote command
|
||||
// attachment/execution. It is the 4th version of the subprotocol and
|
||||
// adds support for exit codes.
|
||||
StreamProtocolV4Name = "v4.channel.k8s.io"
|
||||
)
|
||||
|
||||
var SupportedStreamingProtocols = []string{StreamProtocolV4Name, StreamProtocolV3Name, StreamProtocolV2Name, StreamProtocolV1Name}
|
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"
|
83
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/exec.go
generated
vendored
Normal file
83
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/exec.go
generated
vendored
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
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"
|
||||
utilexec "k8s.io/kubernetes/pkg/util/exec"
|
||||
"k8s.io/kubernetes/pkg/util/term"
|
||||
)
|
||||
|
||||
const (
|
||||
NonZeroExitCodeReason = metav1.StatusReason("NonZeroExitCode")
|
||||
ExitCodeCauseType = metav1.CauseType("ExitCode")
|
||||
)
|
||||
|
||||
// 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 term.Size, 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: NonZeroExitCodeReason,
|
||||
Details: &metav1.StatusDetails{
|
||||
Causes: []metav1.StatusCause{
|
||||
{
|
||||
Type: 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,
|
||||
}})
|
||||
}
|
||||
}
|
446
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/httpstream.go
generated
vendored
Normal file
446
vendor/k8s.io/kubernetes/pkg/kubelet/server/remotecommand/httpstream.go
generated
vendored
Normal file
|
@ -0,0 +1,446 @@
|
|||
/*
|
||||
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/runtime"
|
||||
"k8s.io/apiserver/pkg/util/wsstream"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/util/httpstream"
|
||||
"k8s.io/kubernetes/pkg/util/httpstream/spdy"
|
||||
"k8s.io/kubernetes/pkg/util/term"
|
||||
|
||||
"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 term.Size
|
||||
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 term.Size)
|
||||
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 StreamProtocolV4Name:
|
||||
handler = &v4ProtocolHandler{}
|
||||
case StreamProtocolV3Name:
|
||||
handler = &v3ProtocolHandler{}
|
||||
case StreamProtocolV2Name:
|
||||
handler = &v2ProtocolHandler{}
|
||||
case "":
|
||||
glog.V(4).Infof("Client did not request protocol negotiaion. Falling back to %q", StreamProtocolV1Name)
|
||||
fallthrough
|
||||
case 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<- term.Size) {
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
decoder := json.NewDecoder(stream)
|
||||
for {
|
||||
size := term.Size{}
|
||||
if err := decoder.Decode(&size); err != nil {
|
||||
break
|
||||
}
|
||||
channel <- size
|
||||
}
|
||||
}
|
||||
|
||||
func v1WriteStatusFunc(stream io.WriteCloser) 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.WriteCloser) 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/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
|
||||
}
|
740
vendor/k8s.io/kubernetes/pkg/kubelet/server/server.go
generated
vendored
Normal file
740
vendor/k8s.io/kubernetes/pkg/kubelet/server/server.go
generated
vendored
Normal file
|
@ -0,0 +1,740 @@
|
|||
/*
|
||||
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"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"github.com/golang/glog"
|
||||
cadvisorapi "github.com/google/cadvisor/info/v1"
|
||||
cadvisorapiv2 "github.com/google/cadvisor/info/v2"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/healthz"
|
||||
"k8s.io/apiserver/pkg/httplog"
|
||||
"k8s.io/apiserver/pkg/util/flushwriter"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/pkg/api/v1/validation"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/portforward"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/stats"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/streaming"
|
||||
"k8s.io/kubernetes/pkg/util/configz"
|
||||
"k8s.io/kubernetes/pkg/util/limitwriter"
|
||||
"k8s.io/kubernetes/pkg/util/term"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
)
|
||||
|
||||
const (
|
||||
metricsPath = "/metrics"
|
||||
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 bool,
|
||||
runtime kubecontainer.Runtime,
|
||||
criHandler http.Handler) {
|
||||
glog.Infof("Starting to listen on %s:%d", address, port)
|
||||
handler := NewServer(host, resourceAnalyzer, auth, enableDebuggingHandlers, 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
|
||||
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, 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 testablitiy.
|
||||
type HostInterface interface {
|
||||
GetContainerInfo(podFullName string, uid types.UID, containerName string, req *cadvisorapi.ContainerInfoRequest) (*cadvisorapi.ContainerInfo, error)
|
||||
GetContainerInfoV2(name string, options cadvisorapiv2.RequestOptions) (map[string]cadvisorapiv2.ContainerInfo, error)
|
||||
GetRawContainerInfo(containerName string, req *cadvisorapi.ContainerInfoRequest, subcontainers bool) (map[string]*cadvisorapi.ContainerInfo, error)
|
||||
GetCachedMachineInfo() (*cadvisorapi.MachineInfo, error)
|
||||
GetPods() []*v1.Pod
|
||||
GetRunningPods() ([]*v1.Pod, error)
|
||||
GetPodByName(namespace, name string) (*v1.Pod, bool)
|
||||
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 term.Size, timeout time.Duration) error
|
||||
AttachContainer(name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan term.Size) 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 uint16, stream io.ReadWriteCloser) error
|
||||
StreamingConnectionIdleTimeout() time.Duration
|
||||
ResyncInterval() time.Duration
|
||||
GetHostname() string
|
||||
GetNode() (*v1.Node, error)
|
||||
GetNodeConfig() cm.NodeConfig
|
||||
LatestLoopEntryTime() time.Time
|
||||
ImagesFsInfo() (cadvisorapiv2.FsInfo, error)
|
||||
RootFsInfo() (cadvisorapiv2.FsInfo, error)
|
||||
ListVolumesForPod(podUID types.UID) (map[string]volume.Volume, bool)
|
||||
GetExec(podFullName string, podUID types.UID, containerName string, cmd []string, streamOpts remotecommand.Options) (*url.URL, error)
|
||||
GetAttach(podFullName string, podUID types.UID, containerName string, streamOpts remotecommand.Options) (*url.URL, error)
|
||||
GetPortForward(podName, podNamespace string, podUID types.UID) (*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 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)
|
||||
}
|
||||
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
|
||||
authorized, _, 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 !authorized {
|
||||
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),
|
||||
)
|
||||
var ws *restful.WebService
|
||||
ws = new(restful.WebService)
|
||||
ws.
|
||||
Path("/pods").
|
||||
Produces(restful.MIME_JSON)
|
||||
ws.Route(ws.GET("").
|
||||
To(s.getPods).
|
||||
Operation("getPods"))
|
||||
s.restfulCont.Add(ws)
|
||||
|
||||
s.restfulCont.Add(stats.CreateHandlers(statsPath, s.host, s.resourceAnalyzer))
|
||||
s.restfulCont.Handle(metricsPath, prometheus.Handler())
|
||||
|
||||
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/"
|
||||
|
||||
// InstallDeguggingHandlers registers the HTTP request patterns that serve logs or run commands/containers
|
||||
func (s *Server) InstallDebuggingHandlers(criHandler http.Handler) {
|
||||
var ws *restful.WebService
|
||||
|
||||
ws = new(restful.WebService)
|
||||
ws.
|
||||
Path("/run")
|
||||
ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
|
||||
To(s.getRun).
|
||||
Operation("getRun"))
|
||||
ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}/{containerName}").
|
||||
To(s.getRun).
|
||||
Operation("getRun"))
|
||||
s.restfulCont.Add(ws)
|
||||
|
||||
ws = new(restful.WebService)
|
||||
ws.
|
||||
Path("/exec")
|
||||
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.POST("/{podNamespace}/{podID}").
|
||||
To(s.getPortForward).
|
||||
Operation("getPortForward"))
|
||||
ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}").
|
||||
To(s.getPortForward).
|
||||
Operation("getPortForward"))
|
||||
s.restfulCont.Add(ws)
|
||||
|
||||
ws = new(restful.WebService)
|
||||
ws.
|
||||
Path(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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := api.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(apierrs.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 := api.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 requestParams struct {
|
||||
podNamespace string
|
||||
podName string
|
||||
podUID types.UID
|
||||
containerName string
|
||||
cmd []string
|
||||
}
|
||||
|
||||
func getRequestParams(req *restful.Request) requestParams {
|
||||
return requestParams{
|
||||
podNamespace: req.PathParameter("podNamespace"),
|
||||
podName: req.PathParameter("podID"),
|
||||
podUID: types.UID(req.PathParameter("uid")),
|
||||
containerName: req.PathParameter("containerName"),
|
||||
cmd: req.Request.URL.Query()[api.ExecCommandParamm],
|
||||
}
|
||||
}
|
||||
|
||||
// getAttach handles requests to attach to a container.
|
||||
func (s *Server) getAttach(request *restful.Request, response *restful.Response) {
|
||||
params := getRequestParams(request)
|
||||
streamOpts, err := remotecommand.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
|
||||
}
|
||||
|
||||
remotecommand.ServeAttach(response.ResponseWriter,
|
||||
request.Request,
|
||||
s.host,
|
||||
podFullName,
|
||||
params.podUID,
|
||||
params.containerName,
|
||||
streamOpts,
|
||||
s.host.StreamingConnectionIdleTimeout(),
|
||||
remotecommand.DefaultStreamCreationTimeout,
|
||||
remotecommand.SupportedStreamingProtocols)
|
||||
}
|
||||
|
||||
// getExec handles requests to run a command inside a container.
|
||||
func (s *Server) getExec(request *restful.Request, response *restful.Response) {
|
||||
params := getRequestParams(request)
|
||||
streamOpts, err := remotecommand.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
|
||||
}
|
||||
|
||||
remotecommand.ServeExec(response.ResponseWriter,
|
||||
request.Request,
|
||||
s.host,
|
||||
podFullName,
|
||||
params.podUID,
|
||||
params.containerName,
|
||||
params.cmd,
|
||||
streamOpts,
|
||||
s.host.StreamingConnectionIdleTimeout(),
|
||||
remotecommand.DefaultStreamCreationTimeout,
|
||||
remotecommand.SupportedStreamingProtocols)
|
||||
}
|
||||
|
||||
// getRun handles requests to run a command inside a container.
|
||||
func (s *Server) getRun(request *restful.Request, response *restful.Response) {
|
||||
params := getRequestParams(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 := getRequestParams(request)
|
||||
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)
|
||||
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,
|
||||
s.host.StreamingConnectionIdleTimeout(),
|
||||
remotecommand.DefaultStreamCreationTimeout)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
1641
vendor/k8s.io/kubernetes/pkg/kubelet/server/server_test.go
generated
vendored
Normal file
1641
vendor/k8s.io/kubernetes/pkg/kubelet/server/server_test.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
78
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/BUILD
generated
vendored
Normal file
78
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/BUILD
generated
vendored
Normal file
|
@ -0,0 +1,78 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
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",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api/v1:go_default_library",
|
||||
"//pkg/kubelet/api/v1alpha1/stats:go_default_library",
|
||||
"//pkg/kubelet/cm:go_default_library",
|
||||
"//pkg/kubelet/container:go_default_library",
|
||||
"//pkg/kubelet/leaky:go_default_library",
|
||||
"//pkg/kubelet/network:go_default_library",
|
||||
"//pkg/kubelet/types:go_default_library",
|
||||
"//pkg/kubelet/util/format:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//vendor:github.com/emicklei/go-restful",
|
||||
"//vendor:github.com/golang/glog",
|
||||
"//vendor:github.com/google/cadvisor/info/v1",
|
||||
"//vendor:github.com/google/cadvisor/info/v2",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/wait",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"mocks_test.go",
|
||||
"summary_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api/v1:go_default_library",
|
||||
"//pkg/kubelet/api/v1alpha1/stats:go_default_library",
|
||||
"//pkg/kubelet/cm:go_default_library",
|
||||
"//pkg/kubelet/container:go_default_library",
|
||||
"//pkg/kubelet/leaky:go_default_library",
|
||||
"//pkg/volume:go_default_library",
|
||||
"//vendor:github.com/google/cadvisor/info/v1",
|
||||
"//vendor:github.com/google/cadvisor/info/v2",
|
||||
"//vendor:github.com/google/gofuzz",
|
||||
"//vendor:github.com/stretchr/testify/assert",
|
||||
"//vendor:github.com/stretchr/testify/mock",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
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)
|
||||
}
|
||||
|
||||
// diskResourceAnalyzer provider 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()
|
||||
}
|
||||
}
|
249
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/handler.go
generated
vendored
Normal file
249
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/handler.go
generated
vendored
Normal file
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/golang/glog"
|
||||
cadvisorapi "github.com/google/cadvisor/info/v1"
|
||||
cadvisorapiv2 "github.com/google/cadvisor/info/v2"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
"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 {
|
||||
GetContainerInfo(podFullName string, uid types.UID, containerName string, req *cadvisorapi.ContainerInfoRequest) (*cadvisorapi.ContainerInfo, error)
|
||||
GetContainerInfoV2(name string, options cadvisorapiv2.RequestOptions) (map[string]cadvisorapiv2.ContainerInfo, error)
|
||||
GetRawContainerInfo(containerName string, req *cadvisorapi.ContainerInfoRequest, subcontainers bool) (map[string]*cadvisorapi.ContainerInfo, error)
|
||||
GetPodByName(namespace, name string) (*v1.Pod, bool)
|
||||
GetNode() (*v1.Node, error)
|
||||
GetNodeConfig() cm.NodeConfig
|
||||
ImagesFsInfo() (cadvisorapiv2.FsInfo, error)
|
||||
RootFsInfo() (cadvisorapiv2.FsInfo, error)
|
||||
ListVolumesForPod(podUID types.UID) (map[string]volume.Volume, bool)
|
||||
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": v1.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)
|
||||
}
|
||||
}
|
244
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/mocks_test.go
generated
vendored
Normal file
244
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/mocks_test.go
generated
vendored
Normal file
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
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 (
|
||||
cadvisorapi "github.com/google/cadvisor/info/v1"
|
||||
cadvisorapiv2 "github.com/google/cadvisor/info/v2"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
)
|
||||
|
||||
// DO NOT EDIT
|
||||
// GENERATED BY mockery
|
||||
|
||||
type MockStatsProvider struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetContainerInfo provides a mock function with given fields: podFullName, uid, containerName, req
|
||||
func (_m *MockStatsProvider) GetContainerInfo(podFullName string, uid types.UID, containerName string, req *cadvisorapi.ContainerInfoRequest) (*cadvisorapi.ContainerInfo, error) {
|
||||
ret := _m.Called(podFullName, uid, containerName, req)
|
||||
|
||||
var r0 *cadvisorapi.ContainerInfo
|
||||
if rf, ok := ret.Get(0).(func(string, types.UID, string, *cadvisorapi.ContainerInfoRequest) *cadvisorapi.ContainerInfo); ok {
|
||||
r0 = rf(podFullName, uid, containerName, req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*cadvisorapi.ContainerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, types.UID, string, *cadvisorapi.ContainerInfoRequest) error); ok {
|
||||
r1 = rf(podFullName, uid, containerName, req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetContainerInfoV2 provides a mock function with given fields: name, options
|
||||
func (_m *MockStatsProvider) GetContainerInfoV2(name string, options cadvisorapiv2.RequestOptions) (map[string]cadvisorapiv2.ContainerInfo, error) {
|
||||
ret := _m.Called(name, options)
|
||||
|
||||
var r0 map[string]cadvisorapiv2.ContainerInfo
|
||||
if rf, ok := ret.Get(0).(func(string, cadvisorapiv2.RequestOptions) map[string]cadvisorapiv2.ContainerInfo); ok {
|
||||
r0 = rf(name, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]cadvisorapiv2.ContainerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, cadvisorapiv2.RequestOptions) error); ok {
|
||||
r1 = rf(name, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetRawContainerInfo provides a mock function with given fields: containerName, req, subcontainers
|
||||
func (_m *MockStatsProvider) GetRawContainerInfo(containerName string, req *cadvisorapi.ContainerInfoRequest, subcontainers bool) (map[string]*cadvisorapi.ContainerInfo, error) {
|
||||
ret := _m.Called(containerName, req, subcontainers)
|
||||
|
||||
var r0 map[string]*cadvisorapi.ContainerInfo
|
||||
if rf, ok := ret.Get(0).(func(string, *cadvisorapi.ContainerInfoRequest, bool) map[string]*cadvisorapi.ContainerInfo); ok {
|
||||
r0 = rf(containerName, req, subcontainers)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]*cadvisorapi.ContainerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *cadvisorapi.ContainerInfoRequest, bool) error); ok {
|
||||
r1 = rf(containerName, req, subcontainers)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetPodByName provides a mock function with given fields: namespace, name
|
||||
func (_m *MockStatsProvider) GetPodByName(namespace string, name string) (*v1.Pod, bool) {
|
||||
ret := _m.Called(namespace, name)
|
||||
|
||||
var r0 *v1.Pod
|
||||
if rf, ok := ret.Get(0).(func(string, string) *v1.Pod); ok {
|
||||
r0 = rf(namespace, name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1.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
|
||||
}
|
||||
|
||||
// GetNode provides a mock function with given fields:
|
||||
func (_m *MockStatsProvider) GetNode() (*v1.Node, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *v1.Node
|
||||
if rf, ok := ret.Get(0).(func() *v1.Node); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1.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 *MockStatsProvider) 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
|
||||
}
|
||||
|
||||
// ImagesFsInfo provides a mock function with given fields:
|
||||
func (_m *MockStatsProvider) ImagesFsInfo() (cadvisorapiv2.FsInfo, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 cadvisorapiv2.FsInfo
|
||||
if rf, ok := ret.Get(0).(func() cadvisorapiv2.FsInfo); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(cadvisorapiv2.FsInfo)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RootFsInfo provides a mock function with given fields:
|
||||
func (_m *MockStatsProvider) RootFsInfo() (cadvisorapiv2.FsInfo, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 cadvisorapiv2.FsInfo
|
||||
if rf, ok := ret.Get(0).(func() cadvisorapiv2.FsInfo); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(cadvisorapiv2.FsInfo)
|
||||
}
|
||||
|
||||
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 *MockStatsProvider) 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
|
||||
}
|
||||
|
||||
// GetPods provides a mock function with given fields:
|
||||
func (_m *MockStatsProvider) GetPods() []*v1.Pod {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*v1.Pod
|
||||
if rf, ok := ret.Get(0).(func() []*v1.Pod); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*v1.Pod)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
51
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/resource_analyzer.go
generated
vendored
Normal file
51
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/resource_analyzer.go
generated
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"k8s.io/kubernetes/pkg/kubelet/container"
|
||||
)
|
||||
|
||||
// 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, runtime container.Runtime) ResourceAnalyzer {
|
||||
fsAnalyzer := newFsResourceAnalyzer(statsProvider, calVolumeFrequency)
|
||||
summaryProvider := NewSummaryProvider(statsProvider, fsAnalyzer, runtime)
|
||||
return &resourceAnalyzer{fsAnalyzer, summaryProvider}
|
||||
}
|
||||
|
||||
// Start starts background functions necessary for the ResourceAnalyzer to function
|
||||
func (ra *resourceAnalyzer) Start() {
|
||||
ra.fsResourceAnalyzer.Start()
|
||||
}
|
434
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/summary.go
generated
vendored
Normal file
434
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/summary.go
generated
vendored
Normal file
|
@ -0,0 +1,434 @@
|
|||
/*
|
||||
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"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kubetypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/stats"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
"k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/leaky"
|
||||
"k8s.io/kubernetes/pkg/kubelet/network"
|
||||
"k8s.io/kubernetes/pkg/kubelet/types"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
cadvisorapiv1 "github.com/google/cadvisor/info/v1"
|
||||
cadvisorapiv2 "github.com/google/cadvisor/info/v2"
|
||||
)
|
||||
|
||||
type SummaryProvider interface {
|
||||
// Get provides a new Summary using the latest results from cadvisor
|
||||
Get() (*stats.Summary, error)
|
||||
}
|
||||
|
||||
type summaryProviderImpl struct {
|
||||
provider StatsProvider
|
||||
fsResourceAnalyzer fsResourceAnalyzerInterface
|
||||
runtime container.Runtime
|
||||
}
|
||||
|
||||
var _ SummaryProvider = &summaryProviderImpl{}
|
||||
|
||||
// NewSummaryProvider returns a new SummaryProvider
|
||||
func NewSummaryProvider(statsProvider StatsProvider, fsResourceAnalyzer fsResourceAnalyzerInterface, cruntime container.Runtime) SummaryProvider {
|
||||
return &summaryProviderImpl{statsProvider, fsResourceAnalyzer, cruntime}
|
||||
}
|
||||
|
||||
// Get implements the SummaryProvider interface
|
||||
// Query cadvisor for the latest resource metrics and build into a summary
|
||||
func (sp *summaryProviderImpl) Get() (*stats.Summary, error) {
|
||||
options := cadvisorapiv2.RequestOptions{
|
||||
IdType: cadvisorapiv2.TypeName,
|
||||
Count: 2, // 2 samples are needed to compute "instantaneous" CPU
|
||||
Recursive: true,
|
||||
}
|
||||
infos, err := sp.provider.GetContainerInfoV2("/", options)
|
||||
if err != nil {
|
||||
if _, ok := infos["/"]; ok {
|
||||
// If the failure is partial, log it and return a best-effort response.
|
||||
glog.Errorf("Partial failure issuing GetContainerInfoV2: %v", err)
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed GetContainerInfoV2: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 GetNode: %v", err)
|
||||
}
|
||||
|
||||
nodeConfig := sp.provider.GetNodeConfig()
|
||||
rootFsInfo, err := sp.provider.RootFsInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed RootFsInfo: %v", err)
|
||||
}
|
||||
imageFsInfo, err := sp.provider.ImagesFsInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed DockerImagesFsInfo: %v", err)
|
||||
}
|
||||
imageStats, err := sp.runtime.ImageStats()
|
||||
if err != nil || imageStats == nil {
|
||||
return nil, fmt.Errorf("failed ImageStats: %v", err)
|
||||
}
|
||||
sb := &summaryBuilder{sp.fsResourceAnalyzer, node, nodeConfig, rootFsInfo, imageFsInfo, *imageStats, infos}
|
||||
return sb.build()
|
||||
}
|
||||
|
||||
// summaryBuilder aggregates the datastructures provided by cadvisor into a Summary result
|
||||
type summaryBuilder struct {
|
||||
fsResourceAnalyzer fsResourceAnalyzerInterface
|
||||
node *v1.Node
|
||||
nodeConfig cm.NodeConfig
|
||||
rootFsInfo cadvisorapiv2.FsInfo
|
||||
imageFsInfo cadvisorapiv2.FsInfo
|
||||
imageStats container.ImageStats
|
||||
infos map[string]cadvisorapiv2.ContainerInfo
|
||||
}
|
||||
|
||||
// build returns a Summary from aggregating the input data
|
||||
func (sb *summaryBuilder) build() (*stats.Summary, error) {
|
||||
rootInfo, found := sb.infos["/"]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("Missing stats for root container")
|
||||
}
|
||||
|
||||
var nodeFsInodesUsed *uint64
|
||||
if sb.rootFsInfo.Inodes != nil && sb.rootFsInfo.InodesFree != nil {
|
||||
nodeFsIU := *sb.rootFsInfo.Inodes - *sb.rootFsInfo.InodesFree
|
||||
nodeFsInodesUsed = &nodeFsIU
|
||||
}
|
||||
|
||||
var imageFsInodesUsed *uint64
|
||||
if sb.imageFsInfo.Inodes != nil && sb.imageFsInfo.InodesFree != nil {
|
||||
imageFsIU := *sb.imageFsInfo.Inodes - *sb.imageFsInfo.InodesFree
|
||||
imageFsInodesUsed = &imageFsIU
|
||||
}
|
||||
|
||||
rootStats := sb.containerInfoV2ToStats("", &rootInfo)
|
||||
nodeStats := stats.NodeStats{
|
||||
NodeName: sb.node.Name,
|
||||
CPU: rootStats.CPU,
|
||||
Memory: rootStats.Memory,
|
||||
Network: sb.containerInfoV2ToNetworkStats("node:"+sb.node.Name, &rootInfo),
|
||||
Fs: &stats.FsStats{
|
||||
AvailableBytes: &sb.rootFsInfo.Available,
|
||||
CapacityBytes: &sb.rootFsInfo.Capacity,
|
||||
UsedBytes: &sb.rootFsInfo.Usage,
|
||||
InodesFree: sb.rootFsInfo.InodesFree,
|
||||
Inodes: sb.rootFsInfo.Inodes,
|
||||
InodesUsed: nodeFsInodesUsed,
|
||||
},
|
||||
StartTime: rootStats.StartTime,
|
||||
Runtime: &stats.RuntimeStats{
|
||||
ImageFs: &stats.FsStats{
|
||||
AvailableBytes: &sb.imageFsInfo.Available,
|
||||
CapacityBytes: &sb.imageFsInfo.Capacity,
|
||||
UsedBytes: &sb.imageStats.TotalStorageBytes,
|
||||
InodesFree: sb.imageFsInfo.InodesFree,
|
||||
Inodes: sb.imageFsInfo.Inodes,
|
||||
InodesUsed: imageFsInodesUsed,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
systemContainers := map[string]string{
|
||||
stats.SystemContainerKubelet: sb.nodeConfig.KubeletCgroupsName,
|
||||
stats.SystemContainerRuntime: sb.nodeConfig.RuntimeCgroupsName,
|
||||
stats.SystemContainerMisc: sb.nodeConfig.SystemCgroupsName,
|
||||
}
|
||||
for sys, name := range systemContainers {
|
||||
if info, ok := sb.infos[name]; ok {
|
||||
sysCont := sb.containerInfoV2ToStats(sys, &info)
|
||||
// System containers don't have a filesystem associated with them.
|
||||
sysCont.Rootfs = nil
|
||||
sysCont.Logs = nil
|
||||
nodeStats.SystemContainers = append(nodeStats.SystemContainers, sysCont)
|
||||
}
|
||||
}
|
||||
|
||||
summary := stats.Summary{
|
||||
Node: nodeStats,
|
||||
Pods: sb.buildSummaryPods(),
|
||||
}
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
// containerInfoV2FsStats populates the container fs stats
|
||||
func (sb *summaryBuilder) containerInfoV2FsStats(
|
||||
info *cadvisorapiv2.ContainerInfo,
|
||||
cs *stats.ContainerStats) {
|
||||
|
||||
// The container logs live on the node rootfs device
|
||||
cs.Logs = &stats.FsStats{
|
||||
AvailableBytes: &sb.rootFsInfo.Available,
|
||||
CapacityBytes: &sb.rootFsInfo.Capacity,
|
||||
InodesFree: sb.rootFsInfo.InodesFree,
|
||||
Inodes: sb.rootFsInfo.Inodes,
|
||||
}
|
||||
|
||||
if sb.rootFsInfo.Inodes != nil && sb.rootFsInfo.InodesFree != nil {
|
||||
logsInodesUsed := *sb.rootFsInfo.Inodes - *sb.rootFsInfo.InodesFree
|
||||
cs.Logs.InodesUsed = &logsInodesUsed
|
||||
}
|
||||
|
||||
// The container rootFs lives on the imageFs devices (which may not be the node root fs)
|
||||
cs.Rootfs = &stats.FsStats{
|
||||
AvailableBytes: &sb.imageFsInfo.Available,
|
||||
CapacityBytes: &sb.imageFsInfo.Capacity,
|
||||
InodesFree: sb.imageFsInfo.InodesFree,
|
||||
Inodes: sb.imageFsInfo.Inodes,
|
||||
}
|
||||
lcs, found := sb.latestContainerStats(info)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
cfs := lcs.Filesystem
|
||||
|
||||
if cfs != nil {
|
||||
if cfs.BaseUsageBytes != nil {
|
||||
rootfsUsage := *cfs.BaseUsageBytes
|
||||
cs.Rootfs.UsedBytes = &rootfsUsage
|
||||
if cfs.TotalUsageBytes != nil {
|
||||
logsUsage := *cfs.TotalUsageBytes - *cfs.BaseUsageBytes
|
||||
cs.Logs.UsedBytes = &logsUsage
|
||||
}
|
||||
}
|
||||
if cfs.InodeUsage != nil {
|
||||
rootInodes := *cfs.InodeUsage
|
||||
cs.Rootfs.InodesUsed = &rootInodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// latestContainerStats returns the latest container stats from cadvisor, or nil if none exist
|
||||
func (sb *summaryBuilder) latestContainerStats(info *cadvisorapiv2.ContainerInfo) (*cadvisorapiv2.ContainerStats, bool) {
|
||||
stats := info.Stats
|
||||
if len(stats) < 1 {
|
||||
return nil, false
|
||||
}
|
||||
latest := stats[len(stats)-1]
|
||||
if latest == nil {
|
||||
return nil, false
|
||||
}
|
||||
return latest, true
|
||||
}
|
||||
|
||||
// buildSummaryPods aggregates and returns the container stats in cinfos by the Pod managing the container.
|
||||
// Containers not managed by a Pod are omitted.
|
||||
func (sb *summaryBuilder) buildSummaryPods() []stats.PodStats {
|
||||
// Map each container to a pod and update the PodStats with container data
|
||||
podToStats := map[stats.PodReference]*stats.PodStats{}
|
||||
for key, cinfo := range sb.infos {
|
||||
// on systemd using devicemapper each mount into the container has an associated cgroup.
|
||||
// we ignore them to ensure we do not get duplicate entries in our summary.
|
||||
// for details on .mount units: http://man7.org/linux/man-pages/man5/systemd.mount.5.html
|
||||
if strings.HasSuffix(key, ".mount") {
|
||||
continue
|
||||
}
|
||||
// Build the Pod key if this container is managed by a Pod
|
||||
if !sb.isPodManagedContainer(&cinfo) {
|
||||
continue
|
||||
}
|
||||
ref := sb.buildPodRef(&cinfo)
|
||||
|
||||
// Lookup the PodStats for the pod using the PodRef. If none exists, initialize a new entry.
|
||||
podStats, found := podToStats[ref]
|
||||
if !found {
|
||||
podStats = &stats.PodStats{PodRef: ref}
|
||||
podToStats[ref] = podStats
|
||||
}
|
||||
|
||||
// Update the PodStats entry with the stats from the container by adding it to stats.Containers
|
||||
containerName := types.GetContainerName(cinfo.Spec.Labels)
|
||||
if containerName == leaky.PodInfraContainerName {
|
||||
// Special case for infrastructure container which is hidden from the user and has network stats
|
||||
podStats.Network = sb.containerInfoV2ToNetworkStats("pod:"+ref.Namespace+"_"+ref.Name, &cinfo)
|
||||
podStats.StartTime = metav1.NewTime(cinfo.Spec.CreationTime)
|
||||
} else {
|
||||
podStats.Containers = append(podStats.Containers, sb.containerInfoV2ToStats(containerName, &cinfo))
|
||||
}
|
||||
}
|
||||
|
||||
// Add each PodStats to the result
|
||||
result := make([]stats.PodStats, 0, len(podToStats))
|
||||
for _, podStats := range podToStats {
|
||||
// Lookup the volume stats for each pod
|
||||
podUID := kubetypes.UID(podStats.PodRef.UID)
|
||||
if vstats, found := sb.fsResourceAnalyzer.GetPodVolumeStats(podUID); found {
|
||||
podStats.VolumeStats = vstats.Volumes
|
||||
}
|
||||
result = append(result, *podStats)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// buildPodRef returns a PodReference that identifies the Pod managing cinfo
|
||||
func (sb *summaryBuilder) buildPodRef(cinfo *cadvisorapiv2.ContainerInfo) stats.PodReference {
|
||||
podName := types.GetPodName(cinfo.Spec.Labels)
|
||||
podNamespace := types.GetPodNamespace(cinfo.Spec.Labels)
|
||||
podUID := types.GetPodUID(cinfo.Spec.Labels)
|
||||
return stats.PodReference{Name: podName, Namespace: podNamespace, UID: podUID}
|
||||
}
|
||||
|
||||
// isPodManagedContainer returns true if the cinfo container is managed by a Pod
|
||||
func (sb *summaryBuilder) isPodManagedContainer(cinfo *cadvisorapiv2.ContainerInfo) bool {
|
||||
podName := types.GetPodName(cinfo.Spec.Labels)
|
||||
podNamespace := types.GetPodNamespace(cinfo.Spec.Labels)
|
||||
managed := podName != "" && podNamespace != ""
|
||||
if !managed && podName != podNamespace {
|
||||
glog.Warningf(
|
||||
"Expect container to have either both podName (%s) and podNamespace (%s) labels, or neither.",
|
||||
podName, podNamespace)
|
||||
}
|
||||
return managed
|
||||
}
|
||||
|
||||
func (sb *summaryBuilder) containerInfoV2ToStats(
|
||||
name string,
|
||||
info *cadvisorapiv2.ContainerInfo) stats.ContainerStats {
|
||||
cStats := stats.ContainerStats{
|
||||
StartTime: metav1.NewTime(info.Spec.CreationTime),
|
||||
Name: name,
|
||||
}
|
||||
cstat, found := sb.latestContainerStats(info)
|
||||
if !found {
|
||||
return cStats
|
||||
}
|
||||
if info.Spec.HasCpu {
|
||||
cpuStats := stats.CPUStats{
|
||||
Time: metav1.NewTime(cstat.Timestamp),
|
||||
}
|
||||
if cstat.CpuInst != nil {
|
||||
cpuStats.UsageNanoCores = &cstat.CpuInst.Usage.Total
|
||||
}
|
||||
if cstat.Cpu != nil {
|
||||
cpuStats.UsageCoreNanoSeconds = &cstat.Cpu.Usage.Total
|
||||
}
|
||||
cStats.CPU = &cpuStats
|
||||
}
|
||||
if info.Spec.HasMemory {
|
||||
pageFaults := cstat.Memory.ContainerData.Pgfault
|
||||
majorPageFaults := cstat.Memory.ContainerData.Pgmajfault
|
||||
cStats.Memory = &stats.MemoryStats{
|
||||
Time: metav1.NewTime(cstat.Timestamp),
|
||||
UsageBytes: &cstat.Memory.Usage,
|
||||
WorkingSetBytes: &cstat.Memory.WorkingSet,
|
||||
RSSBytes: &cstat.Memory.RSS,
|
||||
PageFaults: &pageFaults,
|
||||
MajorPageFaults: &majorPageFaults,
|
||||
}
|
||||
// availableBytes = memory limit (if known) - workingset
|
||||
if !isMemoryUnlimited(info.Spec.Memory.Limit) {
|
||||
availableBytes := info.Spec.Memory.Limit - cstat.Memory.WorkingSet
|
||||
cStats.Memory.AvailableBytes = &availableBytes
|
||||
}
|
||||
}
|
||||
|
||||
sb.containerInfoV2FsStats(info, &cStats)
|
||||
cStats.UserDefinedMetrics = sb.containerInfoV2ToUserDefinedMetrics(info)
|
||||
return cStats
|
||||
}
|
||||
|
||||
// Size after which we consider memory to be "unlimited". This is not
|
||||
// MaxInt64 due to rounding by the kernel.
|
||||
// TODO: cadvisor should export this https://github.com/google/cadvisor/blob/master/metrics/prometheus.go#L596
|
||||
const maxMemorySize = uint64(1 << 62)
|
||||
|
||||
func isMemoryUnlimited(v uint64) bool {
|
||||
return v > maxMemorySize
|
||||
}
|
||||
|
||||
func (sb *summaryBuilder) containerInfoV2ToNetworkStats(name string, info *cadvisorapiv2.ContainerInfo) *stats.NetworkStats {
|
||||
if !info.Spec.HasNetwork {
|
||||
return nil
|
||||
}
|
||||
cstat, found := sb.latestContainerStats(info)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
for _, inter := range cstat.Network.Interfaces {
|
||||
if inter.Name == network.DefaultInterfaceName {
|
||||
return &stats.NetworkStats{
|
||||
Time: metav1.NewTime(cstat.Timestamp),
|
||||
RxBytes: &inter.RxBytes,
|
||||
RxErrors: &inter.RxErrors,
|
||||
TxBytes: &inter.TxBytes,
|
||||
TxErrors: &inter.TxErrors,
|
||||
}
|
||||
}
|
||||
}
|
||||
glog.V(4).Infof("Missing default interface %q for %s", network.DefaultInterfaceName, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sb *summaryBuilder) containerInfoV2ToUserDefinedMetrics(info *cadvisorapiv2.ContainerInfo) []stats.UserDefinedMetric {
|
||||
type specVal struct {
|
||||
ref stats.UserDefinedMetricDescriptor
|
||||
valType cadvisorapiv1.DataType
|
||||
time time.Time
|
||||
value float64
|
||||
}
|
||||
udmMap := map[string]*specVal{}
|
||||
for _, spec := range info.Spec.CustomMetrics {
|
||||
udmMap[spec.Name] = &specVal{
|
||||
ref: stats.UserDefinedMetricDescriptor{
|
||||
Name: spec.Name,
|
||||
Type: stats.UserDefinedMetricType(spec.Type),
|
||||
Units: spec.Units,
|
||||
},
|
||||
valType: spec.Format,
|
||||
}
|
||||
}
|
||||
for _, stat := range info.Stats {
|
||||
for name, values := range stat.CustomMetrics {
|
||||
specVal, ok := udmMap[name]
|
||||
if !ok {
|
||||
glog.Warningf("spec for custom metric %q is missing from cAdvisor output. Spec: %+v, Metrics: %+v", name, info.Spec, stat.CustomMetrics)
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
// Pick the most recent value
|
||||
if value.Timestamp.Before(specVal.time) {
|
||||
continue
|
||||
}
|
||||
specVal.time = value.Timestamp
|
||||
specVal.value = value.FloatValue
|
||||
if specVal.valType == cadvisorapiv1.IntType {
|
||||
specVal.value = float64(value.IntValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var udm []stats.UserDefinedMetric
|
||||
for _, specVal := range udmMap {
|
||||
udm = append(udm, stats.UserDefinedMetric{
|
||||
UserDefinedMetricDescriptor: specVal.ref,
|
||||
Time: metav1.NewTime(specVal.time),
|
||||
Value: specVal.value,
|
||||
})
|
||||
}
|
||||
return udm
|
||||
}
|
455
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/summary_test.go
generated
vendored
Normal file
455
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/summary_test.go
generated
vendored
Normal file
|
@ -0,0 +1,455 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"github.com/google/cadvisor/info/v1"
|
||||
"github.com/google/cadvisor/info/v2"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8sv1 "k8s.io/kubernetes/pkg/api/v1"
|
||||
kubestats "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/stats"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
"k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/leaky"
|
||||
)
|
||||
|
||||
const (
|
||||
// Offsets from seed value in generated container stats.
|
||||
offsetCPUUsageCores = iota
|
||||
offsetCPUUsageCoreSeconds
|
||||
offsetMemPageFaults
|
||||
offsetMemMajorPageFaults
|
||||
offsetMemUsageBytes
|
||||
offsetMemRSSBytes
|
||||
offsetMemWorkingSetBytes
|
||||
offsetNetRxBytes
|
||||
offsetNetRxErrors
|
||||
offsetNetTxBytes
|
||||
offsetNetTxErrors
|
||||
)
|
||||
|
||||
var (
|
||||
timestamp = time.Now()
|
||||
creationTime = timestamp.Add(-5 * time.Minute)
|
||||
)
|
||||
|
||||
func TestBuildSummary(t *testing.T) {
|
||||
node := k8sv1.Node{}
|
||||
node.Name = "FooNode"
|
||||
nodeConfig := cm.NodeConfig{
|
||||
RuntimeCgroupsName: "/docker-daemon",
|
||||
SystemCgroupsName: "/system",
|
||||
KubeletCgroupsName: "/kubelet",
|
||||
}
|
||||
const (
|
||||
namespace0 = "test0"
|
||||
namespace2 = "test2"
|
||||
)
|
||||
const (
|
||||
seedRoot = 0
|
||||
seedRuntime = 100
|
||||
seedKubelet = 200
|
||||
seedMisc = 300
|
||||
seedPod0Infra = 1000
|
||||
seedPod0Container0 = 2000
|
||||
seedPod0Container1 = 2001
|
||||
seedPod1Infra = 3000
|
||||
seedPod1Container = 4000
|
||||
seedPod2Infra = 5000
|
||||
seedPod2Container = 6000
|
||||
)
|
||||
const (
|
||||
pName0 = "pod0"
|
||||
pName1 = "pod1"
|
||||
pName2 = "pod0" // ensure pName2 conflicts with pName0, but is in a different namespace
|
||||
)
|
||||
const (
|
||||
cName00 = "c0"
|
||||
cName01 = "c1"
|
||||
cName10 = "c0" // ensure cName10 conflicts with cName02, but is in a different pod
|
||||
cName20 = "c1" // ensure cName20 conflicts with cName01, but is in a different pod + namespace
|
||||
)
|
||||
const (
|
||||
rootfsCapacity = uint64(10000000)
|
||||
rootfsAvailable = uint64(5000000)
|
||||
rootfsInodesFree = uint64(1000)
|
||||
rootfsInodes = uint64(2000)
|
||||
imagefsCapacity = uint64(20000000)
|
||||
imagefsAvailable = uint64(8000000)
|
||||
imagefsInodesFree = uint64(2000)
|
||||
imagefsInodes = uint64(4000)
|
||||
)
|
||||
|
||||
prf0 := kubestats.PodReference{Name: pName0, Namespace: namespace0, UID: "UID" + pName0}
|
||||
prf1 := kubestats.PodReference{Name: pName1, Namespace: namespace0, UID: "UID" + pName1}
|
||||
prf2 := kubestats.PodReference{Name: pName2, Namespace: namespace2, UID: "UID" + pName2}
|
||||
infos := map[string]v2.ContainerInfo{
|
||||
"/": summaryTestContainerInfo(seedRoot, "", "", ""),
|
||||
"/docker-daemon": summaryTestContainerInfo(seedRuntime, "", "", ""),
|
||||
"/kubelet": summaryTestContainerInfo(seedKubelet, "", "", ""),
|
||||
"/system": summaryTestContainerInfo(seedMisc, "", "", ""),
|
||||
// Pod0 - Namespace0
|
||||
"/pod0-i": summaryTestContainerInfo(seedPod0Infra, pName0, namespace0, leaky.PodInfraContainerName),
|
||||
"/pod0-c0": summaryTestContainerInfo(seedPod0Container0, pName0, namespace0, cName00),
|
||||
"/pod0-c1": summaryTestContainerInfo(seedPod0Container1, pName0, namespace0, cName01),
|
||||
// Pod1 - Namespace0
|
||||
"/pod1-i": summaryTestContainerInfo(seedPod1Infra, pName1, namespace0, leaky.PodInfraContainerName),
|
||||
"/pod1-c0": summaryTestContainerInfo(seedPod1Container, pName1, namespace0, cName10),
|
||||
// Pod2 - Namespace2
|
||||
"/pod2-i": summaryTestContainerInfo(seedPod2Infra, pName2, namespace2, leaky.PodInfraContainerName),
|
||||
"/pod2-c0": summaryTestContainerInfo(seedPod2Container, pName2, namespace2, cName20),
|
||||
}
|
||||
|
||||
freeRootfsInodes := rootfsInodesFree
|
||||
totalRootfsInodes := rootfsInodes
|
||||
rootfs := v2.FsInfo{
|
||||
Capacity: rootfsCapacity,
|
||||
Available: rootfsAvailable,
|
||||
InodesFree: &freeRootfsInodes,
|
||||
Inodes: &totalRootfsInodes,
|
||||
}
|
||||
freeImagefsInodes := imagefsInodesFree
|
||||
totalImagefsInodes := imagefsInodes
|
||||
imagefs := v2.FsInfo{
|
||||
Capacity: imagefsCapacity,
|
||||
Available: imagefsAvailable,
|
||||
InodesFree: &freeImagefsInodes,
|
||||
Inodes: &totalImagefsInodes,
|
||||
}
|
||||
|
||||
// memory limit overrides for each container (used to test available bytes if a memory limit is known)
|
||||
memoryLimitOverrides := map[string]uint64{
|
||||
"/": uint64(1 << 30),
|
||||
"/pod2-c0": uint64(1 << 15),
|
||||
}
|
||||
for name, memoryLimitOverride := range memoryLimitOverrides {
|
||||
info, found := infos[name]
|
||||
if !found {
|
||||
t.Errorf("No container defined with name %v", name)
|
||||
}
|
||||
info.Spec.Memory.Limit = memoryLimitOverride
|
||||
infos[name] = info
|
||||
}
|
||||
|
||||
sb := &summaryBuilder{
|
||||
newFsResourceAnalyzer(&MockStatsProvider{}, time.Minute*5), &node, nodeConfig, rootfs, imagefs, container.ImageStats{}, infos}
|
||||
summary, err := sb.build()
|
||||
|
||||
assert.NoError(t, err)
|
||||
nodeStats := summary.Node
|
||||
assert.Equal(t, "FooNode", nodeStats.NodeName)
|
||||
assert.EqualValues(t, testTime(creationTime, seedRoot).Unix(), nodeStats.StartTime.Time.Unix())
|
||||
checkCPUStats(t, "Node", seedRoot, nodeStats.CPU)
|
||||
checkMemoryStats(t, "Node", seedRoot, infos["/"], nodeStats.Memory)
|
||||
checkNetworkStats(t, "Node", seedRoot, nodeStats.Network)
|
||||
|
||||
systemSeeds := map[string]int{
|
||||
kubestats.SystemContainerRuntime: seedRuntime,
|
||||
kubestats.SystemContainerKubelet: seedKubelet,
|
||||
kubestats.SystemContainerMisc: seedMisc,
|
||||
}
|
||||
systemContainerToNodeCgroup := map[string]string{
|
||||
kubestats.SystemContainerRuntime: nodeConfig.RuntimeCgroupsName,
|
||||
kubestats.SystemContainerKubelet: nodeConfig.KubeletCgroupsName,
|
||||
kubestats.SystemContainerMisc: nodeConfig.SystemCgroupsName,
|
||||
}
|
||||
for _, sys := range nodeStats.SystemContainers {
|
||||
name := sys.Name
|
||||
info := infos[systemContainerToNodeCgroup[name]]
|
||||
seed, found := systemSeeds[name]
|
||||
if !found {
|
||||
t.Errorf("Unknown SystemContainer: %q", name)
|
||||
}
|
||||
assert.EqualValues(t, testTime(creationTime, seed).Unix(), sys.StartTime.Time.Unix(), name+".StartTime")
|
||||
checkCPUStats(t, name, seed, sys.CPU)
|
||||
checkMemoryStats(t, name, seed, info, sys.Memory)
|
||||
assert.Nil(t, sys.Logs, name+".Logs")
|
||||
assert.Nil(t, sys.Rootfs, name+".Rootfs")
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, len(summary.Pods))
|
||||
indexPods := make(map[kubestats.PodReference]kubestats.PodStats, len(summary.Pods))
|
||||
for _, pod := range summary.Pods {
|
||||
indexPods[pod.PodRef] = pod
|
||||
}
|
||||
|
||||
// Validate Pod0 Results
|
||||
ps, found := indexPods[prf0]
|
||||
assert.True(t, found)
|
||||
assert.Len(t, ps.Containers, 2)
|
||||
indexCon := make(map[string]kubestats.ContainerStats, len(ps.Containers))
|
||||
for _, con := range ps.Containers {
|
||||
indexCon[con.Name] = con
|
||||
}
|
||||
con := indexCon[cName00]
|
||||
assert.EqualValues(t, testTime(creationTime, seedPod0Container0).Unix(), con.StartTime.Time.Unix())
|
||||
checkCPUStats(t, "Pod0Container0", seedPod0Container0, con.CPU)
|
||||
checkMemoryStats(t, "Pod0Conainer0", seedPod0Container0, infos["/pod0-c0"], con.Memory)
|
||||
|
||||
con = indexCon[cName01]
|
||||
assert.EqualValues(t, testTime(creationTime, seedPod0Container1).Unix(), con.StartTime.Time.Unix())
|
||||
checkCPUStats(t, "Pod0Container1", seedPod0Container1, con.CPU)
|
||||
checkMemoryStats(t, "Pod0Container1", seedPod0Container1, infos["/pod0-c1"], con.Memory)
|
||||
|
||||
assert.EqualValues(t, testTime(creationTime, seedPod0Infra).Unix(), ps.StartTime.Time.Unix())
|
||||
checkNetworkStats(t, "Pod0", seedPod0Infra, ps.Network)
|
||||
|
||||
// Validate Pod1 Results
|
||||
ps, found = indexPods[prf1]
|
||||
assert.True(t, found)
|
||||
assert.Len(t, ps.Containers, 1)
|
||||
con = ps.Containers[0]
|
||||
assert.Equal(t, cName10, con.Name)
|
||||
checkCPUStats(t, "Pod1Container0", seedPod1Container, con.CPU)
|
||||
checkMemoryStats(t, "Pod1Container0", seedPod1Container, infos["/pod1-c0"], con.Memory)
|
||||
checkNetworkStats(t, "Pod1", seedPod1Infra, ps.Network)
|
||||
|
||||
// Validate Pod2 Results
|
||||
ps, found = indexPods[prf2]
|
||||
assert.True(t, found)
|
||||
assert.Len(t, ps.Containers, 1)
|
||||
con = ps.Containers[0]
|
||||
assert.Equal(t, cName20, con.Name)
|
||||
checkCPUStats(t, "Pod2Container0", seedPod2Container, con.CPU)
|
||||
checkMemoryStats(t, "Pod2Container0", seedPod2Container, infos["/pod2-c0"], con.Memory)
|
||||
checkNetworkStats(t, "Pod2", seedPod2Infra, ps.Network)
|
||||
}
|
||||
|
||||
func generateCustomMetricSpec() []v1.MetricSpec {
|
||||
f := fuzz.New().NilChance(0).Funcs(
|
||||
func(e *v1.MetricSpec, c fuzz.Continue) {
|
||||
c.Fuzz(&e.Name)
|
||||
switch c.Intn(3) {
|
||||
case 0:
|
||||
e.Type = v1.MetricGauge
|
||||
case 1:
|
||||
e.Type = v1.MetricCumulative
|
||||
case 2:
|
||||
e.Type = v1.MetricDelta
|
||||
}
|
||||
switch c.Intn(2) {
|
||||
case 0:
|
||||
e.Format = v1.IntType
|
||||
case 1:
|
||||
e.Format = v1.FloatType
|
||||
}
|
||||
c.Fuzz(&e.Units)
|
||||
})
|
||||
var ret []v1.MetricSpec
|
||||
f.Fuzz(&ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
func generateCustomMetrics(spec []v1.MetricSpec) map[string][]v1.MetricVal {
|
||||
ret := map[string][]v1.MetricVal{}
|
||||
for _, metricSpec := range spec {
|
||||
f := fuzz.New().NilChance(0).Funcs(
|
||||
func(e *v1.MetricVal, c fuzz.Continue) {
|
||||
switch metricSpec.Format {
|
||||
case v1.IntType:
|
||||
c.Fuzz(&e.IntValue)
|
||||
case v1.FloatType:
|
||||
c.Fuzz(&e.FloatValue)
|
||||
}
|
||||
})
|
||||
|
||||
var metrics []v1.MetricVal
|
||||
f.Fuzz(&metrics)
|
||||
ret[metricSpec.Name] = metrics
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func summaryTestContainerInfo(seed int, podName string, podNamespace string, containerName string) v2.ContainerInfo {
|
||||
labels := map[string]string{}
|
||||
if podName != "" {
|
||||
labels = map[string]string{
|
||||
"io.kubernetes.pod.name": podName,
|
||||
"io.kubernetes.pod.uid": "UID" + podName,
|
||||
"io.kubernetes.pod.namespace": podNamespace,
|
||||
"io.kubernetes.container.name": containerName,
|
||||
}
|
||||
}
|
||||
// by default, kernel will set memory.limit_in_bytes to 1 << 63 if not bounded
|
||||
unlimitedMemory := uint64(1 << 63)
|
||||
spec := v2.ContainerSpec{
|
||||
CreationTime: testTime(creationTime, seed),
|
||||
HasCpu: true,
|
||||
HasMemory: true,
|
||||
HasNetwork: true,
|
||||
Labels: labels,
|
||||
Memory: v2.MemorySpec{
|
||||
Limit: unlimitedMemory,
|
||||
},
|
||||
CustomMetrics: generateCustomMetricSpec(),
|
||||
}
|
||||
|
||||
stats := v2.ContainerStats{
|
||||
Timestamp: testTime(timestamp, seed),
|
||||
Cpu: &v1.CpuStats{},
|
||||
CpuInst: &v2.CpuInstStats{},
|
||||
Memory: &v1.MemoryStats{
|
||||
Usage: uint64(seed + offsetMemUsageBytes),
|
||||
WorkingSet: uint64(seed + offsetMemWorkingSetBytes),
|
||||
RSS: uint64(seed + offsetMemRSSBytes),
|
||||
ContainerData: v1.MemoryStatsMemoryData{
|
||||
Pgfault: uint64(seed + offsetMemPageFaults),
|
||||
Pgmajfault: uint64(seed + offsetMemMajorPageFaults),
|
||||
},
|
||||
},
|
||||
Network: &v2.NetworkStats{
|
||||
Interfaces: []v1.InterfaceStats{{
|
||||
Name: "eth0",
|
||||
RxBytes: uint64(seed + offsetNetRxBytes),
|
||||
RxErrors: uint64(seed + offsetNetRxErrors),
|
||||
TxBytes: uint64(seed + offsetNetTxBytes),
|
||||
TxErrors: uint64(seed + offsetNetTxErrors),
|
||||
}, {
|
||||
Name: "cbr0",
|
||||
RxBytes: 100,
|
||||
RxErrors: 100,
|
||||
TxBytes: 100,
|
||||
TxErrors: 100,
|
||||
}},
|
||||
},
|
||||
CustomMetrics: generateCustomMetrics(spec.CustomMetrics),
|
||||
}
|
||||
stats.Cpu.Usage.Total = uint64(seed + offsetCPUUsageCoreSeconds)
|
||||
stats.CpuInst.Usage.Total = uint64(seed + offsetCPUUsageCores)
|
||||
return v2.ContainerInfo{
|
||||
Spec: spec,
|
||||
Stats: []*v2.ContainerStats{&stats},
|
||||
}
|
||||
}
|
||||
|
||||
func testTime(base time.Time, seed int) time.Time {
|
||||
return base.Add(time.Duration(seed) * time.Second)
|
||||
}
|
||||
|
||||
func checkNetworkStats(t *testing.T, label string, seed int, stats *kubestats.NetworkStats) {
|
||||
assert.NotNil(t, stats)
|
||||
assert.EqualValues(t, testTime(timestamp, seed).Unix(), stats.Time.Time.Unix(), label+".Net.Time")
|
||||
assert.EqualValues(t, seed+offsetNetRxBytes, *stats.RxBytes, label+".Net.RxBytes")
|
||||
assert.EqualValues(t, seed+offsetNetRxErrors, *stats.RxErrors, label+".Net.RxErrors")
|
||||
assert.EqualValues(t, seed+offsetNetTxBytes, *stats.TxBytes, label+".Net.TxBytes")
|
||||
assert.EqualValues(t, seed+offsetNetTxErrors, *stats.TxErrors, label+".Net.TxErrors")
|
||||
}
|
||||
|
||||
func checkCPUStats(t *testing.T, label string, seed int, stats *kubestats.CPUStats) {
|
||||
assert.EqualValues(t, testTime(timestamp, seed).Unix(), stats.Time.Time.Unix(), label+".CPU.Time")
|
||||
assert.EqualValues(t, seed+offsetCPUUsageCores, *stats.UsageNanoCores, label+".CPU.UsageCores")
|
||||
assert.EqualValues(t, seed+offsetCPUUsageCoreSeconds, *stats.UsageCoreNanoSeconds, label+".CPU.UsageCoreSeconds")
|
||||
}
|
||||
|
||||
func checkMemoryStats(t *testing.T, label string, seed int, info v2.ContainerInfo, stats *kubestats.MemoryStats) {
|
||||
assert.EqualValues(t, testTime(timestamp, seed).Unix(), stats.Time.Time.Unix(), label+".Mem.Time")
|
||||
assert.EqualValues(t, seed+offsetMemUsageBytes, *stats.UsageBytes, label+".Mem.UsageBytes")
|
||||
assert.EqualValues(t, seed+offsetMemWorkingSetBytes, *stats.WorkingSetBytes, label+".Mem.WorkingSetBytes")
|
||||
assert.EqualValues(t, seed+offsetMemRSSBytes, *stats.RSSBytes, label+".Mem.RSSBytes")
|
||||
assert.EqualValues(t, seed+offsetMemPageFaults, *stats.PageFaults, label+".Mem.PageFaults")
|
||||
assert.EqualValues(t, seed+offsetMemMajorPageFaults, *stats.MajorPageFaults, label+".Mem.MajorPageFaults")
|
||||
if !info.Spec.HasMemory || isMemoryUnlimited(info.Spec.Memory.Limit) {
|
||||
assert.Nil(t, stats.AvailableBytes, label+".Mem.AvailableBytes")
|
||||
} else {
|
||||
expected := info.Spec.Memory.Limit - *stats.WorkingSetBytes
|
||||
assert.EqualValues(t, expected, *stats.AvailableBytes, label+".Mem.AvailableBytes")
|
||||
}
|
||||
}
|
||||
|
||||
func checkFsStats(t *testing.T, capacity uint64, Available uint64, inodes uint64, inodesFree uint64, fs *kubestats.FsStats) {
|
||||
assert.EqualValues(t, capacity, *fs.CapacityBytes)
|
||||
assert.EqualValues(t, Available, *fs.AvailableBytes)
|
||||
assert.EqualValues(t, inodesFree, *fs.InodesFree)
|
||||
assert.EqualValues(t, inodes, *fs.Inodes)
|
||||
}
|
||||
|
||||
func TestCustomMetrics(t *testing.T) {
|
||||
spec := []v1.MetricSpec{
|
||||
{
|
||||
Name: "qos",
|
||||
Type: v1.MetricGauge,
|
||||
Format: v1.IntType,
|
||||
Units: "per second",
|
||||
},
|
||||
{
|
||||
Name: "cpuLoad",
|
||||
Type: v1.MetricCumulative,
|
||||
Format: v1.FloatType,
|
||||
Units: "count",
|
||||
},
|
||||
}
|
||||
timestamp1 := time.Now()
|
||||
timestamp2 := time.Now().Add(time.Minute)
|
||||
metrics := map[string][]v1.MetricVal{
|
||||
"qos": {
|
||||
{
|
||||
Timestamp: timestamp1,
|
||||
IntValue: 10,
|
||||
},
|
||||
{
|
||||
Timestamp: timestamp2,
|
||||
IntValue: 100,
|
||||
},
|
||||
},
|
||||
"cpuLoad": {
|
||||
{
|
||||
Timestamp: timestamp1,
|
||||
FloatValue: 1.2,
|
||||
},
|
||||
{
|
||||
Timestamp: timestamp2,
|
||||
FloatValue: 2.1,
|
||||
},
|
||||
},
|
||||
}
|
||||
cInfo := v2.ContainerInfo{
|
||||
Spec: v2.ContainerSpec{
|
||||
CustomMetrics: spec,
|
||||
},
|
||||
Stats: []*v2.ContainerStats{
|
||||
{
|
||||
CustomMetrics: metrics,
|
||||
},
|
||||
},
|
||||
}
|
||||
sb := &summaryBuilder{}
|
||||
assert.Contains(t, sb.containerInfoV2ToUserDefinedMetrics(&cInfo),
|
||||
kubestats.UserDefinedMetric{
|
||||
UserDefinedMetricDescriptor: kubestats.UserDefinedMetricDescriptor{
|
||||
Name: "qos",
|
||||
Type: kubestats.MetricGauge,
|
||||
Units: "per second",
|
||||
},
|
||||
Time: metav1.NewTime(timestamp2),
|
||||
Value: 100,
|
||||
},
|
||||
kubestats.UserDefinedMetric{
|
||||
UserDefinedMetricDescriptor: kubestats.UserDefinedMetricDescriptor{
|
||||
Name: "cpuLoad",
|
||||
Type: kubestats.MetricCumulative,
|
||||
Units: "count",
|
||||
},
|
||||
Time: metav1.NewTime(timestamp2),
|
||||
Value: 2.1,
|
||||
})
|
||||
}
|
126
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/volume_stat_calculator.go
generated
vendored
Normal file
126
vendor/k8s.io/kubernetes/pkg/kubelet/server/stats/volume_stat_calculator.go
generated
vendored
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
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/util/wait"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/stats"
|
||||
"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 all VolumeStats for a pod
|
||||
type PodVolumeStats struct {
|
||||
Volumes []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.
|
||||
func (s *volumeStatCalculator) calcAndStoreStats() {
|
||||
// Find all Volumes for the Pod
|
||||
volumes, found := s.statsProvider.ListVolumesForPod(s.pod.UID)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
// Call GetMetrics on each Volume and copy the result to a new VolumeStats.FsStats
|
||||
stats := make([]stats.VolumeStats, 0, len(volumes))
|
||||
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
|
||||
}
|
||||
stats = append(stats, s.parsePodVolumeStats(name, metric))
|
||||
}
|
||||
|
||||
// Store the new stats
|
||||
s.latest.Store(PodVolumeStats{Volumes: stats})
|
||||
}
|
||||
|
||||
// parsePodVolumeStats converts (internal) volume.Metrics to (external) stats.VolumeStats structures
|
||||
func (s *volumeStatCalculator) parsePodVolumeStats(podName string, metric *volume.Metrics) 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,
|
||||
FsStats: stats.FsStats{AvailableBytes: &available, CapacityBytes: &capacity, UsedBytes: &used,
|
||||
Inodes: &inodes, InodesFree: &inodesFree, InodesUsed: &inodesUsed},
|
||||
}
|
||||
}
|
65
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/BUILD
generated
vendored
Normal file
65
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/BUILD
generated
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
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",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/kubelet/api/v1alpha1/runtime:go_default_library",
|
||||
"//pkg/kubelet/server/portforward:go_default_library",
|
||||
"//pkg/kubelet/server/remotecommand:go_default_library",
|
||||
"//pkg/util/clock:go_default_library",
|
||||
"//pkg/util/term:go_default_library",
|
||||
"//vendor:github.com/emicklei/go-restful",
|
||||
"//vendor:google.golang.org/grpc",
|
||||
"//vendor:google.golang.org/grpc/codes",
|
||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"request_cache_test.go",
|
||||
"server_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/client/restclient:go_default_library",
|
||||
"//pkg/client/unversioned/remotecommand:go_default_library",
|
||||
"//pkg/kubelet/api/v1alpha1/runtime:go_default_library",
|
||||
"//pkg/kubelet/server/portforward:go_default_library",
|
||||
"//pkg/kubelet/server/remotecommand:go_default_library",
|
||||
"//pkg/util/term:go_default_library",
|
||||
"//vendor:github.com/stretchr/testify/assert",
|
||||
"//vendor:github.com/stretchr/testify/require",
|
||||
"//vendor:k8s.io/client-go/pkg/api",
|
||||
"//vendor:k8s.io/client-go/pkg/util/clock",
|
||||
],
|
||||
)
|
||||
|
||||
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/kubernetes/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/client-go/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
|
||||
}
|
330
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/server.go
generated
vendored
Normal file
330
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/server.go
generated
vendored
Normal file
|
@ -0,0 +1,330 @@
|
|||
/*
|
||||
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"
|
||||
runtimeapi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/portforward"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
|
||||
"k8s.io/kubernetes/pkg/util/term"
|
||||
)
|
||||
|
||||
// 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 term.Size) error
|
||||
Attach(containerID string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan term.Size) 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.
|
||||
SupportedProtocols []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: remotecommand.DefaultStreamCreationTimeout,
|
||||
SupportedProtocols: remotecommand.SupportedStreamingProtocols,
|
||||
}
|
||||
|
||||
// TODO(timstclair): 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
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type server struct {
|
||||
config Config
|
||||
runtime *criAdapter
|
||||
handler http.Handler
|
||||
cache *requestCache
|
||||
}
|
||||
|
||||
func (s *server) GetExec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
|
||||
if req.GetContainerId() == "" {
|
||||
return nil, grpc.Errorf(codes.InvalidArgument, "missing required container_id")
|
||||
}
|
||||
token, err := s.cache.Insert(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &runtimeapi.ExecResponse{
|
||||
Url: s.buildURL("exec", token),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) GetAttach(req *runtimeapi.AttachRequest) (*runtimeapi.AttachResponse, error) {
|
||||
if req.GetContainerId() == "" {
|
||||
return nil, grpc.Errorf(codes.InvalidArgument, "missing required container_id")
|
||||
}
|
||||
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.GetPodSandboxId() == "" {
|
||||
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(timstclair): Implement this.
|
||||
return errors.New("stayUp=false is not yet implemented")
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: s.config.Addr,
|
||||
Handler: s.handler,
|
||||
TLSConfig: s.config.TLSConfig,
|
||||
}
|
||||
if s.config.TLSConfig != nil {
|
||||
return server.ListenAndServeTLS("", "") // Use certs from TLSConfig.
|
||||
} else {
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) Stop() error {
|
||||
// TODO(timstclair): Implement this.
|
||||
return errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *server) buildURL(method, token string) *string {
|
||||
loc := s.config.BaseURL.ResolveReference(&url.URL{
|
||||
Path: path.Join(method, token),
|
||||
}).String()
|
||||
return &loc
|
||||
}
|
||||
|
||||
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 := &remotecommand.Options{
|
||||
Stdin: exec.GetStdin(),
|
||||
Stdout: true,
|
||||
Stderr: !exec.GetTty(),
|
||||
TTY: exec.GetTty(),
|
||||
}
|
||||
|
||||
remotecommand.ServeExec(
|
||||
resp.ResponseWriter,
|
||||
req.Request,
|
||||
s.runtime,
|
||||
"", // unused: podName
|
||||
"", // unusued: podUID
|
||||
exec.GetContainerId(),
|
||||
exec.GetCmd(),
|
||||
streamOpts,
|
||||
s.config.StreamIdleTimeout,
|
||||
s.config.StreamCreationTimeout,
|
||||
s.config.SupportedProtocols)
|
||||
}
|
||||
|
||||
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 := &remotecommand.Options{
|
||||
Stdin: attach.GetStdin(),
|
||||
Stdout: true,
|
||||
Stderr: !attach.GetTty(),
|
||||
TTY: attach.GetTty(),
|
||||
}
|
||||
remotecommand.ServeAttach(
|
||||
resp.ResponseWriter,
|
||||
req.Request,
|
||||
s.runtime,
|
||||
"", // unused: podName
|
||||
"", // unusued: podUID
|
||||
attach.GetContainerId(),
|
||||
streamOpts,
|
||||
s.config.StreamIdleTimeout,
|
||||
s.config.StreamCreationTimeout,
|
||||
s.config.SupportedProtocols)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
portforward.ServePortForward(
|
||||
resp.ResponseWriter,
|
||||
req.Request,
|
||||
s.runtime,
|
||||
pf.GetPodSandboxId(),
|
||||
"", // unused: podUID
|
||||
s.config.StreamIdleTimeout,
|
||||
s.config.StreamCreationTimeout)
|
||||
}
|
||||
|
||||
// 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 _ remotecommand.Executor = &criAdapter{}
|
||||
var _ remotecommand.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 term.Size, timeout time.Duration) error {
|
||||
return a.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 term.Size) error {
|
||||
return a.Attach(container, in, out, err, tty, resize)
|
||||
}
|
||||
|
||||
func (a *criAdapter) PortForward(podName string, podUID types.UID, port uint16, stream io.ReadWriteCloser) error {
|
||||
return a.Runtime.PortForward(podName, int32(port), stream)
|
||||
}
|
423
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/server_test.go
generated
vendored
Normal file
423
vendor/k8s.io/kubernetes/pkg/kubelet/server/streaming/server_test.go
generated
vendored
Normal file
|
@ -0,0 +1,423 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"k8s.io/client-go/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/client/restclient"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/remotecommand"
|
||||
runtimeapi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime"
|
||||
kubeletportforward "k8s.io/kubernetes/pkg/kubelet/server/portforward"
|
||||
kubeletremotecommand "k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
|
||||
"k8s.io/kubernetes/pkg/util/term"
|
||||
)
|
||||
|
||||
const (
|
||||
testAddr = "localhost:12345"
|
||||
testContainerID = "container789"
|
||||
testPodSandboxID = "pod0987"
|
||||
)
|
||||
|
||||
func TestGetExec(t *testing.T) {
|
||||
type testcase struct {
|
||||
cmd []string
|
||||
tty bool
|
||||
stdin bool
|
||||
}
|
||||
testcases := []testcase{
|
||||
{[]string{"echo", "foo"}, false, false},
|
||||
{[]string{"date"}, true, false},
|
||||
{[]string{"date"}, false, true},
|
||||
{[]string{"date"}, true, true},
|
||||
}
|
||||
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(test testcase, cache *requestCache, token string) {
|
||||
req, ok := cache.Consume(token)
|
||||
require.True(t, ok, "token %s not found! testcase=%+v", token, test)
|
||||
assert.Equal(t, testContainerID, req.(*runtimeapi.ExecRequest).GetContainerId(), "testcase=%+v", test)
|
||||
assert.Equal(t, test.cmd, req.(*runtimeapi.ExecRequest).GetCmd(), "testcase=%+v", test)
|
||||
assert.Equal(t, test.tty, req.(*runtimeapi.ExecRequest).GetTty(), "testcase=%+v", test)
|
||||
assert.Equal(t, test.stdin, req.(*runtimeapi.ExecRequest).GetStdin(), "testcase=%+v", test)
|
||||
}
|
||||
containerID := testContainerID
|
||||
for _, test := range testcases {
|
||||
request := &runtimeapi.ExecRequest{
|
||||
ContainerId: &containerID,
|
||||
Cmd: test.cmd,
|
||||
Tty: &test.tty,
|
||||
Stdin: &test.stdin,
|
||||
}
|
||||
{ // Non-TLS
|
||||
resp, err := serv.GetExec(request)
|
||||
assert.NoError(t, err, "testcase=%+v", test)
|
||||
expectedURL := "http://" + testAddr + "/exec/"
|
||||
assert.Contains(t, resp.GetUrl(), expectedURL, "testcase=%+v", test)
|
||||
token := strings.TrimPrefix(resp.GetUrl(), expectedURL)
|
||||
assertRequestToken(test, serv.(*server).cache, token)
|
||||
}
|
||||
|
||||
{ // TLS
|
||||
resp, err := tlsServer.GetExec(request)
|
||||
assert.NoError(t, err, "testcase=%+v", test)
|
||||
expectedURL := "https://" + testAddr + "/exec/"
|
||||
assert.Contains(t, resp.GetUrl(), expectedURL, "testcase=%+v", test)
|
||||
token := strings.TrimPrefix(resp.GetUrl(), expectedURL)
|
||||
assertRequestToken(test, tlsServer.(*server).cache, token)
|
||||
}
|
||||
|
||||
{ // Path prefix
|
||||
resp, err := prefixServer.GetExec(request)
|
||||
assert.NoError(t, err, "testcase=%+v", test)
|
||||
expectedURL := "http://" + testAddr + "/" + pathPrefix + "/exec/"
|
||||
assert.Contains(t, resp.GetUrl(), expectedURL, "testcase=%+v", test)
|
||||
token := strings.TrimPrefix(resp.GetUrl(), expectedURL)
|
||||
assertRequestToken(test, prefixServer.(*server).cache, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAttach(t *testing.T) {
|
||||
type testcase struct {
|
||||
tty bool
|
||||
stdin bool
|
||||
}
|
||||
testcases := []testcase{
|
||||
{false, false},
|
||||
{true, false},
|
||||
{false, true},
|
||||
{true, true},
|
||||
}
|
||||
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)
|
||||
|
||||
assertRequestToken := func(test testcase, cache *requestCache, token string) {
|
||||
req, ok := cache.Consume(token)
|
||||
require.True(t, ok, "token %s not found! testcase=%+v", token, test)
|
||||
assert.Equal(t, testContainerID, req.(*runtimeapi.AttachRequest).GetContainerId(), "testcase=%+v", test)
|
||||
assert.Equal(t, test.tty, req.(*runtimeapi.AttachRequest).GetTty(), "testcase=%+v", test)
|
||||
assert.Equal(t, test.stdin, req.(*runtimeapi.AttachRequest).GetStdin(), "testcase=%+v", test)
|
||||
}
|
||||
containerID := testContainerID
|
||||
for _, test := range testcases {
|
||||
request := &runtimeapi.AttachRequest{
|
||||
ContainerId: &containerID,
|
||||
Stdin: &test.stdin,
|
||||
Tty: &test.tty,
|
||||
}
|
||||
{ // Non-TLS
|
||||
resp, err := serv.GetAttach(request)
|
||||
assert.NoError(t, err, "testcase=%+v", test)
|
||||
expectedURL := "http://" + testAddr + "/attach/"
|
||||
assert.Contains(t, resp.GetUrl(), expectedURL, "testcase=%+v", test)
|
||||
token := strings.TrimPrefix(resp.GetUrl(), expectedURL)
|
||||
assertRequestToken(test, serv.(*server).cache, token)
|
||||
}
|
||||
|
||||
{ // TLS
|
||||
resp, err := tlsServer.GetAttach(request)
|
||||
assert.NoError(t, err, "testcase=%+v", test)
|
||||
expectedURL := "https://" + testAddr + "/attach/"
|
||||
assert.Contains(t, resp.GetUrl(), expectedURL, "testcase=%+v", test)
|
||||
token := strings.TrimPrefix(resp.GetUrl(), expectedURL)
|
||||
assertRequestToken(test, 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.GetUrl(), expectedURL))
|
||||
token := strings.TrimPrefix(resp.GetUrl(), expectedURL)
|
||||
req, ok := serv.(*server).cache.Consume(token)
|
||||
require.True(t, ok, "token %s not found!", token)
|
||||
assert.Equal(t, testPodSandboxID, req.(*runtimeapi.PortForwardRequest).GetPodSandboxId())
|
||||
}
|
||||
|
||||
{ // 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.GetUrl(), expectedURL))
|
||||
token := strings.TrimPrefix(resp.GetUrl(), expectedURL)
|
||||
req, ok := tlsServer.(*server).cache.Consume(token)
|
||||
require.True(t, ok, "token %s not found!", token)
|
||||
assert.Equal(t, testPodSandboxID, req.(*runtimeapi.PortForwardRequest).GetPodSandboxId())
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
podSandboxID := testPodSandboxID
|
||||
resp, err := s.GetPortForward(&runtimeapi.PortForwardRequest{
|
||||
PodSandboxId: &podSandboxID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
reqURL, err := url.Parse(resp.GetUrl())
|
||||
require.NoError(t, err)
|
||||
|
||||
exec, err := remotecommand.NewExecutor(&restclient.Config{}, "POST", reqURL)
|
||||
require.NoError(t, err)
|
||||
streamConn, _, err := exec.Dial(kubeletportforward.PortForwardProtocolV1Name)
|
||||
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 := true
|
||||
containerID := testContainerID
|
||||
switch commandType {
|
||||
case "exec":
|
||||
resp, err := s.GetExec(&runtimeapi.ExecRequest{
|
||||
ContainerId: &containerID,
|
||||
Cmd: []string{"echo"},
|
||||
Stdin: &stdin,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
reqURL, err = url.Parse(resp.GetUrl())
|
||||
require.NoError(t, err)
|
||||
case "attach":
|
||||
resp, err := s.GetAttach(&runtimeapi.AttachRequest{
|
||||
ContainerId: &containerID,
|
||||
Stdin: &stdin,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
reqURL, err = url.Parse(resp.GetUrl())
|
||||
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.NewExecutor(&restclient.Config{}, "POST", reqURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := remotecommand.StreamOptions{
|
||||
SupportedProtocols: kubeletremotecommand.SupportedStreamingProtocols,
|
||||
Stdin: stdinR,
|
||||
Stdout: stdoutW,
|
||||
Stderr: stderrW,
|
||||
Tty: false,
|
||||
TerminalSizeQueue: nil,
|
||||
}
|
||||
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 term.Size) 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 term.Size) 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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue