Skip to content

Commit 0137720

Browse files
Merge pull request #492 from openshift-bot/synchronize-upstream
NO-ISSUE: Synchronize From Upstream Repositories
2 parents 6f500ac + e2942f6 commit 0137720

File tree

6 files changed

+292
-5
lines changed

6 files changed

+292
-5
lines changed

commitchecker.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
expectedMergeBase: c6dd08b391c92802ab40e38aba6672bae694a27d
1+
expectedMergeBase: dcf2963d03fee8a2e9c15c4adcf98127288af04e
22
upstreamBranch: main
33
upstreamOrg: operator-framework
44
upstreamRepo: operator-controller

internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety.go

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
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"
@@ -99,8 +100,9 @@ func (p *Preflight) runPreflight(ctx context.Context, relObjects []client.Object
99100
resultErrs := crdWideErrors(results)
100101
resultErrs = append(resultErrs, sameVersionErrors(results)...)
101102
resultErrs = append(resultErrs, servedVersionErrors(results)...)
102-
103-
validateErrors = append(validateErrors, fmt.Errorf("validating upgrade for CRD %q: %w", newCrd.Name, errors.Join(resultErrs...)))
103+
if len(resultErrs) > 0 {
104+
validateErrors = append(validateErrors, fmt.Errorf("validating upgrade for CRD %q: %w", newCrd.Name, errors.Join(resultErrs...)))
105+
}
104106
}
105107
}
106108

@@ -162,7 +164,11 @@ func sameVersionErrors(results *runner.Results) []error {
162164
for property, comparisonResults := range propertyResults {
163165
for _, result := range comparisonResults {
164166
for _, err := range result.Errors {
165-
errs = append(errs, fmt.Errorf("%s: %s: %s: %s", version, property, result.Name, err))
167+
msg := err
168+
if result.Name == "unhandled" {
169+
msg = conciseUnhandledMessage(err)
170+
}
171+
errs = append(errs, fmt.Errorf("%s: %s: %s: %s", version, property, result.Name, msg))
166172
}
167173
}
168174
}
@@ -181,11 +187,145 @@ func servedVersionErrors(results *runner.Results) []error {
181187
for property, comparisonResults := range propertyResults {
182188
for _, result := range comparisonResults {
183189
for _, err := range result.Errors {
184-
errs = append(errs, fmt.Errorf("%s: %s: %s: %s", version, property, result.Name, err))
190+
msg := err
191+
if result.Name == "unhandled" {
192+
msg = conciseUnhandledMessage(err)
193+
}
194+
errs = append(errs, fmt.Errorf("%s: %s: %s: %s", version, property, result.Name, msg))
185195
}
186196
}
187197
}
188198
}
189199

190200
return errs
191201
}
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+
}

internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1616
"k8s.io/apimachinery/pkg/runtime"
1717
"k8s.io/apimachinery/pkg/runtime/schema"
18+
crdifyconfig "sigs.k8s.io/crdify/pkg/config"
1819

