Skip to content

Commit

Permalink
Add show user command
Browse files Browse the repository at this point in the history
Adds a command to show the contents of the pguser Secrets.

Issue: PGO-470
  • Loading branch information
tjmoore4 authored and benjaminjb committed Nov 13, 2023
1 parent 8ea30a4 commit d1535cd
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/content/reference/pgo_show.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,5 @@ HA
* [pgo](/reference/) - pgo is a kubectl plugin for PGO, the open source Postgres Operator
* [pgo show backup](/reference/pgo_show_backup/) - Show backup information for a PostgresCluster
* [pgo show ha](/reference/pgo_show_ha/) - Show 'patronictl list' for a PostgresCluster.
* [pgo show user](/reference/pgo_show_user/) - Show pguser Secret details for a PostgresCluster.

76 changes: 76 additions & 0 deletions docs/content/reference/pgo_show_user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: pgo show user
---
## pgo show user

Show pguser Secret details for a PostgresCluster.

### Synopsis

Show pguser Secret details for a PostgresCluster.

#### RBAC Requirements
Resources Verbs
--------- -----
secrets [list]

### Usage

```
pgo show user CLUSTER_NAME [flags]
```

### Examples

```
# Show non-sensitive contents of 'pguser' Secret
pgo show user hippo
# Show contents of 'pguser' Secret, including sensitive fields
pgo show user hippo --show-sensitive-fields
```
### Example output
```
pgo show user hippo
SECRET: hippo-pguser-hippo
DBNAME: hippo
HOST: hippo-primary.postgres-operator.svc
PORT: 5432
USER: hippo
```

### Options

```
-h, --help help for user
-f, --show-sensitive-fields show sensitive user fields
```

### Options inherited from parent commands

```
--as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace.
--as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
--as-uid string UID to impersonate for the operation.
--cache-dir string Default cache directory (default "$HOME/.kube/cache")
--certificate-authority string Path to a cert file for the certificate authority
--client-certificate string Path to a client certificate file for TLS
--client-key string Path to a client key file for TLS
--cluster string The name of the kubeconfig cluster to use
--context string The name of the kubeconfig context to use
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--kubeconfig string Path to the kubeconfig file to use for CLI requests.
-n, --namespace string If present, the namespace scope for this CLI request
--request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
-s, --server string The address and port of the Kubernetes API server
--tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used
--token string Bearer token for authentication to the API server
--user string The name of the kubeconfig user to use
```

### SEE ALSO

* [pgo show](/reference/pgo_show/) - Show PostgresCluster details

154 changes: 152 additions & 2 deletions internal/cmd/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ package cmd

