diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000..2f79d01 --- /dev/null +++ b/.bazelignore @@ -0,0 +1,14 @@ +# Ignore bazel symlink directories +bazel-bin +bazel-out +bazel-testlogs +bazel-ksniff + +# Ignore existing build artifacts +kubectl-sniff +kubectl-sniff-windows +kubectl-sniff-darwin +kubectl-sniff-darwin-arm64 +static-tcpdump +ksniff.zip +tcpdump-* diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..996dd9c --- /dev/null +++ b/.bazelrc @@ -0,0 +1,21 @@ +# Enable Bzlmod +common --enable_bzlmod + +# Common build options +build --incompatible_enable_cc_toolchain_resolution +build --enable_platform_specific_config + +# Go specific options +build --@rules_go//go/config:pure + +# Platform-specific configurations +build:linux --platforms=@rules_go//go/toolchain:linux_amd64 +build:darwin --platforms=@rules_go//go/toolchain:darwin_amd64 +build:darwin_arm64 --platforms=@rules_go//go/toolchain:darwin_arm64 +build:windows --platforms=@rules_go//go/toolchain:windows_amd64 + +# Test output +test --test_output=errors + +# Performance +build --jobs=auto diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..e7fdef7 --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +8.4.2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b8743e --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +*~ +.#* + +# OS X Desktop Service Store +.DS_Store + +.generated + +# Build and test process +.coverage +coverage_html + +# bzlmod lockfile which is useless: +# "Importantly, it only includes dependencies that are included in the current invocation of the build" +/MODULE.bazel.lock + +/bazel-* + +# Local bazel overrides +.bazelrc-local + +# Go +go.mod +go.sum +!/go.mod +!/go.sum diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2fc4045 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,88 @@ +# Copyright (C) 2021 Cisco Systems, Inc. + +# See CONTRIBUTING.md for instructions. +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +# +# Hook naming convention - prefixes by category (run order): +# Format: - Code formatters that modify files +# Deps: - Dependency/lockfile hygiene (go mod tidy, pip-compile, tfupdate) +# Lint: - Static analysis/linting (read-only checks) +# Files: - File hygiene checks (whitespace, symlinks, text normalization) +# License: - Copyright/license checks +# SAST: - Security static analysis (secrets, config scanning, code analysis) +# Vuln: - Dependency vulnerability scanning + +repos: + # ============================================================================ + # FORMAT: Code formatters (modify files) + # ============================================================================ + - repo: https://github.com/keith/pre-commit-buildifier + rev: 8.2.1 + hooks: + - id: buildifier + name: "Format: buildifier" + args: + # Keep this argument in sync with .bazelci/presubmit.yml and the buildifier-lint hook in LINT section + - --warnings=-bzl-visibility,-function-docstring-args,-function-docstring-return,-print,-unnamed-macro,-provider-params,-function-docstring-header,-no-effect,-uninitialized,-rule-impl-return + + # ============================================================================ + # LINT: Static analysis (read-only checks) + # ============================================================================ + - repo: https://github.com/keith/pre-commit-buildifier + rev: 8.2.1 + hooks: + - id: buildifier-lint + name: "Lint: buildifier-lint" + args: + # Keep this argument in sync with the buildifier hook in FORMAT section + - --warnings=-bzl-visibility,-function-docstring-args,-function-docstring-return,-print,-unnamed-macro,-provider-params,-function-docstring-header,-no-effect,-uninitialized,-rule-impl-return + + - repo: https://github.com/syntaqx/git-hooks + rev: v0.0.18 + hooks: + - id: shellcheck + name: "Lint: shellcheck" + exclude: "aws_launch_template_.*" + + - repo: git@github.com:omnicate/pre-commit-policy-bot.git + rev: v0.0.2 + hooks: + - id: validate + name: "Lint: policy-bot-validate" + + # ============================================================================ + # FILES: File hygiene checks + # ============================================================================ + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + name: "Files: trailing-whitespace" + args: [--markdown-linebreak-ext=md] + # provisioning-tools has test fixtures with significant formatting + exclude: "connectivity/provisioning-tools/src/test/kotlin/io/omnicate/connectivity/tools/(ImportSimCardsTest|mvno/MappingTest).kt" + - id: check-merge-conflict + name: "Files: check-merge-conflict" + - id: end-of-file-fixer + name: "Files: end-of-file-fixer" + exclude: ".*\\.svg$|voice/li/test/data/.*\\.bin$|copyright.txt|vendor-images/README.md" + - id: check-executables-have-shebangs + name: "Files: check-executables-have-shebangs" + - id: check-shebang-scripts-are-executable + name: "Files: check-shebang-scripts-are-executable" + exclude: "aws_launch_template_.*" + - id: check-symlinks + name: "Files: check-symlinks" + - id: destroyed-symlinks + name: "Files: destroyed-symlinks" + + - repo: https://github.com/sirosen/texthooks + rev: 0.7.1 + hooks: + - id: fix-smartquotes + name: "Files: fix-smartquotes" + exclude: ".+\\.vue$|.+\\.vm|.+\\.java|.+\\.kt|.+\\.xml|.+\\.json|.+\\.sql|.*.gen.go$" + - id: fix-ligatures + name: "Files: fix-ligatures" + exclude: ".+\\.vue$|.+\\.vm|.+\\.java|.+\\.kt|.+\\.xml|.+\\.json|.+\\.sql|.*.gen.go$" diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..0b987a0 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,81 @@ +load("@gazelle//:def.bzl", "gazelle") +load("@rules_go//go:def.bzl", "go_binary") + +# gazelle:prefix ksniff +gazelle(name = "gazelle") + +# Linux binary +go_binary( + name = "kubectl-sniff-linux", + embed = ["//cmd:kubectl-sniff_lib"], + goarch = "amd64", + goos = "linux", + pure = "on", + visibility = ["//visibility:public"], +) + +# Darwin (macOS) binary - amd64 +go_binary( + name = "kubectl-sniff-darwin", + embed = ["//cmd:kubectl-sniff_lib"], + goarch = "amd64", + goos = "darwin", + pure = "on", + visibility = ["//visibility:public"], +) + +# Darwin (macOS) binary - arm64 +go_binary( + name = "kubectl-sniff-darwin-arm64", + embed = ["//cmd:kubectl-sniff_lib"], + goarch = "arm64", + goos = "darwin", + pure = "on", + visibility = ["//visibility:public"], +) + +# Windows binary +go_binary( + name = "kubectl-sniff-windows", + embed = ["//cmd:kubectl-sniff_lib"], + goarch = "amd64", + goos = "windows", + pure = "on", + visibility = ["//visibility:public"], +) + +# Default binary for current platform +go_binary( + name = "kubectl-sniff", + embed = ["//cmd:kubectl-sniff_lib"], + pure = "on", + visibility = ["//visibility:public"], +) + +# Static tcpdump binary +# This target builds a statically-linked tcpdump binary from source. +# On Linux: Builds a fully static tcpdump binary (equivalent to Makefile's static-tcpdump target) +# On macOS/other: Creates a placeholder script explaining that this must be built on Linux +# +# Usage: +# bazel build //:static-tcpdump +# +# The binary will be available at: +# bazel-bin/external/+_repo_rules+tcpdump/tcpdump_bin +alias( + name = "static-tcpdump", + actual = "@tcpdump//:tcpdump_bin", + visibility = ["//visibility:public"], +) + +# Filegroup for all binaries +filegroup( + name = "all-binaries", + srcs = [ + ":kubectl-sniff-darwin", + ":kubectl-sniff-darwin-arm64", + ":kubectl-sniff-linux", + ":kubectl-sniff-windows", + ], + visibility = ["//visibility:public"], +) diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..9d91cdd --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,38 @@ +module( + name = "ksniff", + version = "0.0.0", +) + +bazel_dep(name = "rules_go", version = "0.59.0") +bazel_dep(name = "gazelle", version = "0.47.0") +bazel_dep(name = "platforms", version = "1.0.0") + +go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk") +go_sdk.download(version = "1.25.7") + +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "//:go.mod") + +# Use all Go dependencies +use_repo(go_deps, "com_github_mitchellh_go_homedir") +use_repo(go_deps, "com_github_pkg_errors") +use_repo(go_deps, "com_github_sirupsen_logrus") +use_repo(go_deps, "com_github_spf13_cobra") +use_repo(go_deps, "com_github_spf13_pflag") +use_repo(go_deps, "com_github_spf13_viper") +use_repo(go_deps, "com_github_stretchr_testify") +use_repo(go_deps, "io_k8s_api") +use_repo(go_deps, "io_k8s_apimachinery") +use_repo(go_deps, "io_k8s_cli_runtime") +use_repo(go_deps, "io_k8s_client_go") + +# HTTP archive for tcpdump +http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "tcpdump", + build_file = "//:tcpdump.BUILD", + sha256 = "798b3536a29832ce0cbb07fafb1ce5097c95e308a6f592d14052e1ef1505fe79", + strip_prefix = "tcpdump-4.9.2", + urls = ["https://www.tcpdump.org/release/tcpdump-4.9.2.tar.gz"], +) diff --git a/README.md b/README.md index 3cf50c5..07e01ba 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ Ksniff [isn't production ready yet](https://github.com/eldadru/ksniff/issues/96# Installation via krew (https://github.com/GoogleContainerTools/krew) kubectl krew install sniff - -For manual installation, download the latest release package, unzip it and use the attached makefile: + +For manual installation, download the latest release package, unzip it and use the attached makefile: unzip ksniff.zip make install @@ -42,11 +42,11 @@ Requirements: 2. go 1.11 or newer Compiling: - + linux: make linux windows: make windows mac: make darwin - + To compile a static tcpdump binary: @@ -56,10 +56,10 @@ To compile a static tcpdump binary: kubectl < 1.12: kubectl plugin sniff [-n ] [-c ] [-i ] [-f ] [-o OUTPUT_FILE] [-l LOCAL_TCPDUMP_FILE] [-r REMOTE_TCPDUMP_FILE] - + kubectl >= 1.12: kubectl sniff [-n ] [-c ] [-i ] [-f ] [-o OUTPUT_FILE] [-l LOCAL_TCPDUMP_FILE] [-r REMOTE_TCPDUMP_FILE] - + POD_NAME: Required. the name of the kubernetes pod to start capture it's traffic. NAMESPACE_NAME: Optional. Namespace name. used to specify the target namespace to operate on. CONTAINER_NAME: Optional. If omitted, the first container in the pod will be chosen. @@ -71,9 +71,16 @@ To compile a static tcpdump binary: #### Air gapped environments Use `--image` and `--tcpdump-image` flags (or KUBECTL_PLUGINS_LOCAL_FLAG_IMAGE and KUBECTL_PLUGINS_LOCAL_FLAG_TCPDUMP_IMAGE environment variables) to override the default container images and use your own e.g (docker): - + kubectl plugin sniff [-n ] [-c ] --image /docker --tcpdump-image /tcpdump - + +#### Private registry with imagePullSecret +If your custom images are stored in a private registry that requires authentication, you can specify an imagePullSecret using the `--image-pull-secret` flag (or KUBECTL_PLUGINS_LOCAL_FLAG_IMAGE_PULL_SECRET environment variable): + + kubectl plugin sniff [-n ] [-c ] --image /docker --tcpdump-image /tcpdump --image-pull-secret + +The secret must exist in the same namespace where the ksniff privileged pod will be created. + #### Non-Privileged and Scratch Pods To reduce attack surface and have small and lean containers, many production-ready containers runs as non-privileged user @@ -82,7 +89,7 @@ or even as a scratch container. To support those containers as well, ksniff now ships with the "-p" (privileged) mode. When executed with the -p flag, ksniff will create a new pod on the remote kubernetes cluster that will have access to the node docker daemon. -ksniff will than use that pod to execute a container attached to the target container network namespace +ksniff will than use that pod to execute a container attached to the target container network namespace and perform the actual network capture. #### Piping output to stdout @@ -94,12 +101,12 @@ Example using `tshark`: kubectl sniff pod-name -f "port 80" -o - | tshark -r - ### Contribution -More than welcome! please don't hesitate to open bugs, questions, pull requests +More than welcome! please don't hesitate to open bugs, questions, pull requests ### Future Work 1. Instead of uploading static tcpdump, use the future support of "kubectl debug" feature (https://github.com/kubernetes/community/pull/649) which should be a much cleaner solution. - + ### Known Issues #### Wireshark and TShark cannot read pcap diff --git a/README_golang.md b/README_golang.md new file mode 100644 index 0000000..79665b3 --- /dev/null +++ b/README_golang.md @@ -0,0 +1,14 @@ +# Go dependencies + +For the following commands its recommended to use the Bazel target `rules_go//go`, ensuring a consistent Go version. + +In order to add a dependency, you need to: +1. add it to the global `go.mod` using `bazel run @rules_go//go -- get name@version`. To view the latest version, run `go list -m -versions name`. +2. run `bazel run @rules_go//go -- mod tidy`. + +Since this will update dependencies for _all_ Go programs in the +monorepo, you should retest all the Go dependencies afterwards: + +``` +bazel test $(bazel query 'kind(go_.*, //...) except attr("tags", "manual", //...)') +``` diff --git a/cmd/BUILD.bazel b/cmd/BUILD.bazel new file mode 100644 index 0000000..c9ad7cb --- /dev/null +++ b/cmd/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "kubectl-sniff_lib", + srcs = ["kubectl-sniff.go"], + importpath = "ksniff/cmd", + visibility = ["//visibility:public"], + deps = [ + "//pkg/cmd", + "@com_github_spf13_pflag//:pflag", + "@io_k8s_cli_runtime//pkg/genericclioptions", + ], +) diff --git a/go.mod b/go.mod index 48353a2..7468dc2 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,88 @@ module ksniff +go 1.23.0 + +toolchain go1.23.4 + require ( github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect - github.com/elazarl/goproxy v1.2.1 // indirect - github.com/emicklei/go-restful v2.16.0+incompatible // indirect github.com/go-openapi/spec v0.19.5 // indirect github.com/go-openapi/swag v0.19.6 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/imdario/mergo v0.3.8 // indirect github.com/mitchellh/go-homedir v1.1.0 - github.com/pelletier/go-toml v1.6.0 // indirect github.com/pkg/errors v0.9.1 - github.com/sirupsen/logrus v1.8.3 - github.com/spf13/cast v1.3.1 // indirect - github.com/spf13/cobra v1.1.3 - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.7.0 - github.com/stretchr/testify v1.7.0 - gopkg.in/ini.v1 v1.51.1 // indirect + github.com/sirupsen/logrus v1.9.4 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.11.1 k8s.io/api v0.20.6 k8s.io/apimachinery v0.20.6 k8s.io/cli-runtime v0.20.6 k8s.io/client-go v0.20.6 ) -go 1.13 +require ( + cloud.google.com/go v0.54.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.1 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.5 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.0 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/evanphx/json-patch v4.9.0+incompatible // indirect + github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-logr/logr v0.2.0 // indirect + github.com/go-openapi/jsonpointer v0.19.3 // indirect + github.com/go-openapi/jsonreference v0.19.3 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.4.3 // indirect + github.com/google/btree v1.0.0 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/googleapis/gnostic v0.4.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.10 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect + google.golang.org/appengine v1.6.5 // indirect + google.golang.org/protobuf v1.25.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.4.0 // indirect + k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd // indirect + k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect + sigs.k8s.io/kustomize v2.0.3+incompatible // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.0.3 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) + +require ( + github.com/elazarl/goproxy v1.2.1 // indirect + github.com/emicklei/go-restful v2.16.0+incompatible // indirect + golang.org/x/term v0.27.0 // indirect +) diff --git a/go.sum b/go.sum index d5bfb4f..d5d44fa 100644 --- a/go.sum +++ b/go.sum @@ -39,7 +39,6 @@ github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8 github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -70,6 +69,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -93,9 +93,12 @@ github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -122,6 +125,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.6 h1:JSUbVWlaTLMhXeOMyArSUNCdroxZu2j1TcrsOV8Mj7Q= github.com/go-openapi/swag v0.19.6/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -176,7 +181,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -200,7 +204,6 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -212,15 +215,15 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -229,15 +232,16 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -255,7 +259,6 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -276,8 +279,8 @@ github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= -github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -299,37 +302,44 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.8.3 h1:DBBfY8eMYazKEJHb3JKpSPfpgd2mBCoNFlQx6C5fftU= -github.com/sirupsen/logrus v1.8.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -337,15 +347,15 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -354,6 +364,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -363,10 +375,6 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -399,11 +407,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -431,13 +434,6 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -453,12 +449,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -488,26 +478,8 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -516,14 +488,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -565,10 +531,6 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -636,8 +598,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho= -gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -648,8 +608,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/kube/BUILD.bazel b/kube/BUILD.bazel new file mode 100644 index 0000000..702c8da --- /dev/null +++ b/kube/BUILD.bazel @@ -0,0 +1,26 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "kube", + srcs = [ + "kubernetes_api_service.go", + "ops.go", + "tar.go", + ], + importpath = "ksniff/kube", + visibility = ["//visibility:public"], + deps = [ + "//pkg/service/sniffer/runtime", + "//utils", + "@com_github_pkg_errors//:errors", + "@com_github_sirupsen_logrus//:logrus", + "@io_k8s_api//core/v1:core", + "@io_k8s_apimachinery//pkg/api/resource", + "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", + "@io_k8s_client_go//kubernetes", + "@io_k8s_client_go//kubernetes/scheme", + "@io_k8s_client_go//rest", + "@io_k8s_client_go//tools/remotecommand", + "@io_k8s_client_go//util/exec", + ], +) diff --git a/kube/kubernetes_api_service.go b/kube/kubernetes_api_service.go index 1ed14a6..233a787 100644 --- a/kube/kubernetes_api_service.go +++ b/kube/kubernetes_api_service.go @@ -25,18 +25,18 @@ type KubernetesApiService interface { DeletePod(podName string) error - CreatePrivilegedPod(nodeName string, containerName string, image string, socketPath string, timeout time.Duration, serviceaccount string) (*corev1.Pod, error) + CreatePrivilegedPod(nodeName string, containerName string, image string, socketPath string, timeout time.Duration, serviceaccount string, imagePullSecret string) (*corev1.Pod, error) UploadFile(localPath string, remotePath string, podName string, containerName string) error } type KubernetesApiServiceImpl struct { - clientset *kubernetes.Clientset + clientset kubernetes.Interface restConfig *rest.Config targetNamespace string } -func NewKubernetesApiService(clientset *kubernetes.Clientset, +func NewKubernetesApiService(clientset kubernetes.Interface, restConfig *rest.Config, targetNamespace string) KubernetesApiService { return &KubernetesApiServiceImpl{clientset: clientset, @@ -102,7 +102,7 @@ func (k *KubernetesApiServiceImpl) DeletePod(podName string) error { return err } -func (k *KubernetesApiServiceImpl) CreatePrivilegedPod(nodeName string, containerName string, image string, socketPath string, timeout time.Duration, serviceaccount string) (*corev1.Pod, error) { +func (k *KubernetesApiServiceImpl) CreatePrivilegedPod(nodeName string, containerName string, image string, socketPath string, timeout time.Duration, serviceaccount string, imagePullSecret string) (*corev1.Pod, error) { log.Debugf("creating privileged pod on remote node") isSupported, err := k.IsSupportedContainerRuntime(nodeName) @@ -198,6 +198,14 @@ func (k *KubernetesApiServiceImpl) CreatePrivilegedPod(nodeName string, containe podSpecs.ServiceAccountName = serviceaccount } + if imagePullSecret != "" { + podSpecs.ImagePullSecrets = []corev1.LocalObjectReference{ + { + Name: imagePullSecret, + }, + } + } + pod := corev1.Pod{ TypeMeta: typeMetadata, ObjectMeta: objectMetadata, diff --git a/kube/ops.go b/kube/ops.go index 27326fa..dc4bb5c 100644 --- a/kube/ops.go +++ b/kube/ops.go @@ -16,7 +16,7 @@ import ( ) type KubeRequest struct { - Clientset *kubernetes.Clientset + Clientset kubernetes.Interface RestConfig *rest.Config Namespace string Pod string diff --git a/pkg/cmd/BUILD.bazel b/pkg/cmd/BUILD.bazel new file mode 100644 index 0000000..9067d10 --- /dev/null +++ b/pkg/cmd/BUILD.bazel @@ -0,0 +1,46 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "cmd", + srcs = ["sniff.go"], + importpath = "ksniff/pkg/cmd", + visibility = ["//visibility:public"], + deps = [ + "//kube", + "//pkg/config", + "//pkg/service/sniffer", + "//pkg/service/sniffer/runtime", + "@com_github_mitchellh_go_homedir//:go-homedir", + "@com_github_pkg_errors//:errors", + "@com_github_sirupsen_logrus//:logrus", + "@com_github_spf13_cobra//:cobra", + "@com_github_spf13_viper//:viper", + "@io_k8s_api//core/v1:core", + "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", + "@io_k8s_cli_runtime//pkg/genericclioptions", + "@io_k8s_client_go//kubernetes", + "@io_k8s_client_go//plugin/pkg/client/auth/azure", + "@io_k8s_client_go//plugin/pkg/client/auth/gcp", + "@io_k8s_client_go//plugin/pkg/client/auth/oidc", + "@io_k8s_client_go//rest", + "@io_k8s_client_go//tools/clientcmd", + "@io_k8s_client_go//tools/clientcmd/api", + ], +) + +go_test( + name = "cmd_test", + srcs = ["sniff_test.go"], + embed = [":cmd"], + deps = [ + "//pkg/config", + "@com_github_spf13_cobra//:cobra", + "@com_github_stretchr_testify//assert", + "@io_k8s_api//core/v1:core", + "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", + "@io_k8s_cli_runtime//pkg/genericclioptions", + "@io_k8s_client_go//kubernetes/fake", + "@io_k8s_client_go//rest", + "@io_k8s_client_go//tools/clientcmd/api", + ], +) diff --git a/pkg/cmd/sniff.go b/pkg/cmd/sniff.go index c79db3e..866ddb6 100644 --- a/pkg/cmd/sniff.go +++ b/pkg/cmd/sniff.go @@ -9,6 +9,7 @@ import ( "os/signal" "path/filepath" "strings" + "sync" "syscall" "time" @@ -48,12 +49,13 @@ var tcpdumpLocalBinaryPathLookupList []string type Ksniff struct { configFlags *genericclioptions.ConfigFlags resultingContext *api.Context - clientset *kubernetes.Clientset + clientset kubernetes.Interface restConfig *rest.Config rawConfig api.Config settings *config.KsniffSettings snifferService sniffer.SnifferService wireshark *exec.Cmd + cleanupOnce sync.Once } func NewKsniff(settings *config.KsniffSettings) *Ksniff { @@ -155,11 +157,31 @@ func NewCmdSniff(streams genericclioptions.IOStreams) *cobra.Command { _ = viper.BindEnv("serviceaccount", "KUBECTL_PLUGINS_LOCAL_FLAG_SERVICE_ACCOUNT") _ = viper.BindPFlag("serviceaccount", cmd.Flags().Lookup("serviceaccount")) + cmd.Flags().StringVarP(&ksniffSettings.UserSpecifiedImagePullSecret, "image-pull-secret", "", "", + "the image pull secret name for private registries (optional)") + _ = viper.BindEnv("image-pull-secret", "KUBECTL_PLUGINS_LOCAL_FLAG_IMAGE_PULL_SECRET") + _ = viper.BindPFlag("image-pull-secret", cmd.Flags().Lookup("image-pull-secret")) + return cmd } func (o *Ksniff) Complete(cmd *cobra.Command, args []string) error { + // Validate and populate basic settings + if err := o.completeBasicSettings(cmd, args); err != nil { + return err + } + + // Load Kubernetes configuration + if err := o.loadKubeConfig(); err != nil { + return err + } + + return nil +} +// completeBasicSettings validates arguments and populates settings from viper +// This method doesn't require Kubernetes configuration and can be tested independently +func (o *Ksniff) completeBasicSettings(cmd *cobra.Command, args []string) error { if len(args) < minimumNumberOfArguments { _ = cmd.Usage() return errors.New("not enough arguments") @@ -187,19 +209,27 @@ func (o *Ksniff) Complete(cmd *cobra.Command, args []string) error { o.settings.UseDefaultTCPDumpImage = !viper.IsSet("tcpdump-image") o.settings.UseDefaultSocketPath = !viper.IsSet("socket") o.settings.UserSpecifiedServiceAccount = viper.GetString("serviceaccount") - - var err error + o.settings.UserSpecifiedImagePullSecret = viper.GetString("image-pull-secret") if o.settings.UserSpecifiedVerboseMode { log.Info("running in verbose mode") log.SetLevel(log.DebugLevel) } + var err error tcpdumpLocalBinaryPathLookupList, err = o.buildTcpdumpBinaryPathLookupList() if err != nil { return err } + return nil +} + +// loadKubeConfig loads the Kubernetes configuration and initializes the clientset +// This method is separated to allow for easier testing and mocking +func (o *Ksniff) loadKubeConfig() error { + var err error + o.rawConfig, err = o.configFlags.ToRawKubeConfigLoader().RawConfig() if err != nil { return err @@ -361,6 +391,32 @@ func findLocalTcpdumpBinaryPath() (string, error) { return "", errors.Errorf("couldn't find static tcpdump binary on any of: '%v'", tcpdumpLocalBinaryPathLookupList) } +func (o *Ksniff) performCleanup() { + o.cleanupOnce.Do(func() { + log.Info("starting sniffer cleanup") + + // Kill wireshark if used + if o.wireshark != nil { + if o.wireshark.Process != nil { + err := o.wireshark.Process.Kill() + if err != nil && err != os.ErrProcessDone { + log.WithError(err).Error("failed to kill wireshark process") + } else { + log.Debug("wireshark process killed") + } + } + } + + // Cleanup sniffer service (removes privileged pods, etc.) + err := o.snifferService.Cleanup() + if err != nil { + log.WithError(err).Error("failed to cleanup sniffer, manual cleanup may be required") + } else { + log.Info("sniffer cleanup completed successfully") + } + }) +} + func (o *Ksniff) setupSignalHandler() chan interface{} { signals := make(chan os.Signal, 1) exit := make(chan interface{}) @@ -371,26 +427,9 @@ func (o *Ksniff) setupSignalHandler() chan interface{} { select { case sig := <-signals: if sig == syscall.SIGINT || sig == syscall.SIGTERM { - log.Info("starting sniffer cleanup") - err := o.snifferService.Cleanup() - if err != nil { - log.WithError(err).Error("failed to teardown sniffer, a manual teardown is required.") - } - log.Info("sniffer cleanup completed successfully") - - // Kill wireshark if used - if o.wireshark != nil { - if o.wireshark.Process != nil { - err = o.wireshark.Process.Kill() - if err != nil && err != os.ErrProcessDone { - log.WithError(err).Error("failed to kill wireshark process") - } else { - log.Debug("wireshark process killed") - } - } - } - + o.performCleanup() close(signals) + os.Exit(0) } case <-exit: return @@ -413,8 +452,9 @@ func (o *Ksniff) Run() error { // Ensure sniffer is clean on interrupt closeHandler := o.setupSignalHandler() - // Ensure sniffer is clean on complete + // Ensure sniffer is cleaned up on exit (both normal and error cases) defer func() { + o.performCleanup() closeHandler <- true }() diff --git a/pkg/cmd/sniff_test.go b/pkg/cmd/sniff_test.go index 2872c4b..92cd76a 100644 --- a/pkg/cmd/sniff_test.go +++ b/pkg/cmd/sniff_test.go @@ -1,14 +1,19 @@ package cmd import ( + "context" "ksniff/pkg/config" "strings" + "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" - - "testing" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd/api" ) func TestComplete_NotEnoughArguments(t *testing.T) { @@ -49,9 +54,471 @@ func TestComplete_PodNameSpecified(t *testing.T) { var commands []string // when - err := sniff.Complete(cmd, append(commands, "pod-name")) + // Only test the basic settings completion, not the kubeconfig loading + err := sniff.completeBasicSettings(cmd, append(commands, "pod-name")) // then assert.Nil(t, err) assert.Equal(t, "pod-name", settings.UserSpecifiedPodName) } + +func TestCompleteBasicSettings_WithMultipleFlags(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set some flags + _ = cmd.Flags().Set("namespace", "test-namespace") + _ = cmd.Flags().Set("container", "test-container") + _ = cmd.Flags().Set("interface", "eth0") + _ = cmd.Flags().Set("filter", "port 80") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.Equal(t, "test-pod", settings.UserSpecifiedPodName) + assert.Equal(t, "test-namespace", settings.UserSpecifiedNamespace) + assert.Equal(t, "test-container", settings.UserSpecifiedContainer) + assert.Equal(t, "eth0", settings.UserSpecifiedInterface) + assert.Equal(t, "port 80", settings.UserSpecifiedFilter) +} + +func TestCompleteBasicSettings_WithPrivilegedMode(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set privileged mode flag + _ = cmd.Flags().Set("privileged", "true") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.True(t, settings.UserSpecifiedPrivilegedMode) +} + +func TestCompleteBasicSettings_WithOutputFile(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set output file flag + _ = cmd.Flags().Set("output-file", "/tmp/capture.pcap") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.Equal(t, "/tmp/capture.pcap", settings.UserSpecifiedOutputFile) +} + +func TestCompleteBasicSettings_WithVerboseMode(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set verbose mode flag + _ = cmd.Flags().Set("verbose", "true") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.True(t, settings.UserSpecifiedVerboseMode) +} + +func TestCompleteBasicSettings_WithImage(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set image flag + _ = cmd.Flags().Set("image", "custom/tcpdump:latest") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.Equal(t, "custom/tcpdump:latest", settings.Image) + assert.False(t, settings.UseDefaultImage) +} + +func TestCompleteBasicSettings_WithTcpdumpImage(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set tcpdump-image flag + _ = cmd.Flags().Set("tcpdump-image", "custom/tcpdump:v2") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.Equal(t, "custom/tcpdump:v2", settings.TCPDumpImage) + assert.False(t, settings.UseDefaultTCPDumpImage) +} + +func TestCompleteBasicSettings_WithImagePullSecret(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set image-pull-secret flag + _ = cmd.Flags().Set("image-pull-secret", "my-registry-secret") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.Equal(t, "my-registry-secret", settings.UserSpecifiedImagePullSecret) +} + +func TestCompleteBasicSettings_WithSocket(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set socket flag + _ = cmd.Flags().Set("socket", "/custom/path/docker.sock") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.Equal(t, "/custom/path/docker.sock", settings.SocketPath) + assert.False(t, settings.UseDefaultSocketPath) +} + +func TestCompleteBasicSettings_WithServiceAccount(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set serviceaccount flag + _ = cmd.Flags().Set("serviceaccount", "custom-sa") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.Equal(t, "custom-sa", settings.UserSpecifiedServiceAccount) +} + +func TestCompleteBasicSettings_WithLocalTcpdump(t *testing.T) { + // given + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + sniff := NewKsniff(settings) + cmd := NewCmdSniff(genericclioptions.IOStreams{}) + + // Set local-tcpdump-path flag + _ = cmd.Flags().Set("local-tcpdump-path", "/usr/bin/tcpdump") + + var commands []string + + // when + err := sniff.completeBasicSettings(cmd, append(commands, "test-pod")) + + // then + assert.Nil(t, err) + assert.Equal(t, "/usr/bin/tcpdump", settings.UserSpecifiedLocalTcpdumpPath) +} + +// Tests with mocked Kubernetes client + +func TestValidate_WithRunningPod(t *testing.T) { + // Create fake clientset + fakeClientset := fake.NewSimpleClientset() + + // Create test pod + testPod := &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "test-node", + Containers: []corev1.Container{ + {Name: "test-container"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "test-container", + ContainerID: "docker://abc123", + }, + }, + }, + } + + // Add pod to fake clientset + _, err := fakeClientset.CoreV1().Pods("default").Create(context.TODO(), testPod, v1.CreateOptions{}) + assert.Nil(t, err) + + // Create Ksniff instance with mocked clientset + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + settings.UserSpecifiedPodName = "test-pod" + settings.UserSpecifiedContainer = "test-container" + settings.UserSpecifiedPrivilegedMode = true // Use privileged mode to avoid tcpdump binary check + + ksniff := NewKsniff(settings) + ksniff.clientset = fakeClientset + ksniff.rawConfig = api.Config{CurrentContext: "test-context"} + ksniff.resultingContext = &api.Context{Namespace: "default"} + ksniff.restConfig = &rest.Config{} + + // Test Validate + err = ksniff.Validate() + assert.Nil(t, err) + assert.Equal(t, "test-node", settings.DetectedPodNodeName) + assert.Equal(t, "docker", settings.DetectedContainerRuntime) + assert.Equal(t, "abc123", settings.DetectedContainerId) +} + +func TestValidate_PodNotFound(t *testing.T) { + // Create fake clientset with no pods + fakeClientset := fake.NewSimpleClientset() + + // Create Ksniff instance with mocked clientset + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + settings.UserSpecifiedPodName = "nonexistent-pod" + settings.UserSpecifiedPrivilegedMode = true // Use privileged mode to avoid tcpdump binary check + + ksniff := NewKsniff(settings) + ksniff.clientset = fakeClientset + ksniff.rawConfig = api.Config{CurrentContext: "test-context"} + ksniff.resultingContext = &api.Context{Namespace: "default"} + ksniff.restConfig = &rest.Config{} + + // Test Validate + err := ksniff.Validate() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestValidate_PodInFailedState(t *testing.T) { + // Create fake clientset + fakeClientset := fake.NewSimpleClientset() + + // Create test pod in failed state + testPod := &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "failed-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "test-node", + Containers: []corev1.Container{ + {Name: "test-container"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodFailed, + }, + } + + // Add pod to fake clientset + _, err := fakeClientset.CoreV1().Pods("default").Create(context.TODO(), testPod, v1.CreateOptions{}) + assert.Nil(t, err) + + // Create Ksniff instance with mocked clientset + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + settings.UserSpecifiedPodName = "failed-pod" + settings.UserSpecifiedPrivilegedMode = true // Use privileged mode to avoid tcpdump binary check + + ksniff := NewKsniff(settings) + ksniff.clientset = fakeClientset + ksniff.rawConfig = api.Config{CurrentContext: "test-context"} + ksniff.resultingContext = &api.Context{Namespace: "default"} + ksniff.restConfig = &rest.Config{} + + // Test Validate + err = ksniff.Validate() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "cannot sniff on a container in a completed pod") +} + +func TestValidate_PodWithNoContainers(t *testing.T) { + // Create fake clientset + fakeClientset := fake.NewSimpleClientset() + + // Create test pod with no containers + testPod := &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "empty-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "test-node", + Containers: []corev1.Container{}, // No containers + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + } + + // Add pod to fake clientset + _, err := fakeClientset.CoreV1().Pods("default").Create(context.TODO(), testPod, v1.CreateOptions{}) + assert.Nil(t, err) + + // Create Ksniff instance with mocked clientset + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + settings.UserSpecifiedPodName = "empty-pod" + settings.UserSpecifiedPrivilegedMode = true + + ksniff := NewKsniff(settings) + ksniff.clientset = fakeClientset + ksniff.rawConfig = api.Config{CurrentContext: "test-context"} + ksniff.resultingContext = &api.Context{Namespace: "default"} + ksniff.restConfig = &rest.Config{} + + // Test Validate + err = ksniff.Validate() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "no containers in specified pod") +} + +func TestValidate_AutoSelectFirstContainer(t *testing.T) { + // Create fake clientset + fakeClientset := fake.NewSimpleClientset() + + // Create test pod with multiple containers + testPod := &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "multi-container-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "test-node", + Containers: []corev1.Container{ + {Name: "first-container"}, + {Name: "second-container"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "first-container", + ContainerID: "containerd://xyz789", + }, + { + Name: "second-container", + ContainerID: "containerd://abc123", + }, + }, + }, + } + + // Add pod to fake clientset + _, err := fakeClientset.CoreV1().Pods("default").Create(context.TODO(), testPod, v1.CreateOptions{}) + assert.Nil(t, err) + + // Create Ksniff instance with mocked clientset (no container specified) + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + settings.UserSpecifiedPodName = "multi-container-pod" + settings.UserSpecifiedPrivilegedMode = true + // Don't specify container - should auto-select first one + + ksniff := NewKsniff(settings) + ksniff.clientset = fakeClientset + ksniff.rawConfig = api.Config{CurrentContext: "test-context"} + ksniff.resultingContext = &api.Context{Namespace: "default"} + ksniff.restConfig = &rest.Config{} + + // Test Validate + err = ksniff.Validate() + assert.Nil(t, err) + assert.Equal(t, "first-container", settings.UserSpecifiedContainer) + assert.Equal(t, "containerd", settings.DetectedContainerRuntime) + assert.Equal(t, "xyz789", settings.DetectedContainerId) +} + +func TestValidate_ContainerNotFound(t *testing.T) { + // Create fake clientset + fakeClientset := fake.NewSimpleClientset() + + // Create test pod + testPod := &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "test-node", + Containers: []corev1.Container{ + {Name: "existing-container"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "existing-container", + ContainerID: "docker://abc123", + }, + }, + }, + } + + // Add pod to fake clientset + _, err := fakeClientset.CoreV1().Pods("default").Create(context.TODO(), testPod, v1.CreateOptions{}) + assert.Nil(t, err) + + // Create Ksniff instance with mocked clientset + settings := config.NewKsniffSettings(genericclioptions.IOStreams{}) + settings.UserSpecifiedPodName = "test-pod" + settings.UserSpecifiedContainer = "nonexistent-container" // Container doesn't exist + settings.UserSpecifiedPrivilegedMode = true + + ksniff := NewKsniff(settings) + ksniff.clientset = fakeClientset + ksniff.rawConfig = api.Config{CurrentContext: "test-context"} + ksniff.resultingContext = &api.Context{Namespace: "default"} + ksniff.restConfig = &rest.Config{} + + // Test Validate + err = ksniff.Validate() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "couldn't find container") +} diff --git a/pkg/config/BUILD.bazel b/pkg/config/BUILD.bazel new file mode 100644 index 0000000..7b8fd3a --- /dev/null +++ b/pkg/config/BUILD.bazel @@ -0,0 +1,9 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "config", + srcs = ["settings.go"], + importpath = "ksniff/pkg/config", + visibility = ["//visibility:public"], + deps = ["@io_k8s_cli_runtime//pkg/genericclioptions"], +) diff --git a/pkg/config/settings.go b/pkg/config/settings.go index f8b1e8a..738fbf4 100644 --- a/pkg/config/settings.go +++ b/pkg/config/settings.go @@ -30,6 +30,7 @@ type KsniffSettings struct { SocketPath string UseDefaultSocketPath bool UserSpecifiedServiceAccount string + UserSpecifiedImagePullSecret string } func NewKsniffSettings(streams genericclioptions.IOStreams) *KsniffSettings { diff --git a/pkg/service/sniffer/BUILD.bazel b/pkg/service/sniffer/BUILD.bazel new file mode 100644 index 0000000..6800c25 --- /dev/null +++ b/pkg/service/sniffer/BUILD.bazel @@ -0,0 +1,20 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "sniffer", + srcs = [ + "privileged_pod_sniffer_service.go", + "sniffer_service.go", + "static_tcpdump_sniffer_service.go", + ], + importpath = "ksniff/pkg/service/sniffer", + visibility = ["//visibility:public"], + deps = [ + "//kube", + "//pkg/config", + "//pkg/service/sniffer/runtime", + "@com_github_pkg_errors//:errors", + "@com_github_sirupsen_logrus//:logrus", + "@io_k8s_api//core/v1:core", + ], +) diff --git a/pkg/service/sniffer/privileged_pod_sniffer_service.go b/pkg/service/sniffer/privileged_pod_sniffer_service.go index 9c101a5..830b742 100644 --- a/pkg/service/sniffer/privileged_pod_sniffer_service.go +++ b/pkg/service/sniffer/privileged_pod_sniffer_service.go @@ -49,6 +49,7 @@ func (p *PrivilegedPodSnifferService) Setup() error { p.settings.SocketPath, p.settings.UserSpecifiedPodCreateTimeout, p.settings.UserSpecifiedServiceAccount, + p.settings.UserSpecifiedImagePullSecret, ) if err != nil { log.WithError(err).Errorf("failed to create privileged pod on node: '%s'", p.settings.DetectedPodNodeName) diff --git a/pkg/service/sniffer/runtime/BUILD.bazel b/pkg/service/sniffer/runtime/BUILD.bazel new file mode 100644 index 0000000..7be08fc --- /dev/null +++ b/pkg/service/sniffer/runtime/BUILD.bazel @@ -0,0 +1,29 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "runtime", + srcs = [ + "containerd.go", + "crio.go", + "docker.go", + "runtime.go", + ], + importpath = "ksniff/pkg/service/sniffer/runtime", + visibility = ["//visibility:public"], + deps = [ + "//utils", + "@com_github_pkg_errors//:errors", + "@com_github_sirupsen_logrus//:logrus", + ], +) + +go_test( + name = "runtime_test", + srcs = [ + "crio_test.go", + "docker_test.go", + "runtime_test.go", + ], + embed = [":runtime"], + deps = ["@com_github_stretchr_testify//assert"], +) diff --git a/pkg/service/sniffer/runtime/runtime_test.go b/pkg/service/sniffer/runtime/runtime_test.go index e3cd75b..c8b1269 100644 --- a/pkg/service/sniffer/runtime/runtime_test.go +++ b/pkg/service/sniffer/runtime/runtime_test.go @@ -16,6 +16,269 @@ func TestNewContainerRuntimeBridge_Crio(t *testing.T) { assert.IsType(t, &CrioBridge{}, bridge) } +func TestNewContainerRuntimeBridge_Containerd(t *testing.T) { + bridge := NewContainerRuntimeBridge("containerd") + assert.IsType(t, &ContainerdBridge{}, bridge) +} + func TestNewContainerRuntimeBridge_Invalid(t *testing.T) { assert.Panics(t, func() { NewContainerRuntimeBridge("i-do-not-exist") }) } + +// Docker Bridge Tests +func TestDockerBridge_NeedsPid(t *testing.T) { + bridge := NewDockerBridge() + assert.False(t, bridge.NeedsPid()) +} + +func TestDockerBridge_GetDefaultImage(t *testing.T) { + bridge := NewDockerBridge() + assert.Equal(t, "docker", bridge.GetDefaultImage()) +} + +func TestDockerBridge_GetDefaultTCPImage(t *testing.T) { + bridge := NewDockerBridge() + assert.Equal(t, "maintained/tcpdump", bridge.GetDefaultTCPImage()) +} + +func TestDockerBridge_GetDefaultSocketPath(t *testing.T) { + bridge := NewDockerBridge() + assert.Equal(t, "/var/run/docker.sock", bridge.GetDefaultSocketPath()) +} + +func TestDockerBridge_BuildInspectCommand_Panics(t *testing.T) { + bridge := NewDockerBridge() + assert.Panics(t, func() { bridge.BuildInspectCommand("test-container") }) +} + +func TestDockerBridge_ExtractPid_Panics(t *testing.T) { + bridge := NewDockerBridge() + assert.Panics(t, func() { bridge.ExtractPid("test-inspection") }) +} + +func TestDockerBridge_BuildTcpdumpCommand(t *testing.T) { + bridge := NewDockerBridge() + containerId := "test-container-id" + netInterface := "eth0" + filter := "port 80" + socketPath := "/var/run/docker.sock" + tcpdumpImage := "maintained/tcpdump" + + command := bridge.BuildTcpdumpCommand(&containerId, netInterface, filter, nil, socketPath, tcpdumpImage) + + assert.NotNil(t, command) + assert.Contains(t, command, "docker") + assert.Contains(t, command, "--host") + assert.Contains(t, command, "unix:///var/run/docker.sock") + assert.Contains(t, command, "run") + assert.Contains(t, command, "--rm") + assert.Contains(t, command, "--net=container:test-container-id") + assert.Contains(t, command, tcpdumpImage) + assert.Contains(t, command, "-i") + assert.Contains(t, command, netInterface) + assert.Contains(t, command, filter) +} + +func TestDockerBridge_BuildCleanupCommand(t *testing.T) { + bridge := NewDockerBridge() + containerId := "test-container-id" + socketPath := "/var/run/docker.sock" + + // First build tcpdump command to initialize cleanup command + bridge.BuildTcpdumpCommand(&containerId, "eth0", "port 80", nil, socketPath, "maintained/tcpdump") + + cleanupCommand := bridge.BuildCleanupCommand() + + assert.NotNil(t, cleanupCommand) + assert.Contains(t, cleanupCommand, "docker") + assert.Contains(t, cleanupCommand, "--host") + assert.Contains(t, cleanupCommand, "unix:///var/run/docker.sock") + assert.Contains(t, cleanupCommand, "rm") + assert.Contains(t, cleanupCommand, "-f") +} + +// CRI-O Bridge Tests +func TestCrioBridge_NeedsPid(t *testing.T) { + bridge := NewCrioBridge() + assert.True(t, bridge.NeedsPid()) +} + +func TestCrioBridge_GetDefaultImage(t *testing.T) { + bridge := NewCrioBridge() + assert.Equal(t, "maintained/tcpdump", bridge.GetDefaultImage()) +} + +func TestCrioBridge_GetDefaultTCPImage(t *testing.T) { + bridge := NewCrioBridge() + assert.Equal(t, "", bridge.GetDefaultTCPImage()) +} + +func TestCrioBridge_GetDefaultSocketPath(t *testing.T) { + bridge := NewCrioBridge() + assert.Equal(t, "/var/run/crio/crio.sock", bridge.GetDefaultSocketPath()) +} + +func TestCrioBridge_BuildInspectCommand(t *testing.T) { + bridge := NewCrioBridge() + containerId := "test-container-id" + + command := bridge.BuildInspectCommand(containerId) + + assert.NotNil(t, command) + assert.Equal(t, []string{"chroot", "/host", "crictl", "inspect", "--output", "json", containerId}, command) +} + +func TestCrioBridge_ExtractPid_Crio117(t *testing.T) { + bridge := NewCrioBridge() + inspection := `{"pid": 12345}` + + pid, err := bridge.ExtractPid(inspection) + + assert.Nil(t, err) + assert.NotNil(t, pid) + assert.Equal(t, "12345", *pid) +} + +func TestCrioBridge_ExtractPid_Crio118(t *testing.T) { + bridge := NewCrioBridge() + inspection := `{"info": {"pid": 67890}}` + + pid, err := bridge.ExtractPid(inspection) + + assert.Nil(t, err) + assert.NotNil(t, pid) + assert.Equal(t, "67890", *pid) +} + +func TestCrioBridge_ExtractPid_InvalidJSON(t *testing.T) { + bridge := NewCrioBridge() + inspection := `invalid json` + + pid, err := bridge.ExtractPid(inspection) + + assert.NotNil(t, err) + assert.Nil(t, pid) +} + +func TestCrioBridge_ExtractPid_UnknownVersion(t *testing.T) { + bridge := NewCrioBridge() + inspection := `{"unknown": "field"}` + + pid, err := bridge.ExtractPid(inspection) + + assert.NotNil(t, err) + assert.Nil(t, pid) + assert.Contains(t, err.Error(), "unable to identify CRI-O version") +} + +func TestCrioBridge_ExtractPid_Crio117_InvalidPid(t *testing.T) { + bridge := NewCrioBridge() + inspection := `{"pid": "not-a-number"}` + + pid, err := bridge.ExtractPid(inspection) + + assert.NotNil(t, err) + assert.Nil(t, pid) +} + +func TestCrioBridge_ExtractPid_Crio118_InvalidInfo(t *testing.T) { + bridge := NewCrioBridge() + inspection := `{"info": "not-an-object"}` + + pid, err := bridge.ExtractPid(inspection) + + assert.NotNil(t, err) + assert.Nil(t, pid) +} + +func TestCrioBridge_BuildTcpdumpCommand(t *testing.T) { + bridge := NewCrioBridge() + containerId := "test-container-id" + netInterface := "eth0" + filter := "port 80" + pid := "12345" + + command := bridge.BuildTcpdumpCommand(&containerId, netInterface, filter, &pid, "", "") + + assert.NotNil(t, command) + assert.Equal(t, []string{"nsenter", "-n", "-t", "12345", "--", "tcpdump", "-i", "eth0", "-U", "-w", "-", "port 80"}, command) +} + +func TestCrioBridge_BuildCleanupCommand(t *testing.T) { + bridge := NewCrioBridge() + + cleanupCommand := bridge.BuildCleanupCommand() + + assert.Nil(t, cleanupCommand) +} + +// Containerd Bridge Tests +func TestContainerdBridge_NeedsPid(t *testing.T) { + bridge := NewContainerdBridge() + assert.False(t, bridge.NeedsPid()) +} + +func TestContainerdBridge_GetDefaultImage(t *testing.T) { + bridge := NewContainerdBridge() + assert.Equal(t, "docker.io/hamravesh/ksniff-helper:v3", bridge.GetDefaultImage()) +} + +func TestContainerdBridge_GetDefaultTCPImage(t *testing.T) { + bridge := NewContainerdBridge() + assert.Equal(t, "docker.io/maintained/tcpdump:latest", bridge.GetDefaultTCPImage()) +} + +func TestContainerdBridge_GetDefaultSocketPath(t *testing.T) { + bridge := NewContainerdBridge() + assert.Equal(t, "/run/containerd/containerd.sock", bridge.GetDefaultSocketPath()) +} + +func TestContainerdBridge_BuildInspectCommand_Panics(t *testing.T) { + bridge := NewContainerdBridge() + assert.Panics(t, func() { bridge.BuildInspectCommand("test-container") }) +} + +func TestContainerdBridge_ExtractPid_Panics(t *testing.T) { + bridge := NewContainerdBridge() + assert.Panics(t, func() { bridge.ExtractPid("test-inspection") }) +} + +func TestContainerdBridge_BuildTcpdumpCommand(t *testing.T) { + bridge := NewContainerdBridge() + containerId := "test-container-id" + netInterface := "eth0" + filter := "port 80" + socketPath := "/run/containerd/containerd.sock" + tcpdumpImage := "docker.io/maintained/tcpdump:latest" + + command := bridge.BuildTcpdumpCommand(&containerId, netInterface, filter, nil, socketPath, tcpdumpImage) + + assert.NotNil(t, command) + assert.Equal(t, 3, len(command)) + assert.Equal(t, "/bin/sh", command[0]) + assert.Equal(t, "-c", command[1]) + assert.Contains(t, command[2], "crictl pull") + assert.Contains(t, command[2], tcpdumpImage) + assert.Contains(t, command[2], containerId) + assert.Contains(t, command[2], socketPath) + assert.Contains(t, command[2], "tcpdump -i eth0 -U -w - port 80") +} + +func TestContainerdBridge_BuildCleanupCommand(t *testing.T) { + bridge := NewContainerdBridge() + containerId := "test-container-id" + socketPath := "/run/containerd/containerd.sock" + + // First build tcpdump command to initialize internal state + bridge.BuildTcpdumpCommand(&containerId, "eth0", "port 80", nil, socketPath, "docker.io/maintained/tcpdump:latest") + + cleanupCommand := bridge.BuildCleanupCommand() + + assert.NotNil(t, cleanupCommand) + assert.Equal(t, 3, len(cleanupCommand)) + assert.Equal(t, "/bin/sh", cleanupCommand[0]) + assert.Equal(t, "-c", cleanupCommand[1]) + assert.Contains(t, cleanupCommand[2], "ctr -a") + assert.Contains(t, cleanupCommand[2], socketPath) + assert.Contains(t, cleanupCommand[2], "task kill -s SIGKILL") +} diff --git a/tcpdump.BUILD b/tcpdump.BUILD new file mode 100644 index 0000000..7d6cb73 --- /dev/null +++ b/tcpdump.BUILD @@ -0,0 +1,49 @@ +filegroup( + name = "all_srcs", + srcs = glob(["**"]), + visibility = ["//visibility:public"], +) + +# Build tcpdump using a genrule +# Note: This is designed to build a static binary on Linux. +# On macOS, this may not work due to limitations with static linking and configure script compatibility. +# For production use on Linux, this will create a statically-linked tcpdump binary. +genrule( + name = "tcpdump", + srcs = [":all_srcs"], + outs = ["tcpdump_bin"], + cmd = select({ + "@platforms//os:linux": """ + set -e + # Create a temporary build directory + BUILD_DIR=$$(mktemp -d) + trap "rm -rf $$BUILD_DIR" EXIT + + # Copy all sources to build directory + SRC_DIR=$$(dirname $$(echo $(SRCS) | cut -d' ' -f1)) + cp -r $$SRC_DIR/* $$BUILD_DIR/ + cd $$BUILD_DIR + + # Configure and build with static linking + CFLAGS=-static ./configure --without-crypto + make + + # Copy the binary to the output + cp tcpdump $(location tcpdump_bin) + """, + "//conditions:default": """ + # On non-Linux platforms, create a placeholder script + # that explains how to build static-tcpdump + cat > $(location tcpdump_bin) << 'EOF' +#!/bin/bash +echo "Error: static-tcpdump must be built on Linux" +echo "To build static-tcpdump:" +echo " 1. Run this build on a Linux machine or in a Linux container" +echo " 2. Or use the Makefile target: make static-tcpdump" +exit 1 +EOF + chmod +x $(location tcpdump_bin) + """, + }), + visibility = ["//visibility:public"], +) diff --git a/utils/BUILD.bazel b/utils/BUILD.bazel new file mode 100644 index 0000000..59c7140 --- /dev/null +++ b/utils/BUILD.bazel @@ -0,0 +1,15 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "utils", + srcs = ["utils.go"], + importpath = "ksniff/utils", + visibility = ["//visibility:public"], +) + +go_test( + name = "utils_test", + srcs = ["utils_test.go"], + embed = [":utils"], + deps = ["@com_github_stretchr_testify//assert"], +)