Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2720,6 +2720,10 @@ spec:
description: MajorVersion represents the major version of the running
pgAdmin.
type: integer
minorVersion:
description: MinorVersion represents the minor version of the running
pgAdmin.
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
on which the status was based.
Expand Down
64 changes: 48 additions & 16 deletions internal/controller/standalone_pgadmin/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,40 +80,55 @@ func (r *PGAdminReconciler) reconcilePGAdminUsers(ctx context.Context, pgadmin *
return nil
}

// If the pgAdmin version is not in the status or the image SHA has changed, get
// the pgAdmin version and store it in the status.
var pgadminVersion int
if pgadmin.Status.MajorVersion == 0 || pgadmin.Status.ImageSHA != pgAdminImageSha {
pgadminVersion, err = r.reconcilePGAdminMajorVersion(ctx, podExecutor)
// If the pgAdmin major or minor version is not in the status or the image
// SHA has changed, get the pgAdmin version and store it in the status.
var pgadminMajorVersion int
if pgadmin.Status.MajorVersion == 0 || pgadmin.Status.MinorVersion == "" ||
pgadmin.Status.ImageSHA != pgAdminImageSha {

pgadminMinorVersion, err := r.reconcilePGAdminVersion(ctx, podExecutor)
if err != nil {
return err
}
pgadmin.Status.MajorVersion = pgadminVersion

// ensure minor version is valid before storing in status
parsedMinorVersion, err := strconv.ParseFloat(pgadminMinorVersion, 64)
if err != nil {
return err
}

// Note: "When converting a floating-point number to an integer, the
// fraction is discarded (truncation towards zero)."
// - https://go.dev/ref/spec#Conversions
pgadminMajorVersion = int(parsedMinorVersion)

pgadmin.Status.MinorVersion = pgadminMinorVersion
pgadmin.Status.MajorVersion = pgadminMajorVersion
pgadmin.Status.ImageSHA = pgAdminImageSha
} else {
pgadminVersion = pgadmin.Status.MajorVersion
pgadminMajorVersion = pgadmin.Status.MajorVersion
}

// If the pgAdmin version is not v8 or higher, return early as user management is
// only supported for pgAdmin v8 and higher.
if pgadminVersion < 8 {
if pgadminMajorVersion < 8 {
// If pgAdmin version is less than v8 and user management is being attempted,
// log a message clarifying that it is only supported for pgAdmin v8 and higher.
if len(pgadmin.Spec.Users) > 0 {
log.Info("User management is only supported for pgAdmin v8 and higher.",
"pgadminVersion", pgadminVersion)
"pgadminVersion", pgadminMajorVersion)
}
return err
}

return r.writePGAdminUsers(ctx, pgadmin, podExecutor)
}

// reconcilePGAdminMajorVersion execs into the pgAdmin pod and retrieves the pgAdmin major version
func (r *PGAdminReconciler) reconcilePGAdminMajorVersion(ctx context.Context, exec Executor) (int, error) {
// reconcilePGAdminVersion execs into the pgAdmin pod and retrieves the pgAdmin minor version
func (r *PGAdminReconciler) reconcilePGAdminVersion(ctx context.Context, exec Executor) (string, error) {
script := fmt.Sprintf(`
PGADMIN_DIR=%s
cd $PGADMIN_DIR && python3 -c "import config; print(config.APP_RELEASE)"
cd $PGADMIN_DIR && python3 -c "import config; print(config.APP_VERSION)"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 It looks like the result of this is only used when reconciling users. Can we move this into the commands there?

📝 It looks like pgAdmin 8.4 and newer have a version module with just these constants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into whether moving the commands makes sense. My guess is that would probably be fine as a refactor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. The previous functions been removed and one test added to account for the scenario that wasn't being tested.

`, pgAdminDir)

var stdin, stdout, stderr bytes.Buffer
Expand All @@ -122,10 +137,10 @@ cd $PGADMIN_DIR && python3 -c "import config; print(config.APP_RELEASE)"
[]string{"bash", "-ceu", "--", script}...)

if err != nil {
return 0, err
return "", err
}

return strconv.Atoi(strings.TrimSpace(stdout.String()))
return strings.TrimSpace(stdout.String()), nil
}

