Repository: knative/pkg
Pull request #2363 (2021-11-26 11:00:23)

Allow Unstructured callback from Defaulting Webhook
Issue #1140

**Proposed Changes**

Allow Defaulting webhook to register generic Unstructured callbacks so that implementers can create generic defaulting logic outside of the APIs package.

This will later be used in Eventing Kafka Broker.

<!-- Thanks for sending a pull request! -->

# Changes

<!-- 
Describe your changes here- ideally you can get that description straight from
your descriptive commit message(s)! 

- :gift: Add new feature
- :bug: Fix bug
- :broom: Update or clean up current behavior
- :wastebasket: Remove feature or internal logic
-->

- Allow Unstructured callback from Defaulting Webhook

<!--
In addition, categorize the changes you're making using the "/kind" Prow command, example:

/kind enhancement

Supported kinds are: api-change, bug, cleanup, deprecation, removal, documentation, enhancement, performance

-->

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->

<!-- Please include the 'why' behind your changes if no issue exists -->

**Release Note**

<!--
:page_facing_up: If this change has user-visible impact, write a release note in the block
below. Include the string "action required" if additional action is required of
users switching to the new release, for example in case of a breaking change.

Write as if you are speaking to users, not other Knative contributors. If this
change has no user-visible impact, no release-note is needed.
-->
```release-note
Allow Unstructured callback from Defaulting Webhook
```


knative-prow-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/2363#" title="Author self-approved">pierDipi</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **rhuss** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @rhuss` 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=knative%2Fpkg).

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

