diff --git a/.github/workflows/nilaway-lint.yaml b/.github/workflows/nilaway-lint.yaml new file mode 100644 index 00000000..41b171f0 --- /dev/null +++ b/.github/workflows/nilaway-lint.yaml @@ -0,0 +1,14 @@ +name: Nilaway lint + +on: + pull_request: + +jobs: + nilaway-lint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4.1.3 + - uses: actions/setup-go@v5.0.0 + with: + go-version: 1.22.2 + - run: make nilaway-lint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af9d055e..a612aa6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,9 +24,9 @@ repos: entry: sh -c "make lint-fix" language: system require_serial: true - - id: make-nilaway-lint - name: make-nilaway-lint - entry: sh -c "make nilaway-lint" + - id: make-mod-tidy + name: make-mod-tidy + entry: sh -c "make mod-tidy" language: system require_serial: true - id: make-helm-lint diff --git a/Makefile b/Makefile index 2ff7d420..a8497afb 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,10 @@ fmt: ## Run go fmt against code. vet: ## Run go vet against code. go vet ./... +.PHONY: mod-tidy +mod-tidy: ## Run go mod tidy against code. + go mod tidy + .PHONY: test test: manifests generate fmt vet envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION_TRIMMED_V) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out diff --git a/cmd/plugin/main.go b/cmd/plugin/main.go new file mode 100644 index 00000000..be85fb9a --- /dev/null +++ b/cmd/plugin/main.go @@ -0,0 +1,229 @@ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + 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 kubeconfig *string + if home := homedir.HomeDir(); home != "" { + kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") + } else { + kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") + } + + namespace := flag.String("namespace", "default", "namespace of the etcd pod") + podName := flag.String("pod", "", "name of the etcd pod") + flag.Parse() + + if *podName == "" { + fmt.Println("You must specify the pod name") + return + } + + config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) + if err != nil { + fmt.Printf("Error building kubeconfig: %s\n", err) + return + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + fmt.Printf("Error creating Kubernetes client: %s\n", err) + return + } + + pod, err := clientset.CoreV1().Pods(*namespace).Get(context.Background(), *podName, metav1.GetOptions{}) + if err != nil { + fmt.Printf("Failed to get pod: %s\n", err) + return + } + + path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", *namespace, *podName) + transport, upgrader, err := spdy.RoundTripperFor(config) + if err != nil { + fmt.Printf("Failed to create round tripper: %s\n", err) + return + } + + hostURL, err := url.Parse(config.Host) + if err != nil { + fmt.Printf("Failed to parse host URL: %s\n", err) + return + } + + fullPath := *hostURL + fullPath.Path = path + + transport, upgrader, err = spdy.RoundTripperFor(config) + if err != nil { + fmt.Printf("Failed to create round tripper: %s\n", err) + return + } + + stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1) + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", &fullPath) + + portForwarder, err := portforward.New(dialer, []string{"0:2379"}, stopChan, readyChan, os.Stdout, os.Stderr) + if err != nil { + fmt.Printf("Failed to create port forwarder: %s\n", err) + return + } + + fmt.Println("Starting port forwarding...") + go func() { + if err := portForwarder.ForwardPorts(); err != nil { + fmt.Printf("Failed to start port forwarding: %s\n", err) + } + }() + + <-readyChan // Wait for port forwarding to be ready + fmt.Println("Port forwarding ready") + + // Поиск локального порта, использованного для форвардинга + forwardedPorts, err := portForwarder.GetPorts() + if err != nil { + fmt.Printf("Failed to get forwarded ports: %s\n", err) + return + } + + localPort := forwardedPorts[0].Local + + // Check for the etcd container and extract TLS configuration + tlsConfig, err := getTLSConfig(clientset, pod, *namespace) + if err != nil { + fmt.Printf("Failed to get TLS config: %s\n", err) + return + } + + // Connecting to the etcd server with TLS config + cli, err := clientv3.New(clientv3.Config{ + Endpoints: []string{fmt.Sprintf("localhost:%d", localPort)}, + DialTimeout: 5 * time.Second, + TLS: tlsConfig, + }) + if err != nil { + fmt.Printf("Failed to connect to etcd server: %s\n", err) + return + } + defer cli.Close() + + fmt.Println("Successfully connected to etcd with TLS!") + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + members, err := cli.MemberList(ctx) + if err != nil { + fmt.Printf("Failed to list etcd members: %s\n", err) + return + } + + for _, member := range members.Members { + fmt.Printf("ID: %d, Name: %s, PeerURLs: %v, ClientURLs: %v\n", + member.ID, member.Name, member.PeerURLs, member.ClientURLs) + } + + // Stop port forwarding + close(stopChan) + fmt.Println("Stopped port forwarding.") +} + +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 { + 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 +} diff --git a/go.sum b/go.sum index 923f64f0..31c50a34 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -203,8 +201,6 @@ k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= -k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= -k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=