Skip to content

Commit 683c1e1

Browse files
authored
include kubectl describe output (#137)
* gather pg_settings Query pg_settings to capture current Postgres settings directly from the DB. * run kubectl describe commands on various resources * removing cluster-info cluster-info returns a lot of sensitive information and it out-of-scope for the support export. * getting CRDs Remove the `kubectl describe crds` and replace with getting the CRDs via the correct APIs. This brings CRDs into the support export output on the same level as pods, sts, etc. * fix unnecessary use of fmt.Sprintf * better gathering of PGAdmin resources Treats PGAdmin resources separately since they are independent of a PostgresCluster. Gathers all PGAdmin resources in a namespace. This is usually one, but it could be several. * fix describe for clusterrole and clusterrolebinding on openshift * refactor describe clusterrole and clusterrolebinding This makes it more clear that 'postgresoperator' is a special case for certain installers. * refactor to account for labels on some installers Some installers don't label the CRD with app.kubernetes.io/name=pgo. This refactor filters all the CRDs for a name containing "postgres-operator.crunchydata.com"
1 parent 97e24ea commit 683c1e1

File tree

3 files changed

+273
-1
lines changed

3 files changed

+273
-1
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2021 - 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package v1beta1
6+
7+
import (
8+
"k8s.io/apimachinery/pkg/api/meta"
9+
"k8s.io/cli-runtime/pkg/resource"
10+
"k8s.io/client-go/dynamic"
11+
)
12+
13+
func NewPgadminClient(rcg resource.RESTClientGetter) (
14+
*meta.RESTMapping, dynamic.NamespaceableResourceInterface, error,
15+
) {
16+
gvk := GroupVersion.WithKind("PGAdmin")
17+
18+
mapper, err := rcg.ToRESTMapper()
19+
if err != nil {
20+
return nil, nil, err
21+
}
22+
23+
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
24+
if err != nil {
25+
return nil, nil, err
26+
}
27+
28+
config, err := rcg.ToRESTConfig()
29+
if err != nil {
30+
return nil, nil, err
31+
}
32+
33+
client, err := dynamic.NewForConfig(config)
34+
if err != nil {
35+
return nil, nil, err
36+
}
37+
38+
return mapping, client.Resource(mapping.Resource), nil
39+
}

internal/cmd/export.go

Lines changed: 231 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import (
2727
networkingv1 "k8s.io/api/networking/v1"
2828
policyv1 "k8s.io/api/policy/v1"
2929
policyv1beta1 "k8s.io/api/policy/v1beta1"
30+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
31+
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
3032
apierrors "k8s.io/apimachinery/pkg/api/errors"
3133
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3234
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -341,6 +343,11 @@ Collecting PGO CLI logs...
341343
return err
342344
}
343345

346+
apiExtensionClientSet, err := apiextensionsclientset.NewForConfig(restConfig)
347+
if err != nil {
348+
return err
349+
}
350+
344351
discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig)
345352
if err != nil {
346353
return err
@@ -456,6 +463,12 @@ Collecting PGO CLI logs...
456463
writeInfo(cmd, fmt.Sprintf("Error gathering Namespaced API Resources: %s", err))
457464
}
458465

466+
// Gather CRDs
467+
err = gatherCrds(ctx, apiExtensionClientSet, clusterName, tw, cmd)
468+
if err != nil {
469+
writeInfo(cmd, fmt.Sprintf("Error gathering CRDs: %s", err))
470+
}
471+
459472
// Gather Events
460473
err = gatherEvents(ctx, clientset, namespace, clusterName, tw, cmd)
461474
if err != nil {
@@ -581,6 +594,60 @@ Collecting PGO CLI logs...
581594
writeInfo(cmd, fmt.Sprintf("There is no PGUpgrade object associated with cluster '%s'", clusterName))
582595
}
583596

