Repository: kubernetes-sigs/cluster-api
Pull request #6313 (2022-03-18 10:56:56)

✨ Add IPAddress and IPAddressClaim CRs to Experimental API
**What this PR does / why we need it**:
This implements the API contract described in the IPAM Proposal (#6000).
It adds two new Custom Resources to the experimental API: IPAddress and IPAddressClaim.


**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:

k8s-ci-robot: Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with `/test all`
k8s-ci-robot: @schrej: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **sbueringer** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @sbueringer` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["sbueringer"]} -->
k8s-ci-robot: @schrej: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
k8s-ci-robot: @schrej: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
k8s-ci-robot: @schrej: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-verify-main | 379f44c3eb608406fd1c819e52e8d5cf61a8bb2d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-verify-main/1542512653786681344) | true | `/test pull-cluster-api-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
schrej: Addressed @AectannArd's comments and replaced `net/netip` with `inet.af/netaddr` (only used in webhooks) to remove dependency on go 1.18.
#6350
schrej: /hold cancel
as no longer depending on go 1.18
k8s-ci-robot: @schrej: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-verify-main | 0395bd73368fe53ab9a5596de69343be122076f1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-verify-main/1542517236323848192) | true | `/test pull-cluster-api-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
sbueringer: First round of review, mostly nits.

@fabriziopandini Depending on how fast we can get this reviewed / findings addressed this might be a candidate to include in v1.2.0
k8s-ci-robot: @schrej: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-verify-main | bc8f67be6d0fa7d0b66a6815167347225a7b1593 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-verify-main/1542882964273106944) | true | `/test pull-cluster-api-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
sbueringer: lgtm from my side pending some opinions / decisions on the two points above
k8s-ci-robot: @schrej: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-verify-main | e81ce4187a2661cb3ab32be789507176794f9d78 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-verify-main/1544608541459877888) | true | `/test pull-cluster-api-verify-main`
pull-cluster-api-e2e-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-main/1544612046773948416) | true | `/test pull-cluster-api-e2e-main`
pull-cluster-api-e2e-informing-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-main/1544612047717666816) | false | `/test pull-cluster-api-e2e-informing-main`
pull-cluster-api-e2e-informing-ipv6-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-ipv6-main/1544612048552333312) | false | `/test pull-cluster-api-e2e-informing-ipv6-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @schrej: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-informing-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-main/1544612047717666816) | false | `/test pull-cluster-api-e2e-informing-main`
pull-cluster-api-e2e-informing-ipv6-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-ipv6-main/1544612048552333312) | false | `/test pull-cluster-api-e2e-informing-ipv6-main`
pull-cluster-api-e2e-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-main/1544615810830438400) | true | `/test pull-cluster-api-e2e-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @schrej: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-informing-ipv6-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-ipv6-main/1544612048552333312) | false | `/test pull-cluster-api-e2e-informing-ipv6-main`
pull-cluster-api-e2e-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-main/1544615810830438400) | true | `/test pull-cluster-api-e2e-main`
pull-cluster-api-e2e-informing-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-main/1544615810872381440) | false | `/test pull-cluster-api-e2e-informing-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @schrej: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-main/1544615810830438400) | true | `/test pull-cluster-api-e2e-main`
pull-cluster-api-e2e-informing-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-main/1544615810872381440) | false | `/test pull-cluster-api-e2e-informing-main`
pull-cluster-api-e2e-informing-ipv6-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-ipv6-main/1544615810918518784) | false | `/test pull-cluster-api-e2e-informing-ipv6-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @schrej: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-main/1544615810830438400) | true | `/test pull-cluster-api-e2e-main`
pull-cluster-api-e2e-informing-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-main/1544615810872381440) | false | `/test pull-cluster-api-e2e-informing-main`
pull-cluster-api-e2e-informing-ipv6-main | 7e855869f247b8aafd18ea5ee5ddea523c7bd74d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-ipv6-main/1544624205163663360) | false | `/test pull-cluster-api-e2e-informing-ipv6-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @schrej: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-main | 3cd24277f7835d4cf709cb453ff4dbb083d6ee1e | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-main/1544615810830438400) | true | `/test pull-cluster-api-e2e-main`
pull-cluster-api-e2e-informing-ipv6-main | 7e855869f247b8aafd18ea5ee5ddea523c7bd74d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-ipv6-main/1544624205163663360) | false | `/test pull-cluster-api-e2e-informing-ipv6-main`
pull-cluster-api-e2e-informing-main | 7e855869f247b8aafd18ea5ee5ddea523c7bd74d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-main/1544624205113331712) | false | `/test pull-cluster-api-e2e-informing-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @schrej: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-informing-ipv6-main | 7e855869f247b8aafd18ea5ee5ddea523c7bd74d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-ipv6-main/1544624205163663360) | false | `/test pull-cluster-api-e2e-informing-ipv6-main`
pull-cluster-api-e2e-informing-main | 7e855869f247b8aafd18ea5ee5ddea523c7bd74d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-informing-main/1544624205113331712) | false | `/test pull-cluster-api-e2e-informing-main`
pull-cluster-api-e2e-main | 7e855869f247b8aafd18ea5ee5ddea523c7bd74d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6313/pull-cluster-api-e2e-main/1544624205079777280) | true | `/test pull-cluster-api-e2e-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=6313). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aschrej). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
schrej: Moved it to `exp/ipam` and `ipam.cluster.x-k8s.io`. I've also removed the validation that the claim exists.
sbueringer: > Considering that the groupversion_info.go files usually contain package comments in API packages, do we really need the doc.go files everywhere just to have an additional package comment?

I think package comments are usually in `doc.go` when there are multiple files in a package (I guess because it's easier to find them for readers that way). 
schrej: > I think package comments are usually in `doc.go` when there are multiple files in a package (I guess because it's easier to find them for readers that way).

Imo there should only be one package comment though, so we should decide on one location and then clean up. It's probably duplicated to `groupversion_info.go` so the linter doesn't complain about the kubebuilder comments. They seem to work if there is a space between the package name and the comment though, so the package comment could be removed from those files.
sbueringer: @schrej If we want to standardize, I would suggest let's see if there is a guideline on what the right way is (in something like "effective go" or "go doc comments"). Then we can open an issue and follow that consistently.
enxebre: thanks! lgtm overall other than this question https://github.com/kubernetes-sigs/cluster-api/pull/6313#discussion_r915093766

Patch:
diff --git a/Makefile b/Makefile
index ec2f01ce3..8f4956241 100644
--- a/Makefile
+++ b/Makefile
@@ -111,7 +111,7 @@ GO_APIDIFF_BIN := go-apidiff
 GO_APIDIFF := $(abspath $(TOOLS_BIN_DIR)/$(GO_APIDIFF_BIN)-$(GO_APIDIFF_VER))
 GO_APIDIFF_PKG := github.com/joelanford/go-apidiff
 
-KPROMO_VER := v3.4.4
+KPROMO_VER := v3.4.1
 KPROMO_BIN := kpromo
 KPROMO :=  $(abspath $(TOOLS_BIN_DIR)/$(KPROMO_BIN)-$(KPROMO_VER))
 KPROMO_PKG := sigs.k8s.io/promo-tools/v3/cmd/kpromo
@@ -222,6 +222,8 @@ generate-manifests-core: $(CONTROLLER_GEN) $(KUSTOMIZE) ## Generate manifests e.
 		paths=./$(EXP_DIR)/internal/controllers/... \
 		paths=./$(EXP_DIR)/addons/api/... \
 		paths=./$(EXP_DIR)/addons/internal/controllers/... \
+		paths=./$(EXP_DIR)/ipam/api/... \
+		paths=./$(EXP_DIR)/ipam/internal/webhooks/... \
 		paths=./$(EXP_DIR)/runtime/api/... \
 		paths=./$(EXP_DIR)/runtime/internal/controllers/... \
 		crd:crdVersions=v1 \
@@ -288,6 +290,7 @@ generate-go-deepcopy-core: $(CONTROLLER_GEN) ## Generate deepcopy go code for co
 		paths=./api/... \
 		paths=./$(EXP_DIR)/api/... \
 		paths=./$(EXP_DIR)/addons/api/... \
+		paths=./$(EXP_DIR)/ipam/api/... \
 		paths=./$(EXP_DIR)/runtime/api/... \
 		paths=./$(EXP_DIR)/runtime/hooks/api/... \
 		paths=./internal/runtime/test/... \
@@ -825,7 +828,7 @@ release-notes: $(RELEASE_NOTES_DIR) $(RELEASE_NOTES)
 
 .PHONY: promote-images
 promote-images: $(KPROMO)
-	$(KPROMO) pr --project cluster-api --tag $(RELEASE_TAG) --reviewers "$(IMAGE_REVIEWERS)" --fork $(USER_FORK) --image cluster-api-controller --image kubeadm-control-plane-controller --image kubeadm-bootstrap-controller --image clusterctl
+	$(KPROMO) pr --project cluster-api --tag $(RELEASE_TAG) --reviewers "$(IMAGE_REVIEWERS)" --fork $(USER_FORK) --image cluster-api-controller --image kubeadm-control-plane-controller --image kubeadm-bootstrap-controller --image clusterctl --digests ""
 
 ## --------------------------------------
 ## Docker
diff --git a/cmd/clusterctl/hack/create-local-repository.py b/cmd/clusterctl/hack/create-local-repository.py
index 0a472b324..5a5579671 100755
--- a/cmd/clusterctl/hack/create-local-repository.py
+++ b/cmd/clusterctl/hack/create-local-repository.py
@@ -53,24 +53,24 @@ settings = {}
 providers = {
       'cluster-api': {
               'componentsFile': 'core-components.yaml',
-              'nextVersion': 'v1.3.99',
+              'nextVersion': 'v1.2.99',
               'type': 'CoreProvider',
       },
       'bootstrap-kubeadm': {
             'componentsFile': 'bootstrap-components.yaml',
-            'nextVersion': 'v1.3.99',
+            'nextVersion': 'v1.2.99',
             'type': 'BootstrapProvider',
             'configFolder': 'bootstrap/kubeadm/config/default',
       },
       'control-plane-kubeadm': {
             'componentsFile': 'control-plane-components.yaml',
-            'nextVersion': 'v1.3.99',
+            'nextVersion': 'v1.2.99',
             'type': 'ControlPlaneProvider',
             'configFolder': 'controlplane/kubeadm/config/default',
       },
       'infrastructure-docker': {
           'componentsFile': 'infrastructure-components.yaml',
-          'nextVersion': 'v1.3.99',
+          'nextVersion': 'v1.2.99',
           'type': 'InfrastructureProvider',
           'configFolder': 'test/infrastructure/docker/config/default',
       },
diff --git a/config/crd/bases/ipam.cluster.x-k8s.io_ipaddressclaims.yaml b/config/crd/bases/ipam.cluster.x-k8s.io_ipaddressclaims.yaml
new file mode 100644
index 000000000..5bae75497
--- /dev/null
+++ b/config/crd/bases/ipam.cluster.x-k8s.io_ipaddressclaims.yaml
@@ -0,0 +1,137 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.1
+  creationTimestamp: null
+  name: ipaddressclaims.ipam.cluster.x-k8s.io
+spec:
+  group: ipam.cluster.x-k8s.io
+  names:
+    categories:
+    - cluster-api
+    kind: IPAddressClaim
+    listKind: IPAddressClaimList
+    plural: ipaddressclaims
+    singular: ipaddressclaim
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: Name of the pool to allocate an address from
+      jsonPath: .spec.poolRef.name
+      name: Pool Name
+      type: string
+    - description: Kind of the pool to allocate an address from
+      jsonPath: .spec.poolRef.kind
+      name: Pool Kind
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: IPAddressClaim is the Schema for the ipaddressclaim API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: IPAddressClaimSpec is the desired state of an IPAddressClaim.
+            properties:
+              poolRef:
+                description: PoolRef is a reference to the pool from which an IP address
+                  should be created.
+                properties:
+                  apiGroup:
+                    description: APIGroup is the group for the resource being referenced.
+                      If APIGroup is not specified, the specified Kind must be in
+                      the core API group. For any other third-party types, APIGroup
+                      is required.
+                    type: string
+                  kind:
+                    description: Kind is the type of resource being referenced
+                    type: string
+                  name:
+                    description: Name is the name of resource being referenced
+                    type: string
+                required:
+                - apiGroup
+                - kind
+                - name
+                type: object
+            required:
+            - poolRef
+            type: object
+          status:
+            description: IPAddressClaimStatus is the observed status of a IPAddressClaim.
+            properties:
+              addressRef:
+                description: AddressRef is a reference to the address that was created
+                  for this claim.
+                properties:
+                  name:
+                    description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+                      TODO: Add other useful fields. apiVersion, kind, uid?'
+                    type: string
+                type: object
+              conditions:
+                description: Conditions summarises the current state of the IPAddressClaim
+                items:
+                  description: Condition defines an observation of a Cluster API resource
+                    operational state.
+                  properties:
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status
+                        to another. This should be when the underlying condition changed.
+                        If that is not known, then using the time when the API field
+                        changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: A human readable message indicating details about
+                        the transition. This field may be empty.
+                      type: string
+                    reason:
+                      description: The reason for the condition's last transition
+                        in CamelCase. The specific API may choose whether or not this
+                        field is considered a guaranteed API. This field may not be
+                        empty.
+                      type: string
+                    severity:
+                      description: Severity provides an explicit classification of
+                        Reason code, so the users or machines can immediately understand
+                        the current situation and act accordingly. The Severity field
+                        MUST be set only when Status=False.
+                      type: string
+                    status:
+                      description: Status of the condition, one of True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type of condition in CamelCase or in foo.example.com/CamelCase.
+                        Many .condition.type values are consistent across resources
+                        like Available, but because arbitrary conditions can be useful
+                        (see .node.status.conditions), the ability to deconflict is
+                        important.
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - status
+                  - type
+                  type: object
+                type: array
+            required:
+            - addressRef
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/config/crd/bases/ipam.cluster.x-k8s.io_ipaddresses.yaml b/config/crd/bases/ipam.cluster.x-k8s.io_ipaddresses.yaml
new file mode 100644
index 000000000..7af8b5c20
--- /dev/null
+++ b/config/crd/bases/ipam.cluster.x-k8s.io_ipaddresses.yaml
@@ -0,0 +1,103 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.1
+  creationTimestamp: null
+  name: ipaddresses.ipam.cluster.x-k8s.io
+spec:
+  group: ipam.cluster.x-k8s.io
+  names:
+    categories:
+    - cluster-api
+    kind: IPAddress
+    listKind: IPAddressList
+    plural: ipaddresses
+    singular: ipaddress
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: Address
+      jsonPath: .spec.address
+      name: Address
+      type: string
+    - description: Name of the pool the address is from
+      jsonPath: .spec.poolRef.name
+      name: Pool Name
+      type: string
+    - description: Kind of the pool the address is from
+      jsonPath: .spec.poolRef.kind
+      name: Pool Kind
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: IPAddress is the Schema for the ipaddress API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: IPAddressSpec is the desired state of an IPAddress.
+            properties:
+              address:
+                description: Address is the IP address.
+                type: string
+              claimRef:
+                description: ClaimRef is a reference to the claim this IPAddress was
+                  created for.
+                properties:
+                  name:
+                    description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+                      TODO: Add other useful fields. apiVersion, kind, uid?'
+                    type: string
+                type: object
+              gateway:
+                description: Gateway is the network gateway of the network the address
+                  is from.
+                type: string
+              poolRef:
+                description: PoolRef is a reference to the pool that this IPAddress
+                  was created from.
+                properties:
+                  apiGroup:
+                    description: APIGroup is the group for the resource being referenced.
+                      If APIGroup is not specified, the specified Kind must be in
+                      the core API group. For any other third-party types, APIGroup
+                      is required.
+                    type: string
+                  kind:
+                    description: Kind is the type of resource being referenced
+                    type: string
+                  name:
+                    description: Name is the name of resource being referenced
+                    type: string
+                required:
+                - apiGroup
+                - kind
+                - name
+                type: object
+              prefix:
+                description: Prefix is the prefix of the address.
+                type: integer
+            required:
+            - address
+            - claimRef
+            - gateway
+            - poolRef
+            - prefix
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 513cdffe8..0ea28e7ba 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -12,6 +12,8 @@ resources:
 - bases/addons.cluster.x-k8s.io_clusterresourcesetbindings.yaml
 - bases/cluster.x-k8s.io_machinehealthchecks.yaml
 - bases/runtime.cluster.x-k8s.io_extensionconfigs.yaml
+- bases/ipam.cluster.x-k8s.io_ipaddresses.yaml
+- bases/ipam.cluster.x-k8s.io_ipaddressclaims.yaml
 # +kubebuilder:scaffold:crdkustomizeresource
 
 patchesStrategicMerge:
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 62ccec4f1..9a2a8ca3b 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -290,6 +290,14 @@ rules:
   - list
   - patch
   - watch
+- apiGroups:
+  - ipam.cluster.x-k8s.io
+  resources:
+  - ipaddressclaims
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - runtime.cluster.x-k8s.io
   resources:
diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index e5ab42bdf..f38f2a1e6 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -410,3 +410,49 @@ webhooks:
     resources:
     - clusterresourcesets
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  - v1beta1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-ipam-cluster-x-k8s-io-v1alpha1-ipaddress
+  failurePolicy: Fail
+  matchPolicy: Equivalent
+  name: validation.ipaddress.ipam.cluster.x-k8s.io
+  rules:
+  - apiGroups:
+    - ipam.cluster.x-k8s.io
+    apiVersions:
+    - v1alpha1
+    operations:
+    - CREATE
+    - UPDATE
+    - DELETE
+    resources:
+    - ipaddresses
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  - v1beta1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-ipam-cluster-x-k8s-io-v1alpha1-ipaddressclaim
+  failurePolicy: Fail
+  matchPolicy: Equivalent
+  name: validation.ipaddressclaim.ipam.cluster.x-k8s.io
+  rules:
+  - apiGroups:
+    - ipam.cluster.x-k8s.io
+    apiVersions:
+    - v1alpha1
+    operations:
+    - CREATE
+    - UPDATE
+    - DELETE
+    resources:
+    - ipaddressclaims
+  sideEffects: None
diff --git a/docs/book/Makefile b/docs/book/Makefile
index afcb67b9e..7a8314093 100644
--- a/docs/book/Makefile
+++ b/docs/book/Makefile
@@ -38,19 +38,10 @@ MDBOOK := $(TOOLS_BIN_DIR)/mdbook
 $(MDBOOK):
 	$(CRATE_INSTALL) --git rust-lang/mdBook --tag v0.4.11 --to $(TOOLS_BIN_DIR) --force
 
-RUNTIME_OPENAPI_GEN_BIN := runtime-openapi-gen
-RUNTIME_OPENAPI_GEN := $(abspath $(TOOLS_BIN_DIR)/$(RUNTIME_OPENAPI_GEN_BIN))
-
-runtimesdk-yaml:
-	$(MAKE) -C $(ROOT_DIR)/../../ $(RUNTIME_OPENAPI_GEN_BIN)
-	$(RUNTIME_OPENAPI_GEN) --version book --output-file ./src/tasks/experimental-features/runtime-sdk/runtime-sdk-openapi.yaml
-
-BOOK_DEPS := runtimesdk-yaml
-
 .PHONY: serve
-serve: $(MDBOOK) $(TABULATE) $(EMBED) $(RELEASELINK) runtimesdk-yaml
+serve: $(MDBOOK) $(TABULATE) $(EMBED) $(RELEASELINK)
 	$(MDBOOK) serve
 
 .PHONY: build
-build: $(MDBOOK) $(TABULATE) $(EMBED) $(RELEASELINK) runtimesdk-yaml
+build: $(MDBOOK) $(TABULATE) $(EMBED) $(RELEASELINK)
 	$(MDBOOK) build
diff --git a/docs/book/src/clusterctl/commands/alpha-topology-plan.md b/docs/book/src/clusterctl/commands/alpha-topology-plan.md
index 3f95b01f4..f991bd255 100644
--- a/docs/book/src/clusterctl/commands/alpha-topology-plan.md
+++ b/docs/book/src/clusterctl/commands/alpha-topology-plan.md
@@ -24,7 +24,7 @@ the input should have all the objects needed.
 
 <aside class="note">
 
-<h1>Limitations: Server Side Apply</h1>
+<h1>Limitations</h1>
 
 The topology controllers uses [Server Side Apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/)
 to support use cases where other controllers are co-authoring the same objects, but this kind of interactions can't be recreated
@@ -43,14 +43,6 @@ More specifically:
 
 </aside>
 
-<aside class="note">
-
-<h1>Limitations: RuntimeSDK</h1>
-
-Please note that `clusterctl` doesn't support Runtime SDK yet. This means that ClusterClasses with external patches are not yet supported.
-
-</aside>
-
 ## Example use cases
  
 ### Designing a new ClusterClass
diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/.gitignore b/docs/book/src/tasks/experimental-features/runtime-sdk/.gitignore
deleted file mode 100644
index 1b4dc5bb6..000000000
--- a/docs/book/src/tasks/experimental-features/runtime-sdk/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# Generated via make
-runtime-sdk-openapi.yaml
diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md
index abb3b5d3f..adede3e2d 100644
--- a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md
+++ b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md
@@ -67,7 +67,7 @@ message: "error message if status == Failure"
 retryAfterSeconds: 10
 ```
 