// writePGAdminUsers takes the users in the pgAdmin spec and writes (adds or updates) their data
Expand Down Expand Up @@ -171,10 +186,23 @@ cd $PGADMIN_DIR
for _, user := range existingUsersArr {
existingUsersMap[user.Username] = user
}

var olderThan9_3 bool
versionFloat, err := strconv.ParseFloat(pgadmin.Status.MinorVersion, 32)
Copy link
Contributor

@benjaminjb benjaminjb Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why bitsize 32 here but 64 when we retrieve the number to put into the status?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short version: That can probably be a 64, I'll make that change.

Longer version: I originally used 32 because I was intending to store the value directly into the status as a float. However, that's counter-indicated due to issues in float implementation and a string value is recommended instead. When the conversion happens, that function allows for 32 to be set so that the result will be convertible to float32 without changing its value despite returning a type float64 and, at the time, I wanted to use float32 because float64 was overkill for a simple version value.

if err != nil {
return err
}
if versionFloat < 9.3 {
olderThan9_3 = true
}

intentUsers := []pgAdminUserForJson{}
for _, user := range pgadmin.Spec.Users {
var stdin, stdout, stderr bytes.Buffer
typeFlag := "--nonadmin"
typeFlag := "--role User"
if olderThan9_3 {
typeFlag = "--nonadmin"
}
isAdmin := false
if user.Role == "Administrator" {
typeFlag = "--admin"
Expand Down Expand Up @@ -230,6 +258,8 @@ cd $PGADMIN_DIR
log.Error(err, "PodExec failed: ")
intentUsers = append(intentUsers, existingUser)
continue
} else if strings.Contains(strings.TrimSpace(stderr.String()), "UserWarning: pkg_resources is deprecated as an API") {
log.Info(stderr.String())
} else if strings.TrimSpace(stderr.String()) != "" {
log.Error(errors.New(stderr.String()), fmt.Sprintf("pgAdmin setup.py error for %s: ",
intentUser.Username))
Expand Down Expand Up @@ -264,7 +294,9 @@ cd $PGADMIN_DIR
log.Error(err, "PodExec failed: ")
continue
}
if strings.TrimSpace(stderr.String()) != "" {
if strings.Contains(strings.TrimSpace(stderr.String()), "UserWarning: pkg_resources is deprecated as an API") {
log.Info(stderr.String())
} else if strings.TrimSpace(stderr.String()) != "" {
log.Error(errors.New(stderr.String()), fmt.Sprintf("pgAdmin setup.py error for %s: ",
intentUser.Username))
continue
Expand Down
90 changes: 61 additions & 29 deletions internal/controller/standalone_pgadmin/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,16 @@ func TestReconcilePGAdminUsers(t *testing.T) {
assert.Equal(t, namespace, pgadmin.Namespace)
assert.Equal(t, container, naming.ContainerPGAdmin)

// Simulate a v7 version of pgAdmin by setting stdout to "7" for
// podexec call in reconcilePGAdminMajorVersion
_, _ = stdout.Write([]byte("7"))
// Simulate a v7.1 version of pgAdmin by setting stdout to "7.1"
// for podexec call in reconcilePGAdminVersion
_, _ = stdout.Write([]byte("7.1"))
return nil
}

assert.NilError(t, r.reconcilePGAdminUsers(ctx, pgadmin))
assert.Equal(t, calls, 1, "PodExec should be called once")
assert.Equal(t, pgadmin.Status.MajorVersion, 7)
assert.Equal(t, pgadmin.Status.MinorVersion, "7.1")
assert.Equal(t, pgadmin.Status.ImageSHA, "fakeSHA")
})

Expand All @@ -145,20 +146,58 @@ func TestReconcilePGAdminUsers(t *testing.T) {
) error {
calls++

// Simulate a v7 version of pgAdmin by setting stdout to "7" for
// podexec call in reconcilePGAdminMajorVersion
_, _ = stdout.Write([]byte("7"))
// Simulate a v7.1 version of pgAdmin by setting stdout to "7.1"
// for podexec call in reconcilePGAdminVersion
_, _ = stdout.Write([]byte("7.1"))
return nil
}

assert.NilError(t, r.reconcilePGAdminUsers(ctx, pgadmin))
assert.Equal(t, calls, 1, "PodExec should be called once")
assert.Equal(t, pgadmin.Status.MajorVersion, 7)
assert.Equal(t, pgadmin.Status.MinorVersion, "7.1")
assert.Equal(t, pgadmin.Status.ImageSHA, "newFakeSHA")
})

