diff --git a/Dockerfile b/Dockerfile index f3175bcf..ab23db9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ COPY internal/ ./internal/ # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS="${TARGETOS:-linux}" GOARCH="${TARGETARCH}" go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS="${TARGETOS:-linux}" GOARCH="${TARGETARCH}" go build -a -o manager cmd/manager/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/Makefile b/Makefile index b2e24953..b324f38c 100644 --- a/Makefile +++ b/Makefile @@ -118,7 +118,10 @@ helm-crd-copy: yq kustomize ## Copy CRDs from kustomize to helm-chart .PHONY: build build: manifests generate fmt vet ## Build manager binary. - go build -o bin/manager cmd/main.go + go build -o bin/manager cmd/manager/main.go + +build-plugin: + go build -o bin/kubectl-etcd cmd/kubectl-etcd/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/cmd/kubectl-etcd/main.go b/cmd/kubectl-etcd/main.go new file mode 100644 index 00000000..05be2e06 --- /dev/null +++ b/cmd/kubectl-etcd/main.go @@ -0,0 +1,702 @@ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/dustin/go-humanize" + "github.com/spf13/cobra" + clientv3 "go.etcd.io/etcd/client/v3" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" + "k8s.io/client-go/util/homedir" +) + +func main() { + var rootCmd = &cobra.Command{ + Use: "kubectl-etcd", + Short: "Kubectl etcd plugin", + Long: `Manage etcd pods spawned by etcd-operator`, + } + + // Initialize configuration + config := initializeConfig(rootCmd) + + // Register subcommands + rootCmd.AddCommand( + createStatusCmd(config), + createDefragCmd(config), + createCompactCmd(config), + createAlarmCmd(config), + createForfeitLeadershipCmd(config), + createLeaveCmd(config), + createMembersCmd(config), + createRemoveMemberCmd(config), + createAddMemberCmd(config), + createSnapshotCmd(config), + ) + + // Execute the root command + if err := rootCmd.Execute(); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } +} + +func createStatusCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "status", + Short: "Get the status of etcd cluster member", + Run: func(cmd *cobra.Command, args []string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println(err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + status, err := etcdClient.Status(ctx, etcdClient.Endpoints()[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get etcd status: %v\n", err) + return + } + + fmt.Printf("%-17s %-9s %-15s %-18s %-11s %-20s %-8s %-s\n", + "MEMBER", "DB SIZE", "IN USE", "LEADER", "RAFT INDEX", "RAFT APPLIED INDEX", "LEARNER", "ERRORS") + inUse := fmt.Sprintf("%.2f%%", float64(status.DbSizeInUse)/float64(status.DbSize)*100) + fmt.Printf("%-17x %-9s %-15s %-18x %-11d %-20d %-8v\n", + status.Header.MemberId, humanize.Bytes(uint64(status.DbSize)), + fmt.Sprintf("%s (%s)", humanize.Bytes(uint64(status.DbSizeInUse)), inUse), + status.Leader, status.RaftIndex, status.RaftAppliedIndex, status.IsLearner) + }, + } +} + +func createDefragCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "defrag", + Short: "Defragment etcd database on the node", + Long: `Defragmentation is a maintenance operation that compacts the historical +records and optimizes the database storage.`, + Run: func(cmd *cobra.Command, args []string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println(err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + _, err = etcdClient.Defragment(ctx, etcdClient.Endpoints()[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to defragment etcd database: %v\n", err) + return + } + }, + } +} + +func createCompactCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "compact", + Short: "Compact the etcd database", + Long: `Compacts the etcd database up to the latest revision to free up space. +This removes old versions of keys and their associated data.`, + Run: func(cmd *cobra.Command, args []string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println("Error setting up etcd client:", err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Fetch the latest revision + statusResp, err := etcdClient.Status(ctx, etcdClient.Endpoints()[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get etcd status: %v\n", err) + return + } + + // Compact the etcd database up to the latest revision + _, err = etcdClient.Compact(ctx, statusResp.Header.Revision) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to compact etcd database: %v\n", err) + return + } + }, + } +} + +func createAlarmCmd(config *Config) *cobra.Command { + alarmCmd := &cobra.Command{ + Use: "alarm", + Short: "Manage etcd alarms", + Long: `Manage the alarms of an etcd cluster.`, + } + + alarmCmd.AddCommand( + createAlarmsListCmd(config), + createAlarmsDisarmCmd(config), + ) + + return alarmCmd +} + +func createAlarmsListCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List the etcd alarms for the node", + Run: func(cmd *cobra.Command, args []string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println("Error setting up etcd client:", err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Call to etcd client to list alarms + resp, err := etcdClient.AlarmList(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to list etcd alarms: %v\n", err) + return + } + + for _, alarm := range resp.Alarms { + fmt.Printf("Alarm: %v, MemberID: %x\n", alarm.Alarm, alarm.MemberID) + } + }, + } +} + +func createAlarmsDisarmCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "disarm", + Short: "Disarm the etcd alarms for the node", + Run: func(cmd *cobra.Command, args []string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println("Error setting up etcd client:", err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Call to etcd client to disarm alarms + _, err = etcdClient.AlarmDisarm(ctx, &clientv3.AlarmMember{}) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to disarm etcd alarms: %v\n", err) + return + } + }, + } +} + +// setupEtcdClient sets up the port forwarding and creates an etcd client. +func setupEtcdClient(config *Config) (*clientv3.Client, error) { + if config.PodName == "" { + return nil, fmt.Errorf("You must specify the pod name") + } + + clientConfig, err := clientcmd.BuildConfigFromFlags("", config.Kubeconfig) + if err != nil { + return nil, fmt.Errorf("error building kubeconfig: %s", err) + } + + clientset, err := kubernetes.NewForConfig(clientConfig) + if err != nil { + return nil, fmt.Errorf("error creating Kubernetes client: %s", err) + } + + tlsConfig, localPort, err := setupPortForwarding(config, clientset) + if err != nil { + return nil, fmt.Errorf("failed to setup port forwarding: %s", err) + } + + etcdConfig := clientv3.Config{ + Endpoints: []string{fmt.Sprintf("localhost:%d", localPort)}, + DialTimeout: 5 * time.Second, + } + if tlsConfig != nil { + etcdConfig.TLS = tlsConfig + } + + etcdClient, err := clientv3.New(etcdConfig) + if err != nil { + return nil, fmt.Errorf("failed to connect to etcd server: %s", err) + } + + return etcdClient, nil +} + +func createForfeitLeadershipCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "forfeit-leadership", + Short: "Tell node to forfeit etcd cluster leadership", + Run: func(cmd *cobra.Command, args []string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println("Error setting up etcd client:", err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Retrieve the current status to find the leader + status, err := etcdClient.Status(ctx, etcdClient.Endpoints()[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get current etcd status: %v\n", err) + return + } + + // Retrieve member list to find a member to transfer leadership to + members, err := etcdClient.MemberList(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get etcd member list: %v\n", err) + return + } + + for _, member := range members.Members { + if member.ID != status.Leader { + _, err = etcdClient.MoveLeader(ctx, member.ID) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to forfeit leadership: %v\n", err) + return + } + return + } + } + fmt.Println("No eligible member found to transfer leadership to or already not the leader.") + }, + } +} + +func createLeaveCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "leave", + Short: "Tell node to leave etcd cluster", + Run: func(cmd *cobra.Command, args []string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println("Error setting up etcd client:", err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // This operation might require administrative privileges on the etcd cluster. + memberListResp, err := etcdClient.MemberList(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to retrieve member list: %v\n", err) + return + } + + for _, member := range memberListResp.Members { + if member.Name == config.PodName { // Assuming PodName is set as the member name + _, err = etcdClient.MemberRemove(ctx, member.ID) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to remove member from cluster: %v\n", err) + return + } + return + } + } + + fmt.Println("Specified pod is not a member of the etcd cluster.") + }, + } +} + +func createMembersCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "members", + Short: "Get the list of etcd cluster members", + Run: func(cmd *cobra.Command, args []string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println("Error setting up etcd client:", err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + membersResp, err := etcdClient.MemberList(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to list etcd members: %v\n", err) + return + } + + // Header for the table + fmt.Printf("%-19s %-10s %-30s %-30s %-7s\n", "ID", "HOSTNAME", "PEER URLS", "CLIENT URLS", "LEARNER") + for _, member := range membersResp.Members { + fmt.Printf("%-19x %-10s %-30s %-30s %-7v\n", + member.ID, member.Name, strings.Join(member.PeerURLs, ","), strings.Join(member.ClientURLs, ","), member.IsLearner) + } + }, + } +} + +func createRemoveMemberCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "remove-member ", + Short: "Remove a node from the etcd cluster", + Long: `Remove a member from the etcd cluster using its member ID.`, + Args: cobra.ExactArgs(1), // Ensures exactly one argument is passed + Run: func(cmd *cobra.Command, args []string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println("Error setting up etcd client:", err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Parse the member ID from the command line argument + memberID, err := strconv.ParseUint(args[0], 16, 64) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid member ID format: %v\n", err) + return + } + + // Remove the member using the provided member ID + _, err = etcdClient.MemberRemove(ctx, memberID) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to remove member: %v\n", err) + return + } + }, + } +} + +func createAddMemberCmd(config *Config) *cobra.Command { + return &cobra.Command{ + Use: "add-member [urls]", + Short: "Add a new member to the etcd cluster", + Long: `Add a new member to the etcd cluster using specified peer URLs.`, + Args: cobra.ExactArgs(1), // Requires exactly one argument: the new member URL + Run: func(cmd *cobra.Command, args []string) { + addMember(config, args[0]) + }, + } +} + +func addMember(config *Config, memberURL string) { + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Printf("Failed to set up etcd client: %s\n", err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + urls := []string{memberURL} + _, err = etcdClient.MemberAdd(ctx, urls) + if err != nil { + fmt.Printf("Failed to add member: %s\n", err) + return + } + + fmt.Println("Member successfully added") +} + +func createSnapshotCmd(config *Config) *cobra.Command { + var snapshotCmd = &cobra.Command{ + Use: "snapshot ", + Short: "Stream snapshot of the etcd node to the path.", + Long: `Take a snapshot of the etcd database and save it to a specified file path. +This operation is typically used for backup purposes.`, + Args: cobra.ExactArgs(1), // This command requires exactly one argument for the file path + Run: func(cmd *cobra.Command, args []string) { + path := args[0] // The file path where the snapshot will be saved + + etcdClient, err := setupEtcdClient(config) + if err != nil { + fmt.Println("Error setting up etcd client:", err) + return + } + //nolint:errcheck + defer etcdClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) // Snapshot can take time + defer cancel() + + // Requesting a snapshot from the etcd server + r, err := etcdClient.Snapshot(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create snapshot: %v\n", err) + return + } + //nolint:errcheck + defer r.Close() // Make sure to close the snapshot reader + + // Open the file for writing the snapshot + f, err := os.Create(path) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to open file %s for writing: %v\n", path, err) + return + } + //nolint:errcheck + defer f.Close() // Ensure file is closed after writing + + // Copy the snapshot stream to the file + if _, err = io.Copy(f, r); err != nil { + fmt.Fprintf(os.Stderr, "Failed to write snapshot to file: %v\n", err) + return + } + }, + } + + // Optional flags can be added here + + return snapshotCmd +} + +func setupPortForwarding(config *Config, clientset *kubernetes.Clientset) (*tls.Config, uint16, error) { + pod, err := clientset.CoreV1().Pods(config.Namespace).Get(context.Background(), config.PodName, metav1.GetOptions{}) + if err != nil { + return nil, 0, fmt.Errorf("failed to get pod: %w", err) + } + + path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", config.Namespace, config.PodName) + clientConfig, err := clientcmd.BuildConfigFromFlags("", config.Kubeconfig) + if err != nil { + return nil, 0, fmt.Errorf("error building kubeconfig: %w", err) + } + + transport, upgrader, err := spdy.RoundTripperFor(clientConfig) + if err != nil { + return nil, 0, fmt.Errorf("failed to create round tripper: %w", err) + } + + hostURL, err := url.Parse(clientConfig.Host) + if err != nil { + return nil, 0, fmt.Errorf("failed to parse host URL: %w", err) + } + + hostURL.Path = path + + stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1) + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", hostURL) + + silentOut := &silentWriter{} + portForwarder, err := portforward.New(dialer, []string{"0:2379"}, stopChan, readyChan, silentOut, os.Stderr) + if err != nil { + return nil, 0, fmt.Errorf("failed to create port forwarder: %w", err) + } + + // Starting port forwarding + go func() { + if err := portForwarder.ForwardPorts(); err != nil { + fmt.Printf("Failed to start port forwarding: %s\n", err) + } + }() + + <-readyChan // Waiting for port forwarding to be ready + + // Obtaining the local port used for forwarding + forwardedPorts, err := portForwarder.GetPorts() + if err != nil { + return nil, 0, fmt.Errorf("failed to get forwarded ports: %w", err) + } + + localPort := forwardedPorts[0].Local + + tlsConfig, err := getTLSConfig(clientset, pod, config.Namespace) + if err != nil { + return nil, 0, fmt.Errorf("failed to get TLS config: %w", err) + } + + return tlsConfig, localPort, nil +} + +// Initialize configuration via Cobra command +func initializeConfig(cmd *cobra.Command) *Config { + var kubeconfig, namespace, podName string + + // Checking environment variable first + envKubeconfig := os.Getenv("KUBECONFIG") + if envKubeconfig != "" { + kubeconfig = envKubeconfig + } else { + // Use default kubeconfig from home directory + kubeconfig = filepath.Join(homedir.HomeDir(), ".kube", "config") + } + + // Binding flags + cmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "k", kubeconfig, "Path to the kubeconfig file") + cmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", + "Namespace of the etcd pod (default is the current namespace from kubeconfig)") + cmd.PersistentFlags().StringVarP(&podName, "pod", "p", "", "Name of the etcd pod") + + // Parse flags + if err := cmd.ParseFlags(os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse flags: %v\n", err) + os.Exit(1) + } + + // If namespace is not specified, fetch it from kubeconfig context + if namespace == "" { + configLoader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + &clientcmd.ConfigOverrides{}) + ns, _, err := configLoader.Namespace() + if err != nil { + fmt.Printf("Error fetching namespace from kubeconfig: %s\n", err) + os.Exit(1) + } + namespace = ns + if namespace == "" { + namespace = "default" // Default to "default" if not specified + } + } + + return &Config{ + Kubeconfig: kubeconfig, + Namespace: namespace, + PodName: podName, + } +} + +// Config struct to hold configuration +type Config struct { + Kubeconfig string + Namespace string + PodName string +} + +func getTLSConfig(clientset *kubernetes.Clientset, pod *corev1.Pod, namespace string) (*tls.Config, error) { + for _, container := range pod.Spec.Containers { + if container.Name == "etcd" { + secretName, err := findSecretNameForTLS(pod, container) + if err != nil { + if err.Error() == "trusted CA file path not specified in container args" { + return nil, nil + } + return nil, err + } + + caCertPool, clientCert, err := extractTLSFiles(clientset, namespace, secretName) + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{*clientCert}, + RootCAs: caCertPool, + }, nil + } + } + return nil, fmt.Errorf("etcd container not found") +} + +func findSecretNameForTLS(pod *corev1.Pod, container corev1.Container) (string, error) { + caFilePath := "" + for _, arg := range append(container.Command, container.Args...) { + if strings.HasPrefix(arg, "--trusted-ca-file=") { + caFilePath = strings.TrimPrefix(arg, "--trusted-ca-file=") + break + } + } + + if caFilePath == "" { + return "", fmt.Errorf("trusted CA file path not specified in container args") + } + + for _, vm := range container.VolumeMounts { + if strings.HasPrefix(caFilePath, vm.MountPath) { + // We found the mount path, now find the volume + for _, vol := range pod.Spec.Volumes { + if vol.Name == vm.Name && vol.Secret != nil { + return vol.Secret.SecretName, nil + } + } + } + } + + return "", fmt.Errorf("secret for the trusted CA file not found") +} + +func extractTLSFiles(clientset *kubernetes.Clientset, namespace, secretName string) ( + *x509.CertPool, *tls.Certificate, error) { + secret, err := clientset.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{}) + if err != nil { + return nil, nil, err + } + + caPem, ok := secret.Data["ca.crt"] + if !ok { + return nil, nil, fmt.Errorf("CA certificate not found in secret") + } + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caPem) { + return nil, nil, fmt.Errorf("failed to parse CA certificate") + } + + certPem, ok := secret.Data["tls.crt"] + if !ok { + return nil, nil, fmt.Errorf("TLS certificate not found in secret") + } + keyPem, ok := secret.Data["tls.key"] + if !ok { + return nil, nil, fmt.Errorf("TLS key not found in secret") + } + + clientCert, err := tls.X509KeyPair(certPem, keyPem) + if err != nil { + return nil, nil, fmt.Errorf("failed to create X509 key pair: %s", err) + } + + return caCertPool, &clientCert, nil +} + +type silentWriter struct{} + +func (sw *silentWriter) Write(p []byte) (int, error) { + return len(p), nil +} diff --git a/cmd/main.go b/cmd/manager/main.go similarity index 100% rename from cmd/main.go rename to cmd/manager/main.go diff --git a/go.mod b/go.mod index ae9ce48c..d74f4195 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,12 @@ module github.com/aenix-io/etcd-operator go 1.22.4 require ( + github.com/dustin/go-humanize v1.0.1 github.com/go-logr/logr v1.4.2 github.com/google/uuid v1.6.0 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 go.etcd.io/etcd/client/v3 v3.5.14 @@ -40,17 +42,21 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.6 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 // indirect diff --git a/go.sum b/go.sum index a0c4a088..a13159d3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -6,11 +8,14 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= @@ -55,10 +60,15 @@ github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQN github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -80,6 +90,8 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvls github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 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= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -87,6 +99,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= @@ -108,6 +122,7 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -118,6 +133,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 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.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=