Skip to content

Commit c8a5a35

Browse files
authored
⚠️ Add subresource apply support (#3321)
* ✨ Add subresource apply support * Revert "Revert deprecation of client.Apply" This reverts commit 6e1e8b2. * Fixups
1 parent 655fb2c commit c8a5a35

21 files changed

+543
-35
lines changed

pkg/client/client.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,30 @@ func (po *SubResourcePatchOptions) ApplyToSubResourcePatch(o *SubResourcePatchOp
544544
}
545545
}
546546

547+
// SubResourceApplyOptions are the options for a subresource
548+
// apply request.
549+
type SubResourceApplyOptions struct {
550+
ApplyOptions
551+
SubResourceBody runtime.ApplyConfiguration
552+
}
553+
554+
// ApplyOpts applies the given options.
555+
func (ao *SubResourceApplyOptions) ApplyOpts(opts []SubResourceApplyOption) *SubResourceApplyOptions {
556+
for _, o := range opts {
557+
o.ApplyToSubResourceApply(ao)
558+
}
559+
560+
return ao
561+
}
562+
563+
// ApplyToSubResourceApply applies the configuration on the given patch options.
564+
func (ao *SubResourceApplyOptions) ApplyToSubResourceApply(o *SubResourceApplyOptions) {
565+
ao.ApplyOptions.ApplyToApply(&o.ApplyOptions)
566+
if ao.SubResourceBody != nil {
567+
o.SubResourceBody = ao.SubResourceBody
568+
}
569+
}
570+
547571
func (sc *subResourceClient) Get(ctx context.Context, obj Object, subResource Object, opts ...SubResourceGetOption) error {
548572
switch obj.(type) {
549573
case runtime.Unstructured:
@@ -595,3 +619,13 @@ func (sc *subResourceClient) Patch(ctx context.Context, obj Object, patch Patch,
595619
return sc.client.typedClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...)
596620
}
597621
}
622+
623+
func (sc *subResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error {
624+
switch obj := obj.(type) {
625+
case *unstructuredApplyConfiguration:
626+
defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
627+
return sc.client.unstructuredClient.ApplySubResource(ctx, obj, sc.subResource, opts...)
628+
default:
629+
return sc.client.typedClient.ApplySubResource(ctx, obj, sc.subResource, opts...)
630+
}
631+
}

pkg/client/client_test.go

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import (
4343
"k8s.io/apimachinery/pkg/runtime"
4444
"k8s.io/apimachinery/pkg/runtime/schema"
4545
"k8s.io/apimachinery/pkg/types"
46+
appsv1applyconfigurations "k8s.io/client-go/applyconfigurations/apps/v1"
47+
autoscaling1applyconfigurations "k8s.io/client-go/applyconfigurations/autoscaling/v1"
4648
corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1"
4749
kscheme "k8s.io/client-go/kubernetes/scheme"
4850
"k8s.io/client-go/rest"
@@ -1127,6 +1129,34 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC
11271129
Expect(err).NotTo(HaveOccurred())
11281130
Expect(*dep.Spec.Replicas).To(Equal(replicaCount))
11291131
})
1132+
1133+
It("should be able to apply the scale subresource", func(ctx SpecContext) {
1134+
cl, err := client.New(cfg, client.Options{})
1135+
Expect(err).NotTo(HaveOccurred())
1136+
Expect(cl).NotTo(BeNil())
1137+
1138+
By("Creating a deployment")
1139+
dep, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{})
1140+
Expect(err).NotTo(HaveOccurred())
1141+
replicaCount := *dep.Spec.Replicas + 1
1142+
1143+
By("Applying the scale subresurce")
1144+
deploymentAC, err := appsv1applyconfigurations.ExtractDeployment(dep, "foo")
1145+
Expect(err).NotTo(HaveOccurred())
1146+
scale := autoscaling1applyconfigurations.Scale().
1147+
WithSpec(autoscaling1applyconfigurations.ScaleSpec().WithReplicas(replicaCount))
1148+
err = cl.SubResource("scale").Apply(ctx, deploymentAC,
1149+
&client.SubResourceApplyOptions{SubResourceBody: scale},
1150+
client.FieldOwner("foo"),
1151+
client.ForceOwnership,
1152+
)
1153+
Expect(err).NotTo(HaveOccurred())
1154+
1155+
By("Asserting replicas got updated")
1156+
dep, err = clientset.AppsV1().Deployments(dep.Namespace).Get(ctx, dep.Name, metav1.GetOptions{})
1157+
Expect(err).NotTo(HaveOccurred())
1158+
Expect(*dep.Spec.Replicas).To(Equal(replicaCount))
1159+
})
11301160
})
11311161