t.Run("PodHealthyBadVersion", func(t *testing.T) {
pgadmin := pgadmin.DeepCopy()
pod := pod.DeepCopy()

pod.DeletionTimestamp = nil
pod.Status.ContainerStatuses =
[]corev1.ContainerStatus{{Name: naming.ContainerPGAdmin}}
pod.Status.ContainerStatuses[0].State.Running =
new(corev1.ContainerStateRunning)
pod.Status.ContainerStatuses[0].ImageID = "fakeSHA"

r := new(PGAdminReconciler)
r.Reader = fake.NewClientBuilder().WithObjects(pod).Build()

calls := 0
r.PodExec = func(
ctx context.Context, namespace, pod, container string,
stdin io.Reader, stdout, stderr io.Writer, command ...string,
) error {
calls++

assert.Equal(t, pod, "pgadmin-123-0")
assert.Equal(t, namespace, pgadmin.Namespace)
assert.Equal(t, container, naming.ContainerPGAdmin)

// set expected version to something completely wrong
_, _ = stdout.Write([]byte("woot"))
return nil
}

assert.ErrorContains(t, r.reconcilePGAdminUsers(ctx, pgadmin), "strconv.ParseFloat: parsing \"woot\": invalid syntax")
assert.Equal(t, calls, 1, "PodExec should be called once")
assert.Equal(t, pgadmin.Status.MajorVersion, 0)
assert.Equal(t, pgadmin.Status.MinorVersion, "")
assert.Equal(t, pgadmin.Status.ImageSHA, "")
})
}

func TestReconcilePGAdminMajorVersion(t *testing.T) {
func TestReconcilePGAdminVersion(t *testing.T) {
ctx := context.Background()
pod := corev1.Pod{}
pod.Namespace = "test-namespace"
Expand All @@ -180,30 +219,15 @@ func TestReconcilePGAdminMajorVersion(t *testing.T) {
assert.Equal(t, namespace, "test-namespace")
assert.Equal(t, container, naming.ContainerPGAdmin)

// Simulate a v7 version of pgAdmin by setting stdout to "7" for
// podexec call in reconcilePGAdminMajorVersion
_, _ = stdout.Write([]byte("7"))
// Simulate a v9.3 version of pgAdmin by setting stdout to "9.3"
// for podexec call in reconcilePGAdminVersion
_, _ = stdout.Write([]byte("9.3"))
return nil
}

version, err := reconciler.reconcilePGAdminMajorVersion(ctx, podExecutor)
version, err := reconciler.reconcilePGAdminVersion(ctx, podExecutor)
assert.NilError(t, err)
assert.Equal(t, version, 7)
})