-For additional details, you can see the full schema in <button onclick="openSwaggerUI()">Swagger UI</button>.
+For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml).
 
 (*) The objects which are part of a Cluster topology are the infrastructure Cluster, the Control Plane, the 
 MachineDeployments and the templates derived from the ClusterClass.
@@ -104,7 +104,7 @@ status: Success # or Failure
 message: "error message if status == Failure"
 ```
 
-For additional details, you can see the full schema in <button onclick="openSwaggerUI()">Swagger UI</button>.
+For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml).
 
 ###  BeforeClusterUpgrade
 
@@ -141,7 +141,7 @@ message: "error message if status == Failure"
 retryAfterSeconds: 10
 ```
 
-For additional details, you can see the full schema in <button onclick="openSwaggerUI()">Swagger UI</button>.
+For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml).
 
 (*) Under normal circumstances `spec.topology.version` gets propagated to the control plane immediately; however 
   if previous upgrades or worker machine rollouts are still in progress, the system waits for those operations 
@@ -182,7 +182,7 @@ message: "error message if status == Failure"
 retryAfterSeconds: 10
 ```
 
-For additional details, you can see the full schema in <button onclick="openSwaggerUI()">Swagger UI</button>.
+For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml).
 
 ###  AfterClusterUpgrade
 
@@ -252,12 +252,4 @@ message: "error message if status == Failure"
 retryAfterSeconds: 10
 ```
 
