Skip to content

Commit 505896b

Browse files
committed
Extract generateRandomString to common.go and add tests
Move generateRandomString from postgres_reconciler.go to common.go so the helper is available across the package. Add tests covering output length, hex character validity, and uniqueness. Also, make the generated password longer and thus more secure (16 -> 32).
1 parent 34d3392 commit 505896b

3 files changed

Lines changed: 122 additions & 10 deletions

File tree

internal/controller/common.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ package controller
1818

1919
import (
2020
"context"
21+
"crypto/rand"
2122
_ "embed"
23+
"encoding/hex"
2224
"encoding/json"
2325
"errors"
2426
"fmt"
@@ -184,3 +186,17 @@ func getDeployment(ctx context.Context, h *common_helper.Helper, name string, na
184186

185187
return deployment, nil
186188
}
189+
190+
// generateRandomString generates a random hex string of the given length.
191+
func generateRandomString(secretLength int) (string, error) {
192+
// Ceiling division: hex.EncodeToString doubles the byte count, so we need ceil(secretLength/2) bytes.
193+
randDataLen := (secretLength + 1) / 2
194+
195+
b := make([]byte, randDataLen)
196+
if _, err := rand.Read(b); err != nil {
197+
return "", fmt.Errorf("failed to generate secret: %w", err)
198+
}
199+
200+
randString := hex.EncodeToString(b)
201+
return randString[:secretLength], nil
202+
}

internal/controller/common_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"testing"
21+
)
22+
23+
func TestGenerateRandomStringLength(t *testing.T) {
24+
tests := []struct {
25+
name string
26+
length int
27+
}{
28+
{
29+
name: "Zero length",
30+
length: 0,
31+
},
32+
{
33+
name: "Odd length 1",
34+
length: 1,
35+
},
36+
{
37+
name: "Even length 2",
38+
length: 2,
39+
},
40+
{
41+
name: "Odd length 7",
42+
length: 7,
43+
},
44+
{
45+
name: "Even length 8",
46+
length: 8,
47+
},
48+
{
49+
name: "Odd length 15",
50+
length: 15,
51+
},
52+
{
53+
name: "Even length 16",
54+
length: 16,
55+
},
56+
{
57+
name: "Even length 32",
58+
length: 32,
59+
},
60+
}
61+
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
result, err := generateRandomString(tt.length)
65+
if err != nil {
66+
t.Errorf("generateRandomString(%d) unexpected error: %v", tt.length, err)
67+
}
68+
if len(result) != tt.length {
69+
t.Errorf("generateRandomString(%d) returned length %d, want %d", tt.length, len(result), tt.length)
70+
}
71+
})
72+
}
73+
}
74+
75+
func TestGenerateRandomStringHexCharacters(t *testing.T) {
76+
result, err := generateRandomString(32)
77+
if err != nil {
78+
t.Fatalf("generateRandomString(32) unexpected error: %v", err)
79+
}
80+
for i, c := range result {
81+
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
82+
t.Errorf("generateRandomString(32) character at index %d is %q, not a lowercase hex character", i, c)
83+
}
84+
}
85+
}
86+
87+
func TestGenerateRandomStringUniqueness(t *testing.T) {
88+
const length = 16
89+
a, err := generateRandomString(length)
90+
if err != nil {
91+
t.Fatalf("first call unexpected error: %v", err)
92+
}
93+
b, err := generateRandomString(length)
94+
if err != nil {
95+
t.Fatalf("second call unexpected error: %v", err)
96+
}
97+
if a == b {
98+
t.Errorf("generateRandomString(%d) returned identical values across two calls: %q", length, a)
99+
}
100+
}

internal/controller/postgres_reconciler.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ package controller
1818

1919
import (
2020
"context"
21-
"crypto/rand"
22-
"encoding/base64"
2321
"fmt"
2422

2523
common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
@@ -143,16 +141,14 @@ func reconcilePostgresSecret(h *common_helper.Helper, ctx context.Context, _ *ap
143141
}
144142

145143
// Only set password if not already present (preserve existing password)
146-
if len(secret.Data) == 0 || secret.Data[OpenStackLightspeedComponentPasswordFileName] == nil {
147-
// Generate random password only on first creation
148-
randomPassword := make([]byte, 12)
149-
if _, err := rand.Read(randomPassword); err != nil {
144+
if secret.Data[OpenStackLightspeedComponentPasswordFileName] == nil {
145+
const PostgreSQLPasswordLen = 32
146+
password, err := generateRandomString(PostgreSQLPasswordLen)
147+
if err != nil {
150148
return fmt.Errorf("%w: %v", ErrGeneratePostgresSecret, err)
151149
}
152-
encodedPassword := base64.StdEncoding.EncodeToString(randomPassword)
153-
secret.Data = map[string][]byte{
154-
OpenStackLightspeedComponentPasswordFileName: []byte(encodedPassword),
155-
}
150+
151+
secret.Data[OpenStackLightspeedComponentPasswordFileName] = []byte(password)
156152
}
157153

158154
secret.Data[PostgresUsernameSecretKey] = []byte(PostgresSQLUsername)

0 commit comments

Comments
 (0)