diff --git a/go.mod b/go.mod index 2affe849..b583f355 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/percona/percona-toolkit -go 1.21 +go 1.22.0 + toolchain go1.22.2 require ( @@ -25,6 +26,7 @@ require ( github.com/rs/zerolog v1.32.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/sirupsen/logrus v1.9.3 + github.com/smallstep/certinfo v1.12.2 github.com/stretchr/testify v1.9.0 github.com/xlab/treeprint v1.2.0 go.mongodb.org/mongo-driver v1.15.0 @@ -43,6 +45,7 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/certificate-transparency-go v1.1.7 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.3 // indirect @@ -59,7 +62,7 @@ require ( github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/sync v0.1.0 // indirect + golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index ee0ea253..f3f7a306 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw= +github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -90,6 +92,8 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smallstep/certinfo v1.12.2 h1:cuyiPNo86yekliQduAGP/5BDR4JA/8S1UCtDtpKl8fQ= +github.com/smallstep/certinfo v1.12.2/go.mod h1:J8E+AF8ZPEaCqG+eM3gAKGGfo7Zb9DSghjf9VG96x/0= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -149,6 +153,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/src/go/pt-k8s-debug-collector/dumper/dumper.go b/src/go/pt-k8s-debug-collector/dumper/dumper.go index 73b784d8..5c803861 100644 --- a/src/go/pt-k8s-debug-collector/dumper/dumper.go +++ b/src/go/pt-k8s-debug-collector/dumper/dumper.go @@ -6,6 +6,7 @@ import ( "compress/gzip" "encoding/base64" "encoding/json" + "html/template" "log" "os" "os/exec" @@ -18,6 +19,13 @@ import ( corev1 "k8s.io/api/core/v1" ) +// sslSecret struct for dumping certificates +type sslSecret struct { + secret string + resource string + dataNames []string +} + // Dumper struct is for dumping cluster type Dumper struct { cmd string @@ -31,6 +39,7 @@ type Dumper struct { mode int64 crType string forwardport string + sslSecrets []sslSecret } var resourcesRe = regexp.MustCompile(`(\w+)\.(\w+).percona\.com`) @@ -113,8 +122,36 @@ func New(location, namespace, resource string, kubeconfig string, forwardport st "perconaservermongodbs", ) } + sslSecrets := make([]sslSecret, 0) filePaths := make([]string, 0) - if resourceType(resource) == "pxc" { + switch resource { + case "pg": + sslSecrets = append(sslSecrets, + sslSecret{ + secret: "{{ .Name }}-ssl-ca", + resource: "perconapgclusters", + dataNames: []string{"ca.crt"}, + }, + sslSecret{ + secret: "pgo.tls", + resource: "perconapgclusters", + dataNames: []string{"tls.crt"}, + }, + ) + case "pgv2": + sslSecrets = append(sslSecrets, + sslSecret{ + secret: "{{ .Name }}-cluster-cert", + resource: "pg", + dataNames: []string{"ca.crt", "tls.crt"}, + }, + sslSecret{ + secret: "pgo-root-cacert", + resource: "pg", + dataNames: []string{"root.crt"}, + }, + ) + case "pxc": filePaths = append(filePaths, "var/lib/mysql/mysqld-error.log", "var/lib/mysql/innobackup.backup.log", @@ -126,8 +163,32 @@ func New(location, namespace, resource string, kubeconfig string, forwardport st "var/lib/mysql/auto.cnf", ) d.fileContainer = "logs" + sslSecrets = append(sslSecrets, + sslSecret{ + secret: "{{ .Name }}-ssl", + resource: "pxc", + dataNames: []string{"ca.crt", "tls.crt"}, + }, + ) + case "ps": + sslSecrets = append(sslSecrets, + sslSecret{ + secret: "{{ .Name }}-ssl", + resource: "ps", + dataNames: []string{"ca.crt", "tls.crt"}, + }, + ) + case "psmdb": + sslSecrets = append(sslSecrets, + sslSecret{ + secret: "{{ .Name }}-ssl", + resource: "psmdb", + dataNames: []string{"ca.crt", "tls.crt"}, + }, + ) } d.resources = resources + d.sslSecrets = sslSecrets d.crType = resource d.filePaths = filePaths return d @@ -256,7 +317,7 @@ func (d *Dumper) DumpCluster() error { crName = pod.Labels["app.kubernetes.io/instance"] } // Get summary - output, err = d.getPodSummary(resourceType(d.crType), pod.Name, crName, ns.Name, tw) + output, err = d.getPodSummary(resourceType(d.crType), pod.Name, crName, ns.Name) if err != nil { d.logError(err.Error(), d.crType, pod.Name) err = addToArchive(location, d.mode, []byte(err.Error()), tw) @@ -289,6 +350,13 @@ func (d *Dumper) DumpCluster() error { log.Printf("Error: get %s resource: %v", resource, err) } } + + for _, s := range d.sslSecrets { + err = d.getSSLCertificates(s, ns.Name, tw) + if err != nil { + log.Printf("Error: get SSL certificates in %s: %v", s.secret, err) + } + } } err = d.getResource("nodes", "", false, tw) @@ -390,7 +458,7 @@ func (d *Dumper) getIndividualFiles(namespace string, podName, path, location st return addToArchive(location+"/"+path, d.mode, output, tw) } -func (d *Dumper) getPodSummary(resource, podName, crName string, namespace string, tw *tar.Writer) ([]byte, error) { +func (d *Dumper) getPodSummary(resource, podName, crName string, namespace string) ([]byte, error) { var ( summCmdName string ports string @@ -476,7 +544,7 @@ func (d *Dumper) getPodSummary(resource, podName, crName string, namespace strin cmd.Stderr = &errb err := cmd.Run() if err != nil { - return nil, errors.Errorf("error: %v\nstderr: %sstdout: %s", err, errb.String(), outb.String()) + return nil, errors.Wrapf(err, "stderr: %s\nstdout: %s", errb.String(), outb.String()) } return outb.Bytes(), nil } @@ -508,6 +576,81 @@ func (d *Dumper) getDataFromSecret(secretName, dataName string, namespace string return string(pass), nil } +func (d *Dumper) getSSLDataFromSecret(secretName, dataName string, namespace string) (string, error) { + data, err := d.runCmd("get", "secrets/"+secretName, "-o", "go-template='{{ index .data \""+dataName+"\" | base64decode }}'", "-n", namespace) + if err != nil { + return "", errors.Wrap(err, "run get secret cmd") + } + + return string(data), nil +} + +func (d *Dumper) getSSLCertificates(secret sslSecret, namespace string, tw *tar.Writer) error { + cr := struct { + Items []struct { + Metadata struct { + Name string `json:"name"` + } `json:"metadata"` + } `json:"items"` + }{} + + output, err := d.runCmd("get", secret.resource, "-o", "json", "-n", namespace) + if err != nil { + return errors.Wrap(err, "get "+secret.resource) + } + err = json.Unmarshal(output, &cr) + if err != nil { + return errors.Wrapf(err, "unmarshal %s cr", secret.resource) + } + + if len(cr.Items) > 1 { + return errors.Wrap(err, "Unexpected structure for resource "+secret.resource) + } + + for _, item := range cr.Items { + location := d.location + + if len(namespace) > 0 { + location = filepath.Join(d.location, namespace) + } + + var nb bytes.Buffer + t := template.Must(template.New("secret").Parse(secret.secret)) + t.Execute(&nb, item.Metadata) + + name := nb.String() + location = filepath.Join(location, name) + + result := make([]byte, 0) + for _, dn := range secret.dataNames { + result = append(result, dn+"\n"...) + data, err := d.getSSLDataFromSecret(name, dn, namespace) + if err != nil { + errors.Wrapf(err, "Error getting certificate %s from secret %s", dn, name) + } + + var outb, errb bytes.Buffer + cmd := exec.Command("sh", "-c", "echo "+data+" | openssl x509 -noout -text") + cmd.Stdout = &outb + cmd.Stderr = &errb + err = cmd.Run() + if err != nil { + errors.Wrapf(err, "stderr: %s\nstdout: %s", errb.String(), outb.String()) + } + result = append(result, outb.Bytes()...) + } + + err = addToArchive(location, d.mode, result, tw) + + if err != nil { + return errors.Wrapf(err, "Cannot add certificates in the secret %s into resulting archive", name) + } + + } + + return nil +} + func resourceType(s string) string { if s == "pxc" || strings.HasPrefix(s, "pxc/") { return "pxc" diff --git a/src/go/pt-k8s-debug-collector/main_test.go b/src/go/pt-k8s-debug-collector/main_test.go index 216f05c9..849daf08 100644 --- a/src/go/pt-k8s-debug-collector/main_test.go +++ b/src/go/pt-k8s-debug-collector/main_test.go @@ -214,6 +214,123 @@ func TestResourceOption(t *testing.T) { } } +/* +PT-2299 - collect openssl x509 certificate information for each secret +*/ +func TestSSLResourceOption(t *testing.T) { + tests := []struct { + name string + resource string + cmds [][]string // slice of commands to execute + want []string // slice of expected results + kubeconfig string + }{ + { + name: "auto pxc", + resource: "auto", + cmds: [][]string{ + {"tar", "--to-command", "grep -m 1 -o ca.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl"}, + {"tar", "--to-command", "grep -m 1 -o Certificate", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl"}, + {"tar", "--to-command", "grep -m 1 -o tls.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl"}, + }, + want: []string{ + "ca.crt", + "Certificate", + "tls.crt", + }, + kubeconfig: os.Getenv("KUBECONFIG_PXC"), + }, + { + name: "auto ps", + resource: "auto", + cmds: [][]string{ + {"tar", "--to-command", "grep -m 1 -o ca.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl"}, + {"tar", "--to-command", "grep -m 1 -o Certificate", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl"}, + {"tar", "--to-command", "grep -m 1 -o tls.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl"}, + }, + want: []string{ + "ca.crt", + "Certificate", + "tls.crt", + }, + kubeconfig: os.Getenv("KUBECONFIG_PS"), + }, + { + name: "auto psmdb", + resource: "auto", + cmds: [][]string{ + {"tar", "--to-command", "grep -m 1 -o ca.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl"}, + {"tar", "--to-command", "grep -m 1 -o Certificate", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl"}, + {"tar", "--to-command", "grep -m 1 -o tls.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl"}, + }, + want: []string{ + "ca.crt", + "Certificate", + "tls.crt", + }, + kubeconfig: os.Getenv("KUBECONFIG_PSMDB"), + }, + { + name: "auto pg", + resource: "auto", + cmds: [][]string{ + {"tar", "--to-command", "grep -m 1 -o ca.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl-ca"}, + {"tar", "--to-command", "grep -m 1 -o Certificate", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*ssl-ca"}, + {"tar", "--to-command", "grep -m 1 -o tls.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/pgo.tls"}, + {"tar", "--to-command", "grep -m 1 -o Certificate", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/pgo.tls"}, + }, + want: []string{ + "ca.crt", + "Certificate", + "tls.crt", + "Certificate", + }, + kubeconfig: os.Getenv("KUBECONFIG_PG"), + }, + { + name: "auto pgv2", + resource: "auto", + cmds: [][]string{ + {"tar", "--to-command", "grep -m 1 -o ca.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*cluster-cert"}, + {"tar", "--to-command", "grep -m 1 -o Certificate", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*cluster-cert"}, + {"tar", "--to-command", "grep -m 1 -o tls.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/*cluster-cert"}, + {"tar", "--to-command", "grep -m 1 -o root.crt", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/pgo-root-cacert"}, + {"tar", "--to-command", "grep -m 1 -o Certificate", "-xzf", "cluster-dump.tar.gz", "--wildcards", "cluster-dump/*/pgo-root-cacert"}, + }, + want: []string{ + "ca.crt", + "Certificate", + "tls.crt", + "root.crt", + "Certificate", + }, + kubeconfig: os.Getenv("KUBECONFIG_PG2"), + }, + } + + for _, test := range tests { + cmd := exec.Command("../../../bin/pt-k8s-debug-collector", "--kubeconfig", test.kubeconfig, "--forwardport", os.Getenv("FORWARDPORT"), "--resource", test.resource) + if err := cmd.Run(); err != nil { + t.Errorf("error executing pt-k8s-debug-collector: %s", err.Error()) + } + defer func() { + cmd = exec.Command("rm", "-f", "cluster-dump.tar.gz") + if err := cmd.Run(); err != nil { + t.Errorf("error cleaning up test data: %s", err.Error()) + } + }() + for ind, testcmd := range test.cmds { + out, err := exec.Command(testcmd[0], testcmd[1:]...).Output() + if err != nil { + t.Errorf("test %s, error running command %s:\n%s\n\nCommand output:\n%s", test.name, testcmd, err.Error(), out) + } + if strings.TrimRight(bytes.NewBuffer(out).String(), "\n") != test.want[ind] { + t.Errorf("test %s, output is not as expected\nOutput: %s\nWanted: %s", test.name, out, test.want) + } + } + } +} + /* Option --version */