generated from amazon-archives/__template_Apache-2.0
-
Notifications
You must be signed in to change notification settings - Fork 274
feat(crd): Prevent breaking changes in RGD schemas #352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
a-hilaly
wants to merge
1
commit into
kubernetes-sigs:main
Choose a base branch
from
a-hilaly:crd-guard
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,547
−14
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| // Copyright 2025 The Kube Resource Orchestrator Authors. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"). You may | ||
| // not use this file except in compliance with the License. A copy of the | ||
| // License is located at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // or in the "license" file accompanying this file. This file is distributed | ||
| // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
| // express or implied. See the License for the specific language governing | ||
| // permissions and limitations under the License. | ||
|
|
||
| package compat | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
| ) | ||
|
|
||
| // ChangeType represents the type of schema change | ||
| type ChangeType string | ||
|
|
||
| const ( | ||
| // Breaking change types | ||
| PropertyRemoved ChangeType = "PROPERTY_REMOVED" | ||
| TypeChanged ChangeType = "TYPE_CHANGED" | ||
| RequiredAdded ChangeType = "REQUIRED_ADDED" | ||
| EnumRestricted ChangeType = "ENUM_RESTRICTED" | ||
| PatternChanged ChangeType = "PATTERN_CHANGED" | ||
| PatternAdded ChangeType = "PATTERN_ADDED" | ||
|
|
||
| // Non-breaking change types | ||
| PropertyAdded ChangeType = "PROPERTY_ADDED" | ||
| DescriptionChanged ChangeType = "DESCRIPTION_CHANGED" | ||
| DefaultChanged ChangeType = "DEFAULT_CHANGED" | ||
| RequiredRemoved ChangeType = "REQUIRED_REMOVED" | ||
| EnumExpanded ChangeType = "ENUM_EXPANDED" | ||
| PatternRemoved ChangeType = "PATTERN_REMOVED" | ||
| ) | ||
|
|
||
| // Change represents a single schema change | ||
| type Change struct { | ||
| // Path is the JSON path to the changed property | ||
| Path string | ||
| // ChangeType is the type of change | ||
| ChangeType ChangeType | ||
| // OldValue is the string representation of the old value (if applicable) | ||
| OldValue string | ||
| // NewValue is the string representation of the new value (if applicable) | ||
| NewValue string | ||
| } | ||
|
|
||
| // Report contains the full analysis of schema differences | ||
| type Report struct { | ||
| // BreakingChanges are changes that break backward compatibility | ||
| BreakingChanges []Change | ||
| // NonBreakingChanges are changes that maintain backward compatibility | ||
| NonBreakingChanges []Change | ||
| } | ||
|
|
||
| // IsCompatible returns true if no breaking changes were detected | ||
| func (r *Report) IsCompatible() bool { | ||
| return len(r.BreakingChanges) == 0 | ||
| } | ||
|
|
||
| // HasBreakingChanges returns true if breaking changes were detected | ||
| func (r *Report) HasBreakingChanges() bool { | ||
| return len(r.BreakingChanges) > 0 | ||
| } | ||
|
|
||
| // HasChanges returns true if any changes were detected | ||
| func (r *Report) HasChanges() bool { | ||
| return len(r.BreakingChanges) > 0 || len(r.NonBreakingChanges) > 0 | ||
| } | ||
|
|
||
| const maxBreakingChangesSummary = 3 | ||
|
|
||
| // SummarizeBreakingChanges returns a user-friendly summary of breaking changes | ||
| func (r *Report) String() string { | ||
| if !r.HasBreakingChanges() { | ||
| return "no breaking changes" | ||
| } | ||
|
|
||
| changeDescs := make([]string, 0, maxBreakingChangesSummary) | ||
|
|
||
| for i, change := range r.BreakingChanges { | ||
| // Cut off the summary if there are too many breaking changes | ||
| if i >= maxBreakingChangesSummary { | ||
| remaining := len(r.BreakingChanges) - i | ||
| if remaining > 0 { | ||
| changeDescs = append(changeDescs, fmt.Sprintf("and %d more changes", remaining)) | ||
| } | ||
a-hilaly marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| break | ||
| } | ||
| changeDescs = append(changeDescs, change.Description()) | ||
| } | ||
|
|
||
| return strings.Join(changeDescs, "; ") | ||
| } | ||
|
|
||
| // AddBreakingChange adds a breaking change to the result with automatically generated description | ||
| func (r *Report) AddBreakingChange(path string, changeType ChangeType, oldValue, newValue string) { | ||
| r.BreakingChanges = append(r.BreakingChanges, Change{ | ||
| Path: path, | ||
| ChangeType: changeType, | ||
| OldValue: oldValue, | ||
| NewValue: newValue, | ||
| }) | ||
| } | ||
|
|
||
| // AddNonBreakingChange adds a non-breaking change to the result with automatically generated description | ||
| func (r *Report) AddNonBreakingChange(path string, changeType ChangeType, oldValue, newValue string) { | ||
| r.NonBreakingChanges = append(r.NonBreakingChanges, Change{ | ||
| Path: path, | ||
| ChangeType: changeType, | ||
| OldValue: oldValue, | ||
| NewValue: newValue, | ||
| }) | ||
| } | ||
|
|
||
| // lastPathComponent extracts the last component from a JSON path | ||
| func lastPathComponent(path string) string { | ||
| parts := strings.Split(path, ".") | ||
| if len(parts) == 0 { | ||
| return path | ||
| } | ||
| return parts[len(parts)-1] | ||
| } | ||
|
|
||
| // Description generates a human-readable description based on the change type | ||
| func (c Change) Description() string { | ||
| propName := lastPathComponent(c.Path) | ||
|
|
||
| switch c.ChangeType { | ||
| case PropertyRemoved: | ||
| return fmt.Sprintf("Property %s was removed", propName) | ||
| case PropertyAdded: | ||
| if c.NewValue == "required" { | ||
| return fmt.Sprintf("Required property %s was added", propName) | ||
| } | ||
| return fmt.Sprintf("Optional property %s was added", propName) | ||
| case TypeChanged: | ||
| return fmt.Sprintf("Type changed from %s to %s", c.OldValue, c.NewValue) | ||
| case RequiredAdded: | ||
| return fmt.Sprintf("Field %s is newly required", c.NewValue) | ||
| case RequiredRemoved: | ||
| return fmt.Sprintf("Field %s is no longer required", c.OldValue) | ||
| case EnumRestricted: | ||
| return fmt.Sprintf("Enum value %s was removed", c.OldValue) | ||
| case EnumExpanded: | ||
| return fmt.Sprintf("Enum value %s was added", c.NewValue) | ||
| case PatternChanged: | ||
| return fmt.Sprintf("Validation pattern changed from %s to %s", c.OldValue, c.NewValue) | ||
| case PatternAdded: | ||
| return fmt.Sprintf("Validation pattern %s was added", c.NewValue) | ||
| case PatternRemoved: | ||
| return fmt.Sprintf("Validation pattern %s was removed", c.OldValue) | ||
| case DescriptionChanged: | ||
| return "Description field was changed" | ||
| case DefaultChanged: | ||
| return "Default value was changed" | ||
| default: | ||
| return fmt.Sprintf("Unknown change to %s", c.Path) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // Copyright 2025 The Kube Resource Orchestrator Authors. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"). You may | ||
| // not use this file except in compliance with the License. A copy of the | ||
| // License is located at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // or in the "license" file accompanying this file. This file is distributed | ||
| // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
| // express or implied. See the License for the specific language governing | ||
| // permissions and limitations under the License. | ||
|
|
||
| package compat | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||
| ) | ||
|
|
||
| // CompareVersions compares CRD versions and returns a compatibility report. | ||
| // This is a convenience wrapper that extracts schemas from version slices. | ||
| // It expects exactly one version in each slice. | ||
| func CompareVersions(oldVersions, newVersions []v1.CustomResourceDefinitionVersion) (*Report, error) { | ||
| if len(oldVersions) != 1 || len(newVersions) != 1 { | ||
| return nil, fmt.Errorf("expected exactly one version in each CRD, got %d old and %d new versions", | ||
| len(oldVersions), len(newVersions)) | ||
| } | ||
|
|
||
| oldVersion := oldVersions[0] | ||
| newVersion := newVersions[0] | ||
|
|
||
| // Check version names match | ||
| if oldVersion.Name != newVersion.Name { | ||
| return &Report{ | ||
| BreakingChanges: []Change{ | ||
| { | ||
| Path: "version", | ||
| ChangeType: TypeChanged, | ||
| OldValue: oldVersion.Name, | ||
| NewValue: newVersion.Name, | ||
| }, | ||
| }, | ||
| }, nil | ||
| } | ||
|
|
||
| // Verify schemas exist | ||
| if oldVersion.Schema == nil || oldVersion.Schema.OpenAPIV3Schema == nil { | ||
| return nil, fmt.Errorf("old version %s has no schema", oldVersion.Name) | ||
| } | ||
|
|
||
| if newVersion.Schema == nil || newVersion.Schema.OpenAPIV3Schema == nil { | ||
| return nil, fmt.Errorf("new version %s has no schema", newVersion.Name) | ||
| } | ||
|
|
||
| // Compare schemas first | ||
| report := Compare(oldVersion.Schema.OpenAPIV3Schema, newVersion.Schema.OpenAPIV3Schema) | ||
|
|
||
| // Check version metadata changes that could break kro | ||
| // Served going from true to false would break API access | ||
| if oldVersion.Served && !newVersion.Served { | ||
| report.AddBreakingChange("version.served", TypeChanged, "true", "false") | ||
| } | ||
|
|
||
| // Status subresource removal would break status updates | ||
| oldHasStatus := oldVersion.Subresources != nil && oldVersion.Subresources.Status != nil | ||
| newHasStatus := newVersion.Subresources != nil && newVersion.Subresources.Status != nil | ||
| if oldHasStatus && !newHasStatus { | ||
| report.AddBreakingChange("version.subresources.status", PropertyRemoved, "", "") | ||
| } | ||
|
|
||
| return report, nil | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.