mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-01 17:49:02 +00:00
cmd/tsrecorder: adds sending api level logging to tsrecorder (#16960)
Updates #17141 Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
This commit is contained in:
@@ -6,6 +6,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
||||||
|
github.com/blang/semver/v4 from k8s.io/component-base/metrics
|
||||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
||||||
github.com/coder/websocket from tailscale.com/util/eventbus
|
github.com/coder/websocket from tailscale.com/util/eventbus
|
||||||
github.com/coder/websocket/internal/errd from github.com/coder/websocket
|
github.com/coder/websocket/internal/errd from github.com/coder/websocket
|
||||||
@@ -60,6 +61,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
github.com/google/gofuzz/bytesource from github.com/google/gofuzz
|
github.com/google/gofuzz/bytesource from github.com/google/gofuzz
|
||||||
github.com/google/uuid from github.com/prometheus-community/pro-bing+
|
github.com/google/uuid from github.com/prometheus-community/pro-bing+
|
||||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||||
|
W 💣 github.com/inconshreveable/mousetrap from github.com/spf13/cobra
|
||||||
github.com/josharian/intern from github.com/mailru/easyjson/jlexer
|
github.com/josharian/intern from github.com/mailru/easyjson/jlexer
|
||||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
|
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
|
||||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||||
@@ -87,17 +89,18 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil from github.com/prometheus/client_golang/prometheus/promhttp
|
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil from github.com/prometheus/client_golang/prometheus/promhttp
|
||||||
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header from github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil
|
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header from github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil
|
||||||
💣 github.com/prometheus/client_golang/prometheus from github.com/prometheus/client_golang/prometheus/collectors+
|
💣 github.com/prometheus/client_golang/prometheus from github.com/prometheus/client_golang/prometheus/collectors+
|
||||||
github.com/prometheus/client_golang/prometheus/collectors from sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics
|
github.com/prometheus/client_golang/prometheus/collectors from sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics+
|
||||||
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus+
|
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus+
|
||||||
github.com/prometheus/client_golang/prometheus/promhttp from sigs.k8s.io/controller-runtime/pkg/metrics/server+
|
github.com/prometheus/client_golang/prometheus/promhttp from sigs.k8s.io/controller-runtime/pkg/metrics/server+
|
||||||
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+
|
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+
|
||||||
github.com/prometheus/common/expfmt from github.com/prometheus/client_golang/prometheus+
|
github.com/prometheus/common/expfmt from github.com/prometheus/client_golang/prometheus+
|
||||||
github.com/prometheus/common/model from github.com/prometheus/client_golang/prometheus+
|
github.com/prometheus/common/model from github.com/prometheus/client_golang/prometheus+
|
||||||
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
|
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus+
|
||||||
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
|
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
|
||||||
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
|
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
|
||||||
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf
|
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf
|
||||||
github.com/spf13/pflag from k8s.io/client-go/tools/clientcmd
|
github.com/spf13/cobra from k8s.io/component-base/cli/flag
|
||||||
|
github.com/spf13/pflag from k8s.io/client-go/tools/clientcmd+
|
||||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||||
@@ -124,6 +127,12 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||||
|
go.opentelemetry.io/otel/attribute from go.opentelemetry.io/otel/trace
|
||||||
|
go.opentelemetry.io/otel/codes from go.opentelemetry.io/otel/trace
|
||||||
|
💣 go.opentelemetry.io/otel/internal from go.opentelemetry.io/otel/attribute
|
||||||
|
go.opentelemetry.io/otel/internal/attribute from go.opentelemetry.io/otel/attribute
|
||||||
|
go.opentelemetry.io/otel/trace from k8s.io/component-base/metrics
|
||||||
|
go.opentelemetry.io/otel/trace/embedded from go.opentelemetry.io/otel/trace
|
||||||
go.uber.org/multierr from go.uber.org/zap+
|
go.uber.org/multierr from go.uber.org/zap+
|
||||||
go.uber.org/zap from github.com/go-logr/zapr+
|
go.uber.org/zap from github.com/go-logr/zapr+
|
||||||
go.uber.org/zap/buffer from go.uber.org/zap/internal/bufferpool+
|
go.uber.org/zap/buffer from go.uber.org/zap/internal/bufferpool+
|
||||||
@@ -283,8 +292,9 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
k8s.io/apimachinery/pkg/api/meta/testrestmapper from k8s.io/client-go/testing
|
k8s.io/apimachinery/pkg/api/meta/testrestmapper from k8s.io/client-go/testing
|
||||||
k8s.io/apimachinery/pkg/api/resource from k8s.io/api/autoscaling/v1+
|
k8s.io/apimachinery/pkg/api/resource from k8s.io/api/autoscaling/v1+
|
||||||
k8s.io/apimachinery/pkg/api/validation from k8s.io/apimachinery/pkg/util/managedfields/internal+
|
k8s.io/apimachinery/pkg/api/validation from k8s.io/apimachinery/pkg/util/managedfields/internal+
|
||||||
|
k8s.io/apimachinery/pkg/api/validation/path from k8s.io/apiserver/pkg/endpoints/request
|
||||||
💣 k8s.io/apimachinery/pkg/apis/meta/internalversion from k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme+
|
💣 k8s.io/apimachinery/pkg/apis/meta/internalversion from k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme+
|
||||||
k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme from k8s.io/client-go/metadata
|
k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme from k8s.io/client-go/metadata+
|
||||||
k8s.io/apimachinery/pkg/apis/meta/internalversion/validation from k8s.io/client-go/util/watchlist
|
k8s.io/apimachinery/pkg/apis/meta/internalversion/validation from k8s.io/client-go/util/watchlist
|
||||||
💣 k8s.io/apimachinery/pkg/apis/meta/v1 from k8s.io/api/admission/v1+
|
💣 k8s.io/apimachinery/pkg/apis/meta/v1 from k8s.io/api/admission/v1+
|
||||||
k8s.io/apimachinery/pkg/apis/meta/v1/unstructured from k8s.io/apimachinery/pkg/runtime/serializer/versioning+
|
k8s.io/apimachinery/pkg/apis/meta/v1/unstructured from k8s.io/apimachinery/pkg/runtime/serializer/versioning+
|
||||||
@@ -327,13 +337,18 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
k8s.io/apimachinery/pkg/util/uuid from sigs.k8s.io/controller-runtime/pkg/internal/controller+
|
k8s.io/apimachinery/pkg/util/uuid from sigs.k8s.io/controller-runtime/pkg/internal/controller+
|
||||||
k8s.io/apimachinery/pkg/util/validation from k8s.io/apimachinery/pkg/api/validation+
|
k8s.io/apimachinery/pkg/util/validation from k8s.io/apimachinery/pkg/api/validation+
|
||||||
k8s.io/apimachinery/pkg/util/validation/field from k8s.io/apimachinery/pkg/api/errors+
|
k8s.io/apimachinery/pkg/util/validation/field from k8s.io/apimachinery/pkg/api/errors+
|
||||||
|
k8s.io/apimachinery/pkg/util/version from k8s.io/apiserver/pkg/features+
|
||||||
k8s.io/apimachinery/pkg/util/wait from k8s.io/client-go/tools/cache+
|
k8s.io/apimachinery/pkg/util/wait from k8s.io/client-go/tools/cache+
|
||||||
k8s.io/apimachinery/pkg/util/yaml from k8s.io/apimachinery/pkg/runtime/serializer/json
|
k8s.io/apimachinery/pkg/util/yaml from k8s.io/apimachinery/pkg/runtime/serializer/json
|
||||||
k8s.io/apimachinery/pkg/version from k8s.io/client-go/discovery+
|
k8s.io/apimachinery/pkg/version from k8s.io/client-go/discovery+
|
||||||
k8s.io/apimachinery/pkg/watch from k8s.io/apimachinery/pkg/apis/meta/v1+
|
k8s.io/apimachinery/pkg/watch from k8s.io/apimachinery/pkg/apis/meta/v1+
|
||||||
k8s.io/apimachinery/third_party/forked/golang/json from k8s.io/apimachinery/pkg/util/strategicpatch
|
k8s.io/apimachinery/third_party/forked/golang/json from k8s.io/apimachinery/pkg/util/strategicpatch
|
||||||
k8s.io/apimachinery/third_party/forked/golang/reflect from k8s.io/apimachinery/pkg/conversion
|
k8s.io/apimachinery/third_party/forked/golang/reflect from k8s.io/apimachinery/pkg/conversion
|
||||||
|
k8s.io/apiserver/pkg/authentication/user from k8s.io/apiserver/pkg/endpoints/request
|
||||||
|
k8s.io/apiserver/pkg/endpoints/request from tailscale.com/k8s-operator/api-proxy
|
||||||
|
k8s.io/apiserver/pkg/features from k8s.io/apiserver/pkg/endpoints/request
|
||||||
k8s.io/apiserver/pkg/storage/names from tailscale.com/cmd/k8s-operator
|
k8s.io/apiserver/pkg/storage/names from tailscale.com/cmd/k8s-operator
|
||||||
|
k8s.io/apiserver/pkg/util/feature from k8s.io/apiserver/pkg/endpoints/request+
|
||||||
k8s.io/client-go/applyconfigurations/admissionregistration/v1 from k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1+
|
k8s.io/client-go/applyconfigurations/admissionregistration/v1 from k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1+
|
||||||
k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1 from k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1
|
k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1 from k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1
|
||||||
k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1 from k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1
|
k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1 from k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1
|
||||||
@@ -603,6 +618,13 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
k8s.io/client-go/util/keyutil from k8s.io/client-go/util/cert
|
k8s.io/client-go/util/keyutil from k8s.io/client-go/util/cert
|
||||||
k8s.io/client-go/util/watchlist from k8s.io/client-go/dynamic+
|
k8s.io/client-go/util/watchlist from k8s.io/client-go/dynamic+
|
||||||
k8s.io/client-go/util/workqueue from k8s.io/client-go/transport+
|
k8s.io/client-go/util/workqueue from k8s.io/client-go/transport+
|
||||||
|
k8s.io/component-base/cli/flag from k8s.io/component-base/featuregate
|
||||||
|
k8s.io/component-base/featuregate from k8s.io/apiserver/pkg/features+
|
||||||
|
k8s.io/component-base/metrics from k8s.io/component-base/metrics/legacyregistry+
|
||||||
|
k8s.io/component-base/metrics/legacyregistry from k8s.io/component-base/metrics/prometheus/feature
|
||||||
|
k8s.io/component-base/metrics/prometheus/feature from k8s.io/component-base/featuregate
|
||||||
|
k8s.io/component-base/metrics/prometheusextension from k8s.io/component-base/metrics
|
||||||
|
k8s.io/component-base/version from k8s.io/component-base/featuregate+
|
||||||
k8s.io/klog/v2 from k8s.io/apimachinery/pkg/api/meta+
|
k8s.io/klog/v2 from k8s.io/apimachinery/pkg/api/meta+
|
||||||
k8s.io/klog/v2/internal/buffer from k8s.io/klog/v2
|
k8s.io/klog/v2/internal/buffer from k8s.io/klog/v2
|
||||||
k8s.io/klog/v2/internal/clock from k8s.io/klog/v2
|
k8s.io/klog/v2/internal/clock from k8s.io/klog/v2
|
||||||
@@ -1162,7 +1184,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
sync/atomic from context+
|
sync/atomic from context+
|
||||||
syscall from crypto/internal/sysrand+
|
syscall from crypto/internal/sysrand+
|
||||||
text/tabwriter from k8s.io/apimachinery/pkg/util/diff+
|
text/tabwriter from k8s.io/apimachinery/pkg/util/diff+
|
||||||
text/template from html/template
|
text/template from html/template+
|
||||||
text/template/parse from html/template+
|
text/template/parse from html/template+
|
||||||
time from compress/gzip+
|
time from compress/gzip+
|
||||||
unicode from bytes+
|
unicode from bytes+
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const (
|
|||||||
AnnotationHostname = "tailscale.com/hostname"
|
AnnotationHostname = "tailscale.com/hostname"
|
||||||
annotationTailnetTargetIPOld = "tailscale.com/ts-tailnet-target-ip"
|
annotationTailnetTargetIPOld = "tailscale.com/ts-tailnet-target-ip"
|
||||||
AnnotationTailnetTargetIP = "tailscale.com/tailnet-ip"
|
AnnotationTailnetTargetIP = "tailscale.com/tailnet-ip"
|
||||||
//MagicDNS name of tailnet node.
|
// MagicDNS name of tailnet node.
|
||||||
AnnotationTailnetTargetFQDN = "tailscale.com/tailnet-fqdn"
|
AnnotationTailnetTargetFQDN = "tailscale.com/tailnet-fqdn"
|
||||||
|
|
||||||
AnnotationProxyGroup = "tailscale.com/proxy-group"
|
AnnotationProxyGroup = "tailscale.com/proxy-group"
|
||||||
|
|||||||
@@ -151,5 +151,5 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
# nix-direnv cache busting line: sha256-jsmQ0S1Uh1cU/kr0onYLJY9VYcFx297QZjQALM3wX10=
|
# nix-direnv cache busting line: sha256-rV3C2Vi48FCifGt58OdEO4+Av0HRIs8sUJVvp/gEBLw=
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -136,6 +136,7 @@ require (
|
|||||||
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
|
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
|
||||||
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
|
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
|
||||||
github.com/armon/go-metrics v0.4.1 // indirect
|
github.com/armon/go-metrics v0.4.1 // indirect
|
||||||
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
github.com/boltdb/bolt v1.3.1 // indirect
|
github.com/boltdb/bolt v1.3.1 // indirect
|
||||||
github.com/bombsimon/wsl/v4 v4.2.1 // indirect
|
github.com/bombsimon/wsl/v4 v4.2.1 // indirect
|
||||||
github.com/butuzov/mirror v1.1.0 // indirect
|
github.com/butuzov/mirror v1.1.0 // indirect
|
||||||
@@ -186,6 +187,7 @@ require (
|
|||||||
go.uber.org/automaxprocs v1.5.3 // indirect
|
go.uber.org/automaxprocs v1.5.3 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||||
|
k8s.io/component-base v0.32.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
sha256-jsmQ0S1Uh1cU/kr0onYLJY9VYcFx297QZjQALM3wX10=
|
sha256-rV3C2Vi48FCifGt58OdEO4+Av0HRIs8sUJVvp/gEBLw=
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -178,6 +178,8 @@ github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJ
|
|||||||
github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM=
|
github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM=
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||||
|
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
|
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
|
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
|
||||||
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
|
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
|
||||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||||
@@ -1546,6 +1548,8 @@ k8s.io/apiserver v0.32.0 h1:VJ89ZvQZ8p1sLeiWdRJpRD6oLozNZD2+qVSLi+ft5Qs=
|
|||||||
k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag=
|
k8s.io/apiserver v0.32.0/go.mod h1:HFh+dM1/BE/Hm4bS4nTXHVfN6Z6tFIZPi649n83b4Ag=
|
||||||
k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
|
k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
|
||||||
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
|
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
|
||||||
|
k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU=
|
||||||
|
k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM=
|
||||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
package apiproxy
|
package apiproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@@ -19,13 +22,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/transport"
|
"k8s.io/client-go/transport"
|
||||||
"tailscale.com/client/local"
|
"tailscale.com/client/local"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/k8s-operator/sessionrecording"
|
|
||||||
ksr "tailscale.com/k8s-operator/sessionrecording"
|
ksr "tailscale.com/k8s-operator/sessionrecording"
|
||||||
"tailscale.com/kube/kubetypes"
|
"tailscale.com/kube/kubetypes"
|
||||||
|
"tailscale.com/net/netx"
|
||||||
|
"tailscale.com/sessionrecording"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tsnet"
|
"tailscale.com/tsnet"
|
||||||
"tailscale.com/util/clientmetric"
|
"tailscale.com/util/clientmetric"
|
||||||
@@ -83,12 +89,13 @@ func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsn
|
|||||||
}
|
}
|
||||||
|
|
||||||
ap := &APIServerProxy{
|
ap := &APIServerProxy{
|
||||||
log: zlog,
|
log: zlog,
|
||||||
lc: lc,
|
lc: lc,
|
||||||
authMode: mode == kubetypes.APIServerProxyModeAuth,
|
authMode: mode == kubetypes.APIServerProxyModeAuth,
|
||||||
https: https,
|
https: https,
|
||||||
upstreamURL: u,
|
upstreamURL: u,
|
||||||
ts: ts,
|
ts: ts,
|
||||||
|
sendEventFunc: sessionrecording.SendEvent,
|
||||||
}
|
}
|
||||||
ap.rp = &httputil.ReverseProxy{
|
ap.rp = &httputil.ReverseProxy{
|
||||||
Rewrite: func(pr *httputil.ProxyRequest) {
|
Rewrite: func(pr *httputil.ProxyRequest) {
|
||||||
@@ -183,6 +190,8 @@ type APIServerProxy struct {
|
|||||||
ts *tsnet.Server
|
ts *tsnet.Server
|
||||||
hs *http.Server
|
hs *http.Server
|
||||||
upstreamURL *url.URL
|
upstreamURL *url.URL
|
||||||
|
|
||||||
|
sendEventFunc func(ap netip.AddrPort, event io.Reader, dial netx.DialFunc) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveDefault is the default handler for Kubernetes API server requests.
|
// serveDefault is the default handler for Kubernetes API server requests.
|
||||||
@@ -192,7 +201,16 @@ func (ap *APIServerProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
|
|||||||
ap.authError(w, err)
|
ap.authError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = ap.recordRequestAsEvent(r, who); err != nil {
|
||||||
|
msg := fmt.Sprintf("error recording Kubernetes API request: %v", err)
|
||||||
|
ap.log.Errorf(msg)
|
||||||
|
http.Error(w, msg, http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
counterNumRequestsProxied.Add(1)
|
counterNumRequestsProxied.Add(1)
|
||||||
|
|
||||||
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +238,7 @@ func (ap *APIServerProxy) serveAttachWS(w http.ResponseWriter, r *http.Request)
|
|||||||
ap.sessionForProto(w, r, ksr.AttachSessionType, ksr.WSProtocol)
|
ap.sessionForProto(w, r, ksr.AttachSessionType, ksr.WSProtocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request, sessionType sessionrecording.SessionType, proto ksr.Protocol) {
|
func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request, sessionType ksr.SessionType, proto ksr.Protocol) {
|
||||||
const (
|
const (
|
||||||
podNameKey = "pod"
|
podNameKey = "pod"
|
||||||
namespaceNameKey = "namespace"
|
namespaceNameKey = "namespace"
|
||||||
@@ -232,6 +250,14 @@ func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request
|
|||||||
ap.authError(w, err)
|
ap.authError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = ap.recordRequestAsEvent(r, who); err != nil {
|
||||||
|
msg := fmt.Sprintf("error recording Kubernetes API request: %v", err)
|
||||||
|
ap.log.Errorf(msg)
|
||||||
|
http.Error(w, msg, http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
counterNumRequestsProxied.Add(1)
|
counterNumRequestsProxied.Add(1)
|
||||||
failOpen, addrs, err := determineRecorderConfig(who)
|
failOpen, addrs, err := determineRecorderConfig(who)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -283,6 +309,107 @@ func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request
|
|||||||
ap.rp.ServeHTTP(h, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
ap.rp.ServeHTTP(h, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ap *APIServerProxy) recordRequestAsEvent(req *http.Request, who *apitype.WhoIsResponse) error {
|
||||||
|
failOpen, addrs, err := determineRecorderConfig(who)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error trying to determine whether the kubernetes api request needs to be recorded: %w", err)
|
||||||
|
}
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
if failOpen {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("forbidden: kubernetes api request must be recorded, but no recorders are available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
factory := &request.RequestInfoFactory{
|
||||||
|
APIPrefixes: sets.NewString("api", "apis"),
|
||||||
|
GrouplessAPIPrefixes: sets.NewString("api"),
|
||||||
|
}
|
||||||
|
|
||||||
|
reqInfo, err := factory.NewRequestInfo(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing request %s %s: %w", req.Method, req.URL.Path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeReqInfo := sessionrecording.KubernetesRequestInfo{
|
||||||
|
IsResourceRequest: reqInfo.IsResourceRequest,
|
||||||
|
Path: reqInfo.Path,
|
||||||
|
Verb: reqInfo.Verb,
|
||||||
|
APIPrefix: reqInfo.APIPrefix,
|
||||||
|
APIGroup: reqInfo.APIGroup,
|
||||||
|
APIVersion: reqInfo.APIVersion,
|
||||||
|
Namespace: reqInfo.Namespace,
|
||||||
|
Resource: reqInfo.Resource,
|
||||||
|
Subresource: reqInfo.Subresource,
|
||||||
|
Name: reqInfo.Name,
|
||||||
|
Parts: reqInfo.Parts,
|
||||||
|
FieldSelector: reqInfo.FieldSelector,
|
||||||
|
LabelSelector: reqInfo.LabelSelector,
|
||||||
|
}
|
||||||
|
event := &sessionrecording.Event{
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Kubernetes: kubeReqInfo,
|
||||||
|
Type: sessionrecording.KubernetesAPIEventType,
|
||||||
|
UserAgent: req.UserAgent(),
|
||||||
|
Request: sessionrecording.Request{
|
||||||
|
Method: req.Method,
|
||||||
|
Path: req.URL.String(),
|
||||||
|
QueryParameters: req.URL.Query(),
|
||||||
|
},
|
||||||
|
Source: sessionrecording.Source{
|
||||||
|
NodeID: who.Node.StableID,
|
||||||
|
Node: strings.TrimSuffix(who.Node.Name, "."),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !who.Node.IsTagged() {
|
||||||
|
event.Source.NodeUser = who.UserProfile.LoginName
|
||||||
|
event.Source.NodeUserID = who.UserProfile.ID
|
||||||
|
} else {
|
||||||
|
event.Source.NodeTags = who.Node.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read body: %w", err)
|
||||||
|
}
|
||||||
|
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
event.Request.Body = bodyBytes
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
// TODO: ChaosInTheCRD ensure that if there are multiple addrs timing out we don't experience slowdown on client waiting for response.
|
||||||
|
fail := true
|
||||||
|
for _, addr := range addrs {
|
||||||
|
data := new(bytes.Buffer)
|
||||||
|
if err := json.NewEncoder(data).Encode(event); err != nil {
|
||||||
|
return fmt.Errorf("error marshaling request event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ap.sendEventFunc(addr, data, ap.ts.Dial); err != nil {
|
||||||
|
if apiSupportErr, ok := err.(sessionrecording.EventAPINotSupportedErr); ok {
|
||||||
|
ap.log.Warnf(apiSupportErr.Error())
|
||||||
|
fail = false
|
||||||
|
} else {
|
||||||
|
err := fmt.Errorf("error sending event to recorder with address %q: %v", addr.String(), err)
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
merr := errors.Join(errs...)
|
||||||
|
if fail && failOpen {
|
||||||
|
msg := fmt.Sprintf("[unexpected] failed to send event to recorders with errors: %s", merr.Error())
|
||||||
|
msg = msg + "; failure mode is 'fail open'; continuing request without recording."
|
||||||
|
ap.log.Warn(msg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return merr
|
||||||
|
}
|
||||||
|
|
||||||
func (ap *APIServerProxy) addImpersonationHeadersAsRequired(r *http.Request) {
|
func (ap *APIServerProxy) addImpersonationHeadersAsRequired(r *http.Request) {
|
||||||
r.URL.Scheme = ap.upstreamURL.Scheme
|
r.URL.Scheme = ap.upstreamURL.Scheme
|
||||||
r.URL.Host = ap.upstreamURL.Host
|
r.URL.Host = ap.upstreamURL.Host
|
||||||
|
|||||||
548
k8s-operator/api-proxy/proxy_events_test.go
Normal file
548
k8s-operator/api-proxy/proxy_events_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,97 @@ func supportsV2(ctx context.Context, hc *http.Client, ap netip.AddrPort) bool {
|
|||||||
return resp.StatusCode == http.StatusOK && resp.ProtoMajor > 1
|
return resp.StatusCode == http.StatusOK && resp.ProtoMajor > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// supportsEvent checks whether a recorder instance supports the /v2/event
|
||||||
|
// endpoint.
|
||||||
|
func supportsEvent(ctx context.Context, hc *http.Client, ap netip.AddrPort) (bool, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, http2ProbeTimeout)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, httpm.HEAD, fmt.Sprintf("http://%s/v2/event", ap), nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
resp, err := hc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusNotFound {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
// Handle the case where reading the body itself fails
|
||||||
|
return false, fmt.Errorf("server returned non-OK status: %s, and failed to read body: %w", resp.Status, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("server returned non-OK status: %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressNotSupportEventv2 = `recorder at address %q does not support "/v2/event" endpoint`
|
||||||
|
|
||||||
|
type EventAPINotSupportedErr struct {
|
||||||
|
ap netip.AddrPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e EventAPINotSupportedErr) Error() string {
|
||||||
|
return fmt.Sprintf(addressNotSupportEventv2, e.ap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEvent sends an event the tsrecorders /v2/event endpoint.
|
||||||
|
func SendEvent(ap netip.AddrPort, event io.Reader, dial netx.DialFunc) (retErr error) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer func() {
|
||||||
|
if retErr != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := clientHTTP1(ctx, dial)
|
||||||
|
|
||||||
|
supported, err := supportsEvent(ctx, client, ap)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error checking support for `/v2/event` endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !supported {
|
||||||
|
return EventAPINotSupportedErr{
|
||||||
|
ap: ap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://%s/v2/event", ap.String()), event)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error sending request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
// Handle the case where reading the body itself fails
|
||||||
|
return fmt.Errorf("server returned non-OK status: %s, and failed to read body: %w", resp.Status, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("server returned non-OK status: %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// connectV1 connects to the legacy /record endpoint on the recorder. It is
|
// connectV1 connects to the legacy /record endpoint on the recorder. It is
|
||||||
// used for backwards-compatibility with older tsrecorder instances.
|
// used for backwards-compatibility with older tsrecorder instances.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -148,9 +150,9 @@ func TestConnectToRecorder(t *testing.T) {
|
|||||||
// Wire up h2c-compatible HTTP/2 server. This is optional
|
// Wire up h2c-compatible HTTP/2 server. This is optional
|
||||||
// because the v1 recorder didn't support HTTP/2 and we try to
|
// because the v1 recorder didn't support HTTP/2 and we try to
|
||||||
// mimic that.
|
// mimic that.
|
||||||
h2s := &http2.Server{}
|
s := &http2.Server{}
|
||||||
srv.Config.Handler = h2c.NewHandler(mux, h2s)
|
srv.Config.Handler = h2c.NewHandler(mux, s)
|
||||||
if err := http2.ConfigureServer(srv.Config, h2s); err != nil {
|
if err := http2.ConfigureServer(srv.Config, s); err != nil {
|
||||||
t.Errorf("configuring HTTP/2 support in server: %v", err)
|
t.Errorf("configuring HTTP/2 support in server: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,3 +189,97 @@ func TestConnectToRecorder(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSendEvent(t *testing.T) {
|
||||||
|
t.Run("supported", func(t *testing.T) {
|
||||||
|
eventBody := `{"foo":"bar"}`
|
||||||
|
eventRecieved := make(chan []byte, 1)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("HEAD /v2/event", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("POST /v2/event", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
eventRecieved <- body
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := httptest.NewUnstartedServer(mux)
|
||||||
|
s := &http2.Server{}
|
||||||
|
srv.Config.Handler = h2c.NewHandler(mux, s)
|
||||||
|
if err := http2.ConfigureServer(srv.Config, s); err != nil {
|
||||||
|
t.Fatalf("configuring HTTP/2 support in server: %v", err)
|
||||||
|
}
|
||||||
|
srv.Start()
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
d := new(net.Dialer)
|
||||||
|
addr := netip.MustParseAddrPort(srv.Listener.Addr().String())
|
||||||
|
err := SendEvent(addr, bytes.NewBufferString(eventBody), d.DialContext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendEvent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if recv := string(<-eventRecieved); recv != eventBody {
|
||||||
|
t.Errorf("mismatch in event body, sent %q, received %q", eventBody, recv)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not_supported", func(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("HEAD /v2/event", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := httptest.NewUnstartedServer(mux)
|
||||||
|
s := &http2.Server{}
|
||||||
|
srv.Config.Handler = h2c.NewHandler(mux, s)
|
||||||
|
if err := http2.ConfigureServer(srv.Config, s); err != nil {
|
||||||
|
t.Fatalf("configuring HTTP/2 support in server: %v", err)
|
||||||
|
}
|
||||||
|
srv.Start()
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
d := new(net.Dialer)
|
||||||
|
addr := netip.MustParseAddrPort(srv.Listener.Addr().String())
|
||||||
|
err := SendEvent(addr, nil, d.DialContext)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), fmt.Sprintf(addressNotSupportEventv2, srv.Listener.Addr().String())) {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("server_error", func(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("HEAD /v2/event", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("POST /v2/event", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := httptest.NewUnstartedServer(mux)
|
||||||
|
s := &http2.Server{}
|
||||||
|
srv.Config.Handler = h2c.NewHandler(mux, s)
|
||||||
|
if err := http2.ConfigureServer(srv.Config, s); err != nil {
|
||||||
|
t.Fatalf("configuring HTTP/2 support in server: %v", err)
|
||||||
|
}
|
||||||
|
srv.Start()
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
d := new(net.Dialer)
|
||||||
|
addr := netip.MustParseAddrPort(srv.Listener.Addr().String())
|
||||||
|
err := SendEvent(addr, nil, d.DialContext)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "server returned non-OK status") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
104
sessionrecording/event.go
Normal file
104
sessionrecording/event.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
@@ -62,7 +62,6 @@ type CastHeader struct {
|
|||||||
ConnectionID string `json:"connectionID"`
|
ConnectionID string `json:"connectionID"`
|
||||||
|
|
||||||
// Fields that are only set for Kubernetes API server proxy session recordings:
|
// Fields that are only set for Kubernetes API server proxy session recordings:
|
||||||
|
|
||||||
Kubernetes *Kubernetes `json:"kubernetes,omitempty"`
|
Kubernetes *Kubernetes `json:"kubernetes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user