1920
"github.com/operator-framework/operator-controller/internal/operator-controller/applier"
2021
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety"
@@ -386,3 +387,42 @@ func TestUpgrade(t *testing.T) {
386387
})
387388
}
388389
}
390+
391+
func TestUpgrade_UnhandledChanges_InSpec_DefaultPolicy(t *testing.T) {
392+
t.Run("unhandled spec changes cause error by default", func(t *testing.T) {
393+
preflight := newMockPreflight(getCrdFromManifestFile(t, "crd-unhandled-old.json"), nil)
394+
rel := &release.Release{
395+
Name: "test-release",
396+
Manifest: getManifestString(t, "crd-unhandled-new.json"),
397+
}
398+
objs, err := applier.HelmReleaseToObjectsConverter{}.GetObjectsFromRelease(rel)
399+
require.NoError(t, err)
400+
err = preflight.Upgrade(context.Background(), objs)
401+
require.Error(t, err)
402+
require.ErrorContains(t, err, "unhandled changes found")
403+
require.ErrorContains(t, err, "Format \"\" -> \"email\"")
404+
require.NotContains(t, err.Error(), "v1.JSONSchemaProps", "error message should be concise without raw diff")
405+
})
406+
}
407+
408+
func TestUpgrade_UnhandledChanges_PolicyError(t *testing.T) {
409+
t.Run("unhandled changes error when policy is Error", func(t *testing.T) {
410+
oldCrd := getCrdFromManifestFile(t, "crd-unhandled-old.json")
411+
preflight := crdupgradesafety.NewPreflight(&MockCRDGetter{oldCrd: oldCrd}, crdupgradesafety.WithConfig(&crdifyconfig.Config{
412+
Conversion: crdifyconfig.ConversionPolicyIgnore,
413+
UnhandledEnforcement: crdifyconfig.EnforcementPolicyError,
414+
}))
415+
416+
rel := &release.Release{
417+
Name: "test-release",
418+
Manifest: getManifestString(t, "crd-unhandled-new.json"),
419+
}
420+
421+
objs, err := applier.HelmReleaseToObjectsConverter{}.GetObjectsFromRelease(rel)
422+
require.NoError(t, err)
423+
err = preflight.Upgrade(context.Background(), objs)
424+
require.Error(t, err)
425+
require.ErrorContains(t, err, "unhandled changes found")
426+
require.ErrorContains(t, err, "Format \"\" -> \"email\"")
427+
})
428+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"apiVersion": "apiextensions.k8s.io/v1",
3+
"kind": "CustomResourceDefinition",
4+
"metadata": {
5+
"name": "widgets.example.com"
6+
},
7+
"spec": {
8+
"group": "example.com",
9+
"versions": [
10+
{
11+
"name": "v1alpha1",
12+
"served": true,
13+
"storage": true,
14+
"schema": {
15+
"openAPIV3Schema": {
16+
"type": "object",
17+
"properties": {
18+
"spec": {
19+
"type": "object",
20+
"properties": {
21+
"field": {
22+
"type": "string",
23+
"format": "email"
24+
}
25+
}
26+
}
27+
}
28+
}
29+
}
30+
}
31+
],
32+
"scope": "Namespaced",
33+
"names": {
34+
"plural": "widgets",
35+
"singular": "widget",
36+
"kind": "Widget"
37+
}
38+
}
39+
}
40+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"apiVersion": "apiextensions.k8s.io/v1",
3+
"kind": "CustomResourceDefinition",
4+
"metadata": {
5+
"name": "widgets.example.com"
6+
},
7+
"spec": {
8+
"group": "example.com",
9+
"versions": [
10+
{
11+
"name": "v1alpha1",
12+
"served": true,
13+
"storage": true,
14+
"schema": {
15+
"openAPIV3Schema": {
16+
"type": "object",
17+
"properties": {
18+
"spec": {
19+
"type": "object",
20+
"properties": {
21+
"field": {
22+
"type": "string"
23+
}
24+
}
25+
}
26+
}
27+
}
28+
}
29+
}
30+
],
31+
"scope": "Namespaced",
32+
"names": {
33+
"plural": "widgets",
34+
"singular": "widget",
35+
"kind": "Widget"
36+
}
37+
}
38+
}
39+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package crdupgradesafety
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestConciseUnhandledMessage_NoPrefix(t *testing.T) {
10+
raw := "some other error"
11+
require.Equal(t, raw, conciseUnhandledMessage(raw))
12+
}
13+
14+
func TestConciseUnhandledMessage_SingleChange(t *testing.T) {
15+
raw := "unhandled changes found :\n- Format: \"\"\n+ Format: \"email\"\n"
16+
require.Equal(t, "unhandled changes found (Format \"\" -> \"email\")", conciseUnhandledMessage(raw))
17+
}
18+
19+
func TestConciseUnhandledMessage_MultipleChanges(t *testing.T) {
20+
raw := "unhandled changes found :\n- Format: \"\"\n+ Format: \"email\"\n- Default: nil\n+ Default: \"value\"\n- Title: \"\"\n+ Title: \"Widget\"\n- Description: \"old\"\n+ Description: \"new\"\n"
21+
got := conciseUnhandledMessage(raw)
22+
require.Equal(t, "unhandled changes found (Format \"\" -> \"email\"; Default nil -> \"value\"; Title \"\" -> \"Widget\"; Description \"old\" -> \"new\")", got)
23+
}
24+
25+
func TestConciseUnhandledMessage_SkipComplexValues(t *testing.T) {
26+
raw := "unhandled changes found :\n- Default: &v1.JSONSchemaProps{}\n+ Default: &v1.JSONSchemaProps{Type: \"string\"}\n"
27+
require.Equal(t, "unhandled changes found (Default <complex value> -> <complex value> (changed))", conciseUnhandledMessage(raw))
28+
}

0 commit comments

Comments
 (0)