import (
"context"
"encoding/base64"
"fmt"
"io"
"os"
"sort"
"strings"

"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"sigs.k8s.io/yaml"

"github.com/crunchydata/postgres-operator-client/internal"
"github.com/crunchydata/postgres-operator-client/internal/util"
Expand Down Expand Up @@ -77,6 +82,7 @@ HA
cmdShow.AddCommand(
newShowBackupCommand(config),
newShowHACommand(config),
newShowUserCommand(config),
)

// Limit the number of args, that is, only one cluster name
Expand Down Expand Up @@ -278,6 +284,150 @@ func showHA(
return Executor(exec).patronictl("list", output)
}

// newShowUserCommand returns the decoded contents of the cluster's user Secrets.
func newShowUserCommand(config *internal.Config) *cobra.Command {

cmdShowUser := &cobra.Command{
Use: "user CLUSTER_NAME",
Short: "Show pguser Secret details for a PostgresCluster.",
Long: `Show pguser Secret details for a PostgresCluster.
#### RBAC Requirements
Resources Verbs
--------- -----
secrets [list]
### Usage`}

cmdShowUser.Example = internal.FormatExample(`# Show non-sensitive contents of 'pguser' Secret
pgo show user hippo
# Show contents of 'pguser' Secret, including sensitive fields
pgo show user hippo --show-sensitive-fields
### Example output
pgo show user hippo
SECRET: hippo-pguser-hippo
DBNAME: hippo
HOST: hippo-primary.postgres-operator.svc
PORT: 5432
USER: hippo
`)

var fields bool
cmdShowUser.Flags().BoolVarP(&fields, "show-sensitive-fields", "f", false, "show sensitive user fields")

// Limit the number of args, that is, only one cluster name
cmdShowUser.Args = cobra.ExactArgs(1)

// Define the 'show backup' command
cmdShowUser.RunE = func(cmd *cobra.Command, args []string) error {

stdout, err := showUser(config, args, fields)
if err != nil {
return err
}

cmd.Print(stdout)

return nil
}

return cmdShowUser
}

// showUser returns a string with the decoded contents of the cluster's user Secrets.
func showUser(config *internal.Config, args []string, showSensitive bool) (string, error) {

// break out keys based on whether sensitive information is included
var fields = []string{"dbname", "host", "pgbouncer-host", "pgbouncer-port", "port", "user"}
var sensitive = []string{"jdbc-uri", "password", "pgbouncer-jdbc-uri", "pgbouncer-uri", "uri", "verifier"}

if showSensitive {
fields = append(fields, sensitive...)

fmt.Print("WARNING: This command will show sensitive password information." +
"\nAre you sure you want to continue? (yes/no): ")

var confirmed *bool
for i := 0; confirmed == nil && i < 10; i++ {
// retry 10 times or until a confirmation is given or denied,
// whichever comes first
confirmed = confirm(os.Stdin, os.Stdout)
}

if confirmed == nil || !*confirmed {
return "", nil
}
}

// configure client
ctx := context.Background()
rest, err := config.ToRESTConfig()
if err != nil {
return "", err
}
client, err := v1.NewForConfig(rest)
if err != nil {
return "", err
}

// Get the namespace. This will either be from the Kubernetes configuration
// or from the --namespace (-n) flag.
configNamespace, err := config.Namespace()
if err != nil {
return "", err
}

list, err := client.Secrets(configNamespace).List(ctx, metav1.ListOptions{
LabelSelector: util.PostgresUserSecretLabels(args[0]),
})
if err != nil {
return "", err
}

return userData(fields, list)
}

// userData returns the requested user data from the provided Secret List.
// If the Secret List is empty, return a message stating that.
func userData(fields []string, list *corev1.SecretList) (string, error) {

var output string

if len(list.Items) == 0 {
output += fmt.Sprintln("No user Secrets found.")
}

for _, secret := range list.Items {
output += fmt.Sprintf("SECRET: %s\n", secret.Name)

// sort keys
keys := make([]string, 0, len(secret.Data))
for k := range secret.Data {
keys = append(keys, k)
}
sort.Strings(keys)

// decode and print keys and values from Secret
for _, k := range keys {
b, err := yaml.Marshal(secret.Data[k])
if err != nil {
return output, err
}
d := make([]byte, base64.StdEncoding.EncodedLen(len(b)))
_, err = base64.StdEncoding.Decode(d, b)
if err != nil {
return output, err
}
if containsString(fields, k) {
output += fmt.Sprintf(" %s: %s\n", strings.ToUpper(k), string(d))
}
}
}
return output, nil
}

// getPrimaryExec returns a executor function for the primary Pod to allow for
// commands to be run against it.
func getPrimaryExec(config *internal.Config, args []string) (
Expand All @@ -291,7 +441,7 @@ func getPrimaryExec(config *internal.Config, args []string) (
if err != nil {
return nil, err
}
client, err := corev1.NewForConfig(rest)
client, err := v1.NewForConfig(rest)
if err != nil {
return nil, err
}
Expand Down
9 changes: 9 additions & 0 deletions internal/util/naming.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ const (
// RolePatroniLeader is the LabelRole that Patroni sets on the Pod that is
// currently the leader.
RolePatroniLeader = "master"

// RolePostgresUser is the LabelRole applied to PostgreSQL user secrets.
RolePostgresUser = "pguser"
)

const (
Expand All @@ -62,3 +65,9 @@ func PrimaryInstanceLabels(clusterName string) string {
LabelData + "=" + DataPostgres + "," +
LabelRole + "=" + RolePatroniLeader
}

// PostgresUserSecretLabels provides labels for the Postgres user Secret
func PostgresUserSecretLabels(clusterName string) string {
return LabelCluster + "=" + clusterName + "," +
LabelRole + "=" + RolePostgresUser
}
43 changes: 43 additions & 0 deletions testing/kuttl/e2e/show/08--show-user.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: |
PRIMARY=$(
kubectl get pod --namespace "${NAMESPACE}" \
--output name --selector '
postgres-operator.crunchydata.com/cluster=show-cluster,
postgres-operator.crunchydata.com/role=master'
)
CLI_USER=$(
kubectl-pgo --namespace "${NAMESPACE}" show user show-cluster
)
status=$?
if [ $status -ne 0 ]; then
echo "pgo command unsuccessful"
exit 1
fi
# expected output
SHOW_USER_OUTPUT="SECRET: show-cluster-pguser-show-cluster
DBNAME: show-cluster
HOST: show-cluster-primary.${NAMESPACE}.svc
PORT: 5432
USER: show-cluster"
# check command output is not empty and equals the expected output
if [[ -z $CLI_USER && "$CLI_USER" != "$SHOW_USER_OUTPUT" ]]; then
exit 1
fi
CLI_USER_SENSITIVE=$(
echo yes | kubectl-pgo --namespace "${NAMESPACE}" show user show-cluster --show-sensitive-fields
)
# check command output is not empty and contains the password field
if [[ -n $CLI_USER_SENSITIVE && "$CLI_USER_SENSITIVE" == *"PASSWORD:"* ]]; then
exit 0
fi
exit 1

0 comments on commit d1535cd

Please sign in to comment.