Skip to content

✨ Allow changing DNSNameservers in subnet config for OpenstackCluster #2511

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

Merged
merged 1 commit into from
May 14, 2025

Conversation

pbasov
Copy link
Contributor

@pbasov pbasov commented Apr 12, 2025

What this PR does / why we need it:
Allows changing DNSNameservers in subnet config for OpenstackCluster after the initial deployment

Which issue(s) this PR fixes (optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close the issue(s) when PR gets merged):
Fixes #2468

/hold

@k8s-ci-robot k8s-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Apr 12, 2025
Copy link

linux-foundation-easycla bot commented Apr 12, 2025

CLA Signed

The committers listed above are authorized under a signed CLA.

  • ✅ login: pbasov / name: Paul Basov (7d47c65)

@k8s-ci-robot k8s-ci-robot added the cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. label Apr 12, 2025
@k8s-ci-robot
Copy link
Contributor

Welcome @pbasov!

It looks like this is your first PR to kubernetes-sigs/cluster-api-provider-openstack 🎉. Please refer to our pull request process documentation to help your PR have a smooth ride to approval.

You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. Here is the bot commands documentation.

You can also check if kubernetes-sigs/cluster-api-provider-openstack has its own contribution guidelines.

You may want to refer to our testing guide if you run into trouble with your tests not passing.

If you are having difficulty getting your pull request seen, please follow the recommended escalation practices. Also, for tips and tricks in the contribution process you may want to read the Kubernetes contributor cheat sheet. We want to make sure your contribution gets all the attention it needs!

Thank you, and welcome to Kubernetes. 😃

@k8s-ci-robot
Copy link
Contributor

Hi @pbasov. Thanks for your PR.

I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@k8s-ci-robot k8s-ci-robot added needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Apr 12, 2025
Copy link

netlify bot commented Apr 12, 2025

Deploy Preview for kubernetes-sigs-cluster-api-openstack ready!

Name Link
🔨 Latest commit 7d47c65
🔍 Latest deploy log https://app.netlify.com/sites/kubernetes-sigs-cluster-api-openstack/deploys/6824718c0046b000087d009c
😎 Deploy Preview https://deploy-preview-2511--kubernetes-sigs-cluster-api-openstack.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@pbasov
Copy link
Contributor Author

pbasov commented Apr 12, 2025

I absolutely hated making DNSNameservers mutable in the webhook, it was tricky because subnets is a list (even though we support only ONE per network). Surely there's a better way to manage mutability for the config.

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. and removed cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels Apr 12, 2025
@pbasov
Copy link
Contributor Author

pbasov commented Apr 12, 2025

So yeah, why are we doing these webhook gymnastics when kubebuilder has immutable fields?
https://kubernetes.io/blog/2022/09/29/enforce-immutability-using-cel/#immutablility-after-first-modification

@lentzi90
Copy link
Contributor

/ok-to-test

@k8s-ci-robot k8s-ci-robot added ok-to-test Indicates a non-member PR verified by an org member that is safe to test. and removed needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Apr 15, 2025
@lentzi90
Copy link
Contributor

So yeah, why are we doing these webhook gymnastics when kubebuilder has immutable fields? https://kubernetes.io/blog/2022/09/29/enforce-immutability-using-cel/#immutablility-after-first-modification

Likely because no-one has taken the time to refactor it yet

@s3rj1k s3rj1k force-pushed the dns-update branch 3 times, most recently from d5ed5f7 to aa1e818 Compare April 29, 2025 12:05
@k8s-ci-robot k8s-ci-robot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Apr 29, 2025
@s3rj1k s3rj1k force-pushed the dns-update branch 2 times, most recently from c87b8b0 to a9f9c62 Compare April 29, 2025 12:51
@k8s-ci-robot k8s-ci-robot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Apr 29, 2025
@pbasov
Copy link
Contributor Author

pbasov commented Apr 29, 2025

@lentzi90 @mdbooth finished with the help of @s3rj1k, ready for review, tested manually on internal openstack cloud as well.

Copy link
Contributor

@mnaser mnaser left a comment

Choose a reason for hiding this comment

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

looks good to me otherwise