597+
// Run kubectl describe and similar commands
598+
writeInfo(cmd, "Running kubectl describe nodes...")
599+
err = runKubectlCommand(tw, cmd, clusterName+"/describe/nodes", "describe", "nodes")
600+
if err != nil {
601+
writeInfo(cmd, fmt.Sprintf("Error running kubectl describe nodes: %s", err))
602+
}
603+
604+
writeInfo(cmd, "Running kubectl describe postgrescluster...")
605+
err = runKubectlCommand(tw, cmd, clusterName+"/describe/postgrescluster", "describe", "postgrescluster", clusterName, "-n", namespace)
606+
if err != nil {
607+
writeInfo(cmd, fmt.Sprintf("Error running kubectl describe postgrescluster: %s", err))
608+
}
609+
610+
// Resource name is generally 'postgres-operator' but in some environments
611+
// like Openshift it could be 'postgresoperator'
612+
writeInfo(cmd, "Running kubectl describe clusterrole...")
613+
err = runKubectlCommand(tw, cmd, clusterName+"/describe/clusterrole", "describe", "clusterrole", "postgres-operator")
614+
if err != nil {
615+
writeInfo(cmd, fmt.Sprintf("Error running kubectl describe clusterrole: %s", err))
616+
writeInfo(cmd, "Could not find clusterrole 'postgres-operator'. Looking for 'postgresoperator'...")
617+
618+
// Check for the alternative spelling with 'postgresoperator'
619+
err = runKubectlCommand(tw, cmd, clusterName+"/describe/clusterrole", "describe", "clusterrole", "postgresoperator")
620+
if err != nil {
621+
writeInfo(cmd, fmt.Sprintf("Error running kubectl describe clusterrole: %s", err))
622+
}
623+
}
624+
625+
// Resource name is generally 'postgres-operator' but in some environments
626+
// like Openshift it could be 'postgresoperator'
627+
writeInfo(cmd, "Running kubectl describe clusterrolebinding...")
628+
err = runKubectlCommand(tw, cmd, clusterName+"/describe/clusterrolebinding", "describe", "clusterrolebinding", "postgres-operator")
629+
if err != nil {
630+
writeInfo(cmd, fmt.Sprintf("Error running kubectl describe clusterrolebinding: %s", err))
631+
632+
// Check for the alternative spelling with 'postgresoperator'
633+
writeInfo(cmd, "Could not find clusterrolebinding 'postgres-operator'. Looking for 'postgresoperator'...")
634+
err = runKubectlCommand(tw, cmd, clusterName+"/describe/clusterrolebinding", "describe", "clusterrolebinding", "postgresoperator")
635+
if err != nil {
636+
writeInfo(cmd, fmt.Sprintf("Error running kubectl describe clusterrolebinding: %s", err))
637+
}
638+
}
639+
640+
writeInfo(cmd, "Running kubectl describe lease...")
641+
err = runKubectlCommand(tw, cmd, "operator/describe/lease", "describe", "lease", "-n", operatorNamespace)
642+
if err != nil {
643+
writeInfo(cmd, fmt.Sprintf("Error running kubectl describe lease: %s", err))
644+
}
645+
646+
err = gatherPgadminResources(config, clientset, ctx, namespace, tw, cmd)
647+
if err != nil {
648+
writeInfo(cmd, fmt.Sprintf("Error gathering PGAdmin Resources: %s", err))
649+
}
650+
584651
// Print cli output
585652
writeInfo(cmd, "Collecting PGO CLI logs...")
586653
path := clusterName + "/cli.log"
@@ -598,6 +665,74 @@ Collecting PGO CLI logs...
598665
return cmd
599666
}
600667