- **[OWNERS](https://github.com/knative/pkg/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":["rhuss"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/knative/pkg/pull/2363?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) Report
> Merging [#2363](https://codecov.io/gh/knative/pkg/pull/2363?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) (885e287) into [main](https://codecov.io/gh/knative/pkg/commit/b35fcddda935c8d7de1eb58c505f6a022e719bc9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) (b35fcdd) will **increase** coverage by `0.01%`.
> The diff coverage is `74.50%`.

[![Impacted file tree graph](https://codecov.io/gh/knative/pkg/pull/2363/graphs/tree.svg?width=650&height=150&src=pr&token=rlqpDnfxbO&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative)](https://codecov.io/gh/knative/pkg/pull/2363?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative)

```diff
@@            Coverage Diff             @@
##             main    #2363      +/-   ##
==========================================
+ Coverage   63.66%   63.68%   +0.01%     
==========================================
  Files         228      228              
  Lines        9854     9896      +42     
==========================================
+ Hits         6274     6302      +28     
- Misses       3305     3313       +8     
- Partials      275      281       +6     
```


| [Impacted Files](https://codecov.io/gh/knative/pkg/pull/2363?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative) | Coverage Δ | |
|---|---|---|
| [metrics/config.go](https://codecov.io/gh/knative/pkg/pull/2363/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-bWV0cmljcy9jb25maWcuZ28=) | `78.35% <60.00%> (+0.57%)` | :arrow_up: |
| [webhook/resourcesemantics/defaulting/controller.go](https://codecov.io/gh/knative/pkg/pull/2363/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-d2ViaG9vay9yZXNvdXJjZXNlbWFudGljcy9kZWZhdWx0aW5nL2NvbnRyb2xsZXIuZ28=) | `93.75% <75.00%> (-6.25%)` | :arrow_down: |
| [webhook/resourcesemantics/defaulting/defaulting.go](https://codecov.io/gh/knative/pkg/pull/2363/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-d2ViaG9vay9yZXNvdXJjZXNlbWFudGljcy9kZWZhdWx0aW5nL2RlZmF1bHRpbmcuZ28=) | `78.78% <76.31%> (-0.85%)` | :arrow_down: |
| [test/gcs/mock/mock.go](https://codecov.io/gh/knative/pkg/pull/2363/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative#diff-dGVzdC9nY3MvbW9jay9tb2NrLmdv) | `90.32% <0.00%> (-2.16%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/knative/pkg/pull/2363?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/knative/pkg/pull/2363?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative). Last update [608fc87...885e287](https://codecov.io/gh/knative/pkg/pull/2363?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative).

pierDipi: /cc @dprotaso @julz 
knative-prow-robot: @pierDipi: 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-knative-pkg-unit-tests | fd777cc18b740474b950b87703ea3d8c9f76da74 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/2363/pull-knative-pkg-unit-tests/1466830039218655232) | true | `/test pull-knative-pkg-unit-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/2363). [Your PR dashboard](https://gubernator.knative.dev/pr/pierDipi).

<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 -->
knative-prow-robot: @pierDipi: 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-knative-pkg-unit-tests | b2524a0474735c5d189cc3b0ecadbcca26f1f0af | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/2363/pull-knative-pkg-unit-tests/1466831626859188224) | true | `/test pull-knative-pkg-unit-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/2363). [Your PR dashboard](https://gubernator.knative.dev/pr/pierDipi).

<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 -->
pierDipi: ```
=== RUN   TestMetricsExport/OpenCensus
    e2e_test.go:194: Created exporter at localhost:12345
{"level":"info","ts":1638555035.7005816,"logger":"fallback","caller":"metrics/metrics_worker.go:76","msg":"Flushing the existing exporter before setting up the new exporter."}
{"level":"info","ts":1638555035.7940946,"logger":"fallback","caller":"metrics/opencensus_exporter.go:56","msg":"Created OpenCensus exporter with config:","config":{}}
{"level":"info","ts":1638555035.796918,"logger":"fallback","caller":"metrics/metrics_worker.go:91","msg":"Successfully updated the metrics exporter; old config: &{knative.dev/project testComponent prometheus 1000000000 <nil>  false 19090 0.0.0.0}; new config &{knative.dev/project testComponent opencensus 1000000000 <nil> localhost:12345 false 0 }"}
    e2e_test.go:232: Timeout reading input
    e2e_test.go:238: Unexpected OpenCensus exports (-want +got):
          []metrics.metricExtract(Inverse(Sort, []string{
        - 	"knative.dev/project/testComponent/global_export_counts<>:2",
        - 	"knative.dev/project/testComponent/resource_global_export_count<>:2",
```
https://github.com/knative/pkg/issues/1672
/retest
pierDipi: ```
2021/12/03 19:01:22 http: TLS handshake error from 127.0.0.1:36974: EOF
2021/12/03 19:01:22 http: TLS handshake error from 127.0.0.1:37248: EOF
2021/12/03 19:01:23 http: TLS handshake error from 127.0.0.1:37972: EOF
2021/12/03 19:01:23 http: TLS handshake error from 127.0.0.1:49456: tls: no certificates configured
2021/12/03 19:01:23 http: TLS handshake error from 127.0.0.1:49454: EOF
--- FAIL: TestConversionEmptyRequestBody (0.21s)
    logger.go:130: 2021-12-03T19:01:23.406Z	INFO	webhook/webhook.go:200	Informers have been synced, unblocking admission webhooks.
    logger.go:130: 2021-12-03T19:01:23.512Z	ERROR	webhook/webhook.go:150	failed to fetch secret	{"error": "secret \"webhook-certs\" not found"}
    webhook_integration_test.go:142: failed to get resp Get "https://0.0.0.0:38147/bazinga": remote error: tls: unrecognized name
    logger.go:130: 2021-12-03T19:01:23.513Z	INFO	webhook/webhook.go:242	Starting to fail readiness probes...
2021/12/03 19:01:23 http: TLS handshake error from 127.0.0.1:49644: EOF
2021/12/03 19:01:23 http: TLS handshake error from 127.0.0.1:34188: EOF
2021/12/03 19:01:24 http: TLS handshake error from 127.0.0.1:41514: EOF
FAIL
```

Flake in GH action https://github.com/knative/pkg/issues/1509
pierDipi: @julz @markusthoemmes can you please review this PR since Dave is on vacation?
pierDipi: :pray: @dprotaso 
knative-prow-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/2363#" title="Author self-approved">pierDipi</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **tcnghia** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @tcnghia` 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=knative%2Fpkg).

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

- **[OWNERS](https://github.com/knative/pkg/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":["tcnghia"]} -->

Patch:
diff --git a/.github/workflows/knative-releasability.yaml b/.github/workflows/knative-releasability.yaml
index a9a2955a0..df686a891 100644
--- a/.github/workflows/knative-releasability.yaml
+++ b/.github/workflows/knative-releasability.yaml
@@ -38,9 +38,9 @@ jobs:
     env:
       #########################################
       #   Update this section each release.   #
-      RELEASE: 'v1.1'
-      MODULE_RELEASE: 'v0.28'
-      SLACK_CHANNEL: 'release-1dot1'
+      RELEASE: 'v1.2'
+      MODULE_RELEASE: 'v0.29'
+      SLACK_CHANNEL: 'release-1dot2'
       #########################################
 
     steps:
diff --git a/leaderelection/config.go b/leaderelection/config.go
index 0694395bf..f07c30cea 100644
--- a/leaderelection/config.go
+++ b/leaderelection/config.go
@@ -98,9 +98,9 @@ func (c *Config) GetComponentConfig(name string) ComponentConfig {
 func defaultConfig() *Config {
 	return &Config{
 		Buckets:       1,
-		LeaseDuration: 15 * time.Second,
-		RenewDeadline: 10 * time.Second,
-		RetryPeriod:   2 * time.Second,
+		LeaseDuration: 60 * time.Second,
+		RenewDeadline: 40 * time.Second,
+		RetryPeriod:   10 * time.Second,
 	}
 }
 
diff --git a/leaderelection/doc.go b/leaderelection/doc.go
new file mode 100644
index 000000000..fb7e162f7
--- /dev/null
+++ b/leaderelection/doc.go
@@ -0,0 +1,27 @@
+/*
+Copyright 2020 The Knative 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.
+*/
+
+// LeaderElection provides an interface for controllers implementing using
+// controller injection:
+// https://github.com/knative/pkg/blob/main/injection/README.md
+//
+// Leaderelection uses the context-stuffing mechanism to provide config-driven
+// management of multiple election strategies (currently, using Kubernetes
+// etcd-based election primitives or StatefulSet indexes and counts).
+//
+// For more details, see the original design document:
+// https://docs.google.com/document/d/e/2PACX-1vTh40N-Kk6EPNzYpITiLg8YJk0qZyZv7KgMpcQS72T9Lv_F2PQeGybx4TtH0E1N1aUgLQer7b8u3lDc/pub
+package leaderelection
diff --git a/webhook/resourcesemantics/defaulting/controller.go b/webhook/resourcesemantics/defaulting/controller.go
index a4a41d0be..b50fcf8f4 100644
--- a/webhook/resourcesemantics/defaulting/controller.go
+++ b/webhook/resourcesemantics/defaulting/controller.go
@@ -29,6 +29,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/tools/cache"
+
 	"knative.dev/pkg/controller"
 	"knative.dev/pkg/system"
 	"knative.dev/pkg/webhook"
@@ -42,6 +43,7 @@ func NewAdmissionController(
 	handlers map[schema.GroupVersionKind]resourcesemantics.GenericCRD,
 	wc func(context.Context) context.Context,
 	disallowUnknownFields bool,
+	callbacks ...map[schema.GroupVersionKind]Callback,
 ) *controller.Impl {
 
 	client := kubeclient.Get(ctx)
@@ -51,6 +53,19 @@ func NewAdmissionController(
 
 	key := types.NamespacedName{Name: name}
 
+	// This not ideal, we are using a variadic argument to effectively make callbacks optional
+	// This allows this addition to be non-breaking to consumers of /pkg
+	// TODO: once all sub-repos have adopted this, we might move this back to a traditional param.
+	var unwrappedCallbacks map[schema.GroupVersionKind]Callback
+	switch len(callbacks) {
+	case 0:
+		unwrappedCallbacks = map[schema.GroupVersionKind]Callback{}
+	case 1:
+		unwrappedCallbacks = callbacks[0]
+	default:
+		panic("NewAdmissionController may not be called with multiple callback maps")
+	}
+
 	wh := &reconciler{
 		LeaderAwareFuncs: pkgreconciler.LeaderAwareFuncs{
 			// Have this reconciler enqueue our singleton whenever it becomes leader.
@@ -60,9 +75,10 @@ func NewAdmissionController(
 			},
 		},
 
-		key:      key,
-		path:     path,
-		handlers: handlers,
+		key:       key,
+		path:      path,
+		handlers:  handlers,
+		callbacks: unwrappedCallbacks,
 
 		withContext:           wc,
 		disallowUnknownFields: disallowUnknownFields,
diff --git a/webhook/resourcesemantics/defaulting/defaulting.go b/webhook/resourcesemantics/defaulting/defaulting.go
index 504dcaa06..a8a30fae1 100644
--- a/webhook/resourcesemantics/defaulting/defaulting.go
+++ b/webhook/resourcesemantics/defaulting/defaulting.go
@@ -25,16 +25,18 @@ import (
 
 	"github.com/gobuffalo/flect"
 	"go.uber.org/zap"
-	jsonpatch "gomodules.xyz/jsonpatch/v2"
+	"gomodules.xyz/jsonpatch/v2"
 	admissionv1 "k8s.io/api/admission/v1"
 	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/kubernetes"
 	admissionlisters "k8s.io/client-go/listers/admissionregistration/v1"
 	corelisters "k8s.io/client-go/listers/core/v1"
+
 	"knative.dev/pkg/apis"
 	"knative.dev/pkg/apis/duck"
 	"knative.dev/pkg/controller"
@@ -56,9 +58,10 @@ type reconciler struct {
 	webhook.StatelessAdmissionImpl
 	pkgreconciler.LeaderAwareFuncs
 
-	key      types.NamespacedName
-	path     string
-	handlers map[schema.GroupVersionKind]resourcesemantics.GenericCRD
+	key       types.NamespacedName
+	path      string
+	handlers  map[schema.GroupVersionKind]resourcesemantics.GenericCRD
+	callbacks map[schema.GroupVersionKind]Callback
 
 	withContext func(context.Context) context.Context
 
@@ -70,6 +73,37 @@ type reconciler struct {
 	secretName            string
 }
 
+// CallbackFunc is the function to be invoked.
+type CallbackFunc func(ctx context.Context, unstructured *unstructured.Unstructured) error
+
+// Callback is a generic function to be called by a consumer of defaulting.
+type Callback struct {
+	// function is the callback to be invoked.
+	function CallbackFunc
+
+	// supportedVerbs are the verbs supported for the callback.
+	// The function will only be called on these actions.
+	supportedVerbs map[webhook.Operation]struct{}
+}
+
+// NewCallback creates a new callback function to be invoked on supported verbs.
+func NewCallback(function func(context.Context, *unstructured.Unstructured) error, supportedVerbs ...webhook.Operation) Callback {
+	if function == nil {
+		panic("expected function, got nil")
+	}
+	m := make(map[webhook.Operation]struct{})
+	for _, op := range supportedVerbs {
+		if op == webhook.Delete {
+			panic("Verb " + webhook.Delete + " not allowed")
+		}
+		if _, has := m[op]; has {
+			panic("duplicate verbs not allowed")
+		}
+		m[op] = struct{}{}
+	}
+	return Callback{function: function, supportedVerbs: m}
+}
+
 var _ controller.Reconciler = (*reconciler)(nil)
 var _ pkgreconciler.LeaderAware = (*reconciler)(nil)
 var _ webhook.AdmissionController = (*reconciler)(nil)
@@ -137,7 +171,17 @@ func (ac *reconciler) reconcileMutatingWebhook(ctx context.Context, caCert []byt
 	logger := logging.FromContext(ctx)
 
 	rules := make([]admissionregistrationv1.RuleWithOperations, 0, len(ac.handlers))
+	gvks := make(map[schema.GroupVersionKind]struct{}, len(ac.handlers)+len(ac.callbacks))
 	for gvk := range ac.handlers {
+		gvks[gvk] = struct{}{}
+	}
+	for gvk := range ac.callbacks {
+		if _, ok := gvks[gvk]; !ok {
+			gvks[gvk] = struct{}{}
+		}
+	}
+
+	for gvk := range gvks {
 		plural := strings.ToLower(flect.Pluralize(gvk.Kind))
 
 		rules = append(rules, admissionregistrationv1.RuleWithOperations{
@@ -231,8 +275,18 @@ func (ac *reconciler) mutate(ctx context.Context, req *admissionv1.AdmissionRequ
 	logger := logging.FromContext(ctx)
 	handler, ok := ac.handlers[gvk]
 	if !ok {
-		logger.Error("Unhandled kind: ", gvk)
-		return nil, fmt.Errorf("unhandled kind: %v", gvk)
+		if _, ok := ac.callbacks[gvk]; !ok {
+			logger.Error("Unhandled kind: ", gvk)
+			return nil, fmt.Errorf("unhandled kind: %v", gvk)
+		}
+		patches, err := ac.callback(ctx, gvk, req, true /* shouldSetUserInfo */, duck.JSONPatch{})
+		if err != nil {
+			logger.Errorw("Failed the callback defaulter", zap.Error(err))
+			// Return the error message as-is to give the defaulter callback
+			// discretion over (our portion of) the message that the user sees.
+			return nil, err
+		}
+		return json.Marshal(patches)
 	}
 
 	// nil values denote absence of `old` (create) or `new` (delete) objects.
@@ -302,6 +356,13 @@ func (ac *reconciler) mutate(ctx context.Context, req *admissionv1.AdmissionRequ
 		return nil, err
 	}
 
+	if patches, err = ac.callback(ctx, gvk, req, false /* shouldSetUserInfo */, patches); err != nil {
+		logger.Errorw("Failed the callback defaulter", zap.Error(err))
+		// Return the error message as-is to give the defaulter callback
+		// discretion over (our portion of) the message that the user sees.
+		return nil, err
+	}
+
 	// None of the validators will accept a nil value for newObj.
 	if newObj == nil {
 		return nil, errMissingNewObject
@@ -329,6 +390,57 @@ func (ac *reconciler) setUserInfoAnnotations(ctx context.Context, patches duck.J
 	return append(patches, patch...), nil
 }
 
+func (ac *reconciler) callback(ctx context.Context, gvk schema.GroupVersionKind, req *admissionv1.AdmissionRequest, shouldSetUserInfo bool, patches duck.JSONPatch) (duck.JSONPatch, error) {
+	// Get callback.
+	callback, ok := ac.callbacks[gvk]
+	if !ok {
+		return patches, nil
+	}
+
+	// Check if request operation is a supported webhook operation.
+	if _, isSupported := callback.supportedVerbs[req.Operation]; !isSupported {
+		return patches, nil
+	}
+
+	oldBytes := req.OldObject.Raw
+	newBytes := req.Object.Raw
+
+	before := &unstructured.Unstructured{}
+	after := &unstructured.Unstructured{}
+
+	// Get unstructured object.
+	if err := json.Unmarshal(newBytes, before); err != nil {
+		return nil, fmt.Errorf("cannot decode object: %w", err)
+	}
+	// Copy before in after unstructured objects.
+	before.DeepCopyInto(after)
+
+	// Setup context.
+	if len(oldBytes) != 0 {
+		if req.SubResource == "" {
+			ctx = apis.WithinUpdate(ctx, before)
+		} else {
+			ctx = apis.WithinSubResourceUpdate(ctx, before, req.SubResource)
+		}
+	} else {
+		ctx = apis.WithinCreate(ctx)
+	}
+	ctx = apis.WithUserInfo(ctx, &req.UserInfo)
+
+	// Call callback passing after.
+	if err := callback.function(ctx, after); err != nil {
+		return patches, err
+	}
+
+	if shouldSetUserInfo {
+		setUserInfoAnnotations(adaptUnstructuredHasSpecCtx(ctx, req), unstructuredHasSpec{after}, req.Resource.Group)
+	}
+
+	// Create patches.
+	patch, err := duck.CreatePatch(before.Object, after.Object)
+	return append(patches, patch...), err
+}
+
 // roundTripPatch generates the JSONPatch that corresponds to round tripping the given bytes through
 // the Golang type (JSON -> Golang type -> JSON). Because it is not always true that
 // bytes == json.Marshal(json.Unmarshal(bytes)).
diff --git a/webhook/resourcesemantics/defaulting/user_info.go b/webhook/resourcesemantics/defaulting/user_info.go
index 36c1cfbd2..fcd97a456 100644
--- a/webhook/resourcesemantics/defaulting/user_info.go
+++ b/webhook/resourcesemantics/defaulting/user_info.go
@@ -19,8 +19,11 @@ package defaulting
 import (
 	"context"
 
+	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/api/equality"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
 	"knative.dev/pkg/apis"
 )
 
@@ -48,5 +51,35 @@ func setUserInfoAnnotations(ctx context.Context, resource apis.HasSpec, groupNam
 			annotations[groupName+apis.CreatorAnnotationSuffix] = ui.Username
 			annotations[groupName+apis.UpdaterAnnotationSuffix] = ui.Username
 		}
+		objectMetaAccessor.GetObjectMeta().SetAnnotations(annotations)
+	}
+}
+
+type unstructuredHasSpec struct {
+	*unstructured.Unstructured
+}
+
+func (us unstructuredHasSpec) GetObjectMeta() metav1.Object {
+	return us.Unstructured
+}
+
+var _ metav1.ObjectMetaAccessor = unstructuredHasSpec{}
+
+func (us unstructuredHasSpec) GetUntypedSpec() interface{} {
+	if s, ok := us.Unstructured.Object["spec"]; ok {
+		return s
+	}
+	return nil
+}
+
+func adaptUnstructuredHasSpecCtx(ctx context.Context, req *admissionv1.AdmissionRequest) context.Context {
+	if apis.IsInUpdate(ctx) {
+		b := apis.GetBaseline(ctx)
+		if apis.IsInStatusUpdate(ctx) {
+			ctx = apis.WithinSubResourceUpdate(ctx, unstructuredHasSpec{b.(*unstructured.Unstructured)}, req.SubResource)
+		} else {
+			ctx = apis.WithinUpdate(ctx, unstructuredHasSpec{b.(*unstructured.Unstructured)})
+		}
 	}
+	return ctx
 }


Test patch:
diff --git a/apis/duck/v1/source_types_test.go b/apis/duck/v1/source_types_test.go
index a53519916..b1b036bc7 100644
--- a/apis/duck/v1/source_types_test.go
+++ b/apis/duck/v1/source_types_test.go
@@ -44,10 +44,7 @@ func TestSourceValidate(t *testing.T) {
 		name: "empty source ceOverrides extensions validation",
 		src: &Source{Spec: SourceSpec{
 			Sink: Destination{
-				URI: func() *apis.URL {
-					u, _ := apis.ParseURL("https://localhost")
-					return u
-				}(),
+				URI: apis.HTTP("localhost"),
 			},
 			CloudEventOverrides: &CloudEventOverrides{Extensions: map[string]string{}},
 		}},
@@ -56,10 +53,7 @@ func TestSourceValidate(t *testing.T) {
 		name: "empty extension name error",
 		src: &Source{Spec: SourceSpec{
 			Sink: Destination{
-				URI: func() *apis.URL {
-					u, _ := apis.ParseURL("https://localhost")
-					return u
-				}(),
+				URI: apis.HTTP("localhost"),
 			},
 			CloudEventOverrides: &CloudEventOverrides{Extensions: map[string]string{"": "test"}},
 		}},
@@ -72,10 +66,7 @@ func TestSourceValidate(t *testing.T) {
 		name: "long extension key name is valid",
 		src: &Source{Spec: SourceSpec{
 			Sink: Destination{
-				URI: func() *apis.URL {
-					u, _ := apis.ParseURL("https://localhost")
-					return u
-				}(),
+				URI: apis.HTTP("localhost"),
 			},
 			CloudEventOverrides: &CloudEventOverrides{
 				Extensions: map[string]string{"nameLongerThan20Characters": "test"},
@@ -86,10 +77,7 @@ func TestSourceValidate(t *testing.T) {
 		name: "invalid extension name",
 		src: &Source{Spec: SourceSpec{
 			Sink: Destination{
-				URI: func() *apis.URL {
-					u, _ := apis.ParseURL("https://localhost")
-					return u
-				}(),
+				URI: apis.HTTP("localhost"),
 			},
 			CloudEventOverrides: &CloudEventOverrides{Extensions: map[string]string{"invalid_name": "test"}},
 		}},
@@ -102,10 +90,7 @@ func TestSourceValidate(t *testing.T) {
 		name: "valid extension name",
 		src: &Source{Spec: SourceSpec{
 			Sink: Destination{
-				URI: func() *apis.URL {
-					u, _ := apis.ParseURL("https://localhost")
-					return u
-				}(),
+				URI: apis.HTTP("localhost"),
 			},
 			CloudEventOverrides: &CloudEventOverrides{
 				Extensions: map[string]string{"validName": "test"},
diff --git a/testing/resource.go b/testing/resource.go
index d562b7ce9..e26ca317e 100644
--- a/testing/resource.go
+++ b/testing/resource.go
@@ -23,6 +23,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+
 	"knative.dev/pkg/apis"
 )
 
@@ -43,12 +44,15 @@ var _ apis.Listable = (*Resource)(nil)
 
 // ResourceSpec represents test resource spec.
 type ResourceSpec struct {
-	FieldWithDefault               string `json:"fieldWithDefault,omitempty"`
-	FieldWithContextDefault        string `json:"fieldWithContextDefault,omitempty"`
-	FieldWithValidation            string `json:"fieldWithValidation,omitempty"`
-	FieldThatsImmutable            string `json:"fieldThatsImmutable,omitempty"`
-	FieldThatsImmutableWithDefault string `json:"fieldThatsImmutableWithDefault,omitempty"`
-	FieldForCallbackValidation     string `json:"fieldThatCallbackRejects,omitempty"`
+	FieldWithDefault                         string `json:"fieldWithDefault,omitempty"`
+	FieldWithContextDefault                  string `json:"fieldWithContextDefault,omitempty"`
+	FieldWithValidation                      string `json:"fieldWithValidation,omitempty"`
+	FieldThatsImmutable                      string `json:"fieldThatsImmutable,omitempty"`
+	FieldThatsImmutableWithDefault           string `json:"fieldThatsImmutableWithDefault,omitempty"`
+	FieldForCallbackValidation               string `json:"fieldThatCallbackRejects,omitempty"`
+	FieldForCallbackDefaulting               string `json:"fieldForCallbackDefaulting,omitempty"`
+	FieldForCallbackDefaultingIsWithinUpdate bool   `json:"fieldForCallbackDefaultingIsWithinUpdate,omitempty"`
+	FieldForCallbackDefaultingUsername       string `json:"fieldForCallbackDefaultingUsername,omitempty"`
 }
 
 // GetGroupVersionKind returns the GroupVersionKind.
@@ -56,6 +60,16 @@ func (r *Resource) GetGroupVersionKind() schema.GroupVersionKind {
 	return SchemeGroupVersion.WithKind("Resource")
 }
 
+// GetGroupVersionKindMeta returns the metav1.GroupVersionKind.
+func (r *Resource) GetGroupVersionKindMeta() metav1.GroupVersionKind {
+	gvk := r.GroupVersionKind()
+	return metav1.GroupVersionKind{
+		Group:   gvk.Group,
+		Version: gvk.Version,
+		Kind:    gvk.Kind,
+	}
+}
+
 // GetUntypedSpec returns the spec of the resource.
 func (r *Resource) GetUntypedSpec() interface{} {
 	return r.Spec
diff --git a/webhook/resourcesemantics/defaulting/defaulting_test.go b/webhook/resourcesemantics/defaulting/defaulting_test.go
index dd5230762..fc8ba71ec 100644
--- a/webhook/resourcesemantics/defaulting/defaulting_test.go
+++ b/webhook/resourcesemantics/defaulting/defaulting_test.go
@@ -19,14 +19,20 @@ package defaulting
 import (
 	"context"
 	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
 	"testing"
 
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+
 	// Injection stuff
 	_ "knative.dev/pkg/client/injection/kube/client/fake"
 	_ "knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/mutatingwebhookconfiguration/fake"
 	_ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake"
 
-	jsonpatch "gomodules.xyz/jsonpatch/v2"
+	"gomodules.xyz/jsonpatch/v2"
 	admissionv1 "k8s.io/api/admission/v1"
 	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
 	authenticationv1 "k8s.io/api/authentication/v1"
@@ -78,6 +84,29 @@ var (
 		}: &InnerDefaultResource{},
 	}
 
+	callbacks = map[schema.GroupVersionKind]Callback{
+		{
+			Group:   "pkg.knative.dev",
+			Version: "v1alpha1",
+			Kind:    "Resource",
+		}: NewCallback(resourceCallback, webhook.Create, webhook.Update),
+		{
+			Group:   "pkg.knative.dev",
+			Version: "v1beta1",
+			Kind:    "Resource",
+		}: NewCallback(resourceCallback, webhook.Create, webhook.Update),
+		{
+			Group:   "pkg.knative.dev",
+			Version: "v1beta1",
+			Kind:    "ResourceCallbackDefault",
+		}: NewCallback(resourceCallback, webhook.Create, webhook.Update),
+		{
+			Group:   "pkg.knative.dev",
+			Version: "v1beta1",
+			Kind:    "ResourceCallbackDefaultCreate",
+		}: NewCallback(resourceCallback, webhook.Create),
+	}
+
 	initialResourceWebhook = &admissionregistrationv1.MutatingWebhookConfiguration{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: "webhook.knative.dev",
@@ -218,10 +247,11 @@ func TestUnknownMetadataFieldSucceeds(t *testing.T) {
 
 func TestAdmitCreates(t *testing.T) {
 	tests := []struct {
-		name      string
-		setup     func(context.Context, *Resource)
-		rejection string
-		patches   []jsonpatch.JsonPatchOperation
+		name              string
+		setup             func(context.Context, *Resource)
+		rejection         string
+		patches           []jsonpatch.JsonPatchOperation
+		createRequestFunc func(ctx context.Context, t *testing.T, r *Resource) *admissionv1.AdmissionRequest
 	}{{
 		name: "test simple creation (alpha, no diff)",
 		setup: func(ctx context.Context, r *Resource) {
@@ -323,6 +353,72 @@ func TestAdmitCreates(t *testing.T) {
 			Path:      "/metadata/annotations/pkg.knative.dev~1creator",
 			Value:     user1,
 		}},
+	}, {
+		name: "test simple creation (callback return error)",
+		setup: func(ctx context.Context, resource *Resource) {
+			resource.Spec.FieldForCallbackDefaulting = "no magic value"
+		},
+		rejection: "no magic value",
+	}, {
+		name: "test simple creation (resource and callback defaults)",
+		setup: func(ctx context.Context, r *Resource) {
+			r.SetDefaults(ctx)
+			r.Spec.FieldForCallbackDefaulting = "magic value"
+			// THIS IS NOT WHO IS CREATING IT, IT IS LIES!
+			r.Annotations = map[string]string{
+				"pkg.knative.dev/lastModifier": user2,
+			}
+		},
+		patches: []jsonpatch.JsonPatchOperation{{
+			Operation: "replace",
+			Path:      "/spec/fieldForCallbackDefaulting",
+			Value:     "I'm a default",
+		}, {
+			Operation: "replace",
+			Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
+			Value:     user1,
+		}, {
+			Operation: "add",
+			Path:      "/metadata/annotations/pkg.knative.dev~1creator",
+			Value:     user1,
+		}, {
+			Operation: "add",
+			Path:      "/spec/fieldForCallbackDefaultingUsername",
+			Value:     user1,
+		}},
+	}, {
+		name: "test simple creation (only callback defaults)",
+		setup: func(ctx context.Context, r *Resource) {
+			r.TypeMeta.APIVersion = "pkg.knative.dev/v1beta1"
+			r.TypeMeta.Kind = "ResourceCallbackDefault"
+			r.Spec.FieldForCallbackDefaulting = "magic value"
+			// THIS IS NOT WHO IS CREATING IT, IT LIES!
+			r.Annotations = map[string]string{
+				"pkg.knative.dev/lastModifier": user2,
+			}
+		},
+		createRequestFunc: func(ctx context.Context, t *testing.T, r *Resource) *admissionv1.AdmissionRequest {
+			req := createCreateResource(ctx, t, r)
+			req.Kind = r.GetGroupVersionKindMeta()
+			return req
+		},
+		patches: []jsonpatch.JsonPatchOperation{{
+			Operation: "replace",
+			Path:      "/spec/fieldForCallbackDefaulting",
+			Value:     "I'm a default",
+		}, {
+			Operation: "add",
+			Path:      "/spec/fieldForCallbackDefaultingUsername",
+			Value:     user1,
+		}, {
+			Operation: "replace",
+			Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
+			Value:     user1,
+		}, {
+			Operation: "add",
+			Path:      "/metadata/annotations/pkg.knative.dev~1creator",
+			Value:     user1,
+		}},
 	}}
 
 	for _, tc := range tests {
@@ -336,7 +432,13 @@ func TestAdmitCreates(t *testing.T) {
 			tc.setup(ctx, r)
 
 			_, ac := newNonRunningTestResourceAdmissionController(t)
-			resp := ac.Admit(ctx, createCreateResource(ctx, t, r))
+			var req *admissionv1.AdmissionRequest
+			if tc.createRequestFunc == nil {
+				req = createCreateResource(ctx, t, r)
+			} else {
+				req = tc.createRequestFunc(ctx, t, r)
+			}
+			resp := ac.Admit(ctx, req)
 
 			if tc.rejection == "" {
 				ExpectAllowed(t, resp)
@@ -385,6 +487,15 @@ func TestAdmitUpdates(t *testing.T) {
 			// annotation doesn't change.
 		},
 		patches: []jsonpatch.JsonPatchOperation{},
+	}, {
+		name: "test simple update (callback defaults error)",
+		setup: func(ctx context.Context, r *Resource) {
+			r.SetDefaults(ctx)
+		},
+		mutate: func(ctx context.Context, r *Resource) {
+			r.Spec.FieldForCallbackDefaulting = "no magic value"
+		},
+		rejection: "no magic value",
 	}, {
 		name: "test simple update (update updater annotation)",
 		setup: func(ctx context.Context, r *Resource) {
@@ -428,7 +539,9 @@ func TestAdmitUpdates(t *testing.T) {
 
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
-			old := CreateResource("a name")
+			name := "a name"
+
+			old := CreateResource(name)
 			ctx := TestContextWithLogger(t)
 
 			old.Annotations = map[string]string{
@@ -446,7 +559,174 @@ func TestAdmitUpdates(t *testing.T) {
 			tc.mutate(ctx, new)
 
 			_, ac := newNonRunningTestResourceAdmissionController(t)
-			resp := ac.Admit(ctx, createUpdateResource(ctx, t, old, new))
+
+			req := createUpdateResource(ctx, t, old, new)
+
+			resp := ac.Admit(ctx, req)
+
+			if tc.rejection == "" {
+				ExpectAllowed(t, resp)
+				ExpectPatches(t, resp.Patch, tc.patches)
+			} else {
+				ExpectFailsWith(t, resp, tc.rejection)
+			}
+		})
+	}
+}
+
+func TestAdmitUpdatesCallback(t *testing.T) {
+	tests := []struct {
+		name                     string
+		setup                    func(context.Context, *Resource)
+		mutate                   func(context.Context, *Resource)
+		createResourceFunc       func(name string) *Resource
+		createUpdateResourceFunc func(ctx context.Context, t *testing.T, old, new *Resource) *admissionv1.AdmissionRequest
+		rejection                string
+		patches                  []jsonpatch.JsonPatchOperation
+	}{
+		{
+			name: "test simple update (callback defaults error)",
+			setup: func(ctx context.Context, r *Resource) {
+				r.SetDefaults(ctx)
+			},
+			mutate: func(ctx context.Context, r *Resource) {
+				r.Spec.FieldForCallbackDefaulting = "no magic value"
+			},
+			rejection:                "no magic value",
+			createUpdateResourceFunc: createUpdateResource,
+		}, {
+			name: "test simple update (callback defaults)",
+			setup: func(ctx context.Context, r *Resource) {
+				r.SetDefaults(ctx)
+			},
+			mutate: func(ctx context.Context, r *Resource) {
+				r.Spec.FieldForCallbackDefaulting = "magic value"
+			},
+			patches: []jsonpatch.JsonPatchOperation{{
+				Operation: "replace",
+				Path:      "/spec/fieldForCallbackDefaulting",
+				Value:     "I'm a default",
+			}, {
+
+				Operation: "replace",
+				Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
+				Value:     user2,
+			}, {
+				Operation: "add",
+				Path:      "/spec/fieldForCallbackDefaultingIsWithinUpdate",
+				Value:     true,
+			}, {
+				Operation: "add",
+				Path:      "/spec/fieldForCallbackDefaultingUsername",
+				Value:     user2,
+			}},
+			createUpdateResourceFunc: createUpdateResource,
+		}, {
+			name: "test simple update (callback defaults only)",
+			setup: func(ctx context.Context, r *Resource) {
+				r.TypeMeta.APIVersion = "pkg.knative.dev/v1beta1"
+				r.TypeMeta.Kind = "ResourceCallbackDefault"
+				r.Spec.FieldForCallbackDefaulting = "magic value"
+				r.SetDefaults(ctx)
+			},
+			mutate: func(ctx context.Context, r *Resource) {
+				r.Spec.FieldForCallbackDefaulting = "magic value"
+			},
+			createUpdateResourceFunc: func(ctx context.Context, t *testing.T, old, new *Resource) *admissionv1.AdmissionRequest {
+				req := createUpdateResource(ctx, t, old, new)
+				req.Kind = new.GetGroupVersionKindMeta()
+				return req
+			},
+			patches: []jsonpatch.JsonPatchOperation{{
+				Operation: "replace",
+				Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
+				Value:     user2,
+			}, {
+				Operation: "replace",
+				Path:      "/spec/fieldForCallbackDefaulting",
+				Value:     "I'm a default",
+			}, {
+				Operation: "add",
+				Path:      "/spec/fieldForCallbackDefaultingIsWithinUpdate",
+				Value:     true,
+			}, {
+				Operation: "add",
+				Path:      "/spec/fieldForCallbackDefaultingUsername",
+				Value:     user2,
+			}},
+		}, {
+			name: "test simple update (callback defaults only, operation not supported)",
+			setup: func(ctx context.Context, r *Resource) {
+				r.TypeMeta.APIVersion = "pkg.knative.dev/v1beta1"
+				r.TypeMeta.Kind = "ResourceCallbackDefaultCreate"
+				r.Spec.FieldForCallbackDefaulting = "magic value"
+				r.SetDefaults(ctx)
+			},
+			mutate: func(ctx context.Context, r *Resource) {
+				r.Spec.FieldForCallbackDefaulting = "magic value"
+			},
+			createUpdateResourceFunc: func(ctx context.Context, t *testing.T, old, new *Resource) *admissionv1.AdmissionRequest {
+				req := createUpdateResource(ctx, t, old, new)
+				req.Operation = admissionv1.Update
+				req.Kind = new.GetGroupVersionKindMeta()
+				return req
+			},
+		}, {
+			name: "test simple update (callback defaults)",
+			setup: func(ctx context.Context, r *Resource) {
+				r.SetDefaults(ctx)
+			},
+			mutate: func(ctx context.Context, r *Resource) {
+				r.Spec.FieldForCallbackDefaulting = "magic value"
+			},
+			patches: []jsonpatch.JsonPatchOperation{{
+				Operation: "replace",
+				Path:      "/spec/fieldForCallbackDefaulting",
+				Value:     "I'm a default",
+			}, {
+
+				Operation: "replace",
+				Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
+				Value:     user2,
+			}, {
+				Operation: "add",
+				Path:      "/spec/fieldForCallbackDefaultingIsWithinUpdate",
+				Value:     true,
+			}, {
+				Operation: "add",
+				Path:      "/spec/fieldForCallbackDefaultingUsername",
+				Value:     user2,
+			}},
+			createUpdateResourceFunc: createUpdateResource,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			name := "a name"
+
+			old := CreateResource(name)
+			ctx := TestContextWithLogger(t)
+
+			old.Annotations = map[string]string{
+				"pkg.knative.dev/creator":      user1,
+				"pkg.knative.dev/lastModifier": user1,
+			}
+
+			tc.setup(ctx, old)
+
+			new := old.DeepCopy()
+
+			// Mutate the resource using the update context as user2
+			ctx = apis.WithUserInfo(apis.WithinUpdate(ctx, old),
+				&authenticationv1.UserInfo{Username: user2})
+			tc.mutate(ctx, new)
+
+			_, ac := newNonRunningTestResourceAdmissionController(t)
+
+			req := tc.createUpdateResourceFunc(ctx, t, old, new)
+
+			resp := ac.Admit(ctx, req)
 
 			if tc.rejection == "" {
 				ExpectAllowed(t, resp)
@@ -511,6 +791,10 @@ func TestValidCreateResourceSucceedsWithRoundTripAndDefaultPatch(t *testing.T) {
 func createInnerDefaultResourceWithoutSpec(t *testing.T) []byte {
 	t.Helper()
 	r := InnerDefaultResource{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "InnerDefaultResource",
+			APIVersion: fmt.Sprintf("%s/%s", SchemeGroupVersion.Group, SchemeGroupVersion.Version),
+		},
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: system.Namespace(),
 			Name:      "a name",
@@ -543,5 +827,39 @@ func newTestResourceAdmissionController(t *testing.T) webhook.AdmissionControlle
 		ctx, testResourceValidationName, testResourceValidationPath,
 		handlers, func(ctx context.Context) context.Context {
 			return ctx
-		}, true).Reconciler.(*reconciler)
+		}, true, callbacks).Reconciler.(*reconciler)
+}
+
+func resourceCallback(ctx context.Context, uns *unstructured.Unstructured) error {
+	var resource Resource
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(uns.UnstructuredContent(), &resource); err != nil {
+		return err
+	}
+
+	if resource.Spec.FieldForCallbackDefaulting != "" {
+		if resource.Spec.FieldForCallbackDefaulting != "magic value" {
+			return errors.New(resource.Spec.FieldForCallbackDefaulting)
+		}
+		resource.Spec.FieldForCallbackDefaultingIsWithinUpdate = apis.IsInUpdate(ctx)
+		resource.Spec.FieldForCallbackDefaultingUsername = apis.GetUserInfo(ctx).Username
+		resource.Spec.FieldForCallbackDefaulting = "I'm a default"
+		if apis.IsInUpdate(ctx) {
+			if apis.GetBaseline(ctx) == nil {
+				return fmt.Errorf("expected baseline object")
+			}
+			if v, ok := apis.GetBaseline(ctx).(*unstructured.Unstructured); !ok {
+				return fmt.Errorf("expected *unstructured.Unstructured, got %v", reflect.TypeOf(v))
+			}
+		} else if !apis.IsInCreate(ctx) {
+			return fmt.Errorf("expected to have context within update or create")
+		}
+	}
+
+	u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&resource)
+	if err != nil {
+		return err
+	}
+	uns.Object = u
+
+	return nil
 }
diff --git a/webhook/resourcesemantics/defaulting/table_test.go b/webhook/resourcesemantics/defaulting/table_test.go
index 1c7fa107d..6f5177cf0 100644
--- a/webhook/resourcesemantics/defaulting/table_test.go
+++ b/webhook/resourcesemantics/defaulting/table_test.go
@@ -33,6 +33,7 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/wait"
 	clientgotesting "k8s.io/client-go/testing"
+
 	"knative.dev/pkg/configmap"
 	"knative.dev/pkg/controller"
 	"knative.dev/pkg/ptr"
@@ -91,6 +92,20 @@ func TestReconcile(t *testing.T) {
 			APIVersions: []string{"v1alpha1"},
 			Resources:   []string{"resources", "resources/status"},
 		},
+	}, {
+		Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
+		Rule: admissionregistrationv1.Rule{
+			APIGroups:   []string{"pkg.knative.dev"},
+			APIVersions: []string{"v1beta1"},
+			Resources:   []string{"resourcecallbackdefaultcreates", "resourcecallbackdefaultcreates/status"},
+		},
+	}, {
+		Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
+		Rule: admissionregistrationv1.Rule{
+			APIGroups:   []string{"pkg.knative.dev"},
+			APIVersions: []string{"v1beta1"},
+			Resources:   []string{"resourcecallbackdefaults", "resourcecallbackdefaults/status"},
+		},
 	}, {
 		Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
 		Rule: admissionregistrationv1.Rule{
@@ -419,7 +434,8 @@ func TestReconcile(t *testing.T) {
 			},
 			path: path,
 
-			handlers: handlers,
+			handlers:  handlers,
+			callbacks: callbacks,
 
 			client:       kubeclient.Get(ctx),
 			mwhlister:    listers.GetMutatingWebhookConfigurationLister(),