Skip to content

Commit 8b3c00c

Browse files
authored
Merge pull request kubernetes#89799 from julianvmodesto/patcher
Make kubectl client-side apply with server-side dry-run safer
2 parents 5fc4f4d + f0eb68c commit 8b3c00c

File tree

8 files changed

+67
-51
lines changed

8 files changed

+67
-51
lines changed

staging/src/k8s.io/kubectl/pkg/cmd/apply/BUILD

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ go_test(
6868
"//staging/src/k8s.io/client-go/dynamic/fake:go_default_library",
6969
"//staging/src/k8s.io/client-go/rest:go_default_library",
7070
"//staging/src/k8s.io/client-go/rest/fake:go_default_library",
71-
"//staging/src/k8s.io/client-go/testing:go_default_library",
7271
"//staging/src/k8s.io/kubectl/pkg/cmd/testing:go_default_library",
7372
"//staging/src/k8s.io/kubectl/pkg/cmd/util:go_default_library",
7473
"//staging/src/k8s.io/kubectl/pkg/scheme:go_default_library",

staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,20 @@ func (o *ApplyOptions) applyOneObject(info *resource.Info) error {
407407
klog.V(4).Infof("error recording current command: %v", err)
408408
}
409409

410+
helper := resource.NewHelper(info.Client, info.Mapping).
411+
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
412+
WithFieldManager(o.FieldManager)
413+
414+
if o.DryRunStrategy == cmdutil.DryRunServer {
415+
// Ensure the APIServer supports server-side dry-run for the resource,
416+
// otherwise fail early.
417+
// For APIServers that don't support server-side dry-run will persist
418+
// changes.
419+
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
420+
return err
421+
}
422+
}
423+
410424
if o.ServerSideApply {
411425
// Send the full object to be applied on the server side.
412426
data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object)
@@ -417,14 +431,6 @@ func (o *ApplyOptions) applyOneObject(info *resource.Info) error {
417431
options := metav1.PatchOptions{
418432
Force: &o.ForceConflicts,
419433
}
420-
helper := resource.NewHelper(info.Client, info.Mapping).
421-
WithFieldManager(o.FieldManager)
422-
if o.DryRunStrategy == cmdutil.DryRunServer {
423-
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
424-
return err
425-
}
426-
helper.DryRun(true)
427-
}
428434
obj, err := helper.Patch(
429435
info.Namespace,
430436
info.Name,
@@ -495,14 +501,6 @@ See http://k8s.io/docs/reference/using-api/api-concepts/#conflicts`, err)
495501

496502
if o.DryRunStrategy != cmdutil.DryRunClient {
497503
// Then create the resource and skip the three-way merge
498-
helper := resource.NewHelper(info.Client, info.Mapping).
499-
WithFieldManager(o.FieldManager)
500-
if o.DryRunStrategy == cmdutil.DryRunServer {
501-
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
502-
return cmdutil.AddSourceToErr("creating", info.Source, err)
503-
}
504-
helper.DryRun(true)
505-
}
506504
obj, err := helper.Create(info.Namespace, true, info.Object)
507505
if err != nil {
508506
return cmdutil.AddSourceToErr("creating", info.Source, err)
@@ -539,7 +537,7 @@ See http://k8s.io/docs/reference/using-api/api-concepts/#conflicts`, err)
539537
fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, o.cmdBaseName)
540538
}
541539

542-
patcher, err := newPatcher(o, info)
540+
patcher, err := newPatcher(o, info, helper)
543541
if err != nil {
544542
return err
545543
}

staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_test.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import (
4444
dynamicfakeclient "k8s.io/client-go/dynamic/fake"
4545
restclient "k8s.io/client-go/rest"
4646
"k8s.io/client-go/rest/fake"
47-
clienttesting "k8s.io/client-go/testing"
4847
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
4948
cmdutil "k8s.io/kubectl/pkg/cmd/util"
5049
"k8s.io/kubectl/pkg/scheme"
@@ -1333,6 +1332,11 @@ func TestForceApply(t *testing.T) {
13331332
}
13341333
t.Fatalf("unexpected request: %#v after %v tries\n%#v", req.URL, counts["patch"], req)
13351334
return nil, nil
1335+
case strings.HasSuffix(p, pathRC) && m == "DELETE":
1336+
counts["delete"]++
1337+
deleted = true
1338+
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
1339+
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
13361340
case strings.HasSuffix(p, pathRC) && m == "PUT":
13371341
counts["put"]++
13381342
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
@@ -1351,16 +1355,6 @@ func TestForceApply(t *testing.T) {
13511355
}),
13521356
}
13531357
fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
1354-
fakeDynamicClient.PrependReactor("delete", "replicationcontrollers", func(action clienttesting.Action) (bool, runtime.Object, error) {
1355-
if deleteAction, ok := action.(clienttesting.DeleteAction); ok {
1356-
if deleteAction.GetName() == nameRC {
1357-
counts["delete"]++
1358-
deleted = true
1359-
return true, nil, nil
1360-
}
1361-
}
1362-
return false, nil, nil
1363-
})
13641358
tf.FakeDynamicClient = fakeDynamicClient
13651359
tf.OpenAPISchemaFunc = fn
13661360
tf.Client = tf.UnstructuredClient

