@@ -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