diff --git a/staging/src/k8s.io/cli-runtime/pkg/printers/warningprinter.go b/staging/src/k8s.io/cli-runtime/pkg/printers/warningprinter.go new file mode 100644 index 00000000000..b3a8264f78a --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/printers/warningprinter.go @@ -0,0 +1,55 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package printers + +import ( + "fmt" + "io" +) + +const ( + yellowColor = "\u001b[33;1m" + resetColor = "\u001b[0m" +) + +type WarningPrinter struct { + // out is the writer to output warnings to + out io.Writer + // opts contains options controlling warning output + opts WarningPrinterOptions +} + +// WarningPrinterOptions controls the behavior of a WarningPrinter constructed using NewWarningPrinter() +type WarningPrinterOptions struct { + // Color indicates that warning output can include ANSI color codes + Color bool +} + +// NewWarningPrinter returns an implementation of warningPrinter that outputs warnings to the specified writer. +func NewWarningPrinter(out io.Writer, opts WarningPrinterOptions) *WarningPrinter { + h := &WarningPrinter{out: out, opts: opts} + return h +} + +// Print prints warnings to the configured writer. +func (w *WarningPrinter) Print(message string) { + if w.opts.Color { + fmt.Fprintf(w.out, "%sWarning:%s %s\n", yellowColor, resetColor, message) + } else { + fmt.Fprintf(w.out, "Warning: %s\n", message) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani.go b/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani.go index 3e140fe12ca..60b6e16a43f 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani.go @@ -43,6 +43,7 @@ import ( "k8s.io/kubectl/pkg/describe" rbacutil "k8s.io/kubectl/pkg/util/rbac" "k8s.io/kubectl/pkg/util/templates" + "k8s.io/kubectl/pkg/util/term" ) // CanIOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of @@ -63,6 +64,7 @@ type CanIOptions struct { List bool genericclioptions.IOStreams + warningPrinter *printers.WarningPrinter } var ( @@ -144,6 +146,8 @@ func NewCmdCanI(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C // Complete completes all the required options func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error { + o.warningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) + if o.List { if len(args) != 0 { return errors.New("list option must be specified with no arguments") @@ -201,6 +205,10 @@ func (o *CanIOptions) Validate() error { return nil } + if o.warningPrinter == nil { + return fmt.Errorf("warningPrinter can not be used without initialization") + } + if o.NonResourceURL != "" { if o.Subresource != "" { return fmt.Errorf("--subresource can not be used with NonResourceURL") @@ -209,20 +217,19 @@ func (o *CanIOptions) Validate() error { return fmt.Errorf("NonResourceURL and ResourceName can not specified together") } if !isKnownNonResourceVerb(o.Verb) { - fmt.Fprintf(o.ErrOut, "Warning: verb '%s' is not a known verb\n", o.Verb) + o.warningPrinter.Print(fmt.Sprintf("verb '%s' is not a known verb\n", o.Verb)) } } else if !o.Resource.Empty() && !o.AllNamespaces && o.DiscoveryClient != nil { if namespaced, err := isNamespaced(o.Resource, o.DiscoveryClient); err == nil && !namespaced { if len(o.Resource.Group) == 0 { - fmt.Fprintf(o.ErrOut, "Warning: resource '%s' is not namespace scoped\n", o.Resource.Resource) + o.warningPrinter.Print(fmt.Sprintf("resource '%s' is not namespace scoped\n", o.Resource.Resource)) } else { - fmt.Fprintf(o.ErrOut, "Warning: resource '%s' is not namespace scoped in group '%s'\n", o.Resource.Resource, o.Resource.Group) + o.warningPrinter.Print(fmt.Sprintf("resource '%s' is not namespace scoped in group '%s'\n", o.Resource.Resource, o.Resource.Group)) } } if !isKnownResourceVerb(o.Verb) { - fmt.Fprintf(o.ErrOut, "Warning: verb '%s' is not a known verb\n", o.Verb) + o.warningPrinter.Print(fmt.Sprintf("verb '%s' is not a known verb\n", o.Verb)) } - } if o.NoHeaders { @@ -309,9 +316,9 @@ func (o *CanIOptions) resourceFor(mapper meta.RESTMapper, resourceArg string) sc if err != nil { if !nonStandardResourceNames.Has(groupResource.String()) { if len(groupResource.Group) == 0 { - fmt.Fprintf(o.ErrOut, "Warning: the server doesn't have a resource type '%s'\n", groupResource.Resource) + o.warningPrinter.Print(fmt.Sprintf("the server doesn't have a resource type '%s'\n", groupResource.Resource)) } else { - fmt.Fprintf(o.ErrOut, "Warning: the server doesn't have a resource type '%s' in group '%s'\n", groupResource.Resource, groupResource.Group) + o.warningPrinter.Print(fmt.Sprintf("the server doesn't have a resource type '%s' in group '%s'\n", groupResource.Resource, groupResource.Group)) } } return schema.GroupVersionResource{Resource: resourceArg} @@ -323,7 +330,7 @@ func (o *CanIOptions) resourceFor(mapper meta.RESTMapper, resourceArg string) sc func (o *CanIOptions) printStatus(status authorizationv1.SubjectRulesReviewStatus) error { if status.Incomplete { - fmt.Fprintf(o.ErrOut, "warning: the list may be incomplete: %v\n", status.EvaluationError) + o.warningPrinter.Print(fmt.Sprintf("the list may be incomplete: %v", status.EvaluationError)) } breakdownRules := []rbacv1.PolicyRule{} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani_test.go index deff3708274..2924ca4f526 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani_test.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" @@ -281,7 +282,7 @@ func TestRunResourceFor(t *testing.T) { expectGVR: schema.GroupVersionResource{ Resource: "invalid", }, - expectedErrOut: "Warning: the server doesn't have a resource type 'invalid'\n", + expectedErrOut: "Warning: the server doesn't have a resource type 'invalid'\n\n", }, } @@ -292,6 +293,7 @@ func TestRunResourceFor(t *testing.T) { ioStreams, _, _, buf := genericclioptions.NewTestIOStreams() test.o.IOStreams = ioStreams + test.o.warningPrinter = printers.NewWarningPrinter(test.o.IOStreams.ErrOut, printers.WarningPrinterOptions{Color: false}) restMapper, err := tf.ToRESTMapper() if err != nil { diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go index 12abb8b2a2f..0b830509bd4 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go @@ -37,6 +37,7 @@ import ( "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" @@ -50,6 +51,7 @@ import ( "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" + "k8s.io/kubectl/pkg/util/term" "k8s.io/utils/pointer" ) @@ -127,6 +129,7 @@ type DebugOptions struct { podClient corev1client.CoreV1Interface genericclioptions.IOStreams + warningPrinter *printers.WarningPrinter } // NewDebugOptions returns a DebugOptions initialized with default values. @@ -217,6 +220,9 @@ func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st o.attachChanged = cmd.Flags().Changed("attach") o.shareProcessedChanged = cmd.Flags().Changed("share-processes") + // Warning printer + o.warningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) + return nil } @@ -293,6 +299,11 @@ func (o *DebugOptions) Validate() error { return fmt.Errorf("-i/--stdin is required for containers with -t/--tty=true") } + // warningPrinter + if o.warningPrinter == nil { + return fmt.Errorf("warningPrinter can not be used without initialization") + } + return nil } @@ -742,7 +753,7 @@ func (o *DebugOptions) waitForContainer(ctx context.Context, ns, podName, contai return true, nil } if !o.Quiet && s.State.Waiting != nil && s.State.Waiting.Message != "" { - fmt.Fprintf(o.ErrOut, "Warning: container %s: %s\n", containerName, s.State.Waiting.Message) + o.warningPrinter.Print(fmt.Sprintf("container %s: %s", containerName, s.State.Waiting.Message)) } return false, nil }) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go b/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go index f59e99e8725..f53bb5b7f80 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go @@ -39,6 +39,7 @@ import ( "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" + "k8s.io/kubectl/pkg/util/term" ) var ( @@ -133,6 +134,7 @@ type DeleteOptions struct { Result *resource.Result genericclioptions.IOStreams + warningPrinter *printers.WarningPrinter } func NewCmdDelete(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { @@ -230,6 +232,8 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co } } + o.warningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) + return nil } @@ -244,10 +248,13 @@ func (o *DeleteOptions) Validate() error { if o.DeleteAll && len(o.FieldSelector) > 0 { return fmt.Errorf("cannot set --all and --field-selector at the same time") } + if o.warningPrinter == nil { + return fmt.Errorf("warningPrinter can not be used without initialization") + } switch { case o.GracePeriod == 0 && o.ForceDeletion: - fmt.Fprintf(o.ErrOut, "warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n") + o.warningPrinter.Print("Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.") case o.GracePeriod > 0 && o.ForceDeletion: return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together") } @@ -311,7 +318,7 @@ func (o *DeleteOptions) DeleteResult(r *resource.Result) error { options.PropagationPolicy = &o.CascadingStrategy if warnClusterScope && info.Mapping.Scope.Name() == meta.RESTScopeNameRoot { - fmt.Fprintf(o.ErrOut, "warning: deleting cluster-scoped resources, not scoped to the provided namespace\n") + o.warningPrinter.Print("deleting cluster-scoped resources, not scoped to the provided namespace") warnClusterScope = false } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_test.go index dbe94cad3e7..50903f28c0e 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_test.go @@ -290,7 +290,7 @@ func TestGracePeriodScenarios(t *testing.T) { forceFlag: true, expectedGracePeriod: "0", expectedOut: "pod/foo\n", - expectedErrOut: "warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n", + expectedErrOut: "Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n", expectedDeleteRequestPath: "/namespaces/test/pods/foo", }, { @@ -300,7 +300,7 @@ func TestGracePeriodScenarios(t *testing.T) { gracePeriodFlag: "0", expectedGracePeriod: "0", expectedOut: "pod/foo\n", - expectedErrOut: "warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n", + expectedErrOut: "Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n", expectedDeleteRequestPath: "/namespaces/test/pods/foo", }, { diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain.go b/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain.go index c867aa9bf42..afa55c57d38 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain.go @@ -27,7 +27,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" @@ -37,6 +36,7 @@ import ( "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" + "k8s.io/kubectl/pkg/util/term" ) type DrainCmdOptions struct { @@ -49,6 +49,7 @@ type DrainCmdOptions struct { nodeInfos []*resource.Info genericclioptions.IOStreams + warningPrinter *printers.WarningPrinter } var ( @@ -258,6 +259,8 @@ func (o *DrainCmdOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args [ return printer.PrintObj, nil } + o.warningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) + builder := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). @@ -338,7 +341,7 @@ func (o *DrainCmdOptions) deleteOrEvictPodsSimple(nodeInfo *resource.Info) error return utilerrors.NewAggregate(errs) } if warnings := list.Warnings(); warnings != "" { - fmt.Fprintf(o.ErrOut, "WARNING: %s\n", warnings) + o.warningPrinter.Print(warnings) } if o.drainer.DryRunStrategy == cmdutil.DryRunClient { for _, pod := range list.Pods() { diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain_test.go index b3f33ab9677..aa602ed482a 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain_test.go @@ -599,7 +599,7 @@ func TestDrain(t *testing.T) { args: []string{"node", "--force"}, expectFatal: false, expectDelete: true, - expectWarning: "WARNING: deleting Pods that declare no controller: default/bar", + expectWarning: "Warning: deleting Pods that declare no controller: default/bar", expectOutputToContain: "node/node drained", }, { @@ -620,7 +620,7 @@ func TestDrain(t *testing.T) { pods: []corev1.Pod{dsPodWithEmptyDir}, rcs: []corev1.ReplicationController{rc}, args: []string{"node", "--ignore-daemonsets"}, - expectWarning: "WARNING: ignoring DaemonSet-managed Pods: default/bar", + expectWarning: "Warning: ignoring DaemonSet-managed Pods: default/bar", expectFatal: false, expectDelete: false, expectOutputToContain: "node/node drained", diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go b/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go index b45fcfde21b..78da2e553c7 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go @@ -40,6 +40,7 @@ import ( "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" + "k8s.io/kubectl/pkg/util/term" ) var ( @@ -130,6 +131,7 @@ type EnvOptions struct { clientset *kubernetes.Clientset genericclioptions.IOStreams + warningPrinter *printers.WarningPrinter } // NewEnvOptions returns an EnvOptions indicating all containers in the selected @@ -140,14 +142,14 @@ func NewEnvOptions(streams genericclioptions.IOStreams) *EnvOptions { ContainerSelector: "*", Overwrite: true, - - IOStreams: streams, + IOStreams: streams, } } // NewCmdEnv implements the OpenShift cli env command func NewCmdEnv(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := NewEnvOptions(streams) + cmd := &cobra.Command{ Use: "env RESOURCE/NAME KEY_1=VAL_1 ... KEY_N=VAL_N", DisableFlagsInUseLine: true, @@ -206,7 +208,7 @@ func contains(key string, keyList []string) bool { func (o *EnvOptions) keyToEnvName(key string) string { envName := strings.ToUpper(validEnvNameRegexp.ReplaceAllString(key, "_")) if envName != key { - fmt.Fprintf(o.ErrOut, "warning: key %s transferred to %s\n", key, envName) + o.warningPrinter.Print(fmt.Sprintf("key %s transferred to %s", key, envName)) } return envName } @@ -251,6 +253,7 @@ func (o *EnvOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri return err } o.builder = f.NewBuilder + o.warningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) return nil } @@ -269,6 +272,9 @@ func (o *EnvOptions) Validate() error { if len(o.Keys) > 0 && len(o.From) == 0 { return fmt.Errorf("when specifying --keys, a configmap or secret must be provided with --from") } + if o.warningPrinter == nil { + return fmt.Errorf("warningPrinter can not be used without initialization") + } return nil } @@ -421,7 +427,7 @@ func (o *EnvOptions) RunEnv() error { } } - fmt.Fprintf(o.ErrOut, "warning: %s/%s does not have any containers matching %q\n", objKind, objName, o.ContainerSelector) + o.warningPrinter.Print(fmt.Sprintf("%s/%s does not have any containers matching %q", objKind, objName, o.ContainerSelector)) } return nil } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go index 8c9ad4f1f22..eb6454edfbf 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go @@ -720,9 +720,9 @@ func TestSetEnvFromResource(t *testing.T) { assert.NoError(t, err) err = opts.RunEnv() if input.warning { - assert.Contains(t, errOut.String(), "warning") + assert.Contains(t, errOut.String(), "Warning") } else { - assert.NotContains(t, errOut.String(), "warning") + assert.NotContains(t, errOut.String(), "Warning") } assert.NoError(t, err) }) diff --git a/test/cmd/core.sh b/test/cmd/core.sh index 8273908127e..4ac781fce70 100755 --- a/test/cmd/core.sh +++ b/test/cmd/core.sh @@ -1481,7 +1481,7 @@ run_namespace_tests() { kubectl create namespace my-namespace kube::test::get_object_assert 'namespaces/my-namespace' "{{$id_field}}" 'my-namespace' output_message=$(! kubectl delete namespace -n my-namespace --all 2>&1 "${kube_flags[@]}") - kube::test::if_has_string "${output_message}" 'warning: deleting cluster-scoped resources' + kube::test::if_has_string "${output_message}" 'Warning: deleting cluster-scoped resources' kube::test::if_has_string "${output_message}" 'namespace "my-namespace" deleted' ### Quota diff --git a/test/cmd/rbac.sh b/test/cmd/rbac.sh index 15c77178a1b..796083bf7d9 100755 --- a/test/cmd/rbac.sh +++ b/test/cmd/rbac.sh @@ -41,7 +41,7 @@ run_clusterroles_tests() { kubectl create "${kube_flags[@]:?}" clusterrole pod-admin --verb=* --resource=pods kube::test::get_object_assert clusterrole/pod-admin "{{range.rules}}{{range.verbs}}{{.}}:{{end}}{{end}}" '\*:' output_message=$(kubectl delete clusterrole pod-admin -n test 2>&1 "${kube_flags[@]}") - kube::test::if_has_string "${output_message}" 'warning: deleting cluster-scoped resources' + kube::test::if_has_string "${output_message}" 'Warning: deleting cluster-scoped resources' kube::test::if_has_string "${output_message}" 'clusterrole.rbac.authorization.k8s.io "pod-admin" deleted' kubectl create "${kube_flags[@]}" clusterrole pod-admin --verb=* --resource=pods diff --git a/test/cmd/storage.sh b/test/cmd/storage.sh index 0c1c140c769..b47fd35ef72 100755 --- a/test/cmd/storage.sh +++ b/test/cmd/storage.sh @@ -46,7 +46,7 @@ run_persistent_volumes_tests() { kubectl create -f test/fixtures/doc-yaml/user-guide/persistent-volumes/volumes/local-01.yaml "${kube_flags[@]}" kube::test::get_object_assert pv "{{range.items}}{{$id_field}}:{{end}}" 'pv0001:' output_message=$(kubectl delete pv -n test --all 2>&1 "${kube_flags[@]}") - kube::test::if_has_string "${output_message}" 'warning: deleting cluster-scoped resources' + kube::test::if_has_string "${output_message}" 'Warning: deleting cluster-scoped resources' kube::test::if_has_string "${output_message}" 'persistentvolume "pv0001" deleted' kube::test::get_object_assert pv "{{range.items}}{{$id_field}}:{{end}}" ''