Merge pull request #89799 from julianvmodesto/patcher

Make kubectl client-side apply with server-side dry-run safer
This commit is contained in:
Kubernetes Prow Robot 2020-05-27 20:50:02 -07:00 committed by GitHub
commit 8b3c00c661
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 67 additions and 51 deletions

View File

@ -68,7 +68,6 @@ go_test(
"//staging/src/k8s.io/client-go/dynamic/fake:go_default_library", "//staging/src/k8s.io/client-go/dynamic/fake:go_default_library",
"//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library",
"//staging/src/k8s.io/client-go/rest/fake:go_default_library", "//staging/src/k8s.io/client-go/rest/fake:go_default_library",
"//staging/src/k8s.io/client-go/testing:go_default_library",
"//staging/src/k8s.io/kubectl/pkg/cmd/testing:go_default_library", "//staging/src/k8s.io/kubectl/pkg/cmd/testing:go_default_library",
"//staging/src/k8s.io/kubectl/pkg/cmd/util:go_default_library", "//staging/src/k8s.io/kubectl/pkg/cmd/util:go_default_library",
"//staging/src/k8s.io/kubectl/pkg/scheme:go_default_library", "//staging/src/k8s.io/kubectl/pkg/scheme:go_default_library",

View File

@ -407,6 +407,20 @@ func (o *ApplyOptions) applyOneObject(info *resource.Info) error {
klog.V(4).Infof("error recording current command: %v", err) klog.V(4).Infof("error recording current command: %v", err)
} }
helper := resource.NewHelper(info.Client, info.Mapping).
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
WithFieldManager(o.FieldManager)
if o.DryRunStrategy == cmdutil.DryRunServer {
// Ensure the APIServer supports server-side dry-run for the resource,
// otherwise fail early.
// For APIServers that don't support server-side dry-run will persist
// changes.
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
return err
}
}
if o.ServerSideApply { if o.ServerSideApply {
// Send the full object to be applied on the server side. // Send the full object to be applied on the server side.
data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object) data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object)
@ -417,14 +431,6 @@ func (o *ApplyOptions) applyOneObject(info *resource.Info) error {
options := metav1.PatchOptions{ options := metav1.PatchOptions{
Force: &o.ForceConflicts, Force: &o.ForceConflicts,
} }
helper := resource.NewHelper(info.Client, info.Mapping).
WithFieldManager(o.FieldManager)
if o.DryRunStrategy == cmdutil.DryRunServer {
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
return err
}
helper.DryRun(true)
}
obj, err := helper.Patch( obj, err := helper.Patch(
info.Namespace, info.Namespace,
info.Name, info.Name,
@ -495,14 +501,6 @@ See http://k8s.io/docs/reference/using-api/api-concepts/#conflicts`, err)
if o.DryRunStrategy != cmdutil.DryRunClient { if o.DryRunStrategy != cmdutil.DryRunClient {
// Then create the resource and skip the three-way merge // Then create the resource and skip the three-way merge
helper := resource.NewHelper(info.Client, info.Mapping).
WithFieldManager(o.FieldManager)
if o.DryRunStrategy == cmdutil.DryRunServer {
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
helper.DryRun(true)
}
obj, err := helper.Create(info.Namespace, true, info.Object) obj, err := helper.Create(info.Namespace, true, info.Object)
if err != nil { if err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err) return cmdutil.AddSourceToErr("creating", info.Source, err)
@ -539,7 +537,7 @@ See http://k8s.io/docs/reference/using-api/api-concepts/#conflicts`, err)
fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, o.cmdBaseName) fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, o.cmdBaseName)
} }
patcher, err := newPatcher(o, info) patcher, err := newPatcher(o, info, helper)
if err != nil { if err != nil {
return err return err
} }

View File

