Add --server-dry-run flag to `kubectl apply`

pull/8/head
Antoine Pelisse 2018-08-30 06:33:34 -07:00
parent 8aea674681
commit 967280b58e
25 changed files with 110 additions and 48 deletions

View File

@ -40,12 +40,16 @@ function run_kube_apiserver() {
# Include RBAC (to exercise bootstrapping), and AlwaysAllow to allow all actions
AUTHORIZATION_MODE="RBAC,AlwaysAllow"
# Enable features
ENABLE_FEATURE_GATES="DryRun=true"
"${KUBE_OUTPUT_HOSTBIN}/kube-apiserver" \
--insecure-bind-address="127.0.0.1" \
--bind-address="127.0.0.1" \
--insecure-port="${API_PORT}" \
--authorization-mode="${AUTHORIZATION_MODE}" \
--secure-port="${SECURE_API_PORT}" \
--feature-gates="${ENABLE_FEATURE_GATES}" \
--enable-admission-plugins="${ENABLE_ADMISSION_PLUGINS}" \
--disable-admission-plugins="${DISABLE_ADMISSION_PLUGINS}" \
--etcd-servers="http://${ETCD_HOST}:${ETCD_PORT}" \

View File

@ -294,7 +294,7 @@ func (o AnnotateOptions) RunAnnotate() error {
helper := resource.NewHelper(client, mapping)
if createdPatch {
outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes)
outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes, nil)
} else {
outputObj, err = helper.Replace(namespace, name, false, obj)
}

View File

