Skip to content

Commit dc03df9

Browse files
committed
feat: Implement Server-Side Diffs
Signed-off-by: Leonardo Luz Almeida <[email protected]>
1 parent b4dd8b8 commit dc03df9

File tree

4 files changed

+93
-3
lines changed

4 files changed

+93
-3
lines changed

pkg/cache/cluster.go

+6
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ type ClusterCache interface {
119119
OnResourceUpdated(handler OnResourceUpdatedHandler) Unsubscribe
120120
// OnEvent register event handler that is executed every time when new K8S event received
121121
OnEvent(handler OnEventHandler) Unsubscribe
122+
123+
GetKubectl() kube.Kubectl
122124
}
123125

124126
type WeightedSemaphore interface {
@@ -303,6 +305,10 @@ func (c *clusterCache) GetGVKParser() *managedfields.GvkParser {
303305
return c.gvkParser
304306
}
305307

308+
func (c *clusterCache) GetKubectl() kube.Kubectl {
309+
return c.kubectl
310+
}
311+
306312
func (c *clusterCache) appendAPIResource(info kube.APIResourceInfo) {
307313
exists := false
308314
for i := range c.apiResources {

pkg/diff/diff.go

+51
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package diff
66

77
import (
88
"bytes"
9+
"context"
910
"encoding/json"
1011
"errors"
1112
"fmt"
@@ -20,6 +21,7 @@ import (
2021
"k8s.io/apimachinery/pkg/util/managedfields"
2122
"k8s.io/apimachinery/pkg/util/strategicpatch"
2223
"k8s.io/client-go/kubernetes/scheme"
24+
cmdutil "k8s.io/kubectl/pkg/cmd/util"
2325
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
2426
"sigs.k8s.io/structured-merge-diff/v4/merge"
2527
"sigs.k8s.io/structured-merge-diff/v4/typed"
@@ -83,6 +85,14 @@ func Diff(config, live *unstructured.Unstructured, opts ...Option) (*DiffResult,
8385
Normalize(live, opts...)
8486
}
8587

88+
if o.serverSideDiff {
89+
r, err := ServerSideDiff(config, live, o.manager, o.kubeApplier, opts...)
90+
if err != nil {
91+
return nil, fmt.Errorf("error calculating server side diff: %w", err)
92+
}
93+
return r, nil
94+
}
95+
8696
// TODO The two variables bellow are necessary because there is a cyclic
8797
// dependency with the kube package that blocks the usage of constants
8898
// from common package. common package needs to be refactored and exclude
@@ -120,6 +130,47 @@ func Diff(config, live *unstructured.Unstructured, opts ...Option) (*DiffResult,
120130
return TwoWayDiff(config, live)
121131
}
122132

133+
func ServerSideDiff(config, live *unstructured.Unstructured, manager string, kubeApplier KubeApplier, opts ...Option) (*DiffResult, error) {
134+
if live != nil && config != nil {
135+
return serverSideDiff(config, live, manager, kubeApplier, opts...)
136+
}
137+
return handleResourceCreateOrDeleteDiff(config, live)
138+
}
139+
140+
func serverSideDiff(config, live *unstructured.Unstructured, manager string, kubeApplier KubeApplier, opts ...Option) (*DiffResult, error) {
141+
predictedLiveStr, err := kubeApplier.ApplyResource(context.Background(), config, cmdutil.DryRunServer, false, false, true, manager)
142+
if err != nil {
143+
return nil, fmt.Errorf("error running server side apply in dryrun mode: %w", err)
144+
}
145+
predictedLive, err := jsonStrToUnstructured(predictedLiveStr)
146+
if err != nil {
147+
return nil, fmt.Errorf("error converting json string to unstructured: %w", err)
148+
}
149+
150+
Normalize(predictedLive, opts...)
151+
152+
predictedLiveBytes, err := json.Marshal(predictedLive)
153+
if err != nil {
154+
return nil, fmt.Errorf("error marshaling predicted live resource: %w", err)
155+
}
156+
157+
unstructured.RemoveNestedField(live.Object, "metadata", "managedFields")
158+
liveBytes, err := json.Marshal(live)
159+
if err != nil {
160+
return nil, fmt.Errorf("error marshaling live resource: %w", err)
161+
}
162+
return buildDiffResult(predictedLiveBytes, liveBytes), nil
163+
}
164+
165+
func jsonStrToUnstructured(jsonString string) (*unstructured.Unstructured, error) {
166+
res := make(map[string]interface{})
167+
err := json.Unmarshal([]byte(jsonString), &res)
168+
if err != nil {
169+
return nil, fmt.Errorf("unmarshal error: %s", err)
170+
}
171+
return &unstructured.Unstructured{Object: res}, nil
172+
}
173+
123174
// StructuredMergeDiff will calculate the diff using the structured-merge-diff
124175
// k8s library (https://github.com/kubernetes-sigs/structured-merge-diff).
125176
func StructuredMergeDiff(config, live *unstructured.Unstructured, gvkParser *managedfields.GvkParser, manager string) (*DiffResult, error) {

pkg/diff/diff_options.go

+22
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package diff
22

33
import (
4+
"context"
5+
46
"github.com/go-logr/logr"
7+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
58
"k8s.io/apimachinery/pkg/util/managedfields"
69
"k8s.io/klog/v2/klogr"
10+
cmdutil "k8s.io/kubectl/pkg/cmd/util"
711
)
812

913
type Option func(*options)
@@ -17,6 +21,8 @@ type options struct {
1721
structuredMergeDiff bool
1822
gvkParser *managedfields.GvkParser
1923
manager string
24+
serverSideDiff bool
25+
kubeApplier KubeApplier
2026
}
2127

2228
func applyOptions(opts []Option) options {
@@ -31,6 +37,10 @@ func applyOptions(opts []Option) options {
3137
return o
3238
}
3339

40+
type KubeApplier interface {
41+
ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate, serverSideApply bool, manager string) (string, error)
42+
}
43+
3444
func IgnoreAggregatedRoles(ignore bool) Option {
3545
return func(o *options) {
3646
o.ignoreAggregatedRoles = ignore
@@ -66,3 +76,15 @@ func WithManager(manager string) Option {
6676
o.manager = manager
6777
}
6878
}
79+
80+
func WithServerSideDiff(ssd bool) Option {
81+
return func(o *options) {
82+
o.serverSideDiff = ssd
83+
}
84+
}
85+
86+
func WithKubeApplier(ka KubeApplier) Option {
87+
return func(o *options) {
88+
o.kubeApplier = ka
89+
}
90+
}

pkg/utils/kube/resource_ops.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,16 @@ func (k *kubectlResourceOperations) newApplyOptions(ioStreams genericclioptions.
292292
return nil, err
293293
}
294294
case cmdutil.DryRunServer:
295-
err = o.PrintFlags.Complete("%s (server dry run)")
296-
if err != nil {
297-
return nil, err
295+
// TODO (SSD): This logic should be refactored. PrintFlags should be
296+
// configurable by the caller.
297+
if serverSideApply {
298+
o.PrintFlags.JSONYamlPrintFlags.ShowManagedFields = false
299+
return o.PrintFlags.JSONYamlPrintFlags.ToPrinter("json")
300+
} else {
301+
err = o.PrintFlags.Complete("%s (server dry run)")
302+
if err != nil {
303+
return nil, err
304+
}
298305
}
299306
}
300307
return o.PrintFlags.ToPrinter()
@@ -312,6 +319,10 @@ func (k *kubectlResourceOperations) newApplyOptions(ioStreams genericclioptions.
312319
return o, nil
313320
}
314321

322+
func toPointer(str string) *string {
323+
return &str
324+
}
325+
315326
func (k *kubectlResourceOperations) newCreateOptions(config *rest.Config, ioStreams genericclioptions.IOStreams, fileName string, dryRunStrategy cmdutil.DryRunStrategy) (*create.CreateOptions, error) {
316327
o := create.NewCreateOptions(ioStreams)
317328

0 commit comments

Comments
 (0)