staging/src/k8s.io/kubectl/pkg/cmd/apply/patcher.go

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import (
3333
"k8s.io/apimachinery/pkg/util/strategicpatch"
3434
"k8s.io/apimachinery/pkg/util/wait"
3535
"k8s.io/cli-runtime/pkg/resource"
36-
"k8s.io/client-go/dynamic"
3736
oapi "k8s.io/kube-openapi/pkg/util/proto"
3837
cmdutil "k8s.io/kubectl/pkg/cmd/util"
3938
"k8s.io/kubectl/pkg/scheme"
@@ -52,18 +51,16 @@ const (
5251

5352
// Patcher defines options to patch OpenAPI objects.
5453
type Patcher struct {
55-
Mapping *meta.RESTMapping
56-
Helper *resource.Helper
57-
DynamicClient dynamic.Interface
54+
Mapping *meta.RESTMapping
55+
Helper *resource.Helper
5856

5957
Overwrite bool
6058
BackOff clockwork.Clock
6159

62-
Force bool
63-
Cascade bool
64-
Timeout time.Duration
65-
GracePeriod int
66-
ServerDryRun bool
60+
Force bool
61+
Cascade bool
62+
Timeout time.Duration
63+
GracePeriod int
6764

6865
// If set, forces the patch against a specific resourceVersion
6966
ResourceVersion *string
@@ -74,30 +71,30 @@ type Patcher struct {
7471
OpenapiSchema openapi.Resources
7572
}
7673

77-
func newPatcher(o *ApplyOptions, info *resource.Info) (*Patcher, error) {
74+
func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) {
7875
var openapiSchema openapi.Resources
7976
if o.OpenAPIPatch {
8077
openapiSchema = o.OpenAPISchema
8178
}
8279

8380
return &Patcher{
8481
Mapping: info.Mapping,
85-
Helper: resource.NewHelper(info.Client, info.Mapping).WithFieldManager(o.FieldManager),
86-
DynamicClient: o.DynamicClient,
82+
Helper: helper,
8783
Overwrite: o.Overwrite,
8884
BackOff: clockwork.NewRealClock(),
8985
Force: o.DeleteOptions.ForceDeletion,
9086
Cascade: o.DeleteOptions.Cascade,
9187
Timeout: o.DeleteOptions.Timeout,
9288
GracePeriod: o.DeleteOptions.GracePeriod,
93-
ServerDryRun: o.DryRunStrategy == cmdutil.DryRunServer,
9489
OpenapiSchema: openapiSchema,
9590
Retries: maxPatchRetry,
9691
}, nil
9792
}
9893

9994
func (p *Patcher) delete(namespace, name string) error {
100-
return runDelete(namespace, name, p.Mapping, p.DynamicClient, p.Cascade, p.GracePeriod, p.ServerDryRun)
95+
options := asDeleteOptions(p.Cascade, p.GracePeriod)
96+
_, err := p.Helper.DeleteWithOptions(namespace, name, &options)
97+
return err
10198
}
10299

103100
func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
@@ -178,7 +175,7 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, names
178175
}
179176
}
180177

181-
patchedObj, err := p.Helper.DryRun(p.ServerDryRun).Patch(namespace, name, patchType, patch, nil)
178+
patchedObj, err := p.Helper.Patch(namespace, name, patchType, patch, nil)
182179
return patch, patchedObj, err
183180
}
184181