@ -44,7 +44,6 @@ import (
dynamicfakeclient "k8s.io/client-go/dynamic/fake" dynamicfakeclient "k8s.io/client-go/dynamic/fake"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"k8s.io/client-go/rest/fake" "k8s.io/client-go/rest/fake"
clienttesting "k8s.io/client-go/testing"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
@ -1333,6 +1332,11 @@ func TestForceApply(t *testing.T) {
} }
t.Fatalf("unexpected request: %#v after %v tries\n%#v", req.URL, counts["patch"], req) t.Fatalf("unexpected request: %#v after %v tries\n%#v", req.URL, counts["patch"], req)
return nil, nil return nil, nil
case strings.HasSuffix(p, pathRC) && m == "DELETE":
counts["delete"]++
deleted = true
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
case strings.HasSuffix(p, pathRC) && m == "PUT": case strings.HasSuffix(p, pathRC) && m == "PUT":
counts["put"]++ counts["put"]++
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC)) bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
@ -1351,16 +1355,6 @@ func TestForceApply(t *testing.T) {
}), }),
} }
fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
fakeDynamicClient.PrependReactor("delete", "replicationcontrollers", func(action clienttesting.Action) (bool, runtime.Object, error) {
if deleteAction, ok := action.(clienttesting.DeleteAction); ok {
if deleteAction.GetName() == nameRC {
counts["delete"]++
deleted = true
return true, nil, nil
}
}
return false, nil, nil
})
tf.FakeDynamicClient = fakeDynamicClient tf.FakeDynamicClient = fakeDynamicClient
tf.OpenAPISchemaFunc = fn tf.OpenAPISchemaFunc = fn
tf.Client = tf.UnstructuredClient tf.Client = tf.UnstructuredClient

View File

