@@ -50,6 +50,26 @@ func TestPostgresConfigParametersV1beta1(t *testing.T) {
5050 assert .Equal (t , u .GetAPIVersion (), "postgres-operator.crunchydata.com/v1beta1" )
5151
5252 testPostgresConfigParametersCommon (t , cc , u )
53+
54+ t .Run ("Logging" , func (t * testing.T ) {
55+ t .Run ("Allowed" , func (t * testing.T ) {
56+ for _ , tt := range []struct {
57+ key string
58+ value any
59+ }{
60+ {key : "log_directory" , value : "anything" },
61+ } {
62+ t .Run (tt .key , func (t * testing.T ) {
63+ cluster := u .DeepCopy ()
64+ require .UnmarshalIntoField (t , cluster ,
65+ require .Value (yaml .Marshal (tt .value )),
66+ "spec" , "config" , "parameters" , tt .key )
67+
68+ assert .NilError (t , cc .Create (ctx , cluster , client .DryRunAll ))
69+ })
70+ }
71+ })
72+ })
5373}
5474
5575func TestPostgresConfigParametersV1 (t * testing.T ) {
@@ -82,6 +102,194 @@ func TestPostgresConfigParametersV1(t *testing.T) {
82102 assert .Equal (t , u .GetAPIVersion (), "postgres-operator.crunchydata.com/v1" )
83103
84104 testPostgresConfigParametersCommon (t , cc , u )
105+
106+ t .Run ("Logging" , func (t * testing.T ) {
107+ t .Run ("log_directory" , func (t * testing.T ) {
108+ volume := `{ accessModes: [ReadWriteOnce], resources: { requests: { storage: 1Mi } } }`
109+
110+ for _ , tt := range []struct {
111+ name string
112+ value any
113+ valid bool
114+ message string
115+ instances string
116+ }{
117+ // Only a few prefixes are allowed.
118+ {valid : false , value : 99 , message : `must start with "/` },
119+ {valid : false , value : "relative" , message : `must start with "/` },
120+ {valid : false , value : "/absolute" , message : `must start with "/pg` },
121+
122+ // These are the two acceptable directories inside /pgdata.
123+ {valid : true , value : "log" },
124+ {valid : true , value : "/pgdata/logs/postgres" },
125+ {valid : false , value : "/pgdata" , message : `"/pgdata/logs/postgres"` },
126+ {valid : false , value : "/pgdata/elsewhere" , message : `"/pgdata/logs/postgres"` },
127+ {valid : false , value : "/pgdata/sub/dir" , message : `"/pgdata/logs/postgres"` },
128+
129+ // There is one acceptable directory inside /pgtmp, but every instance set needs a temp volume.
130+ {
131+ name : "two instance sets and two temp volumes" ,
132+ value : "/pgtmp/logs/postgres" ,
133+ instances : `[
134+ { name: one, dataVolumeClaimSpec: ` + volume + `, volumes: { temp: ` + volume + ` } },
135+ { name: two, dataVolumeClaimSpec: ` + volume + `, volumes: { temp: ` + volume + ` } },
136+ ]` ,
137+ valid : true ,
138+ },
139+ {
140+ name : "two instance sets and one temp volume" ,
141+ value : "/pgtmp/logs/postgres" ,
142+ instances : `[
143+ { name: one, dataVolumeClaimSpec: ` + volume + `, volumes: { temp: ` + volume + ` } },
144+ { name: two, dataVolumeClaimSpec: ` + volume + ` },
145+ ]` ,
146+ valid : false ,
147+ message : `all instances need "volumes.temp"` ,
148+ },
149+ {
150+ name : "two instance sets and no temp volumes" ,
151+ value : "/pgtmp/logs/postgres" ,
152+ instances : `[
153+ { name: one, dataVolumeClaimSpec: ` + volume + ` },
154+ { name: two, dataVolumeClaimSpec: ` + volume + ` },
155+ ]` ,
156+ valid : false ,
157+ message : `all instances need "volumes.temp"` ,
158+ },
159+
160+ // These directories inside /pgtmp are unacceptable, regardless of volumes.
161+ {
162+ name : "no temp volumes" ,
163+ value : "/pgtmp/elsewhere" ,
164+ instances : `[{ name: any, dataVolumeClaimSpec: ` + volume + ` }]` ,
165+ valid : false ,
166+ message : `"/pgtmp/logs/postgres"` ,
167+ },
168+ {
169+ name : "two temp volumes" ,
170+ value : "/pgtmp" ,
171+ instances : `[
172+ { name: one, dataVolumeClaimSpec: ` + volume + `, volumes: { temp: ` + volume + ` } },
173+ { name: two, dataVolumeClaimSpec: ` + volume + `, volumes: { temp: ` + volume + ` } },
174+ ]` ,
175+ valid : false ,
176+ message : `"/pgtmp/logs/postgres"` ,
177+ },
178+
179+ // There is one acceptable directory inside /pgwal, but every instance set needs a WAL volume.
180+ {
181+ name : "two instance sets and two WAL volumes" ,
182+ value : "/pgwal/logs/postgres" ,
183+ instances : `[
184+ { name: one, dataVolumeClaimSpec: ` + volume + `, walVolumeClaimSpec: ` + volume + ` },
185+ { name: two, dataVolumeClaimSpec: ` + volume + `, walVolumeClaimSpec: ` + volume + ` },
186+ ]` ,
187+ valid : true ,
188+ },
189+ {
190+ name : "two instance sets and one WAL volume" ,
191+ value : "/pgwal/logs/postgres" ,
192+ instances : `[
193+ { name: one, dataVolumeClaimSpec: ` + volume + `, walVolumeClaimSpec: ` + volume + ` },
194+ { name: two, dataVolumeClaimSpec: ` + volume + ` },
195+ ]` ,
196+ valid : false ,
197+ message : `all instances need "walVolumeClaimSpec"` ,
198+ },
199+ {
200+ name : "two instance sets and no WAL volumes" ,
201+ value : "/pgwal/logs/postgres" ,
202+ instances : `[
203+ { name: one, dataVolumeClaimSpec: ` + volume + ` },
204+ { name: two, dataVolumeClaimSpec: ` + volume + ` },
205+ ]` ,
206+ valid : false ,
207+ message : `all instances need "walVolumeClaimSpec"` ,
208+ },
209+
210+ // These directories inside /pgwal are unacceptable, regardless of volumes.
211+ {
212+ name : "no WAL volumes" ,
213+ value : "/pgwal/elsewhere" ,
214+ instances : `[{ name: any, dataVolumeClaimSpec: ` + volume + ` }]` ,
215+ valid : false ,
216+ message : `"/pgwal/logs/postgres"` ,
217+ },
218+ {
219+ name : "two WAL volumes" ,
220+ value : "/pgwal" ,
221+ instances : `[
222+ { name: one, dataVolumeClaimSpec: ` + volume + `, walVolumeClaimSpec: ` + volume + ` },
223+ { name: two, dataVolumeClaimSpec: ` + volume + `, walVolumeClaimSpec: ` + volume + ` },
224+ ]` ,
225+ valid : false ,
226+ message : `"/pgwal/logs/postgres"` ,
227+ },
228+
229+ // Directories inside /volumes are acceptable, but every instance set needs additional volumes.
230+ //
231+ // TODO(validation): This could be more precise and check the directory name of each additional
232+ // volume, but Kubernetes 1.33 incorrectly estimates the cost of volume.name:
233+ // https://github.com/kubernetes-sigs/controller-tools/pull/1270#issuecomment-3272211184
234+ {
235+ name : "two instance sets and two additional volumes" ,
236+ value : "/volumes/anything" ,
237+ instances : `[
238+ { name: one, dataVolumeClaimSpec: ` + volume + `, volumes: { additional: [{ name: dir, claimName: a }] } },
239+ { name: two, dataVolumeClaimSpec: ` + volume + `, volumes: { additional: [{ name: dir, claimName: b }] } },
240+ ]` ,
241+ valid : true ,
242+ },
243+ {
244+ name : "two instance sets and one additional volume" ,
245+ value : "/volumes/anything" ,
246+ instances : `[
247+ { name: one, dataVolumeClaimSpec: ` + volume + `, volumes: { additional: [{ name: dir, claimName: a }] } },
248+ { name: two, dataVolumeClaimSpec: ` + volume + ` },
249+ ]` ,
250+ valid : false ,
251+ message : `all instances need an additional volume` ,
252+ },
253+ {
254+ name : "two instance sets and no additional volumes" ,
255+ value : "/volumes/anything" ,
256+ instances : `[
257+ { name: one, dataVolumeClaimSpec: ` + volume + ` },
258+ { name: two, dataVolumeClaimSpec: ` + volume + ` },
259+ ]` ,
260+ valid : false ,
261+ message : `all instances need an additional volume` ,
262+ },
263+ } {
264+ t .Run (cmp .Or (tt .name , fmt .Sprint (tt .valid , tt .value )), func (t * testing.T ) {
265+ cluster := u .DeepCopy ()
266+ if tt .instances != "" {
267+ require .UnmarshalIntoField (t , cluster , tt .instances , "spec" , "instances" )
268+ }
269+ require .UnmarshalIntoField (t , cluster , require .Value (yaml .Marshal (tt .value )),
270+ "spec" , "config" , "parameters" , "log_directory" )
271+
272+ err := cc .Create (ctx , cluster , client .DryRunAll )
273+
274+ if tt .valid {
275+ assert .NilError (t , err )
276+ assert .Equal (t , "" , tt .message , "BUG IN TEST: no message expected when valid" )
277+ } else {
278+ assert .Assert (t , apierrors .IsInvalid (err ))
279+
280+ details := require .StatusErrorDetails (t , err )
281+ assert .Assert (t , cmp .Len (details .Causes , 1 ))
282+
283+ // https://issue.k8s.io/133761
284+ if details .Causes [0 ].Field != "spec.config.parameters.[log_directory]" {
285+ assert .Check (t , cmp .Equal (details .Causes [0 ].Field , "spec.config.parameters[log_directory]" ))
286+ }
287+ assert .Assert (t , cmp .Contains (details .Causes [0 ].Message , tt .message ))
288+ }
289+ })
290+ }
291+ })
292+ })
85293}
86294
87295func testPostgresConfigParametersCommon (t * testing.T , cc client.Client , base unstructured.Unstructured ) {
@@ -155,7 +363,6 @@ func testPostgresConfigParametersCommon(t *testing.T, cc client.Client, base uns
155363 {valid : false , key : "logging_collector" , value : "on" , message : "unsafe" },
156364
157365 {valid : true , key : "log_destination" , value : "anything" },
158- {valid : true , key : "log_directory" , value : "anything" },
159366 {valid : true , key : "log_filename" , value : "anything" },
160367 {valid : true , key : "log_filename" , value : "percent-%s-too" },
161368 {valid : true , key : "log_rotation_age" , value : "7d" },
0 commit comments