PT-2299 - collect openssl x509 certificate information for each secret

- Implemented the feature
- Added test case
This commit is contained in:
Sveta Smirnova
2024-05-10 00:05:47 +03:00
parent 782a3c0b1b
commit e1390c4a52
4 changed files with 275 additions and 6 deletions

7
go.mod
View File

@@ -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

6
go.sum
View File

@@ -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=

View File

@@ -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"

View File

@@ -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
*/