668+
func gatherPgadminResources(config *internal.Config,
669+
clientset *kubernetes.Clientset,
670+
ctx context.Context,
671+
namespace string,
672+
tw *tar.Writer, cmd *cobra.Command) error {
673+
674+
_, pgadminClient, err := v1beta1.NewPgadminClient(config)
675+
676+
if err != nil {
677+
return err
678+
}
679+
680+
pgadmins, err := pgadminClient.Namespace(namespace).List(ctx, metav1.ListOptions{})
681+
if err != nil {
682+
if apierrors.IsForbidden(err) {
683+
writeInfo(cmd, err.Error())
684+
return nil
685+
}
686+
return err
687+
}
688+
689+
if len(pgadmins.Items) == 0 {
690+
// If we didn't find any resources, skip
691+
writeInfo(cmd, "Resource PGAdmin not found, skipping")
692+
return nil
693+
}
694+
695+
// Create a buffer to generate string with the table formatted list
696+
var buf bytes.Buffer
697+
if err := printers.NewTablePrinter(printers.PrintOptions{}).
698+
PrintObj(pgadmins, &buf); err != nil {
699+
return err
700+
}
701+
702+
// Define the file name/path where the list file will be created and
703+
// write to the tar
704+
path := "pgadmin" + "/list"
705+
if err := writeTar(tw, buf.Bytes(), path, cmd); err != nil {
706+
return err
707+
}
708+
709+
for _, obj := range pgadmins.Items {
710+
b, err := yaml.Marshal(obj)
711+
if err != nil {
712+
return err
713+
}
714+
715+
path := "pgadmin" + "/" + obj.GetName() + ".yaml"
716+
if err := writeTar(tw, b, path, cmd); err != nil {
717+
return err
718+
}
719+
720+
writeInfo(cmd, "Collecting PGAdmin pod logs...")
721+
err = gatherPodLogs(ctx, clientset, namespace, fmt.Sprintf("%s=%s", util.LabelPgadmin, obj.GetName()), "pgadmin", tw, cmd)
722+
if err != nil {
723+
writeInfo(cmd, fmt.Sprintf("Error gathering PGAdmin pod logs: %s", err))
724+
}
725+
726+
writeInfo(cmd, "Running kubectl describe pgadmin")
727+
err = runKubectlCommand(tw, cmd, "pgadmin/describe/"+obj.GetName(), "describe", "pgadmin", obj.GetName(), "-n", namespace)
728+
if err != nil {
729+
writeInfo(cmd, fmt.Sprintf("Error running kubectl describe pgadmin: %s", err))
730+
}
731+
}
732+
733+
return nil
734+
}
735+
601736
func gatherPluginList(clusterName string, tw *tar.Writer, cmd *cobra.Command) error {
602737
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
603738
defer cancel() // Ensure the context is canceled to avoid leaks
@@ -640,6 +775,29 @@ There was an error running 'kubectl get pgupgrade'. Verify permissions and that
640775
return nil
641776
}
642777

778+
func runKubectlCommand(tw *tar.Writer, cmd *cobra.Command, path string, cmdArgs ...string) error {
779+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
780+
defer cancel() // Ensure the context is canceled to avoid leaks
781+
782+
ex := exec.CommandContext(ctx, "kubectl", cmdArgs...)
783+
msg, err := ex.Output()
784+
785+
if err != nil {
786+
msg = append(msg, err.Error()...)
787+
msg = append(msg, []byte(`
788+
There was an error running the command. Verify permissions and that the resource exists.`)...)
789+
790+
writeInfo(cmd, fmt.Sprintf("Error: '%s'", msg))
791+
return err
792+
}
793+
794+
if err := writeTar(tw, msg, path, cmd); err != nil {
795+
return err
796+
}
797+
798+
return nil
799+
}
800+
643801
// exportSizeReport defines the message displayed when a support export archive
644802
// is created. If the size of the archive file is greater than 25MiB, an alternate
645803
// message is displayed.
@@ -956,6 +1114,73 @@ func gatherNamespacedAPIResources(ctx context.Context,
9561114
return nil
9571115
}
9581116