t.Run("FailedRetrieval", func(t *testing.T) {
reconciler.PodExec = func(
ctx context.Context, namespace, pod, container string,
stdin io.Reader, stdout, stderr io.Writer, command ...string,
) error {
// Simulate the python call giving bad data (not a version int)
_, _ = stdout.Write([]byte("asdfjkl;"))
return nil
}

version, err := reconciler.reconcilePGAdminMajorVersion(ctx, podExecutor)
assert.Check(t, err != nil)
assert.Equal(t, version, 0)
assert.Equal(t, version, "9.3")
})

t.Run("PodExecError", func(t *testing.T) {
Expand All @@ -214,9 +238,9 @@ func TestReconcilePGAdminMajorVersion(t *testing.T) {
return errors.New("PodExecError")
}

version, err := reconciler.reconcilePGAdminMajorVersion(ctx, podExecutor)
version, err := reconciler.reconcilePGAdminVersion(ctx, podExecutor)
assert.Check(t, err != nil)
assert.Equal(t, version, 0)
assert.Equal(t, version, "")
})
}

Expand Down Expand Up @@ -244,6 +268,14 @@ func TestWritePGAdminUsers(t *testing.T) {
}`)
assert.NilError(t, cc.Create(ctx, pgadmin))

// fake the status so that the correct commands will be used when creating
// users.
pgadmin.Status = v1beta1.PGAdminStatus{
ImageSHA: "fakesha",
MajorVersion: 9,
MinorVersion: "9.3",
}

userPasswordSecret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "user-password-secret1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ type PGAdminStatus struct {
// +optional
MajorVersion int `json:"majorVersion,omitempty"`

// MinorVersion represents the minor version of the running pgAdmin.
// +optional
MinorVersion string `json:"minorVersion,omitempty"`

// observedGeneration represents the .metadata.generation on which the status was based.
// +optional
// +kubebuilder:validation:Minimum=0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ commands:
pod_name=$(kubectl get pod -n "${NAMESPACE}" -l postgres-operator.crunchydata.com/pgadmin=pgadmin -o name)
secret_name=$(kubectl get secret -n "${NAMESPACE}" -l postgres-operator.crunchydata.com/pgadmin=pgadmin -o name)

# /usr/local/lib/python3.11/site-packages/pgadmin4 allows for various Python versions to be referenced in testing
users_in_pgadmin=$(kubectl exec -n "${NAMESPACE}" "${pod_name}" -- bash -c "python3 /usr/local/lib/python3.11/site-packages/pgadmin4/setup.py get-users --json")

bob_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="bob@example.com") | .role')
dave_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="dave@example.com") | .role')
bob_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="bob@example.com") | .role')
dave_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="dave@example.com") | .role')

[ $bob_role = 1 ] && [ $dave_role = 2 ] || exit 1
# Prior to pgAdmin 9.3, the role values were integers rather than strings. This supports both variations.
( [ $bob_role = 1 ] && [ $dave_role = 2 ] ) || ( [ $bob_role = "Administrator" ] && [ $dave_role = "User" ] ) || exit 1

users_in_secret=$(kubectl get "${secret_name}" -n "${NAMESPACE}" -o 'go-template={{index .data "users.json" }}' | base64 -d)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ commands:
pod_name=$(kubectl get pod -n "${NAMESPACE}" -l postgres-operator.crunchydata.com/pgadmin=pgadmin -o name)
secret_name=$(kubectl get secret -n "${NAMESPACE}" -l postgres-operator.crunchydata.com/pgadmin=pgadmin -o name)

# /usr/local/lib/python3.11/site-packages/pgadmin4 allows for various Python versions to be referenced in testing
users_in_pgadmin=$(kubectl exec -n "${NAMESPACE}" "${pod_name}" -- bash -c "python3 /usr/local/lib/python3.11/site-packages/pgadmin4/setup.py get-users --json")

bob_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="bob@example.com") | .role')
dave_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="dave@example.com") | .role')
jimi_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="jimi@example.com") | .role')
bob_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="bob@example.com") | .role')
dave_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="dave@example.com") | .role')
jimi_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="jimi@example.com") | .role')

[ $bob_role = 1 ] && [ $dave_role = 1 ] && [ $jimi_role = 2 ] || exit 1
# Prior to pgAdmin 9.3, the role values were integers rather than strings. This supports both variations.
( [ $bob_role = 1 ] && [ $dave_role = 1 ] && [ $jimi_role = 2 ] ) || ( [ $bob_role = "Administrator" ] && [ $dave_role = "Administrator" ] && [ $jimi_role = "User" ] ) || exit 1

users_in_secret=$(kubectl get "${secret_name}" -n "${NAMESPACE}" -o 'go-template={{index .data "users.json" }}' | base64 -d)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ commands:
pod_name=$(kubectl get pod -n "${NAMESPACE}" -l postgres-operator.crunchydata.com/pgadmin=pgadmin -o name)
secret_name=$(kubectl get secret -n "${NAMESPACE}" -l postgres-operator.crunchydata.com/pgadmin=pgadmin -o name)

# /usr/local/lib/python3.11/site-packages/pgadmin4 allows for various Python versions to be referenced in testing
users_in_pgadmin=$(kubectl exec -n "${NAMESPACE}" "${pod_name}" -- bash -c "python3 /usr/local/lib/python3.11/site-packages/pgadmin4/setup.py get-users --json")

bob_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="bob@example.com") | .role')
dave_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="dave@example.com") | .role')
jimi_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="jimi@example.com") | .role')
bob_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="bob@example.com") | .role')
dave_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="dave@example.com") | .role')
jimi_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="jimi@example.com") | .role')

[ $bob_role = 1 ] && [ $dave_role = 1 ] && [ $jimi_role = 2 ] || exit 1
# Prior to pgAdmin 9.3, the role values were integers rather than strings. This supports both variations.
( [ $bob_role = 1 ] && [ $dave_role = 1 ] && [ $jimi_role = 2 ] ) || ( [ $bob_role = "Administrator" ] && [ $dave_role = "Administrator" ] && [ $jimi_role = "User" ] ) || exit 1

users_in_secret=$(kubectl get "${secret_name}" -n "${NAMESPACE}" -o 'go-template={{index .data "users.json" }}' | base64 -d)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ commands:
pod_name=$(kubectl get pod -n "${NAMESPACE}" -l postgres-operator.crunchydata.com/pgadmin=pgadmin -o name)
secret_name=$(kubectl get secret -n "${NAMESPACE}" -l postgres-operator.crunchydata.com/pgadmin=pgadmin -o name)

# /usr/local/lib/python3.11/site-packages/pgadmin4 allows for various Python versions to be referenced in testing
users_in_pgadmin=$(kubectl exec -n "${NAMESPACE}" "${pod_name}" -- bash -c "python3 /usr/local/lib/python3.11/site-packages/pgadmin4/setup.py get-users --json")

bob_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="bob@example.com") | .role')
dave_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="dave@example.com") | .role')
jimi_role=$(printf '%s\n' $users_in_pgadmin | jq '.[] | select(.username=="jimi@example.com") | .role')
bob_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="bob@example.com") | .role')
dave_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="dave@example.com") | .role')
jimi_role=$(printf '%s\n' $users_in_pgadmin | jq -r '.[] | select(.username=="jimi@example.com") | .role')

[ $bob_role = 1 ] && [ $dave_role = 1 ] && [ $jimi_role = 2 ] || exit 1
# Prior to pgAdmin 9.3, the role values were integers rather than strings. This supports both variations.
( [ $bob_role = 1 ] && [ $dave_role = 1 ] && [ $jimi_role = 2 ] ) || ( [ $bob_role = "Administrator" ] && [ $dave_role = "Administrator" ] && [ $jimi_role = "User" ] ) || exit 1

users_in_secret=$(kubectl get "${secret_name}" -n "${NAMESPACE}" -o 'go-template={{index .data "users.json" }}' | base64 -d)

Expand Down
Loading