Skip to content
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

🐛 ClusterClass: allow accessing nestedFields via valueFrom.variable #5925

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion internal/webhooks/patch_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,12 @@ func validateJSONPatchValues(jsonPatch clusterv1.JSONPatch, variableSet map[stri
))
}
} else {
if _, ok := variableSet[*jsonPatch.ValueFrom.Variable]; !ok {
// Note: We're only validating if the variable name exists without
// validating if the whole path is an existing variable.
// This could be done by re-using getVariableValue of the json patch
// generator but requires a refactoring first.
variableName := getVariableName(*jsonPatch.ValueFrom.Variable)
Copy link
Member Author

@sbueringer sbueringer Jan 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware that we're duplicating more logic here, similar to how we did above with the builtin variable validation.

But I think for now it's the pragmatic thing to do. I think long-term we should clean it up so that the webhook can re-use code of the JSON patch generator for validation to allow more powerful validations (and less duplicate code/logic).

if _, ok := variableSet[variableName]; !ok {
allErrs = append(allErrs,
field.Invalid(
path.Child("valueFrom", "variable"),
Expand All @@ -309,6 +314,12 @@ func validateJSONPatchValues(jsonPatch clusterv1.JSONPatch, variableSet map[stri
return allErrs
}

func getVariableName(variable string) string {
return strings.FieldsFunc(variable, func(r rune) bool {
return r == '[' || r == '.'
})[0]
}

// This contains a list of all of the valid builtin variables.
// TODO(killianmuldoon): Match this list to controllers/topology/internal/extensions/patches/variables as those structs become available across the code base i.e. public or top-level internal.
var builtinVariables = sets.NewString(
Expand Down
141 changes: 141 additions & 0 deletions internal/webhooks/patch_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,115 @@ func TestValidatePatches(t *testing.T) {
},
wantErr: true,
},
{
name: "pass if jsonPatch uses a user-defined variable which is defined",
clusterClass: clusterv1.ClusterClass{
Spec: clusterv1.ClusterClassSpec{
ControlPlane: clusterv1.ControlPlaneClass{
LocalObjectTemplate: clusterv1.LocalObjectTemplate{
Ref: &corev1.ObjectReference{
APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
Kind: "ControlPlaneTemplate",
},
},
},
Patches: []clusterv1.ClusterClassPatch{
{
Name: "patch1",
Definitions: []clusterv1.PatchDefinition{
{
Selector: clusterv1.PatchSelector{
APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
Kind: "ControlPlaneTemplate",
MatchResources: clusterv1.PatchSelectorMatch{
ControlPlane: true,
},
},
JSONPatches: []clusterv1.JSONPatch{
{
Op: "add",
Path: "/spec/template/spec/",
ValueFrom: &clusterv1.JSONPatchValue{
Variable: pointer.String("variableName"),
},
},
},
},
},
},
},
Variables: []clusterv1.ClusterClassVariable{
{
Name: "variableName",
Required: true,
Schema: clusterv1.VariableSchema{
OpenAPIV3Schema: clusterv1.JSONSchemaProps{
Type: "string",
},
},
},
},
},
},
wantErr: false,
},
{
name: "pass if jsonPatch uses a nested user-defined variable which is defined",
clusterClass: clusterv1.ClusterClass{
Spec: clusterv1.ClusterClassSpec{
ControlPlane: clusterv1.ControlPlaneClass{
LocalObjectTemplate: clusterv1.LocalObjectTemplate{
Ref: &corev1.ObjectReference{
APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
Kind: "ControlPlaneTemplate",
},
},
},
Patches: []clusterv1.ClusterClassPatch{
{
Name: "patch1",
Definitions: []clusterv1.PatchDefinition{
{
Selector: clusterv1.PatchSelector{
APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
Kind: "ControlPlaneTemplate",
MatchResources: clusterv1.PatchSelectorMatch{
ControlPlane: true,
},
},
JSONPatches: []clusterv1.JSONPatch{
{
Op: "add",
Path: "/spec/template/spec/",
ValueFrom: &clusterv1.JSONPatchValue{
Variable: pointer.String("variableName.nestedField"),
},
},
},
},
},
},
},
Variables: []clusterv1.ClusterClassVariable{
{
Name: "variableName",
Required: true,
Schema: clusterv1.VariableSchema{
OpenAPIV3Schema: clusterv1.JSONSchemaProps{
Type: "object",
Properties: map[string]clusterv1.JSONSchemaProps{
"nestedField": {
Type: "string",
},
},
},
},
},
},
},
},
wantErr: false,
},
{
name: "error if jsonPatch uses a builtin variable which is not defined",
clusterClass: clusterv1.ClusterClass{
Expand Down Expand Up @@ -1628,3 +1737,35 @@ func Test_validateSelectors(t *testing.T) {
})
}
}

func TestGetVariableName(t *testing.T) {
tests := []struct {
name string
variable string
variableName string
}{
{
name: "simple variable",
variable: "variableA",
variableName: "variableA",
},
{
name: "variable object",
variable: "variableObject.field",
variableName: "variableObject",
},
{
name: "variable array",
variable: "variableArray[0]",
variableName: "variableArray",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

g.Expect(getVariableName(tt.variable)).To(Equal(tt.variableName))
})
}
}