11321162
Context("with unstructured objects", func() {
@@ -1322,8 +1352,8 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC
13221352
By("Creating a deployment")
13231353
dep, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{})
13241354
Expect(err).NotTo(HaveOccurred())
1325-
dep.APIVersion = "apps/v1"
1326-
dep.Kind = "Deployment"
1355+
dep.APIVersion = appsv1.SchemeGroupVersion.String()
1356+
dep.Kind = "Deployment" //nolint:goconst
13271357
depUnstructured, err := toUnstructured(dep)
13281358
Expect(err).NotTo(HaveOccurred())
13291359

@@ -1374,6 +1404,41 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC
13741404
Expect(err).NotTo(HaveOccurred())
13751405
Expect(*dep.Spec.Replicas).To(Equal(replicaCount))
13761406
})
1407+
1408+
It("should be able to apply the scale subresource", func(ctx SpecContext) {
1409+
cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()})
1410+
Expect(err).NotTo(HaveOccurred())
1411+
Expect(cl).NotTo(BeNil())
1412+
1413+
By("Creating a deployment")
1414+
dep, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{})
1415+
Expect(err).NotTo(HaveOccurred())
1416+
dep.APIVersion = "apps/v1"
1417+
dep.Kind = "Deployment"
1418+
depUnstructured, err := toUnstructured(dep)
1419+
Expect(err).NotTo(HaveOccurred())
1420+
1421+
By("Updating the scale subresurce")
1422+
replicaCount := *dep.Spec.Replicas + 1
1423+
scale := &unstructured.Unstructured{}
1424+
scale.SetAPIVersion("autoscaling/v1")
1425+
scale.SetKind("Scale")
1426+
Expect(unstructured.SetNestedField(scale.Object, int64(replicaCount), "spec", "replicas")).NotTo(HaveOccurred())
1427+
err = cl.SubResource("scale").Apply(ctx,
1428+
client.ApplyConfigurationFromUnstructured(depUnstructured),
1429+
&client.SubResourceApplyOptions{SubResourceBody: client.ApplyConfigurationFromUnstructured(scale)},
1430+
client.FieldOwner("foo"),
1431+
client.ForceOwnership,
1432+
)
1433+
Expect(err).NotTo(HaveOccurred())
1434+
Expect(scale.GetAPIVersion()).To(Equal("autoscaling/v1"))
1435+
Expect(scale.GetKind()).To(Equal("Scale"))
1436+
1437+
By("Asserting replicas got updated")
1438+
dep, err = clientset.AppsV1().Deployments(dep.Namespace).Get(ctx, dep.Name, metav1.GetOptions{})
1439+
Expect(err).NotTo(HaveOccurred())
1440+
Expect(*dep.Spec.Replicas).To(Equal(replicaCount))
1441+
})
13771442
})
13781443

13791444
})
@@ -1440,6 +1505,29 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC
14401505
Expect(dep.GroupVersionKind()).To(Equal(depGvk))
14411506
})
14421507