Comment on lines +262 to +267
var needsUpdate bool
if len(desiredNameservers) != len(currentNameservers) {
needsUpdate = true
} else {
needsUpdate = !equality.Semantic.DeepEqual(currentNameservers, desiredNameservers)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably not that big of a deal but I'm curious why don't we just drop needsUpdate and use the equality all the time. Is this an optimization or is it simply something I don't know? :)

Copy link
Contributor

Choose a reason for hiding this comment

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

yea, len is much quicker vs equality.Semantic.DeepEqual but for sure we can drop that optimization and keep code more simple

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Given that they're just slices of strings I'd personally do something a bit less voodoo than Semantic.DeepEqual like slices.Sort(); slices.Equal().

Copy link
Contributor

Choose a reason for hiding this comment

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

equality.Semantic.DeepEqual is basically what most of people use in K8s land (before it was regular reflect.DeepEqual), but sure we can switch to sort and equal approach

Copy link
Contributor

Choose a reason for hiding this comment

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

equality.Semantic.DeepEqual is used in multiple places, so I guess it makes sense to have a wrapper function to compare slices, something like this

func EqualIgnoreOrder[T cmp.Ordered](a, b []T) bool {
	if len(a) != len(b) {
		return false
	}

	if len(a) == 0 {
		return true
	}

	aCopy := slices.Clone(a)
	bCopy := slices.Clone(b)

	slices.Sort(aCopy)
	slices.Sort(bCopy)

	return slices.Equal(aCopy, bCopy)
}

Copy link
Contributor

Choose a reason for hiding this comment

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

This is just a nit, and mostly because I've spent time in the past reading Semantic.DeepEqual trying to determine the precise behaviour of its edge cases.

I only mention it at all because my personal preference would be to use something more explicit, especially when the data structure is simple as here. But this works and the PR has great test coverage, so there's no pressing need to update this.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am fine replacing if we bring similar helper in, I would hate to replace 1 func call with len,copy,sort,equal checks in multiple places, this can be followup if that makes sense

@mnaser
Copy link
Contributor

mnaser commented May 1, 2025

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label May 1, 2025
@lentzi90
Copy link
Contributor

lentzi90 commented May 5, 2025

/test pull-cluster-api-provider-openstack-e2e-full-test

Copy link
Contributor

@lentzi90 lentzi90 left a comment

Choose a reason for hiding this comment

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

/approve

@k8s-ci-robot k8s-ci-robot added approved Indicates a PR has been approved by an approver from all required OWNERS files. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. labels May 5, 2025
@k8s-ci-robot k8s-ci-robot removed lgtm "Looks good to me", indicates that a PR is ready to be merged. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. labels May 9, 2025
Copy link
Contributor

@mdbooth mdbooth left a comment

Choose a reason for hiding this comment

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

Thanks for this, and especially for the amazing test coverage 👌

I'm not 100% confident of my assertion that order of DNSNameservers doesn't matter. It feels like one of those things that shouldn't matter, but might in practise. Let me know if I'm wrong about that.

Strong preference for an extra unit test covering an ordering-only difference, with the behaviour asserted being whatever we decide it should be.

Suggestion inline for a significant simplification of the webhook. Let me know if that makes sense.

Comment on lines +262 to +267
var needsUpdate bool
if len(desiredNameservers) != len(currentNameservers) {
needsUpdate = true
} else {
needsUpdate = !equality.Semantic.DeepEqual(currentNameservers, desiredNameservers)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Given that they're just slices of strings I'd personally do something a bit less voodoo than Semantic.DeepEqual like slices.Sort(); slices.Equal().

DNSNameservers: []string{"8.8.8.8", "8.8.4.4"},
}, nil)
},
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Please can we have another test where they're the same, but in a different order? I think we should not update in this case.

Copy link
Contributor

Choose a reason for hiding this comment

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

We think that order probably does matter, so please can we have another test where the set of nameservers is identical but the order is different? We should assert that they're updated to be in the specified order.

Comment on lines 203 to 205
// Build maps of subnets by CIDR
oldSubnetMap := make(map[string]infrav1.SubnetSpec)
newSubnetMap := make(map[string]infrav1.SubnetSpec)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion:

You only need oldSubnetMap here, but make it contain pointers to infrav1.SubnetSpec instead so you can modifiy the originals in place. That removes the need for some of the gymnastics below.

}

