mirror of https://github.com/tailscale/tailscale/
cmd/tsrecorder: adds sending api level logging to tsrecorder (#16960)
Updates #17141 Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>pull/17497/head
parent
f25e47cdeb
commit
cd2a3425cb
@ -1 +1 @@
|
||||
sha256-jsmQ0S1Uh1cU/kr0onYLJY9VYcFx297QZjQALM3wX10=
|
||||
sha256-rV3C2Vi48FCifGt58OdEO4+Av0HRIs8sUJVvp/gEBLw=
|
||||
|
||||
@ -0,0 +1,548 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package apiproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/net/netx"
|
||||
"tailscale.com/sessionrecording"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
type fakeSender struct {
|
||||
sent map[netip.AddrPort][]byte
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *fakeSender) Send(ap netip.AddrPort, event io.Reader, dial netx.DialFunc) error {
|
||||
s.calls++
|
||||
if s.err != nil {
|
||||
return s.err
|
||||
}
|
||||
if s.sent == nil {
|
||||
s.sent = make(map[netip.AddrPort][]byte)
|
||||
}
|
||||
data, _ := io.ReadAll(event)
|
||||
s.sent[ap] = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeSender) Reset() {
|
||||
s.sent = nil
|
||||
s.err = nil
|
||||
s.calls = 0
|
||||
}
|
||||
|
||||
func TestRecordRequestAsEvent(t *testing.T) {
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sender := &fakeSender{}
|
||||
ap := &APIServerProxy{
|
||||
log: zl.Sugar(),
|
||||
ts: &tsnet.Server{},
|
||||
sendEventFunc: sender.Send,
|
||||
}
|
||||
|
||||
defaultWho := &apitype.WhoIsResponse{
|
||||
Node: &tailcfg.Node{
|
||||
StableID: "stable-id",
|
||||
Name: "node.ts.net.",
|
||||
},
|
||||
UserProfile: &tailcfg.UserProfile{
|
||||
ID: 1,
|
||||
LoginName: "user@example.com",
|
||||
},
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityKubernetes: []tailcfg.RawMessage{
|
||||
tailcfg.RawMessage(`{"recorderAddrs":["127.0.0.1:1234"]}`),
|
||||
tailcfg.RawMessage(`{"enforceRecorder": true}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defaultSource := sessionrecording.Source{
|
||||
Node: "node.ts.net",
|
||||
NodeID: "stable-id",
|
||||
NodeUser: "user@example.com",
|
||||
NodeUserID: 1,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req func() *http.Request
|
||||
who *apitype.WhoIsResponse
|
||||
setupSender func()
|
||||
wantErr bool
|
||||
wantEvent *sessionrecording.Event
|
||||
wantNumCalls int
|
||||
}{
|
||||
{
|
||||
name: "request-with-dot-in-name",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/namespaces/default/pods/foo.bar", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/namespaces/default/pods/foo.bar",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/namespaces/default/pods/foo.bar",
|
||||
Verb: "get",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Namespace: "default",
|
||||
Resource: "pods",
|
||||
Name: "foo.bar",
|
||||
Parts: []string{"pods", "foo.bar"},
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "request-with-dash-in-name",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/namespaces/default/pods/foo-bar", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/namespaces/default/pods/foo-bar",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/namespaces/default/pods/foo-bar",
|
||||
Verb: "get",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Namespace: "default",
|
||||
Resource: "pods",
|
||||
Name: "foo-bar",
|
||||
Parts: []string{"pods", "foo-bar"},
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "request-with-query-parameter",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/pods?watch=true", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/pods?watch=true",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{"watch": []string{"true"}},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/pods",
|
||||
Verb: "watch",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Resource: "pods",
|
||||
Parts: []string{"pods"},
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "request-with-label-selector",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/pods?labelSelector=app%3Dfoo", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/pods?labelSelector=app%3Dfoo",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{"labelSelector": []string{"app=foo"}},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/pods",
|
||||
Verb: "list",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Resource: "pods",
|
||||
Parts: []string{"pods"},
|
||||
LabelSelector: "app=foo",
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "request-with-field-selector",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/pods?fieldSelector=status.phase%3DRunning", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/pods?fieldSelector=status.phase%3DRunning",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{"fieldSelector": []string{"status.phase=Running"}},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/pods",
|
||||
Verb: "list",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Resource: "pods",
|
||||
Parts: []string{"pods"},
|
||||
FieldSelector: "status.phase=Running",
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "request-for-non-existent-resource",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/foo", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/foo",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/foo",
|
||||
Verb: "list",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Resource: "foo",
|
||||
Parts: []string{"foo"},
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "basic-request",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/pods", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/pods",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/pods",
|
||||
Verb: "list",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Resource: "pods",
|
||||
Parts: []string{"pods"},
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple-recorders",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/pods", nil)
|
||||
},
|
||||
who: &apitype.WhoIsResponse{
|
||||
Node: defaultWho.Node,
|
||||
UserProfile: defaultWho.UserProfile,
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityKubernetes: []tailcfg.RawMessage{
|
||||
tailcfg.RawMessage(`{"recorderAddrs":["127.0.0.1:1234", "127.0.0.1:5678"]}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "request-with-body",
|
||||
req: func() *http.Request {
|
||||
req := httptest.NewRequest("POST", "/api/v1/pods", bytes.NewBufferString(`{"foo":"bar"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "POST",
|
||||
Path: "/api/v1/pods",
|
||||
Body: json.RawMessage(`{"foo":"bar"}`),
|
||||
QueryParameters: url.Values{},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/pods",
|
||||
Verb: "create",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Resource: "pods",
|
||||
Parts: []string{"pods"},
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tagged-node",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/pods", nil)
|
||||
},
|
||||
who: &apitype.WhoIsResponse{
|
||||
Node: &tailcfg.Node{
|
||||
StableID: "stable-id",
|
||||
Name: "node.ts.net.",
|
||||
Tags: []string{"tag:foo"},
|
||||
},
|
||||
UserProfile: &tailcfg.UserProfile{},
|
||||
CapMap: defaultWho.CapMap,
|
||||
},
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/pods",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/pods",
|
||||
Verb: "list",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Resource: "pods",
|
||||
Parts: []string{"pods"},
|
||||
},
|
||||
Source: sessionrecording.Source{
|
||||
Node: "node.ts.net",
|
||||
NodeID: "stable-id",
|
||||
NodeTags: []string{"tag:foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-recorders",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/pods", nil)
|
||||
},
|
||||
who: &apitype.WhoIsResponse{
|
||||
Node: defaultWho.Node,
|
||||
UserProfile: defaultWho.UserProfile,
|
||||
CapMap: tailcfg.PeerCapMap{},
|
||||
},
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 0,
|
||||
},
|
||||
{
|
||||
name: "error-sending",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/pods", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() {
|
||||
sender.Reset()
|
||||
sender.err = errors.New("send error")
|
||||
},
|
||||
wantErr: true,
|
||||
wantNumCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "request-for-crd",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/apis/custom.example.com/v1/myresources", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/apis/custom.example.com/v1/myresources",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/apis/custom.example.com/v1/myresources",
|
||||
Verb: "list",
|
||||
APIPrefix: "apis",
|
||||
APIGroup: "custom.example.com",
|
||||
APIVersion: "v1",
|
||||
Resource: "myresources",
|
||||
Parts: []string{"myresources"},
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "request-with-proxy-verb",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/namespaces/default/pods/foo/proxy", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/namespaces/default/pods/foo/proxy",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/namespaces/default/pods/foo/proxy",
|
||||
Verb: "get",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Namespace: "default",
|
||||
Resource: "pods",
|
||||
Subresource: "proxy",
|
||||
Name: "foo",
|
||||
Parts: []string{"pods", "foo", "proxy"},
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "request-with-complex-path",
|
||||
req: func() *http.Request {
|
||||
return httptest.NewRequest("GET", "/api/v1/namespaces/default/services/foo:8080/proxy-subpath/more/segments", nil)
|
||||
},
|
||||
who: defaultWho,
|
||||
setupSender: func() { sender.Reset() },
|
||||
wantNumCalls: 1,
|
||||
wantEvent: &sessionrecording.Event{
|
||||
Type: sessionrecording.KubernetesAPIEventType,
|
||||
Request: sessionrecording.Request{
|
||||
Method: "GET",
|
||||
Path: "/api/v1/namespaces/default/services/foo:8080/proxy-subpath/more/segments",
|
||||
Body: nil,
|
||||
QueryParameters: url.Values{},
|
||||
},
|
||||
Kubernetes: sessionrecording.KubernetesRequestInfo{
|
||||
IsResourceRequest: true,
|
||||
Path: "/api/v1/namespaces/default/services/foo:8080/proxy-subpath/more/segments",
|
||||
Verb: "get",
|
||||
APIPrefix: "api",
|
||||
APIVersion: "v1",
|
||||
Namespace: "default",
|
||||
Resource: "services",
|
||||
Subresource: "proxy-subpath",
|
||||
Name: "foo:8080",
|
||||
Parts: []string{"services", "foo:8080", "proxy-subpath", "more", "segments"},
|
||||
},
|
||||
Source: defaultSource,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.setupSender()
|
||||
|
||||
req := tt.req()
|
||||
err := ap.recordRequestAsEvent(req, tt.who)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("recordRequestAsEvent() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if sender.calls != tt.wantNumCalls {
|
||||
t.Fatalf("expected %d calls to sender, got %d", tt.wantNumCalls, sender.calls)
|
||||
}
|
||||
|
||||
if tt.wantEvent != nil {
|
||||
for _, sentData := range sender.sent {
|
||||
var got sessionrecording.Event
|
||||
if err := json.Unmarshal(sentData, &got); err != nil {
|
||||
t.Fatalf("failed to unmarshal sent event: %v", err)
|
||||
}
|
||||
|
||||
got.Timestamp = 0
|
||||
tt.wantEvent.Timestamp = got.Timestamp
|
||||
|
||||
got.UserAgent = ""
|
||||
tt.wantEvent.UserAgent = ""
|
||||
|
||||
if !bytes.Equal(got.Request.Body, tt.wantEvent.Request.Body) {
|
||||
t.Errorf("sent event body does not match wanted event body.\nGot: %s\nWant: %s", string(got.Request.Body), string(tt.wantEvent.Request.Body))
|
||||
}
|
||||
got.Request.Body = nil
|
||||
tt.wantEvent.Request.Body = nil
|
||||
|
||||
if !reflect.DeepEqual(&got, tt.wantEvent) {
|
||||
t.Errorf("sent event does not match wanted event.\nGot: %#v\nWant: %#v", &got, tt.wantEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package sessionrecording
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const (
|
||||
KubernetesAPIEventType = "kubernetes-api-request"
|
||||
)
|
||||
|
||||
// Event represents the top-level structure of a tsrecorder event.
|
||||
type Event struct {
|
||||
// Type specifies the kind of event being recorded (e.g., "kubernetes-api-request").
|
||||
Type string `json:"type"`
|
||||
|
||||
// ID is a reference of the path that this event is stored at in tsrecorder
|
||||
ID string `json:"id"`
|
||||
|
||||
// Timestamp is the time when the event was recorded represented as a unix timestamp.
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
|
||||
// UserAgent is the UerAgent specified in the request, which helps identify
|
||||
// the client software that initiated the request.
|
||||
UserAgent string `json:"userAgent"`
|
||||
|
||||
// Request holds details of the HTTP request.
|
||||
Request Request `json:"request"`
|
||||
|
||||
// Kubernetes contains Kubernetes-specific information about the request (if
|
||||
// the type is `kubernetes-api-request`)
|
||||
Kubernetes KubernetesRequestInfo `json:"kubernetes"`
|
||||
|
||||
// Source provides details about the client that initiated the request.
|
||||
Source Source `json:"source"`
|
||||
}
|
||||
|
||||
// copied from https://github.com/kubernetes/kubernetes/blob/11ade2f7dd264c2f52a4a1342458abbbaa3cb2b1/staging/src/k8s.io/apiserver/pkg/endpoints/request/requestinfo.go#L44
|
||||
// KubernetesRequestInfo contains Kubernetes specific information in the request (if the type is `kubernetes-api-request`)
|
||||
type KubernetesRequestInfo struct {
|
||||
// IsResourceRequest indicates whether or not the request is for an API resource or subresource
|
||||
IsResourceRequest bool
|
||||
// Path is the URL path of the request
|
||||
Path string
|
||||
// Verb is the kube verb associated with the request for API requests, not the http verb. This includes things like list and watch.
|
||||
// for non-resource requests, this is the lowercase http verb
|
||||
Verb string
|
||||
|
||||
APIPrefix string
|
||||
APIGroup string
|
||||
APIVersion string
|
||||
|
||||
Namespace string
|
||||
// Resource is the name of the resource being requested. This is not the kind. For example: pods
|
||||
Resource string
|
||||
// Subresource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind.
|
||||
// For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
|
||||
// (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
|
||||
Subresource string
|
||||
// Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in.
|
||||
Name string
|
||||
// Parts are the path parts for the request, always starting with /{resource}/{name}
|
||||
Parts []string
|
||||
|
||||
// FieldSelector contains the unparsed field selector from a request. It is only present if the apiserver
|
||||
// honors field selectors for the verb this request is associated with.
|
||||
FieldSelector string
|
||||
// LabelSelector contains the unparsed field selector from a request. It is only present if the apiserver
|
||||
// honors field selectors for the verb this request is associated with.
|
||||
LabelSelector string
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
// Node is the FQDN of the node originating the connection.
|
||||
// It is also the MagicDNS name for the node.
|
||||
// It does not have a trailing dot.
|
||||
// e.g. "host.tail-scale.ts.net"
|
||||
Node string `json:"node"`
|
||||
|
||||
// NodeID is the node ID of the node originating the connection.
|
||||
NodeID tailcfg.StableNodeID `json:"nodeID"`
|
||||
|
||||
// Tailscale-specific fields:
|
||||
// NodeTags is the list of tags on the node originating the connection (if any).
|
||||
NodeTags []string `json:"nodeTags,omitempty"`
|
||||
|
||||
// NodeUserID is the user ID of the node originating the connection (if not tagged).
|
||||
NodeUserID tailcfg.UserID `json:"nodeUserID,omitempty"` // if not tagged
|
||||
|
||||
// NodeUser is the LoginName of the node originating the connection (if not tagged).
|
||||
NodeUser string `json:"nodeUser,omitempty"`
|
||||
}
|
||||
|
||||
// Request holds information about a request.
|
||||
type Request struct {
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Body []byte `json:"body"`
|
||||
QueryParameters url.Values `json:"queryParameters"`
|
||||
}
|
||||
Loading…
Reference in New Issue