1508+
It("should apply status", func(ctx SpecContext) {
1509+
cl, err := client.New(cfg, client.Options{})
1510+
Expect(err).NotTo(HaveOccurred())
1511+
Expect(cl).NotTo(BeNil())
1512+
1513+
By("initially creating a Deployment")
1514+
dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{})
1515+
Expect(err).NotTo(HaveOccurred())
1516+
Expect(dep.Status.Replicas).To(BeEquivalentTo(0))
1517+
1518+
By("applying the status of Deployment")
1519+
deploymentAC, err := appsv1applyconfigurations.ExtractDeployment(dep, "foo")
1520+
Expect(err).NotTo(HaveOccurred())
1521+
deploymentAC.WithStatus(&appsv1applyconfigurations.DeploymentStatusApplyConfiguration{
1522+
Replicas: ptr.To(int32(1)),
1523+
})
1524+
Expect(cl.Status().Apply(ctx, deploymentAC, client.FieldOwner("foo"))).To(Succeed())
1525+
1526+
dep, err = clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{})
1527+
Expect(err).NotTo(HaveOccurred())
1528+
Expect(dep.Status.Replicas).To(BeEquivalentTo(1))
1529+
})
1530+
14431531
It("should not update spec of an existing object", func(ctx SpecContext) {
14441532
cl, err := client.New(cfg, client.Options{})
14451533
Expect(err).NotTo(HaveOccurred())
@@ -1592,6 +1680,34 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC
15921680
Expect(actual.Status.Replicas).To(BeEquivalentTo(1))
15931681
})
15941682

1683+
It("should apply status and preserve type information", func(ctx SpecContext) {
1684+
cl, err := client.New(cfg, client.Options{})
1685+
Expect(err).NotTo(HaveOccurred())
1686+
Expect(cl).NotTo(BeNil())
1687+
1688+
By("initially creating a Deployment")
1689+
dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{})
1690+
Expect(err).NotTo(HaveOccurred())
1691+
Expect(dep.Status.Replicas).To(BeEquivalentTo(0))
1692+
1693+
By("applying the status of Deployment")
1694+
dep.Status.Replicas = 1
1695+
dep.ManagedFields = nil // Must be unset in SSA requests
1696+
u := &unstructured.Unstructured{}
1697+
Expect(scheme.Convert(dep, u, nil)).To(Succeed())
1698+
err = cl.Status().Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner("foo"))
1699+
Expect(err).NotTo(HaveOccurred())
1700+
1701+
By("validating updated Deployment has type information")
1702+
Expect(u.GroupVersionKind()).To(Equal(depGvk))
1703+
1704+
By("validating patched Deployment has new status")
1705+
actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{})
1706+
Expect(err).NotTo(HaveOccurred())
1707+
Expect(actual).NotTo(BeNil())
1708+
Expect(actual.Status.Replicas).To(BeEquivalentTo(1))
1709+
})
1710+
15951711
It("should not update spec of an existing object", func(ctx SpecContext) {
15961712
cl, err := client.New(cfg, client.Options{})
15971713
Expect(err).NotTo(HaveOccurred())

pkg/client/dryrun.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,7 @@ func (sw *dryRunSubResourceClient) Update(ctx context.Context, obj Object, opts
132132
func (sw *dryRunSubResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
133133
return sw.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...)
134134
}
135+
136+
func (sw *dryRunSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error {
137+
return sw.client.Apply(ctx, obj, append(opts, DryRunAll)...)
138+
}

pkg/client/dryrun_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
apierrors "k8s.io/apimachinery/pkg/api/errors"
2828
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2929
"k8s.io/apimachinery/pkg/types"
30+
appsv1applyconfigurations "k8s.io/client-go/applyconfigurations/apps/v1"
3031
"k8s.io/utils/ptr"
3132

3233
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -260,4 +261,36 @@ var _ = Describe("DryRunClient", func() {
260261
Expect(actual).NotTo(BeNil())
261262
Expect(actual).To(BeEquivalentTo(dep))
262263
})
264+
265+
It("should not change objects via status apply", func(ctx SpecContext) {
266+
deploymentAC, err := appsv1applyconfigurations.ExtractDeployment(dep, "test-owner")
267+
Expect(err).NotTo(HaveOccurred())
268+
deploymentAC.WithStatus(&appsv1applyconfigurations.DeploymentStatusApplyConfiguration{
269+
Replicas: ptr.To(int32(99)),
270+
})
271+
272+
Expect(getClient().Status().Apply(ctx, deploymentAC, client.FieldOwner("test-owner"))).NotTo(HaveOccurred())
273+
274+
actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{})
275+
Expect(err).NotTo(HaveOccurred())
276+
Expect(actual).NotTo(BeNil())
277+
Expect(actual).To(BeEquivalentTo(dep))
278+
})
279+
280+
It("should not change objects via status apply with opts", func(ctx SpecContext) {
281+
deploymentAC, err := appsv1applyconfigurations.ExtractDeployment(dep, "test-owner")
282+
Expect(err).NotTo(HaveOccurred())
283+
deploymentAC.WithStatus(&appsv1applyconfigurations.DeploymentStatusApplyConfiguration{
284+
Replicas: ptr.To(int32(99)),
285+
})
286+
287+
opts := &client.SubResourceApplyOptions{ApplyOptions: client.ApplyOptions{DryRun: []string{"Bye", "Pippa"}}}
288+
289+
Expect(getClient().Status().Apply(ctx, deploymentAC, client.FieldOwner("test-owner"), opts)).NotTo(HaveOccurred())
290+
291+
actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{})
292+
Expect(err).NotTo(HaveOccurred())
293+
Expect(actual).NotTo(BeNil())
294+
Expect(actual).To(BeEquivalentTo(dep))
295+
})
263296
})