@ -33,7 +33,6 @@ import (
"k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic"
oapi "k8s.io/kube-openapi/pkg/util/proto" oapi "k8s.io/kube-openapi/pkg/util/proto"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
@ -52,18 +51,16 @@ const (
// Patcher defines options to patch OpenAPI objects. // Patcher defines options to patch OpenAPI objects.
type Patcher struct { type Patcher struct {
Mapping *meta.RESTMapping Mapping *meta.RESTMapping
Helper *resource.Helper Helper *resource.Helper
DynamicClient dynamic.Interface
Overwrite bool Overwrite bool
BackOff clockwork.Clock BackOff clockwork.Clock
Force bool Force bool
Cascade bool Cascade bool
Timeout time.Duration Timeout time.Duration
GracePeriod int GracePeriod int
ServerDryRun bool
// If set, forces the patch against a specific resourceVersion // If set, forces the patch against a specific resourceVersion
ResourceVersion *string ResourceVersion *string
@ -74,7 +71,7 @@ type Patcher struct {
OpenapiSchema openapi.Resources OpenapiSchema openapi.Resources
} }
func newPatcher(o *ApplyOptions, info *resource.Info) (*Patcher, error) { func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) {
var openapiSchema openapi.Resources var openapiSchema openapi.Resources
if o.OpenAPIPatch { if o.OpenAPIPatch {
openapiSchema = o.OpenAPISchema openapiSchema = o.OpenAPISchema
@ -82,22 +79,22 @@ func newPatcher(o *ApplyOptions, info *resource.Info) (*Patcher, error) {
return &Patcher{ return &Patcher{
Mapping: info.Mapping, Mapping: info.Mapping,
Helper: resource.NewHelper(info.Client, info.Mapping).WithFieldManager(o.FieldManager), Helper: helper,
DynamicClient: o.DynamicClient,
Overwrite: o.Overwrite, Overwrite: o.Overwrite,
BackOff: clockwork.NewRealClock(), BackOff: clockwork.NewRealClock(),
Force: o.DeleteOptions.ForceDeletion, Force: o.DeleteOptions.ForceDeletion,
Cascade: o.DeleteOptions.Cascade, Cascade: o.DeleteOptions.Cascade,
Timeout: o.DeleteOptions.Timeout, Timeout: o.DeleteOptions.Timeout,
GracePeriod: o.DeleteOptions.GracePeriod, GracePeriod: o.DeleteOptions.GracePeriod,
ServerDryRun: o.DryRunStrategy == cmdutil.DryRunServer,
OpenapiSchema: openapiSchema, OpenapiSchema: openapiSchema,
Retries: maxPatchRetry, Retries: maxPatchRetry,
}, nil }, nil
} }
func (p *Patcher) delete(namespace, name string) error { func (p *Patcher) delete(namespace, name string) error {
return runDelete(namespace, name, p.Mapping, p.DynamicClient, p.Cascade, p.GracePeriod, p.ServerDryRun) options := asDeleteOptions(p.Cascade, p.GracePeriod)
_, err := p.Helper.DeleteWithOptions(namespace, name, &options)
return err
} }
func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) { 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
} }
} }
patchedObj, err := p.Helper.DryRun(p.ServerDryRun).Patch(namespace, name, patchType, patch, nil) patchedObj, err := p.Helper.Patch(namespace, name, patchType, patch, nil)
return patch, patchedObj, err return patch, patchedObj, err
} }
@ -223,11 +220,11 @@ func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, name
if err != nil { if err != nil {
return modified, nil, err return modified, nil, err
} }
createdObject, err := p.Helper.DryRun(p.ServerDryRun).Create(namespace, true, versionedObject) createdObject, err := p.Helper.Create(namespace, true, versionedObject)
if err != nil { if err != nil {
// restore the original object if we fail to create the new one // restore the original object if we fail to create the new one
// but still propagate and advertise error to user // but still propagate and advertise error to user
recreated, recreateErr := p.Helper.DryRun(p.ServerDryRun).Create(namespace, true, original) recreated, recreateErr := p.Helper.Create(namespace, true, original)
if recreateErr != nil { if recreateErr != nil {
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) 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)
} else { } else {

View File

@ -143,19 +143,24 @@ func (p *pruner) delete(namespace, name string, mapping *meta.RESTMapping) error
} }
func runDelete(namespace, name string, mapping *meta.RESTMapping, c dynamic.Interface, cascade bool, gracePeriod int, serverDryRun bool) error { func runDelete(namespace, name string, mapping *meta.RESTMapping, c dynamic.Interface, cascade bool, gracePeriod int, serverDryRun bool) error {
options := asDeleteOptions(cascade, gracePeriod)
if serverDryRun {
options.DryRun = []string{metav1.DryRunAll}
}
return c.Resource(mapping.Resource).Namespace(namespace).Delete(context.TODO(), name, options)
}
func asDeleteOptions(cascade bool, gracePeriod int) metav1.DeleteOptions {
options := metav1.DeleteOptions{} options := metav1.DeleteOptions{}
if gracePeriod >= 0 { if gracePeriod >= 0 {
options = *metav1.NewDeleteOptions(int64(gracePeriod)) options = *metav1.NewDeleteOptions(int64(gracePeriod))
} }
if serverDryRun {
options.DryRun = []string{metav1.DryRunAll}
}
policy := metav1.DeletePropagationForeground policy := metav1.DeletePropagationForeground
if !cascade { if !cascade {
policy = metav1.DeletePropagationOrphan policy = metav1.DeletePropagationOrphan
} }
options.PropagationPolicy = &policy options.PropagationPolicy = &policy
return c.Resource(mapping.Resource).Namespace(namespace).Delete(context.TODO(), name, options) return options
} }
type pruneResource struct { type pruneResource struct {

View File

@ -367,7 +367,6 @@ func (obj InfoObject) Merged() (runtime.Object, error) {
Helper: helper, Helper: helper,
Overwrite: true, Overwrite: true,
BackOff: clockwork.NewRealClock(), BackOff: clockwork.NewRealClock(),
ServerDryRun: true,
OpenapiSchema: obj.OpenAPI, OpenapiSchema: obj.OpenAPI,
ResourceVersion: resourceVersion, ResourceVersion: resourceVersion,
} }

View File

@ -523,8 +523,23 @@ func GetFieldManagerFlag(cmd *cobra.Command) string {
type DryRunStrategy int type DryRunStrategy int
const ( const (
// DryRunNone indicates the client will make all mutating calls
DryRunNone DryRunStrategy = iota DryRunNone DryRunStrategy = iota
// DryRunClient, or client-side dry-run, indicates the client will prevent
// making mutating calls such as CREATE, PATCH, and DELETE
DryRunClient DryRunClient
// DryRunServer, or server-side dry-run, indicates the client will send
// mutating calls to the APIServer with the dry-run parameter to prevent
// persisting changes.
//
// Note that clients sending server-side dry-run calls should verify that
// the APIServer and the resource supports server-side dry-run, and otherwise
// clients should fail early.
//
// If a client sends a server-side dry-run call to an APIServer that doesn't
// support server-side dry-run, then the APIServer will persist changes inadvertently.
DryRunServer DryRunServer
) )

View File

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