@ -66,6 +66,7 @@ type ApplyOptions struct {
Selector string
DryRun bool
ServerDryRun bool
Prune bool
PruneResources []pruneResource
cmdBaseName string
@ -172,6 +173,7 @@ func NewCmdApply(baseName string, f cmdutil.Factory, ioStreams genericclioptions
cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources in the namespace of the specified resource types.")
cmd.Flags().StringArrayVar(&o.PruneWhitelist, "prune-whitelist", o.PruneWhitelist, "Overwrite the default whitelist with <group/version/kind> for --prune")
cmd.Flags().BoolVar(&o.OpenApiPatch, "openapi-patch", o.OpenApiPatch, "If true, use openapi to calculate diff when the openapi presents and the resource can be found in the openapi spec. Otherwise, fall back to use baked-in types.")
cmd.Flags().BoolVar(&o.ServerDryRun, "server-dry-run", o.ServerDryRun, "If true, request will be sent to server with dry-run flag, which means the modifications won't be persisted. This is an alpha feature and flag.")
cmdutil.AddDryRunFlag(cmd)
cmdutil.AddIncludeUninitializedFlag(cmd)
@ -186,13 +188,19 @@ func NewCmdApply(baseName string, f cmdutil.Factory, ioStreams genericclioptions
func (o *ApplyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
o.DryRun = cmdutil.GetDryRunFlag(cmd)
if o.DryRun && o.ServerDryRun {
return fmt.Errorf("--dry-run and --server-dry-run can't be used together")
}
// allow for a success message operation to be specified at print time
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
o.PrintFlags.NamePrintFlags.Operation = operation
if o.DryRun {
o.PrintFlags.Complete("%s (dry run)")
}
if o.ServerDryRun {
o.PrintFlags.Complete("%s (server dry run)")
}
return o.PrintFlags.ToPrinter()
}
@ -354,7 +362,11 @@ func (o *ApplyOptions) Run() error {
if !o.DryRun {
// Then create the resource and skip the three-way merge
obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object)
options := metav1.CreateOptions{}
if o.ServerDryRun {
options.DryRun = []string{metav1.DryRunAll}
}
obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, &options)
if err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
@ -402,6 +414,7 @@ func (o *ApplyOptions) Run() error {
cascade: o.DeleteOptions.Cascade,
timeout: o.DeleteOptions.Timeout,
gracePeriod: o.DeleteOptions.GracePeriod,
serverDryRun: o.ServerDryRun,
openapiSchema: openapiSchema,
}
@ -485,6 +498,7 @@ func (o *ApplyOptions) Run() error {
cascade: o.DeleteOptions.Cascade,
dryRun: o.DryRun,
serverDryRun: o.ServerDryRun,
gracePeriod: o.DeleteOptions.GracePeriod,
toPrinter: o.ToPrinter,
@ -573,6 +587,7 @@ type pruner struct {
fieldSelector string
cascade bool
serverDryRun bool
dryRun bool
gracePeriod int
@ -629,14 +644,17 @@ func (p *pruner) prune(namespace string, mapping *meta.RESTMapping, includeUnini
}
func (p *pruner) delete(namespace, name string, mapping *meta.RESTMapping) error {
return runDelete(namespace, name, mapping, p.dynamicClient, p.cascade, p.gracePeriod)
return runDelete(namespace, name, mapping, p.dynamicClient, p.cascade, p.gracePeriod, p.serverDryRun)
}
func runDelete(namespace, name string, mapping *meta.RESTMapping, c dynamic.Interface, cascade bool, gracePeriod int) error {
func runDelete(namespace, name string, mapping *meta.RESTMapping, c dynamic.Interface, cascade bool, gracePeriod int, serverDryRun bool) error {
options := &metav1.DeleteOptions{}
if gracePeriod >= 0 {
options = metav1.NewDeleteOptions(int64(gracePeriod))
}
if serverDryRun {
options.DryRun = []string{metav1.DryRunAll}
}
policy := metav1.DeletePropagationForeground
if !cascade {
policy = metav1.DeletePropagationOrphan
@ -646,7 +664,7 @@ func runDelete(namespace, name string, mapping *meta.RESTMapping, c dynamic.Inte
}
func (p *patcher) delete(namespace, name string) error {
return runDelete(namespace, name, p.mapping, p.dynamicClient, p.cascade, p.gracePeriod)
return runDelete(namespace, name, p.mapping, p.dynamicClient, p.cascade, p.gracePeriod, p.serverDryRun)
}
type patcher struct {
@ -661,6 +679,7 @@ type patcher struct {
cascade bool
timeout time.Duration
gracePeriod int
serverDryRun bool
openapiSchema openapi.Resources
}
@ -736,7 +755,12 @@ func (p *patcher) patchSimple(obj runtime.Object, modified []byte, source, names
return patch, obj, nil
}
patchedObj, err := p.helper.Patch(namespace, name, patchType, patch)
options := metav1.UpdateOptions{}
if p.serverDryRun {
options.DryRun = []string{metav1.DryRunAll}
}
patchedObj, err := p.helper.Patch(namespace, name, patchType, patch, &options)
return patch, patchedObj, err
}
@ -776,11 +800,15 @@ func (p *patcher) deleteAndCreate(original runtime.Object, modified []byte, name
if err != nil {
return modified, nil, err
}
createdObject, err := p.helper.Create(namespace, true, versionedObject)
options := metav1.CreateOptions{}
if p.serverDryRun {
options.DryRun = []string{metav1.DryRunAll}
}
createdObject, err := p.helper.Create(namespace, true, versionedObject, &options)
if err != nil {
// restore the original object if we fail to create the new one
// but still propagate and advertise error to user
recreated, recreateErr := p.helper.Create(namespace, true, original)
recreated, recreateErr := p.helper.Create(namespace, true, original, &options)
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\n", err, recreateErr)
} else {

View File

@ -200,7 +200,7 @@ func (o *SetLastAppliedOptions) RunSetLastApplied() error {
return err
}
helper := resource.NewHelper(client, mapping)
finalObj, err = helper.Patch(o.namespace, info.Name, patch.PatchType, patch.Patch)
finalObj, err = helper.Patch(o.namespace, info.Name, patch.PatchType, patch.Patch, nil)
if err != nil {
return err
}

View File

@ -318,7 +318,7 @@ func RunEditOnCreate(f cmdutil.Factory, printFlags *genericclioptions.PrintFlags
// createAndRefresh creates an object from input info and refreshes info with that object
func createAndRefresh(info *resource.Info) error {
obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object)
obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, nil)
if err != nil {
return err
}

View File

@ -760,7 +760,7 @@ func (o *DrainOptions) RunCordonOrUncordon(desired bool) error {
fmt.Printf("error: unable to %s node %q: %v", cordonOrUncordon, nodeInfo.Name, err)
continue
}
_, err = helper.Patch(o.Namespace, nodeInfo.Name, types.StrategicMergePatchType, patchBytes)
_, err = helper.Patch(o.Namespace, nodeInfo.Name, types.StrategicMergePatchType, patchBytes, nil)
if err != nil {
fmt.Printf("error: unable to %s node %q: %v", cordonOrUncordon, nodeInfo.Name, err)
continue

View File

@ -304,7 +304,7 @@ func (o *LabelOptions) RunLabel() error {
helper := resource.NewHelper(client, mapping)
if createdPatch {
outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes)
outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes, nil)
} else {
outputObj, err = helper.Replace(namespace, name, false, obj)
}

View File

@ -220,7 +220,7 @@ func (o *PatchOptions) RunPatch() error {
}
helper := resource.NewHelper(client, mapping)
patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes)
patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil)
if err != nil {
return err
}
@ -231,7 +231,7 @@ func (o *PatchOptions) RunPatch() error {
if mergePatch, err := o.Recorder.MakeRecordMergePatch(patchedObj); err != nil {
glog.V(4).Infof("error recording current command: %v", err)
} else if len(mergePatch) > 0 {
if recordedObj, err := helper.Patch(info.Namespace, info.Name, types.MergePatchType, mergePatch); err != nil {
if recordedObj, err := helper.Patch(info.Namespace, info.Name, types.MergePatchType, mergePatch, nil); err != nil {
glog.V(4).Infof("error recording reason: %v", err)
} else {
patchedObj = recordedObj

View File

@ -317,7 +317,7 @@ func (o *ReplaceOptions) forceReplace() error {
glog.V(4).Infof("error recording current command: %v", err)
}
obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object)
obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, nil)
if err != nil {
return err
}

View File

@ -174,7 +174,7 @@ func (o PauseConfig) RunPause() error {
continue
}
obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch)
obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
if err != nil {
allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err))
continue

View File

@ -178,7 +178,7 @@ func (o ResumeOptions) RunResume() error {
continue
}
obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch)
obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
if err != nil {
allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err))
continue

View File

@ -667,7 +667,7 @@ func (o *RunOptions) createGeneratedObject(f cmdutil.Factory, cmd *cobra.Command
if err != nil {
return nil, err
}
actualObj, err = resource.NewHelper(client, mapping).Create(namespace, false, obj)
actualObj, err = resource.NewHelper(client, mapping).Create(namespace, false, obj, nil)
if err != nil {
return nil, err
}

View File

@ -244,7 +244,7 @@ func (o *ScaleOptions) RunScale() error {
return err
}
helper := resource.NewHelper(client, mapping)
if _, err := helper.Patch(info.Namespace, info.Name, types.MergePatchType, mergePatch); err != nil {
if _, err := helper.Patch(info.Namespace, info.Name, types.MergePatchType, mergePatch, nil); err != nil {
glog.V(4).Infof("error recording reason: %v", err)
}
}

View File

@ -487,7 +487,7 @@ func (o *EnvOptions) RunEnv() error {
continue
}
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch)
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
if err != nil {
allErrs = append(allErrs, fmt.Errorf("failed to patch env update to pod template: %v\n", err))
continue

View File

@ -282,7 +282,7 @@ func (o *SetImageOptions) Run() error {
}
// patch the change
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch)
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
if err != nil {
allErrs = append(allErrs, fmt.Errorf("failed to patch image update to pod template: %v\n", err))
continue

View File

@ -276,7 +276,7 @@ func (o *SetResourcesOptions) Run() error {
continue
}
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch)
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
if err != nil {
allErrs = append(allErrs, fmt.Errorf("failed to patch limit update to pod template %v\n", err))
continue

View File

@ -184,7 +184,7 @@ func (o *SetSelectorOptions) RunSelector() error {
return o.PrintObj(info.Object, o.Out)
}
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch)
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
if err != nil {
return err
}

View File

@ -202,7 +202,7 @@ func (o *SetServiceAccountOptions) Run() error {
}
continue
}
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch)
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
if err != nil {
patchErrs = append(patchErrs, fmt.Errorf("failed to patch ServiceAccountName %v", err))
continue

View File

@ -255,7 +255,7 @@ func (o *SubjectOptions) Run(fn updateSubjects) error {
continue
}
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch)
actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
if err != nil {
allErrs = append(allErrs, fmt.Errorf("failed to patch subjects to rolebinding: %v\n", err))
continue

View File

@ -274,7 +274,7 @@ func (o TaintOptions) RunTaint() error {
var outputObj runtime.Object
if createdPatch {
outputObj, err = helper.Patch(namespace, name, types.StrategicMergePatchType, patchBytes)
outputObj, err = helper.Patch(namespace, name, types.StrategicMergePatchType, patchBytes, nil)
} else {
outputObj, err = helper.Replace(namespace, name, false, obj)
}

View File

@ -503,7 +503,7 @@ func (o *EditOptions) annotationPatch(update *resource.Info) error {
return err
}
helper := resource.NewHelper(client, mapping)
_, err = helper.Patch(o.CmdNamespace, update.Name, patchType, patch)
_, err = helper.Patch(o.CmdNamespace, update.Name, patchType, patch, nil)
if err != nil {
return err
}
@ -632,7 +632,7 @@ func (o *EditOptions) visitToPatch(originalInfos []*resource.Info, patchVisitor
fmt.Fprintf(o.Out, "Patch: %s\n", string(patch))
}
patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, patchType, patch)
patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, patchType, patch, nil)
if err != nil {
fmt.Fprintln(o.ErrOut, results.addError(err, info))
return nil

View File

@ -108,13 +108,16 @@ func (m *Helper) DeleteWithOptions(namespace, name string, options *metav1.Delet
Get()
}
func (m *Helper) Create(namespace string, modify bool, obj runtime.Object) (runtime.Object, error) {
func (m *Helper) Create(namespace string, modify bool, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
if options == nil {
options = &metav1.CreateOptions{}
}
if modify {
// Attempt to version the object based on client logic.
version, err := metadataAccessor.ResourceVersion(obj)
if err != nil {
// We don't know how to clear the version on this object, so send it to the server as is
return m.createResource(m.RESTClient, m.Resource, namespace, obj)
return m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
}
if version != "" {
if err := metadataAccessor.SetResourceVersion(obj, ""); err != nil {
@ -123,17 +126,27 @@ func (m *Helper) Create(namespace string, modify bool, obj runtime.Object) (runt
}
}
return m.createResource(m.RESTClient, m.Resource, namespace, obj)
return m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
}
func (m *Helper) createResource(c RESTClient, resource, namespace string, obj runtime.Object) (runtime.Object, error) {
return c.Post().NamespaceIfScoped(namespace, m.NamespaceScoped).Resource(resource).Body(obj).Do().Get()
func (m *Helper) createResource(c RESTClient, resource, namespace string, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
return c.Post().
NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(resource).
VersionedParams(options, metav1.ParameterCodec).
Body(obj).
Do().
Get()
}
func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte, options *metav1.UpdateOptions) (runtime.Object, error) {
if options == nil {
options = &metav1.UpdateOptions{}
}
func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte) (runtime.Object, error) {
return m.RESTClient.Patch(pt).
NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource).
Name(name).
VersionedParams(options, metav1.ParameterCodec).
Body(data).
Do().
Get()

View File

@ -228,7 +228,7 @@ func TestHelperCreate(t *testing.T) {
RESTClient: client,
NamespaceScoped: true,
}
_, err := modifier.Create("bar", tt.Modify, tt.Object)
_, err := modifier.Create("bar", tt.Modify, tt.Object, nil)
if (err != nil) != tt.Err {
t.Errorf("%d: unexpected error: %t %v", i, tt.Err, err)
}

View File

@ -656,7 +656,7 @@ func RetrieveLazy(info *Info, err error) error {
// CreateAndRefresh creates an object from input info and refreshes info with that object
func CreateAndRefresh(info *Info) error {
obj, err := NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object)
obj, err := NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, nil)
if err != nil {
return err
}

View File

@ -75,6 +75,23 @@ run_kubectl_apply_tests() {
# cleanup
kubectl delete pods selector-test-pod
## kubectl apply --server-dry-run
# Pre-Condition: no POD exists
kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" ''
# apply dry-run
kubectl apply --server-dry-run -f hack/testdata/pod.yaml "${kube_flags[@]}"
# No pod exists
kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" ''
# apply non dry-run creates the pod
kubectl apply -f hack/testdata/pod.yaml "${kube_flags[@]}"
# apply changes
kubectl apply --server-dry-run -f hack/testdata/pod-apply.yaml "${kube_flags[@]}"
# Post-Condition: label still has initial value
kube::test::get_object_assert 'pods test-pod' "{{${labels_field}.name}}" 'test-pod-label'
# clean-up
kubectl delete -f hack/testdata/pod.yaml "${kube_flags[@]}"
## kubectl apply --prune
# Pre-Condition: no POD exists