├── .gitignore ├── test.sh ├── .golangci.yaml ├── main.go ├── .vscode └── settings.json ├── cmd ├── root.go ├── deltas-cmd.go ├── logs.go └── recordcmd.go ├── LICENSE ├── usage.md ├── go.mod ├── README.md ├── internal └── deltas │ └── deltas.go ├── record └── record.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | *kubeconfig.yaml 3 | *.kubeconfig 4 | tmp 5 | watchall-output 6 | ignore-log-lines.regexs 7 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Bash Strict Mode: https://github.com/guettli/bash-strict-mode 3 | trap 'echo "Warning: A command has failed. Exiting the script. Line was ($0:$LINENO): $(sed -n "${LINENO}p" "$0")"; exit 3' ERR 4 | set -Eeuo pipefail 5 | 6 | go mod tidy 7 | 8 | go test ./... 9 | 10 | golangci-lint run ./... 11 | 12 | go run main.go gendocs 13 | 14 | git status 15 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable-all: true 3 | disable: 4 | - depguard 5 | - lll 6 | - nlreturn 7 | - wsl 8 | - mnd 9 | - tenv 10 | - gochecknoinits 11 | - err113 12 | - exhaustruct 13 | - forbidigo 14 | - gochecknoglobals 15 | - varnamelen 16 | - funlen 17 | - cyclop 18 | enable: 19 | - errcheck 20 | linters-settings: 21 | cyclop: 22 | max-complexity: 15 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 Thomas Güttler 3 | See LICENSE file. 4 | */ 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "os" 11 | 12 | "github.com/gavv/cobradoc" 13 | "github.com/guettli/watchall/cmd" 14 | ) 15 | 16 | func main() { 17 | if len(os.Args) > 1 && os.Args[1] == "gendocs" { 18 | b := &bytes.Buffer{} 19 | err := cobradoc.WriteDocument(b, cmd.RootCmd, cobradoc.Markdown, cobradoc.Options{}) 20 | if err != nil { 21 | fmt.Println(err) 22 | os.Exit(1) 23 | } 24 | usageFile := "usage.md" 25 | err = os.WriteFile(usageFile, b.Bytes(), 0o600) 26 | if err != nil { 27 | fmt.Println(err) 28 | os.Exit(1) 29 | } 30 | fmt.Printf("Created %q\n", usageFile) 31 | os.Exit(0) 32 | } 33 | 34 | cmd.Execute() 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#fbed80", 4 | "activityBar.background": "#fbed80", 5 | "activityBar.foreground": "#15202b", 6 | "activityBar.inactiveForeground": "#15202b99", 7 | "activityBarBadge.background": "#06b9a5", 8 | "activityBarBadge.foreground": "#15202b", 9 | "commandCenter.border": "#15202b99", 10 | "sash.hoverBorder": "#fbed80", 11 | "statusBar.background": "#f9e64f", 12 | "statusBar.foreground": "#15202b", 13 | "statusBarItem.hoverBackground": "#f7df1e", 14 | "statusBarItem.remoteBackground": "#f9e64f", 15 | "statusBarItem.remoteForeground": "#15202b", 16 | "titleBar.activeBackground": "#f9e64f", 17 | "titleBar.activeForeground": "#15202b", 18 | "titleBar.inactiveBackground": "#f9e64f99", 19 | "titleBar.inactiveForeground": "#15202b99" 20 | }, 21 | "peacock.color": "#f9e64f" 22 | } -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/guettli/watchall/record" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var RootCmd = &cobra.Command{ 11 | Use: "watchall", 12 | Short: "Watch resources in your Kubernetes cluster.", 13 | Long: `...`, 14 | } 15 | 16 | // Execute adds all child commands to the root command and sets flags appropriately. 17 | // This is called by main.main(). It only needs to happen once to the rootCmd. 18 | func Execute() { 19 | if err := RootCmd.Execute(); err != nil { 20 | os.Exit(1) 21 | } 22 | } 23 | 24 | var arguments = record.Arguments{} 25 | 26 | func init() { 27 | // Here you will define your flags and configuration settings. 28 | // Cobra supports persistent flags, which, if defined here, 29 | // will be global for your application. 30 | RootCmd.PersistentFlags().BoolVarP(&arguments.Verbose, "verbose", "v", false, "Create more output") 31 | RootCmd.PersistentFlags().StringVarP(&arguments.OutputDirectory, "outdir", "o", "watchall-output", "Directory to store output") 32 | } 33 | -------------------------------------------------------------------------------- /cmd/deltas-cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/guettli/watchall/internal/deltas" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var deltasCmd = &cobra.Command{ 9 | Use: "deltas dir", 10 | Short: "show the deltas (changes) of resource objects", 11 | Long: `This reads the files from the local disk and shows the changes. No connection to a cluster is needed.`, 12 | Args: cobra.ExactArgs(1), 13 | RunE: func(_ *cobra.Command, args []string) error { 14 | dir := args[0] 15 | return deltas.Deltas(dir, skipPatterns, onlyPatterns, skipInitial) 16 | }, 17 | SilenceUsage: true, 18 | } 19 | 20 | var ( 21 | skipPatterns []string 22 | onlyPatterns []string 23 | skipInitial bool 24 | ) 25 | 26 | func init() { 27 | RootCmd.AddCommand(deltasCmd) 28 | deltasCmd.Flags().StringSliceVar(&skipPatterns, "skip", []string{}, "comma separated list of regex patterns to skip") 29 | deltasCmd.Flags().StringSliceVar(&onlyPatterns, "only", []string{}, "comma separated list of regex patterns to show") 30 | deltasCmd.Flags().BoolVar(&skipInitial, "skip-initial", false, "skip the initial output of the current state of the resources") 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Thomas Güttler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /usage.md: -------------------------------------------------------------------------------- 1 | # Watchall Manual 2 | 3 | ... 4 | 5 | ```text 6 | watchall [command] [global flags] [command flags] 7 | ``` 8 | 9 | ### Global Flags 10 | 11 | ```text 12 | -o, --outdir string Directory to store output (default "watchall-output") 13 | -v, --verbose Create more output 14 | ``` 15 | 16 | ### Commands 17 | 18 | * [watchall deltas](#watchall-deltas) 19 | * [watchall help](#watchall-help) 20 | * [watchall logs](#watchall-logs) 21 | * [watchall record](#watchall-record) 22 | 23 | # Commands 24 | 25 | ## `watchall deltas` 26 | 27 | This reads the files from the local disk and shows the changes. No connection to a cluster is needed. 28 | 29 | ```text 30 | watchall deltas dir [flags] 31 | ``` 32 | 33 | ### Command Flags 34 | 35 | ```text 36 | -h, --help help for deltas 37 | --only strings comma separated list of regex patterns to show 38 | --skip strings comma separated list of regex patterns to skip 39 | ``` 40 | 41 | ## `watchall help` 42 | 43 | Help about any command 44 | 45 | ```text 46 | watchall help [command] [flags] 47 | ``` 48 | 49 | ### Command Flags 50 | 51 | ```text 52 | -h, --help help for help 53 | ``` 54 | 55 | ## `watchall logs` 56 | 57 | ... 58 | 59 | ```text 60 | watchall logs [flags] 61 | ``` 62 | 63 | ### Command Flags 64 | 65 | ```text 66 | -h, --help help for logs 67 | ``` 68 | 69 | ## `watchall record` 70 | 71 | ... 72 | 73 | ```text 74 | watchall record [flags] 75 | ``` 76 | 77 | ### Command Flags 78 | 79 | ```text 80 | -h, --help help for record 81 | ``` 82 | -------------------------------------------------------------------------------- /cmd/logs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/client-go/tools/clientcmd" 13 | ) 14 | 15 | var logsCmd = &cobra.Command{ 16 | Use: "logs", 17 | Short: "Check all logs of all pods", 18 | Long: `...`, 19 | Run: func(_ *cobra.Command, _ []string) { 20 | runLogs() 21 | }, 22 | } 23 | 24 | func init() { 25 | RootCmd.AddCommand(logsCmd) 26 | } 27 | 28 | func runLogs() { 29 | ctx := context.Background() 30 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 31 | configOverrides := &clientcmd.ConfigOverrides{} 32 | kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) 33 | 34 | config, err := kubeconfig.ClientConfig() 35 | if err != nil { 36 | panic(err.Error()) 37 | } 38 | 39 | config.QPS = 1000 40 | config.Burst = 1000 41 | 42 | clientset, err := kubernetes.NewForConfig(config) 43 | if err != nil { 44 | panic(err.Error()) 45 | } 46 | 47 | // List all pods 48 | pods, err := clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) 49 | if err != nil { 50 | panic(err.Error()) 51 | } 52 | 53 | for _, pod := range pods.Items { 54 | for _, container := range pod.Spec.Containers { 55 | req := clientset.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &corev1.PodLogOptions{ 56 | Container: container.Name, 57 | }) 58 | 59 | podLogs, err := req.Stream(ctx) 60 | if err != nil { 61 | panic(err.Error()) 62 | } 63 | defer podLogs.Close() 64 | 65 | _, err = io.Copy(os.Stdout, podLogs) 66 | if err != nil { 67 | panic(err.Error()) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/guettli/watchall 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.3 6 | 7 | require ( 8 | github.com/gavv/cobradoc v1.1.0 9 | github.com/spf13/cobra v1.7.0 10 | k8s.io/api v0.32.2 11 | k8s.io/apimachinery v0.32.2 12 | k8s.io/client-go v0.32.2 13 | sigs.k8s.io/yaml v1.4.0 14 | ) 15 | 16 | require ( 17 | github.com/akedrou/textdiff v0.1.0 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 19 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 20 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 21 | github.com/go-logr/logr v1.4.2 // indirect 22 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 23 | github.com/go-openapi/jsonreference v0.20.2 // indirect 24 | github.com/go-openapi/swag v0.23.0 // indirect 25 | github.com/gogo/protobuf v1.3.2 // indirect 26 | github.com/golang/protobuf v1.5.4 // indirect 27 | github.com/google/gnostic-models v0.6.8 // indirect 28 | github.com/google/go-cmp v0.6.0 // indirect 29 | github.com/google/gofuzz v1.2.0 // indirect 30 | github.com/google/uuid v1.6.0 // indirect 31 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 32 | github.com/josharian/intern v1.0.0 // indirect 33 | github.com/json-iterator/go v1.1.12 // indirect 34 | github.com/mailru/easyjson v0.7.7 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 38 | github.com/pkg/errors v0.9.1 // indirect 39 | github.com/spf13/pflag v1.0.5 // indirect 40 | github.com/x448/float16 v0.8.4 // indirect 41 | golang.org/x/net v0.30.0 // indirect 42 | golang.org/x/oauth2 v0.23.0 // indirect 43 | golang.org/x/sys v0.26.0 // indirect 44 | golang.org/x/term v0.25.0 // indirect 45 | golang.org/x/text v0.19.0 // indirect 46 | golang.org/x/time v0.7.0 // indirect 47 | google.golang.org/protobuf v1.35.1 // indirect 48 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 49 | gopkg.in/inf.v0 v0.9.1 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | k8s.io/klog/v2 v2.130.1 // indirect 52 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 53 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 54 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 55 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 56 | ) 57 | -------------------------------------------------------------------------------- /cmd/recordcmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/guettli/watchall/record" 11 | "github.com/spf13/cobra" 12 | "k8s.io/client-go/tools/clientcmd" 13 | ) 14 | 15 | var recordCmd = &cobra.Command{ 16 | Use: "record", 17 | Short: "record all changes to resource objects", 18 | Long: `...`, 19 | Run: func(_ *cobra.Command, _ []string) { 20 | runRecord(arguments) 21 | }, 22 | } 23 | 24 | func init() { 25 | recordCmd.Flags().BoolVarP(&arguments.WithLogs, "with-logs", "w", false, "Record logs of pods") 26 | recordCmd.Flags().StringVar(&arguments.IgnoreLogLinesFile, "ignore-log-lines-file", "", "Path to a file containing log lines to ignore. Syntax of the line-based file format: 'filename-regex ~~ line-regex'. If line-regex is empty, the pod won't be watched. Lines starting with '#', and empty lines, are ignored. Example to ignore info lines of cilium: kube-system/cilium ~~ level=info. Alternatively, you can use --skip when using the 'deltas' sub-command.") 27 | recordCmd.Flags().BoolVarP(&arguments.DisableResourceRecording, "disable-resource-recording", "", false, "Do not watch/record changes to resources. Only meaningful if you only want logs: --with-logs.") 28 | RootCmd.AddCommand(recordCmd) 29 | } 30 | 31 | func runRecord(args record.Arguments) { 32 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 33 | configOverrides := &clientcmd.ConfigOverrides{} 34 | kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) 35 | if args.DisableResourceRecording && !args.WithLogs { 36 | fmt.Println("Error: --skip-recording-resources is only meaningful with --with-logs") 37 | os.Exit(1) 38 | } 39 | if args.IgnoreLogLinesFile != "" { 40 | err := parseIgnoreLogLinesFile(args.IgnoreLogLinesFile, &args) 41 | if err != nil { 42 | fmt.Printf("Error parsing ignore-log-lines-file %q: %v\n", args.IgnoreLogLinesFile, err) 43 | os.Exit(1) 44 | } 45 | } 46 | 47 | wg, err := record.RunRecordWithContext(context.Background(), args, kubeconfig) 48 | if err != nil { 49 | fmt.Println(err.Error()) 50 | os.Exit(1) 51 | } 52 | wg.Wait() 53 | } 54 | 55 | func parseIgnoreLogLinesFile(filename string, args *record.Arguments) error { 56 | if _, err := os.Stat(filename); os.IsNotExist(err) { 57 | return err 58 | } 59 | lines, err := os.ReadFile(filename) 60 | if err != nil { 61 | return fmt.Errorf("Error reading file: %w", err) 62 | } 63 | for _, line := range strings.Split(string(lines), "\n") { 64 | line = strings.TrimSpace(line) 65 | if line == "" || strings.HasPrefix(line, "#") { 66 | continue // Skip empty lines and comments 67 | } 68 | parts := strings.Split(line, "~~") 69 | if len(parts) > 2 { 70 | return fmt.Errorf("Invalid line. Expected file-regex ~~ line-regex: %q\n", line) 71 | } 72 | fileRegex := strings.TrimSpace(parts[0]) 73 | if fileRegex == "" { 74 | return fmt.Errorf("File regex is empty: %q\n", line) 75 | } 76 | fRegex, err := regexp.Compile(fileRegex) 77 | if err != nil { 78 | return fmt.Errorf("Invalid file regex %q: %w", fileRegex, err) 79 | } 80 | 81 | var lineRegex string 82 | if len(parts) == 2 { 83 | lineRegex = strings.TrimSpace(parts[1]) 84 | } 85 | 86 | if lineRegex == "" { 87 | args.IgnorePods = append(args.IgnorePods, fRegex) 88 | continue 89 | } 90 | lRegex, err := regexp.Compile(lineRegex) 91 | if err != nil { 92 | return fmt.Errorf("Invalid line regex %q: %w", lineRegex, err) 93 | } 94 | args.IgnoreLogLines = append(args.IgnoreLogLines, record.IgnoreLogLine{ 95 | FileRegex: fRegex, 96 | LineRegex: lRegex, 97 | }) 98 | } 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Watch all Resources in a Kubernetes Cluster 2 | 3 | `watchall` is a tool which records changes to Kubernetes resources. 4 | 5 | ## Step 1: Record 6 | 7 | Be sure that `KUBECONFIG` is set correctly. 8 | 9 | ```bash 10 | go run github.com/guettli/watchall@latest record 11 | ``` 12 | 13 | The `record` command will dump all resources of the cluster into the directory 14 | `watchall-output/your-cluster:port`. 15 | 16 | ```log 17 | Watching "" "serviceaccounts" 18 | Watching "" "configmaps" 19 | ... 20 | ADDED Node /apo-e2e-control-plane 21 | ADDED ConfigMap argocd/kube-root-ca.crt 22 | ... 23 | ``` 24 | 25 | Then the tool waits for changes: 26 | 27 | ```log 28 | MODIFIED Event foo-system/foo-manager-64756cd977-gbnk5.182813b2d92d9874 29 | ... 30 | ``` 31 | 32 | You can have a look at the manifests: 33 | 34 | ```log 35 | find watchall-output/ 36 | 37 | watchall-output/127.0.0.1:41209/ConfigMap 38 | watchall-output/127.0.0.1:41209/ConfigMap/kube-node-lease 39 | watchall-output/127.0.0.1:41209/ConfigMap/kube-node-lease/kube-root-ca.crt 40 | watchall-output/127.0.0.1:41209/ConfigMap/kube-node-lease/kube-root-ca.crt/20250227-120853.212.yaml 41 | ... 42 | ``` 43 | 44 | As soon as a resource gets changed, the tool creates a new file with a new timestamp. 45 | 46 | Data in secrets get redacted with the sha256 hash. 47 | 48 | ## Step 2: Show Deltas 49 | 50 | If you are interested how resources change over time, use the `deltas` sub-command: 51 | 52 | ```text 53 | ❯ go run github.com/guettli/watchall@latest deltas -h 54 | 55 | This reads the files from the local disk and shows the changes. No connection to a cluster is needed. 56 | 57 | Usage: 58 | watchall deltas dir [flags] 59 | 60 | Flags: 61 | -h, --help help for deltas 62 | --only strings comma separated list of regex patterns to show 63 | --skip strings comma separated list of regex patterns to skip 64 | 65 | Global Flags: 66 | -o, --outdir string Directory to store output (default "watchall-output") 67 | -v, --verbose Create more output 68 | ``` 69 | 70 | Example: 71 | 72 | ```sh 73 | go run github.com/guettli/watchall@latest deltas watchall-output/127.0.0.1:41209/ 74 | ``` 75 | 76 | ```diff 77 | Using "watchall-output/127.0.0.1:41209/record-20250227-152147.53092" as start timestamp 78 | Diff of "Pod/foo-system/foo-manager-64756cd977-jx76c/20250227-152147.59958.yaml" "20250227-152234.11902.yaml" 79 | --- 20250227-152147.59958.yaml 80 | +++ 20250227-152234.11902.yaml 81 | @@ -16,7 +16,7 @@ 82 | kind: ReplicaSet 83 | name: foo-manager-64756cd977 84 | uid: 5106005f-af0b-435a-b152-12475e442f4a 85 | - resourceVersion: "724012" 86 | + resourceVersion: "725443" 87 | uid: 73e53f49-9f41-4bb5-a233-69f00e2dd2d0 88 | spec: 89 | containers: 90 | @@ -159,24 +159,23 @@ 91 | status: "True" 92 | type: PodScheduled 93 | containerStatuses: 94 | - - containerID: containerd://f607f6a8c928d143a77fd593c4fc7f49be8dcb0a8e7fc73b4cfc06da9b90ff00 95 | - image: ghcr.io/example/foo-manager:v1.4.0-beta.5 96 | - imageID: ghcr.io/example/foo-manager@sha256:cc899be1d48d5f61a784f240a4db63302d546a401929dda3fd46528e3e535e6e 97 | - lastState: 98 | - terminated: 99 | - containerID: containerd://f607f6a8c928d143a77fd593c4fc7f49be8dcb0a8e7fc73b4cfc06da9b90ff00 100 | - exitCode: 1 101 | - finishedAt: "2025-02-27T14:17:24Z" 102 | - reason: Error 103 | - startedAt: "2025-02-27T14:17:05Z" 104 | - name: manager 105 | - ready: false 106 | - restartCount: 14 107 | - started: false 108 | - state: 109 | - waiting: 110 | - message: back-off 5m0s restarting failed container=manager pod=foo-manager-64756cd977-jx76c_foo-system(73e53f49-9f41-4bb5-a233-69f00e2dd2d0) 111 | + - containerID: containerd://1a655f083606a3b0347e05966762c255baa1826842b58369b00bce543571c3a7 112 | + image: ghcr.io/example/foo-manager:v1.4.0-beta.5 113 | + imageID: ghcr.io/example/foo-manager@sha256:cc899be1d48d5f61a784f240a4db63302d546a401929dda3fd46528e3e535e6e 114 | + lastState: 115 | + terminated: 116 | + containerID: containerd://f607f6a8c928d143a77fd593c4fc7f49be8dcb0a8e7fc73b4cfc06da9b90ff00 117 | + exitCode: 1 118 | + finishedAt: "2025-02-27T14:17:24Z" 119 | + reason: Error 120 | + startedAt: "2025-02-27T14:17:05Z" 121 | + name: manager 122 | + ready: false 123 | + restartCount: 15 124 | + started: true 125 | + state: 126 | + running: 127 | - reason: CrashLoopBackOff 128 | + startedAt: "2025-02-27T14:22:34Z" 129 | hostIP: 172.18.0.2 130 | hostIPs: 131 | - ip: 172.18.0.2 132 | 133 | ... 134 | ``` 135 | 136 | Every time you start `record` a new record-TIMESTAMP file gets created. When you run `deltas` only 137 | the last changes get shown. 138 | 139 | TODO: Command line argument to define custom starttimestamps, or make the user choose one. 140 | 141 | ## Usage 142 | 143 | [Usage](https://github.com/guettli/watchall/blob/main/usage.md) 144 | 145 | ## Related 146 | 147 | [guettli/check-conditions: Check Conditions of all Kubernets Resources](https://github.com/guettli/check-conditions) 148 | -------------------------------------------------------------------------------- /internal/deltas/deltas.go: -------------------------------------------------------------------------------- 1 | package deltas 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "slices" 10 | "sort" 11 | "strings" 12 | "time" 13 | 14 | "github.com/akedrou/textdiff" 15 | "github.com/guettli/watchall/record" 16 | "k8s.io/apimachinery/pkg/api/equality" 17 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 18 | "k8s.io/apimachinery/pkg/runtime/serializer/json" 19 | "k8s.io/apimachinery/pkg/util/yaml" 20 | ) 21 | 22 | var resourcesToSkip = []string{ 23 | // "events.k8s.io/Event", // events do not get updated. No need to show a delta. 24 | } 25 | 26 | type fileType struct { 27 | basename string 28 | path string 29 | } 30 | 31 | func (f fileType) String() string { 32 | return filepath.Join(f.path, f.basename) 33 | } 34 | 35 | func Deltas(baseDir string, skipPatterns, onlyPatterns []string, skipInitial bool) error { 36 | baseDir = filepath.Clean(baseDir) 37 | skipRegex := make([]*regexp.Regexp, 0, len(skipPatterns)) 38 | for _, pattern := range skipPatterns { 39 | r, err := regexp.Compile(pattern) 40 | if err != nil { 41 | return fmt.Errorf("regexp.Compile() failed: %q %w", pattern, err) 42 | } 43 | skipRegex = append(skipRegex, r) 44 | } 45 | 46 | onlyRegex := make([]*regexp.Regexp, 0, len(onlyPatterns)) 47 | for _, pattern := range onlyPatterns { 48 | r, err := regexp.Compile(pattern) 49 | if err != nil { 50 | return fmt.Errorf("regexp.Compile() failed: %q %w", pattern, err) 51 | } 52 | onlyRegex = append(onlyRegex, r) 53 | } 54 | records, err := filepath.Glob(filepath.Join(baseDir, "record-*")) 55 | if err != nil { 56 | return fmt.Errorf("os.Glob() failed: %w", err) 57 | } 58 | if len(records) == 0 { 59 | return fmt.Errorf("no record-YYYYMM... file found in %s", baseDir) 60 | } 61 | slices.Sort(records) 62 | record := records[len(records)-1] 63 | startTimestamp := strings.SplitN(filepath.Base(record), "-", 2)[1] 64 | fmt.Printf("Using %q as start timestamp\n", record) 65 | var files []fileType 66 | 67 | err = filepath.WalkDir(baseDir, func(path string, info os.DirEntry, err error) error { 68 | if err != nil { 69 | return fmt.Errorf("filepath.WalkDir() failed: %w", err) 70 | } 71 | if info.IsDir() { 72 | return nil 73 | } 74 | if filepath.Dir(path) == baseDir { 75 | return nil 76 | } 77 | if info.Name() < startTimestamp { 78 | return nil 79 | } 80 | if doSkip(skipRegex, onlyRegex, path) { 81 | return nil 82 | } 83 | p, err := filepath.Rel(baseDir, filepath.Dir(path)) 84 | if err != nil { 85 | return fmt.Errorf("filepath.Rel() failed: %w", err) 86 | } 87 | files = append(files, fileType{ 88 | basename: info.Name(), 89 | path: p, 90 | }) 91 | return nil 92 | }) 93 | if err != nil { 94 | return fmt.Errorf("filepath.WalkDir() failed: %w", err) 95 | } 96 | 97 | sort.Slice(files, func(i, j int) bool { 98 | return files[i].basename < files[j].basename 99 | }) 100 | for _, file := range files { 101 | err := showFile(baseDir, file, startTimestamp, !skipInitial) 102 | if err != nil { 103 | return fmt.Errorf("showFile() failed: %w", err) 104 | } 105 | } 106 | return nil 107 | } 108 | 109 | func doSkip(skipRegex, onlyRegex []*regexp.Regexp, path string) bool { 110 | if len(onlyRegex) > 0 { 111 | for _, r := range onlyRegex { 112 | if r.MatchString(path) { 113 | return false 114 | } 115 | } 116 | return true 117 | } 118 | for _, r := range skipRegex { 119 | if r.MatchString(path) { 120 | return true 121 | } 122 | } 123 | return false 124 | } 125 | 126 | func showFile(baseDir string, file fileType, startTimestamp string, showInitialYaml bool) error { 127 | if file.basename < startTimestamp { 128 | // fmt.Printf("Skipping %q because before %s %s\n", file.String(), startTimestamp, previous) 129 | return nil 130 | } 131 | for _, resource := range resourcesToSkip { 132 | if strings.HasPrefix(file.path, resource+string(filepath.Separator)) { 133 | // fmt.Printf("Skipping %s\n", file.String()) 134 | return nil 135 | } 136 | } 137 | 138 | if strings.HasSuffix(file.basename, ".log") { 139 | data, err := os.ReadFile(filepath.Join(baseDir, file.path, file.basename)) 140 | if err != nil { 141 | return fmt.Errorf("os.ReadFile() failed: %w", err) 142 | } 143 | fmt.Printf("Log: %s\n%s\n\n", file.String(), data) 144 | return nil 145 | } 146 | 147 | absDir := filepath.Join(baseDir, file.path) 148 | // find previous file 149 | dirEntries, err := os.ReadDir(absDir) 150 | if err != nil { 151 | return fmt.Errorf("os.ReadDir() failed: %w", err) 152 | } 153 | sort.Slice(dirEntries, func(i, j int) bool { 154 | return dirEntries[i].Name() > dirEntries[j].Name() 155 | }) 156 | found := false 157 | previous := "" 158 | for _, entry := range dirEntries { 159 | if entry.IsDir() { 160 | continue 161 | } 162 | if !strings.HasSuffix(entry.Name(), ".yaml") { 163 | continue 164 | } 165 | if found { 166 | previous = entry.Name() 167 | break 168 | } 169 | if entry.Name() == file.basename { 170 | found = true 171 | } 172 | } 173 | if !found { 174 | return fmt.Errorf("internal error. Not found: %q %s", file.path, file.basename) 175 | } 176 | if previous == "" { 177 | if showInitialYaml { 178 | content, err := os.ReadFile(filepath.Join(absDir, file.basename)) 179 | if err != nil { 180 | return fmt.Errorf("os.ReadFile() failed: %w", err) 181 | } 182 | obj, err := yamlToUnstructured(content) 183 | if err != nil { 184 | return fmt.Errorf("failed to decode first YAML: %w", err) 185 | } 186 | stripIrrelevantFields(obj) 187 | s, err := unstructuredToString(obj) 188 | if err != nil { 189 | return fmt.Errorf("unstructuredToString failed %q: %w", file.basename, err) 190 | } 191 | fmt.Printf("\nInitial YAML: %s\n%s", file.String(), s) 192 | } 193 | return nil 194 | } 195 | return compareTwoYamlFiles(baseDir, filepath.Join(absDir, previous), 196 | filepath.Join(absDir, file.basename)) 197 | } 198 | 199 | func compareTwoYamlFiles(baseDir, f1, f2 string) error { 200 | yaml1, err := os.ReadFile(f1) 201 | if err != nil { 202 | return fmt.Errorf("failed to read %q: %w", f1, err) 203 | } 204 | 205 | yaml2, err := os.ReadFile(f2) 206 | if err != nil { 207 | return fmt.Errorf("failed to read %q: %w", f2, err) 208 | } 209 | 210 | // Decode the YAML into unstructured objects 211 | obj1, err := yamlToUnstructured(yaml1) 212 | if err != nil { 213 | return fmt.Errorf("failed to decode first YAML: %q %w", f1, err) 214 | } 215 | 216 | obj2, err := yamlToUnstructured(yaml2) 217 | if err != nil { 218 | return fmt.Errorf("failed to decode second YAML: %q %w", f2, err) 219 | } 220 | 221 | // Strip irrelevant fields (like resourceVersion) 222 | stripIrrelevantFields(obj1) 223 | stripIrrelevantFields(obj2) 224 | 225 | // Compare the objects 226 | if equality.Semantic.DeepEqual(obj1, obj2) { 227 | fmt.Printf("No changes in %q %q\n\n", f1, f2) 228 | return nil 229 | } 230 | s1, err := unstructuredToString(obj1) 231 | if err != nil { 232 | return fmt.Errorf("unstructuredToString failed %q: %w", f1, err) 233 | } 234 | s2, err := unstructuredToString(obj2) 235 | if err != nil { 236 | return fmt.Errorf("unstructuredToString failed %q: %w", f2, err) 237 | } 238 | 239 | diff := textdiff.Unified(filepath.Base(f1), filepath.Base(f2), s1, s2) 240 | p, err := filepath.Rel(baseDir, f1) 241 | if err != nil { 242 | return fmt.Errorf("filepath.Rel() failed: %w", err) 243 | } 244 | time1, err := baseNameToTimestamp(filepath.Base(f1)) 245 | if err != nil { 246 | return fmt.Errorf("baseNameToTimestamp failed: %w", err) 247 | } 248 | time2, err := baseNameToTimestamp(filepath.Base(f2)) 249 | if err != nil { 250 | return fmt.Errorf("baseNameToTimestamp failed: %w", err) 251 | } 252 | d := time2.Sub(time1) 253 | fmt.Printf("\nDiff of %q %q (%s)\n%s\n\n", p, filepath.Base(f2), 254 | d.Truncate(time.Second).String(), diff) 255 | return nil 256 | } 257 | 258 | func baseNameToTimestamp(baseName string) (time.Time, error) { 259 | baseName = strings.TrimSuffix(baseName, ".yaml") 260 | t, err := time.Parse(record.TimeFormat, baseName) 261 | if err != nil { 262 | return time.Time{}, fmt.Errorf("time.Parse() format=%s failed: %w", record.TimeFormat, err) 263 | } 264 | return t, nil 265 | } 266 | 267 | func unstructuredToString(obj *unstructured.Unstructured) (string, error) { 268 | serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil) 269 | var buffer bytes.Buffer 270 | err := serializer.Encode(obj, &buffer) 271 | if err != nil { 272 | return "", fmt.Errorf("failed to serialize to YAML: %w", err) 273 | } 274 | return buffer.String(), nil 275 | } 276 | 277 | func yamlToUnstructured(yamlData []byte) (*unstructured.Unstructured, error) { 278 | // Convert YAML to JSON 279 | jsonData, err := yaml.ToJSON(yamlData) 280 | if err != nil { 281 | return nil, fmt.Errorf("failed to convert YAML to JSON: %w", err) 282 | } 283 | 284 | // Unmarshal JSON into an unstructured.Unstructured object 285 | obj := &unstructured.Unstructured{} 286 | if err := obj.UnmarshalJSON(jsonData); err != nil { 287 | return nil, fmt.Errorf("failed to unmarshal JSON to Unstructured: %w", err) 288 | } 289 | 290 | return obj, nil 291 | } 292 | 293 | func stripIrrelevantFields(obj *unstructured.Unstructured) { 294 | // Remove metadata fields that are not relevant 295 | unstructured.RemoveNestedField(obj.Object, "metadata", "managedFields") 296 | unstructured.RemoveNestedField(obj.Object, "metadata", "annotations", "kubectl.kubernetes.io/last-applied-configuration") 297 | unstructured.RemoveNestedField(obj.Object, "metadata", "resourceVersion") 298 | unstructured.RemoveNestedField(obj.Object, "metadata", "generation") 299 | unstructured.RemoveNestedField(obj.Object, "metadata", "uid") 300 | } 301 | -------------------------------------------------------------------------------- /record/record.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/sha256" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "slices" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | corev1 "k8s.io/api/core/v1" 17 | "k8s.io/utils/ptr" 18 | 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "k8s.io/apimachinery/pkg/watch" 23 | "k8s.io/client-go/discovery" 24 | "k8s.io/client-go/dynamic" 25 | "k8s.io/client-go/kubernetes" 26 | "k8s.io/client-go/tools/clientcmd" 27 | "sigs.k8s.io/yaml" 28 | ) 29 | 30 | const TimeFormat = "20060102-150405.00000" 31 | 32 | type IgnoreLogLine struct { 33 | FileRegex *regexp.Regexp 34 | LineRegex *regexp.Regexp 35 | } 36 | type Arguments struct { 37 | Verbose bool 38 | OutputDirectory string 39 | WithLogs bool 40 | DisableResourceRecording bool 41 | IgnoreLogLinesFile string 42 | IgnoreLogLines []IgnoreLogLine 43 | IgnorePods []*regexp.Regexp 44 | } 45 | 46 | func RunRecordWithContext(ctx context.Context, args Arguments, kubeconfig clientcmd.ClientConfig) (*sync.WaitGroup, error) { 47 | config, err := kubeconfig.ClientConfig() 48 | if err != nil { 49 | return nil, fmt.Errorf("kubeconfig.ClientConfig() failed: %w", err) 50 | } 51 | 52 | config.QPS = -1 53 | config.Burst = -1 54 | 55 | clientset, err := kubernetes.NewForConfig(config) 56 | if err != nil { 57 | return nil, fmt.Errorf("kubernetes.NewForConfig() failed: %w", err) 58 | } 59 | 60 | dynClient, err := dynamic.NewForConfig(config) 61 | if err != nil { 62 | return nil, fmt.Errorf("dynamic.NewForConfig() failed: %w", err) 63 | } 64 | 65 | discoveryClient := clientset.Discovery() 66 | 67 | // Get the list of all API resources available 68 | serverResources, err := discoveryClient.ServerPreferredResources() 69 | if err != nil { 70 | if discovery.IsGroupDiscoveryFailedError(err) { 71 | fmt.Printf("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s\n", err.Error()) 72 | fmt.Printf("WARNING: To fix this, kubectl delete apiservice \n") 73 | } else { 74 | return nil, fmt.Errorf("discoveryClient.ServerPreferredResources() failed: %w", err) 75 | } 76 | } 77 | host := strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(config.Host, "https://"), "http://"), ":443") 78 | var wg sync.WaitGroup 79 | 80 | if !args.DisableResourceRecording { 81 | err = createRecorders(ctx, &wg, serverResources, args, dynClient, host) 82 | if err != nil { 83 | return nil, fmt.Errorf("createRecorders() failed: %w", err) 84 | } 85 | } 86 | 87 | if args.WithLogs { 88 | err = createLogScraper(ctx, &wg, clientset, args, host) 89 | if err != nil { 90 | return nil, fmt.Errorf("createLogScraper() failed: %w", err) 91 | } 92 | } 93 | return &wg, nil 94 | } 95 | 96 | func createLogScraper(ctx context.Context, wg *sync.WaitGroup, 97 | clientset *kubernetes.Clientset, args Arguments, host string, 98 | ) error { 99 | pods, err := clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) 100 | if err != nil { 101 | return fmt.Errorf("clientset.CoreV1().Pods().List() failed: %w", err) 102 | } 103 | 104 | for _, pod := range pods.Items { 105 | skip := false 106 | for _, ignorePod := range args.IgnorePods { 107 | if ignorePod.MatchString(pod.Name) { 108 | fmt.Printf("Skipping pod %s/%s because it matches ignore-pod-regex %q\n", pod.Namespace, pod.Name, ignorePod.String()) 109 | skip = true 110 | break 111 | } 112 | } 113 | if skip { 114 | continue 115 | } 116 | var regexOfThisPod []*regexp.Regexp 117 | for _, ignoreLine := range args.IgnoreLogLines { 118 | if ignoreLine.FileRegex.MatchString(pod.Name) { 119 | regexOfThisPod = append(regexOfThisPod, ignoreLine.LineRegex) 120 | } 121 | } 122 | for _, container := range pod.Spec.Containers { 123 | wg.Add(1) 124 | go readPodLogs(ctx, wg, clientset, args, host, pod.Name, pod.Namespace, 125 | container.Name, regexOfThisPod) 126 | } 127 | } 128 | return nil 129 | } 130 | 131 | func readPodLogs(ctx context.Context, wg *sync.WaitGroup, clientset *kubernetes.Clientset, args Arguments, host, podName, namespace, containerName string, ignoreLineRegexs []*regexp.Regexp) { 132 | defer wg.Done() 133 | fmt.Printf("Watching logs for pod %s/%s container %s\n", namespace, podName, containerName) 134 | stream, err := clientset.CoreV1().Pods(namespace).GetLogs(podName, 135 | &corev1.PodLogOptions{ 136 | Container: containerName, 137 | Follow: true, 138 | SinceSeconds: ptr.To(int64(1)), 139 | }, 140 | ).Stream(ctx) 141 | if err != nil { 142 | fmt.Fprintf(os.Stderr, "Error streaming logs for %s/%s [%s]: %v\n", namespace, podName, containerName, err) 143 | return 144 | } 145 | defer stream.Close() 146 | 147 | scanner := bufio.NewScanner(stream) 148 | for scanner.Scan() { 149 | line := scanner.Text() 150 | for _, ignoreLineRegex := range ignoreLineRegexs { 151 | if ignoreLineRegex.MatchString(line) { 152 | fmt.Printf("Ignoring log line for pod %s/%s container %s: %q\n", namespace, podName, containerName, line) 153 | continue 154 | } 155 | } 156 | dir := filepath.Join(args.OutputDirectory, host, "core", "Pod", namespace, podName) 157 | err = os.MkdirAll(dir, 0o700) 158 | if err != nil { 159 | fmt.Printf("Error creating directory %s: %s\n", dir, err) 160 | continue 161 | } 162 | 163 | file := filepath.Join(dir, time.Now().UTC().Format(TimeFormat)+".log") 164 | 165 | if err := os.WriteFile(file, []byte(line+"\n"), 0o600); err != nil { 166 | fmt.Printf("Error writing log file %s: %s\n", file, err) 167 | continue 168 | } 169 | fmt.Printf("Created log file %s for pod %s/%s container %s\n", file, namespace, podName, containerName) 170 | } 171 | if err := scanner.Err(); err != nil { 172 | fmt.Fprintf(os.Stderr, "Error reading logs for %s/%s [%s]: %v\n", namespace, podName, containerName, err) 173 | } 174 | } 175 | 176 | func createRecorders(ctx context.Context, wg *sync.WaitGroup, serverResources []*metav1.APIResourceList, args Arguments, dynClient *dynamic.DynamicClient, host string) error { 177 | baseDir := filepath.Join(args.OutputDirectory, host) 178 | err := os.MkdirAll(baseDir, 0o700) 179 | if err != nil { 180 | return fmt.Errorf("os.MkdirAll() failed: %w", err) 181 | } 182 | 183 | // the recordFile creates a marker file, so that the current time gets recorded. 184 | // This is useful to find out when the recording started. 185 | recordFile := filepath.Join(baseDir, "record-"+time.Now().UTC().Format(TimeFormat)) 186 | 187 | err = os.WriteFile(recordFile, []byte(""), 0o600) 188 | if err != nil { 189 | return fmt.Errorf("os.WriteFile() failed %q: %w", recordFile, err) 190 | } 191 | 192 | for _, resourceList := range serverResources { 193 | groupVersion, err := schema.ParseGroupVersion(resourceList.GroupVersion) 194 | if err != nil { 195 | fmt.Fprintf(os.Stderr, "Failed to parse group version: %v\n", err) 196 | continue 197 | } 198 | for i := range resourceList.APIResources { 199 | resourceName := resourceList.APIResources[i].Name 200 | if slices.Contains(resourcesToSkip, groupResource{groupVersion.Group, resourceName}) { 201 | continue 202 | } 203 | wg.Add(1) 204 | go watchGVR(ctx, wg, &args, dynClient, schema.GroupVersionResource{ 205 | Group: groupVersion.Group, 206 | Version: groupVersion.Version, 207 | Resource: resourceName, 208 | }, host) 209 | } 210 | } 211 | return nil 212 | } 213 | 214 | type groupResource struct { 215 | group string 216 | resource string 217 | } 218 | 219 | var resourcesToSkip = []groupResource{ 220 | {"authentication.k8s.io", "tokenreviews"}, 221 | {"authorization.k8s.io", "localsubjectaccessreviews"}, 222 | {"authorization.k8s.io", "subjectaccessreviews"}, 223 | {"authorization.k8s.io", "selfsubjectrulesreviews"}, 224 | {"authorization.k8s.io", "selfsubjectaccessreviews"}, 225 | {"", "componentstatuses"}, 226 | {"", "bindings"}, 227 | {"", "events"}, // exists twice. Second time with group events.k8s.io 228 | {"metallb.io", "addresspools"}, 229 | {"coordination.k8s.io", "leases"}, // Leases create too many modifications 230 | } 231 | 232 | // watchGVR is called as Goroutine. It prints errors. 233 | func watchGVR(ctx context.Context, wg *sync.WaitGroup, args *Arguments, dynClient *dynamic.DynamicClient, gvr schema.GroupVersionResource, host string) { 234 | defer wg.Done() 235 | fmt.Printf("Watching %q %q\n", gvr.Group, gvr.Resource) 236 | 237 | watch, err := dynClient.Resource(gvr).Watch(ctx, metav1.ListOptions{}) 238 | if err != nil { 239 | fmt.Printf("..Error watching %v. group %q version %q resource %q\n", err, 240 | gvr.Group, gvr.Version, gvr.Resource) 241 | return 242 | } 243 | defer watch.Stop() 244 | for { 245 | select { 246 | case event, ok := <-watch.ResultChan(): 247 | if !ok { 248 | // If there are not objects in a resource, the watch gets closed. 249 | return 250 | } 251 | err := handleEvent(args, gvr, event, host) 252 | if err != nil { 253 | fmt.Printf("Error handling event: %v\n", err) 254 | } 255 | case <-ctx.Done(): 256 | return 257 | } 258 | } 259 | } 260 | 261 | func handleEvent(args *Arguments, gvr schema.GroupVersionResource, event watch.Event, host string) error { 262 | if event.Object == nil { 263 | return fmt.Errorf("event.Object is nil? Skipping this event. Type=%s %+v gvr: (group=%s version=%s resource=%s)", event.Type, event, 264 | gvr.Group, gvr.Version, gvr.Resource) 265 | } 266 | gvk := event.Object.GetObjectKind().GroupVersionKind() 267 | obj, ok := event.Object.(*unstructured.Unstructured) 268 | if !ok { 269 | return fmt.Errorf("internal Error, could not cast to Unstructered %T %+v", event.Object, event.Object) 270 | } 271 | switch event.Type { 272 | case watch.Modified, watch.Added, watch.Deleted, watch.Bookmark, watch.Error: 273 | fmt.Printf("%s %s %s/%s\n", event.Type, gvk.Kind, 274 | getString(obj, "metadata", "namespace"), 275 | getString(obj, "metadata", "name"), 276 | ) 277 | if err := storeResource(args, gvk.Group, gvk.Kind, obj, host); err != nil { 278 | return fmt.Errorf("error storing resource: %w", err) 279 | } 280 | default: 281 | fmt.Printf("Internal Error, unknown event %s %+v %s\n", event.Type, gvk, event.Object) 282 | } 283 | return nil 284 | } 285 | 286 | func redactSecret(obj *unstructured.Unstructured) { 287 | for _, key := range []string{"data", "stringData"} { 288 | m, found, err := unstructured.NestedStringMap(obj.Object, key) 289 | if !found || err != nil { 290 | continue 291 | } 292 | for k, v := range m { 293 | if v == "" { 294 | continue 295 | } 296 | m[k] = fmt.Sprintf("redacted-to-sha256:%x", sha256.Sum256([]byte(v))) 297 | } 298 | err = unstructured.SetNestedStringMap(obj.Object, m, key) 299 | if err != nil { 300 | continue 301 | } 302 | } 303 | } 304 | 305 | func storeResource(args *Arguments, group string, kind string, obj *unstructured.Unstructured, host string) error { 306 | if group == "" && kind == "Secret" { 307 | redactSecret(obj) 308 | } 309 | bytes, err := yaml.Marshal(obj) 310 | if err != nil { 311 | return fmt.Errorf("yaml.Marshal(obj) failed: %w", err) 312 | } 313 | name := getString(obj, "metadata", "name") 314 | if name == "" { 315 | return fmt.Errorf("obj has no name? %+v", obj) 316 | } 317 | ns := getString(obj, "metadata", "namespace") 318 | if group == "" { 319 | group = "core" 320 | } 321 | dir := filepath.Join(args.OutputDirectory, host, group, kind, ns, name) 322 | err = os.MkdirAll(dir, 0o700) 323 | if err != nil { 324 | return fmt.Errorf("os.MkdirAll() failed: %w", err) 325 | } 326 | file := filepath.Join(dir, time.Now().UTC().Format(TimeFormat)+".yaml") 327 | if err := os.WriteFile(file, bytes, 0o600); err != nil { 328 | return fmt.Errorf("os.WriteFile() failed: %w", err) 329 | } 330 | return nil 331 | } 332 | 333 | func getString(obj *unstructured.Unstructured, fields ...string) string { 334 | val, found, err := unstructured.NestedString(obj.Object, fields...) 335 | if !found || err != nil { 336 | return "" 337 | } 338 | return val 339 | } 340 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/akedrou/textdiff v0.1.0 h1:K7nbOVQju7/coCXnJRJ2fsltTwbSvC+M4hKBUJRBRGY= 2 | github.com/akedrou/textdiff v0.1.0/go.mod h1:a9CCC49AKtFTmVDNFHDlCg7V/M7C7QExDAhb2SkL6DQ= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 10 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 11 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 12 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 13 | github.com/gavv/cobradoc v1.1.0 h1:UVAfle1xOwRdqlY5qJhDEJ4SgLyQtTdGCqLNYixp1m4= 14 | github.com/gavv/cobradoc v1.1.0/go.mod h1:Dz0SCTUhrmpCY1FJ+hudpCMWUeCjasu6SREDygYw9sA= 15 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 16 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 17 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 18 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 19 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 20 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 21 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 22 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 23 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 24 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 25 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 26 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 27 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 28 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 29 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 30 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 31 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 32 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 33 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 34 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 35 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 36 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 37 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 38 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 39 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 40 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 41 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 42 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 43 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 44 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 45 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 46 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 47 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 48 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 49 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 50 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 51 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 52 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 53 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 54 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 55 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 56 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 57 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 58 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 59 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 60 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 61 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 65 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 66 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 67 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 68 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 69 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 70 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 71 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 72 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 73 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 74 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 75 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 76 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 77 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 78 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 79 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 80 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 81 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 82 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 83 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 84 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 85 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 86 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 87 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 88 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 89 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 90 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 91 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 92 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 93 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 94 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 95 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 96 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 97 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 98 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 99 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 100 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 101 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 102 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 103 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 104 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 105 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 106 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 107 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 108 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 109 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 110 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 111 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 112 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 113 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 114 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 115 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 116 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 119 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 120 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 121 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 128 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 129 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 130 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 131 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= 132 | golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= 133 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 134 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 135 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 136 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 137 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 138 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 139 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 140 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 141 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 142 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 143 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 144 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 145 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 146 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 147 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 148 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 149 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 150 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 151 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 152 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 153 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 154 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 155 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 156 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 157 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 158 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 159 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 160 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 161 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 162 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 163 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 164 | k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= 165 | k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= 166 | k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= 167 | k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 168 | k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= 169 | k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= 170 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 171 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 172 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 173 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 174 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 175 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 176 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 177 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 178 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 179 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 180 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 181 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 182 | --------------------------------------------------------------------------------