@@ -223,11 +220,11 @@ func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, name
223220
if err != nil {
224221
return modified, nil, err
225222
}
226-
createdObject, err := p.Helper.DryRun(p.ServerDryRun).Create(namespace, true, versionedObject)
223+
createdObject, err := p.Helper.Create(namespace, true, versionedObject)
227224
if err != nil {
228225
// restore the original object if we fail to create the new one
229226
// but still propagate and advertise error to user
230-
recreated, recreateErr := p.Helper.DryRun(p.ServerDryRun).Create(namespace, true, original)
227+
recreated, recreateErr := p.Helper.Create(namespace, true, original)
231228
if recreateErr != nil {
232229
err = fmt.Errorf("An error occurred force-replacing the existing object with the newly provided one:\n\n%v.\n\nAdditionally, an error occurred attempting to restore the original object:\n\n%v", err, recreateErr)
233230
} else {

staging/src/k8s.io/kubectl/pkg/cmd/apply/prune.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,19 +143,24 @@ func (p *pruner) delete(namespace, name string, mapping *meta.RESTMapping) error
143143
}
144144

145145
func runDelete(namespace, name string, mapping *meta.RESTMapping, c dynamic.Interface, cascade bool, gracePeriod int, serverDryRun bool) error {
146+
options := asDeleteOptions(cascade, gracePeriod)
147+
if serverDryRun {
148+
options.DryRun = []string{metav1.DryRunAll}
149+
}
150+
return c.Resource(mapping.Resource).Namespace(namespace).Delete(context.TODO(), name, options)
151+
}
152+
153+
func asDeleteOptions(cascade bool, gracePeriod int) metav1.DeleteOptions {
146154
options := metav1.DeleteOptions{}
147155
if gracePeriod >= 0 {
148156
options = *metav1.NewDeleteOptions(int64(gracePeriod))
149157
}
150-
if serverDryRun {
151-
options.DryRun = []string{metav1.DryRunAll}
152-
}
153158
policy := metav1.DeletePropagationForeground
154159
if !cascade {
155160
policy = metav1.DeletePropagationOrphan
156161
}
157162
options.PropagationPolicy = &policy
158-
return c.Resource(mapping.Resource).Namespace(namespace).Delete(context.TODO(), name, options)
163+
return options
159164
}
160165

161166
type pruneResource struct {

staging/src/k8s.io/kubectl/pkg/cmd/diff/diff.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,6 @@ func (obj InfoObject) Merged() (runtime.Object, error) {
367367
Helper: helper,
368368
Overwrite: true,
369369
BackOff: clockwork.NewRealClock(),
370-
ServerDryRun: true,
371370
OpenapiSchema: obj.OpenAPI,
372371
ResourceVersion: resourceVersion,
373372
}

staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,23 @@ func GetFieldManagerFlag(cmd *cobra.Command) string {
523523
type DryRunStrategy int
524524

525525
const (
526+
// DryRunNone indicates the client will make all mutating calls
526527
DryRunNone DryRunStrategy = iota
528+
529+
// DryRunClient, or client-side dry-run, indicates the client will prevent
530+
// making mutating calls such as CREATE, PATCH, and DELETE
527531
DryRunClient
532+
533+
// DryRunServer, or server-side dry-run, indicates the client will send
534+
// mutating calls to the APIServer with the dry-run parameter to prevent
535+
// persisting changes.
536+
//
537+
// Note that clients sending server-side dry-run calls should verify that
538+
// the APIServer and the resource supports server-side dry-run, and otherwise
539+
// clients should fail early.
540+
//
541+
// If a client sends a server-side dry-run call to an APIServer that doesn't
542+
// support server-side dry-run, then the APIServer will persist changes inadvertently.
528543
DryRunServer
529544
)
530545

test/cmd/apply.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,15 @@ run_kubectl_apply_tests() {
109109
kube::test::get_object_assert pods "{{range.items}}{{${id_field:?}}}:{{end}}" ''
110110
# apply non dry-run creates the pod
111111
kubectl apply -f hack/testdata/pod.yaml "${kube_flags[@]:?}"
112+
initialResourceVersion=$(kubectl get "${kube_flags[@]:?}" -f hack/testdata/pod.yaml -o go-template='{{ .metadata.resourceVersion }}')
112113
# apply changes
114+
kubectl apply --dry-run=client -f hack/testdata/pod-apply.yaml "${kube_flags[@]:?}"
113115
kubectl apply --dry-run=server -f hack/testdata/pod-apply.yaml "${kube_flags[@]:?}"
114116
# Post-Condition: label still has initial value
115117
kube::test::get_object_assert 'pods test-pod' "{{${labels_field:?}.name}}" 'test-pod-label'
118+
# Ensure dry-run doesn't persist change
119+
resourceVersion=$(kubectl get "${kube_flags[@]:?}" -f hack/testdata/pod.yaml -o go-template='{{ .metadata.resourceVersion }}')
120+
kube::test::if_has_string "${resourceVersion}" "${initialResourceVersion}"
116121

117122
# clean-up
118123
kubectl delete -f hack/testdata/pod.yaml "${kube_flags[@]:?}"
@@ -377,10 +382,14 @@ run_kubectl_server_side_apply_tests() {
377382
kube::test::get_object_assert pods "{{range.items}}{{${id_field:?}}}:{{end}}" ''
378383
# apply non dry-run creates the pod
379384
kubectl apply --server-side -f hack/testdata/pod.yaml "${kube_flags[@]:?}"
385+
initialResourceVersion=$(kubectl get "${kube_flags[@]:?}" -f hack/testdata/pod.yaml -o go-template='{{ .metadata.resourceVersion }}')
380386
# apply changes
381387
kubectl apply --server-side --dry-run=server -f hack/testdata/pod-apply.yaml "${kube_flags[@]:?}"
382388
# Post-Condition: label still has initial value
383389
kube::test::get_object_assert 'pods test-pod' "{{${labels_field:?}.name}}" 'test-pod-label'
390+
# Ensure dry-run doesn't persist change
391+
resourceVersion=$(kubectl get "${kube_flags[@]:?}" -f hack/testdata/pod.yaml -o go-template='{{ .metadata.resourceVersion }}')
392+
kube::test::if_has_string "${resourceVersion}" "${initialResourceVersion}"
384393

385394
# clean-up
386395
kubectl delete -f hack/testdata/pod.yaml "${kube_flags[@]:?}"

0 commit comments

Comments
 (0)