1117+
// gatherCrds gathers all the CRDs with a name=pgo label
1118+
func gatherCrds(ctx context.Context,
1119+
clientset *apiextensionsclientset.Clientset,
1120+
clusterName string,
1121+
tw *tar.Writer,
1122+
cmd *cobra.Command,
1123+
) error {
1124+
writeInfo(cmd, "Collecting CRDs...")
1125+
1126+
crdList, err := clientset.ApiextensionsV1().CustomResourceDefinitions().List(ctx, metav1.ListOptions{})
1127+
1128+
if err != nil {
1129+
if apierrors.IsForbidden(err) {
1130+
writeInfo(cmd, err.Error())
1131+
return nil
1132+
}
1133+
return err
1134+
}
1135+
1136+
// Get only the CRDs matching our filter
1137+
nameFilter := "postgres-operator.crunchydata.com"
1138+
1139+
filteredCRDs := &apiextensionsv1.CustomResourceDefinitionList{
1140+
Items: []apiextensionsv1.CustomResourceDefinition{},
1141+
}
1142+
for _, crd := range crdList.Items {
1143+
if strings.Contains(crd.Name, nameFilter) {
1144+
filteredCRDs.Items = append(filteredCRDs.Items, crd)
1145+
}
1146+
}
1147+
1148+
if len(filteredCRDs.Items) == 0 {
1149+
// If we didn't find any resources, skip
1150+
writeInfo(cmd, "Resource CRDs not found, skipping")
1151+
return nil
1152+
}
1153+
1154+
// Create a buffer to generate string with the table formatted list
1155+
var buf bytes.Buffer
1156+
if err := printers.NewTablePrinter(printers.PrintOptions{}).
1157+
PrintObj(filteredCRDs, &buf); err != nil {
1158+
return err
1159+
}
1160+
1161+
// Define the file name/path where the list file will be created and
1162+
// write to the tar
1163+
path := clusterName + "/" + "crds" + "/list"
1164+
if err := writeTar(tw, buf.Bytes(), path, cmd); err != nil {
1165+
return err
1166+
}
1167+
1168+
for _, obj := range filteredCRDs.Items {
1169+
b, err := yaml.Marshal(obj)
1170+
if err != nil {
1171+
return err
1172+
}
1173+
1174+
path := clusterName + "/" + "crds" + "/" + obj.GetName() + ".yaml"
1175+
if err := writeTar(tw, b, path, cmd); err != nil {
1176+
return err
1177+
}
1178+
}
1179+
1180+
return nil
1181+
1182+
}
1183+
9591184
// gatherEvents gathers all events from a namespace, selects information (based on
9601185
// what kubectl outputs), formats the data then prints to the tar file
9611186
func gatherEvents(ctx context.Context,
@@ -1229,8 +1454,9 @@ func gatherPostgresLogsAndConfigs(ctx context.Context,
12291454
commands := []Command{
12301455
{path: "pg_controldata", description: "pg_controldata"},
12311456
{path: "df -h /pgdata", description: "disk free"},
1232-
{path: "du -h /pgdata", description: "disk usage"},
1457+
{path: "du -h /pgdata | column -t -o \" \"", description: "disk usage"},
12331458
{path: "ls /pgdata/*/archive_status/*.ready | wc -l", description: "Archive Ready File Count"},
1459+
{path: "psql -P format=wrapped -P columns=180 -c \"select name,setting,source,sourcefile,sourceline FROM pg_settings order by 1\"", description: "PG Settings"},
12341460
}
12351461

12361462
var buf bytes.Buffer
@@ -1670,6 +1896,10 @@ func gatherPodLogs(ctx context.Context,
16701896
}
16711897

16721898
for _, pod := range pods.Items {
1899+
err = runKubectlCommand(tw, cmd, rootDir+"/describe/"+"pods/"+pod.GetName(), "describe", "pods", pod.GetName(), "-n", namespace)
1900+
if err != nil {
1901+
writeInfo(cmd, fmt.Sprintf("Error running kubectl describe pods: %s", err))
1902+
}
16731903
containers := pod.Spec.Containers
16741904
containers = append(containers, pod.Spec.InitContainers...)
16751905
for _, container := range containers {

internal/util/naming.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const (
1313
// LabelCluster is used to label PostgresCluster objects.
1414
LabelCluster = labelPrefix + "cluster"
1515

16+
// LabelPgadmin is used to label PGAdmin objects.
17+
LabelPgadmin = labelPrefix + "pgadmin"
18+
1619
// LabelData is used to identify Pods and Volumes store Postgres data.
1720
LabelData = labelPrefix + "data"
1821

0 commit comments

Comments
 (0)