// Check if all new subnets have matching old subnets with the same CIDR
for _, newSubnet := range newObj.Spec.ManagedSubnets {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
for _, newSubnet := range newObj.Spec.ManagedSubnets {
for i := range newObj.Spec.ManagedSubnets {
newSubnet := &newObj.Spec.ManagedSubnets[i]

We've now got pointers to the old and new original objects in place.

Comment on lines 233 to 249
// Create modified copies of the subnets with DNSNameservers cleared
oldSubnets := make([]infrav1.SubnetSpec, 0, len(oldObj.Spec.ManagedSubnets))
newSubnets := make([]infrav1.SubnetSpec, 0, len(newObj.Spec.ManagedSubnets))
for _, subnet := range oldObj.Spec.ManagedSubnets {
subnetCopy := subnet
subnetCopy.DNSNameservers = nil
oldSubnets = append(oldSubnets, subnetCopy)

if newSubnet, exists := newSubnetMap[subnet.CIDR]; exists {
newSubnetCopy := newSubnet
newSubnetCopy.DNSNameservers = nil
newSubnets = append(newSubnets, newSubnetCopy)
}
}

oldObj.Spec.ManagedSubnets = oldSubnets
newObj.Spec.ManagedSubnets = newSubnets
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need this any more.

Comment on lines 222 to 230
// Check if AllocationPools have changed
if !equality.Semantic.DeepEqual(oldSubnet.AllocationPools, newSubnet.AllocationPools) {
allErrs = append(allErrs, field.Forbidden(
field.NewPath("spec", "managedSubnets").Child("allocationPools"),
"cannot modify allocation pools in existing subnet",
))
}

newSubnetMap[newSubnet.CIDR] = newSubnet
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Check if AllocationPools have changed
if !equality.Semantic.DeepEqual(oldSubnet.AllocationPools, newSubnet.AllocationPools) {
allErrs = append(allErrs, field.Forbidden(
field.NewPath("spec", "managedSubnets").Child("allocationPools"),
"cannot modify allocation pools in existing subnet",
))
}
newSubnetMap[newSubnet.CIDR] = newSubnet
// DNSNameservers is mutable
oldSubnet.DNSNameservers = nil
newSubnet.DNSNameservers = nil

Copy link
Contributor

Choose a reason for hiding this comment

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

suggest to change to something like this, what do you think?

	// Allow changes only to DNSNameservers in ManagedSubnets spec
	if newObj.Spec.ManagedSubnets != nil && oldObj.Spec.ManagedSubnets != nil {
		// Check subnet count hasn't changed
		if len(oldObj.Spec.ManagedSubnets) != len(newObj.Spec.ManagedSubnets) {
			allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "managedSubnets"), "cannot add or remove subnets"))

			return aggregateObjErrors(newObj.GroupVersionKind().GroupKind(), newObj.Name, allErrs)
		}

		// Build map of old subnets by CIDR
		oldSubnetMap := make(map[string]infrav1.SubnetSpec, len(oldObj.Spec.ManagedSubnets))
		for _, subnet := range oldObj.Spec.ManagedSubnets {
			oldSubnetMap[subnet.CIDR] = subnet
		}

		// Validate each new subnet
		for i, newSubnet := range newObj.Spec.ManagedSubnets {
			oldSubnet, exists := oldSubnetMap[newSubnet.CIDR]
			if !exists {
				allErrs = append(allErrs, field.Forbidden(
					field.NewPath("spec", "managedSubnets").Index(i).Child("cidr"),
					fmt.Sprintf("cannot change subnet CIDR to %s", newSubnet.CIDR),
				))

				continue
			}

			// Check AllocationPools haven't changed
			if !equality.Semantic.DeepEqual(oldSubnet.AllocationPools, newSubnet.AllocationPools) {
				allErrs = append(allErrs, field.Forbidden(
					field.NewPath("spec", "managedSubnets").Index(i).Child("allocationPools"),
					"cannot modify allocation pools in existing subnet",
				))
			}
		}

		// If there are errors, return early without modifying objects
		if len(allErrs) > 0 {
			return aggregateObjErrors(newObj.GroupVersionKind().GroupKind(), newObj.Name, allErrs)
		}

		// Clear DNSNameservers from both objects
		for i := range oldObj.Spec.ManagedSubnets {
			oldObj.Spec.ManagedSubnets[i].DNSNameservers = nil
		}

		for i := range newObj.Spec.ManagedSubnets {
			newObj.Spec.ManagedSubnets[i].DNSNameservers = nil
		}
	}

Copy link
Contributor

Choose a reason for hiding this comment

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

I was trying to get rid of the AllocationPools check. I think this is fragile because if we added another field to this struct we'd need to also check it hasn't changed. The chances of that being forgotten are high.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should be solved by 7b5086d, thanks

@s3rj1k
Copy link
Contributor

s3rj1k commented May 13, 2025

/test pull-cluster-api-provider-openstack-e2e-test

@mdbooth
Copy link
Contributor

mdbooth commented May 14, 2025

This looks great. Can you squash the commits and I'll approve.

Co-authored-by: s3rj1k <[email protected]>
Co-authored-by: Matthew Booth <[email protected]>
Signed-off-by: s3rj1k <[email protected]>
@s3rj1k
Copy link
Contributor

s3rj1k commented May 14, 2025

This looks great. Can you squash the commits and I'll approve.

Done, thanks for helping out and reviewing

@mdbooth
Copy link
Contributor

mdbooth commented May 14, 2025

/approve

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: lentzi90, mdbooth, mnaser

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Copy link
Contributor

@lentzi90 lentzi90 left a comment

Choose a reason for hiding this comment

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

/lgtm
/unhold

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label May 14, 2025
@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label May 14, 2025
@k8s-ci-robot k8s-ci-robot merged commit 26e9bd3 into kubernetes-sigs:main May 14, 2025
9 checks passed
@github-project-automation github-project-automation bot moved this from Inbox to Done in CAPO Roadmap May 14, 2025
@s3rj1k s3rj1k deleted the dns-update branch May 14, 2025 11:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. ok-to-test Indicates a non-member PR verified by an org member that is safe to test. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Allow modifications to dnsNameservers in OpenstackCluster spec post-deployment
6 participants