-For additional details, you can see the full schema in <button onclick="openSwaggerUI()">Swagger UI</button>.
-
-<script>
-// openSwaggerUI calculates the absolute URL of the RuntimeSDK YAML file and opens Swagger UI.
-function openSwaggerUI() {
-  var schemaURL = new URL("runtime-sdk-openapi.yaml", document.baseURI).href
-  window.open("https://editor.swagger.io/?url=" + schemaURL)
-}
-</script>
+For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml).
diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md
index 22c382098..ce73a29c1 100644
--- a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md
+++ b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md
@@ -125,7 +125,7 @@ items:
   patch: <JSON-patch>
 ```
 
-For additional details, you can see the full schema in <button onclick="openSwaggerUI()">Swagger UI</button>.
+For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/topology-mutation-hook/runtime-sdk-openapi.yaml).
 
 We are considering to introduce a library to facilitate development of External Patch Extensions. It would provide capabilities like:
 * Accessing builtin variables
@@ -179,12 +179,4 @@ status: Success # or Failure
 message: "error message if status == Failure"
 ```
 
-For additional details, you can see the full schema in <button onclick="openSwaggerUI()">Swagger UI</button>.
-
-<script>
-// openSwaggerUI calculates the absolute URL of the RuntimeSDK YAML file and opens Swagger UI.
-function openSwaggerUI() {
-  var schemaURL = new URL("runtime-sdk-openapi.yaml", document.baseURI).href
-  window.open("https://editor.swagger.io/?url=" + schemaURL)
-}
-</script>
+For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/topology-mutation-hook/runtime-sdk-openapi.yaml).
diff --git a/docs/developer/releasing.md b/docs/developer/releasing.md
index 0d5dd3cda..a73b1b14b 100644
--- a/docs/developer/releasing.md
+++ b/docs/developer/releasing.md
@@ -21,9 +21,6 @@ Images are built by the [post push images job](https://testgrid.k8s.io/sig-clust
    - `export GITHUB_TOKEN=<your GH token>`
    - `make promote-images`
 
-*Note*: `kpromo` uses `git@github.com:...` as remote to push the branch for the PR. If you don't have `ssh` set up you can configure 
-        git to use `https` instead via `git config --global url."https://github.com/".insteadOf git@github.com:`.
-
 This will automatically create a PR in [k8s.io](https://github.com/kubernetes/k8s.io) and assign the CAPI maintainers.
 
 ## Release in GitHub
@@ -31,11 +28,6 @@ This will automatically create a PR in [k8s.io](https://github.com/kubernetes/k8
 1. Review the draft release on GitHub. Pay close attention to the `## :question: Sort these by hand` section, as it contains items that need to be manually sorted.
 1. Publish the release
 
-## Homebrew
-
-1. Publish `clusterctl` to Homebrew by bumping the version in [clusterctl.rb](https://github.com/Homebrew/homebrew-core/blob/master/Formula/clusterctl.rb).
-   For an example please see: [PR: clusterctl 1.1.5](https://github.com/Homebrew/homebrew-core/pull/105075/files).
-
 ### Versioning
 
 See the [versioning documentation](./../../CONTRIBUTING.md#versioning) for more information.
diff --git a/exp/api/v1beta1/doc.go b/exp/api/v1beta1/doc.go
index d55ab5609..b5028b59f 100644
--- a/exp/api/v1beta1/doc.go
+++ b/exp/api/v1beta1/doc.go
@@ -14,4 +14,5 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+// Package v1beta1 contains experimental v1beta1 API implementation.
 package v1beta1
diff --git a/exp/ipam/api/v1alpha1/doc.go b/exp/ipam/api/v1alpha1/doc.go
new file mode 100644
index 000000000..7c7deb4b1
--- /dev/null
+++ b/exp/ipam/api/v1alpha1/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 v1alpha1 contains API Schema definitions for the exp v1alpha1 IPAM API.
+package v1alpha1
diff --git a/exp/ipam/api/v1alpha1/groupversion_info.go b/exp/ipam/api/v1alpha1/groupversion_info.go
new file mode 100644
index 000000000..7be93c0c9
--- /dev/null
+++ b/exp/ipam/api/v1alpha1/groupversion_info.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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.
+*/
+
+// +kubebuilder:object:generate=true
+// +groupName=ipam.cluster.x-k8s.io
+
+package v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+	// GroupVersion is group version used to register these objects.
+	GroupVersion = schema.GroupVersion{Group: "ipam.cluster.x-k8s.io", Version: "v1alpha1"}
+
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+	// AddToScheme adds the types in this group-version to the given scheme.
+	AddToScheme = SchemeBuilder.AddToScheme
+)
diff --git a/exp/ipam/api/v1alpha1/ipaddress_types.go b/exp/ipam/api/v1alpha1/ipaddress_types.go
new file mode 100644
index 000000000..145855527
--- /dev/null
+++ b/exp/ipam/api/v1alpha1/ipaddress_types.go
@@ -0,0 +1,68 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 v1alpha1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// IPAddressSpec is the desired state of an IPAddress.
+type IPAddressSpec struct {
+	// ClaimRef is a reference to the claim this IPAddress was created for.
+	ClaimRef corev1.LocalObjectReference `json:"claimRef"`
+
+	// PoolRef is a reference to the pool that this IPAddress was created from.
+	PoolRef corev1.TypedLocalObjectReference `json:"poolRef"`
+
+	// Address is the IP address.
+	Address string `json:"address"`
+
+	// Prefix is the prefix of the address.
+	Prefix int `json:"prefix"`
+
+	// Gateway is the network gateway of the network the address is from.
+	Gateway string `json:"gateway"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:path=ipaddresses,scope=Namespaced,categories=cluster-api
+// +kubebuilder:storageversion
+// +kubebuilder:printcolumn:name="Address",type="string",JSONPath=".spec.address",description="Address"
+// +kubebuilder:printcolumn:name="Pool Name",type="string",JSONPath=".spec.poolRef.name",description="Name of the pool the address is from"
+// +kubebuilder:printcolumn:name="Pool Kind",type="string",JSONPath=".spec.poolRef.kind",description="Kind of the pool the address is from"
+
+// IPAddress is the Schema for the ipaddress API.
+type IPAddress struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec IPAddressSpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// IPAddressList is a list of IPAddress.
+type IPAddressList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []IPAddress `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&IPAddress{}, &IPAddressList{})
+}
diff --git a/exp/ipam/api/v1alpha1/ipaddressclaim_types.go b/exp/ipam/api/v1alpha1/ipaddressclaim_types.go
new file mode 100644
index 000000000..864b9653b
--- /dev/null
+++ b/exp/ipam/api/v1alpha1/ipaddressclaim_types.go
@@ -0,0 +1,69 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 v1alpha1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+)
+
+// IPAddressClaimSpec is the desired state of an IPAddressClaim.
+type IPAddressClaimSpec struct {
+	// PoolRef is a reference to the pool from which an IP address should be created.
+	PoolRef corev1.TypedLocalObjectReference `json:"poolRef"`
+}
+
+// IPAddressClaimStatus is the observed status of a IPAddressClaim.
+type IPAddressClaimStatus struct {
+	// AddressRef is a reference to the address that was created for this claim.
+	AddressRef corev1.LocalObjectReference `json:"addressRef"`
+
+	// Conditions summarises the current state of the IPAddressClaim
+	// +optional
+	Conditions clusterv1.Conditions `json:"conditions,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:path=ipaddressclaims,scope=Namespaced,categories=cluster-api
+// +kubebuilder:subresource:status
+// +kubebuilder:storageversion
+// +kubebuilder:printcolumn:name="Pool Name",type="string",JSONPath=".spec.poolRef.name",description="Name of the pool to allocate an address from"
+// +kubebuilder:printcolumn:name="Pool Kind",type="string",JSONPath=".spec.poolRef.kind",description="Kind of the pool to allocate an address from"
+
+// IPAddressClaim is the Schema for the ipaddressclaim API.
+type IPAddressClaim struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   IPAddressClaimSpec   `json:"spec,omitempty"`
+	Status IPAddressClaimStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// IPAddressClaimList is a list of IPAddressClaims.
+type IPAddressClaimList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []IPAddressClaim `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&IPAddressClaim{}, &IPAddressClaimList{})
+}
diff --git a/exp/ipam/api/v1alpha1/zz_generated.deepcopy.go b/exp/ipam/api/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 000000000..88cc359b9
--- /dev/null
+++ b/exp/ipam/api/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,200 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/cluster-api/api/v1beta1"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IPAddress) DeepCopyInto(out *IPAddress) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddress.
+func (in *IPAddress) DeepCopy() *IPAddress {
+	if in == nil {
+		return nil
+	}
+	out := new(IPAddress)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *IPAddress) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IPAddressClaim) DeepCopyInto(out *IPAddressClaim) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaim.
+func (in *IPAddressClaim) DeepCopy() *IPAddressClaim {
+	if in == nil {
+		return nil
+	}
+	out := new(IPAddressClaim)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *IPAddressClaim) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IPAddressClaimList) DeepCopyInto(out *IPAddressClaimList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]IPAddressClaim, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaimList.
+func (in *IPAddressClaimList) DeepCopy() *IPAddressClaimList {
+	if in == nil {
+		return nil
+	}
+	out := new(IPAddressClaimList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *IPAddressClaimList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IPAddressClaimSpec) DeepCopyInto(out *IPAddressClaimSpec) {
+	*out = *in
+	in.PoolRef.DeepCopyInto(&out.PoolRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaimSpec.
+func (in *IPAddressClaimSpec) DeepCopy() *IPAddressClaimSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(IPAddressClaimSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IPAddressClaimStatus) DeepCopyInto(out *IPAddressClaimStatus) {
+	*out = *in
+	out.AddressRef = in.AddressRef
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make(v1beta1.Conditions, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressClaimStatus.
+func (in *IPAddressClaimStatus) DeepCopy() *IPAddressClaimStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(IPAddressClaimStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IPAddressList) DeepCopyInto(out *IPAddressList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]IPAddress, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressList.
+func (in *IPAddressList) DeepCopy() *IPAddressList {
+	if in == nil {
+		return nil
+	}
+	out := new(IPAddressList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *IPAddressList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IPAddressSpec) DeepCopyInto(out *IPAddressSpec) {
+	*out = *in
+	out.ClaimRef = in.ClaimRef
+	in.PoolRef.DeepCopyInto(&out.PoolRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAddressSpec.
+func (in *IPAddressSpec) DeepCopy() *IPAddressSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(IPAddressSpec)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/exp/ipam/internal/webhooks/doc.go b/exp/ipam/internal/webhooks/doc.go
new file mode 100644
index 000000000..fded86ca9
--- /dev/null
+++ b/exp/ipam/internal/webhooks/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 webhooks implements experimental webhooks.
+package webhooks
diff --git a/exp/ipam/internal/webhooks/ipaddress.go b/exp/ipam/internal/webhooks/ipaddress.go
new file mode 100644
index 000000000..7ce3915cd
--- /dev/null
+++ b/exp/ipam/internal/webhooks/ipaddress.go
@@ -0,0 +1,170 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 webhooks
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	"github.com/pkg/errors"
+	"inet.af/netaddr"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+
+	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1alpha1"
+)
+
+// SetupWebhookWithManager sets up IPAddress webhooks.
+func (webhook *IPAddress) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&ipamv1.IPAddress{}).
+		WithValidator(webhook).
+		Complete()
+}
+
+// +kubebuilder:webhook:verbs=create;update;delete,path=/validate-ipam-cluster-x-k8s-io-v1alpha1-ipaddress,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=ipam.cluster.x-k8s.io,resources=ipaddresses,versions=v1alpha1,name=validation.ipaddress.ipam.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
+// +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddressclaims,verbs=get;list;watch
+
+// IPAddress implements a validating webhook for IPAddress.
+type IPAddress struct {
+	Client client.Reader
+}
+
+var _ webhook.CustomValidator = &IPAddress{}
+
+// ValidateCreate implements webhook.CustomValidator.
+func (webhook *IPAddress) ValidateCreate(ctx context.Context, obj runtime.Object) error {
+	ip, ok := obj.(*ipamv1.IPAddress)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an IPAddress but got a %T", obj))
+	}
+	return webhook.validate(ctx, ip)
+}
+
+// ValidateUpdate implements webhook.CustomValidator.
+func (webhook *IPAddress) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
+	oldIP, ok := oldObj.(*ipamv1.IPAddress)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an IPAddress but got a %T", oldObj))
+	}
+	newIP, ok := newObj.(*ipamv1.IPAddress)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an IPAddress but got a %T", newObj))
+	}
+
+	if !reflect.DeepEqual(oldIP.Spec, newIP.Spec) {
+		return field.Forbidden(field.NewPath("spec"), "the spec of IPAddress is immutable")
+	}
+	return nil
+}
+
+// ValidateDelete implements webhook.CustomValidator.
+func (webhook *IPAddress) ValidateDelete(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
+
+func (webhook *IPAddress) validate(ctx context.Context, ip *ipamv1.IPAddress) error {
+	log := ctrl.LoggerFrom(ctx)
+	allErrs := field.ErrorList{}
+	specPath := field.NewPath("spec")
+
+	addr, err := netaddr.ParseIP(ip.Spec.Address)
+	if err != nil {
+		allErrs = append(allErrs,
+			field.Invalid(
+				specPath.Child("address"),
+				ip.Spec.Address,
+				"not a valid IP address",
+			))
+	}
+
+	if ip.Spec.Prefix < 0 {
+		allErrs = append(allErrs,
+			field.Invalid(
+				specPath.Child("prefix"),
+				ip.Spec.Prefix,
+				"prefix cannot be negative",
+			))
+	}
+	if addr.Is4() && ip.Spec.Prefix > 32 {
+		allErrs = append(allErrs,
+			field.Invalid(
+				specPath.Child("prefix"),
+				ip.Spec.Prefix,
+				"prefix is too large for an IPv4 address",
+			))
+	}
+	if addr.Is6() && ip.Spec.Prefix > 128 {
+		allErrs = append(allErrs,
+			field.Invalid(
+				specPath.Child("prefix"),
+				ip.Spec.Prefix,
+				"prefix is too large for an IPv6 address",
+			))
+	}
+
+	_, err = netaddr.ParseIP(ip.Spec.Gateway)
+	if err != nil {
+		allErrs = append(allErrs,
+			field.Invalid(
+				specPath.Child("gateway"),
+				ip.Spec.Gateway,
+				"not a valid IP address",
+			))
+	}
+
+	if ip.Spec.PoolRef.APIGroup == nil {
+		allErrs = append(allErrs,
+			field.Invalid(
+				specPath.Child("poolRef.apiGroup"),
+				ip.Spec.PoolRef.APIGroup,
+				"the pool reference needs to contain a group"))
+	}
+
+	claim := &ipamv1.IPAddressClaim{}
+	err = webhook.Client.Get(ctx, types.NamespacedName{Name: ip.Spec.ClaimRef.Name, Namespace: ip.ObjectMeta.Namespace}, claim)
+	if err != nil && !apierrors.IsNotFound(err) {
+		log.Error(err, "failed to fetch claim", "name", ip.Spec.ClaimRef.Name)
+		allErrs = append(allErrs,
+			field.InternalError(
+				specPath.Child("claimRef"),
+				errors.Wrap(err, "failed to fetch claim"),
+			),
+		)
+	}
+
+	if claim.Name != "" && // only report non-matching pool if the claim exists
+		!(ip.Spec.PoolRef.APIGroup != nil && claim.Spec.PoolRef.APIGroup != nil &&
+			*ip.Spec.PoolRef.APIGroup == *claim.Spec.PoolRef.APIGroup &&
+			ip.Spec.PoolRef.Kind == claim.Spec.PoolRef.Kind &&
+			ip.Spec.PoolRef.Name == claim.Spec.PoolRef.Name) {
+		allErrs = append(allErrs,
+			field.Invalid(
+				specPath.Child("poolRef"),
+				ip.Spec.PoolRef,
+				"the referenced pool is different from the pool referenced by the claim this address should fulfill",
+			))
+	}
+
+	return allErrs.ToAggregate()
+}
diff --git a/exp/ipam/internal/webhooks/ipaddressclaim.go b/exp/ipam/internal/webhooks/ipaddressclaim.go
new file mode 100644
index 000000000..5daf69cc4
--- /dev/null
+++ b/exp/ipam/internal/webhooks/ipaddressclaim.go
@@ -0,0 +1,89 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 webhooks
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+
+	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1alpha1"
+)
+
+// SetupWebhookWithManager sets up IPAddressClaim webhooks.
+func (webhook *IPAddressClaim) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&ipamv1.IPAddressClaim{}).
+		WithValidator(webhook).
+		Complete()
+}
+
+// +kubebuilder:webhook:verbs=create;update;delete,path=/validate-ipam-cluster-x-k8s-io-v1alpha1-ipaddressclaim,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=ipam.cluster.x-k8s.io,resources=ipaddressclaims,versions=v1alpha1,name=validation.ipaddressclaim.ipam.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
+
+// IPAddressClaim implements a validating webhook for IPAddressClaim.
+type IPAddressClaim struct {
+}
+
+var _ webhook.CustomValidator = &IPAddressClaim{}
+
+// ValidateCreate implements webhook.CustomValidator.
+func (webhook *IPAddressClaim) ValidateCreate(ctx context.Context, obj runtime.Object) error {
+	claim, ok := obj.(*ipamv1.IPAddressClaim)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an IPAddressClaim but got a %T", obj))
+	}
+
+	if claim.Spec.PoolRef.APIGroup == nil {
+		return field.Invalid(
+			field.NewPath("spec.poolRef.apiGroup"),
+			claim.Spec.PoolRef.APIGroup,
+			"the pool reference needs to contain a group")
+	}
+
+	return nil
+}
+
+// ValidateUpdate implements webhook.CustomValidator.
+func (webhook *IPAddressClaim) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
+	oldClaim, ok := oldObj.(*ipamv1.IPAddressClaim)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an IPAddressClaim but got a %T", oldObj))
+	}
+	newClaim, ok := newObj.(*ipamv1.IPAddressClaim)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an IPAddressClaim but got a %T", newObj))
+	}
+
+	if !reflect.DeepEqual(oldClaim.Spec, newClaim.Spec) {
+		return field.Forbidden(
+			field.NewPath("spec"),
+			"the spec of IPAddressClaim is immutable",
+		)
+	}
+	return nil
+}
+
+// ValidateDelete implements webhook.CustomValidator.
+func (webhook *IPAddressClaim) ValidateDelete(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
diff --git a/exp/ipam/webhooks/alias.go b/exp/ipam/webhooks/alias.go
new file mode 100644
index 000000000..092347cf7
--- /dev/null
+++ b/exp/ipam/webhooks/alias.go
@@ -0,0 +1,45 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 webhooks
+
+import (
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"sigs.k8s.io/cluster-api/exp/ipam/internal/webhooks"
+)
+
+// IPAddress implements a validating and defaulting webhook for IPAddress.
+type IPAddress struct {
+	Client client.Reader
+}
+
+// SetupWebhookWithManager sets up IPAddress webhooks.
+func (webhook *IPAddress) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return (&webhooks.IPAddress{
+		Client: webhook.Client,
+	}).SetupWebhookWithManager(mgr)
+}
+
+// IPAddressClaim implements a validating and defaulting webhook for IPAddressClaim.
+type IPAddressClaim struct {
+}
+
+// SetupWebhookWithManager sets up IPAddressClaim webhooks.
+func (webhook *IPAddressClaim) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return (&webhooks.IPAddressClaim{}).SetupWebhookWithManager(mgr)
+}
diff --git a/exp/ipam/webhooks/doc.go b/exp/ipam/webhooks/doc.go
new file mode 100644
index 000000000..ff54398db
--- /dev/null
+++ b/exp/ipam/webhooks/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 webhooks contains external webhook implementations for some of our API types.
+package webhooks
diff --git a/go.mod b/go.mod
index f84cc9f25..e73b1ee7a 100644
--- a/go.mod
+++ b/go.mod
@@ -32,6 +32,7 @@ require (
 	golang.org/x/net v0.0.0-20220617184016-355a448f1bc9
 	golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb
 	google.golang.org/grpc v1.47.0
+	inet.af/netaddr v0.0.0-20220617031823-097006376321
 	k8s.io/api v0.24.2
 	k8s.io/apiextensions-apiserver v0.24.2
 	k8s.io/apimachinery v0.24.2
@@ -42,7 +43,7 @@ require (
 	k8s.io/klog/v2 v2.60.1
 	k8s.io/kubectl v0.24.0
 	k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
-	sigs.k8s.io/controller-runtime v0.12.3
+	sigs.k8s.io/controller-runtime v0.12.2
 	sigs.k8s.io/yaml v1.3.0
 )
 
@@ -85,7 +86,7 @@ require (
 	github.com/google/cel-go v0.10.1 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
-	github.com/google/uuid v1.1.2 // indirect
+	github.com/google/uuid v1.2.0 // indirect
 	github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
@@ -163,4 +164,6 @@ require (
 	github.com/google/gnostic v0.5.7-v3refs // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.1 // indirect
+	go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
+	go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
 )
diff --git a/go.sum b/go.sum
index 7e9099f96..22de76fcd 100644
--- a/go.sum
+++ b/go.sum
@@ -191,6 +191,7 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3
 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0=
 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -371,8 +372,9 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -782,6 +784,11 @@ go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
 go4.org v0.0.0-20160314031811-03efcb870d84/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
 go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8=
 go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
+go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
+go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -1339,6 +1346,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
+inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
 k8s.io/api v0.24.0/go.mod h1:5Jl90IUrJHUJYEMANRURMiVvJ0g7Ax7r3R1bqO8zx8I=
 k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI=
 k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg=
@@ -1382,8 +1391,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw=
-sigs.k8s.io/controller-runtime v0.12.3 h1:FCM8xeY/FI8hoAfh/V4XbbYMY20gElh9yh+A98usMio=
-sigs.k8s.io/controller-runtime v0.12.3/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0=
+sigs.k8s.io/controller-runtime v0.12.2 h1:nqV02cvhbAj7tbt21bpPpTByrXGn2INHRsi39lXy9sE=
+sigs.k8s.io/controller-runtime v0.12.2/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0=
 sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y=
 sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY=
 sigs.k8s.io/kustomize/api v0.11.4 h1:/0Mr3kfBBNcNPOW5Qwk/3eb8zkswCwnqQxxKtmrTkRo=
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index 40d3dde18..d8ef4cd6a 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -25,7 +25,7 @@ require (
 	k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
 	sigs.k8s.io/cluster-api v0.0.0-00010101000000-000000000000
 	sigs.k8s.io/cluster-api/test v0.0.0-00010101000000-000000000000
-	sigs.k8s.io/controller-runtime v0.12.3
+	sigs.k8s.io/controller-runtime v0.12.2
 	sigs.k8s.io/controller-tools v0.9.1
 	sigs.k8s.io/kubebuilder/docs/book/utils v0.0.0-20211028165026-57688c578b5d
 	sigs.k8s.io/yaml v1.3.0
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index 1689adcd8..ed17e56d2 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -190,6 +190,7 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3
 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0=
 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -767,7 +768,13 @@ go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
 go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
 go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
 go4.org v0.0.0-20160314031811-03efcb870d84/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8=
 go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
+go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
+go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -1332,6 +1339,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
+inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
 k8s.io/api v0.24.0/go.mod h1:5Jl90IUrJHUJYEMANRURMiVvJ0g7Ax7r3R1bqO8zx8I=
 k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI=
 k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg=
@@ -1375,8 +1384,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw=
-sigs.k8s.io/controller-runtime v0.12.3 h1:FCM8xeY/FI8hoAfh/V4XbbYMY20gElh9yh+A98usMio=
-sigs.k8s.io/controller-runtime v0.12.3/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0=
+sigs.k8s.io/controller-runtime v0.12.2 h1:nqV02cvhbAj7tbt21bpPpTByrXGn2INHRsi39lXy9sE=
+sigs.k8s.io/controller-runtime v0.12.2/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0=
 sigs.k8s.io/controller-tools v0.9.1 h1:/mnVp0o+DTwn503e55sq0vUa58DkE4JUQmJNTV9iq1Y=
 sigs.k8s.io/controller-tools v0.9.1/go.mod h1:NUkn8FTV3Sad3wWpSK7dt/145qfuQ8CKJV6j4jHC5rM=
 sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y=
diff --git a/hack/tools/tilt-prepare/main.go b/hack/tools/tilt-prepare/main.go
index 5c5eac29a..a242e1060 100644
--- a/hack/tools/tilt-prepare/main.go
+++ b/hack/tools/tilt-prepare/main.go
@@ -440,7 +440,7 @@ func kustomizeTask(path, out string) taskFunction {
 	}
 }
 
-// providerTask generates a task for creating the component yaml for a provider and saving the output on a file.
+// providerTask generates a task for creating the component yal for a provider and saving the output on a file.
 // NOTE: This task has several sub steps including running kustomize, envsubst, fixing components for debugging,
 // and adding the Provider resource mimicking what clusterctl init does.
 func providerTask(name, path string, ts *tiltSettings) taskFunction {
@@ -672,7 +672,7 @@ func getProviderObj(prefix string, objs []unstructured.Unstructured) (*unstructu
 		},
 		ProviderName: providerName,
 		Type:         providerType,
-		Version:      "v1.3.99",
+		Version:      "v1.2.99",
 	}
 
 	providerObj := &unstructured.Unstructured{}
diff --git a/main.go b/main.go
index bc90d0835..ba81125db 100644
--- a/main.go
+++ b/main.go
@@ -54,6 +54,8 @@ import (
 	expv1alpha4 "sigs.k8s.io/cluster-api/exp/api/v1alpha4"
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
 	expcontrollers "sigs.k8s.io/cluster-api/exp/controllers"
+	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1alpha1"
+	expipamwebhooks "sigs.k8s.io/cluster-api/exp/ipam/webhooks"
 	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
 	runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
 	runtimecontrollers "sigs.k8s.io/cluster-api/exp/runtime/controllers"
@@ -114,6 +116,8 @@ func init() {
 	_ = addonsv1.AddToScheme(scheme)
 
 	_ = runtimev1.AddToScheme(scheme)
+
+	_ = ipamv1.AddToScheme(scheme)
 	// +kubebuilder:scaffold:scheme
 
 	// Register the RuntimeHook types into the catalog.
@@ -519,6 +523,18 @@ func setupWebhooks(mgr ctrl.Manager) {
 		setupLog.Error(err, "unable to create webhook", "webhook", "ExtensionConfig")
 		os.Exit(1)
 	}
+
+	if err := (&expipamwebhooks.IPAddress{
+		// We are using GetAPIReader here to avoid caching all IPAddressClaims
+		Client: mgr.GetAPIReader(),
+	}).SetupWebhookWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create webhook", "webhook", "IPAddress")
+		os.Exit(1)
+	}
+	if err := (&expipamwebhooks.IPAddressClaim{}).SetupWebhookWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create webhook", "webhook", "IPAddressClaim")
+		os.Exit(1)
+	}
 }
 
 func concurrency(c int) controller.Options {
diff --git a/metadata.yaml b/metadata.yaml
index 61ee165dc..625544516 100644
--- a/metadata.yaml
+++ b/metadata.yaml
@@ -6,9 +6,6 @@
 apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
 kind: Metadata
 releaseSeries:
-  - major: 1
-    minor: 3
-    contract: v1beta1
   - major: 1
     minor: 2
     contract: v1beta1
diff --git a/netlify.toml b/netlify.toml
index db2191779..e021feac7 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -25,9 +25,3 @@
     to = "http://main.cluster-api.sigs.k8s.io/:splat"
     status = 301
     force = true
-
-[[headers]]
-    for = "/tasks/experimental-features/runtime-sdk/runtime-sdk-openapi.yaml"
-    [headers.values]
-        Access-Control-Allow-Origin = "*"
-        Access-Control-Allow-Methods = "*"


Test patch:
diff --git a/exp/ipam/internal/webhooks/ipaddress_test.go b/exp/ipam/internal/webhooks/ipaddress_test.go
new file mode 100644
index 000000000..5c8809fa2
--- /dev/null
+++ b/exp/ipam/internal/webhooks/ipaddress_test.go
@@ -0,0 +1,224 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 webhooks
+
+import (
+	"context"
+	"testing"
+
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/utils/pointer"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1alpha1"
+)
+
+func TestIPAddressValidateCreate(t *testing.T) {
+	claim := &ipamv1.IPAddressClaim{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: ipamv1.GroupVersion.String(),
+			Kind:       "IPAddressClaim",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "claim",
+			Namespace: "default",
+		},
+		Spec: ipamv1.IPAddressClaimSpec{
+			PoolRef: corev1.TypedLocalObjectReference{
+				Kind:     "TestPool",
+				Name:     "pool",
+				APIGroup: pointer.String("ipam.cluster.x-k8s.io"),
+			},
+		},
+	}
+
+	getAddress := func(v6 bool, fn func(addr *ipamv1.IPAddress)) ipamv1.IPAddress {
+		addr := ipamv1.IPAddress{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "default",
+			},
+			Spec: ipamv1.IPAddressSpec{
+				ClaimRef: corev1.LocalObjectReference{Name: claim.Name},
+				PoolRef:  claim.Spec.PoolRef,
+				Address:  "10.0.0.1",
+				Prefix:   24,
+				Gateway:  "10.0.0.254",
+			},
+		}
+		if v6 {
+			addr.Spec.Address = "42::1"
+			addr.Spec.Prefix = 64
+			addr.Spec.Gateway = "42::ffff"
+		}
+		fn(&addr)
+		return addr
+	}
+
+	tests := []struct {
+		name      string
+		ip        ipamv1.IPAddress
+		extraObjs []client.Object
+		expectErr bool
+	}{
+		{
+			name:      "a valid IPv4 Address should be accepted",
+			ip:        getAddress(false, func(addr *ipamv1.IPAddress) {}),
+			extraObjs: []client.Object{claim},
+			expectErr: false,
+		},
+		{
+			name:      "a valid IPv6 Address should be accepted",
+			ip:        getAddress(true, func(addr *ipamv1.IPAddress) {}),
+			extraObjs: []client.Object{claim},
+			expectErr: false,
+		},
+		{
+			name: "a prefix that is negative should be rejected",
+			ip: getAddress(false, func(addr *ipamv1.IPAddress) {
+				addr.Spec.Prefix = -1
+			}),
+			extraObjs: []client.Object{claim},
+			expectErr: true,
+		},
+		{
+			name: "a prefix that is too large for v4 should be rejected",
+			ip: getAddress(false, func(addr *ipamv1.IPAddress) {
+				addr.Spec.Prefix = 64
+			}),
+			extraObjs: []client.Object{claim},
+			expectErr: true,
+		},
+		{
+			name: "a prefix that is too large for v6 should be rejected",
+			ip: getAddress(true, func(addr *ipamv1.IPAddress) {
+				addr.Spec.Prefix = 256
+			}),
+			extraObjs: []client.Object{claim},
+			expectErr: true,
+		},
+		{
+			name: "an invalid address should be rejected",
+			ip: getAddress(false, func(addr *ipamv1.IPAddress) {
+				addr.Spec.Address = "42"
+			}),
+			extraObjs: []client.Object{claim},
+			expectErr: true,
+		},
+		{
+			name: "an invalid gateway should be rejected",
+			ip: getAddress(false, func(addr *ipamv1.IPAddress) {
+				addr.Spec.Gateway = "42"
+			}),
+			extraObjs: []client.Object{claim},
+			expectErr: true,
+		},
+		{
+			name: "a pool reference that does not match the claim should be rejected",
+			ip: getAddress(false, func(addr *ipamv1.IPAddress) {
+				addr.Spec.PoolRef.Name = "nothing"
+			}),
+			extraObjs: []client.Object{claim},
+			expectErr: true,
+		},
+		{
+			name: "a pool reference that does not contain a group should be rejected",
+			ip: getAddress(false, func(addr *ipamv1.IPAddress) {
+				addr.Spec.PoolRef.APIGroup = nil
+			}),
+			extraObjs: []client.Object{claim},
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+			scheme := runtime.NewScheme()
+			g.Expect(ipamv1.AddToScheme(scheme)).To(Succeed())
+			wh := IPAddress{
+				Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.extraObjs...).Build(),
+			}
+			if tt.expectErr {
+				g.Expect(wh.validate(context.Background(), &tt.ip)).NotTo(Succeed())
+			} else {
+				g.Expect(wh.validate(context.Background(), &tt.ip)).To(Succeed())
+			}
+		})
+	}
+}
+
+func TestIPAddressValidateUpdate(t *testing.T) {
+	getAddress := func(fn func(addr *ipamv1.IPAddress)) ipamv1.IPAddress {
+		addr := ipamv1.IPAddress{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "default",
+			},
+			Spec: ipamv1.IPAddressSpec{
+				ClaimRef: corev1.LocalObjectReference{},
+				PoolRef:  corev1.TypedLocalObjectReference{},
+				Address:  "10.0.0.1",
+				Prefix:   24,
+				Gateway:  "10.0.0.254",
+			},
+		}
+		fn(&addr)
+		return addr
+	}
+
+	tests := []struct {
+		name      string
+		oldIP     ipamv1.IPAddress
+		newIP     ipamv1.IPAddress
+		extraObjs []client.Object
+		expectErr bool
+	}{
+		{
+			name:      "should accept objects with identical spec",
+			oldIP:     getAddress(func(addr *ipamv1.IPAddress) {}),
+			newIP:     getAddress(func(addr *ipamv1.IPAddress) {}),
+			expectErr: false,
+		},
+		{
+			name:  "should reject objects with different spec",
+			oldIP: getAddress(func(addr *ipamv1.IPAddress) {}),
+			newIP: getAddress(func(addr *ipamv1.IPAddress) {
+				addr.Spec.Address = "10.0.0.2"
+			}),
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+			scheme := runtime.NewScheme()
+			g.Expect(ipamv1.AddToScheme(scheme)).To(Succeed())
+			wh := IPAddress{
+				Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.extraObjs...).Build(),
+			}
+			if tt.expectErr {
+				g.Expect(wh.ValidateUpdate(context.Background(), &tt.oldIP, &tt.newIP)).NotTo(Succeed())
+			} else {
+				g.Expect(wh.ValidateUpdate(context.Background(), &tt.oldIP, &tt.newIP)).To(Succeed())
+			}
+		})
+	}
+}
diff --git a/exp/ipam/internal/webhooks/ipaddressclaim_test.go b/exp/ipam/internal/webhooks/ipaddressclaim_test.go
new file mode 100644
index 000000000..2b0679788
--- /dev/null
+++ b/exp/ipam/internal/webhooks/ipaddressclaim_test.go
@@ -0,0 +1,123 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License 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 webhooks
+
+import (
+	"context"
+	"testing"
+
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/utils/pointer"
+
+	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1alpha1"
+)
+
+func TestIPAddressClaimValidateCreate(t *testing.T) {
+	getClaim := func(fn func(addr *ipamv1.IPAddressClaim)) ipamv1.IPAddressClaim {
+		claim := ipamv1.IPAddressClaim{
+			Spec: ipamv1.IPAddressClaimSpec{
+				PoolRef: corev1.TypedLocalObjectReference{
+					Name:     "identical",
+					Kind:     "TestPool",
+					APIGroup: pointer.String("ipam.cluster.x-k8s.io"),
+				},
+			},
+		}
+		fn(&claim)
+		return claim
+	}
+
+	tests := []struct {
+		name      string
+		claim     ipamv1.IPAddressClaim
+		expectErr bool
+	}{
+		{
+			name:      "should accept a valid claim",
+			claim:     getClaim(func(addr *ipamv1.IPAddressClaim) {}),
+			expectErr: false,
+		},
+		{
+			name: "should reject a pool reference without a group",
+			claim: getClaim(func(addr *ipamv1.IPAddressClaim) {
+				addr.Spec.PoolRef.APIGroup = nil
+			}),
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+			wh := IPAddressClaim{}
+			if tt.expectErr {
+				g.Expect(wh.ValidateCreate(context.Background(), &tt.claim)).NotTo(Succeed())
+			} else {
+				g.Expect(wh.ValidateCreate(context.Background(), &tt.claim)).To(Succeed())
+			}
+		})
+	}
+}
+
+func TestIPAddressClaimValidateUpdate(t *testing.T) {
+	getClaim := func(fn func(addr *ipamv1.IPAddressClaim)) ipamv1.IPAddressClaim {
+		claim := ipamv1.IPAddressClaim{
+			Spec: ipamv1.IPAddressClaimSpec{
+				PoolRef: corev1.TypedLocalObjectReference{
+					Name: "identical",
+				},
+			},
+		}
+		fn(&claim)
+		return claim
+	}
+
+	tests := []struct {
+		name      string
+		oldClaim  ipamv1.IPAddressClaim
+		newClaim  ipamv1.IPAddressClaim
+		expectErr bool
+	}{
+		{
+			name:      "should accept objects with identical spec",
+			oldClaim:  getClaim(func(addr *ipamv1.IPAddressClaim) {}),
+			newClaim:  getClaim(func(addr *ipamv1.IPAddressClaim) {}),
+			expectErr: false,
+		},
+		{
+			name:     "should reject objects with different spec",
+			oldClaim: getClaim(func(addr *ipamv1.IPAddressClaim) {}),
+			newClaim: getClaim(func(addr *ipamv1.IPAddressClaim) {
+				addr.Spec.PoolRef.Name = "different"
+			}),
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+			wh := IPAddressClaim{}
+			if tt.expectErr {
+				g.Expect(wh.ValidateUpdate(context.Background(), &tt.oldClaim, &tt.newClaim)).NotTo(Succeed())
+			} else {
+				g.Expect(wh.ValidateUpdate(context.Background(), &tt.oldClaim, &tt.newClaim)).To(Succeed())
+			}
+		})
+	}
+}
diff --git a/internal/test/envtest/environment.go b/internal/test/envtest/environment.go
index 42f9ade73..f0e4bf608 100644
--- a/internal/test/envtest/environment.go
+++ b/internal/test/envtest/environment.go
@@ -54,6 +54,8 @@ import (
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
 	addonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1"
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
+	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1alpha1"
+	expipamwebhooks "sigs.k8s.io/cluster-api/exp/ipam/webhooks"
 	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
 	"sigs.k8s.io/cluster-api/internal/test/builder"
 	runtimewebhooks "sigs.k8s.io/cluster-api/internal/webhooks/runtime"
@@ -82,6 +84,7 @@ func init() {
 	utilruntime.Must(controlplanev1.AddToScheme(scheme.Scheme))
 	utilruntime.Must(admissionv1.AddToScheme(scheme.Scheme))
 	utilruntime.Must(runtimev1.AddToScheme(scheme.Scheme))
+	utilruntime.Must(ipamv1.AddToScheme(scheme.Scheme))
 }
 
 // RunInput is the input for Run.
@@ -279,6 +282,12 @@ func newEnvironment(uncachedObjs ...client.Object) *Environment {
 	if err := (&runtimewebhooks.ExtensionConfig{}).SetupWebhookWithManager(mgr); err != nil {
 		klog.Fatalf("unable to create webhook for extensionconfig: %+v", err)
 	}
+	if err := (&expipamwebhooks.IPAddress{}).SetupWebhookWithManager(mgr); err != nil {
+		klog.Fatalf("unable to create webhook for ipaddress: %v", err)
+	}
+	if err := (&expipamwebhooks.IPAddressClaim{}).SetupWebhookWithManager(mgr); err != nil {
+		klog.Fatalf("unable to create webhook for ipaddressclaim: %v", err)
+	}
 
 	return &Environment{
 		Manager: mgr,
diff --git a/test/e2e/cluster_upgrade_runtimesdk.go b/test/e2e/cluster_upgrade_runtimesdk.go
index d0952d6c6..f65449c29 100644
--- a/test/e2e/cluster_upgrade_runtimesdk.go
+++ b/test/e2e/cluster_upgrade_runtimesdk.go
@@ -28,7 +28,6 @@ import (
 	. "github.com/onsi/gomega"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/utils/pointer"
@@ -126,7 +125,7 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
 		clusterResources = new(clusterctl.ApplyClusterTemplateAndWaitResult)
 	})
 
-	It("Should create, upgrade and delete a workload cluster", func() {
+	It("Should create and upgrade a workload cluster", func() {
 		clusterName := fmt.Sprintf("%s-%s", specName, util.RandomString(6))
 		By("Deploy Test Extension")
 		testExtensionDeploymentTemplate, err := os.ReadFile(testExtensionPath) //nolint:gosec
@@ -229,17 +228,11 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
 			WaitForNodesReady: input.E2EConfig.GetIntervals(specName, "wait-nodes-ready"),
 		})
 
-		By("Dumping resources and deleting the workload cluster")
-		dumpAndDeleteCluster(ctx, input.BootstrapClusterProxy, namespace.Name, clusterName, input.ArtifactFolder)
-
-		beforeClusterDeleteHandler(ctx, input.BootstrapClusterProxy.GetClient(), namespace.Name, clusterName, input.E2EConfig.GetIntervals(specName, "wait-delete-cluster"))
-
 		By("Checking all lifecycle hooks have been called")
 		// Assert that each hook has been called and returned "Success" during the test.
 		err = checkLifecycleHookResponses(ctx, input.BootstrapClusterProxy.GetClient(), namespace.Name, clusterName, map[string]string{
 			"BeforeClusterCreate":          "Success",
 			"BeforeClusterUpgrade":         "Success",
-			"BeforeClusterDelete":          "Success",
 			"AfterControlPlaneInitialized": "Success",
 			"AfterControlPlaneUpgrade":     "Success",
 			"AfterClusterUpgrade":          "Success",
@@ -307,7 +300,6 @@ func responsesConfigMap(name string, namespace *corev1.Namespace) *corev1.Config
 			"BeforeClusterCreate-preloadedResponse":      fmt.Sprintf(`{"Status": "Failure", "Message": %q}`, hookFailedMessage),
 			"BeforeClusterUpgrade-preloadedResponse":     fmt.Sprintf(`{"Status": "Failure", "Message": %q}`, hookFailedMessage),
 			"AfterControlPlaneUpgrade-preloadedResponse": fmt.Sprintf(`{"Status": "Failure", "Message": %q}`, hookFailedMessage),
-			"BeforeClusterDelete-preloadedResponse":      fmt.Sprintf(`{"Status": "Failure", "Message": %q}`, hookFailedMessage),
 
 			// Non-blocking hooks are set to Status:Success.
 			"AfterControlPlaneInitialized-preloadedResponse": `{"Status": "Success"}`,
@@ -355,8 +347,9 @@ func getLifecycleHookResponsesFromConfigMap(ctx context.Context, c client.Client
 // beforeClusterCreateTestHandler calls runtimeHookTestHandler with a blockedCondition function which returns false if
 // the Cluster has entered ClusterPhaseProvisioned.
 func beforeClusterCreateTestHandler(ctx context.Context, c client.Client, namespace, clusterName string, intervals []interface{}) {
+	log.Logf("Blocking with BeforeClusterCreate hook")
 	hookName := "BeforeClusterCreate"
-	runtimeHookTestHandler(ctx, c, namespace, clusterName, hookName, true, func() bool {
+	runtimeHookTestHandler(ctx, c, namespace, clusterName, hookName, func() bool {
 		blocked := true
 		// This hook should block the Cluster from entering the "Provisioned" state.
 		cluster := &clusterv1.Cluster{}
@@ -378,8 +371,9 @@ func beforeClusterCreateTestHandler(ctx context.Context, c client.Client, namesp
 // beforeClusterUpgradeTestHandler calls runtimeHookTestHandler with a blocking function which returns false if the
 // Cluster has controlplanev1.RollingUpdateInProgressReason in its ReadyCondition.
 func beforeClusterUpgradeTestHandler(ctx context.Context, c client.Client, namespace, clusterName string, intervals []interface{}) {
+	log.Logf("Blocking with BeforeClusterUpgrade hook")
 	hookName := "BeforeClusterUpgrade"
-	runtimeHookTestHandler(ctx, c, namespace, clusterName, hookName, true, func() bool {
+	runtimeHookTestHandler(ctx, c, namespace, clusterName, hookName, func() bool {
 		var blocked = true
 
 		cluster := &clusterv1.Cluster{}
@@ -403,8 +397,9 @@ func beforeClusterUpgradeTestHandler(ctx context.Context, c client.Client, names
 // afterControlPlaneUpgradeTestHandler calls runtimeHookTestHandler with a blocking function which returns false if any
 // MachineDeployment in the Cluster has upgraded to the target Kubernetes version.
 func afterControlPlaneUpgradeTestHandler(ctx context.Context, c client.Client, namespace, clusterName, version string, intervals []interface{}) {
+	log.Logf("Blocking with AfterControlPlaneUpgrade hook")
 	hookName := "AfterControlPlaneUpgrade"
-	runtimeHookTestHandler(ctx, c, namespace, clusterName, hookName, true, func() bool {
+	runtimeHookTestHandler(ctx, c, namespace, clusterName, hookName, func() bool {
 		var blocked = true
 		cluster := &clusterv1.Cluster{}
 		Eventually(func() error {
@@ -434,32 +429,15 @@ func afterControlPlaneUpgradeTestHandler(ctx context.Context, c client.Client, n
 	}, intervals)
 }
 
-// beforeClusterDeleteHandler calls runtimeHookTestHandler with a blocking function which returns false if the Cluster
-// can not be found in the API server.
-func beforeClusterDeleteHandler(ctx context.Context, c client.Client, namespace, clusterName string, intervals []interface{}) {
-	hookName := "BeforeClusterDelete"
-	runtimeHookTestHandler(ctx, c, namespace, clusterName, hookName, false, func() bool {
-		var blocked = true
-
-		// If the Cluster is not found it has been deleted and the hook is unblocked.
-		if apierrors.IsNotFound(c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: clusterName}, &clusterv1.Cluster{})) {
-			blocked = false
-		}
-		return blocked
-	}, intervals)
-}
-
 // runtimeHookTestHandler runs a series of tests in sequence to check if the runtimeHook passed to it succeeds.
-//	1) Checks that the hook has been called at least once and, if withTopologyReconciledCondition is set, checks that the TopologyReconciled condition is a Failure.
+//	1) Checks that the hook has been called at least once the TopologyReconciled condition is a Failure.
 //	2) Check that the hook's blockingCondition is consistently true.
 //	- At this point the function sets the hook's response to be non-blocking.
 //	3) Check that the hook's blocking condition becomes false.
 // Note: runtimeHookTestHandler assumes that the hook passed to it is currently returning a blocking response.
 // Updating the response to be non-blocking happens inline in the function.
-func runtimeHookTestHandler(ctx context.Context, c client.Client, namespace, clusterName, hookName string, withTopologyReconciledCondition bool, blockingCondition func() bool, intervals []interface{}) {
-	log.Logf("Blocking with %s hook", hookName)
-
-	// Check that the LifecycleHook has been called at least once and - when required - that the TopologyReconciled condition is a Failure.
+func runtimeHookTestHandler(ctx context.Context, c client.Client, namespace, clusterName, hookName string, blockingCondition func() bool, intervals []interface{}) {
+	// Check that the LifecycleHook has been called at least once and the TopologyReconciled condition is a Failure.
 	Eventually(func() error {
 		if err := checkLifecycleHooksCalledAtLeastOnce(ctx, c, namespace, clusterName, []string{hookName}); err != nil {
 			return err
@@ -468,10 +446,7 @@ func runtimeHookTestHandler(ctx context.Context, c client.Client, namespace, clu
 		if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: clusterName}, cluster); err != nil {
 			return err
 		}
-
-		// Check for the existence of the condition if withTopologyReconciledCondition is true.
-		if withTopologyReconciledCondition &&
-			(conditions.GetReason(cluster, clusterv1.TopologyReconciledCondition) != clusterv1.TopologyReconcileFailedReason) {
+		if !(conditions.GetReason(cluster, clusterv1.TopologyReconciledCondition) == clusterv1.TopologyReconcileFailedReason) {
 			return errors.New("Condition not found on Cluster object")
 		}
 		return nil
@@ -509,27 +484,3 @@ func clusterConditionShowsHookFailed(cluster *clusterv1.Cluster, hookName string
 		strings.Contains(conditions.GetMessage(cluster, clusterv1.TopologyReconciledCondition), hookFailedMessage) &&
 		strings.Contains(conditions.GetMessage(cluster, clusterv1.TopologyReconciledCondition), hookName)
 }
-
-func dumpAndDeleteCluster(ctx context.Context, proxy framework.ClusterProxy, namespace, clusterName, artifactFolder string) {
-	By("Deleting the workload cluster")
-	cluster := &clusterv1.Cluster{}
-	Eventually(func() error {
-		return proxy.GetClient().Get(ctx, client.ObjectKey{Namespace: namespace, Name: clusterName}, cluster)
-	}).Should(Succeed())
-
-	// Dump all the logs from the workload cluster before deleting them.
-	proxy.CollectWorkloadClusterLogs(ctx, cluster.Namespace, cluster.Name, filepath.Join(artifactFolder, "clusters-beforeClusterDelete", cluster.Name))
-
-	// Dump all Cluster API related resources to artifacts before deleting them.
-	framework.DumpAllResources(ctx, framework.DumpAllResourcesInput{
-		Lister:    proxy.GetClient(),
-		Namespace: namespace,
-		LogPath:   filepath.Join(artifactFolder, "clusters-beforeClusterDelete", proxy.GetName(), "resources"),
-	})
-
-	By("Deleting the workload cluster")
-	framework.DeleteCluster(ctx, framework.DeleteClusterInput{
-		Deleter: proxy.GetClient(),
-		Cluster: cluster,
-	})
-}
diff --git a/test/e2e/config/docker.yaml b/test/e2e/config/docker.yaml
index b8a952e5f..c9ec0b0e3 100644
--- a/test/e2e/config/docker.yaml
+++ b/test/e2e/config/docker.yaml
@@ -49,8 +49,8 @@ providers:
       new: --metrics-addr=:8080
     files:
     - sourcePath: "../data/shared/v1alpha4/metadata.yaml"
-  - name: v1.2.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.2.0-beta.2/core-components.yaml"
+  - name: v1.1.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.1.0/core-components.yaml"
     type: "url"
     contract: v1beta1
     replacements:
@@ -58,7 +58,7 @@ providers:
       new: --metrics-addr=:8080
     files:
     - sourcePath: "../data/shared/v1beta1/metadata.yaml"
-  - name: v1.3.99 # next; use manifest from source files
+  - name: v1.2.99 # next; use manifest from source files
     value: ../../../config/default
     replacements:
     - old: --metrics-bind-addr=localhost:8080
@@ -87,8 +87,8 @@ providers:
       new: --metrics-addr=:8080
     files:
     - sourcePath: "../data/shared/v1alpha4/metadata.yaml"
-  - name: v1.2.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.2.0-beta.2/bootstrap-components.yaml"
+  - name: v1.1.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.1.0/bootstrap-components.yaml"
     type: "url"
     contract: v1beta1
     replacements:
@@ -96,7 +96,7 @@ providers:
       new: --metrics-addr=:8080
     files:
     - sourcePath: "../data/shared/v1beta1/metadata.yaml"
-  - name: v1.3.99 # next; use manifest from source files
+  - name: v1.2.99 # next; use manifest from source files
     value: ../../../bootstrap/kubeadm/config/default
     replacements:
     - old: --metrics-bind-addr=localhost:8080
@@ -125,8 +125,8 @@ providers:
       new: --metrics-addr=:8080
     files:
     - sourcePath: "../data/shared/v1alpha4/metadata.yaml"
-  - name: v1.2.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.2.0-beta.2/control-plane-components.yaml"
+  - name: v1.1.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.1.0/control-plane-components.yaml"
     type: "url"
     contract: v1beta1
     replacements:
@@ -134,7 +134,7 @@ providers:
       new: --metrics-addr=:8080
     files:
     - sourcePath: "../data/shared/v1beta1/metadata.yaml"
-  - name: v1.3.99 # next; use manifest from source files
+  - name: v1.2.99 # next; use manifest from source files
     value: ../../../controlplane/kubeadm/config/default
     replacements:
     - old: --metrics-bind-addr=localhost:8080
@@ -165,8 +165,8 @@ providers:
     files:
     - sourcePath: "../data/shared/v1alpha4/metadata.yaml"
     - sourcePath: "../data/infrastructure-docker/v1alpha4/cluster-template.yaml"
-  - name: v1.2.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.2.0-beta.2/infrastructure-components-development.yaml"
+  - name: v1.1.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.1.0/infrastructure-components-development.yaml"
     type: "url"
     contract: v1beta1
     replacements:
@@ -175,7 +175,7 @@ providers:
     files:
     - sourcePath: "../data/shared/v1beta1/metadata.yaml"
     - sourcePath: "../data/infrastructure-docker/v1beta1/cluster-template.yaml"
-  - name: v1.3.99 # next; use manifest from source files
+  - name: v1.2.99 # next; use manifest from source files
     value: ../../../test/infrastructure/docker/config/default
     replacements:
     - old: --metrics-bind-addr=localhost:8080
diff --git a/test/e2e/data/shared/v1beta1/metadata.yaml b/test/e2e/data/shared/v1beta1/metadata.yaml
index ff9cdc101..10afb3250 100644
--- a/test/e2e/data/shared/v1beta1/metadata.yaml
+++ b/test/e2e/data/shared/v1beta1/metadata.yaml
@@ -1,9 +1,6 @@
 apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
 kind: Metadata
 releaseSeries:
-  - major: 1
-    minor: 3
-    contract: v1beta1
   - major: 1
     minor: 2
     contract: v1beta1
diff --git a/test/extension/handlers/lifecycle/handlers.go b/test/extension/handlers/lifecycle/handlers.go
index 354190a7e..2a1f759bc 100644
--- a/test/extension/handlers/lifecycle/handlers.go
+++ b/test/extension/handlers/lifecycle/handlers.go
@@ -126,23 +126,6 @@ func (h *Handler) DoAfterClusterUpgrade(ctx context.Context, request *runtimehoo
 	}
 }
 
-// DoBeforeClusterDelete implements the BeforeClusterDelete hook.
-func (h *Handler) DoBeforeClusterDelete(ctx context.Context, request *runtimehooksv1.BeforeClusterDeleteRequest, response *runtimehooksv1.BeforeClusterDeleteResponse) {
-	log := ctrl.LoggerFrom(ctx)
-	log.Info("BeforeClusterDelete is called")
-	cluster := request.Cluster
-
-	if err := h.readResponseFromConfigMap(ctx, cluster.Name, cluster.Namespace, runtimehooksv1.BeforeClusterDelete, response); err != nil {
-		response.Status = runtimehooksv1.ResponseStatusFailure
-		response.Message = err.Error()
-		return
-	}
-	if err := h.recordCallInConfigMap(ctx, cluster.Name, cluster.Namespace, runtimehooksv1.BeforeClusterDelete, response); err != nil {
-		response.Status = runtimehooksv1.ResponseStatusFailure
-		response.Message = err.Error()
-	}
-}
-
 func (h *Handler) readResponseFromConfigMap(ctx context.Context, name, namespace string, hook runtimecatalog.Hook, response runtimehooksv1.ResponseObject) error {
 	hookName := runtimecatalog.HookName(hook)
 	configMap := &corev1.ConfigMap{}
diff --git a/test/extension/main.go b/test/extension/main.go
index 05664573a..e60b9d98e 100644
--- a/test/extension/main.go
+++ b/test/extension/main.go
@@ -155,7 +155,6 @@ func main() {
 		setupLog.Error(err, "error adding handler")
 		os.Exit(1)
 	}
-
 	if err := webhookServer.AddExtensionHandler(server.ExtensionHandler{
 		Hook:           runtimehooksv1.AfterControlPlaneInitialized,
 		Name:           "after-control-plane-initialized",
@@ -166,7 +165,6 @@ func main() {
 		setupLog.Error(err, "error adding handler")
 		os.Exit(1)
 	}
-
 	if err := webhookServer.AddExtensionHandler(server.ExtensionHandler{
 		Hook:           runtimehooksv1.BeforeClusterUpgrade,
 		Name:           "before-cluster-upgrade",
@@ -188,7 +186,6 @@ func main() {
 		setupLog.Error(err, "error adding handler")
 		os.Exit(1)
 	}
-
 	if err := webhookServer.AddExtensionHandler(server.ExtensionHandler{
 		Hook:           runtimehooksv1.AfterClusterUpgrade,
 		Name:           "after-cluster-upgrade",
@@ -200,18 +197,7 @@ func main() {
 		os.Exit(1)
 	}
 
-	if err := webhookServer.AddExtensionHandler(server.ExtensionHandler{
-		Hook:           runtimehooksv1.BeforeClusterDelete,
-		Name:           "before-cluster-delete",
-		HandlerFunc:    lifecycleHandler.DoBeforeClusterDelete,
-		TimeoutSeconds: pointer.Int32(5),
-		FailurePolicy:  toPtr(runtimehooksv1.FailurePolicyFail),
-	}); err != nil {
-		setupLog.Error(err, "error adding handler")
-		os.Exit(1)
-	}
-
-	setupLog.Info("starting RuntimeExtension", "version", version.Get().String())
+	setupLog.Info("Starting Runtime Extension server", "version", version.Get().String())
 	if err := webhookServer.Start(ctx); err != nil {
 		setupLog.Error(err, "error running webhook server")
 		os.Exit(1)
diff --git a/test/framework/kubernetesversions/data/debian_injection_script.envsubst.sh.tpl b/test/framework/kubernetesversions/data/debian_injection_script.envsubst.sh.tpl
index 7b5e66b21..481221229 100644
--- a/test/framework/kubernetesversions/data/debian_injection_script.envsubst.sh.tpl
+++ b/test/framework/kubernetesversions/data/debian_injection_script.envsubst.sh.tpl
@@ -104,7 +104,7 @@ if [[ "$${KUBERNETES_VERSION}" != "" ]]; then
   fi
   IMAGE_REGISTRY_PREFIX=registry.k8s.io
   # Kubernetes builds from 1.20 through 1.24 are tagged with k8s.gcr.io
-  if [[ "$${KUBERNETES_VERSION}" =~ ^v1\.(1[0-9]|2[0-4])[\.[0-9]+ ]]; then
+  if [[ "$${CI_VERSION}" =~ ^v1\.(1[0-9]|2[0-4])[\.[0-9]+ ]]; then
     IMAGE_REGISTRY_PREFIX=k8s.gcr.io
   fi
   for CI_CONTAINER in "$${CONTAINERS_TO_TEST[@]}"; do
diff --git a/test/go.mod b/test/go.mod
index e7e101b08..1bca77c61 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -24,7 +24,7 @@ require (
 	k8s.io/klog/v2 v2.60.1
 	k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
 	sigs.k8s.io/cluster-api v0.0.0-00010101000000-000000000000
-	sigs.k8s.io/controller-runtime v0.12.3
+	sigs.k8s.io/controller-runtime v0.12.2
 	sigs.k8s.io/kind v0.14.0
 	sigs.k8s.io/yaml v1.3.0
 )
diff --git a/test/go.sum b/test/go.sum
index 016fa8b0c..8fd250a85 100644
--- a/test/go.sum
+++ b/test/go.sum
@@ -190,6 +190,7 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3
 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0=
 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -760,7 +761,13 @@ go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
 go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
 go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
 go4.org v0.0.0-20160314031811-03efcb870d84/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8=
 go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
+go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
+go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -1317,6 +1324,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
+inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
 k8s.io/api v0.24.0/go.mod h1:5Jl90IUrJHUJYEMANRURMiVvJ0g7Ax7r3R1bqO8zx8I=
 k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI=
 k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg=
@@ -1358,8 +1367,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw=
-sigs.k8s.io/controller-runtime v0.12.3 h1:FCM8xeY/FI8hoAfh/V4XbbYMY20gElh9yh+A98usMio=
-sigs.k8s.io/controller-runtime v0.12.3/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0=
+sigs.k8s.io/controller-runtime v0.12.2 h1:nqV02cvhbAj7tbt21bpPpTByrXGn2INHRsi39lXy9sE=
+sigs.k8s.io/controller-runtime v0.12.2/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0=
 sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y=
 sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY=
 sigs.k8s.io/kind v0.14.0 h1:cNmI3jGBvp7UegEGbC5we8plDtCUmaNRL+bod7JoSCE=