44 "context"
55 "errors"
66 "fmt"
7+ "strings"
78
89 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
910 apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
@@ -165,7 +166,7 @@ func sameVersionErrors(results *runner.Results) []error {
165166 for _ , err := range result .Errors {
166167 msg := err
167168 if result .Name == "unhandled" {
168- msg = "unhandled changes found"
169+ msg = conciseUnhandledMessage ( err )
169170 }
170171 errs = append (errs , fmt .Errorf ("%s: %s: %s: %s" , version , property , result .Name , msg ))
171172 }
@@ -188,7 +189,7 @@ func servedVersionErrors(results *runner.Results) []error {
188189 for _ , err := range result .Errors {
189190 msg := err
190191 if result .Name == "unhandled" {
191- msg = "unhandled changes found"
192+ msg = conciseUnhandledMessage ( err )
192193 }
193194 errs = append (errs , fmt .Errorf ("%s: %s: %s: %s" , version , property , result .Name , msg ))
194195 }
@@ -198,3 +199,133 @@ func servedVersionErrors(results *runner.Results) []error {
198199
199200 return errs
200201}
202+
203+ const unhandledSummaryPrefix = "unhandled changes found"
204+
205+ // conciseUnhandledMessage trims the CRD diff emitted by crdify's "unhandled" comparator
206+ // into a short human readable description so operators get a hint of the change without
207+ // the unreadable Go struct dump.
208+ func conciseUnhandledMessage (raw string ) string {
209+ if ! strings .Contains (raw , unhandledSummaryPrefix ) {
210+ return raw
211+ }
212+
213+ details := extractUnhandledDetails (raw )
214+ if len (details ) == 0 {
215+ return unhandledSummaryPrefix
216+ }
217+
218+ return fmt .Sprintf ("%s (%s)" , unhandledSummaryPrefix , strings .Join (details , "; " ))
219+ }
220+
221+ func extractUnhandledDetails (raw string ) []string {
222+ type diffEntry struct {
223+ before string
224+ after string
225+ beforeRaw string
226+ afterRaw string
227+ }
228+
229+ entries := map [string ]* diffEntry {}
230+ order := []string {}
231+
232+ for _ , line := range strings .Split (raw , "\n " ) {
233+ trimmed := strings .TrimSpace (line )
234+ if len (trimmed ) < 2 {
235+ continue
236+ }
237+
238+ sign := trimmed [0 ]
239+ if sign != '-' && sign != '+' {
240+ continue
241+ }
242+
243+ field , value , rawValue := parseUnhandledDiffValue (trimmed [1 :])
244+ if field == "" {
245+ continue
246+ }
247+
248+ entry , ok := entries [field ]
249+ if ! ok {
250+ entry = & diffEntry {}
251+ entries [field ] = entry
252+ order = append (order , field )
253+ }
254+
255+ if sign == '-' {
256+ entry .before = value
257+ entry .beforeRaw = rawValue
258+ } else {
259+ entry .after = value
260+ entry .afterRaw = rawValue
261+ }
262+ }
263+
264+ details := []string {}
265+ for _ , field := range order {
266+ entry := entries [field ]
267+ if entry .before == "" && entry .after == "" {
268+ continue
269+ }
270+ if entry .before == entry .after && entry .beforeRaw == entry .afterRaw {
271+ continue
272+ }
273+
274+ before := entry .before
275+ if before == "" {
276+ before = "<empty>"
277+ }
278+ after := entry .after
279+ if after == "" {
280+ after = "<empty>"
281+ }
282+ if entry .before == entry .after && entry .beforeRaw != entry .afterRaw {
283+ after = after + " (changed)"
284+ }
285+
286+ details = append (details , fmt .Sprintf ("%s %s -> %s" , field , before , after ))
287+ }
288+
289+ return details
290+ }
291+
292+ func parseUnhandledDiffValue (fragment string ) (string , string , string ) {
293+ cleaned := strings .TrimSpace (fragment )
294+ cleaned = strings .TrimPrefix (cleaned , "\t " )
295+ cleaned = strings .TrimSpace (cleaned )
296+ cleaned = strings .TrimSuffix (cleaned , "," )
297+
298+ parts := strings .SplitN (cleaned , ":" , 2 )
299+ if len (parts ) != 2 {
300+ return "" , "" , ""
301+ }
302+
303+ field := strings .TrimSpace (parts [0 ])
304+ rawValue := strings .TrimSpace (parts [1 ])
305+ value := normalizeUnhandledValue (rawValue )
306+
307+ if field == "" {
308+ return "" , "" , ""
309+ }
310+
311+ return field , value , rawValue
312+ }
313+
314+ func normalizeUnhandledValue (value string ) string {
315+ value = strings .TrimSuffix (value , "," )
316+ value = strings .TrimSpace (value )
317+
318+ switch value {
319+ case "" :
320+ return "<empty>"
321+ case "\" \" " :
322+ return "\" \" "
323+ }
324+
325+ value = strings .ReplaceAll (value , "v1." , "" )
326+ if strings .Contains (value , "JSONSchemaProps" ) {
327+ return "<complex value>"
328+ }
329+
330+ return value
331+ }
0 commit comments