karmadactl support apply command
Signed-off-by: carlory <baofa.fan@daocloud.io>
This commit is contained in:
parent
d2bd9d9823
commit
3610f6dd3e
1
go.mod
1
go.mod
|
@ -88,6 +88,7 @@ require (
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
|
|
@ -0,0 +1,216 @@
|
||||||
|
package karmadactl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
kubectlapply "k8s.io/kubectl/pkg/cmd/apply"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
|
||||||
|
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
|
||||||
|
"github.com/karmada-io/karmada/pkg/karmadactl/options"
|
||||||
|
"github.com/karmada-io/karmada/pkg/util/names"
|
||||||
|
)
|
||||||
|
|
||||||
|
var metadataAccessor = meta.NewAccessor()
|
||||||
|
|
||||||
|
// CommandApplyOptions contains the input to the apply command.
|
||||||
|
type CommandApplyOptions struct {
|
||||||
|
// global flags
|
||||||
|
options.GlobalCommandOptions
|
||||||
|
// apply flags
|
||||||
|
KubectlApplyFlags *kubectlapply.ApplyFlags
|
||||||
|
Namespace string
|
||||||
|
AllClusters bool
|
||||||
|
|
||||||
|
kubectlApplyOptions *kubectlapply.ApplyOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
applyLong = templates.LongDesc(`
|
||||||
|
Apply a configuration to a resource by file name or stdin and propagate them into member clusters.
|
||||||
|
The resource name must be specified. This resource will be created if it doesn't exist yet.
|
||||||
|
To use 'apply', always create the resource initially with either 'apply' or 'create --save-config'.
|
||||||
|
|
||||||
|
JSON and YAML formats are accepted.
|
||||||
|
|
||||||
|
Alpha Disclaimer: the --prune functionality is not yet complete. Do not use unless you are aware of what the current state is. See https://issues.k8s.io/34274.
|
||||||
|
|
||||||
|
Note: It implements the function of 'kubectl apply' by default.
|
||||||
|
If you want to propagate them into member clusters, please use 'kubectl apply --all-clusters'.`)
|
||||||
|
|
||||||
|
applyExample = templates.Examples(`
|
||||||
|
# Apply the configuration without propagation into member clusters. It acts as 'kubectl apply'.
|
||||||
|
%[1]s apply -f manifest.yaml
|
||||||
|
|
||||||
|
# Apply resources from a directory and propagate them into all member clusters.
|
||||||
|
%[1]s apply -f dir/ --all-clusters`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCommandApplyOptions returns an initialized CommandApplyOptions instance
|
||||||
|
func NewCommandApplyOptions() *CommandApplyOptions {
|
||||||
|
streams := genericclioptions.IOStreams{In: getIn, Out: getOut, ErrOut: getErr}
|
||||||
|
flags := kubectlapply.NewApplyFlags(nil, streams)
|
||||||
|
return &CommandApplyOptions{
|
||||||
|
KubectlApplyFlags: flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdApply creates the `apply` command
|
||||||
|
func NewCmdApply(karmadaConfig KarmadaConfig, parentCommand string) *cobra.Command {
|
||||||
|
o := NewCommandApplyOptions()
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "apply (-f FILENAME | -k DIRECTORY)",
|
||||||
|
Short: "Apply a configuration to a resource by file name or stdin and propagate them into member clusters",
|
||||||
|
Long: applyLong,
|
||||||
|
Example: fmt.Sprintf(applyExample, parentCommand),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := o.Complete(karmadaConfig, cmd, parentCommand, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := o.Validate(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return o.Run()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
o.GlobalCommandOptions.AddFlags(cmd.Flags())
|
||||||
|
o.KubectlApplyFlags.AddFlags(cmd)
|
||||||
|
cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", o.Namespace, "If present, the namespace scope for this CLI request")
|
||||||
|
cmd.Flags().BoolVarP(&o.AllClusters, "all-clusters", "", o.AllClusters, "If present, propagates a group of resources to all member clusters.")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes all the required options
|
||||||
|
func (o *CommandApplyOptions) Complete(karmadaConfig KarmadaConfig, cmd *cobra.Command, parentCommand string, args []string) error {
|
||||||
|
restConfig, err := karmadaConfig.GetRestConfig(o.KarmadaContext, o.KubeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
kubeConfigFlags := NewConfigFlags(true).WithDeprecatedPasswordFlag()
|
||||||
|
kubeConfigFlags.Namespace = &o.Namespace
|
||||||
|
kubeConfigFlags.WrapConfigFn = func(config *restclient.Config) *restclient.Config { return restConfig }
|
||||||
|
o.KubectlApplyFlags.Factory = cmdutil.NewFactory(kubeConfigFlags)
|
||||||
|
kubectlApplyOptions, err := o.KubectlApplyFlags.ToOptions(cmd, parentCommand, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.kubectlApplyOptions = kubectlApplyOptions
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate verifies if CommandApplyOptions are valid and without conflicts.
|
||||||
|
func (o *CommandApplyOptions) Validate(cmd *cobra.Command, args []string) error {
|
||||||
|
return o.kubectlApplyOptions.Validate(cmd, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the `apply` command.
|
||||||
|
func (o *CommandApplyOptions) Run() error {
|
||||||
|
if !o.AllClusters {
|
||||||
|
return o.kubectlApplyOptions.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.generateAndInjectPolices(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.kubectlApplyOptions.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateAndInjectPolices generates and injects policies to the given resources.
|
||||||
|
// It returns an error if any of the policies cannot be generated.
|
||||||
|
func (o *CommandApplyOptions) generateAndInjectPolices() error {
|
||||||
|
// load the resources
|
||||||
|
infos, err := o.kubectlApplyOptions.GetObjects()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate policies and append them to the resources
|
||||||
|
var results []*resource.Info
|
||||||
|
for _, info := range infos {
|
||||||
|
results = append(results, info)
|
||||||
|
obj := o.generatePropagationObject(info)
|
||||||
|
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||||
|
mapping, err := o.kubectlApplyOptions.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to recognize resource: %v", err)
|
||||||
|
}
|
||||||
|
client, err := o.KubectlApplyFlags.Factory.ClientForMapping(mapping)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err)
|
||||||
|
}
|
||||||
|
policyName, _ := metadataAccessor.Name(obj)
|
||||||
|
ret := &resource.Info{
|
||||||
|
Namespace: info.Namespace,
|
||||||
|
Name: policyName,
|
||||||
|
Object: obj,
|
||||||
|
Mapping: mapping,
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
results = append(results, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the results object to be sequentially applied
|
||||||
|
o.kubectlApplyOptions.SetObjects(results)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generatePropagationObject generates a propagation object for the given resource info.
|
||||||
|
// It takes the resource namespace, name and GVK as input to generate policy name.
|
||||||
|
// TODO(carlory): allow users to select one or many member clusters to propagate resources.
|
||||||
|
func (o *CommandApplyOptions) generatePropagationObject(info *resource.Info) runtime.Object {
|
||||||
|
gvk := info.Mapping.GroupVersionKind
|
||||||
|
spec := policyv1alpha1.PropagationSpec{
|
||||||
|
ResourceSelectors: []policyv1alpha1.ResourceSelector{
|
||||||
|
{
|
||||||
|
APIVersion: gvk.GroupVersion().String(),
|
||||||
|
Kind: gvk.Kind,
|
||||||
|
Name: info.Name,
|
||||||
|
Namespace: info.Namespace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.AllClusters {
|
||||||
|
spec.Placement.ClusterAffinity = &policyv1alpha1.ClusterAffinity{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for a namespaced-scope resource, we need to generate a PropagationPolicy object.
|
||||||
|
// for a cluster-scope resource, we need to generate a ClusterPropagationPolicy object.
|
||||||
|
var obj runtime.Object
|
||||||
|
policyName := names.GeneratePolicyName(info.Namespace, info.Name, gvk.String())
|
||||||
|
if info.Namespaced() {
|
||||||
|
obj = &policyv1alpha1.PropagationPolicy{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
APIVersion: "policy.karmada.io/v1alpha1",
|
||||||
|
Kind: "PropagationPolicy",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: policyName,
|
||||||
|
Namespace: info.Namespace,
|
||||||
|
},
|
||||||
|
Spec: spec,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj = &policyv1alpha1.ClusterPropagationPolicy{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
APIVersion: "policy.karmada.io/v1alpha1",
|
||||||
|
Kind: "ClusterPropagationPolicy",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: policyName,
|
||||||
|
},
|
||||||
|
Spec: spec,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ func NewKarmadaCtlCommand(cmdUse, parentCommand string) *cobra.Command {
|
||||||
rootCmd.AddCommand(NewCmdCordon(karmadaConfig, parentCommand))
|
rootCmd.AddCommand(NewCmdCordon(karmadaConfig, parentCommand))
|
||||||
rootCmd.AddCommand(NewCmdUncordon(karmadaConfig, parentCommand))
|
rootCmd.AddCommand(NewCmdUncordon(karmadaConfig, parentCommand))
|
||||||
rootCmd.AddCommand(NewCmdGet(karmadaConfig, parentCommand))
|
rootCmd.AddCommand(NewCmdGet(karmadaConfig, parentCommand))
|
||||||
|
rootCmd.AddCommand(NewCmdApply(karmadaConfig, parentCommand))
|
||||||
rootCmd.AddCommand(NewCmdTaint(karmadaConfig, parentCommand))
|
rootCmd.AddCommand(NewCmdTaint(karmadaConfig, parentCommand))
|
||||||
rootCmd.AddCommand(NewCmdPromote(karmadaConfig, parentCommand))
|
rootCmd.AddCommand(NewCmdPromote(karmadaConfig, parentCommand))
|
||||||
rootCmd.AddCommand(NewCmdLogs(karmadaConfig, parentCommand))
|
rootCmd.AddCommand(NewCmdLogs(karmadaConfig, parentCommand))
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
|
@ -0,0 +1,27 @@
|
||||||
|
/.idea/
|
||||||
|
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
|
||||||
|
*.swp
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,80 @@
|
||||||
|
# clockwork
|
||||||
|
|
||||||
|
[](https://github.com/avelino/awesome-go#utilities)
|
||||||
|
|
||||||
|
[](https://github.com/jonboulle/clockwork/actions?query=workflow%3ACI)
|
||||||
|
[](https://goreportcard.com/report/github.com/jonboulle/clockwork)
|
||||||
|

|
||||||
|
[](https://pkg.go.dev/mod/github.com/jonboulle/clockwork)
|
||||||
|
|
||||||
|
**A simple fake clock for Go.**
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Replace uses of the `time` package with the `clockwork.Clock` interface instead.
|
||||||
|
|
||||||
|
For example, instead of using `time.Sleep` directly:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myFunc() {
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
doSomething()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Inject a clock and use its `Sleep` method instead:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myFunc(clock clockwork.Clock) {
|
||||||
|
clock.Sleep(3 * time.Second)
|
||||||
|
doSomething()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can easily test `myFunc` with a `FakeClock`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestMyFunc(t *testing.T) {
|
||||||
|
c := clockwork.NewFakeClock()
|
||||||
|
|
||||||
|
// Start our sleepy function
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
myFunc(c)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Ensure we wait until myFunc is sleeping
|
||||||
|
c.BlockUntil(1)
|
||||||
|
|
||||||
|
assertState()
|
||||||
|
|
||||||
|
// Advance the FakeClock forward in time
|
||||||
|
c.Advance(3 * time.Second)
|
||||||
|
|
||||||
|
// Wait until the function completes
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assertState()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and in production builds, simply inject the real clock instead:
|
||||||
|
|
||||||
|
```go
|
||||||
|
myFunc(clockwork.NewRealClock())
|
||||||
|
```
|
||||||
|
|
||||||
|
See [example_test.go](example_test.go) for a full example.
|
||||||
|
|
||||||
|
|
||||||
|
# Credits
|
||||||
|
|
||||||
|
clockwork is inspired by @wickman's [threaded fake clock](https://gist.github.com/wickman/3840816), and the [Golang playground](https://blog.golang.org/playground#TOC_3.1.)
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache License, Version 2.0. Please see [License File](LICENSE) for more information.
|
|
@ -0,0 +1,195 @@
|
||||||
|
package clockwork
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clock provides an interface that packages can use instead of directly
|
||||||
|
// using the time module, so that chronology-related behavior can be tested
|
||||||
|
type Clock interface {
|
||||||
|
After(d time.Duration) <-chan time.Time
|
||||||
|
Sleep(d time.Duration)
|
||||||
|
Now() time.Time
|
||||||
|
Since(t time.Time) time.Duration
|
||||||
|
NewTicker(d time.Duration) Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
// FakeClock provides an interface for a clock which can be
|
||||||
|
// manually advanced through time
|
||||||
|
type FakeClock interface {
|
||||||
|
Clock
|
||||||
|
// Advance advances the FakeClock to a new point in time, ensuring any existing
|
||||||
|
// sleepers are notified appropriately before returning
|
||||||
|
Advance(d time.Duration)
|
||||||
|
// BlockUntil will block until the FakeClock has the given number of
|
||||||
|
// sleepers (callers of Sleep or After)
|
||||||
|
BlockUntil(n int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRealClock returns a Clock which simply delegates calls to the actual time
|
||||||
|
// package; it should be used by packages in production.
|
||||||
|
func NewRealClock() Clock {
|
||||||
|
return &realClock{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFakeClock returns a FakeClock implementation which can be
|
||||||
|
// manually advanced through time for testing. The initial time of the
|
||||||
|
// FakeClock will be an arbitrary non-zero time.
|
||||||
|
func NewFakeClock() FakeClock {
|
||||||
|
// use a fixture that does not fulfill Time.IsZero()
|
||||||
|
return NewFakeClockAt(time.Date(1984, time.April, 4, 0, 0, 0, 0, time.UTC))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFakeClockAt returns a FakeClock initialised at the given time.Time.
|
||||||
|
func NewFakeClockAt(t time.Time) FakeClock {
|
||||||
|
return &fakeClock{
|
||||||
|
time: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type realClock struct{}
|
||||||
|
|
||||||
|
func (rc *realClock) After(d time.Duration) <-chan time.Time {
|
||||||
|
return time.After(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *realClock) Sleep(d time.Duration) {
|
||||||
|
time.Sleep(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *realClock) Now() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *realClock) Since(t time.Time) time.Duration {
|
||||||
|
return rc.Now().Sub(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *realClock) NewTicker(d time.Duration) Ticker {
|
||||||
|
return &realTicker{time.NewTicker(d)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeClock struct {
|
||||||
|
sleepers []*sleeper
|
||||||
|
blockers []*blocker
|
||||||
|
time time.Time
|
||||||
|
|
||||||
|
l sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// sleeper represents a caller of After or Sleep
|
||||||
|
type sleeper struct {
|
||||||
|
until time.Time
|
||||||
|
done chan time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// blocker represents a caller of BlockUntil
|
||||||
|
type blocker struct {
|
||||||
|
count int
|
||||||
|
ch chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After mimics time.After; it waits for the given duration to elapse on the
|
||||||
|
// fakeClock, then sends the current time on the returned channel.
|
||||||
|
func (fc *fakeClock) After(d time.Duration) <-chan time.Time {
|
||||||
|
fc.l.Lock()
|
||||||
|
defer fc.l.Unlock()
|
||||||
|
now := fc.time
|
||||||
|
done := make(chan time.Time, 1)
|
||||||
|
if d.Nanoseconds() <= 0 {
|
||||||
|
// special case - trigger immediately
|
||||||
|
done <- now
|
||||||
|
} else {
|
||||||
|
// otherwise, add to the set of sleepers
|
||||||
|
s := &sleeper{
|
||||||
|
until: now.Add(d),
|
||||||
|
done: done,
|
||||||
|
}
|
||||||
|
fc.sleepers = append(fc.sleepers, s)
|
||||||
|
// and notify any blockers
|
||||||
|
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
||||||
|
}
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyBlockers notifies all the blockers waiting until the
|
||||||
|
// given number of sleepers are waiting on the fakeClock. It
|
||||||
|
// returns an updated slice of blockers (i.e. those still waiting)
|
||||||
|
func notifyBlockers(blockers []*blocker, count int) (newBlockers []*blocker) {
|
||||||
|
for _, b := range blockers {
|
||||||
|
if b.count == count {
|
||||||
|
close(b.ch)
|
||||||
|
} else {
|
||||||
|
newBlockers = append(newBlockers, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep blocks until the given duration has passed on the fakeClock
|
||||||
|
func (fc *fakeClock) Sleep(d time.Duration) {
|
||||||
|
<-fc.After(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time returns the current time of the fakeClock
|
||||||
|
func (fc *fakeClock) Now() time.Time {
|
||||||
|
fc.l.RLock()
|
||||||
|
t := fc.time
|
||||||
|
fc.l.RUnlock()
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since returns the duration that has passed since the given time on the fakeClock
|
||||||
|
func (fc *fakeClock) Since(t time.Time) time.Duration {
|
||||||
|
return fc.Now().Sub(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *fakeClock) NewTicker(d time.Duration) Ticker {
|
||||||
|
ft := &fakeTicker{
|
||||||
|
c: make(chan time.Time, 1),
|
||||||
|
stop: make(chan bool, 1),
|
||||||
|
clock: fc,
|
||||||
|
period: d,
|
||||||
|
}
|
||||||
|
ft.runTickThread()
|
||||||
|
return ft
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance advances fakeClock to a new point in time, ensuring channels from any
|
||||||
|
// previous invocations of After are notified appropriately before returning
|
||||||
|
func (fc *fakeClock) Advance(d time.Duration) {
|
||||||
|
fc.l.Lock()
|
||||||
|
defer fc.l.Unlock()
|
||||||
|
end := fc.time.Add(d)
|
||||||
|
var newSleepers []*sleeper
|
||||||
|
for _, s := range fc.sleepers {
|
||||||
|
if end.Sub(s.until) >= 0 {
|
||||||
|
s.done <- end
|
||||||
|
} else {
|
||||||
|
newSleepers = append(newSleepers, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fc.sleepers = newSleepers
|
||||||
|
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
||||||
|
fc.time = end
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockUntil will block until the fakeClock has the given number of sleepers
|
||||||
|
// (callers of Sleep or After)
|
||||||
|
func (fc *fakeClock) BlockUntil(n int) {
|
||||||
|
fc.l.Lock()
|
||||||
|
// Fast path: current number of sleepers is what we're looking for
|
||||||
|
if len(fc.sleepers) == n {
|
||||||
|
fc.l.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Otherwise, set up a new blocker
|
||||||
|
b := &blocker{
|
||||||
|
count: n,
|
||||||
|
ch: make(chan struct{}),
|
||||||
|
}
|
||||||
|
fc.blockers = append(fc.blockers, b)
|
||||||
|
fc.l.Unlock()
|
||||||
|
<-b.ch
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package clockwork
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ticker provides an interface which can be used instead of directly
|
||||||
|
// using the ticker within the time module. The real-time ticker t
|
||||||
|
// provides ticks through t.C which becomes now t.Chan() to make
|
||||||
|
// this channel requirement definable in this interface.
|
||||||
|
type Ticker interface {
|
||||||
|
Chan() <-chan time.Time
|
||||||
|
Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
type realTicker struct{ *time.Ticker }
|
||||||
|
|
||||||
|
func (rt *realTicker) Chan() <-chan time.Time {
|
||||||
|
return rt.C
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeTicker struct {
|
||||||
|
c chan time.Time
|
||||||
|
stop chan bool
|
||||||
|
clock FakeClock
|
||||||
|
period time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft *fakeTicker) Chan() <-chan time.Time {
|
||||||
|
return ft.c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ft *fakeTicker) Stop() {
|
||||||
|
ft.stop <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
// runTickThread initializes a background goroutine to send the tick time to the ticker channel
|
||||||
|
// after every period. Tick events are discarded if the underlying ticker channel does not have
|
||||||
|
// enough capacity.
|
||||||
|
func (ft *fakeTicker) runTickThread() {
|
||||||
|
nextTick := ft.clock.Now().Add(ft.period)
|
||||||
|
next := ft.clock.After(ft.period)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ft.stop:
|
||||||
|
return
|
||||||
|
case <-next:
|
||||||
|
// We send the time that the tick was supposed to occur at.
|
||||||
|
tick := nextTick
|
||||||
|
// Before sending the tick, we'll compute the next tick time and star the clock.After call.
|
||||||
|
now := ft.clock.Now()
|
||||||
|
// First, figure out how many periods there have been between "now" and the time we were
|
||||||
|
// supposed to have trigged, then advance over all of those.
|
||||||
|
skipTicks := (now.Sub(tick) + ft.period - 1) / ft.period
|
||||||
|
nextTick = nextTick.Add(skipTicks * ft.period)
|
||||||
|
// Now, keep advancing until we are past now. This should happen at most once.
|
||||||
|
for !nextTick.After(now) {
|
||||||
|
nextTick = nextTick.Add(ft.period)
|
||||||
|
}
|
||||||
|
// Figure out how long between now and the next scheduled tick, then wait that long.
|
||||||
|
remaining := nextTick.Sub(now)
|
||||||
|
next = ft.clock.After(remaining)
|
||||||
|
// Finally, we can actually send the tick.
|
||||||
|
select {
|
||||||
|
case ft.c <- tick:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 jsonmergepatch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/evanphx/json-patch"
|
||||||
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
|
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a 3-way merge patch based-on JSON merge patch.
|
||||||
|
// Calculate addition-and-change patch between current and modified.
|
||||||
|
// Calculate deletion patch between original and modified.
|
||||||
|
func CreateThreeWayJSONMergePatch(original, modified, current []byte, fns ...mergepatch.PreconditionFunc) ([]byte, error) {
|
||||||
|
if len(original) == 0 {
|
||||||
|
original = []byte(`{}`)
|
||||||
|
}
|
||||||
|
if len(modified) == 0 {
|
||||||
|
modified = []byte(`{}`)
|
||||||
|
}
|
||||||
|
if len(current) == 0 {
|
||||||
|
current = []byte(`{}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
addAndChangePatch, err := jsonpatch.CreateMergePatch(current, modified)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Only keep addition and changes
|
||||||
|
addAndChangePatch, addAndChangePatchObj, err := keepOrDeleteNullInJsonPatch(addAndChangePatch, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePatch, err := jsonpatch.CreateMergePatch(original, modified)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Only keep deletion
|
||||||
|
deletePatch, deletePatchObj, err := keepOrDeleteNullInJsonPatch(deletePatch, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasConflicts, err := mergepatch.HasConflicts(addAndChangePatchObj, deletePatchObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if hasConflicts {
|
||||||
|
return nil, mergepatch.NewErrConflict(mergepatch.ToYAMLOrError(addAndChangePatchObj), mergepatch.ToYAMLOrError(deletePatchObj))
|
||||||
|
}
|
||||||
|
patch, err := jsonpatch.MergePatch(deletePatch, addAndChangePatch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var patchMap map[string]interface{}
|
||||||
|
err = json.Unmarshal(patch, &patchMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to unmarshal patch for precondition check: %s", patch)
|
||||||
|
}
|
||||||
|
meetPreconditions, err := meetPreconditions(patchMap, fns...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !meetPreconditions {
|
||||||
|
return nil, mergepatch.NewErrPreconditionFailed(patchMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return patch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// keepOrDeleteNullInJsonPatch takes a json-encoded byte array and a boolean.
|
||||||
|
// It returns a filtered object and its corresponding json-encoded byte array.
|
||||||
|
// It is a wrapper of func keepOrDeleteNullInObj
|
||||||
|
func keepOrDeleteNullInJsonPatch(patch []byte, keepNull bool) ([]byte, map[string]interface{}, error) {
|
||||||
|
var patchMap map[string]interface{}
|
||||||
|
err := json.Unmarshal(patch, &patchMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
filteredMap, err := keepOrDeleteNullInObj(patchMap, keepNull)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
o, err := json.Marshal(filteredMap)
|
||||||
|
return o, filteredMap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// keepOrDeleteNullInObj will keep only the null value and delete all the others,
|
||||||
|
// if keepNull is true. Otherwise, it will delete all the null value and keep the others.
|
||||||
|
func keepOrDeleteNullInObj(m map[string]interface{}, keepNull bool) (map[string]interface{}, error) {
|
||||||
|
filteredMap := make(map[string]interface{})
|
||||||
|
var err error
|
||||||
|
for key, val := range m {
|
||||||
|
switch {
|
||||||
|
case keepNull && val == nil:
|
||||||
|
filteredMap[key] = nil
|
||||||
|
case val != nil:
|
||||||
|
switch typedVal := val.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
// Explicitly-set empty maps are treated as values instead of empty patches
|
||||||
|
if len(typedVal) == 0 {
|
||||||
|
if !keepNull {
|
||||||
|
filteredMap[key] = typedVal
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var filteredSubMap map[string]interface{}
|
||||||
|
filteredSubMap, err = keepOrDeleteNullInObj(typedVal, keepNull)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the returned filtered submap was empty, this is an empty patch for the entire subdict, so the key
|
||||||
|
// should not be set
|
||||||
|
if len(filteredSubMap) != 0 {
|
||||||
|
filteredMap[key] = filteredSubMap
|
||||||
|
}
|
||||||
|
|
||||||
|
case []interface{}, string, float64, bool, int64, nil:
|
||||||
|
// Lists are always replaced in Json, no need to check each entry in the list.
|
||||||
|
if !keepNull {
|
||||||
|
filteredMap[key] = val
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown type: %v", reflect.TypeOf(typedVal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func meetPreconditions(patchObj map[string]interface{}, fns ...mergepatch.PreconditionFunc) (bool, error) {
|
||||||
|
// Apply the preconditions to the patch, and return an error if any of them fail.
|
||||||
|
for _, fn := range fns {
|
||||||
|
if !fn(patchObj) {
|
||||||
|
return false, fmt.Errorf("precondition failed for: %v", patchObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
|
@ -0,0 +1,790 @@
|
||||||
|
/*
|
||||||
|
Copyright 2014 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 apply
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
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"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/delete"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/openapi"
|
||||||
|
"k8s.io/kubectl/pkg/util/prune"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
"k8s.io/kubectl/pkg/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplyFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which
|
||||||
|
// reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes
|
||||||
|
// the logic itself easy to unit test
|
||||||
|
type ApplyFlags struct {
|
||||||
|
Factory cmdutil.Factory
|
||||||
|
|
||||||
|
RecordFlags *genericclioptions.RecordFlags
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
|
||||||
|
DeleteFlags *delete.DeleteFlags
|
||||||
|
|
||||||
|
FieldManager string
|
||||||
|
Selector string
|
||||||
|
Prune bool
|
||||||
|
PruneResources []prune.Resource
|
||||||
|
All bool
|
||||||
|
Overwrite bool
|
||||||
|
OpenAPIPatch bool
|
||||||
|
PruneWhitelist []string
|
||||||
|
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyOptions defines flags and other configuration parameters for the `apply` command
|
||||||
|
type ApplyOptions struct {
|
||||||
|
Recorder genericclioptions.Recorder
|
||||||
|
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||||
|
|
||||||
|
DeleteOptions *delete.DeleteOptions
|
||||||
|
|
||||||
|
ServerSideApply bool
|
||||||
|
ForceConflicts bool
|
||||||
|
FieldManager string
|
||||||
|
Selector string
|
||||||
|
DryRunStrategy cmdutil.DryRunStrategy
|
||||||
|
DryRunVerifier *resource.QueryParamVerifier
|
||||||
|
FieldValidationVerifier *resource.QueryParamVerifier
|
||||||
|
Prune bool
|
||||||
|
PruneResources []prune.Resource
|
||||||
|
cmdBaseName string
|
||||||
|
All bool
|
||||||
|
Overwrite bool
|
||||||
|
OpenAPIPatch bool
|
||||||
|
PruneWhitelist []string
|
||||||
|
|
||||||
|
ValidationDirective string
|
||||||
|
Validator validation.Schema
|
||||||
|
Builder *resource.Builder
|
||||||
|
Mapper meta.RESTMapper
|
||||||
|
DynamicClient dynamic.Interface
|
||||||
|
OpenAPISchema openapi.Resources
|
||||||
|
|
||||||
|
Namespace string
|
||||||
|
EnforceNamespace bool
|
||||||
|
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
|
||||||
|
// Objects (and some denormalized data) which are to be
|
||||||
|
// applied. The standard way to fill in this structure
|
||||||
|
// is by calling "GetObjects()", which will use the
|
||||||
|
// resource builder if "objectsCached" is false. The other
|
||||||
|
// way to set this field is to use "SetObjects()".
|
||||||
|
// Subsequent calls to "GetObjects()" after setting would
|
||||||
|
// not call the resource builder; only return the set objects.
|
||||||
|
objects []*resource.Info
|
||||||
|
objectsCached bool
|
||||||
|
|
||||||
|
// Stores visited objects/namespaces for later use
|
||||||
|
// calculating the set of objects to prune.
|
||||||
|
VisitedUids sets.String
|
||||||
|
VisitedNamespaces sets.String
|
||||||
|
|
||||||
|
// Function run after the objects are generated and
|
||||||
|
// stored in the "objects" field, but before the
|
||||||
|
// apply is run on these objects.
|
||||||
|
PreProcessorFn func() error
|
||||||
|
// Function run after all objects have been applied.
|
||||||
|
// The standard PostProcessorFn is "PrintAndPrunePostProcessor()".
|
||||||
|
PostProcessorFn func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
applyLong = templates.LongDesc(i18n.T(`
|
||||||
|
Apply a configuration to a resource by file name or stdin.
|
||||||
|
The resource name must be specified. This resource will be created if it doesn't exist yet.
|
||||||
|
To use 'apply', always create the resource initially with either 'apply' or 'create --save-config'.
|
||||||
|
|
||||||
|
JSON and YAML formats are accepted.
|
||||||
|
|
||||||
|
Alpha Disclaimer: the --prune functionality is not yet complete. Do not use unless you are aware of what the current state is. See https://issues.k8s.io/34274.`))
|
||||||
|
|
||||||
|
applyExample = templates.Examples(i18n.T(`
|
||||||
|
# Apply the configuration in pod.json to a pod
|
||||||
|
kubectl apply -f ./pod.json
|
||||||
|
|
||||||
|
# Apply resources from a directory containing kustomization.yaml - e.g. dir/kustomization.yaml
|
||||||
|
kubectl apply -k dir/
|
||||||
|
|
||||||
|
# Apply the JSON passed into stdin to a pod
|
||||||
|
cat pod.json | kubectl apply -f -
|
||||||
|
|
||||||
|
# Apply the configuration from all files that end with '.json' - i.e. expand wildcard characters in file names
|
||||||
|
kubectl apply -f '*.json'
|
||||||
|
|
||||||
|
# Note: --prune is still in Alpha
|
||||||
|
# Apply the configuration in manifest.yaml that matches label app=nginx and delete all other resources that are not in the file and match label app=nginx
|
||||||
|
kubectl apply --prune -f manifest.yaml -l app=nginx
|
||||||
|
|
||||||
|
# Apply the configuration in manifest.yaml and delete all the other config maps that are not in the file
|
||||||
|
kubectl apply --prune -f manifest.yaml --all --prune-whitelist=core/v1/ConfigMap`))
|
||||||
|
|
||||||
|
warningNoLastAppliedConfigAnnotation = "Warning: resource %[1]s is missing the %[2]s annotation which is required by %[3]s apply. %[3]s apply should only be used on resources created declaratively by either %[3]s create --save-config or %[3]s apply. The missing annotation will be patched automatically.\n"
|
||||||
|
warningChangesOnDeletingResource = "Warning: Detected changes to resource %[1]s which is currently being deleted.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewApplyFlags returns a default ApplyFlags
|
||||||
|
func NewApplyFlags(f cmdutil.Factory, streams genericclioptions.IOStreams) *ApplyFlags {
|
||||||
|
return &ApplyFlags{
|
||||||
|
Factory: f,
|
||||||
|
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||||
|
DeleteFlags: delete.NewDeleteFlags("that contains the configuration to apply"),
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||||
|
|
||||||
|
Overwrite: true,
|
||||||
|
OpenAPIPatch: true,
|
||||||
|
|
||||||
|
IOStreams: streams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdApply creates the `apply` command
|
||||||
|
func NewCmdApply(baseName string, f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
flags := NewApplyFlags(f, ioStreams)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "apply (-f FILENAME | -k DIRECTORY)",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Apply a configuration to a resource by file name or stdin"),
|
||||||
|
Long: applyLong,
|
||||||
|
Example: applyExample,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
o, err := flags.ToOptions(cmd, baseName, args)
|
||||||
|
cmdutil.CheckErr(err)
|
||||||
|
cmdutil.CheckErr(o.Validate(cmd, args))
|
||||||
|
cmdutil.CheckErr(o.Run())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags.AddFlags(cmd)
|
||||||
|
|
||||||
|
// apply subcommands
|
||||||
|
cmd.AddCommand(NewCmdApplyViewLastApplied(flags.Factory, flags.IOStreams))
|
||||||
|
cmd.AddCommand(NewCmdApplySetLastApplied(flags.Factory, flags.IOStreams))
|
||||||
|
cmd.AddCommand(NewCmdApplyEditLastApplied(flags.Factory, flags.IOStreams))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlags registers flags for a cli
|
||||||
|
func (flags *ApplyFlags) AddFlags(cmd *cobra.Command) {
|
||||||
|
// bind flag structs
|
||||||
|
flags.DeleteFlags.AddFlags(cmd)
|
||||||
|
flags.RecordFlags.AddFlags(cmd)
|
||||||
|
flags.PrintFlags.AddFlags(cmd)
|
||||||
|
|
||||||
|
cmdutil.AddValidateFlags(cmd)
|
||||||
|
cmdutil.AddDryRunFlag(cmd)
|
||||||
|
cmdutil.AddServerSideApplyFlags(cmd)
|
||||||
|
cmdutil.AddFieldManagerFlagVar(cmd, &flags.FieldManager, FieldManagerClientSideApply)
|
||||||
|
cmdutil.AddLabelSelectorFlagVar(cmd, &flags.Selector)
|
||||||
|
|
||||||
|
cmd.Flags().BoolVar(&flags.Overwrite, "overwrite", flags.Overwrite, "Automatically resolve conflicts between the modified and live configuration by using values from the modified configuration")
|
||||||
|
cmd.Flags().BoolVar(&flags.Prune, "prune", flags.Prune, "Automatically delete resource objects, that do not appear in the configs and are created by either apply or create --save-config. Should be used with either -l or --all.")
|
||||||
|
cmd.Flags().BoolVar(&flags.All, "all", flags.All, "Select all resources in the namespace of the specified resource types.")
|
||||||
|
cmd.Flags().StringArrayVar(&flags.PruneWhitelist, "prune-whitelist", flags.PruneWhitelist, "Overwrite the default whitelist with <group/version/kind> for --prune")
|
||||||
|
cmd.Flags().BoolVar(&flags.OpenAPIPatch, "openapi-patch", flags.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.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToOptions converts from CLI inputs to runtime inputs
|
||||||
|
func (flags *ApplyFlags) ToOptions(cmd *cobra.Command, baseName string, args []string) (*ApplyOptions, error) {
|
||||||
|
serverSideApply := cmdutil.GetServerSideApplyFlag(cmd)
|
||||||
|
forceConflicts := cmdutil.GetForceConflictsFlag(cmd)
|
||||||
|
dryRunStrategy, err := cmdutil.GetDryRunStrategy(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicClient, err := flags.Factory.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dryRunVerifier := resource.NewQueryParamVerifier(dynamicClient, flags.Factory.OpenAPIGetter(), resource.QueryParamDryRun)
|
||||||
|
fieldValidationVerifier := resource.NewQueryParamVerifier(dynamicClient, flags.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation)
|
||||||
|
fieldManager := GetApplyFieldManagerFlag(cmd, serverSideApply)
|
||||||
|
|
||||||
|
// allow for a success message operation to be specified at print time
|
||||||
|
toPrinter := func(operation string) (printers.ResourcePrinter, error) {
|
||||||
|
flags.PrintFlags.NamePrintFlags.Operation = operation
|
||||||
|
cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, dryRunStrategy)
|
||||||
|
return flags.PrintFlags.ToPrinter()
|
||||||
|
}
|
||||||
|
|
||||||
|
flags.RecordFlags.Complete(cmd)
|
||||||
|
recorder, err := flags.RecordFlags.ToRecorder()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteOptions, err := flags.DeleteFlags.ToOptions(dynamicClient, flags.IOStreams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = deleteOptions.FilenameOptions.RequireFilenameOrKustomize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
openAPISchema, _ := flags.Factory.OpenAPISchema()
|
||||||
|
|
||||||
|
validationDirective, err := cmdutil.GetValidationDirective(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
validator, err := flags.Factory.Validator(validationDirective, fieldValidationVerifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
builder := flags.Factory.NewBuilder()
|
||||||
|
mapper, err := flags.Factory.ToRESTMapper()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, enforceNamespace, err := flags.Factory.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.Prune {
|
||||||
|
flags.PruneResources, err = prune.ParseResources(mapper, flags.PruneWhitelist)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
o := &ApplyOptions{
|
||||||
|
// Store baseName for use in printing warnings / messages involving the base command name.
|
||||||
|
// This is useful for downstream command that wrap this one.
|
||||||
|
cmdBaseName: baseName,
|
||||||
|
|
||||||
|
PrintFlags: flags.PrintFlags,
|
||||||
|
|
||||||
|
DeleteOptions: deleteOptions,
|
||||||
|
ToPrinter: toPrinter,
|
||||||
|
ServerSideApply: serverSideApply,
|
||||||
|
ForceConflicts: forceConflicts,
|
||||||
|
FieldManager: fieldManager,
|
||||||
|
Selector: flags.Selector,
|
||||||
|
DryRunStrategy: dryRunStrategy,
|
||||||
|
DryRunVerifier: dryRunVerifier,
|
||||||
|
Prune: flags.Prune,
|
||||||
|
PruneResources: flags.PruneResources,
|
||||||
|
All: flags.All,
|
||||||
|
Overwrite: flags.Overwrite,
|
||||||
|
OpenAPIPatch: flags.OpenAPIPatch,
|
||||||
|
PruneWhitelist: flags.PruneWhitelist,
|
||||||
|
|
||||||
|
Recorder: recorder,
|
||||||
|
Namespace: namespace,
|
||||||
|
EnforceNamespace: enforceNamespace,
|
||||||
|
Validator: validator,
|
||||||
|
ValidationDirective: validationDirective,
|
||||||
|
Builder: builder,
|
||||||
|
Mapper: mapper,
|
||||||
|
DynamicClient: dynamicClient,
|
||||||
|
OpenAPISchema: openAPISchema,
|
||||||
|
|
||||||
|
IOStreams: flags.IOStreams,
|
||||||
|
|
||||||
|
objects: []*resource.Info{},
|
||||||
|
objectsCached: false,
|
||||||
|
|
||||||
|
VisitedUids: sets.NewString(),
|
||||||
|
VisitedNamespaces: sets.NewString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
o.PostProcessorFn = o.PrintAndPrunePostProcessor()
|
||||||
|
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate verifies if ApplyOptions are valid and without conflicts.
|
||||||
|
func (o *ApplyOptions) Validate(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.ForceConflicts && !o.ServerSideApply {
|
||||||
|
return fmt.Errorf("--force-conflicts only works with --server-side")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.DryRunStrategy == cmdutil.DryRunClient && o.ServerSideApply {
|
||||||
|
return fmt.Errorf("--dry-run=client doesn't work with --server-side (did you mean --dry-run=server instead?)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.ServerSideApply && o.DeleteOptions.ForceDeletion {
|
||||||
|
return fmt.Errorf("--force cannot be used with --server-side")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.DryRunStrategy == cmdutil.DryRunServer && o.DeleteOptions.ForceDeletion {
|
||||||
|
return fmt.Errorf("--dry-run=server cannot be used with --force")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.All && len(o.Selector) > 0 {
|
||||||
|
return fmt.Errorf("cannot set --all and --selector at the same time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Prune && !o.All && o.Selector == "" {
|
||||||
|
return fmt.Errorf("all resources selected for prune without explicitly passing --all. To prune all resources, pass the --all flag. If you did not mean to prune all resources, specify a label selector")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIncompatibleServerError(err error) bool {
|
||||||
|
// 415: Unsupported media type means we're talking to a server which doesn't
|
||||||
|
// support server-side apply.
|
||||||
|
if _, ok := err.(*errors.StatusError); !ok {
|
||||||
|
// Non-StatusError means the error isn't because the server is incompatible.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return err.(*errors.StatusError).Status().Code == http.StatusUnsupportedMediaType
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjects returns a (possibly cached) version of all the valid objects to apply
|
||||||
|
// as a slice of pointer to resource.Info and an error if one or more occurred.
|
||||||
|
// IMPORTANT: This function can return both valid objects AND an error, since
|
||||||
|
// "ContinueOnError" is set on the builder. This function should not be called
|
||||||
|
// until AFTER the "complete" and "validate" methods have been called to ensure that
|
||||||
|
// the ApplyOptions is filled in and valid.
|
||||||
|
func (o *ApplyOptions) GetObjects() ([]*resource.Info, error) {
|
||||||
|
var err error = nil
|
||||||
|
if !o.objectsCached {
|
||||||
|
r := o.Builder.
|
||||||
|
Unstructured().
|
||||||
|
Schema(o.Validator).
|
||||||
|
ContinueOnError().
|
||||||
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||||
|
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
|
||||||
|
LabelSelectorParam(o.Selector).
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
o.objects, err = r.Infos()
|
||||||
|
o.objectsCached = true
|
||||||
|
}
|
||||||
|
return o.objects, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetObjects stores the set of objects (as resource.Info) to be
|
||||||
|
// subsequently applied.
|
||||||
|
func (o *ApplyOptions) SetObjects(infos []*resource.Info) {
|
||||||
|
o.objects = infos
|
||||||
|
o.objectsCached = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the `apply` command.
|
||||||
|
func (o *ApplyOptions) Run() error {
|
||||||
|
if o.PreProcessorFn != nil {
|
||||||
|
klog.V(4).Infof("Running apply pre-processor function")
|
||||||
|
if err := o.PreProcessorFn(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce CLI specified namespace on server request.
|
||||||
|
if o.EnforceNamespace {
|
||||||
|
o.VisitedNamespaces.Insert(o.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates the objects using the resource builder if they have not
|
||||||
|
// already been stored by calling "SetObjects()" in the pre-processor.
|
||||||
|
errs := []error{}
|
||||||
|
infos, err := o.GetObjects()
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
if len(infos) == 0 && len(errs) == 0 {
|
||||||
|
return fmt.Errorf("no objects passed to apply")
|
||||||
|
}
|
||||||
|
// Iterate through all objects, applying each one.
|
||||||
|
for _, info := range infos {
|
||||||
|
if err := o.applyOneObject(info); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If any errors occurred during apply, then return error (or
|
||||||
|
// aggregate of errors).
|
||||||
|
if len(errs) == 1 {
|
||||||
|
return errs[0]
|
||||||
|
}
|
||||||
|
if len(errs) > 1 {
|
||||||
|
return utilerrors.NewAggregate(errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.PostProcessorFn != nil {
|
||||||
|
klog.V(4).Infof("Running apply post-processor function")
|
||||||
|
if err := o.PostProcessorFn(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ApplyOptions) applyOneObject(info *resource.Info) error {
|
||||||
|
o.MarkNamespaceVisited(info)
|
||||||
|
|
||||||
|
if err := o.Recorder.Record(info.Object); err != nil {
|
||||||
|
klog.V(4).Infof("error recording current command: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(info.Name) == 0 {
|
||||||
|
metadata, _ := meta.Accessor(info.Object)
|
||||||
|
generatedName := metadata.GetGenerateName()
|
||||||
|
if len(generatedName) > 0 {
|
||||||
|
return fmt.Errorf("from %s: cannot use generate name with apply", generatedName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
helper := resource.NewHelper(info.Client, info.Mapping).
|
||||||
|
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
|
||||||
|
WithFieldManager(o.FieldManager).
|
||||||
|
WithFieldValidation(o.ValidationDirective)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// Send the full object to be applied on the server side.
|
||||||
|
data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return cmdutil.AddSourceToErr("serverside-apply", info.Source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := metav1.PatchOptions{
|
||||||
|
Force: &o.ForceConflicts,
|
||||||
|
}
|
||||||
|
obj, err := helper.Patch(
|
||||||
|
info.Namespace,
|
||||||
|
info.Name,
|
||||||
|
types.ApplyPatchType,
|
||||||
|
data,
|
||||||
|
&options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if isIncompatibleServerError(err) {
|
||||||
|
err = fmt.Errorf("Server-side apply not available on the server: (%v)", err)
|
||||||
|
}
|
||||||
|
if errors.IsConflict(err) {
|
||||||
|
err = fmt.Errorf(`%v
|
||||||
|
Please review the fields above--they currently have other managers. Here
|
||||||
|
are the ways you can resolve this warning:
|
||||||
|
* If you intend to manage all of these fields, please re-run the apply
|
||||||
|
command with the `+"`--force-conflicts`"+` flag.
|
||||||
|
* If you do not intend to manage all of the fields, please edit your
|
||||||
|
manifest to remove references to the fields that should keep their
|
||||||
|
current managers.
|
||||||
|
* You may co-own fields by updating your manifest to match the existing
|
||||||
|
value; in this case, you'll become the manager if the other manager(s)
|
||||||
|
stop managing the field (remove it from their configuration).
|
||||||
|
See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts`, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Refresh(obj, true)
|
||||||
|
|
||||||
|
WarnIfDeleting(info.Object, o.ErrOut)
|
||||||
|
|
||||||
|
if err := o.MarkObjectVisited(info); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.shouldPrintObject() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
printer, err := o.ToPrinter("serverside-applied")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = printer.PrintObj(info.Object, o.Out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the modified configuration of the object. Embed the result
|
||||||
|
// as an annotation in the modified configuration, so that it will appear
|
||||||
|
// in the patch sent to the server.
|
||||||
|
modified, err := util.GetModifiedConfiguration(info.Object, true, unstructured.UnstructuredJSONScheme)
|
||||||
|
if err != nil {
|
||||||
|
return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving modified configuration from:\n%s\nfor:", info.String()), info.Source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := info.Get(); err != nil {
|
||||||
|
if !errors.IsNotFound(err) {
|
||||||
|
return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the resource if it doesn't exist
|
||||||
|
// First, update the annotation used by kubectl apply
|
||||||
|
if err := util.CreateApplyAnnotation(info.Object, unstructured.UnstructuredJSONScheme); err != nil {
|
||||||
|
return cmdutil.AddSourceToErr("creating", info.Source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.DryRunStrategy != cmdutil.DryRunClient {
|
||||||
|
// Then create the resource and skip the three-way merge
|
||||||
|
obj, err := helper.Create(info.Namespace, true, info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return cmdutil.AddSourceToErr("creating", info.Source, err)
|
||||||
|
}
|
||||||
|
info.Refresh(obj, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.MarkObjectVisited(info); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.shouldPrintObject() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
printer, err := o.ToPrinter("created")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = printer.PrintObj(info.Object, o.Out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.MarkObjectVisited(info); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.DryRunStrategy != cmdutil.DryRunClient {
|
||||||
|
metadata, _ := meta.Accessor(info.Object)
|
||||||
|
annotationMap := metadata.GetAnnotations()
|
||||||
|
if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok {
|
||||||
|
fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, info.ObjectName(), corev1.LastAppliedConfigAnnotation, o.cmdBaseName)
|
||||||
|
}
|
||||||
|
|
||||||
|
patcher, err := newPatcher(o, info, helper)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
patchBytes, patchedObject, err := patcher.Patch(info.Object, modified, info.Source, info.Namespace, info.Name, o.ErrOut)
|
||||||
|
if err != nil {
|
||||||
|
return cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Refresh(patchedObject, true)
|
||||||
|
|
||||||
|
WarnIfDeleting(info.Object, o.ErrOut)
|
||||||
|
|
||||||
|
if string(patchBytes) == "{}" && !o.shouldPrintObject() {
|
||||||
|
printer, err := o.ToPrinter("unchanged")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = printer.PrintObj(info.Object, o.Out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.shouldPrintObject() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
printer, err := o.ToPrinter("configured")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = printer.PrintObj(info.Object, o.Out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ApplyOptions) shouldPrintObject() bool {
|
||||||
|
// Print object only if output format other than "name" is specified
|
||||||
|
shouldPrint := false
|
||||||
|
output := *o.PrintFlags.OutputFormat
|
||||||
|
shortOutput := output == "name"
|
||||||
|
if len(output) > 0 && !shortOutput {
|
||||||
|
shouldPrint = true
|
||||||
|
}
|
||||||
|
return shouldPrint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ApplyOptions) printObjects() error {
|
||||||
|
|
||||||
|
if !o.shouldPrintObject() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
infos, err := o.GetObjects()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(infos) > 0 {
|
||||||
|
printer, err := o.ToPrinter("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
objToPrint := infos[0].Object
|
||||||
|
if len(infos) > 1 {
|
||||||
|
objs := []runtime.Object{}
|
||||||
|
for _, info := range infos {
|
||||||
|
objs = append(objs, info.Object)
|
||||||
|
}
|
||||||
|
list := &corev1.List{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "List",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ListMeta: metav1.ListMeta{},
|
||||||
|
}
|
||||||
|
if err := meta.SetList(list, objs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
objToPrint = list
|
||||||
|
}
|
||||||
|
if err := printer.PrintObj(objToPrint, o.Out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkNamespaceVisited keeps track of which namespaces the applied
|
||||||
|
// objects belong to. Used for pruning.
|
||||||
|
func (o *ApplyOptions) MarkNamespaceVisited(info *resource.Info) {
|
||||||
|
if info.Namespaced() {
|
||||||
|
o.VisitedNamespaces.Insert(info.Namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkObjectVisited keeps track of UIDs of the applied
|
||||||
|
// objects. Used for pruning.
|
||||||
|
func (o *ApplyOptions) MarkObjectVisited(info *resource.Info) error {
|
||||||
|
metadata, err := meta.Accessor(info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.VisitedUids.Insert(string(metadata.GetUID()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintAndPrunePostProcessor returns a function which meets the PostProcessorFn
|
||||||
|
// function signature. This returned function prints all the
|
||||||
|
// objects as a list (if configured for that), and prunes the
|
||||||
|
// objects not applied. The returned function is the standard
|
||||||
|
// apply post processor.
|
||||||
|
func (o *ApplyOptions) PrintAndPrunePostProcessor() func() error {
|
||||||
|
|
||||||
|
return func() error {
|
||||||
|
if err := o.printObjects(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Prune {
|
||||||
|
p := newPruner(o)
|
||||||
|
return p.pruneAll(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FieldManagerClientSideApply is the default client-side apply field manager.
|
||||||
|
//
|
||||||
|
// The default field manager is not `kubectl-apply` to distinguish from
|
||||||
|
// server-side apply.
|
||||||
|
FieldManagerClientSideApply = "kubectl-client-side-apply"
|
||||||
|
// The default server-side apply field manager is `kubectl`
|
||||||
|
// instead of a field manager like `kubectl-server-side-apply`
|
||||||
|
// for backward compatibility to not conflict with old versions
|
||||||
|
// of kubectl server-side apply where `kubectl` has already been the field manager.
|
||||||
|
fieldManagerServerSideApply = "kubectl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetApplyFieldManagerFlag gets the field manager for kubectl apply
|
||||||
|
// if it is not set.
|
||||||
|
//
|
||||||
|
// The default field manager is not `kubectl-apply` to distinguish between
|
||||||
|
// client-side and server-side apply.
|
||||||
|
func GetApplyFieldManagerFlag(cmd *cobra.Command, serverSide bool) string {
|
||||||
|
// The field manager flag was set
|
||||||
|
if cmd.Flag("field-manager").Changed {
|
||||||
|
return cmdutil.GetFlagString(cmd, "field-manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverSide {
|
||||||
|
return fieldManagerServerSideApply
|
||||||
|
}
|
||||||
|
|
||||||
|
return FieldManagerClientSideApply
|
||||||
|
}
|
||||||
|
|
||||||
|
// WarnIfDeleting prints a warning if a resource is being deleted
|
||||||
|
func WarnIfDeleting(obj runtime.Object, stderr io.Writer) {
|
||||||
|
metadata, _ := meta.Accessor(obj)
|
||||||
|
if metadata != nil && metadata.GetDeletionTimestamp() != nil {
|
||||||
|
// just warn the user about the conflict
|
||||||
|
fmt.Fprintf(stderr, warningChangesOnDeletingResource, metadata.GetName())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 apply
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/util/editor"
|
||||||
|
"k8s.io/kubectl/pkg/util/completion"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
applyEditLastAppliedLong = templates.LongDesc(i18n.T(`
|
||||||
|
Edit the latest last-applied-configuration annotations of resources from the default editor.
|
||||||
|
|
||||||
|
The edit-last-applied command allows you to directly edit any API resource you can retrieve via the
|
||||||
|
command-line tools. It will open the editor defined by your KUBE_EDITOR, or EDITOR
|
||||||
|
environment variables, or fall back to 'vi' for Linux or 'notepad' for Windows.
|
||||||
|
You can edit multiple objects, although changes are applied one at a time. The command
|
||||||
|
accepts file names as well as command-line arguments, although the files you point to must
|
||||||
|
be previously saved versions of resources.
|
||||||
|
|
||||||
|
The default format is YAML. To edit in JSON, specify "-o json".
|
||||||
|
|
||||||
|
The flag --windows-line-endings can be used to force Windows line endings,
|
||||||
|
otherwise the default for your operating system will be used.
|
||||||
|
|
||||||
|
In the event an error occurs while updating, a temporary file will be created on disk
|
||||||
|
that contains your unapplied changes. The most common error when updating a resource
|
||||||
|
is another editor changing the resource on the server. When this occurs, you will have
|
||||||
|
to apply your changes to the newer version of the resource, or update your temporary
|
||||||
|
saved copy to include the latest resource version.`))
|
||||||
|
|
||||||
|
applyEditLastAppliedExample = templates.Examples(`
|
||||||
|
# Edit the last-applied-configuration annotations by type/name in YAML
|
||||||
|
kubectl apply edit-last-applied deployment/nginx
|
||||||
|
|
||||||
|
# Edit the last-applied-configuration annotations by file in JSON
|
||||||
|
kubectl apply edit-last-applied -f deploy.yaml -o json`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCmdApplyEditLastApplied created the cobra CLI command for the `apply edit-last-applied` command.
|
||||||
|
func NewCmdApplyEditLastApplied(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
o := editor.NewEditOptions(editor.ApplyEditMode, ioStreams)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "edit-last-applied (RESOURCE/NAME | -f FILENAME)",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Edit latest last-applied-configuration annotations of a resource/object"),
|
||||||
|
Long: applyEditLastAppliedLong,
|
||||||
|
Example: applyEditLastAppliedExample,
|
||||||
|
ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(o.Complete(f, args, cmd))
|
||||||
|
cmdutil.CheckErr(o.Run())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind flag structs
|
||||||
|
o.RecordFlags.AddFlags(cmd)
|
||||||
|
o.PrintFlags.AddFlags(cmd)
|
||||||
|
|
||||||
|
usage := "to use to edit the resource"
|
||||||
|
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||||
|
cmd.Flags().BoolVar(&o.WindowsLineEndings, "windows-line-endings", o.WindowsLineEndings,
|
||||||
|
"Defaults to the line ending native to your platform.")
|
||||||
|
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, FieldManagerClientSideApply)
|
||||||
|
cmdutil.AddValidateFlags(cmd)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
|
@ -0,0 +1,229 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 apply
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/util/editor"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetLastAppliedOptions defines options for the `apply set-last-applied` command.`
|
||||||
|
type SetLastAppliedOptions struct {
|
||||||
|
CreateAnnotation bool
|
||||||
|
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
PrintObj printers.ResourcePrinterFunc
|
||||||
|
|
||||||
|
FilenameOptions resource.FilenameOptions
|
||||||
|
|
||||||
|
infoList []*resource.Info
|
||||||
|
namespace string
|
||||||
|
enforceNamespace bool
|
||||||
|
dryRunStrategy cmdutil.DryRunStrategy
|
||||||
|
dryRunVerifier *resource.QueryParamVerifier
|
||||||
|
shortOutput bool
|
||||||
|
output string
|
||||||
|
patchBufferList []PatchBuffer
|
||||||
|
builder *resource.Builder
|
||||||
|
unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
|
||||||
|
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatchBuffer caches changes that are to be applied.
|
||||||
|
type PatchBuffer struct {
|
||||||
|
Patch []byte
|
||||||
|
PatchType types.PatchType
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
applySetLastAppliedLong = templates.LongDesc(i18n.T(`
|
||||||
|
Set the latest last-applied-configuration annotations by setting it to match the contents of a file.
|
||||||
|
This results in the last-applied-configuration being updated as though 'kubectl apply -f <file>' was run,
|
||||||
|
without updating any other parts of the object.`))
|
||||||
|
|
||||||
|
applySetLastAppliedExample = templates.Examples(i18n.T(`
|
||||||
|
# Set the last-applied-configuration of a resource to match the contents of a file
|
||||||
|
kubectl apply set-last-applied -f deploy.yaml
|
||||||
|
|
||||||
|
# Execute set-last-applied against each configuration file in a directory
|
||||||
|
kubectl apply set-last-applied -f path/
|
||||||
|
|
||||||
|
# Set the last-applied-configuration of a resource to match the contents of a file; will create the annotation if it does not already exist
|
||||||
|
kubectl apply set-last-applied -f deploy.yaml --create-annotation=true
|
||||||
|
`))
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSetLastAppliedOptions takes option arguments from a CLI stream and returns it at SetLastAppliedOptions type.
|
||||||
|
func NewSetLastAppliedOptions(ioStreams genericclioptions.IOStreams) *SetLastAppliedOptions {
|
||||||
|
return &SetLastAppliedOptions{
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("configured").WithTypeSetter(scheme.Scheme),
|
||||||
|
IOStreams: ioStreams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdApplySetLastApplied creates the cobra CLI `apply` subcommand `set-last-applied`.`
|
||||||
|
func NewCmdApplySetLastApplied(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
o := NewSetLastAppliedOptions(ioStreams)
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "set-last-applied -f FILENAME",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Set the last-applied-configuration annotation on a live object to match the contents of a file"),
|
||||||
|
Long: applySetLastAppliedLong,
|
||||||
|
Example: applySetLastAppliedExample,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(o.Complete(f, cmd))
|
||||||
|
cmdutil.CheckErr(o.Validate())
|
||||||
|
cmdutil.CheckErr(o.RunSetLastApplied())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
o.PrintFlags.AddFlags(cmd)
|
||||||
|
|
||||||
|
cmdutil.AddDryRunFlag(cmd)
|
||||||
|
cmd.Flags().BoolVar(&o.CreateAnnotation, "create-annotation", o.CreateAnnotation, "Will create 'last-applied-configuration' annotations if current objects doesn't have one")
|
||||||
|
cmdutil.AddJsonFilenameFlag(cmd.Flags(), &o.FilenameOptions.Filenames, "Filename, directory, or URL to files that contains the last-applied-configuration annotations")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete populates dry-run and output flag options.
|
||||||
|
func (o *SetLastAppliedOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||||
|
var err error
|
||||||
|
o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dynamicClient, err := f.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.dryRunVerifier = resource.NewQueryParamVerifier(dynamicClient, f.OpenAPIGetter(), resource.QueryParamDryRun)
|
||||||
|
o.output = cmdutil.GetFlagString(cmd, "output")
|
||||||
|
o.shortOutput = o.output == "name"
|
||||||
|
|
||||||
|
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.builder = f.NewBuilder()
|
||||||
|
o.unstructuredClientForMapping = f.UnstructuredClientForMapping
|
||||||
|
|
||||||
|
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy)
|
||||||
|
printer, err := o.PrintFlags.ToPrinter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.PrintObj = printer.PrintObj
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks SetLastAppliedOptions for validity.
|
||||||
|
func (o *SetLastAppliedOptions) Validate() error {
|
||||||
|
r := o.builder.
|
||||||
|
Unstructured().
|
||||||
|
NamespaceParam(o.namespace).DefaultNamespace().
|
||||||
|
FilenameParam(o.enforceNamespace, &o.FilenameOptions).
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
|
||||||
|
err := r.Visit(func(info *resource.Info, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
patchBuf, diffBuf, patchType, err := editor.GetApplyPatch(info.Object.(runtime.Unstructured))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the object exists in the cluster before trying to patch it.
|
||||||
|
if err := info.Get(); err != nil {
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err)
|
||||||
|
}
|
||||||
|
originalBuf, err := util.GetOriginalConfiguration(info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err)
|
||||||
|
}
|
||||||
|
if originalBuf == nil && !o.CreateAnnotation {
|
||||||
|
return fmt.Errorf("no last-applied-configuration annotation found on resource: %s, to create the annotation, run the command with --create-annotation", info.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
//only add to PatchBufferList when changed
|
||||||
|
if !bytes.Equal(cmdutil.StripComments(originalBuf), cmdutil.StripComments(diffBuf)) {
|
||||||
|
p := PatchBuffer{Patch: patchBuf, PatchType: patchType}
|
||||||
|
o.patchBufferList = append(o.patchBufferList, p)
|
||||||
|
o.infoList = append(o.infoList, info)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(o.Out, "set-last-applied %s: no changes required.\n", info.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunSetLastApplied executes the `set-last-applied` command according to SetLastAppliedOptions.
|
||||||
|
func (o *SetLastAppliedOptions) RunSetLastApplied() error {
|
||||||
|
for i, patch := range o.patchBufferList {
|
||||||
|
info := o.infoList[i]
|
||||||
|
finalObj := info.Object
|
||||||
|
|
||||||
|
if o.dryRunStrategy != cmdutil.DryRunClient {
|
||||||
|
mapping := info.ResourceMapping()
|
||||||
|
client, err := o.unstructuredClientForMapping(mapping)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if o.dryRunStrategy == cmdutil.DryRunServer {
|
||||||
|
if err := o.dryRunVerifier.HasSupport(mapping.GroupVersionKind); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helper := resource.
|
||||||
|
NewHelper(client, mapping).
|
||||||
|
DryRun(o.dryRunStrategy == cmdutil.DryRunServer)
|
||||||
|
finalObj, err = helper.Patch(info.Namespace, info.Name, patch.PatchType, patch.Patch, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := o.PrintObj(finalObj, o.Out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 apply
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/completion"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ViewLastAppliedOptions defines options for the `apply view-last-applied` command.`
|
||||||
|
type ViewLastAppliedOptions struct {
|
||||||
|
FilenameOptions resource.FilenameOptions
|
||||||
|
Selector string
|
||||||
|
LastAppliedConfigurationList []string
|
||||||
|
OutputFormat string
|
||||||
|
All bool
|
||||||
|
Factory cmdutil.Factory
|
||||||
|
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
applyViewLastAppliedLong = templates.LongDesc(i18n.T(`
|
||||||
|
View the latest last-applied-configuration annotations by type/name or file.
|
||||||
|
|
||||||
|
The default output will be printed to stdout in YAML format. You can use the -o option
|
||||||
|
to change the output format.`))
|
||||||
|
|
||||||
|
applyViewLastAppliedExample = templates.Examples(i18n.T(`
|
||||||
|
# View the last-applied-configuration annotations by type/name in YAML
|
||||||
|
kubectl apply view-last-applied deployment/nginx
|
||||||
|
|
||||||
|
# View the last-applied-configuration annotations by file in JSON
|
||||||
|
kubectl apply view-last-applied -f deploy.yaml -o json`))
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewViewLastAppliedOptions takes option arguments from a CLI stream and returns it at ViewLastAppliedOptions type.
|
||||||
|
func NewViewLastAppliedOptions(ioStreams genericclioptions.IOStreams) *ViewLastAppliedOptions {
|
||||||
|
return &ViewLastAppliedOptions{
|
||||||
|
OutputFormat: "yaml",
|
||||||
|
|
||||||
|
IOStreams: ioStreams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdApplyViewLastApplied creates the cobra CLI `apply` subcommand `view-last-applied`.`
|
||||||
|
func NewCmdApplyViewLastApplied(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
options := NewViewLastAppliedOptions(ioStreams)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "view-last-applied (TYPE [NAME | -l label] | TYPE/NAME | -f FILENAME)",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("View the latest last-applied-configuration annotations of a resource/object"),
|
||||||
|
Long: applyViewLastAppliedLong,
|
||||||
|
Example: applyViewLastAppliedExample,
|
||||||
|
ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(options.Complete(cmd, f, args))
|
||||||
|
cmdutil.CheckErr(options.Validate(cmd))
|
||||||
|
cmdutil.CheckErr(options.RunApplyViewLastApplied(cmd))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&options.OutputFormat, "output", "o", options.OutputFormat, `Output format. Must be one of (yaml, json)`)
|
||||||
|
cmd.Flags().BoolVar(&options.All, "all", options.All, "Select all resources in the namespace of the specified resource types")
|
||||||
|
usage := "that contains the last-applied-configuration annotations"
|
||||||
|
cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage)
|
||||||
|
cmdutil.AddLabelSelectorFlagVar(cmd, &options.Selector)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete checks an object for last-applied-configuration annotations.
|
||||||
|
func (o *ViewLastAppliedOptions) Complete(cmd *cobra.Command, f cmdutil.Factory, args []string) error {
|
||||||
|
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := f.NewBuilder().
|
||||||
|
Unstructured().
|
||||||
|
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||||
|
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||||
|
ResourceTypeOrNameArgs(enforceNamespace, args...).
|
||||||
|
SelectAllParam(o.All).
|
||||||
|
LabelSelectorParam(o.Selector).
|
||||||
|
Latest().
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
err = r.Err()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Visit(func(info *resource.Info, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configString, err := util.GetOriginalConfiguration(info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if configString == nil {
|
||||||
|
return cmdutil.AddSourceToErr(fmt.Sprintf("no last-applied-configuration annotation found on resource: %s\n", info.Name), info.Source, err)
|
||||||
|
}
|
||||||
|
o.LastAppliedConfigurationList = append(o.LastAppliedConfigurationList, string(configString))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks ViewLastAppliedOptions for validity.
|
||||||
|
func (o *ViewLastAppliedOptions) Validate(cmd *cobra.Command) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunApplyViewLastApplied executes the `view-last-applied` command according to ViewLastAppliedOptions.
|
||||||
|
func (o *ViewLastAppliedOptions) RunApplyViewLastApplied(cmd *cobra.Command) error {
|
||||||
|
for _, str := range o.LastAppliedConfigurationList {
|
||||||
|
switch o.OutputFormat {
|
||||||
|
case "json":
|
||||||
|
jsonBuffer := &bytes.Buffer{}
|
||||||
|
err := json.Indent(jsonBuffer, []byte(str), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprint(o.Out, string(jsonBuffer.Bytes()))
|
||||||
|
case "yaml":
|
||||||
|
yamlOutput, err := yaml.JSONToYAML([]byte(str))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprint(o.Out, string(yamlOutput))
|
||||||
|
default:
|
||||||
|
return cmdutil.UsageErrorf(
|
||||||
|
cmd,
|
||||||
|
"Unexpected -o output mode: %s, the flag 'output' must be one of yaml|json",
|
||||||
|
o.OutputFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,252 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 apply
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/jsonmergepatch"
|
||||||
|
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||||
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
oapi "k8s.io/kube-openapi/pkg/util/proto"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/openapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// maxPatchRetry is the maximum number of conflicts retry for during a patch operation before returning failure
|
||||||
|
maxPatchRetry = 5
|
||||||
|
// backOffPeriod is the period to back off when apply patch results in error.
|
||||||
|
backOffPeriod = 1 * time.Second
|
||||||
|
// how many times we can retry before back off
|
||||||
|
triesBeforeBackOff = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Patcher defines options to patch OpenAPI objects.
|
||||||
|
type Patcher struct {
|
||||||
|
Mapping *meta.RESTMapping
|
||||||
|
Helper *resource.Helper
|
||||||
|
|
||||||
|
Overwrite bool
|
||||||
|
BackOff clockwork.Clock
|
||||||
|
|
||||||
|
Force bool
|
||||||
|
CascadingStrategy metav1.DeletionPropagation
|
||||||
|
Timeout time.Duration
|
||||||
|
GracePeriod int
|
||||||
|
|
||||||
|
// If set, forces the patch against a specific resourceVersion
|
||||||
|
ResourceVersion *string
|
||||||
|
|
||||||
|
// Number of retries to make if the patch fails with conflict
|
||||||
|
Retries int
|
||||||
|
|
||||||
|
OpenapiSchema openapi.Resources
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) {
|
||||||
|
var openapiSchema openapi.Resources
|
||||||
|
if o.OpenAPIPatch {
|
||||||
|
openapiSchema = o.OpenAPISchema
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Patcher{
|
||||||
|
Mapping: info.Mapping,
|
||||||
|
Helper: helper,
|
||||||
|
Overwrite: o.Overwrite,
|
||||||
|
BackOff: clockwork.NewRealClock(),
|
||||||
|
Force: o.DeleteOptions.ForceDeletion,
|
||||||
|
CascadingStrategy: o.DeleteOptions.CascadingStrategy,
|
||||||
|
Timeout: o.DeleteOptions.Timeout,
|
||||||
|
GracePeriod: o.DeleteOptions.GracePeriod,
|
||||||
|
OpenapiSchema: openapiSchema,
|
||||||
|
Retries: maxPatchRetry,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Patcher) delete(namespace, name string) error {
|
||||||
|
options := asDeleteOptions(p.CascadingStrategy, 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) {
|
||||||
|
// Serialize the current configuration of the object from the server.
|
||||||
|
current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("serializing current configuration from:\n%v\nfor:", obj), source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the original configuration of the object from the annotation.
|
||||||
|
original, err := util.GetOriginalConfiguration(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("retrieving original configuration from:\n%v\nfor:", obj), source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var patchType types.PatchType
|
||||||
|
var patch []byte
|
||||||
|
var lookupPatchMeta strategicpatch.LookupPatchMeta
|
||||||
|
var schema oapi.Schema
|
||||||
|
createPatchErrFormat := "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:"
|
||||||
|
|
||||||
|
// Create the versioned struct from the type defined in the restmapping
|
||||||
|
// (which is the API version we'll be submitting the patch to)
|
||||||
|
versionedObject, err := scheme.Scheme.New(p.Mapping.GroupVersionKind)
|
||||||
|
switch {
|
||||||
|
case runtime.IsNotRegisteredError(err):
|
||||||
|
// fall back to generic JSON merge patch
|
||||||
|
patchType = types.MergePatchType
|
||||||
|
preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
|
||||||
|
mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")}
|
||||||
|
patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...)
|
||||||
|
if err != nil {
|
||||||
|
if mergepatch.IsPreconditionFailed(err) {
|
||||||
|
return nil, nil, fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
|
||||||
|
}
|
||||||
|
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
|
||||||
|
}
|
||||||
|
case err != nil:
|
||||||
|
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("getting instance of versioned object for %v:", p.Mapping.GroupVersionKind), source, err)
|
||||||
|
case err == nil:
|
||||||
|
// Compute a three way strategic merge patch to send to server.
|
||||||
|
patchType = types.StrategicMergePatchType
|
||||||
|
|
||||||
|
// Try to use openapi first if the openapi spec is available and can successfully calculate the patch.
|
||||||
|
// Otherwise, fall back to baked-in types.
|
||||||
|
if p.OpenapiSchema != nil {
|
||||||
|
if schema = p.OpenapiSchema.LookupResource(p.Mapping.GroupVersionKind); schema != nil {
|
||||||
|
lookupPatchMeta = strategicpatch.PatchMetaFromOpenAPI{Schema: schema}
|
||||||
|
if openapiPatch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite); err != nil {
|
||||||
|
fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
|
||||||
|
} else {
|
||||||
|
patchType = types.StrategicMergePatchType
|
||||||
|
patch = openapiPatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if patch == nil {
|
||||||
|
lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
|
||||||
|
}
|
||||||
|
patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(patch) == "{}" {
|
||||||
|
return patch, obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ResourceVersion != nil {
|
||||||
|
patch, err = addResourceVersion(patch, *p.ResourceVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, cmdutil.AddSourceToErr("Failed to insert resourceVersion in patch", source, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patchedObj, err := p.Helper.Patch(namespace, name, patchType, patch, nil)
|
||||||
|
return patch, patchedObj, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch tries to patch an OpenAPI resource. On success, returns the merge patch as well
|
||||||
|
// the final patched object. On failure, returns an error.
|
||||||
|
func (p *Patcher) Patch(current runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
|
||||||
|
var getErr error
|
||||||
|
patchBytes, patchObject, err := p.patchSimple(current, modified, source, namespace, name, errOut)
|
||||||
|
if p.Retries == 0 {
|
||||||
|
p.Retries = maxPatchRetry
|
||||||
|
}
|
||||||
|
for i := 1; i <= p.Retries && errors.IsConflict(err); i++ {
|
||||||
|
if i > triesBeforeBackOff {
|
||||||
|
p.BackOff.Sleep(backOffPeriod)
|
||||||
|
}
|
||||||
|
current, getErr = p.Helper.Get(namespace, name)
|
||||||
|
if getErr != nil {
|
||||||
|
return nil, nil, getErr
|
||||||
|
}
|
||||||
|
patchBytes, patchObject, err = p.patchSimple(current, modified, source, namespace, name, errOut)
|
||||||
|
}
|
||||||
|
if err != nil && (errors.IsConflict(err) || errors.IsInvalid(err)) && p.Force {
|
||||||
|
patchBytes, patchObject, err = p.deleteAndCreate(current, modified, namespace, name)
|
||||||
|
}
|
||||||
|
return patchBytes, patchObject, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, namespace, name string) ([]byte, runtime.Object, error) {
|
||||||
|
if err := p.delete(namespace, name); err != nil {
|
||||||
|
return modified, nil, err
|
||||||
|
}
|
||||||
|
// TODO: use wait
|
||||||
|
if err := wait.PollImmediate(1*time.Second, p.Timeout, func() (bool, error) {
|
||||||
|
if _, err := p.Helper.Get(namespace, name); !errors.IsNotFound(err) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}); err != nil {
|
||||||
|
return modified, nil, err
|
||||||
|
}
|
||||||
|
versionedObject, _, err := unstructured.UnstructuredJSONScheme.Decode(modified, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return modified, nil, err
|
||||||
|
}
|
||||||
|
createdObject, err := p.Helper.Create(namespace, true, versionedObject)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
createdObject = recreated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modified, createdObject, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func addResourceVersion(patch []byte, rv string) ([]byte, error) {
|
||||||
|
var patchMap map[string]interface{}
|
||||||
|
err := json.Unmarshal(patch, &patchMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u := unstructured.Unstructured{Object: patchMap}
|
||||||
|
a, err := meta.Accessor(&u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a.SetResourceVersion(rv)
|
||||||
|
|
||||||
|
return json.Marshal(patchMap)
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 apply
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/prune"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pruner struct {
|
||||||
|
mapper meta.RESTMapper
|
||||||
|
dynamicClient dynamic.Interface
|
||||||
|
|
||||||
|
visitedUids sets.String
|
||||||
|
visitedNamespaces sets.String
|
||||||
|
labelSelector string
|
||||||
|
fieldSelector string
|
||||||
|
|
||||||
|
cascadingStrategy metav1.DeletionPropagation
|
||||||
|
dryRunStrategy cmdutil.DryRunStrategy
|
||||||
|
gracePeriod int
|
||||||
|
|
||||||
|
toPrinter func(string) (printers.ResourcePrinter, error)
|
||||||
|
|
||||||
|
out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPruner(o *ApplyOptions) pruner {
|
||||||
|
return pruner{
|
||||||
|
mapper: o.Mapper,
|
||||||
|
dynamicClient: o.DynamicClient,
|
||||||
|
|
||||||
|
labelSelector: o.Selector,
|
||||||
|
visitedUids: o.VisitedUids,
|
||||||
|
visitedNamespaces: o.VisitedNamespaces,
|
||||||
|
|
||||||
|
cascadingStrategy: o.DeleteOptions.CascadingStrategy,
|
||||||
|
dryRunStrategy: o.DryRunStrategy,
|
||||||
|
gracePeriod: o.DeleteOptions.GracePeriod,
|
||||||
|
|
||||||
|
toPrinter: o.ToPrinter,
|
||||||
|
|
||||||
|
out: o.Out,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pruner) pruneAll(o *ApplyOptions) error {
|
||||||
|
|
||||||
|
namespacedRESTMappings, nonNamespacedRESTMappings, err := prune.GetRESTMappings(o.Mapper, o.PruneResources)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error retrieving RESTMappings to prune: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for n := range p.visitedNamespaces {
|
||||||
|
for _, m := range namespacedRESTMappings {
|
||||||
|
if err := p.prune(n, m); err != nil {
|
||||||
|
return fmt.Errorf("error pruning namespaced object %v: %v", m.GroupVersionKind, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, m := range nonNamespacedRESTMappings {
|
||||||
|
if err := p.prune(metav1.NamespaceNone, m); err != nil {
|
||||||
|
return fmt.Errorf("error pruning nonNamespaced object %v: %v", m.GroupVersionKind, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pruner) prune(namespace string, mapping *meta.RESTMapping) error {
|
||||||
|
objList, err := p.dynamicClient.Resource(mapping.Resource).
|
||||||
|
Namespace(namespace).
|
||||||
|
List(context.TODO(), metav1.ListOptions{
|
||||||
|
LabelSelector: p.labelSelector,
|
||||||
|
FieldSelector: p.fieldSelector,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
objs, err := meta.ExtractList(objList)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, obj := range objs {
|
||||||
|
metadata, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
annots := metadata.GetAnnotations()
|
||||||
|
if _, ok := annots[corev1.LastAppliedConfigAnnotation]; !ok {
|
||||||
|
// don't prune resources not created with apply
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uid := metadata.GetUID()
|
||||||
|
if p.visitedUids.Has(string(uid)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := metadata.GetName()
|
||||||
|
if p.dryRunStrategy != cmdutil.DryRunClient {
|
||||||
|
if err := p.delete(namespace, name, mapping); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printer, err := p.toPrinter("pruned")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printer.PrintObj(obj, p.out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pruner) delete(namespace, name string, mapping *meta.RESTMapping) error {
|
||||||
|
return runDelete(namespace, name, mapping, p.dynamicClient, p.cascadingStrategy, p.gracePeriod, p.dryRunStrategy == cmdutil.DryRunServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDelete(namespace, name string, mapping *meta.RESTMapping, c dynamic.Interface, cascadingStrategy metav1.DeletionPropagation, gracePeriod int, serverDryRun bool) error {
|
||||||
|
options := asDeleteOptions(cascadingStrategy, gracePeriod)
|
||||||
|
if serverDryRun {
|
||||||
|
options.DryRun = []string{metav1.DryRunAll}
|
||||||
|
}
|
||||||
|
return c.Resource(mapping.Resource).Namespace(namespace).Delete(context.TODO(), name, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func asDeleteOptions(cascadingStrategy metav1.DeletionPropagation, gracePeriod int) metav1.DeleteOptions {
|
||||||
|
options := metav1.DeleteOptions{}
|
||||||
|
if gracePeriod >= 0 {
|
||||||
|
options = *metav1.NewDeleteOptions(int64(gracePeriod))
|
||||||
|
}
|
||||||
|
options.PropagationPolicy = &cascadingStrategy
|
||||||
|
return options
|
||||||
|
}
|
|
@ -0,0 +1,442 @@
|
||||||
|
/*
|
||||||
|
Copyright 2014 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 delete
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
cmdwait "k8s.io/kubectl/pkg/cmd/wait"
|
||||||
|
"k8s.io/kubectl/pkg/rawhttp"
|
||||||
|
"k8s.io/kubectl/pkg/util/completion"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
deleteLong = templates.LongDesc(i18n.T(`
|
||||||
|
Delete resources by file names, stdin, resources and names, or by resources and label selector.
|
||||||
|
|
||||||
|
JSON and YAML formats are accepted. Only one type of argument may be specified: file names,
|
||||||
|
resources and names, or resources and label selector.
|
||||||
|
|
||||||
|
Some resources, such as pods, support graceful deletion. These resources define a default period
|
||||||
|
before they are forcibly terminated (the grace period) but you may override that value with
|
||||||
|
the --grace-period flag, or pass --now to set a grace-period of 1. Because these resources often
|
||||||
|
represent entities in the cluster, deletion may not be acknowledged immediately. If the node
|
||||||
|
hosting a pod is down or cannot reach the API server, termination may take significantly longer
|
||||||
|
than the grace period. To force delete a resource, you must specify the --force flag.
|
||||||
|
Note: only a subset of resources support graceful deletion. In absence of the support,
|
||||||
|
the --grace-period flag is ignored.
|
||||||
|
|
||||||
|
IMPORTANT: Force deleting pods does not wait for confirmation that the pod's processes have been
|
||||||
|
terminated, which can leave those processes running until the node detects the deletion and
|
||||||
|
completes graceful deletion. If your processes use shared storage or talk to a remote API and
|
||||||
|
depend on the name of the pod to identify themselves, force deleting those pods may result in
|
||||||
|
multiple processes running on different machines using the same identification which may lead
|
||||||
|
to data corruption or inconsistency. Only force delete pods when you are sure the pod is
|
||||||
|
terminated, or if your application can tolerate multiple copies of the same pod running at once.
|
||||||
|
Also, if you force delete pods, the scheduler may place new pods on those nodes before the node
|
||||||
|
has released those resources and causing those pods to be evicted immediately.
|
||||||
|
|
||||||
|
Note that the delete command does NOT do resource version checks, so if someone submits an
|
||||||
|
update to a resource right when you submit a delete, their update will be lost along with the
|
||||||
|
rest of the resource.
|
||||||
|
|
||||||
|
After a CustomResourceDefinition is deleted, invalidation of discovery cache may take up
|
||||||
|
to 10 minutes. If you don't want to wait, you might want to run "kubectl api-resources"
|
||||||
|
to refresh the discovery cache.`))
|
||||||
|
|
||||||
|
deleteExample = templates.Examples(i18n.T(`
|
||||||
|
# Delete a pod using the type and name specified in pod.json
|
||||||
|
kubectl delete -f ./pod.json
|
||||||
|
|
||||||
|
# Delete resources from a directory containing kustomization.yaml - e.g. dir/kustomization.yaml
|
||||||
|
kubectl delete -k dir
|
||||||
|
|
||||||
|
# Delete resources from all files that end with '.json' - i.e. expand wildcard characters in file names
|
||||||
|
kubectl apply -f '*.json'
|
||||||
|
|
||||||
|
# Delete a pod based on the type and name in the JSON passed into stdin
|
||||||
|
cat pod.json | kubectl delete -f -
|
||||||
|
|
||||||
|
# Delete pods and services with same names "baz" and "foo"
|
||||||
|
kubectl delete pod,service baz foo
|
||||||
|
|
||||||
|
# Delete pods and services with label name=myLabel
|
||||||
|
kubectl delete pods,services -l name=myLabel
|
||||||
|
|
||||||
|
# Delete a pod with minimal delay
|
||||||
|
kubectl delete pod foo --now
|
||||||
|
|
||||||
|
# Force delete a pod on a dead node
|
||||||
|
kubectl delete pod foo --force
|
||||||
|
|
||||||
|
# Delete all pods
|
||||||
|
kubectl delete pods --all`))
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeleteOptions struct {
|
||||||
|
resource.FilenameOptions
|
||||||
|
|
||||||
|
LabelSelector string
|
||||||
|
FieldSelector string
|
||||||
|
DeleteAll bool
|
||||||
|
DeleteAllNamespaces bool
|
||||||
|
CascadingStrategy metav1.DeletionPropagation
|
||||||
|
IgnoreNotFound bool
|
||||||
|
DeleteNow bool
|
||||||
|
ForceDeletion bool
|
||||||
|
WaitForDeletion bool
|
||||||
|
Quiet bool
|
||||||
|
WarnClusterScope bool
|
||||||
|
Raw string
|
||||||
|
|
||||||
|
GracePeriod int
|
||||||
|
Timeout time.Duration
|
||||||
|
|
||||||
|
DryRunStrategy cmdutil.DryRunStrategy
|
||||||
|
DryRunVerifier *resource.QueryParamVerifier
|
||||||
|
|
||||||
|
Output string
|
||||||
|
|
||||||
|
DynamicClient dynamic.Interface
|
||||||
|
Mapper meta.RESTMapper
|
||||||
|
Result *resource.Result
|
||||||
|
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmdDelete(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
deleteFlags := NewDeleteCommandFlags("containing the resource to delete.")
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "delete ([-f FILENAME] | [-k DIRECTORY] | TYPE [(NAME | -l label | --all)])",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Delete resources by file names, stdin, resources and names, or by resources and label selector"),
|
||||||
|
Long: deleteLong,
|
||||||
|
Example: deleteExample,
|
||||||
|
ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
o, err := deleteFlags.ToOptions(nil, streams)
|
||||||
|
cmdutil.CheckErr(err)
|
||||||
|
cmdutil.CheckErr(o.Complete(f, args, cmd))
|
||||||
|
cmdutil.CheckErr(o.Validate())
|
||||||
|
cmdutil.CheckErr(o.RunDelete(f))
|
||||||
|
},
|
||||||
|
SuggestFor: []string{"rm"},
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFlags.AddFlags(cmd)
|
||||||
|
cmdutil.AddDryRunFlag(cmd)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Command) error {
|
||||||
|
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.WarnClusterScope = enforceNamespace && !o.DeleteAllNamespaces
|
||||||
|
|
||||||
|
if o.DeleteAll || len(o.LabelSelector) > 0 || len(o.FieldSelector) > 0 {
|
||||||
|
if f := cmd.Flags().Lookup("ignore-not-found"); f != nil && !f.Changed {
|
||||||
|
// If the user didn't explicitly set the option, default to ignoring NotFound errors when used with --all, -l, or --field-selector
|
||||||
|
o.IgnoreNotFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if o.DeleteNow {
|
||||||
|
if o.GracePeriod != -1 {
|
||||||
|
return fmt.Errorf("--now and --grace-period cannot be specified together")
|
||||||
|
}
|
||||||
|
o.GracePeriod = 1
|
||||||
|
}
|
||||||
|
if o.GracePeriod == 0 && !o.ForceDeletion {
|
||||||
|
// To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0
|
||||||
|
// into --grace-period=1. Users may provide --force to bypass this conversion.
|
||||||
|
o.GracePeriod = 1
|
||||||
|
}
|
||||||
|
if o.ForceDeletion && o.GracePeriod < 0 {
|
||||||
|
o.GracePeriod = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dynamicClient, err := f.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.DryRunVerifier = resource.NewQueryParamVerifier(dynamicClient, f.OpenAPIGetter(), resource.QueryParamDryRun)
|
||||||
|
|
||||||
|
if len(o.Raw) == 0 {
|
||||||
|
r := f.NewBuilder().
|
||||||
|
Unstructured().
|
||||||
|
ContinueOnError().
|
||||||
|
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||||
|
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||||
|
LabelSelectorParam(o.LabelSelector).
|
||||||
|
FieldSelectorParam(o.FieldSelector).
|
||||||
|
SelectAllParam(o.DeleteAll).
|
||||||
|
AllNamespaces(o.DeleteAllNamespaces).
|
||||||
|
ResourceTypeOrNameArgs(false, args...).RequireObject(false).
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
err = r.Err()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.Result = r
|
||||||
|
|
||||||
|
o.Mapper, err = f.ToRESTMapper()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.DynamicClient, err = f.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *DeleteOptions) Validate() error {
|
||||||
|
if o.Output != "" && o.Output != "name" {
|
||||||
|
return fmt.Errorf("unexpected -o output mode: %v. We only support '-o name'", o.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.DeleteAll && len(o.LabelSelector) > 0 {
|
||||||
|
return fmt.Errorf("cannot set --all and --selector at the same time")
|
||||||
|
}
|
||||||
|
if o.DeleteAll && len(o.FieldSelector) > 0 {
|
||||||
|
return fmt.Errorf("cannot set --all and --field-selector at the same time")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
case o.GracePeriod > 0 && o.ForceDeletion:
|
||||||
|
return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(o.Raw) > 0 {
|
||||||
|
if len(o.FilenameOptions.Filenames) > 1 {
|
||||||
|
return fmt.Errorf("--raw can only use a single local file or stdin")
|
||||||
|
} else if len(o.FilenameOptions.Filenames) == 1 {
|
||||||
|
if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 {
|
||||||
|
return fmt.Errorf("--raw cannot read from a url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.FilenameOptions.Recursive {
|
||||||
|
return fmt.Errorf("--raw and --recursive are mutually exclusive")
|
||||||
|
}
|
||||||
|
if len(o.Output) > 0 {
|
||||||
|
return fmt.Errorf("--raw and --output are mutually exclusive")
|
||||||
|
}
|
||||||
|
if _, err := url.ParseRequestURI(o.Raw); err != nil {
|
||||||
|
return fmt.Errorf("--raw must be a valid URL path: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *DeleteOptions) RunDelete(f cmdutil.Factory) error {
|
||||||
|
if len(o.Raw) > 0 {
|
||||||
|
restClient, err := f.RESTClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(o.Filenames) == 0 {
|
||||||
|
return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, "")
|
||||||
|
}
|
||||||
|
return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, o.Filenames[0])
|
||||||
|
}
|
||||||
|
return o.DeleteResult(o.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *DeleteOptions) DeleteResult(r *resource.Result) error {
|
||||||
|
found := 0
|
||||||
|
if o.IgnoreNotFound {
|
||||||
|
r = r.IgnoreErrors(errors.IsNotFound)
|
||||||
|
}
|
||||||
|
warnClusterScope := o.WarnClusterScope
|
||||||
|
deletedInfos := []*resource.Info{}
|
||||||
|
uidMap := cmdwait.UIDMap{}
|
||||||
|
err := r.Visit(func(info *resource.Info, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deletedInfos = append(deletedInfos, info)
|
||||||
|
found++
|
||||||
|
|
||||||
|
options := &metav1.DeleteOptions{}
|
||||||
|
if o.GracePeriod >= 0 {
|
||||||
|
options = metav1.NewDeleteOptions(int64(o.GracePeriod))
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
warnClusterScope = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.DryRunStrategy == cmdutil.DryRunClient {
|
||||||
|
if !o.Quiet {
|
||||||
|
o.PrintObj(info)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if o.DryRunStrategy == cmdutil.DryRunServer {
|
||||||
|
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response, err := o.deleteResource(info, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resourceLocation := cmdwait.ResourceLocation{
|
||||||
|
GroupResource: info.Mapping.Resource.GroupResource(),
|
||||||
|
Namespace: info.Namespace,
|
||||||
|
Name: info.Name,
|
||||||
|
}
|
||||||
|
if status, ok := response.(*metav1.Status); ok && status.Details != nil {
|
||||||
|
uidMap[resourceLocation] = status.Details.UID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
responseMetadata, err := meta.Accessor(response)
|
||||||
|
if err != nil {
|
||||||
|
// we don't have UID, but we didn't fail the delete, next best thing is just skipping the UID
|
||||||
|
klog.V(1).Info(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
uidMap[resourceLocation] = responseMetadata.GetUID()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if found == 0 {
|
||||||
|
fmt.Fprintf(o.Out, "No resources found\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !o.WaitForDeletion {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// if we don't have a dynamic client, we don't want to wait. Eventually when delete is cleaned up, this will likely
|
||||||
|
// drop out.
|
||||||
|
if o.DynamicClient == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are dry-running, then we don't want to wait
|
||||||
|
if o.DryRunStrategy != cmdutil.DryRunNone {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveTimeout := o.Timeout
|
||||||
|
if effectiveTimeout == 0 {
|
||||||
|
// if we requested to wait forever, set it to a week.
|
||||||
|
effectiveTimeout = 168 * time.Hour
|
||||||
|
}
|
||||||
|
waitOptions := cmdwait.WaitOptions{
|
||||||
|
ResourceFinder: genericclioptions.ResourceFinderForResult(resource.InfoListVisitor(deletedInfos)),
|
||||||
|
UIDMap: uidMap,
|
||||||
|
DynamicClient: o.DynamicClient,
|
||||||
|
Timeout: effectiveTimeout,
|
||||||
|
|
||||||
|
Printer: printers.NewDiscardingPrinter(),
|
||||||
|
ConditionFn: cmdwait.IsDeleted,
|
||||||
|
IOStreams: o.IOStreams,
|
||||||
|
}
|
||||||
|
err = waitOptions.RunWait()
|
||||||
|
if errors.IsForbidden(err) || errors.IsMethodNotSupported(err) {
|
||||||
|
// if we're forbidden from waiting, we shouldn't fail.
|
||||||
|
// if the resource doesn't support a verb we need, we shouldn't fail.
|
||||||
|
klog.V(1).Info(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *DeleteOptions) deleteResource(info *resource.Info, deleteOptions *metav1.DeleteOptions) (runtime.Object, error) {
|
||||||
|
deleteResponse, err := resource.
|
||||||
|
NewHelper(info.Client, info.Mapping).
|
||||||
|
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
|
||||||
|
DeleteWithOptions(info.Namespace, info.Name, deleteOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cmdutil.AddSourceToErr("deleting", info.Source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !o.Quiet {
|
||||||
|
o.PrintObj(info)
|
||||||
|
}
|
||||||
|
return deleteResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintObj for deleted objects is special because we do not have an object to print.
|
||||||
|
// This mirrors name printer behavior
|
||||||
|
func (o *DeleteOptions) PrintObj(info *resource.Info) {
|
||||||
|
operation := "deleted"
|
||||||
|
groupKind := info.Mapping.GroupVersionKind
|
||||||
|
kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group)
|
||||||
|
if len(groupKind.Group) == 0 {
|
||||||
|
kindString = strings.ToLower(groupKind.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.GracePeriod == 0 {
|
||||||
|
operation = "force deleted"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.DryRunStrategy {
|
||||||
|
case cmdutil.DryRunClient:
|
||||||
|
operation = fmt.Sprintf("%s (dry run)", operation)
|
||||||
|
case cmdutil.DryRunServer:
|
||||||
|
operation = fmt.Sprintf("%s (server dry run)", operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Output == "name" {
|
||||||
|
// -o name: prints resource/name
|
||||||
|
fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// understandable output by default
|
||||||
|
fmt.Fprintf(o.Out, "%s \"%s\" %s\n", kindString, info.Name, operation)
|
||||||
|
}
|
|
@ -0,0 +1,251 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 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 delete
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeleteFlags composes common printer flag structs
|
||||||
|
// used for commands requiring deletion logic.
|
||||||
|
type DeleteFlags struct {
|
||||||
|
FileNameFlags *genericclioptions.FileNameFlags
|
||||||
|
LabelSelector *string
|
||||||
|
FieldSelector *string
|
||||||
|
|
||||||
|
All *bool
|
||||||
|
AllNamespaces *bool
|
||||||
|
CascadingStrategy *string
|
||||||
|
Force *bool
|
||||||
|
GracePeriod *int
|
||||||
|
IgnoreNotFound *bool
|
||||||
|
Now *bool
|
||||||
|
Timeout *time.Duration
|
||||||
|
Wait *bool
|
||||||
|
Output *string
|
||||||
|
Raw *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericclioptions.IOStreams) (*DeleteOptions, error) {
|
||||||
|
options := &DeleteOptions{
|
||||||
|
DynamicClient: dynamicClient,
|
||||||
|
IOStreams: streams,
|
||||||
|
}
|
||||||
|
|
||||||
|
// add filename options
|
||||||
|
if f.FileNameFlags != nil {
|
||||||
|
options.FilenameOptions = f.FileNameFlags.ToOptions()
|
||||||
|
}
|
||||||
|
if f.LabelSelector != nil {
|
||||||
|
options.LabelSelector = *f.LabelSelector
|
||||||
|
}
|
||||||
|
if f.FieldSelector != nil {
|
||||||
|
options.FieldSelector = *f.FieldSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
// add output format
|
||||||
|
if f.Output != nil {
|
||||||
|
options.Output = *f.Output
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.All != nil {
|
||||||
|
options.DeleteAll = *f.All
|
||||||
|
}
|
||||||
|
if f.AllNamespaces != nil {
|
||||||
|
options.DeleteAllNamespaces = *f.AllNamespaces
|
||||||
|
}
|
||||||
|
if f.CascadingStrategy != nil {
|
||||||
|
var err error
|
||||||
|
options.CascadingStrategy, err = parseCascadingFlag(streams, *f.CascadingStrategy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f.Force != nil {
|
||||||
|
options.ForceDeletion = *f.Force
|
||||||
|
}
|
||||||
|
if f.GracePeriod != nil {
|
||||||
|
options.GracePeriod = *f.GracePeriod
|
||||||
|
}
|
||||||
|
if f.IgnoreNotFound != nil {
|
||||||
|
options.IgnoreNotFound = *f.IgnoreNotFound
|
||||||
|
}
|
||||||
|
if f.Now != nil {
|
||||||
|
options.DeleteNow = *f.Now
|
||||||
|
}
|
||||||
|
if f.Timeout != nil {
|
||||||
|
options.Timeout = *f.Timeout
|
||||||
|
}
|
||||||
|
if f.Wait != nil {
|
||||||
|
options.WaitForDeletion = *f.Wait
|
||||||
|
}
|
||||||
|
if f.Raw != nil {
|
||||||
|
options.Raw = *f.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
return options, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DeleteFlags) AddFlags(cmd *cobra.Command) {
|
||||||
|
f.FileNameFlags.AddFlags(cmd.Flags())
|
||||||
|
if f.LabelSelector != nil {
|
||||||
|
cmdutil.AddLabelSelectorFlagVar(cmd, f.LabelSelector)
|
||||||
|
}
|
||||||
|
if f.FieldSelector != nil {
|
||||||
|
cmd.Flags().StringVarP(f.FieldSelector, "field-selector", "", *f.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.")
|
||||||
|
}
|
||||||
|
if f.All != nil {
|
||||||
|
cmd.Flags().BoolVar(f.All, "all", *f.All, "Delete all resources, in the namespace of the specified resource types.")
|
||||||
|
}
|
||||||
|
if f.AllNamespaces != nil {
|
||||||
|
cmd.Flags().BoolVarP(f.AllNamespaces, "all-namespaces", "A", *f.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
|
||||||
|
}
|
||||||
|
if f.Force != nil {
|
||||||
|
cmd.Flags().BoolVar(f.Force, "force", *f.Force, "If true, immediately remove resources from API and bypass graceful deletion. Note that immediate deletion of some resources may result in inconsistency or data loss and requires confirmation.")
|
||||||
|
}
|
||||||
|
if f.CascadingStrategy != nil {
|
||||||
|
cmd.Flags().StringVar(
|
||||||
|
f.CascadingStrategy,
|
||||||
|
"cascade",
|
||||||
|
*f.CascadingStrategy,
|
||||||
|
`Must be "background", "orphan", or "foreground". Selects the deletion cascading strategy for the dependents (e.g. Pods created by a ReplicationController). Defaults to background.`)
|
||||||
|
cmd.Flags().Lookup("cascade").NoOptDefVal = "background"
|
||||||
|
}
|
||||||
|
if f.Now != nil {
|
||||||
|
cmd.Flags().BoolVar(f.Now, "now", *f.Now, "If true, resources are signaled for immediate shutdown (same as --grace-period=1).")
|
||||||
|
}
|
||||||
|
if f.GracePeriod != nil {
|
||||||
|
cmd.Flags().IntVar(f.GracePeriod, "grace-period", *f.GracePeriod, "Period of time in seconds given to the resource to terminate gracefully. Ignored if negative. Set to 1 for immediate shutdown. Can only be set to 0 when --force is true (force deletion).")
|
||||||
|
}
|
||||||
|
if f.Timeout != nil {
|
||||||
|
cmd.Flags().DurationVar(f.Timeout, "timeout", *f.Timeout, "The length of time to wait before giving up on a delete, zero means determine a timeout from the size of the object")
|
||||||
|
}
|
||||||
|
if f.IgnoreNotFound != nil {
|
||||||
|
cmd.Flags().BoolVar(f.IgnoreNotFound, "ignore-not-found", *f.IgnoreNotFound, "Treat \"resource not found\" as a successful delete. Defaults to \"true\" when --all is specified.")
|
||||||
|
}
|
||||||
|
if f.Wait != nil {
|
||||||
|
cmd.Flags().BoolVar(f.Wait, "wait", *f.Wait, "If true, wait for resources to be gone before returning. This waits for finalizers.")
|
||||||
|
}
|
||||||
|
if f.Output != nil {
|
||||||
|
cmd.Flags().StringVarP(f.Output, "output", "o", *f.Output, "Output mode. Use \"-o name\" for shorter output (resource/name).")
|
||||||
|
}
|
||||||
|
if f.Raw != nil {
|
||||||
|
cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeleteCommandFlags provides default flags and values for use with the "delete" command
|
||||||
|
func NewDeleteCommandFlags(usage string) *DeleteFlags {
|
||||||
|
cascadingStrategy := "background"
|
||||||
|
gracePeriod := -1
|
||||||
|
|
||||||
|
// setup command defaults
|
||||||
|
all := false
|
||||||
|
allNamespaces := false
|
||||||
|
force := false
|
||||||
|
ignoreNotFound := false
|
||||||
|
now := false
|
||||||
|
output := ""
|
||||||
|
labelSelector := ""
|
||||||
|
fieldSelector := ""
|
||||||
|
timeout := time.Duration(0)
|
||||||
|
wait := true
|
||||||
|
raw := ""
|
||||||
|
|
||||||
|
filenames := []string{}
|
||||||
|
recursive := false
|
||||||
|
kustomize := ""
|
||||||
|
|
||||||
|
return &DeleteFlags{
|
||||||
|
// Not using helpers.go since it provides function to add '-k' for FileNameOptions, but not FileNameFlags
|
||||||
|
FileNameFlags: &genericclioptions.FileNameFlags{Usage: usage, Filenames: &filenames, Kustomize: &kustomize, Recursive: &recursive},
|
||||||
|
LabelSelector: &labelSelector,
|
||||||
|
FieldSelector: &fieldSelector,
|
||||||
|
|
||||||
|
CascadingStrategy: &cascadingStrategy,
|
||||||
|
GracePeriod: &gracePeriod,
|
||||||
|
|
||||||
|
All: &all,
|
||||||
|
AllNamespaces: &allNamespaces,
|
||||||
|
Force: &force,
|
||||||
|
IgnoreNotFound: &ignoreNotFound,
|
||||||
|
Now: &now,
|
||||||
|
Timeout: &timeout,
|
||||||
|
Wait: &wait,
|
||||||
|
Output: &output,
|
||||||
|
Raw: &raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeleteFlags provides default flags and values for use in commands outside of "delete"
|
||||||
|
func NewDeleteFlags(usage string) *DeleteFlags {
|
||||||
|
cascadingStrategy := "background"
|
||||||
|
gracePeriod := -1
|
||||||
|
|
||||||
|
force := false
|
||||||
|
timeout := time.Duration(0)
|
||||||
|
wait := false
|
||||||
|
|
||||||
|
filenames := []string{}
|
||||||
|
kustomize := ""
|
||||||
|
recursive := false
|
||||||
|
|
||||||
|
return &DeleteFlags{
|
||||||
|
FileNameFlags: &genericclioptions.FileNameFlags{Usage: usage, Filenames: &filenames, Kustomize: &kustomize, Recursive: &recursive},
|
||||||
|
|
||||||
|
CascadingStrategy: &cascadingStrategy,
|
||||||
|
GracePeriod: &gracePeriod,
|
||||||
|
|
||||||
|
// add non-defaults
|
||||||
|
Force: &force,
|
||||||
|
Timeout: &timeout,
|
||||||
|
Wait: &wait,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCascadingFlag(streams genericclioptions.IOStreams, cascadingFlag string) (metav1.DeletionPropagation, error) {
|
||||||
|
boolValue, err := strconv.ParseBool(cascadingFlag)
|
||||||
|
// The flag is not a boolean
|
||||||
|
if err != nil {
|
||||||
|
switch cascadingFlag {
|
||||||
|
case "orphan":
|
||||||
|
return metav1.DeletePropagationOrphan, nil
|
||||||
|
case "foreground":
|
||||||
|
return metav1.DeletePropagationForeground, nil
|
||||||
|
case "background":
|
||||||
|
return metav1.DeletePropagationBackground, nil
|
||||||
|
default:
|
||||||
|
return metav1.DeletePropagationBackground, fmt.Errorf(`invalid cascade value (%v). Must be "background", "foreground", or "orphan"`, cascadingFlag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The flag was a boolean
|
||||||
|
if boolValue {
|
||||||
|
fmt.Fprintf(streams.ErrOut, "warning: --cascade=%v is deprecated (boolean value) and can be replaced with --cascade=%s.\n", cascadingFlag, "background")
|
||||||
|
return metav1.DeletePropagationBackground, nil
|
||||||
|
}
|
||||||
|
fmt.Fprintf(streams.ErrOut, "warning: --cascade=%v is deprecated (boolean value) and can be replaced with --cascade=%s.\n", cascadingFlag, "orphan")
|
||||||
|
return metav1.DeletePropagationOrphan, nil
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 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 crlf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type crlfWriter struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCRLFWriter implements a CR/LF line ending writer used for normalizing
|
||||||
|
// text for Windows platforms.
|
||||||
|
func NewCRLFWriter(w io.Writer) io.Writer {
|
||||||
|
return crlfWriter{w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w crlfWriter) Write(b []byte) (n int, err error) {
|
||||||
|
for i, written := 0, 0; ; {
|
||||||
|
next := bytes.Index(b[i:], []byte("\n"))
|
||||||
|
if next == -1 {
|
||||||
|
n, err := w.Writer.Write(b[i:])
|
||||||
|
return written + n, err
|
||||||
|
}
|
||||||
|
next = next + i
|
||||||
|
n, err := w.Writer.Write(b[i:next])
|
||||||
|
if err != nil {
|
||||||
|
return written + n, err
|
||||||
|
}
|
||||||
|
written += n
|
||||||
|
n, err = w.Writer.Write([]byte("\r\n"))
|
||||||
|
if err != nil {
|
||||||
|
if n > 1 {
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
return written + n, err
|
||||||
|
}
|
||||||
|
written++
|
||||||
|
i = next + 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,928 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 editor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
goruntime "runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
jsonpatch "github.com/evanphx/json-patch"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||||
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/apimachinery/pkg/util/yaml"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/util/editor/crlf"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/slice"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SupportedSubresources = []string{"status"}
|
||||||
|
|
||||||
|
// EditOptions contains all the options for running edit cli command.
|
||||||
|
type EditOptions struct {
|
||||||
|
resource.FilenameOptions
|
||||||
|
RecordFlags *genericclioptions.RecordFlags
|
||||||
|
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||||
|
|
||||||
|
OutputPatch bool
|
||||||
|
WindowsLineEndings bool
|
||||||
|
|
||||||
|
cmdutil.ValidateOptions
|
||||||
|
ValidationDirective string
|
||||||
|
FieldValidationVerifier *resource.QueryParamVerifier
|
||||||
|
|
||||||
|
OriginalResult *resource.Result
|
||||||
|
|
||||||
|
EditMode EditMode
|
||||||
|
|
||||||
|
CmdNamespace string
|
||||||
|
ApplyAnnotation bool
|
||||||
|
ChangeCause string
|
||||||
|
|
||||||
|
managedFields map[types.UID][]metav1.ManagedFieldsEntry
|
||||||
|
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
|
||||||
|
Recorder genericclioptions.Recorder
|
||||||
|
f cmdutil.Factory
|
||||||
|
editPrinterOptions *editPrinterOptions
|
||||||
|
updatedResultGetter func(data []byte) *resource.Result
|
||||||
|
|
||||||
|
FieldManager string
|
||||||
|
|
||||||
|
Subresource string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEditOptions returns an initialized EditOptions instance
|
||||||
|
func NewEditOptions(editMode EditMode, ioStreams genericclioptions.IOStreams) *EditOptions {
|
||||||
|
return &EditOptions{
|
||||||
|
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||||
|
|
||||||
|
EditMode: editMode,
|
||||||
|
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("edited").WithTypeSetter(scheme.Scheme),
|
||||||
|
|
||||||
|
editPrinterOptions: &editPrinterOptions{
|
||||||
|
// create new editor-specific PrintFlags, with all
|
||||||
|
// output flags disabled, except json / yaml
|
||||||
|
printFlags: (&genericclioptions.PrintFlags{
|
||||||
|
JSONYamlPrintFlags: genericclioptions.NewJSONYamlPrintFlags(),
|
||||||
|
}).WithDefaultOutput("yaml"),
|
||||||
|
ext: ".yaml",
|
||||||
|
addHeader: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
WindowsLineEndings: goruntime.GOOS == "windows",
|
||||||
|
|
||||||
|
Recorder: genericclioptions.NoopRecorder{},
|
||||||
|
|
||||||
|
IOStreams: ioStreams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type editPrinterOptions struct {
|
||||||
|
printFlags *genericclioptions.PrintFlags
|
||||||
|
ext string
|
||||||
|
addHeader bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *editPrinterOptions) Complete(fromPrintFlags *genericclioptions.PrintFlags) error {
|
||||||
|
if e.printFlags == nil {
|
||||||
|
return fmt.Errorf("missing PrintFlags in editor printer options")
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind output format from existing printflags
|
||||||
|
if fromPrintFlags != nil && len(*fromPrintFlags.OutputFormat) > 0 {
|
||||||
|
e.printFlags.OutputFormat = fromPrintFlags.OutputFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent a commented header at the top of the user's
|
||||||
|
// default editor if presenting contents as json.
|
||||||
|
if *e.printFlags.OutputFormat == "json" {
|
||||||
|
e.addHeader = false
|
||||||
|
e.ext = ".json"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// we default to yaml if check above is false, as only json or yaml are supported
|
||||||
|
e.addHeader = true
|
||||||
|
e.ext = ".yaml"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *editPrinterOptions) PrintObj(obj runtime.Object, out io.Writer) error {
|
||||||
|
p, err := e.printFlags.ToPrinter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.PrintObj(obj, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes all the required options
|
||||||
|
func (o *EditOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Command) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
o.RecordFlags.Complete(cmd)
|
||||||
|
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.EditMode != NormalEditMode && o.EditMode != EditBeforeCreateMode && o.EditMode != ApplyEditMode {
|
||||||
|
return fmt.Errorf("unsupported edit mode %q", o.EditMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
o.editPrinterOptions.Complete(o.PrintFlags)
|
||||||
|
|
||||||
|
if o.OutputPatch && o.EditMode != NormalEditMode {
|
||||||
|
return fmt.Errorf("the edit mode doesn't support output the patch")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b := f.NewBuilder().
|
||||||
|
Unstructured()
|
||||||
|
if o.EditMode == NormalEditMode || o.EditMode == ApplyEditMode {
|
||||||
|
// when do normal edit or apply edit we need to always retrieve the latest resource from server
|
||||||
|
b = b.ResourceTypeOrNameArgs(true, args...).Latest()
|
||||||
|
}
|
||||||
|
r := b.NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||||
|
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||||
|
Subresource(o.Subresource).
|
||||||
|
ContinueOnError().
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
err = r.Err()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.OriginalResult = r
|
||||||
|
|
||||||
|
o.updatedResultGetter = func(data []byte) *resource.Result {
|
||||||
|
// resource builder to read objects from edited data
|
||||||
|
return f.NewBuilder().
|
||||||
|
Unstructured().
|
||||||
|
Stream(bytes.NewReader(data), "edited-file").
|
||||||
|
Subresource(o.Subresource).
|
||||||
|
ContinueOnError().
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
||||||
|
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||||
|
return o.PrintFlags.ToPrinter()
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicClient, err := f.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.FieldValidationVerifier = resource.NewQueryParamVerifier(dynamicClient, f.OpenAPIGetter(), resource.QueryParamFieldValidation)
|
||||||
|
|
||||||
|
o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.CmdNamespace = cmdNamespace
|
||||||
|
o.f = f
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks the EditOptions to see if there is sufficient information to run the command.
|
||||||
|
func (o *EditOptions) Validate() error {
|
||||||
|
if len(o.Subresource) > 0 && !slice.ContainsString(SupportedSubresources, o.Subresource, nil) {
|
||||||
|
return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, SupportedSubresources)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run performs the execution
|
||||||
|
func (o *EditOptions) Run() error {
|
||||||
|
edit := NewDefaultEditor(editorEnvs())
|
||||||
|
// editFn is invoked for each edit session (once with a list for normal edit, once for each individual resource in a edit-on-create invocation)
|
||||||
|
editFn := func(infos []*resource.Info) error {
|
||||||
|
var (
|
||||||
|
results = editResults{}
|
||||||
|
original = []byte{}
|
||||||
|
edited = []byte{}
|
||||||
|
file string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
containsError := false
|
||||||
|
// loop until we succeed or cancel editing
|
||||||
|
for {
|
||||||
|
// get the object we're going to serialize as input to the editor
|
||||||
|
var originalObj runtime.Object
|
||||||
|
switch len(infos) {
|
||||||
|
case 1:
|
||||||
|
originalObj = infos[0].Object
|
||||||
|
default:
|
||||||
|
l := &unstructured.UnstructuredList{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "List",
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"metadata": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, info := range infos {
|
||||||
|
l.Items = append(l.Items, *info.Object.(*unstructured.Unstructured))
|
||||||
|
}
|
||||||
|
originalObj = l
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate the file to edit
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
var w io.Writer = buf
|
||||||
|
if o.WindowsLineEndings {
|
||||||
|
w = crlf.NewCRLFWriter(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.editPrinterOptions.addHeader {
|
||||||
|
results.header.writeTo(w, o.EditMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !containsError {
|
||||||
|
if err := o.extractManagedFields(originalObj); err != nil {
|
||||||
|
return preservedFile(err, results.file, o.ErrOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.editPrinterOptions.PrintObj(originalObj, w); err != nil {
|
||||||
|
return preservedFile(err, results.file, o.ErrOut)
|
||||||
|
}
|
||||||
|
original = buf.Bytes()
|
||||||
|
} else {
|
||||||
|
// In case of an error, preserve the edited file.
|
||||||
|
// Remove the comments (header) from it since we already
|
||||||
|
// have included the latest header in the buffer above.
|
||||||
|
buf.Write(cmdutil.ManualStrip(edited))
|
||||||
|
}
|
||||||
|
|
||||||
|
// launch the editor
|
||||||
|
editedDiff := edited
|
||||||
|
edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), o.editPrinterOptions.ext, buf)
|
||||||
|
if err != nil {
|
||||||
|
return preservedFile(err, results.file, o.ErrOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're retrying the loop because of an error, and no change was made in the file, short-circuit
|
||||||
|
if containsError && bytes.Equal(cmdutil.StripComments(editedDiff), cmdutil.StripComments(edited)) {
|
||||||
|
return preservedFile(fmt.Errorf("%s", "Edit cancelled, no valid changes were saved."), file, o.ErrOut)
|
||||||
|
}
|
||||||
|
// cleanup any file from the previous pass
|
||||||
|
if len(results.file) > 0 {
|
||||||
|
os.Remove(results.file)
|
||||||
|
}
|
||||||
|
klog.V(4).Infof("User edited:\n%s", string(edited))
|
||||||
|
|
||||||
|
// Apply validation
|
||||||
|
schema, err := o.f.Validator(o.ValidationDirective, o.FieldValidationVerifier)
|
||||||
|
if err != nil {
|
||||||
|
return preservedFile(err, file, o.ErrOut)
|
||||||
|
}
|
||||||
|
err = schema.ValidateBytes(cmdutil.StripComments(edited))
|
||||||
|
if err != nil {
|
||||||
|
results = editResults{
|
||||||
|
file: file,
|
||||||
|
}
|
||||||
|
containsError = true
|
||||||
|
fmt.Fprintln(o.ErrOut, results.addError(apierrors.NewInvalid(corev1.SchemeGroupVersion.WithKind("").GroupKind(),
|
||||||
|
"", field.ErrorList{field.Invalid(nil, "The edited file failed validation", fmt.Sprintf("%v", err))}), infos[0]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare content without comments
|
||||||
|
if bytes.Equal(cmdutil.StripComments(original), cmdutil.StripComments(edited)) {
|
||||||
|
os.Remove(file)
|
||||||
|
fmt.Fprintln(o.ErrOut, "Edit cancelled, no changes made.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lines, err := hasLines(bytes.NewBuffer(edited))
|
||||||
|
if err != nil {
|
||||||
|
return preservedFile(err, file, o.ErrOut)
|
||||||
|
}
|
||||||
|
if !lines {
|
||||||
|
os.Remove(file)
|
||||||
|
fmt.Fprintln(o.ErrOut, "Edit cancelled, saved file was empty.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
results = editResults{
|
||||||
|
file: file,
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the edited file
|
||||||
|
updatedInfos, err := o.updatedResultGetter(edited).Infos()
|
||||||
|
if err != nil {
|
||||||
|
// syntax error
|
||||||
|
containsError = true
|
||||||
|
results.header.reasons = append(results.header.reasons, editReason{head: fmt.Sprintf("The edited file had a syntax error: %v", err)})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// not a syntax error as it turns out...
|
||||||
|
containsError = false
|
||||||
|
updatedVisitor := resource.InfoListVisitor(updatedInfos)
|
||||||
|
|
||||||
|
// we need to add back managedFields to both updated and original object
|
||||||
|
if err := o.restoreManagedFields(updatedInfos); err != nil {
|
||||||
|
return preservedFile(err, file, o.ErrOut)
|
||||||
|
}
|
||||||
|
if err := o.restoreManagedFields(infos); err != nil {
|
||||||
|
return preservedFile(err, file, o.ErrOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to make sure the original namespace wasn't changed while editing
|
||||||
|
if err := updatedVisitor.Visit(resource.RequireNamespace(o.CmdNamespace)); err != nil {
|
||||||
|
return preservedFile(err, file, o.ErrOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate through all items to apply annotations
|
||||||
|
if err := o.visitAnnotation(updatedVisitor); err != nil {
|
||||||
|
return preservedFile(err, file, o.ErrOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.EditMode {
|
||||||
|
case NormalEditMode:
|
||||||
|
err = o.visitToPatch(infos, updatedVisitor, &results)
|
||||||
|
case ApplyEditMode:
|
||||||
|
err = o.visitToApplyEditPatch(infos, updatedVisitor)
|
||||||
|
case EditBeforeCreateMode:
|
||||||
|
err = o.visitToCreate(updatedVisitor)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported edit mode %q", o.EditMode)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return preservedFile(err, results.file, o.ErrOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all possible errors
|
||||||
|
//
|
||||||
|
// 1. retryable: propose kubectl replace -f
|
||||||
|
// 2. notfound: indicate the location of the saved configuration of the deleted resource
|
||||||
|
// 3. invalid: retry those on the spot by looping ie. reloading the editor
|
||||||
|
if results.retryable > 0 {
|
||||||
|
fmt.Fprintf(o.ErrOut, "You can run `%s replace -f %s` to try this update again.\n", filepath.Base(os.Args[0]), file)
|
||||||
|
return cmdutil.ErrExit
|
||||||
|
}
|
||||||
|
if results.notfound > 0 {
|
||||||
|
fmt.Fprintf(o.ErrOut, "The edits you made on deleted resources have been saved to %q\n", file)
|
||||||
|
return cmdutil.ErrExit
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results.edit) == 0 {
|
||||||
|
if results.notfound == 0 {
|
||||||
|
os.Remove(file)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(o.Out, "The edits you made on deleted resources have been saved to %q\n", file)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results.header.reasons) > 0 {
|
||||||
|
containsError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.EditMode {
|
||||||
|
// If doing normal edit we cannot use Visit because we need to edit a list for convenience. Ref: #20519
|
||||||
|
case NormalEditMode:
|
||||||
|
infos, err := o.OriginalResult.Infos()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(infos) == 0 {
|
||||||
|
return errors.New("edit cancelled, no objects found")
|
||||||
|
}
|
||||||
|
return editFn(infos)
|
||||||
|
case ApplyEditMode:
|
||||||
|
infos, err := o.OriginalResult.Infos()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var annotationInfos []*resource.Info
|
||||||
|
for i := range infos {
|
||||||
|
data, err := util.GetOriginalConfiguration(infos[i].Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tempInfos, err := o.updatedResultGetter(data).Infos()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
annotationInfos = append(annotationInfos, tempInfos[0])
|
||||||
|
}
|
||||||
|
if len(annotationInfos) == 0 {
|
||||||
|
return errors.New("no last-applied-configuration annotation found on resources, to create the annotation, use command `kubectl apply set-last-applied --create-annotation`")
|
||||||
|
}
|
||||||
|
return editFn(annotationInfos)
|
||||||
|
// If doing an edit before created, we don't want a list and instead want the normal behavior as kubectl create.
|
||||||
|
case EditBeforeCreateMode:
|
||||||
|
return o.OriginalResult.Visit(func(info *resource.Info, err error) error {
|
||||||
|
return editFn([]*resource.Info{info})
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported edit mode %q", o.EditMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *EditOptions) extractManagedFields(obj runtime.Object) error {
|
||||||
|
o.managedFields = make(map[types.UID][]metav1.ManagedFieldsEntry)
|
||||||
|
if meta.IsListType(obj) {
|
||||||
|
err := meta.EachListItem(obj, func(obj runtime.Object) error {
|
||||||
|
uid, mf, err := clearManagedFields(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.managedFields[uid] = mf
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
uid, mf, err := clearManagedFields(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.managedFields[uid] = mf
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearManagedFields(obj runtime.Object) (types.UID, []metav1.ManagedFieldsEntry, error) {
|
||||||
|
metaObjs, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
mf := metaObjs.GetManagedFields()
|
||||||
|
metaObjs.SetManagedFields(nil)
|
||||||
|
return metaObjs.GetUID(), mf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *EditOptions) restoreManagedFields(infos []*resource.Info) error {
|
||||||
|
for _, info := range infos {
|
||||||
|
metaObjs, err := meta.Accessor(info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mf := o.managedFields[metaObjs.GetUID()]
|
||||||
|
metaObjs.SetManagedFields(mf)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *EditOptions) visitToApplyEditPatch(originalInfos []*resource.Info, patchVisitor resource.Visitor) error {
|
||||||
|
err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error {
|
||||||
|
editObjUID, err := meta.NewAccessor().UID(info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalInfo *resource.Info
|
||||||
|
for _, i := range originalInfos {
|
||||||
|
originalObjUID, err := meta.NewAccessor().UID(i.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if editObjUID == originalObjUID {
|
||||||
|
originalInfo = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if originalInfo == nil {
|
||||||
|
return fmt.Errorf("no original object found for %#v", info.Object)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalJS, err := encodeToJSON(originalInfo.Object.(runtime.Unstructured))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
editedJS, err := encodeToJSON(info.Object.(runtime.Unstructured))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.DeepEqual(originalJS, editedJS) {
|
||||||
|
printer, err := o.ToPrinter("skipped")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printer.PrintObj(info.Object, o.Out)
|
||||||
|
}
|
||||||
|
err = o.annotationPatch(info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
printer, err := o.ToPrinter("edited")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printer.PrintObj(info.Object, o.Out)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *EditOptions) annotationPatch(update *resource.Info) error {
|
||||||
|
patch, _, patchType, err := GetApplyPatch(update.Object.(runtime.Unstructured))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mapping := update.ResourceMapping()
|
||||||
|
client, err := o.f.UnstructuredClientForMapping(mapping)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
helper := resource.NewHelper(client, mapping).
|
||||||
|
WithFieldManager(o.FieldManager).
|
||||||
|
WithFieldValidation(o.ValidationDirective).
|
||||||
|
WithSubresource(o.Subresource)
|
||||||
|
_, err = helper.Patch(o.CmdNamespace, update.Name, patchType, patch, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApplyPatch is used to get and apply patches
|
||||||
|
func GetApplyPatch(obj runtime.Unstructured) ([]byte, []byte, types.PatchType, error) {
|
||||||
|
beforeJSON, err := encodeToJSON(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, []byte(""), types.MergePatchType, err
|
||||||
|
}
|
||||||
|
objCopy := obj.DeepCopyObject()
|
||||||
|
accessor := meta.NewAccessor()
|
||||||
|
annotations, err := accessor.Annotations(objCopy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, beforeJSON, types.MergePatchType, err
|
||||||
|
}
|
||||||
|
if annotations == nil {
|
||||||
|
annotations = map[string]string{}
|
||||||
|
}
|
||||||
|
annotations[corev1.LastAppliedConfigAnnotation] = string(beforeJSON)
|
||||||
|
accessor.SetAnnotations(objCopy, annotations)
|
||||||
|
afterJSON, err := encodeToJSON(objCopy.(runtime.Unstructured))
|
||||||
|
if err != nil {
|
||||||
|
return nil, beforeJSON, types.MergePatchType, err
|
||||||
|
}
|
||||||
|
patch, err := jsonpatch.CreateMergePatch(beforeJSON, afterJSON)
|
||||||
|
return patch, beforeJSON, types.MergePatchType, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeToJSON(obj runtime.Unstructured) ([]byte, error) {
|
||||||
|
serialization, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
js, err := yaml.ToJSON(serialization)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return js, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *EditOptions) visitToPatch(originalInfos []*resource.Info, patchVisitor resource.Visitor, results *editResults) error {
|
||||||
|
err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error {
|
||||||
|
editObjUID, err := meta.NewAccessor().UID(info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalInfo *resource.Info
|
||||||
|
for _, i := range originalInfos {
|
||||||
|
originalObjUID, err := meta.NewAccessor().UID(i.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if editObjUID == originalObjUID {
|
||||||
|
originalInfo = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if originalInfo == nil {
|
||||||
|
return fmt.Errorf("no original object found for %#v", info.Object)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalJS, err := encodeToJSON(originalInfo.Object.(runtime.Unstructured))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
editedJS, err := encodeToJSON(info.Object.(runtime.Unstructured))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.DeepEqual(originalJS, editedJS) {
|
||||||
|
// no edit, so just skip it.
|
||||||
|
printer, err := o.ToPrinter("skipped")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printer.PrintObj(info.Object, o.Out)
|
||||||
|
}
|
||||||
|
|
||||||
|
preconditions := []mergepatch.PreconditionFunc{
|
||||||
|
mergepatch.RequireKeyUnchanged("apiVersion"),
|
||||||
|
mergepatch.RequireKeyUnchanged("kind"),
|
||||||
|
mergepatch.RequireMetadataKeyUnchanged("name"),
|
||||||
|
mergepatch.RequireKeyUnchanged("managedFields"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the versioned struct from the type defined in the mapping
|
||||||
|
// (which is the API version we'll be submitting the patch to)
|
||||||
|
versionedObject, err := scheme.Scheme.New(info.Mapping.GroupVersionKind)
|
||||||
|
var patchType types.PatchType
|
||||||
|
var patch []byte
|
||||||
|
switch {
|
||||||
|
case runtime.IsNotRegisteredError(err):
|
||||||
|
// fall back to generic JSON merge patch
|
||||||
|
patchType = types.MergePatchType
|
||||||
|
patch, err = jsonpatch.CreateMergePatch(originalJS, editedJS)
|
||||||
|
if err != nil {
|
||||||
|
klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var patchMap map[string]interface{}
|
||||||
|
err = json.Unmarshal(patch, &patchMap)
|
||||||
|
if err != nil {
|
||||||
|
klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, precondition := range preconditions {
|
||||||
|
if !precondition(patchMap) {
|
||||||
|
klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
|
||||||
|
return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
patchType = types.StrategicMergePatchType
|
||||||
|
patch, err = strategicpatch.CreateTwoWayMergePatch(originalJS, editedJS, versionedObject, preconditions...)
|
||||||
|
if err != nil {
|
||||||
|
klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err)
|
||||||
|
if mergepatch.IsPreconditionFailed(err) {
|
||||||
|
return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.OutputPatch {
|
||||||
|
fmt.Fprintf(o.Out, "Patch: %s\n", string(patch))
|
||||||
|
}
|
||||||
|
|
||||||
|
patched, err := resource.NewHelper(info.Client, info.Mapping).
|
||||||
|
WithFieldManager(o.FieldManager).
|
||||||
|
WithFieldValidation(o.ValidationDirective).
|
||||||
|
WithSubresource(o.Subresource).
|
||||||
|
Patch(info.Namespace, info.Name, patchType, patch, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(o.ErrOut, results.addError(err, info))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
info.Refresh(patched, true)
|
||||||
|
printer, err := o.ToPrinter("edited")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printer.PrintObj(info.Object, o.Out)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *EditOptions) visitToCreate(createVisitor resource.Visitor) error {
|
||||||
|
err := createVisitor.Visit(func(info *resource.Info, incomingErr error) error {
|
||||||
|
obj, err := resource.NewHelper(info.Client, info.Mapping).
|
||||||
|
WithFieldManager(o.FieldManager).
|
||||||
|
WithFieldValidation(o.ValidationDirective).
|
||||||
|
Create(info.Namespace, true, info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info.Refresh(obj, true)
|
||||||
|
printer, err := o.ToPrinter("created")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printer.PrintObj(info.Object, o.Out)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *EditOptions) visitAnnotation(annotationVisitor resource.Visitor) error {
|
||||||
|
// iterate through all items to apply annotations
|
||||||
|
err := annotationVisitor.Visit(func(info *resource.Info, incomingErr error) error {
|
||||||
|
// put configuration annotation in "updates"
|
||||||
|
if o.ApplyAnnotation {
|
||||||
|
if err := util.CreateOrUpdateAnnotation(true, info.Object, scheme.DefaultJSONEncoder()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := o.Recorder.Record(info.Object); err != nil {
|
||||||
|
klog.V(4).Infof("error recording current command: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditMode can be either NormalEditMode, EditBeforeCreateMode or ApplyEditMode
|
||||||
|
type EditMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NormalEditMode is an edit mode
|
||||||
|
NormalEditMode EditMode = "normal_mode"
|
||||||
|
|
||||||
|
// EditBeforeCreateMode is an edit mode
|
||||||
|
EditBeforeCreateMode EditMode = "edit_before_create_mode"
|
||||||
|
|
||||||
|
// ApplyEditMode is an edit mode
|
||||||
|
ApplyEditMode EditMode = "edit_last_applied_mode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// editReason preserves a message about the reason this file must be edited again
|
||||||
|
type editReason struct {
|
||||||
|
head string
|
||||||
|
other []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// editHeader includes a list of reasons the edit must be retried
|
||||||
|
type editHeader struct {
|
||||||
|
reasons []editReason
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeTo outputs the current header information into a stream
|
||||||
|
func (h *editHeader) writeTo(w io.Writer, editMode EditMode) error {
|
||||||
|
if editMode == ApplyEditMode {
|
||||||
|
fmt.Fprint(w, `# Please edit the 'last-applied-configuration' annotations below.
|
||||||
|
# Lines beginning with a '#' will be ignored, and an empty file will abort the edit.
|
||||||
|
#
|
||||||
|
`)
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored,
|
||||||
|
# and an empty file will abort the edit. If an error occurs while saving this file will be
|
||||||
|
# reopened with the relevant failures.
|
||||||
|
#
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range h.reasons {
|
||||||
|
if len(r.other) > 0 {
|
||||||
|
fmt.Fprintf(w, "# %s:\n", hashOnLineBreak(r.head))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "# %s\n", hashOnLineBreak(r.head))
|
||||||
|
}
|
||||||
|
for _, o := range r.other {
|
||||||
|
fmt.Fprintf(w, "# * %s\n", hashOnLineBreak(o))
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, "#")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// editResults capture the result of an update
|
||||||
|
type editResults struct {
|
||||||
|
header editHeader
|
||||||
|
retryable int
|
||||||
|
notfound int
|
||||||
|
edit []*resource.Info
|
||||||
|
file string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *editResults) addError(err error, info *resource.Info) string {
|
||||||
|
resourceString := info.Mapping.Resource.Resource
|
||||||
|
if len(info.Mapping.Resource.Group) > 0 {
|
||||||
|
resourceString = resourceString + "." + info.Mapping.Resource.Group
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case apierrors.IsInvalid(err):
|
||||||
|
r.edit = append(r.edit, info)
|
||||||
|
reason := editReason{
|
||||||
|
head: fmt.Sprintf("%s %q was not valid", resourceString, info.Name),
|
||||||
|
}
|
||||||
|
if err, ok := err.(apierrors.APIStatus); ok {
|
||||||
|
if details := err.Status().Details; details != nil {
|
||||||
|
for _, cause := range details.Causes {
|
||||||
|
reason.other = append(reason.other, fmt.Sprintf("%s: %s", cause.Field, cause.Message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.header.reasons = append(r.header.reasons, reason)
|
||||||
|
return fmt.Sprintf("error: %s %q is invalid", resourceString, info.Name)
|
||||||
|
case apierrors.IsNotFound(err):
|
||||||
|
r.notfound++
|
||||||
|
return fmt.Sprintf("error: %s %q could not be found on the server", resourceString, info.Name)
|
||||||
|
default:
|
||||||
|
r.retryable++
|
||||||
|
return fmt.Sprintf("error: %s %q could not be patched: %v", resourceString, info.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// preservedFile writes out a message about the provided file if it exists to the
|
||||||
|
// provided output stream when an error happens. Used to notify the user where
|
||||||
|
// their updates were preserved.
|
||||||
|
func preservedFile(err error, path string, out io.Writer) error {
|
||||||
|
if len(path) > 0 {
|
||||||
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||||
|
fmt.Fprintf(out, "A copy of your changes has been stored to %q\n", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasLines returns true if any line in the provided stream is non empty - has non-whitespace
|
||||||
|
// characters, or the first non-whitespace character is a '#' indicating a comment. Returns
|
||||||
|
// any errors encountered reading the stream.
|
||||||
|
func hasLines(r io.Reader) (bool, error) {
|
||||||
|
// TODO: if any files we read have > 64KB lines, we'll need to switch to bytes.ReadLine
|
||||||
|
// TODO: probably going to be secrets
|
||||||
|
s := bufio.NewScanner(r)
|
||||||
|
for s.Scan() {
|
||||||
|
if line := strings.TrimSpace(s.Text()); len(line) > 0 && line[0] != '#' {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil && err != io.EOF {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashOnLineBreak returns a string built from the provided string by inserting any necessary '#'
|
||||||
|
// characters after '\n' characters, indicating a comment.
|
||||||
|
func hashOnLineBreak(s string) string {
|
||||||
|
r := ""
|
||||||
|
for i, ch := range s {
|
||||||
|
j := i + 1
|
||||||
|
if j < len(s) && ch == '\n' && s[j] != '#' {
|
||||||
|
r += "\n# "
|
||||||
|
} else {
|
||||||
|
r += string(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorEnvs returns an ordered list of env vars to check for editor preferences.
|
||||||
|
func editorEnvs() []string {
|
||||||
|
return []string{
|
||||||
|
"KUBE_EDITOR",
|
||||||
|
"EDITOR",
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 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 editor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
|
"k8s.io/kubectl/pkg/util/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// sorry, blame Git
|
||||||
|
// TODO: on Windows rely on 'start' to launch the editor associated
|
||||||
|
// with the given file type. If we can't because of the need of
|
||||||
|
// blocking, use a script with 'ftype' and 'assoc' to detect it.
|
||||||
|
defaultEditor = "vi"
|
||||||
|
defaultShell = "/bin/bash"
|
||||||
|
windowsEditor = "notepad"
|
||||||
|
windowsShell = "cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Editor holds the command-line args to fire up the editor
|
||||||
|
type Editor struct {
|
||||||
|
Args []string
|
||||||
|
Shell bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultEditor creates a struct Editor that uses the OS environment to
|
||||||
|
// locate the editor program, looking at EDITOR environment variable to find
|
||||||
|
// the proper command line. If the provided editor has no spaces, or no quotes,
|
||||||
|
// it is treated as a bare command to be loaded. Otherwise, the string will
|
||||||
|
// be passed to the user's shell for execution.
|
||||||
|
func NewDefaultEditor(envs []string) Editor {
|
||||||
|
args, shell := defaultEnvEditor(envs)
|
||||||
|
return Editor{
|
||||||
|
Args: args,
|
||||||
|
Shell: shell,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultEnvShell() []string {
|
||||||
|
shell := os.Getenv("SHELL")
|
||||||
|
if len(shell) == 0 {
|
||||||
|
shell = platformize(defaultShell, windowsShell)
|
||||||
|
}
|
||||||
|
flag := "-c"
|
||||||
|
if shell == windowsShell {
|
||||||
|
flag = "/C"
|
||||||
|
}
|
||||||
|
return []string{shell, flag}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultEnvEditor(envs []string) ([]string, bool) {
|
||||||
|
var editor string
|
||||||
|
for _, env := range envs {
|
||||||
|
if len(env) > 0 {
|
||||||
|
editor = os.Getenv(env)
|
||||||
|
}
|
||||||
|
if len(editor) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(editor) == 0 {
|
||||||
|
editor = platformize(defaultEditor, windowsEditor)
|
||||||
|
}
|
||||||
|
if !strings.Contains(editor, " ") {
|
||||||
|
return []string{editor}, false
|
||||||
|
}
|
||||||
|
if !strings.ContainsAny(editor, "\"'\\") {
|
||||||
|
return strings.Split(editor, " "), false
|
||||||
|
}
|
||||||
|
// rather than parse the shell arguments ourselves, punt to the shell
|
||||||
|
shell := defaultEnvShell()
|
||||||
|
return append(shell, editor), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Editor) args(path string) []string {
|
||||||
|
args := make([]string, len(e.Args))
|
||||||
|
copy(args, e.Args)
|
||||||
|
if e.Shell {
|
||||||
|
last := args[len(args)-1]
|
||||||
|
args[len(args)-1] = fmt.Sprintf("%s %q", last, path)
|
||||||
|
} else {
|
||||||
|
args = append(args, path)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch opens the described or returns an error. The TTY will be protected, and
|
||||||
|
// SIGQUIT, SIGTERM, and SIGINT will all be trapped.
|
||||||
|
func (e Editor) Launch(path string) error {
|
||||||
|
if len(e.Args) == 0 {
|
||||||
|
return fmt.Errorf("no editor defined, can't open %s", path)
|
||||||
|
}
|
||||||
|
abs, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
args := e.args(abs)
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
klog.V(5).Infof("Opening file with editor %v", args)
|
||||||
|
if err := (term.TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run); err != nil {
|
||||||
|
if err, ok := err.(*exec.Error); ok {
|
||||||
|
if err.Err == exec.ErrNotFound {
|
||||||
|
return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("there was a problem with the editor %q", strings.Join(e.Args, " "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LaunchTempFile reads the provided stream into a temporary file in the given directory
|
||||||
|
// and file prefix, and then invokes Launch with the path of that file. It will return
|
||||||
|
// the contents of the file after launch, any errors that occur, and the path of the
|
||||||
|
// temporary file so the caller can clean it up as needed.
|
||||||
|
func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) ([]byte, string, error) {
|
||||||
|
f, err := os.CreateTemp("", prefix+"*"+suffix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
path := f.Name()
|
||||||
|
if _, err := io.Copy(f, r); err != nil {
|
||||||
|
os.Remove(path)
|
||||||
|
return nil, path, err
|
||||||
|
}
|
||||||
|
// This file descriptor needs to close so the next process (Launch) can claim it.
|
||||||
|
f.Close()
|
||||||
|
if err := e.Launch(path); err != nil {
|
||||||
|
return nil, path, err
|
||||||
|
}
|
||||||
|
bytes, err := ioutil.ReadFile(path)
|
||||||
|
return bytes, path, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func platformize(linux, windows string) string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return windows
|
||||||
|
}
|
||||||
|
return linux
|
||||||
|
}
|
|
@ -0,0 +1,631 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 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 wait
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
watchtools "k8s.io/client-go/tools/watch"
|
||||||
|
"k8s.io/client-go/util/jsonpath"
|
||||||
|
cmdget "k8s.io/kubectl/pkg/cmd/get"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
waitLong = templates.LongDesc(i18n.T(`
|
||||||
|
Experimental: Wait for a specific condition on one or many resources.
|
||||||
|
|
||||||
|
The command takes multiple resources and waits until the specified condition
|
||||||
|
is seen in the Status field of every given resource.
|
||||||
|
|
||||||
|
Alternatively, the command can wait for the given set of resources to be deleted
|
||||||
|
by providing the "delete" keyword as the value to the --for flag.
|
||||||
|
|
||||||
|
A successful message will be printed to stdout indicating when the specified
|
||||||
|
condition has been met. You can use -o option to change to output destination.`))
|
||||||
|
|
||||||
|
waitExample = templates.Examples(i18n.T(`
|
||||||
|
# Wait for the pod "busybox1" to contain the status condition of type "Ready"
|
||||||
|
kubectl wait --for=condition=Ready pod/busybox1
|
||||||
|
|
||||||
|
# The default value of status condition is true; you can wait for other targets after an equal delimiter (compared after Unicode simple case folding, which is a more general form of case-insensitivity):
|
||||||
|
kubectl wait --for=condition=Ready=false pod/busybox1
|
||||||
|
|
||||||
|
# Wait for the pod "busybox1" to contain the status phase to be "Running".
|
||||||
|
kubectl wait --for=jsonpath='{.status.phase}'=Running pod/busybox1
|
||||||
|
|
||||||
|
# Wait for the pod "busybox1" to be deleted, with a timeout of 60s, after having issued the "delete" command
|
||||||
|
kubectl delete pod/busybox1
|
||||||
|
kubectl wait --for=delete pod/busybox1 --timeout=60s`))
|
||||||
|
)
|
||||||
|
|
||||||
|
// errNoMatchingResources is returned when there is no resources matching a query.
|
||||||
|
var errNoMatchingResources = errors.New("no matching resources found")
|
||||||
|
|
||||||
|
// WaitFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which
|
||||||
|
// reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes
|
||||||
|
// the logic itself easy to unit test
|
||||||
|
type WaitFlags struct {
|
||||||
|
RESTClientGetter genericclioptions.RESTClientGetter
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
ResourceBuilderFlags *genericclioptions.ResourceBuilderFlags
|
||||||
|
|
||||||
|
Timeout time.Duration
|
||||||
|
ForCondition string
|
||||||
|
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWaitFlags returns a default WaitFlags
|
||||||
|
func NewWaitFlags(restClientGetter genericclioptions.RESTClientGetter, streams genericclioptions.IOStreams) *WaitFlags {
|
||||||
|
return &WaitFlags{
|
||||||
|
RESTClientGetter: restClientGetter,
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("condition met"),
|
||||||
|
ResourceBuilderFlags: genericclioptions.NewResourceBuilderFlags().
|
||||||
|
WithLabelSelector("").
|
||||||
|
WithFieldSelector("").
|
||||||
|
WithAll(false).
|
||||||
|
WithAllNamespaces(false).
|
||||||
|
WithLocal(false).
|
||||||
|
WithLatest(),
|
||||||
|
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
|
||||||
|
IOStreams: streams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdWait returns a cobra command for waiting
|
||||||
|
func NewCmdWait(restClientGetter genericclioptions.RESTClientGetter, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
flags := NewWaitFlags(restClientGetter, streams)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "wait ([-f FILENAME] | resource.group/resource.name | resource.group [(-l label | --all)]) [--for=delete|--for condition=available|--for=jsonpath='{}'=value]",
|
||||||
|
Short: i18n.T("Experimental: Wait for a specific condition on one or many resources"),
|
||||||
|
Long: waitLong,
|
||||||
|
Example: waitExample,
|
||||||
|
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
o, err := flags.ToOptions(args)
|
||||||
|
cmdutil.CheckErr(err)
|
||||||
|
cmdutil.CheckErr(o.RunWait())
|
||||||
|
},
|
||||||
|
SuggestFor: []string{"list", "ps"},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags.AddFlags(cmd)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlags registers flags for a cli
|
||||||
|
func (flags *WaitFlags) AddFlags(cmd *cobra.Command) {
|
||||||
|
flags.PrintFlags.AddFlags(cmd)
|
||||||
|
flags.ResourceBuilderFlags.AddFlags(cmd.Flags())
|
||||||
|
|
||||||
|
cmd.Flags().DurationVar(&flags.Timeout, "timeout", flags.Timeout, "The length of time to wait before giving up. Zero means check once and don't wait, negative means wait for a week.")
|
||||||
|
cmd.Flags().StringVar(&flags.ForCondition, "for", flags.ForCondition, "The condition to wait on: [delete|condition=condition-name[=condition-value]|jsonpath='{JSONPath expression}'=JSONPath Condition]. The default condition-value is true. Condition values are compared after Unicode simple case folding, which is a more general form of case-insensitivity.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToOptions converts from CLI inputs to runtime inputs
|
||||||
|
func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) {
|
||||||
|
printer, err := flags.PrintFlags.ToPrinter()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
builder := flags.ResourceBuilderFlags.ToBuilder(flags.RESTClientGetter, args)
|
||||||
|
clientConfig, err := flags.RESTClientGetter.ToRESTConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dynamicClient, err := dynamic.NewForConfig(clientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conditionFn, err := conditionFuncFor(flags.ForCondition, flags.ErrOut)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveTimeout := flags.Timeout
|
||||||
|
if effectiveTimeout < 0 {
|
||||||
|
effectiveTimeout = 168 * time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
o := &WaitOptions{
|
||||||
|
ResourceFinder: builder,
|
||||||
|
DynamicClient: dynamicClient,
|
||||||
|
Timeout: effectiveTimeout,
|
||||||
|
ForCondition: flags.ForCondition,
|
||||||
|
|
||||||
|
Printer: printer,
|
||||||
|
ConditionFn: conditionFn,
|
||||||
|
IOStreams: flags.IOStreams,
|
||||||
|
}
|
||||||
|
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error) {
|
||||||
|
if strings.ToLower(condition) == "delete" {
|
||||||
|
return IsDeleted, nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(condition, "condition=") {
|
||||||
|
conditionName := condition[len("condition="):]
|
||||||
|
conditionValue := "true"
|
||||||
|
if equalsIndex := strings.Index(conditionName, "="); equalsIndex != -1 {
|
||||||
|
conditionValue = conditionName[equalsIndex+1:]
|
||||||
|
conditionName = conditionName[0:equalsIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConditionalWait{
|
||||||
|
conditionName: conditionName,
|
||||||
|
conditionStatus: conditionValue,
|
||||||
|
errOut: errOut,
|
||||||
|
}.IsConditionMet, nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(condition, "jsonpath=") {
|
||||||
|
splitStr := strings.Split(condition, "=")
|
||||||
|
if len(splitStr) != 3 {
|
||||||
|
return nil, fmt.Errorf("jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3")
|
||||||
|
}
|
||||||
|
jsonPathExp, jsonPathCond, err := processJSONPathInput(splitStr[1], splitStr[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
j, err := newJSONPathParser(jsonPathExp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return JSONPathWait{
|
||||||
|
jsonPathCondition: jsonPathCond,
|
||||||
|
jsonPathParser: j,
|
||||||
|
errOut: errOut,
|
||||||
|
}.IsJSONPathConditionMet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unrecognized condition: %q", condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newJSONPathParser will create a new JSONPath parser based on the jsonPathExpression
|
||||||
|
func newJSONPathParser(jsonPathExpression string) (*jsonpath.JSONPath, error) {
|
||||||
|
j := jsonpath.New("wait")
|
||||||
|
if jsonPathExpression == "" {
|
||||||
|
return nil, errors.New("jsonpath expression cannot be empty")
|
||||||
|
}
|
||||||
|
if err := j.Parse(jsonPathExpression); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return j, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processJSONPathInput will parses the user's JSONPath input and process the string
|
||||||
|
func processJSONPathInput(jsonPathExpression, jsonPathCond string) (string, string, error) {
|
||||||
|
relaxedJSONPathExp, err := cmdget.RelaxedJSONPathExpression(jsonPathExpression)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if jsonPathCond == "" {
|
||||||
|
return "", "", errors.New("jsonpath wait condition cannot be empty")
|
||||||
|
}
|
||||||
|
jsonPathCond = strings.Trim(jsonPathCond, `'"`)
|
||||||
|
|
||||||
|
return relaxedJSONPathExp, jsonPathCond, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceLocation holds the location of a resource
|
||||||
|
type ResourceLocation struct {
|
||||||
|
GroupResource schema.GroupResource
|
||||||
|
Namespace string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDMap maps ResourceLocation with UID
|
||||||
|
type UIDMap map[ResourceLocation]types.UID
|
||||||
|
|
||||||
|
// WaitOptions is a set of options that allows you to wait. This is the object reflects the runtime needs of a wait
|
||||||
|
// command, making the logic itself easy to unit test with our existing mocks.
|
||||||
|
type WaitOptions struct {
|
||||||
|
ResourceFinder genericclioptions.ResourceFinder
|
||||||
|
// UIDMap maps a resource location to a UID. It is optional, but ConditionFuncs may choose to use it to make the result
|
||||||
|
// more reliable. For instance, delete can look for UID consistency during delegated calls.
|
||||||
|
UIDMap UIDMap
|
||||||
|
DynamicClient dynamic.Interface
|
||||||
|
Timeout time.Duration
|
||||||
|
ForCondition string
|
||||||
|
|
||||||
|
Printer printers.ResourcePrinter
|
||||||
|
ConditionFn ConditionFunc
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConditionFunc is the interface for providing condition checks
|
||||||
|
type ConditionFunc func(info *resource.Info, o *WaitOptions) (finalObject runtime.Object, done bool, err error)
|
||||||
|
|
||||||
|
// RunWait runs the waiting logic
|
||||||
|
func (o *WaitOptions) RunWait() error {
|
||||||
|
visitCount := 0
|
||||||
|
visitFunc := func(info *resource.Info, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
visitCount++
|
||||||
|
finalObject, success, err := o.ConditionFn(info, o)
|
||||||
|
if success {
|
||||||
|
o.Printer.PrintObj(finalObject, o.Out)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("%v unsatisified for unknown reason", finalObject)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
visitor := o.ResourceFinder.Do()
|
||||||
|
isForDelete := strings.ToLower(o.ForCondition) == "delete"
|
||||||
|
if visitor, ok := visitor.(*resource.Result); ok && isForDelete {
|
||||||
|
visitor.IgnoreErrors(apierrors.IsNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := visitor.Visit(visitFunc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if visitCount == 0 && !isForDelete {
|
||||||
|
return errNoMatchingResources
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDeleted is a condition func for waiting for something to be deleted
|
||||||
|
func IsDeleted(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
|
||||||
|
endTime := time.Now().Add(o.Timeout)
|
||||||
|
for {
|
||||||
|
if len(info.Name) == 0 {
|
||||||
|
return info.Object, false, fmt.Errorf("resource name must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
nameSelector := fields.OneTermEqualSelector("metadata.name", info.Name).String()
|
||||||
|
|
||||||
|
// List with a name field selector to get the current resourceVersion to watch from (not the object's resourceVersion)
|
||||||
|
gottenObjList, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).List(context.TODO(), metav1.ListOptions{FieldSelector: nameSelector})
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
return info.Object, true, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// TODO this could do something slightly fancier if we wish
|
||||||
|
return info.Object, false, err
|
||||||
|
}
|
||||||
|
if len(gottenObjList.Items) != 1 {
|
||||||
|
return info.Object, true, nil
|
||||||
|
}
|
||||||
|
gottenObj := &gottenObjList.Items[0]
|
||||||
|
resourceLocation := ResourceLocation{
|
||||||
|
GroupResource: info.Mapping.Resource.GroupResource(),
|
||||||
|
Namespace: gottenObj.GetNamespace(),
|
||||||
|
Name: gottenObj.GetName(),
|
||||||
|
}
|
||||||
|
if uid, ok := o.UIDMap[resourceLocation]; ok {
|
||||||
|
if gottenObj.GetUID() != uid {
|
||||||
|
return gottenObj, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchOptions := metav1.ListOptions{}
|
||||||
|
watchOptions.FieldSelector = nameSelector
|
||||||
|
watchOptions.ResourceVersion = gottenObjList.GetResourceVersion()
|
||||||
|
objWatch, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Watch(context.TODO(), watchOptions)
|
||||||
|
if err != nil {
|
||||||
|
return gottenObj, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := endTime.Sub(time.Now())
|
||||||
|
errWaitTimeoutWithName := extendErrWaitTimeout(wait.ErrWaitTimeout, info)
|
||||||
|
if timeout < 0 {
|
||||||
|
// we're out of time
|
||||||
|
return gottenObj, false, errWaitTimeoutWithName
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout)
|
||||||
|
watchEvent, err := watchtools.UntilWithoutRetry(ctx, objWatch, Wait{errOut: o.ErrOut}.IsDeleted)
|
||||||
|
cancel()
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return watchEvent.Object, true, nil
|
||||||
|
case err == watchtools.ErrWatchClosed:
|
||||||
|
continue
|
||||||
|
case err == wait.ErrWaitTimeout:
|
||||||
|
if watchEvent != nil {
|
||||||
|
return watchEvent.Object, false, errWaitTimeoutWithName
|
||||||
|
}
|
||||||
|
return gottenObj, false, errWaitTimeoutWithName
|
||||||
|
default:
|
||||||
|
return gottenObj, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait has helper methods for handling watches, including error handling.
|
||||||
|
type Wait struct {
|
||||||
|
errOut io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDeleted returns true if the object is deleted. It prints any errors it encounters.
|
||||||
|
func (w Wait) IsDeleted(event watch.Event) (bool, error) {
|
||||||
|
switch event.Type {
|
||||||
|
case watch.Error:
|
||||||
|
// keep waiting in the event we see an error - we expect the watch to be closed by
|
||||||
|
// the server if the error is unrecoverable.
|
||||||
|
err := apierrors.FromObject(event.Object)
|
||||||
|
fmt.Fprintf(w.errOut, "error: An error occurred while waiting for the object to be deleted: %v", err)
|
||||||
|
return false, nil
|
||||||
|
case watch.Deleted:
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type isCondMetFunc func(event watch.Event) (bool, error)
|
||||||
|
type checkCondFunc func(obj *unstructured.Unstructured) (bool, error)
|
||||||
|
|
||||||
|
// getObjAndCheckCondition will make a List query to the API server to get the object and check if the condition is met using check function.
|
||||||
|
// If the condition is not met, it will make a Watch query to the server and pass in the condMet function
|
||||||
|
func getObjAndCheckCondition(info *resource.Info, o *WaitOptions, condMet isCondMetFunc, check checkCondFunc) (runtime.Object, bool, error) {
|
||||||
|
endTime := time.Now().Add(o.Timeout)
|
||||||
|
for {
|
||||||
|
if len(info.Name) == 0 {
|
||||||
|
return info.Object, false, fmt.Errorf("resource name must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
nameSelector := fields.OneTermEqualSelector("metadata.name", info.Name).String()
|
||||||
|
|
||||||
|
var gottenObj *unstructured.Unstructured
|
||||||
|
// List with a name field selector to get the current resourceVersion to watch from (not the object's resourceVersion)
|
||||||
|
gottenObjList, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).List(context.TODO(), metav1.ListOptions{FieldSelector: nameSelector})
|
||||||
|
|
||||||
|
resourceVersion := ""
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return info.Object, false, err
|
||||||
|
case len(gottenObjList.Items) != 1:
|
||||||
|
resourceVersion = gottenObjList.GetResourceVersion()
|
||||||
|
default:
|
||||||
|
gottenObj = &gottenObjList.Items[0]
|
||||||
|
conditionMet, err := check(gottenObj)
|
||||||
|
if conditionMet {
|
||||||
|
return gottenObj, true, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return gottenObj, false, err
|
||||||
|
}
|
||||||
|
resourceVersion = gottenObjList.GetResourceVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
watchOptions := metav1.ListOptions{}
|
||||||
|
watchOptions.FieldSelector = nameSelector
|
||||||
|
watchOptions.ResourceVersion = resourceVersion
|
||||||
|
objWatch, err := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Watch(context.TODO(), watchOptions)
|
||||||
|
if err != nil {
|
||||||
|
return gottenObj, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := endTime.Sub(time.Now())
|
||||||
|
errWaitTimeoutWithName := extendErrWaitTimeout(wait.ErrWaitTimeout, info)
|
||||||
|
if timeout < 0 {
|
||||||
|
// we're out of time
|
||||||
|
return gottenObj, false, errWaitTimeoutWithName
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout)
|
||||||
|
watchEvent, err := watchtools.UntilWithoutRetry(ctx, objWatch, watchtools.ConditionFunc(condMet))
|
||||||
|
cancel()
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return watchEvent.Object, true, nil
|
||||||
|
case err == watchtools.ErrWatchClosed:
|
||||||
|
continue
|
||||||
|
case err == wait.ErrWaitTimeout:
|
||||||
|
if watchEvent != nil {
|
||||||
|
return watchEvent.Object, false, errWaitTimeoutWithName
|
||||||
|
}
|
||||||
|
return gottenObj, false, errWaitTimeoutWithName
|
||||||
|
default:
|
||||||
|
return gottenObj, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConditionalWait hold information to check an API status condition
|
||||||
|
type ConditionalWait struct {
|
||||||
|
conditionName string
|
||||||
|
conditionStatus string
|
||||||
|
// errOut is written to if an error occurs
|
||||||
|
errOut io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConditionMet is a conditionfunc for waiting on an API condition to be met
|
||||||
|
func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
|
||||||
|
return getObjAndCheckCondition(info, o, w.isConditionMet, w.checkCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w ConditionalWait) checkCondition(obj *unstructured.Unstructured) (bool, error) {
|
||||||
|
conditions, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
for _, conditionUncast := range conditions {
|
||||||
|
condition := conditionUncast.(map[string]interface{})
|
||||||
|
name, found, err := unstructured.NestedString(condition, "type")
|
||||||
|
if !found || err != nil || !strings.EqualFold(name, w.conditionName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status, found, err := unstructured.NestedString(condition, "status")
|
||||||
|
if !found || err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
generation, found, _ := unstructured.NestedInt64(obj.Object, "metadata", "generation")
|
||||||
|
if found {
|
||||||
|
observedGeneration, found := getObservedGeneration(obj, condition)
|
||||||
|
if found && observedGeneration < generation {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.EqualFold(status, w.conditionStatus), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w ConditionalWait) isConditionMet(event watch.Event) (bool, error) {
|
||||||
|
if event.Type == watch.Error {
|
||||||
|
// keep waiting in the event we see an error - we expect the watch to be closed by
|
||||||
|
// the server
|
||||||
|
err := apierrors.FromObject(event.Object)
|
||||||
|
fmt.Fprintf(w.errOut, "error: An error occurred while waiting for the condition to be satisfied: %v", err)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if event.Type == watch.Deleted {
|
||||||
|
// this will chain back out, result in another get and an return false back up the chain
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
obj := event.Object.(*unstructured.Unstructured)
|
||||||
|
return w.checkCondition(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extendErrWaitTimeout(err error, info *resource.Info) error {
|
||||||
|
return fmt.Errorf("%s on %s/%s", err.Error(), info.Mapping.Resource.Resource, info.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getObservedGeneration(obj *unstructured.Unstructured, condition map[string]interface{}) (int64, bool) {
|
||||||
|
conditionObservedGeneration, found, _ := unstructured.NestedInt64(condition, "observedGeneration")
|
||||||
|
if found {
|
||||||
|
return conditionObservedGeneration, true
|
||||||
|
}
|
||||||
|
statusObservedGeneration, found, _ := unstructured.NestedInt64(obj.Object, "status", "observedGeneration")
|
||||||
|
return statusObservedGeneration, found
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONPathWait holds a JSONPath Parser which has the ability
|
||||||
|
// to check for the JSONPath condition and compare with the API server provided JSON output.
|
||||||
|
type JSONPathWait struct {
|
||||||
|
jsonPathCondition string
|
||||||
|
jsonPathParser *jsonpath.JSONPath
|
||||||
|
// errOut is written to if an error occurs
|
||||||
|
errOut io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsJSONPathConditionMet fulfills the requirements of the interface ConditionFunc which provides condition check
|
||||||
|
func (j JSONPathWait) IsJSONPathConditionMet(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
|
||||||
|
return getObjAndCheckCondition(info, o, j.isJSONPathConditionMet, j.checkCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isJSONPathConditionMet is a helper function of IsJSONPathConditionMet
|
||||||
|
// which check the watch event and check if a JSONPathWait condition is met
|
||||||
|
func (j JSONPathWait) isJSONPathConditionMet(event watch.Event) (bool, error) {
|
||||||
|
if event.Type == watch.Error {
|
||||||
|
// keep waiting in the event we see an error - we expect the watch to be closed by
|
||||||
|
// the server
|
||||||
|
err := apierrors.FromObject(event.Object)
|
||||||
|
fmt.Fprintf(j.errOut, "error: An error occurred while waiting for the condition to be satisfied: %v", err)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if event.Type == watch.Deleted {
|
||||||
|
// this will chain back out, result in another get and an return false back up the chain
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
// event runtime Object can be safely asserted to Unstructed
|
||||||
|
// because we are working with dynamic client
|
||||||
|
obj := event.Object.(*unstructured.Unstructured)
|
||||||
|
return j.checkCondition(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkCondition uses JSONPath parser to parse the JSON received from the API server
|
||||||
|
// and check if it matches the desired condition
|
||||||
|
func (j JSONPathWait) checkCondition(obj *unstructured.Unstructured) (bool, error) {
|
||||||
|
queryObj := obj.UnstructuredContent()
|
||||||
|
parseResults, err := j.jsonPathParser.FindResults(queryObj)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := verifyParsedJSONPath(parseResults); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
isConditionMet, err := compareResults(parseResults[0][0], j.jsonPathCondition)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return isConditionMet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyParsedJSONPath verifies the JSON received from the API server is valid.
|
||||||
|
// It will only accept a single JSON
|
||||||
|
func verifyParsedJSONPath(results [][]reflect.Value) error {
|
||||||
|
if len(results) == 0 {
|
||||||
|
return errors.New("given jsonpath expression does not match any value")
|
||||||
|
}
|
||||||
|
if len(results) > 1 {
|
||||||
|
return errors.New("given jsonpath expression matches more than one list")
|
||||||
|
}
|
||||||
|
if len(results[0]) > 1 {
|
||||||
|
return errors.New("given jsonpath expression matches more than one value")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareResults will compare the reflect.Value from the result parsed by the
|
||||||
|
// JSONPath parser with the expected value given by the value
|
||||||
|
//
|
||||||
|
// Since this is coming from an unstructured this can only ever be a primitive,
|
||||||
|
// map[string]interface{}, or []interface{}.
|
||||||
|
// We do not support the last two and rely on fmt to handle conversion to string
|
||||||
|
// and compare the result with user input
|
||||||
|
func compareResults(r reflect.Value, expectedVal string) (bool, error) {
|
||||||
|
switch r.Interface().(type) {
|
||||||
|
case map[string]interface{}, []interface{}:
|
||||||
|
return false, errors.New("jsonpath leads to a nested object or list which is not supported")
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("%v", r.Interface())
|
||||||
|
return strings.TrimSpace(s) == strings.TrimSpace(expectedVal), nil
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
Copyright 2014 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var metadataAccessor = meta.NewAccessor()
|
||||||
|
|
||||||
|
// GetOriginalConfiguration retrieves the original configuration of the object
|
||||||
|
// from the annotation, or nil if no annotation was found.
|
||||||
|
func GetOriginalConfiguration(obj runtime.Object) ([]byte, error) {
|
||||||
|
annots, err := metadataAccessor.Annotations(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if annots == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
original, ok := annots[v1.LastAppliedConfigAnnotation]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(original), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOriginalConfiguration sets the original configuration of the object
|
||||||
|
// as the annotation on the object for later use in computing a three way patch.
|
||||||
|
func setOriginalConfiguration(obj runtime.Object, original []byte) error {
|
||||||
|
if len(original) < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
annots, err := metadataAccessor.Annotations(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if annots == nil {
|
||||||
|
annots = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
annots[v1.LastAppliedConfigAnnotation] = string(original)
|
||||||
|
return metadataAccessor.SetAnnotations(obj, annots)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModifiedConfiguration retrieves the modified configuration of the object.
|
||||||
|
// If annotate is true, it embeds the result as an annotation in the modified
|
||||||
|
// configuration. If an object was read from the command input, it will use that
|
||||||
|
// version of the object. Otherwise, it will use the version from the server.
|
||||||
|
func GetModifiedConfiguration(obj runtime.Object, annotate bool, codec runtime.Encoder) ([]byte, error) {
|
||||||
|
// First serialize the object without the annotation to prevent recursion,
|
||||||
|
// then add that serialization to it as the annotation and serialize it again.
|
||||||
|
var modified []byte
|
||||||
|
|
||||||
|
// Otherwise, use the server side version of the object.
|
||||||
|
// Get the current annotations from the object.
|
||||||
|
annots, err := metadataAccessor.Annotations(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if annots == nil {
|
||||||
|
annots = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
original := annots[v1.LastAppliedConfigAnnotation]
|
||||||
|
delete(annots, v1.LastAppliedConfigAnnotation)
|
||||||
|
if err := metadataAccessor.SetAnnotations(obj, annots); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
modified, err = runtime.Encode(codec, obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if annotate {
|
||||||
|
annots[v1.LastAppliedConfigAnnotation] = string(modified)
|
||||||
|
if err := metadataAccessor.SetAnnotations(obj, annots); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
modified, err = runtime.Encode(codec, obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the object to its original condition.
|
||||||
|
annots[v1.LastAppliedConfigAnnotation] = original
|
||||||
|
if err := metadataAccessor.SetAnnotations(obj, annots); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return modified, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateApplyAnnotation calls CreateApplyAnnotation if the last applied
|
||||||
|
// configuration annotation is already present. Otherwise, it does nothing.
|
||||||
|
func updateApplyAnnotation(obj runtime.Object, codec runtime.Encoder) error {
|
||||||
|
if original, err := GetOriginalConfiguration(obj); err != nil || len(original) <= 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return CreateApplyAnnotation(obj, codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateApplyAnnotation gets the modified configuration of the object,
|
||||||
|
// without embedding it again, and then sets it on the object as the annotation.
|
||||||
|
func CreateApplyAnnotation(obj runtime.Object, codec runtime.Encoder) error {
|
||||||
|
modified, err := GetModifiedConfiguration(obj, false, codec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return setOriginalConfiguration(obj, modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdateAnnotation creates the annotation used by
|
||||||
|
// kubectl apply only when createAnnotation is true
|
||||||
|
// Otherwise, only update the annotation when it already exists
|
||||||
|
func CreateOrUpdateAnnotation(createAnnotation bool, obj runtime.Object, codec runtime.Encoder) error {
|
||||||
|
if createAnnotation {
|
||||||
|
return CreateApplyAnnotation(obj, codec)
|
||||||
|
}
|
||||||
|
return updateApplyAnnotation(obj, codec)
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LookupContainerPortNumberByName find containerPort number by its named port name
|
||||||
|
func LookupContainerPortNumberByName(pod v1.Pod, name string) (int32, error) {
|
||||||
|
for _, ctr := range pod.Spec.Containers {
|
||||||
|
for _, ctrportspec := range ctr.Ports {
|
||||||
|
if ctrportspec.Name == name {
|
||||||
|
return ctrportspec.ContainerPort, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return int32(-1), fmt.Errorf("Pod '%s' does not have a named port '%s'", pod.Name, name)
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 prune
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resource struct {
|
||||||
|
group string
|
||||||
|
version string
|
||||||
|
kind string
|
||||||
|
namespaced bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr Resource) String() string {
|
||||||
|
return fmt.Sprintf("%v/%v, Kind=%v, Namespaced=%v", pr.group, pr.version, pr.kind, pr.namespaced)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRESTMappings(mapper meta.RESTMapper, pruneResources []Resource) (namespaced, nonNamespaced []*meta.RESTMapping, err error) {
|
||||||
|
if len(pruneResources) == 0 {
|
||||||
|
// default allowlist
|
||||||
|
pruneResources = []Resource{
|
||||||
|
{"", "v1", "ConfigMap", true},
|
||||||
|
{"", "v1", "Endpoints", true},
|
||||||
|
{"", "v1", "Namespace", false},
|
||||||
|
{"", "v1", "PersistentVolumeClaim", true},
|
||||||
|
{"", "v1", "PersistentVolume", false},
|
||||||
|
{"", "v1", "Pod", true},
|
||||||
|
{"", "v1", "ReplicationController", true},
|
||||||
|
{"", "v1", "Secret", true},
|
||||||
|
{"", "v1", "Service", true},
|
||||||
|
{"batch", "v1", "Job", true},
|
||||||
|
{"batch", "v1", "CronJob", true},
|
||||||
|
{"networking.k8s.io", "v1", "Ingress", true},
|
||||||
|
{"apps", "v1", "DaemonSet", true},
|
||||||
|
{"apps", "v1", "Deployment", true},
|
||||||
|
{"apps", "v1", "ReplicaSet", true},
|
||||||
|
{"apps", "v1", "StatefulSet", true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, resource := range pruneResources {
|
||||||
|
addedMapping, err := mapper.RESTMapping(schema.GroupKind{Group: resource.group, Kind: resource.kind}, resource.version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("invalid resource %v: %v", resource, err)
|
||||||
|
}
|
||||||
|
if resource.namespaced {
|
||||||
|
namespaced = append(namespaced, addedMapping)
|
||||||
|
} else {
|
||||||
|
nonNamespaced = append(nonNamespaced, addedMapping)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespaced, nonNamespaced, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseResources(mapper meta.RESTMapper, gvks []string) ([]Resource, error) {
|
||||||
|
pruneResources := []Resource{}
|
||||||
|
for _, groupVersionKind := range gvks {
|
||||||
|
gvk := strings.Split(groupVersionKind, "/")
|
||||||
|
if len(gvk) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid GroupVersionKind format: %v, please follow <group/version/kind>", groupVersionKind)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gvk[0] == "core" {
|
||||||
|
gvk[0] = ""
|
||||||
|
}
|
||||||
|
mapping, err := mapper.RESTMapping(schema.GroupKind{Group: gvk[0], Kind: gvk[2]}, gvk[1])
|
||||||
|
if err != nil {
|
||||||
|
return pruneResources, err
|
||||||
|
}
|
||||||
|
var namespaced bool
|
||||||
|
namespaceScope := mapping.Scope.Name()
|
||||||
|
switch namespaceScope {
|
||||||
|
case meta.RESTScopeNameNamespace:
|
||||||
|
namespaced = true
|
||||||
|
case meta.RESTScopeNameRoot:
|
||||||
|
namespaced = false
|
||||||
|
default:
|
||||||
|
return pruneResources, fmt.Errorf("Unknown namespace scope: %q", namespaceScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneResources = append(pruneResources, Resource{gvk[0], gvk[1], gvk[2], namespaced})
|
||||||
|
}
|
||||||
|
return pruneResources, nil
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LookupContainerPortNumberByServicePort implements
|
||||||
|
// the handling of resolving container named port, as well as ignoring targetPort when clusterIP=None
|
||||||
|
// It returns an error when a named port can't find a match (with -1 returned), or when the service does not
|
||||||
|
// declare such port (with the input port number returned).
|
||||||
|
func LookupContainerPortNumberByServicePort(svc v1.Service, pod v1.Pod, port int32) (int32, error) {
|
||||||
|
for _, svcportspec := range svc.Spec.Ports {
|
||||||
|
if svcportspec.Port != port {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if svc.Spec.ClusterIP == v1.ClusterIPNone {
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
if svcportspec.TargetPort.Type == intstr.Int {
|
||||||
|
if svcportspec.TargetPort.IntValue() == 0 {
|
||||||
|
// targetPort is omitted, and the IntValue() would be zero
|
||||||
|
return svcportspec.Port, nil
|
||||||
|
}
|
||||||
|
return int32(svcportspec.TargetPort.IntValue()), nil
|
||||||
|
}
|
||||||
|
return LookupContainerPortNumberByName(pod, svcportspec.TargetPort.String())
|
||||||
|
}
|
||||||
|
return port, fmt.Errorf("Service %s does not have a service port %d", svc.Name, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupServicePortNumberByName find service port number by its named port name
|
||||||
|
func LookupServicePortNumberByName(svc v1.Service, name string) (int32, error) {
|
||||||
|
for _, svcportspec := range svc.Spec.Ports {
|
||||||
|
if svcportspec.Name == name {
|
||||||
|
return svcportspec.Port, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return int32(-1), fmt.Errorf("Service '%s' does not have a named port '%s'", svc.Name, name)
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2014 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Umask is a wrapper for `unix.Umask()` on non-Windows platforms
|
||||||
|
func Umask(mask int) (old int, err error) {
|
||||||
|
return unix.Umask(mask), nil
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2014 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Umask returns an error on Windows
|
||||||
|
func Umask(mask int) (int, error) {
|
||||||
|
return 0, errors.New("platform and architecture is not supported")
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseRFC3339 parses an RFC3339 date in either RFC3339Nano or RFC3339 format.
|
||||||
|
func ParseRFC3339(s string, nowFn func() metav1.Time) (metav1.Time, error) {
|
||||||
|
if t, timeErr := time.Parse(time.RFC3339Nano, s); timeErr == nil {
|
||||||
|
return metav1.Time{Time: t}, nil
|
||||||
|
}
|
||||||
|
t, err := time.Parse(time.RFC3339, s)
|
||||||
|
if err != nil {
|
||||||
|
return metav1.Time{}, err
|
||||||
|
}
|
||||||
|
return metav1.Time{Time: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashObject returns the hash of a Object hash by a Codec
|
||||||
|
func HashObject(obj runtime.Object, codec runtime.Codec) (string, error) {
|
||||||
|
data, err := runtime.Encode(codec, obj)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", md5.Sum(data)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFileSource parses the source given.
|
||||||
|
//
|
||||||
|
// Acceptable formats include:
|
||||||
|
// 1. source-path: the basename will become the key name
|
||||||
|
// 2. source-name=source-path: the source-name will become the key name and
|
||||||
|
// source-path is the path to the key file.
|
||||||
|
//
|
||||||
|
// Key names cannot include '='.
|
||||||
|
func ParseFileSource(source string) (keyName, filePath string, err error) {
|
||||||
|
numSeparators := strings.Count(source, "=")
|
||||||
|
switch {
|
||||||
|
case numSeparators == 0:
|
||||||
|
return path.Base(filepath.ToSlash(source)), source, nil
|
||||||
|
case numSeparators == 1 && strings.HasPrefix(source, "="):
|
||||||
|
return "", "", fmt.Errorf("key name for file path %v missing", strings.TrimPrefix(source, "="))
|
||||||
|
case numSeparators == 1 && strings.HasSuffix(source, "="):
|
||||||
|
return "", "", fmt.Errorf("file path for key name %v missing", strings.TrimSuffix(source, "="))
|
||||||
|
case numSeparators > 1:
|
||||||
|
return "", "", errors.New("key names or file paths cannot contain '='")
|
||||||
|
default:
|
||||||
|
components := strings.Split(source, "=")
|
||||||
|
return components[0], components[1], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLiteralSource parses the source key=val pair into its component pieces.
|
||||||
|
// This functionality is distinguished from strings.SplitN(source, "=", 2) since
|
||||||
|
// it returns an error in the case of empty keys, values, or a missing equals sign.
|
||||||
|
func ParseLiteralSource(source string) (keyName, value string, err error) {
|
||||||
|
// leading equal is invalid
|
||||||
|
if strings.Index(source, "=") == 0 {
|
||||||
|
return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source)
|
||||||
|
}
|
||||||
|
// split after the first equal (so values can have the = character)
|
||||||
|
items := strings.SplitN(source, "=", 2)
|
||||||
|
if len(items) != 2 {
|
||||||
|
return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items[0], items[1], nil
|
||||||
|
}
|
|
@ -202,6 +202,9 @@ github.com/imdario/mergo
|
||||||
# github.com/inconshreveable/mousetrap v1.0.0
|
# github.com/inconshreveable/mousetrap v1.0.0
|
||||||
## explicit
|
## explicit
|
||||||
github.com/inconshreveable/mousetrap
|
github.com/inconshreveable/mousetrap
|
||||||
|
# github.com/jonboulle/clockwork v0.2.2
|
||||||
|
## explicit; go 1.13
|
||||||
|
github.com/jonboulle/clockwork
|
||||||
# github.com/josharian/intern v1.0.0
|
# github.com/josharian/intern v1.0.0
|
||||||
## explicit; go 1.5
|
## explicit; go 1.5
|
||||||
github.com/josharian/intern
|
github.com/josharian/intern
|
||||||
|
@ -837,6 +840,7 @@ k8s.io/apimachinery/pkg/util/httpstream
|
||||||
k8s.io/apimachinery/pkg/util/httpstream/spdy
|
k8s.io/apimachinery/pkg/util/httpstream/spdy
|
||||||
k8s.io/apimachinery/pkg/util/intstr
|
k8s.io/apimachinery/pkg/util/intstr
|
||||||
k8s.io/apimachinery/pkg/util/json
|
k8s.io/apimachinery/pkg/util/json
|
||||||
|
k8s.io/apimachinery/pkg/util/jsonmergepatch
|
||||||
k8s.io/apimachinery/pkg/util/managedfields
|
k8s.io/apimachinery/pkg/util/managedfields
|
||||||
k8s.io/apimachinery/pkg/util/mergepatch
|
k8s.io/apimachinery/pkg/util/mergepatch
|
||||||
k8s.io/apimachinery/pkg/util/naming
|
k8s.io/apimachinery/pkg/util/naming
|
||||||
|
@ -1408,14 +1412,20 @@ k8s.io/kube-openapi/pkg/validation/spec
|
||||||
## explicit; go 1.16
|
## explicit; go 1.16
|
||||||
k8s.io/kubectl/pkg/apps
|
k8s.io/kubectl/pkg/apps
|
||||||
k8s.io/kubectl/pkg/cmd/apiresources
|
k8s.io/kubectl/pkg/cmd/apiresources
|
||||||
|
k8s.io/kubectl/pkg/cmd/apply
|
||||||
|
k8s.io/kubectl/pkg/cmd/delete
|
||||||
k8s.io/kubectl/pkg/cmd/exec
|
k8s.io/kubectl/pkg/cmd/exec
|
||||||
k8s.io/kubectl/pkg/cmd/get
|
k8s.io/kubectl/pkg/cmd/get
|
||||||
k8s.io/kubectl/pkg/cmd/util
|
k8s.io/kubectl/pkg/cmd/util
|
||||||
|
k8s.io/kubectl/pkg/cmd/util/editor
|
||||||
|
k8s.io/kubectl/pkg/cmd/util/editor/crlf
|
||||||
k8s.io/kubectl/pkg/cmd/util/podcmd
|
k8s.io/kubectl/pkg/cmd/util/podcmd
|
||||||
|
k8s.io/kubectl/pkg/cmd/wait
|
||||||
k8s.io/kubectl/pkg/describe
|
k8s.io/kubectl/pkg/describe
|
||||||
k8s.io/kubectl/pkg/polymorphichelpers
|
k8s.io/kubectl/pkg/polymorphichelpers
|
||||||
k8s.io/kubectl/pkg/rawhttp
|
k8s.io/kubectl/pkg/rawhttp
|
||||||
k8s.io/kubectl/pkg/scheme
|
k8s.io/kubectl/pkg/scheme
|
||||||
|
k8s.io/kubectl/pkg/util
|
||||||
k8s.io/kubectl/pkg/util/certificate
|
k8s.io/kubectl/pkg/util/certificate
|
||||||
k8s.io/kubectl/pkg/util/completion
|
k8s.io/kubectl/pkg/util/completion
|
||||||
k8s.io/kubectl/pkg/util/deployment
|
k8s.io/kubectl/pkg/util/deployment
|
||||||
|
@ -1426,6 +1436,7 @@ k8s.io/kubectl/pkg/util/interrupt
|
||||||
k8s.io/kubectl/pkg/util/openapi
|
k8s.io/kubectl/pkg/util/openapi
|
||||||
k8s.io/kubectl/pkg/util/openapi/validation
|
k8s.io/kubectl/pkg/util/openapi/validation
|
||||||
k8s.io/kubectl/pkg/util/podutils
|
k8s.io/kubectl/pkg/util/podutils
|
||||||
|
k8s.io/kubectl/pkg/util/prune
|
||||||
k8s.io/kubectl/pkg/util/qos
|
k8s.io/kubectl/pkg/util/qos
|
||||||
k8s.io/kubectl/pkg/util/rbac
|
k8s.io/kubectl/pkg/util/rbac
|
||||||
k8s.io/kubectl/pkg/util/resource
|
k8s.io/kubectl/pkg/util/resource
|
||||||
|
|
Loading…
Reference in New Issue