Move around some packages (#658)
* Move tracing exporter registration This doesn't belong in the library and should be configured by the consumer of the opencensus package. * Rename `vkublet` package to `node` `vkubelet` does not convey any information to the consumers of the package. Really it would be nice to move this package to the root of the repo, but then you wind up with... interesting... import semantics due to the repo name... and after thinking about it some, a subpackage is really not so bad as long as it has a name that convey's some information. `node` was chosen since this package deals with all the semantics of operating a node in Kubernetes.
This commit is contained in:
3
node/api/doc.go
Normal file
3
node/api/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package api implements HTTP handlers for handling requests that the kubelet
|
||||
// would normally implement, such as pod logs, exec, etc.
|
||||
package api
|
||||
174
node/api/exec.go
Normal file
174
node/api/exec.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
remoteutils "k8s.io/client-go/tools/remotecommand"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
|
||||
)
|
||||
|
||||
// ContainerExecHandlerFunc defines the handler function used for "execing" into a
|
||||
// container in a pod.
|
||||
type ContainerExecHandlerFunc func(ctx context.Context, namespace, podName, containerName string, cmd []string, attach AttachIO) error
|
||||
|
||||
// AttachIO is used to pass in streams to attach to a container process
|
||||
type AttachIO interface {
|
||||
Stdin() io.Reader
|
||||
Stdout() io.WriteCloser
|
||||
Stderr() io.WriteCloser
|
||||
TTY() bool
|
||||
Resize() <-chan TermSize
|
||||
}
|
||||
|
||||
// TermSize is used to set the terminal size from attached clients.
|
||||
type TermSize struct {
|
||||
Width uint16
|
||||
Height uint16
|
||||
}
|
||||
|
||||
// HandleContainerExec makes an http handler func from a Provider which execs a command in a pod's container
|
||||
// Note that this handler currently depends on gorrilla/mux to get url parts as variables.
|
||||
// TODO(@cpuguy83): don't force gorilla/mux on consumers of this function
|
||||
func HandleContainerExec(h ContainerExecHandlerFunc) http.HandlerFunc {
|
||||
if h == nil {
|
||||
return NotImplemented
|
||||
}
|
||||
return handleError(func(w http.ResponseWriter, req *http.Request) error {
|
||||
vars := mux.Vars(req)
|
||||
|
||||
namespace := vars["namespace"]
|
||||
pod := vars["pod"]
|
||||
container := vars["container"]
|
||||
|
||||
supportedStreamProtocols := strings.Split(req.Header.Get("X-Stream-Protocol-Version"), ",")
|
||||
|
||||
q := req.URL.Query()
|
||||
command := q["command"]
|
||||
|
||||
streamOpts, err := getExecOptions(req)
|
||||
if err != nil {
|
||||
return errdefs.AsInvalidInput(err)
|
||||
}
|
||||
|
||||
idleTimeout := time.Second * 30
|
||||
streamCreationTimeout := time.Second * 30
|
||||
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
|
||||
exec := &containerExecContext{ctx: ctx, h: h, pod: pod, namespace: namespace, container: container}
|
||||
remotecommand.ServeExec(w, req, exec, "", "", container, command, streamOpts, idleTimeout, streamCreationTimeout, supportedStreamProtocols)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func getExecOptions(req *http.Request) (*remotecommand.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 {
|
||||
return nil, errors.New("cannot exec with tty and stderr")
|
||||
}
|
||||
|
||||
if !stdin && !stdout && !stderr {
|
||||
return nil, errors.New("you must specify at least one of stdin, stdout, stderr")
|
||||
}
|
||||
return &remotecommand.Options{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
TTY: tty,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
type containerExecContext struct {
|
||||
h ContainerExecHandlerFunc
|
||||
eio *execIO
|
||||
namespace, pod, container string
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// ExecInContainer Implements remotecommand.Executor
|
||||
// This is called by remotecommand.ServeExec
|
||||
func (c *containerExecContext) ExecInContainer(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remoteutils.TerminalSize, timeout time.Duration) error {
|
||||
|
||||
eio := &execIO{
|
||||
tty: tty,
|
||||
stdin: in,
|
||||
stdout: out,
|
||||
stderr: err,
|
||||
}
|
||||
|
||||
if tty {
|
||||
eio.chResize = make(chan TermSize)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(c.ctx)
|
||||
defer cancel()
|
||||
|
||||
if tty {
|
||||
go func() {
|
||||
send := func(s remoteutils.TerminalSize) bool {
|
||||
select {
|
||||
case eio.chResize <- TermSize{Width: s.Width, Height: s.Height}:
|
||||
return false
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case s := <-resize:
|
||||
if send(s) {
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return c.h(c.ctx, c.namespace, c.pod, c.container, cmd, eio)
|
||||
}
|
||||
|
||||
type execIO struct {
|
||||
tty bool
|
||||
stdin io.Reader
|
||||
stdout io.WriteCloser
|
||||
stderr io.WriteCloser
|
||||
chResize chan TermSize
|
||||
}
|
||||
|
||||
func (e *execIO) TTY() bool {
|
||||
return e.tty
|
||||
}
|
||||
|
||||
func (e *execIO) Stdin() io.Reader {
|
||||
return e.stdin
|
||||
}
|
||||
|
||||
func (e *execIO) Stdout() io.WriteCloser {
|
||||
return e.stdout
|
||||
}
|
||||
|
||||
func (e *execIO) Stderr() io.WriteCloser {
|
||||
return e.stderr
|
||||
}
|
||||
|
||||
func (e *execIO) Resize() <-chan TermSize {
|
||||
return e.chResize
|
||||
}
|
||||
70
node/api/helpers.go
Normal file
70
node/api/helpers.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/log"
|
||||
)
|
||||
|
||||
type handlerFunc func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
func handleError(f handlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
err := f(w, req)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
code := httpStatusCode(err)
|
||||
w.WriteHeader(code)
|
||||
io.WriteString(w, err.Error())
|
||||
logger := log.G(req.Context()).WithError(err).WithField("httpStatusCode", code)
|
||||
|
||||
if code >= 500 {
|
||||
logger.Error("Internal server error on request")
|
||||
} else {
|
||||
logger.Debug("Error on request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func flushOnWrite(w io.Writer) io.Writer {
|
||||
if fw, ok := w.(writeFlusher); ok {
|
||||
return &flushWriter{fw}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
type flushWriter struct {
|
||||
w writeFlusher
|
||||
}
|
||||
|
||||
type writeFlusher interface {
|
||||
Flush() error
|
||||
Write([]byte) (int, error)
|
||||
}
|
||||
|
||||
func (fw *flushWriter) Write(p []byte) (int, error) {
|
||||
n, err := fw.w.Write(p)
|
||||
if n > 0 {
|
||||
if err := fw.w.Flush(); err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func httpStatusCode(err error) int {
|
||||
switch {
|
||||
case err == nil:
|
||||
return http.StatusOK
|
||||
case errdefs.IsNotFound(err):
|
||||
return http.StatusNotFound
|
||||
case errdefs.IsInvalidInput(err):
|
||||
return http.StatusBadRequest
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
80
node/api/logs.go
Normal file
80
node/api/logs.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/errdefs"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/log"
|
||||
)
|
||||
|
||||
// ContainerLogsHandlerFunc is used in place of backend implementations for getting container logs
|
||||
type ContainerLogsHandlerFunc func(ctx context.Context, namespace, podName, containerName string, opts ContainerLogOpts) (io.ReadCloser, error)
|
||||
|
||||
// ContainerLogOpts are used to pass along options to be set on the container
|
||||
// log stream.
|
||||
type ContainerLogOpts struct {
|
||||
Tail int
|
||||
Since time.Duration
|
||||
LimitBytes int
|
||||
Timestamps bool
|
||||
}
|
||||
|
||||
// HandleContainerLogs creates an http handler function from a provider to serve logs from a pod
|
||||
func HandleContainerLogs(h ContainerLogsHandlerFunc) http.HandlerFunc {
|
||||
if h == nil {
|
||||
return NotImplemented
|
||||
}
|
||||
return handleError(func(w http.ResponseWriter, req *http.Request) error {
|
||||
vars := mux.Vars(req)
|
||||
if len(vars) != 3 {
|
||||
return errdefs.NotFound("not found")
|
||||
}
|
||||
|
||||
ctx := req.Context()
|
||||
|
||||
namespace := vars["namespace"]
|
||||
pod := vars["pod"]
|
||||
container := vars["container"]
|
||||
tail := 10
|
||||
q := req.URL.Query()
|
||||
|
||||
if queryTail := q.Get("tailLines"); queryTail != "" {
|
||||
t, err := strconv.Atoi(queryTail)
|
||||
if err != nil {
|
||||
return errdefs.AsInvalidInput(errors.Wrap(err, "could not parse \"tailLines\""))
|
||||
}
|
||||
tail = t
|
||||
}
|
||||
|
||||
// TODO(@cpuguy83): support v1.PodLogOptions
|
||||
// The kubelet decoding here is not straight forward, so this needs to be disected
|
||||
|
||||
opts := ContainerLogOpts{
|
||||
Tail: tail,
|
||||
}
|
||||
|
||||
logs, err := h(ctx, namespace, pod, container, opts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error getting container logs?)")
|
||||
}
|
||||
|
||||
defer logs.Close()
|
||||
|
||||
req.Header.Set("Transfer-Encoding", "chunked")
|
||||
|
||||
if _, ok := w.(writeFlusher); !ok {
|
||||
log.G(ctx).Debug("http response writer does not support flushes")
|
||||
}
|
||||
|
||||
if _, err := io.Copy(flushOnWrite(w), logs); err != nil {
|
||||
return errors.Wrap(err, "error writing response to client")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
48
node/api/pods.go
Normal file
48
node/api/pods.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
)
|
||||
|
||||
type PodListerFunc func(context.Context) ([]*v1.Pod, error)
|
||||
|
||||
func HandleRunningPods(getPods PodListerFunc) http.HandlerFunc {
|
||||
scheme := runtime.NewScheme()
|
||||
v1.SchemeBuilder.AddToScheme(scheme)
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
|
||||
return handleError(func(w http.ResponseWriter, req *http.Request) error {
|
||||
ctx := req.Context()
|
||||
ctx = log.WithLogger(ctx, log.L)
|
||||
pods, err := getPods(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Borrowed from github.com/kubernetes/kubernetes/pkg/kubelet/server/server.go
|
||||
// encodePods creates an v1.PodList object from pods and returns the encoded
|
||||
// PodList.
|
||||
podList := new(v1.PodList)
|
||||
for _, pod := range pods {
|
||||
podList.Items = append(podList.Items, *pod)
|
||||
}
|
||||
codec := codecs.LegacyCodec(v1.SchemeGroupVersion)
|
||||
data, err := runtime.Encode(codec, podList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
121
node/api/server.go
Normal file
121
node/api/server.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/log"
|
||||
"go.opencensus.io/plugin/ochttp"
|
||||
"go.opencensus.io/plugin/ochttp/propagation/b3"
|
||||
)
|
||||
|
||||
// ServeMux defines an interface used to attach routes to an existing http
|
||||
// serve mux.
|
||||
// It is used to enable callers creating a new server to completely manage
|
||||
// their own HTTP server while allowing us to attach the required routes to
|
||||
// satisfy the Kubelet HTTP interfaces.
|
||||
type ServeMux interface {
|
||||
Handle(path string, h http.Handler)
|
||||
}
|
||||
|
||||
type PodHandlerConfig struct {
|
||||
RunInContainer ContainerExecHandlerFunc
|
||||
GetContainerLogs ContainerLogsHandlerFunc
|
||||
GetPods PodListerFunc
|
||||
}
|
||||
|
||||
// PodHandler creates an http handler for interacting with pods/containers.
|
||||
func PodHandler(p PodHandlerConfig, debug bool) http.Handler {
|
||||
r := mux.NewRouter()
|
||||
|
||||
// This matches the behaviour in the reference kubelet
|
||||
r.StrictSlash(true)
|
||||
if debug {
|
||||
r.HandleFunc("/runningpods/", HandleRunningPods(p.GetPods)).Methods("GET")
|
||||
}
|
||||
r.HandleFunc("/containerLogs/{namespace}/{pod}/{container}", HandleContainerLogs(p.GetContainerLogs)).Methods("GET")
|
||||
r.HandleFunc("/exec/{namespace}/{pod}/{container}", HandleContainerExec(p.RunInContainer)).Methods("POST")
|
||||
r.NotFoundHandler = http.HandlerFunc(NotFound)
|
||||
return r
|
||||
}
|
||||
|
||||
// PodStatsSummaryHandler creates an http handler for serving pod metrics.
|
||||
//
|
||||
// If the passed in handler func is nil this will create handlers which only
|
||||
// serves http.StatusNotImplemented
|
||||
func PodStatsSummaryHandler(f PodStatsSummaryHandlerFunc) http.Handler {
|
||||
if f == nil {
|
||||
return http.HandlerFunc(NotImplemented)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
|
||||
const summaryRoute = "/stats/summary"
|
||||
h := HandlePodStatsSummary(f)
|
||||
|
||||
r.Handle(summaryRoute, ochttp.WithRouteTag(h, "PodStatsSummaryHandler")).Methods("GET")
|
||||
r.Handle(summaryRoute+"/", ochttp.WithRouteTag(h, "PodStatsSummaryHandler")).Methods("GET")
|
||||
|
||||
r.NotFoundHandler = http.HandlerFunc(NotFound)
|
||||
return r
|
||||
}
|
||||
|
||||
// AttachPodRoutes adds the http routes for pod stuff to the passed in serve mux.
|
||||
//
|
||||
// Callers should take care to namespace the serve mux as they see fit, however
|
||||
// these routes get called by the Kubernetes API server.
|
||||
func AttachPodRoutes(p PodHandlerConfig, mux ServeMux, debug bool) {
|
||||
mux.Handle("/", InstrumentHandler(PodHandler(p, debug)))
|
||||
}
|
||||
|
||||
// PodMetricsConfig stores the handlers for pod metrics routes
|
||||
// It is used by AttachPodMetrics.
|
||||
//
|
||||
// The main reason for this struct is in case of expansion we do not need to break
|
||||
// the package level API.
|
||||
type PodMetricsConfig struct {
|
||||
GetStatsSummary PodStatsSummaryHandlerFunc
|
||||
}
|
||||
|
||||
// AttachPodMetricsRoutes adds the http routes for pod/node metrics to the passed in serve mux.
|
||||
//
|
||||
// Callers should take care to namespace the serve mux as they see fit, however
|
||||
// these routes get called by the Kubernetes API server.
|
||||
func AttachPodMetricsRoutes(p PodMetricsConfig, mux ServeMux) {
|
||||
mux.Handle("/", InstrumentHandler(HandlePodStatsSummary(p.GetStatsSummary)))
|
||||
}
|
||||
|
||||
func instrumentRequest(r *http.Request) *http.Request {
|
||||
ctx := r.Context()
|
||||
logger := log.G(ctx).WithFields(log.Fields{
|
||||
"uri": r.RequestURI,
|
||||
"vars": mux.Vars(r),
|
||||
})
|
||||
ctx = log.WithLogger(ctx, logger)
|
||||
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
// InstrumentHandler wraps an http.Handler and injects instrumentation into the request context.
|
||||
func InstrumentHandler(h http.Handler) http.Handler {
|
||||
instrumented := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
req = instrumentRequest(req)
|
||||
h.ServeHTTP(w, req)
|
||||
})
|
||||
return &ochttp.Handler{
|
||||
Handler: instrumented,
|
||||
Propagation: &b3.HTTPFormat{},
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound provides a handler for cases where the requested endpoint doesn't exist
|
||||
func NotFound(w http.ResponseWriter, r *http.Request) {
|
||||
log.G(r.Context()).Debug("404 request not found")
|
||||
http.Error(w, "404 request not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// NotImplemented provides a handler for cases where a provider does not implement a given API
|
||||
func NotImplemented(w http.ResponseWriter, r *http.Request) {
|
||||
log.G(r.Context()).Debug("501 not implemented")
|
||||
http.Error(w, "501 not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
55
node/api/stats.go
Normal file
55
node/api/stats.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
stats "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
|
||||
)
|
||||
|
||||
// PodStatsSummaryHandlerFunc defines the handler for getting pod stats summaries
|
||||
type PodStatsSummaryHandlerFunc func(context.Context) (*stats.Summary, error)
|
||||
|
||||
// HandlePodStatsSummary makes an HTTP handler for implementing the kubelet summary stats endpoint
|
||||
func HandlePodStatsSummary(h PodStatsSummaryHandlerFunc) http.HandlerFunc {
|
||||
if h == nil {
|
||||
return NotImplemented
|
||||
}
|
||||
return handleError(func(w http.ResponseWriter, req *http.Request) error {
|
||||
stats, err := h(req.Context())
|
||||
if err != nil {
|
||||
if isCancelled(err) {
|
||||
return err
|
||||
}
|
||||
return errors.Wrap(err, "error getting status from provider")
|
||||
}
|
||||
|
||||
b, err := json.Marshal(stats)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error marshalling stats")
|
||||
}
|
||||
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return errors.Wrap(err, "could not write to client")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func isCancelled(err error) bool {
|
||||
if err == context.Canceled {
|
||||
return true
|
||||
}
|
||||
|
||||
if e, ok := err.(causal); ok {
|
||||
return isCancelled(e.Cause())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type causal interface {
|
||||
Cause() error
|
||||
error
|
||||
}
|
||||
Reference in New Issue
Block a user