pkg/client/fake/client.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,6 +1335,42 @@ func (sw *fakeSubResourceClient) statusPatch(body client.Object, patch client.Pa
13351335
return sw.client.patch(body, patch, &patchOptions.PatchOptions)
13361336
}
13371337

1338+
func (sw *fakeSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error {
1339+
if sw.subResource != "status" {
1340+
return errors.New("fakeSubResourceClient currently only supports Apply for status subresource")
1341+
}
1342+
1343+
applyOpts := &client.SubResourceApplyOptions{}
1344+
applyOpts.ApplyOpts(opts)
1345+
1346+
data, err := json.Marshal(obj)
1347+
if err != nil {
1348+
return fmt.Errorf("failed to marshal apply configuration: %w", err)
1349+
}
1350+
1351+
u := &unstructured.Unstructured{}
1352+
if err := json.Unmarshal(data, u); err != nil {
1353+
return fmt.Errorf("failed to unmarshal apply configuration: %w", err)
1354+
}
1355+
1356+
patchOpts := &client.SubResourcePatchOptions{}
1357+
patchOpts.Raw = applyOpts.AsPatchOptions()
1358+
1359+
if applyOpts.SubResourceBody != nil {
1360+
subResourceBodySerialized, err := json.Marshal(applyOpts.SubResourceBody)
1361+
if err != nil {
1362+
return fmt.Errorf("failed to serialize subresource body: %w", err)
1363+
}
1364+
subResourceBody := &unstructured.Unstructured{}
1365+
if err := json.Unmarshal(subResourceBodySerialized, subResourceBody); err != nil {
1366+
return fmt.Errorf("failed to unmarshal subresource body: %w", err)
1367+
}
1368+
patchOpts.SubResourceBody = subResourceBody
1369+
}
1370+
1371+
return sw.Patch(ctx, u, &fakeApplyPatch{}, patchOpts)
1372+
}
1373+
13381374
func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool {
13391375
switch gvk.Group {
13401376
case "apps":

0 commit comments

Comments
 (0)