working commit
This commit is contained in:
+201
@@ -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.
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2025 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 (
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// cachingVerifier wraps the given Verifier to cache return values forever.
|
||||
type cachingVerifier struct {
|
||||
cache map[schema.GroupVersionKind]error
|
||||
mu sync.RWMutex
|
||||
next resource.Verifier
|
||||
}
|
||||
|
||||
// newCachingVerifier creates a new cache using the given underlying verifier.
|
||||
func newCachingVerifier(next resource.Verifier) *cachingVerifier {
|
||||
return &cachingVerifier{
|
||||
cache: make(map[schema.GroupVersionKind]error),
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
// HasSupport implements resource.Verifier. It cached return values from the underlying verifier forever.
|
||||
func (cv *cachingVerifier) HasSupport(gvk schema.GroupVersionKind) error {
|
||||
// Try to get the cached value.
|
||||
cv.mu.RLock()
|
||||
err, ok := cv.cache[gvk]
|
||||
cv.mu.RUnlock()
|
||||
if ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache miss. Get the actual result.
|
||||
err = cv.next.HasSupport(gvk)
|
||||
|
||||
// Update the cache.
|
||||
cv.mu.Lock()
|
||||
cv.cache[gvk] = err
|
||||
cv.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
)
|
||||
|
||||
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
|
||||
|
||||
// processEnvFileLine returns a blank key if the line is empty or a comment.
|
||||
// The value will be retrieved from the environment if necessary.
|
||||
func processEnvFileLine(line []byte, filePath string,
|
||||
currentLine int) (key, value string, err error) {
|
||||
|
||||
if !utf8.Valid(line) {
|
||||
return ``, ``, fmt.Errorf("env file %s contains invalid utf8 bytes at line %d: %v",
|
||||
filePath, currentLine+1, line)
|
||||
}
|
||||
|
||||
// We trim UTF8 BOM from the first line of the file but no others
|
||||
if currentLine == 0 {
|
||||
line = bytes.TrimPrefix(line, utf8bom)
|
||||
}
|
||||
|
||||
// trim the line from all leading whitespace first
|
||||
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
|
||||
|
||||
// If the line is empty or a comment, we return a blank key/value pair.
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
return ``, ``, nil
|
||||
}
|
||||
|
||||
data := strings.SplitN(string(line), "=", 2)
|
||||
key = data[0]
|
||||
if errs := validation.IsEnvVarName(key); len(errs) != 0 {
|
||||
return ``, ``, fmt.Errorf("%q is not a valid key name: %s", key, strings.Join(errs, ";"))
|
||||
}
|
||||
|
||||
if len(data) == 2 {
|
||||
value = data[1]
|
||||
} else {
|
||||
// No value (no `=` in the line) is a signal to obtain the value
|
||||
// from the environment.
|
||||
value = os.Getenv(key)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AddFromEnvFile processes an env file allows a generic addTo to handle the
|
||||
// collection of key value pairs or returns an error.
|
||||
func AddFromEnvFile(filePath string, addTo func(key, value string) error) error {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
currentLine := 0
|
||||
for scanner.Scan() {
|
||||
// Process the current line, retrieving a key/value pair if
|
||||
// possible.
|
||||
scannedBytes := scanner.Bytes()
|
||||
key, value, err := processEnvFileLine(scannedBytes, filePath, currentLine)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentLine++
|
||||
|
||||
if len(key) == 0 {
|
||||
// no key means line was empty or a comment
|
||||
continue
|
||||
}
|
||||
|
||||
if err = addTo(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
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/apimachinery/pkg/api/meta"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
openapiclient "k8s.io/client-go/openapi"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
"k8s.io/kubectl/pkg/validation"
|
||||
)
|
||||
|
||||
// Factory provides abstractions that allow the Kubectl command to be extended across multiple types
|
||||
// of resources and different API sets.
|
||||
// The rings are here for a reason. In order for composers to be able to provide alternative factory implementations
|
||||
// they need to provide low level pieces of *certain* functions so that when the factory calls back into itself
|
||||
// it uses the custom version of the function. Rather than try to enumerate everything that someone would want to override
|
||||
// we split the factory into rings, where each ring can depend on methods in an earlier ring, but cannot depend
|
||||
// upon peer methods in its own ring.
|
||||
// TODO: make the functions interfaces
|
||||
// TODO: pass the various interfaces on the factory directly into the command constructors (so the
|
||||
// commands are decoupled from the factory).
|
||||
type Factory interface {
|
||||
genericclioptions.RESTClientGetter
|
||||
|
||||
// DynamicClient returns a dynamic client ready for use
|
||||
DynamicClient() (dynamic.Interface, error)
|
||||
|
||||
// KubernetesClientSet gives you back an external clientset
|
||||
KubernetesClientSet() (*kubernetes.Clientset, error)
|
||||
|
||||
// Returns a RESTClient for accessing Kubernetes resources or an error.
|
||||
RESTClient() (*restclient.RESTClient, error)
|
||||
|
||||
// NewBuilder returns an object that assists in loading objects from both disk and the server
|
||||
// and which implements the common patterns for CLI interactions with generic resources.
|
||||
NewBuilder() *resource.Builder
|
||||
|
||||
// Returns a RESTClient for working with the specified RESTMapping or an error. This is intended
|
||||
// for working with arbitrary resources and is not guaranteed to point to a Kubernetes APIServer.
|
||||
ClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error)
|
||||
// Returns a RESTClient for working with Unstructured objects.
|
||||
UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error)
|
||||
|
||||
// Returns a schema that can validate objects stored on disk.
|
||||
Validator(validationDirective string) (validation.Schema, error)
|
||||
|
||||
// Used for retrieving openapi v2 resources.
|
||||
openapi.OpenAPIResourcesGetter
|
||||
|
||||
// OpenAPIV3Schema returns a client for fetching parsed schemas for
|
||||
// any group version
|
||||
OpenAPIV3Client() (openapiclient.Client, error)
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
Copyright 2016 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.
|
||||
*/
|
||||
|
||||
// this file contains factories with no other dependencies
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
openapiclient "k8s.io/client-go/openapi"
|
||||
"k8s.io/client-go/openapi/cached"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
"k8s.io/kubectl/pkg/validation"
|
||||
)
|
||||
|
||||
type factoryImpl struct {
|
||||
clientGetter genericclioptions.RESTClientGetter
|
||||
|
||||
// Caches OpenAPI document and parsed resources
|
||||
openAPIParser *openapi.CachedOpenAPIParser
|
||||
oapi *openapi.CachedOpenAPIGetter
|
||||
parser sync.Once
|
||||
getter sync.Once
|
||||
}
|
||||
|
||||
func NewFactory(clientGetter genericclioptions.RESTClientGetter) Factory {
|
||||
if clientGetter == nil {
|
||||
panic("attempt to instantiate client_access_factory with nil clientGetter")
|
||||
}
|
||||
f := &factoryImpl{
|
||||
clientGetter: clientGetter,
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *factoryImpl) ToRESTConfig() (*restclient.Config, error) {
|
||||
return f.clientGetter.ToRESTConfig()
|
||||
}
|
||||
|
||||
func (f *factoryImpl) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
return f.clientGetter.ToRESTMapper()
|
||||
}
|
||||
|
||||
func (f *factoryImpl) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
return f.clientGetter.ToDiscoveryClient()
|
||||
}
|
||||
|
||||
func (f *factoryImpl) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
return f.clientGetter.ToRawKubeConfigLoader()
|
||||
}
|
||||
|
||||
func (f *factoryImpl) KubernetesClientSet() (*kubernetes.Clientset, error) {
|
||||
clientConfig, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kubernetes.NewForConfig(clientConfig)
|
||||
}
|
||||
|
||||
func (f *factoryImpl) DynamicClient() (dynamic.Interface, error) {
|
||||
clientConfig, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dynamic.NewForConfig(clientConfig)
|
||||
}
|
||||
|
||||
// NewBuilder returns a new resource builder for structured api objects.
|
||||
func (f *factoryImpl) NewBuilder() *resource.Builder {
|
||||
return resource.NewBuilder(f.clientGetter)
|
||||
}
|
||||
|
||||
func (f *factoryImpl) RESTClient() (*restclient.RESTClient, error) {
|
||||
clientConfig, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setKubernetesDefaults(clientConfig)
|
||||
return restclient.RESTClientFor(clientConfig)
|
||||
}
|
||||
|
||||
func (f *factoryImpl) ClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) {
|
||||
cfg, err := f.clientGetter.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setKubernetesDefaults(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gvk := mapping.GroupVersionKind
|
||||
switch gvk.Group {
|
||||
case corev1.GroupName:
|
||||
cfg.APIPath = "/api"
|
||||
default:
|
||||
cfg.APIPath = "/apis"
|
||||
}
|
||||
gv := gvk.GroupVersion()
|
||||
cfg.GroupVersion = &gv
|
||||
return restclient.RESTClientFor(cfg)
|
||||
}
|
||||
|
||||
func (f *factoryImpl) UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) {
|
||||
cfg, err := f.clientGetter.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := restclient.SetKubernetesDefaults(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.APIPath = "/apis"
|
||||
if mapping.GroupVersionKind.Group == corev1.GroupName {
|
||||
cfg.APIPath = "/api"
|
||||
}
|
||||
gv := mapping.GroupVersionKind.GroupVersion()
|
||||
cfg.ContentConfig = resource.UnstructuredPlusDefaultContentConfig()
|
||||
cfg.GroupVersion = &gv
|
||||
return restclient.RESTClientFor(cfg)
|
||||
}
|
||||
|
||||
func (f *factoryImpl) Validator(validationDirective string) (validation.Schema, error) {
|
||||
// client-side schema validation is only performed
|
||||
// when the validationDirective is strict.
|
||||
// If the directive is warn, we rely on the ParamVerifyingSchema
|
||||
// to ignore the client-side validation and provide a warning
|
||||
// to the user that attempting warn validation when SS validation
|
||||
// is unsupported is inert.
|
||||
if validationDirective == metav1.FieldValidationIgnore {
|
||||
return validation.NullSchema{}, nil
|
||||
}
|
||||
|
||||
schema := validation.ConjunctiveSchema{
|
||||
validation.NewSchemaValidation(f),
|
||||
validation.NoDoubleKeySchema{},
|
||||
}
|
||||
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create the FieldValidationVerifier for use in the ParamVerifyingSchema.
|
||||
discoveryClient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Memory-cache the OpenAPI V3 responses. The disk cache behavior is determined by
|
||||
// the discovery client.
|
||||
oapiV3Client := cached.NewClient(discoveryClient.OpenAPIV3())
|
||||
queryParam := resource.QueryParamFieldValidation
|
||||
primary := newCachingVerifier(resource.NewQueryParamVerifierV3(dynamicClient, oapiV3Client, queryParam))
|
||||
secondary := resource.NewQueryParamVerifier(dynamicClient, f.openAPIGetter(), queryParam)
|
||||
fallback := resource.NewFallbackQueryParamVerifier(primary, secondary)
|
||||
return validation.NewParamVerifyingSchema(schema, fallback, string(validationDirective)), nil
|
||||
}
|
||||
|
||||
// OpenAPISchema returns metadata and structural information about
|
||||
// Kubernetes object definitions.
|
||||
func (f *factoryImpl) OpenAPISchema() (openapi.Resources, error) {
|
||||
openAPIGetter := f.openAPIGetter()
|
||||
if openAPIGetter == nil {
|
||||
return nil, errors.New("no openapi getter")
|
||||
}
|
||||
|
||||
// Lazily initialize the OpenAPIParser once
|
||||
f.parser.Do(func() {
|
||||
// Create the caching OpenAPIParser
|
||||
f.openAPIParser = openapi.NewOpenAPIParser(f.openAPIGetter())
|
||||
})
|
||||
|
||||
// Delegate to the OpenAPIPArser
|
||||
return f.openAPIParser.Parse()
|
||||
}
|
||||
|
||||
func (f *factoryImpl) openAPIGetter() discovery.OpenAPISchemaInterface {
|
||||
discovery, err := f.clientGetter.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
f.getter.Do(func() {
|
||||
f.oapi = openapi.NewOpenAPIGetter(discovery)
|
||||
})
|
||||
|
||||
return f.oapi
|
||||
}
|
||||
|
||||
func (f *factoryImpl) OpenAPIV3Client() (openapiclient.Client, error) {
|
||||
discovery, err := f.clientGetter.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cached.NewClient(discovery.OpenAPIV3()), nil
|
||||
}
|
||||
+930
@@ -0,0 +1,930 @@
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
|
||||
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/runtime"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/scale"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/klog/v2"
|
||||
utilexec "k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
ApplyAnnotationsFlag = "save-config"
|
||||
DefaultErrorExitCode = 1
|
||||
DefaultChunkSize = 500
|
||||
)
|
||||
|
||||
type debugError interface {
|
||||
DebugError() (msg string, args []interface{})
|
||||
}
|
||||
|
||||
// AddSourceToErr adds handleResourcePrefix and source string to error message.
|
||||
// verb is the string like "creating", "deleting" etc.
|
||||
// source is the filename or URL to the template file(*.json or *.yaml), or stdin to use to handle the resource.
|
||||
func AddSourceToErr(verb string, source string, err error) error {
|
||||
if source != "" {
|
||||
if statusError, ok := err.(apierrors.APIStatus); ok {
|
||||
status := statusError.Status()
|
||||
status.Message = fmt.Sprintf("error when %s %q: %v", verb, source, status.Message)
|
||||
return &apierrors.StatusError{ErrStatus: status}
|
||||
}
|
||||
return fmt.Errorf("error when %s %q: %v", verb, source, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var fatalErrHandler = fatal
|
||||
|
||||
// BehaviorOnFatal allows you to override the default behavior when a fatal
|
||||
// error occurs, which is to call os.Exit(code). You can pass 'panic' as a function
|
||||
// here if you prefer the panic() over os.Exit(1).
|
||||
func BehaviorOnFatal(f func(string, int)) {
|
||||
fatalErrHandler = f
|
||||
}
|
||||
|
||||
// DefaultBehaviorOnFatal allows you to undo any previous override. Useful in
|
||||
// tests.
|
||||
func DefaultBehaviorOnFatal() {
|
||||
fatalErrHandler = fatal
|
||||
}
|
||||
|
||||
// fatal prints the message (if provided) and then exits. If V(99) or greater,
|
||||
// klog.Fatal is invoked for extended information. This is intended for maintainer
|
||||
// debugging and out of a reasonable range for users.
|
||||
func fatal(msg string, code int) {
|
||||
// nolint:logcheck // Not using the result of klog.V(99) inside the if
|
||||
// branch is okay, we just use it to determine how to terminate.
|
||||
if klog.V(99).Enabled() {
|
||||
klog.FatalDepth(2, msg)
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
// add newline if needed
|
||||
if !strings.HasSuffix(msg, "\n") {
|
||||
msg += "\n"
|
||||
}
|
||||
fmt.Fprint(os.Stderr, msg)
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// ErrExit may be passed to CheckError to instruct it to output nothing but exit with
|
||||
// status code 1.
|
||||
var ErrExit = fmt.Errorf("exit")
|
||||
|
||||
// CheckErr prints a user friendly error to STDERR and exits with a non-zero
|
||||
// exit code. Unrecognized errors will be printed with an "error: " prefix.
|
||||
//
|
||||
// This method is generic to the command in use and may be used by non-Kubectl
|
||||
// commands.
|
||||
func CheckErr(err error) {
|
||||
checkErr(err, fatalErrHandler)
|
||||
}
|
||||
|
||||
// CheckDiffErr prints a user friendly error to STDERR and exits with a
|
||||
// non-zero and non-one exit code. Unrecognized errors will be printed
|
||||
// with an "error: " prefix.
|
||||
//
|
||||
// This method is meant specifically for `kubectl diff` and may be used
|
||||
// by other commands.
|
||||
func CheckDiffErr(err error) {
|
||||
checkErr(err, func(msg string, code int) {
|
||||
fatalErrHandler(msg, code+1)
|
||||
})
|
||||
}
|
||||
|
||||
// isInvalidReasonStatusError returns true if this is an API Status error with reason=Invalid.
|
||||
// This is distinct from generic 422 errors we want to fall back to generic error handling.
|
||||
func isInvalidReasonStatusError(err error) bool {
|
||||
if !apierrors.IsInvalid(err) {
|
||||
return false
|
||||
}
|
||||
statusError, isStatusError := err.(*apierrors.StatusError)
|
||||
if !isStatusError {
|
||||
return false
|
||||
}
|
||||
status := statusError.Status()
|
||||
return status.Reason == metav1.StatusReasonInvalid
|
||||
}
|
||||
|
||||
// checkErr formats a given error as a string and calls the passed handleErr
|
||||
// func with that string and an kubectl exit code.
|
||||
func checkErr(err error, handleErr func(string, int)) {
|
||||
// unwrap aggregates of 1
|
||||
if agg, ok := err.(utilerrors.Aggregate); ok && len(agg.Errors()) == 1 {
|
||||
err = agg.Errors()[0]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case err == ErrExit:
|
||||
handleErr("", DefaultErrorExitCode)
|
||||
case isInvalidReasonStatusError(err):
|
||||
status := err.(*apierrors.StatusError).Status()
|
||||
details := status.Details
|
||||
s := "The request is invalid"
|
||||
if details == nil {
|
||||
// if we have no other details, include the message from the server if present
|
||||
if len(status.Message) > 0 {
|
||||
s += ": " + status.Message
|
||||
}
|
||||
handleErr(s, DefaultErrorExitCode)
|
||||
return
|
||||
}
|
||||
if len(details.Kind) != 0 || len(details.Name) != 0 {
|
||||
s = fmt.Sprintf("The %s %q is invalid", details.Kind, details.Name)
|
||||
} else if len(status.Message) > 0 && len(details.Causes) == 0 {
|
||||
// only append the message if we have no kind/name details and no causes,
|
||||
// since default invalid error constructors duplicate that information in the message
|
||||
s += ": " + status.Message
|
||||
}
|
||||
|
||||
if len(details.Causes) > 0 {
|
||||
errs := statusCausesToAggrError(details.Causes)
|
||||
handleErr(MultilineError(s+": ", errs), DefaultErrorExitCode)
|
||||
} else {
|
||||
handleErr(s, DefaultErrorExitCode)
|
||||
}
|
||||
case clientcmd.IsConfigurationInvalid(err):
|
||||
handleErr(MultilineError("Error in configuration: ", err), DefaultErrorExitCode)
|
||||
default:
|
||||
switch err := err.(type) {
|
||||
case *meta.NoResourceMatchError:
|
||||
switch {
|
||||
case len(err.PartialResource.Group) > 0 && len(err.PartialResource.Version) > 0:
|
||||
handleErr(fmt.Sprintf("the server doesn't have a resource type %q in group %q and version %q", err.PartialResource.Resource, err.PartialResource.Group, err.PartialResource.Version), DefaultErrorExitCode)
|
||||
case len(err.PartialResource.Group) > 0:
|
||||
handleErr(fmt.Sprintf("the server doesn't have a resource type %q in group %q", err.PartialResource.Resource, err.PartialResource.Group), DefaultErrorExitCode)
|
||||
case len(err.PartialResource.Version) > 0:
|
||||
handleErr(fmt.Sprintf("the server doesn't have a resource type %q in version %q", err.PartialResource.Resource, err.PartialResource.Version), DefaultErrorExitCode)
|
||||
default:
|
||||
handleErr(fmt.Sprintf("the server doesn't have a resource type %q", err.PartialResource.Resource), DefaultErrorExitCode)
|
||||
}
|
||||
case utilerrors.Aggregate:
|
||||
handleErr(MultipleErrors(``, err.Errors()), DefaultErrorExitCode)
|
||||
case utilexec.ExitError:
|
||||
handleErr(err.Error(), err.ExitStatus())
|
||||
default: // for any other error type
|
||||
msg, ok := StandardErrorMessage(err)
|
||||
if !ok {
|
||||
msg = err.Error()
|
||||
if !strings.HasPrefix(msg, "error: ") {
|
||||
msg = fmt.Sprintf("error: %s", msg)
|
||||
}
|
||||
}
|
||||
handleErr(msg, DefaultErrorExitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func statusCausesToAggrError(scs []metav1.StatusCause) utilerrors.Aggregate {
|
||||
errs := make([]error, 0, len(scs))
|
||||
errorMsgs := sets.New[string]()
|
||||
for _, sc := range scs {
|
||||
// check for duplicate error messages and skip them
|
||||
msg := fmt.Sprintf("%s: %s", sc.Field, sc.Message)
|
||||
if errorMsgs.Has(msg) {
|
||||
continue
|
||||
}
|
||||
errorMsgs.Insert(msg)
|
||||
errs = append(errs, errors.New(msg))
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
// StandardErrorMessage translates common errors into a human readable message, or returns
|
||||
// false if the error is not one of the recognized types. It may also log extended
|
||||
// information to klog.
|
||||
//
|
||||
// This method is generic to the command in use and may be used by non-Kubectl
|
||||
// commands.
|
||||
func StandardErrorMessage(err error) (string, bool) {
|
||||
if debugErr, ok := err.(debugError); ok {
|
||||
klog.V(4).Info(debugErr.DebugError())
|
||||
}
|
||||
status, isStatus := err.(apierrors.APIStatus)
|
||||
switch {
|
||||
case isStatus:
|
||||
switch s := status.Status(); {
|
||||
case s.Reason == metav1.StatusReasonUnauthorized:
|
||||
return fmt.Sprintf("error: You must be logged in to the server (%s)", s.Message), true
|
||||
case len(s.Reason) > 0:
|
||||
return fmt.Sprintf("Error from server (%s): %s", s.Reason, err.Error()), true
|
||||
default:
|
||||
return fmt.Sprintf("Error from server: %s", err.Error()), true
|
||||
}
|
||||
case apierrors.IsUnexpectedObjectError(err):
|
||||
return fmt.Sprintf("Server returned an unexpected response: %s", err.Error()), true
|
||||
}
|
||||
switch t := err.(type) {
|
||||
case *url.Error:
|
||||
klog.V(4).Infof("Connection error: %s %s: %v", t.Op, t.URL, t.Err)
|
||||
switch {
|
||||
case strings.Contains(t.Err.Error(), "connection refused"):
|
||||
host := t.URL
|
||||
if server, err := url.Parse(t.URL); err == nil {
|
||||
host = server.Host
|
||||
}
|
||||
return fmt.Sprintf("The connection to the server %s was refused - did you specify the right host or port?", host), true
|
||||
}
|
||||
return fmt.Sprintf("Unable to connect to the server: %v", t.Err), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// MultilineError returns a string representing an error that splits sub errors into their own
|
||||
// lines. The returned string will end with a newline.
|
||||
func MultilineError(prefix string, err error) string {
|
||||
if agg, ok := err.(utilerrors.Aggregate); ok {
|
||||
errs := utilerrors.Flatten(agg).Errors()
|
||||
buf := &bytes.Buffer{}
|
||||
switch len(errs) {
|
||||
case 0:
|
||||
return fmt.Sprintf("%s%v\n", prefix, err)
|
||||
case 1:
|
||||
return fmt.Sprintf("%s%v\n", prefix, messageForError(errs[0]))
|
||||
default:
|
||||
fmt.Fprintln(buf, prefix)
|
||||
for _, err := range errs {
|
||||
fmt.Fprintf(buf, "* %v\n", messageForError(err))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s%s\n", prefix, err)
|
||||
}
|
||||
|
||||
// PrintErrorWithCauses prints an error's kind, name, and each of the error's causes in a new line.
|
||||
// The returned string will end with a newline.
|
||||
// Returns true if a case exists to handle the error type, or false otherwise.
|
||||
func PrintErrorWithCauses(err error, errOut io.Writer) bool {
|
||||
switch t := err.(type) {
|
||||
case *apierrors.StatusError:
|
||||
errorDetails := t.Status().Details
|
||||
if errorDetails != nil {
|
||||
fmt.Fprintf(errOut, "error: %s %q is invalid\n\n", errorDetails.Kind, errorDetails.Name)
|
||||
for _, cause := range errorDetails.Causes {
|
||||
fmt.Fprintf(errOut, "* %s: %s\n", cause.Field, cause.Message)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(errOut, "error: %v\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// MultipleErrors returns a newline delimited string containing
|
||||
// the prefix and referenced errors in standard form.
|
||||
func MultipleErrors(prefix string, errs []error) string {
|
||||
buf := &bytes.Buffer{}
|
||||
for _, err := range errs {
|
||||
fmt.Fprintf(buf, "%s%v\n", prefix, messageForError(err))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// messageForError returns the string representing the error.
|
||||
func messageForError(err error) string {
|
||||
msg, ok := StandardErrorMessage(err)
|
||||
if !ok {
|
||||
msg = err.Error()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func UsageErrorf(cmd *cobra.Command, format string, args ...interface{}) error {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
return fmt.Errorf("%s\nSee '%s -h' for help and examples", msg, cmd.CommandPath())
|
||||
}
|
||||
|
||||
func IsFilenameSliceEmpty(filenames []string, directory string) bool {
|
||||
return len(filenames) == 0 && directory == ""
|
||||
}
|
||||
|
||||
func GetFlagString(cmd *cobra.Command, flag string) string {
|
||||
s, err := cmd.Flags().GetString(flag)
|
||||
if err != nil {
|
||||
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// GetFlagStringSlice can be used to accept multiple argument with flag repetition (e.g. -f arg1,arg2 -f arg3 ...)
|
||||
func GetFlagStringSlice(cmd *cobra.Command, flag string) []string {
|
||||
s, err := cmd.Flags().GetStringSlice(flag)
|
||||
if err != nil {
|
||||
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// GetFlagStringArray can be used to accept multiple argument with flag repetition (e.g. -f arg1 -f arg2 ...)
|
||||
func GetFlagStringArray(cmd *cobra.Command, flag string) []string {
|
||||
s, err := cmd.Flags().GetStringArray(flag)
|
||||
if err != nil {
|
||||
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func GetFlagBool(cmd *cobra.Command, flag string) bool {
|
||||
b, err := cmd.Flags().GetBool(flag)
|
||||
if err != nil {
|
||||
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Assumes the flag has a default value.
|
||||
func GetFlagInt(cmd *cobra.Command, flag string) int {
|
||||
i, err := cmd.Flags().GetInt(flag)
|
||||
if err != nil {
|
||||
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// Assumes the flag has a default value.
|
||||
func GetFlagInt32(cmd *cobra.Command, flag string) int32 {
|
||||
i, err := cmd.Flags().GetInt32(flag)
|
||||
if err != nil {
|
||||
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// Assumes the flag has a default value.
|
||||
func GetFlagInt64(cmd *cobra.Command, flag string) int64 {
|
||||
i, err := cmd.Flags().GetInt64(flag)
|
||||
if err != nil {
|
||||
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func GetFlagDuration(cmd *cobra.Command, flag string) time.Duration {
|
||||
d, err := cmd.Flags().GetDuration(flag)
|
||||
if err != nil {
|
||||
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func GetPodRunningTimeoutFlag(cmd *cobra.Command) (time.Duration, error) {
|
||||
timeout := GetFlagDuration(cmd, "pod-running-timeout")
|
||||
if timeout <= 0 {
|
||||
return timeout, fmt.Errorf("--pod-running-timeout must be higher than zero")
|
||||
}
|
||||
return timeout, nil
|
||||
}
|
||||
|
||||
type FeatureGate string
|
||||
|
||||
const (
|
||||
// owner: @ardaguclu
|
||||
// kep: https://kep.k8s.io/3104
|
||||
//
|
||||
// Separate kubectl user preferences.
|
||||
KubeRC FeatureGate = "KUBECTL_KUBERC"
|
||||
|
||||
// owner: @justinb
|
||||
// kep: https://kep.k8s.io/3659
|
||||
//
|
||||
// Improved kubectl apply --prune behavior.
|
||||
ApplySet FeatureGate = "KUBECTL_APPLYSET"
|
||||
|
||||
// owner: @seans
|
||||
// kep: https://kep.k8s.io/4006
|
||||
//
|
||||
// Transition to WebSockets.
|
||||
RemoteCommandWebsockets FeatureGate = "KUBECTL_REMOTE_COMMAND_WEBSOCKETS"
|
||||
PortForwardWebsockets FeatureGate = "KUBECTL_PORT_FORWARD_WEBSOCKETS"
|
||||
|
||||
// owner: @thockin
|
||||
// kep: https://kep.k8s.io/5296
|
||||
//
|
||||
// Support KYAML output.
|
||||
KYAMLOutput FeatureGate = "KUBECTL_KYAML"
|
||||
)
|
||||
|
||||
// IsEnabled returns true iff environment variable is set to true.
|
||||
// All other cases, it returns false.
|
||||
func (f FeatureGate) IsEnabled() bool {
|
||||
return strings.ToLower(os.Getenv(string(f))) == "true"
|
||||
}
|
||||
|
||||
// IsDisabled returns true iff environment variable is set to false.
|
||||
// All other cases, it returns true.
|
||||
// This function is used for the cases where feature is enabled by default,
|
||||
// but it may be needed to provide a way to ability to disable this feature.
|
||||
func (f FeatureGate) IsDisabled() bool {
|
||||
return strings.ToLower(os.Getenv(string(f))) == "false"
|
||||
}
|
||||
|
||||
func AddValidateFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String(
|
||||
"validate",
|
||||
"strict",
|
||||
`Must be one of: strict (or true), warn, ignore (or false). "true" or "strict" will use a schema to validate the input and fail the request if invalid. It will perform server side validation if ServerSideFieldValidation is enabled on the api-server, but will fall back to less reliable client-side validation if not. "warn" will warn about unknown or duplicate fields without blocking the request if server-side field validation is enabled on the API server, and behave as "ignore" otherwise. "false" or "ignore" will not perform any schema validation, silently dropping any unknown or duplicate fields.`,
|
||||
)
|
||||
|
||||
cmd.Flags().Lookup("validate").NoOptDefVal = "strict"
|
||||
}
|
||||
|
||||
func AddFilenameOptionFlags(cmd *cobra.Command, options *resource.FilenameOptions, usage string) {
|
||||
AddJsonFilenameFlag(cmd.Flags(), &options.Filenames, "Filename, directory, or URL to files "+usage)
|
||||
AddKustomizeFlag(cmd.Flags(), &options.Kustomize)
|
||||
cmd.Flags().BoolVarP(&options.Recursive, "recursive", "R", options.Recursive, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.")
|
||||
}
|
||||
|
||||
func AddJsonFilenameFlag(flags *pflag.FlagSet, value *[]string, usage string) {
|
||||
flags.StringSliceVarP(value, "filename", "f", *value, usage)
|
||||
annotations := make([]string, 0, len(resource.FileExtensions))
|
||||
for _, ext := range resource.FileExtensions {
|
||||
annotations = append(annotations, strings.TrimLeft(ext, "."))
|
||||
}
|
||||
flags.SetAnnotation("filename", cobra.BashCompFilenameExt, annotations)
|
||||
}
|
||||
|
||||
// AddKustomizeFlag adds kustomize flag to a command
|
||||
func AddKustomizeFlag(flags *pflag.FlagSet, value *string) {
|
||||
flags.StringVarP(value, "kustomize", "k", *value, "Process the kustomization directory. This flag can't be used together with -f or -R.")
|
||||
}
|
||||
|
||||
// AddDryRunFlag adds dry-run flag to a command. Usually used by mutations.
|
||||
func AddDryRunFlag(cmd *cobra.Command) {
|
||||
cmd.Flags().String(
|
||||
"dry-run",
|
||||
"none",
|
||||
`Must be "none", "server", or "client". If client strategy, only print the object that would be sent, without sending it. If server strategy, submit server-side request without persisting the resource.`,
|
||||
)
|
||||
cmd.Flags().Lookup("dry-run").NoOptDefVal = "unchanged"
|
||||
}
|
||||
|
||||
func AddFieldManagerFlagVar(cmd *cobra.Command, p *string, defaultFieldManager string) {
|
||||
cmd.Flags().StringVar(p, "field-manager", defaultFieldManager, "Name of the manager used to track field ownership.")
|
||||
}
|
||||
|
||||
func AddContainerVarFlags(cmd *cobra.Command, p *string, containerName string) {
|
||||
cmd.Flags().StringVarP(p, "container", "c", containerName, "Container name. If omitted, use the kubectl.kubernetes.io/default-container annotation for selecting the container to be attached or the first container in the pod will be chosen")
|
||||
}
|
||||
|
||||
func AddServerSideApplyFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().Bool("server-side", false, "If true, apply runs in the server instead of the client.")
|
||||
cmd.Flags().Bool("force-conflicts", false, "If true, server-side apply will force the changes against conflicts.")
|
||||
}
|
||||
|
||||
func AddPodRunningTimeoutFlag(cmd *cobra.Command, defaultTimeout time.Duration) {
|
||||
cmd.Flags().Duration("pod-running-timeout", defaultTimeout, "The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running")
|
||||
}
|
||||
|
||||
func AddApplyAnnotationFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().Bool(ApplyAnnotationsFlag, false, "If true, the configuration of current object will be saved in its annotation. Otherwise, the annotation will be unchanged. This flag is useful when you want to perform kubectl apply on this object in the future.")
|
||||
}
|
||||
|
||||
func AddApplyAnnotationVarFlags(cmd *cobra.Command, applyAnnotation *bool) {
|
||||
cmd.Flags().BoolVar(applyAnnotation, ApplyAnnotationsFlag, *applyAnnotation, "If true, the configuration of current object will be saved in its annotation. Otherwise, the annotation will be unchanged. This flag is useful when you want to perform kubectl apply on this object in the future.")
|
||||
}
|
||||
|
||||
func AddChunkSizeFlag(cmd *cobra.Command, value *int64) {
|
||||
cmd.Flags().Int64Var(value, "chunk-size", *value,
|
||||
"Return large lists in chunks rather than all at once. Pass 0 to disable.")
|
||||
}
|
||||
|
||||
func AddLabelSelectorFlagVar(cmd *cobra.Command, p *string) {
|
||||
cmd.Flags().StringVarP(p, "selector", "l", *p, "Selector (label query) to filter on, supports '=', '==', '!=', 'in', 'notin'.(e.g. -l key1=value1,key2=value2,key3 in (value3)). Matching objects must satisfy all of the specified label constraints.")
|
||||
}
|
||||
|
||||
func AddPruningFlags(cmd *cobra.Command, prune *bool, pruneAllowlist *[]string, all *bool, applySetRef *string) {
|
||||
// Flags associated with the original allowlist-based alpha
|
||||
cmd.Flags().StringArrayVar(pruneAllowlist, "prune-allowlist", *pruneAllowlist, "Overwrite the default allowlist with <group/version/kind> for --prune")
|
||||
cmd.Flags().BoolVar(all, "all", *all, "Select all resources in the namespace of the specified resource types.")
|
||||
|
||||
// Flags associated with the new ApplySet-based alpha
|
||||
if ApplySet.IsEnabled() {
|
||||
cmd.Flags().StringVar(applySetRef, "applyset", *applySetRef, "[alpha] The name of the ApplySet that tracks which resources are being managed, for the purposes of determining what to prune. Live resources that are part of the ApplySet but have been removed from the provided configs will be deleted. Format: [RESOURCE][.GROUP]/NAME. A Secret will be used if no resource or group is specified.")
|
||||
cmd.Flags().BoolVar(prune, "prune", *prune, "Automatically delete previously applied resource objects that do not appear in the provided configs. For alpha1, use with either -l or --all. For alpha2, use with --applyset.")
|
||||
} else {
|
||||
// different docs for the shared --prune flag if only alpha1 is enabled
|
||||
cmd.Flags().BoolVar(prune, "prune", *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.")
|
||||
}
|
||||
}
|
||||
|
||||
func AddSubresourceFlags(cmd *cobra.Command, subresource *string, usage string) {
|
||||
cmd.Flags().StringVar(subresource, "subresource", "", usage)
|
||||
CheckErr(cmd.RegisterFlagCompletionFunc("subresource", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||
var commonSubresources = []string{"status", "scale", "resize"}
|
||||
return commonSubresources, cobra.ShellCompDirectiveNoFileComp
|
||||
}))
|
||||
}
|
||||
|
||||
type ValidateOptions struct {
|
||||
ValidationDirective string
|
||||
}
|
||||
|
||||
// Merge converts the passed in object to JSON, merges the fragment into it using an RFC7396 JSON Merge Patch,
|
||||
// and returns the resulting object
|
||||
// TODO: merge assumes JSON serialization, and does not properly abstract API retrieval
|
||||
func Merge(codec runtime.Codec, dst runtime.Object, fragment string) (runtime.Object, error) {
|
||||
// encode dst into versioned json and apply fragment directly too it
|
||||
target, err := runtime.Encode(codec, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patched, err := jsonpatch.MergePatch(target, []byte(fragment))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := runtime.Decode(codec, patched)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// StrategicMerge converts the passed in object to JSON, merges the fragment into it using a Strategic Merge Patch,
|
||||
// and returns the resulting object
|
||||
func StrategicMerge(codec runtime.Codec, dst runtime.Object, fragment string, dataStruct runtime.Object) (runtime.Object, error) {
|
||||
target, err := runtime.Encode(codec, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patched, err := strategicpatch.StrategicMergePatch(target, []byte(fragment), dataStruct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := runtime.Decode(codec, patched)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// JSONPatch converts the passed in object to JSON, performs an RFC6902 JSON Patch using operations specified in the
|
||||
// fragment, and returns the resulting object
|
||||
func JSONPatch(codec runtime.Codec, dst runtime.Object, fragment string) (runtime.Object, error) {
|
||||
target, err := runtime.Encode(codec, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patch, err := jsonpatch.DecodePatch([]byte(fragment))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patched, err := patch.Apply(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := runtime.Decode(codec, patched)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DumpReaderToFile writes all data from the given io.Reader to the specified file
|
||||
// (usually for temporary use).
|
||||
func DumpReaderToFile(reader io.Reader, filename string) error {
|
||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
count, err := reader.Read(buffer)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.Write(buffer[:count])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetServerSideApplyFlag(cmd *cobra.Command) bool {
|
||||
return GetFlagBool(cmd, "server-side")
|
||||
}
|
||||
|
||||
func GetForceConflictsFlag(cmd *cobra.Command) bool {
|
||||
return GetFlagBool(cmd, "force-conflicts")
|
||||
}
|
||||
|
||||
func GetFieldManagerFlag(cmd *cobra.Command) string {
|
||||
return GetFlagString(cmd, "field-manager")
|
||||
}
|
||||
|
||||
func GetValidationDirective(cmd *cobra.Command) (string, error) {
|
||||
var validateFlag = GetFlagString(cmd, "validate")
|
||||
b, err := strconv.ParseBool(validateFlag)
|
||||
if err != nil {
|
||||
switch validateFlag {
|
||||
case "strict":
|
||||
return metav1.FieldValidationStrict, nil
|
||||
case "warn":
|
||||
return metav1.FieldValidationWarn, nil
|
||||
case "ignore":
|
||||
return metav1.FieldValidationIgnore, nil
|
||||
default:
|
||||
return metav1.FieldValidationStrict, fmt.Errorf(`invalid - validate option %q; must be one of: strict (or true), warn, ignore (or false)`, validateFlag)
|
||||
}
|
||||
}
|
||||
// The flag was a boolean
|
||||
if b {
|
||||
return metav1.FieldValidationStrict, nil
|
||||
}
|
||||
return metav1.FieldValidationIgnore, nil
|
||||
}
|
||||
|
||||
type DryRunStrategy int
|
||||
|
||||
const (
|
||||
// DryRunNone indicates the client will make all mutating calls
|
||||
DryRunNone DryRunStrategy = iota
|
||||
|
||||
// DryRunClient, or client-side dry-run, indicates the client will prevent
|
||||
// making mutating calls such as CREATE, PATCH, and DELETE
|
||||
DryRunClient
|
||||
|
||||
// DryRunServer, or server-side dry-run, indicates the client will send
|
||||
// mutating calls to the APIServer with the dry-run parameter to prevent
|
||||
// persisting changes.
|
||||
//
|
||||
// Note that clients sending server-side dry-run calls should verify that
|
||||
// the APIServer and the resource supports server-side dry-run, and otherwise
|
||||
// clients should fail early.
|
||||
//
|
||||
// If a client sends a server-side dry-run call to an APIServer that doesn't
|
||||
// support server-side dry-run, then the APIServer will persist changes inadvertently.
|
||||
DryRunServer
|
||||
)
|
||||
|
||||
func GetDryRunStrategy(cmd *cobra.Command) (DryRunStrategy, error) {
|
||||
var dryRunFlag = GetFlagString(cmd, "dry-run")
|
||||
b, err := strconv.ParseBool(dryRunFlag)
|
||||
// The flag is not a boolean
|
||||
if err != nil {
|
||||
switch dryRunFlag {
|
||||
case cmd.Flag("dry-run").NoOptDefVal:
|
||||
klog.Warning(`--dry-run is deprecated and can be replaced with --dry-run=client.`)
|
||||
return DryRunClient, nil
|
||||
case "client":
|
||||
return DryRunClient, nil
|
||||
case "server":
|
||||
return DryRunServer, nil
|
||||
case "none":
|
||||
return DryRunNone, nil
|
||||
default:
|
||||
return DryRunNone, fmt.Errorf(`Invalid dry-run value (%v). Must be "none", "server", or "client".`, dryRunFlag)
|
||||
}
|
||||
}
|
||||
// The flag was a boolean
|
||||
if b {
|
||||
klog.Warningf(`--dry-run=%v is deprecated (boolean value) and can be replaced with --dry-run=%s.`, dryRunFlag, "client")
|
||||
return DryRunClient, nil
|
||||
}
|
||||
klog.Warningf(`--dry-run=%v is deprecated (boolean value) and can be replaced with --dry-run=%s.`, dryRunFlag, "none")
|
||||
return DryRunNone, nil
|
||||
}
|
||||
|
||||
// PrintFlagsWithDryRunStrategy sets a success message at print time for the dry run strategy
|
||||
//
|
||||
// TODO(juanvallejo): This can be cleaned up even further by creating
|
||||
// a PrintFlags struct that binds the --dry-run flag, and whose
|
||||
// ToPrinter method returns a printer that understands how to print
|
||||
// this success message.
|
||||
func PrintFlagsWithDryRunStrategy(printFlags *genericclioptions.PrintFlags, dryRunStrategy DryRunStrategy) *genericclioptions.PrintFlags {
|
||||
switch dryRunStrategy {
|
||||
case DryRunClient:
|
||||
printFlags.Complete("%s (dry run)")
|
||||
case DryRunServer:
|
||||
printFlags.Complete("%s (server dry run)")
|
||||
}
|
||||
return printFlags
|
||||
}
|
||||
|
||||
// GetResourcesAndPairs retrieves resources and "KEY=VALUE or KEY-" pair args from given args
|
||||
func GetResourcesAndPairs(args []string, pairType string) (resources []string, pairArgs []string, err error) {
|
||||
foundPair := false
|
||||
for _, s := range args {
|
||||
nonResource := (strings.Contains(s, "=") && s[0] != '=') || (strings.HasSuffix(s, "-") && s != "-")
|
||||
switch {
|
||||
case !foundPair && nonResource:
|
||||
foundPair = true
|
||||
fallthrough
|
||||
case foundPair && nonResource:
|
||||
pairArgs = append(pairArgs, s)
|
||||
case !foundPair && !nonResource:
|
||||
resources = append(resources, s)
|
||||
case foundPair && !nonResource:
|
||||
err = fmt.Errorf("all resources must be specified before %s changes: %s", pairType, s)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParsePairs retrieves new and remove pairs (if supportRemove is true) from "KEY=VALUE or KEY-" pair args
|
||||
func ParsePairs(pairArgs []string, pairType string, supportRemove bool) (newPairs map[string]string, removePairs []string, err error) {
|
||||
newPairs = map[string]string{}
|
||||
if supportRemove {
|
||||
removePairs = []string{}
|
||||
}
|
||||
var invalidBuf bytes.Buffer
|
||||
var invalidBufNonEmpty bool
|
||||
for _, pairArg := range pairArgs {
|
||||
if strings.Contains(pairArg, "=") && pairArg[0] != '=' {
|
||||
parts := strings.SplitN(pairArg, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
if invalidBufNonEmpty {
|
||||
invalidBuf.WriteString(", ")
|
||||
}
|
||||
invalidBuf.WriteString(pairArg)
|
||||
invalidBufNonEmpty = true
|
||||
} else {
|
||||
newPairs[parts[0]] = parts[1]
|
||||
}
|
||||
} else if supportRemove && strings.HasSuffix(pairArg, "-") && pairArg != "-" {
|
||||
removePairs = append(removePairs, pairArg[:len(pairArg)-1])
|
||||
} else {
|
||||
if invalidBufNonEmpty {
|
||||
invalidBuf.WriteString(", ")
|
||||
}
|
||||
invalidBuf.WriteString(pairArg)
|
||||
invalidBufNonEmpty = true
|
||||
}
|
||||
}
|
||||
if invalidBufNonEmpty {
|
||||
err = fmt.Errorf("invalid %s format: %s", pairType, invalidBuf.String())
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsSiblingCommandExists receives a pointer to a cobra command and a target string.
|
||||
// Returns true if the target string is found in the list of sibling commands.
|
||||
func IsSiblingCommandExists(cmd *cobra.Command, targetCmdName string) bool {
|
||||
for _, c := range cmd.Parent().Commands() {
|
||||
if c.Name() == targetCmdName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// DefaultSubCommandRun prints a command's help string to the specified output if no
|
||||
// arguments (sub-commands) are provided, or a usage error otherwise.
|
||||
func DefaultSubCommandRun(out io.Writer) func(c *cobra.Command, args []string) {
|
||||
return func(c *cobra.Command, args []string) {
|
||||
c.SetOut(out)
|
||||
c.SetErr(out)
|
||||
RequireNoArguments(c, args)
|
||||
c.Help()
|
||||
CheckErr(ErrExit)
|
||||
}
|
||||
}
|
||||
|
||||
// RequireNoArguments exits with a usage error if extra arguments are provided.
|
||||
func RequireNoArguments(c *cobra.Command, args []string) {
|
||||
if len(args) > 0 {
|
||||
CheckErr(UsageErrorf(c, "unknown command %q", strings.Join(args, " ")))
|
||||
}
|
||||
}
|
||||
|
||||
// StripComments will transform a YAML file into JSON, thus dropping any comments
|
||||
// in it. Note that if the given file has a syntax error, the transformation will
|
||||
// fail and we will manually drop all comments from the file.
|
||||
func StripComments(file []byte) []byte {
|
||||
stripped := file
|
||||
stripped, err := yaml.ToJSON(stripped)
|
||||
if err != nil {
|
||||
stripped = ManualStrip(file)
|
||||
}
|
||||
return stripped
|
||||
}
|
||||
|
||||
// ManualStrip is used for dropping comments from a YAML file
|
||||
func ManualStrip(file []byte) []byte {
|
||||
stripped := []byte{}
|
||||
lines := bytes.Split(file, []byte("\n"))
|
||||
for i, line := range lines {
|
||||
trimline := bytes.TrimSpace(line)
|
||||
|
||||
if bytes.HasPrefix(trimline, []byte("#")) && !bytes.HasPrefix(trimline, []byte("#!")) {
|
||||
continue
|
||||
}
|
||||
stripped = append(stripped, line...)
|
||||
if i < len(lines)-1 {
|
||||
stripped = append(stripped, '\n')
|
||||
}
|
||||
}
|
||||
return stripped
|
||||
}
|
||||
|
||||
// ScaleClientFunc provides a ScalesGetter
|
||||
type ScaleClientFunc func(genericclioptions.RESTClientGetter) (scale.ScalesGetter, error)
|
||||
|
||||
// ScaleClientFn gives a way to easily override the function for unit testing if needed.
|
||||
var ScaleClientFn ScaleClientFunc = scaleClient
|
||||
|
||||
// scaleClient gives you back scale getter
|
||||
func scaleClient(restClientGetter genericclioptions.RESTClientGetter) (scale.ScalesGetter, error) {
|
||||
discoveryClient, err := restClientGetter.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientConfig, err := restClientGetter.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setKubernetesDefaults(clientConfig)
|
||||
restClient, err := rest.RESTClientFor(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolver := scale.NewDiscoveryScaleKindResolver(discoveryClient)
|
||||
mapper, err := restClientGetter.ToRESTMapper()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return scale.New(restClient, mapper, dynamic.LegacyAPIPathResolverFunc, resolver), nil
|
||||
}
|
||||
|
||||
func Warning(cmdErr io.Writer, newGeneratorName, oldGeneratorName string) {
|
||||
fmt.Fprintf(cmdErr, "WARNING: New generator %q specified, "+
|
||||
"but it isn't available. "+
|
||||
"Falling back to %q.\n",
|
||||
newGeneratorName,
|
||||
oldGeneratorName,
|
||||
)
|
||||
}
|
||||
|
||||
// Difference removes any elements of subArray from fullArray and returns the result
|
||||
func Difference(fullArray []string, subArray []string) []string {
|
||||
exclude := make(map[string]bool, len(subArray))
|
||||
for _, elem := range subArray {
|
||||
exclude[elem] = true
|
||||
}
|
||||
var result []string
|
||||
for _, elem := range fullArray {
|
||||
if _, found := exclude[elem]; !found {
|
||||
result = append(result, elem)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
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 (
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/component-base/version"
|
||||
)
|
||||
|
||||
const (
|
||||
flagMatchBinaryVersion = "match-server-version"
|
||||
)
|
||||
|
||||
// MatchVersionFlags is for setting the "match server version" function.
|
||||
type MatchVersionFlags struct {
|
||||
Delegate genericclioptions.RESTClientGetter
|
||||
|
||||
RequireMatchedServerVersion bool
|
||||
checkServerVersion sync.Once
|
||||
matchesServerVersionErr error
|
||||
}
|
||||
|
||||
var _ genericclioptions.RESTClientGetter = &MatchVersionFlags{}
|
||||
|
||||
func (f *MatchVersionFlags) checkMatchingServerVersion() error {
|
||||
f.checkServerVersion.Do(func() {
|
||||
if !f.RequireMatchedServerVersion {
|
||||
return
|
||||
}
|
||||
discoveryClient, err := f.Delegate.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
f.matchesServerVersionErr = err
|
||||
return
|
||||
}
|
||||
f.matchesServerVersionErr = discovery.MatchesServerVersion(version.Get(), discoveryClient)
|
||||
})
|
||||
|
||||
return f.matchesServerVersionErr
|
||||
}
|
||||
|
||||
// ToRESTConfig implements RESTClientGetter.
|
||||
// Returns a REST client configuration based on a provided path
|
||||
// to a .kubeconfig file, loading rules, and config flag overrides.
|
||||
// Expects the AddFlags method to have been called.
|
||||
func (f *MatchVersionFlags) ToRESTConfig() (*rest.Config, error) {
|
||||
if err := f.checkMatchingServerVersion(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientConfig, err := f.Delegate.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO we should not have to do this. It smacks of something going wrong.
|
||||
setKubernetesDefaults(clientConfig)
|
||||
return clientConfig, nil
|
||||
}
|
||||
|
||||
func (f *MatchVersionFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
return f.Delegate.ToRawKubeConfigLoader()
|
||||
}
|
||||
|
||||
func (f *MatchVersionFlags) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
if err := f.checkMatchingServerVersion(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Delegate.ToDiscoveryClient()
|
||||
}
|
||||
|
||||
// ToRESTMapper returns a mapper.
|
||||
func (f *MatchVersionFlags) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
if err := f.checkMatchingServerVersion(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Delegate.ToRESTMapper()
|
||||
}
|
||||
|
||||
func (f *MatchVersionFlags) AddFlags(flags *pflag.FlagSet) {
|
||||
flags.BoolVar(&f.RequireMatchedServerVersion, flagMatchBinaryVersion, f.RequireMatchedServerVersion, "Require server version to match client version")
|
||||
}
|
||||
|
||||
func NewMatchVersionFlags(delegate genericclioptions.RESTClientGetter) *MatchVersionFlags {
|
||||
return &MatchVersionFlags{
|
||||
Delegate: delegate,
|
||||
}
|
||||
}
|
||||
|
||||
// setKubernetesDefaults sets default values on the provided client config for accessing the
|
||||
// Kubernetes API or returns an error if any of the defaults are impossible or invalid.
|
||||
// TODO this isn't what we want. Each clientset should be setting defaults as it sees fit.
|
||||
func setKubernetesDefaults(config *rest.Config) error {
|
||||
// TODO remove this hack. This is allowing the GetOptions to be serialized.
|
||||
config.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"}
|
||||
|
||||
if config.APIPath == "" {
|
||||
config.APIPath = "/api"
|
||||
}
|
||||
if config.NegotiatedSerializer == nil {
|
||||
// This codec factory ensures the resources are not converted. Therefore, resources
|
||||
// will not be round-tripped through internal versions. Defaulting does not happen
|
||||
// on the client.
|
||||
config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
|
||||
}
|
||||
return rest.SetKubernetesDefaults(config)
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2021 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"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
)
|
||||
|
||||
type OverrideType string
|
||||
|
||||
const (
|
||||
// OverrideTypeJSON will use an RFC6902 JSON Patch to alter the generated output
|
||||
OverrideTypeJSON OverrideType = "json"
|
||||
|
||||
// OverrideTypeMerge will use an RFC7396 JSON Merge Patch to alter the generated output
|
||||
OverrideTypeMerge OverrideType = "merge"
|
||||
|
||||
// OverrideTypeStrategic will use a Strategic Merge Patch to alter the generated output
|
||||
OverrideTypeStrategic OverrideType = "strategic"
|
||||
)
|
||||
|
||||
const DefaultOverrideType = OverrideTypeMerge
|
||||
|
||||
type OverrideOptions struct {
|
||||
Overrides string
|
||||
OverrideType OverrideType
|
||||
}
|
||||
|
||||
func (o *OverrideOptions) AddOverrideFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&o.Overrides, "overrides", "", i18n.T("An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field."))
|
||||
cmd.Flags().StringVar((*string)(&o.OverrideType), "override-type", string(DefaultOverrideType), fmt.Sprintf("The method used to override the generated object: %s, %s, or %s.", OverrideTypeJSON, OverrideTypeMerge, OverrideTypeStrategic))
|
||||
}
|
||||
|
||||
func (o *OverrideOptions) NewOverrider(dataStruct runtime.Object) *Overrider {
|
||||
return &Overrider{
|
||||
Options: o,
|
||||
DataStruct: dataStruct,
|
||||
}
|
||||
}
|
||||
|
||||
type Overrider struct {
|
||||
Options *OverrideOptions
|
||||
DataStruct runtime.Object
|
||||
}
|
||||
|
||||
func (o *Overrider) Apply(obj runtime.Object) (runtime.Object, error) {
|
||||
if len(o.Options.Overrides) == 0 {
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...))
|
||||
|
||||
var overrideType OverrideType
|
||||
if len(o.Options.OverrideType) == 0 {
|
||||
overrideType = DefaultOverrideType
|
||||
} else {
|
||||
overrideType = o.Options.OverrideType
|
||||
}
|
||||
|
||||
switch overrideType {
|
||||
case OverrideTypeJSON:
|
||||
return JSONPatch(codec, obj, o.Options.Overrides)
|
||||
case OverrideTypeMerge:
|
||||
return Merge(codec, obj, o.Options.Overrides)
|
||||
case OverrideTypeStrategic:
|
||||
return StrategicMerge(codec, obj, o.Options.Overrides, o.DataStruct)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid override type: %v", overrideType)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// SuggestAPIResources returns a suggestion to use the "api-resources" command
|
||||
// to retrieve a supported list of resources
|
||||
func SuggestAPIResources(parent string) string {
|
||||
return templates.LongDesc(fmt.Sprintf("Use \"%s api-resources\" for a complete list of supported resources.", parent))
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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 scheme
|
||||
|
||||
import (
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
|
||||
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
imagepolicyv1alpha1 "k8s.io/api/imagepolicy/v1alpha1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
policyv1 "k8s.io/api/policy/v1"
|
||||
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
rbacv1alpha1 "k8s.io/api/rbac/v1alpha1"
|
||||
rbacv1beta1 "k8s.io/api/rbac/v1beta1"
|
||||
schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
storagev1alpha1 "k8s.io/api/storage/v1alpha1"
|
||||
storagev1beta1 "k8s.io/api/storage/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
// Register all groups in the kubectl's registry, but no componentconfig group since it's not in k8s.io/api
|
||||
// The code in this file mostly duplicate the install under k8s.io/kubernetes/pkg/api and k8s.io/kubernetes/pkg/apis,
|
||||
// but does NOT register the internal types.
|
||||
func init() {
|
||||
// Register external types for Scheme
|
||||
metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
|
||||
utilruntime.Must(metav1beta1.AddMetaToScheme(Scheme))
|
||||
utilruntime.Must(metav1.AddMetaToScheme(Scheme))
|
||||
utilruntime.Must(scheme.AddToScheme(Scheme))
|
||||
|
||||
utilruntime.Must(Scheme.SetVersionPriority(corev1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(admissionv1beta1.SchemeGroupVersion, admissionv1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(admissionregistrationv1beta1.SchemeGroupVersion, admissionregistrationv1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(appsv1beta1.SchemeGroupVersion, appsv1beta2.SchemeGroupVersion, appsv1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(authenticationv1.SchemeGroupVersion, authenticationv1beta1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(authorizationv1.SchemeGroupVersion, authorizationv1beta1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(autoscalingv1.SchemeGroupVersion, autoscalingv2.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(batchv1.SchemeGroupVersion, batchv1beta1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(certificatesv1.SchemeGroupVersion, certificatesv1beta1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(extensionsv1beta1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(imagepolicyv1alpha1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(networkingv1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(policyv1beta1.SchemeGroupVersion, policyv1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(rbacv1.SchemeGroupVersion, rbacv1beta1.SchemeGroupVersion, rbacv1alpha1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(schedulingv1alpha1.SchemeGroupVersion))
|
||||
utilruntime.Must(Scheme.SetVersionPriority(storagev1.SchemeGroupVersion, storagev1beta1.SchemeGroupVersion, storagev1alpha1.SchemeGroupVersion))
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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 scheme
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
)
|
||||
|
||||
// All kubectl code should eventually switch to use this Registry and Scheme instead of the global ones.
|
||||
|
||||
// Scheme is the default instance of runtime.Scheme to which types in the Kubernetes API are already registered.
|
||||
var Scheme = runtime.NewScheme()
|
||||
|
||||
// Codecs provides access to encoding and decoding for the scheme
|
||||
var Codecs = serializer.NewCodecFactory(Scheme)
|
||||
|
||||
// ParameterCodec handles versioning of objects that are converted to query parameters.
|
||||
var ParameterCodec = runtime.NewParameterCodec(Scheme)
|
||||
|
||||
// DefaultJSONEncoder returns a default encoder for our scheme
|
||||
func DefaultJSONEncoder() runtime.Encoder {
|
||||
return unstructured.NewJSONFallbackEncoder(Codecs.LegacyCodec(Scheme.PrioritizedVersionsAllGroups()...))
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
Copyright 2016 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 i18n
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/chai2010/gettext-go"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
//go:embed translations
|
||||
var translations embed.FS
|
||||
|
||||
var knownTranslations = map[string][]string{
|
||||
"kubectl": {
|
||||
"default",
|
||||
"en_US",
|
||||
"fr_FR",
|
||||
"zh_CN",
|
||||
"ja_JP",
|
||||
"zh_TW",
|
||||
"it_IT",
|
||||
"de_DE",
|
||||
"ko_KR",
|
||||
"pt_BR",
|
||||
},
|
||||
// only used for unit tests.
|
||||
"test": {
|
||||
"default",
|
||||
"en_US",
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
lazyLoadTranslationsOnce sync.Once
|
||||
LoadTranslationsFunc = func() error {
|
||||
return LoadTranslations("kubectl", nil)
|
||||
}
|
||||
translationsLoaded bool
|
||||
)
|
||||
|
||||
// SetLoadTranslationsFunc sets the function called to lazy load translations.
|
||||
// It must be called in an init() func that occurs BEFORE any i18n.T() calls are made by any package. You can
|
||||
// accomplish this by creating a separate package containing your init() func, and then importing that package BEFORE
|
||||
// any other packages that call i18n.T().
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// package myi18n
|
||||
//
|
||||
// import "k8s.io/kubectl/pkg/util/i18n"
|
||||
//
|
||||
// func init() {
|
||||
// if err := i18n.SetLoadTranslationsFunc(loadCustomTranslations); err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func loadCustomTranslations() error {
|
||||
// // Load your custom translations here...
|
||||
// }
|
||||
//
|
||||
// And then in your main or root command package, import your custom package like this:
|
||||
//
|
||||
// import (
|
||||
// // Other imports that don't need i18n...
|
||||
// _ "example.com/myapp/myi18n"
|
||||
// // Other imports that do need i18n...
|
||||
// )
|
||||
func SetLoadTranslationsFunc(f func() error) error {
|
||||
if translationsLoaded {
|
||||
return errors.New("translations have already been loaded")
|
||||
}
|
||||
LoadTranslationsFunc = func() error {
|
||||
if err := f(); err != nil {
|
||||
return err
|
||||
}
|
||||
translationsLoaded = true
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadSystemLanguage() string {
|
||||
// Implements the following locale priority order: LC_ALL, LC_MESSAGES, LANG
|
||||
// Similarly to: https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
|
||||
langStr := os.Getenv("LC_ALL")
|
||||
if langStr == "" {
|
||||
langStr = os.Getenv("LC_MESSAGES")
|
||||
}
|
||||
if langStr == "" {
|
||||
langStr = os.Getenv("LANG")
|
||||
}
|
||||
|
||||
if langStr == "" {
|
||||
klog.V(3).Infof("Couldn't find the LC_ALL, LC_MESSAGES or LANG environment variables, defaulting to en_US")
|
||||
return "default"
|
||||
}
|
||||
pieces := strings.Split(langStr, ".")
|
||||
if len(pieces) != 2 {
|
||||
klog.V(3).Infof("Unexpected system language (%s), defaulting to en_US", langStr)
|
||||
return "default"
|
||||
}
|
||||
return pieces[0]
|
||||
}
|
||||
|
||||
func findLanguage(root string, getLanguageFn func() string) string {
|
||||
langStr := getLanguageFn()
|
||||
|
||||
translations := knownTranslations[root]
|
||||
for ix := range translations {
|
||||
if translations[ix] == langStr {
|
||||
return langStr
|
||||
}
|
||||
}
|
||||
klog.V(3).Infof("Couldn't find translations for %s, using default", langStr)
|
||||
return "default"
|
||||
}
|
||||
|
||||
// LoadTranslations loads translation files. getLanguageFn should return a language
|
||||
// string (e.g. 'en-US'). If getLanguageFn is nil, then the loadSystemLanguage function
|
||||
// is used, which uses the 'LANG' environment variable.
|
||||
func LoadTranslations(root string, getLanguageFn func() string) error {
|
||||
if getLanguageFn == nil {
|
||||
getLanguageFn = loadSystemLanguage
|
||||
}
|
||||
|
||||
langStr := findLanguage(root, getLanguageFn)
|
||||
translationFiles := []string{
|
||||
fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.po", root, langStr),
|
||||
fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.mo", root, langStr),
|
||||
}
|
||||
|
||||
// TODO: list the directory and load all files.
|
||||
buf := new(bytes.Buffer)
|
||||
w := zip.NewWriter(buf)
|
||||
|
||||
// Make sure to check the error on Close.
|
||||
for _, file := range translationFiles {
|
||||
filename := "translations/" + file
|
||||
f, err := w.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := translations.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write(data); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
gettext.BindLocale(gettext.New("k8s", root+".zip", buf.Bytes()))
|
||||
gettext.SetDomain("k8s")
|
||||
gettext.SetLanguage(langStr)
|
||||
translationsLoaded = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func lazyLoadTranslations() {
|
||||
lazyLoadTranslationsOnce.Do(func() {
|
||||
if translationsLoaded {
|
||||
return
|
||||
}
|
||||
if err := LoadTranslationsFunc(); err != nil {
|
||||
klog.Warning("Failed to load translations")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// T translates a string, possibly substituting arguments into it along
|
||||
// the way. If len(args) is > 0, args1 is assumed to be the plural value
|
||||
// and plural translation is used.
|
||||
func T(defaultValue string, args ...int) string {
|
||||
lazyLoadTranslations()
|
||||
if len(args) == 0 {
|
||||
return gettext.PGettext("", defaultValue)
|
||||
}
|
||||
return fmt.Sprintf(gettext.PNGettext("", defaultValue, defaultValue+".plural", args[0]),
|
||||
args[0])
|
||||
}
|
||||
|
||||
// Errorf produces an error with a translated error string.
|
||||
// Substitution is performed via the `T` function above, following
|
||||
// the same rules.
|
||||
func Errorf(defaultValue string, args ...int) error {
|
||||
return errors.New(T(defaultValue, args...))
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
reviewers: []
|
||||
approvers:
|
||||
- sig-cli-maintainers
|
||||
emeritus_approvers:
|
||||
- brendandburns
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
# Translations README
|
||||
|
||||
This is a basic sketch of the workflow needed to add translations:
|
||||
|
||||
# Adding/Updating Translations
|
||||
|
||||
## New languages
|
||||
Create `staging/src/k8s.io/kubectl/pkg/util/i18n/translations/kubectl/<language>/LC_MESSAGES/k8s.po`. There's
|
||||
no need to update `translations/test/...` which is only used for unit tests.
|
||||
|
||||
There is an example [PR here](https://github.com/kubernetes/kubernetes/pull/40645) which adds support for French.
|
||||
|
||||
Once you've added a new language, you'll need to register it in
|
||||
`staging/src/k8s.io/kubectl/pkg/util/i18n/i18n.go` by adding it to the `knownTranslations` map.
|
||||
|
||||
## Wrapping strings
|
||||
There is a simple script in `staging/src/k8s.io/kubectl/pkg/util/i18n/translations/extract.py` that performs
|
||||
simple regular expression based wrapping of strings. It can always
|
||||
use improvements to understand additional strings.
|
||||
|
||||
## Extracting strings
|
||||
Once the strings are wrapped, you can extract strings from go files using
|
||||
the `go-xgettext` command which can be installed with:
|
||||
|
||||
```console
|
||||
go get github.com/gosexy/gettext/go-xgettext
|
||||
```
|
||||
|
||||
Once that's installed you can run `./hack/update-translations.sh`, which
|
||||
will extract and sort any new strings.
|
||||
|
||||
## Adding new translations
|
||||
Edit the appropriate `k8s.po` file, `poedit` is a popular open source tool
|
||||
for translations. You can load the `staging/src/k8s.io/kubectl/pkg/util/i18n/translations/kubectl/template.pot` file
|
||||
to find messages that might be missing.
|
||||
|
||||
Once you are done with your `k8s.po` file, generate the corresponding `k8s.mo`
|
||||
file. `poedit` does this automatically on save, but you can also run
|
||||
`./hack/update-translations.sh` to perform the `po` to `mo` translation.
|
||||
|
||||
We use the English translation as the `msgid`.
|
||||
|
||||
## Regenerating the bindata file
|
||||
|
||||
> Note: Regeneration of bindata is no more necessary for Kubernetes 1.22+ as
|
||||
> the translations are now embedded into the binary at compile time.
|
||||
> See: https://github.com/kubernetes/kubernetes/pull/99829
|
||||
|
||||
With the `mo` files up to date, you can now convert the generated files
|
||||
into code using `go-bindata` command which can be installed with:
|
||||
|
||||
```console
|
||||
go get github.com/go-bindata/go-bindata/...
|
||||
```
|
||||
|
||||
Run `./hack/generate-bindata.sh`, this will turn the translation files
|
||||
into generated code which will in turn be packaged into the Kubernetes
|
||||
binaries.
|
||||
|
||||
## Extracting strings
|
||||
|
||||
There is a script in `staging/src/k8s.io/kubectl/pkg/util/i18n/translations/extract.py` that knows how to do some
|
||||
simple extraction. It needs a lot of work.
|
||||
|
||||
# Using translations
|
||||
|
||||
To use translations, you simply need to add:
|
||||
```go
|
||||
import pkg/i18n
|
||||
...
|
||||
// Get a translated string
|
||||
translated := i18n.T("Your message in english here")
|
||||
|
||||
// Get a translated plural string
|
||||
translated := i18n.T("You had %d items", items)
|
||||
|
||||
// Translated error
|
||||
return i18n.Error("Something bad happened")
|
||||
|
||||
// Translated plural error
|
||||
return i18n.Error("%d bad things happened")
|
||||
```
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# 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.
|
||||
|
||||
"""Extract strings from command files and externalize into translation files.
|
||||
Expects to be run from the root directory of the repository.
|
||||
|
||||
Usage:
|
||||
extract.py pkg/kubectl/cmd/apply.go
|
||||
|
||||
"""
|
||||
import fileinput
|
||||
import sys
|
||||
import re
|
||||
|
||||
class MatchHandler(object):
|
||||
""" Simple holder for a regular expression and a function
|
||||
to run if that regular expression matches a line.
|
||||
The function should expect (re.match, file, linenumber) as parameters
|
||||
"""
|
||||
def __init__(self, regex, replace_fn):
|
||||
self.regex = re.compile(regex)
|
||||
self.replace_fn = replace_fn
|
||||
|
||||
def short_replace(match, file, line_number):
|
||||
"""Replace a Short: ... cobra command description with an internationalization
|
||||
"""
|
||||
sys.stdout.write('{}i18n.T({}),\n'.format(match.group(1), match.group(2)))
|
||||
|
||||
SHORT_MATCH = MatchHandler(r'(\s+Short:\s+)("[^"]+"),', short_replace)
|
||||
|
||||
def import_replace(match, file, line_number):
|
||||
"""Add an extra import for the i18n library.
|
||||
Doesn't try to be smart and detect if it's already present, assumes a
|
||||
gofmt round wil fix things.
|
||||
"""
|
||||
sys.stdout.write('{}\n"k8s.io/kubectl/pkg/util/i18n"\n'.format(match.group(1)))
|
||||
|
||||
IMPORT_MATCH = MatchHandler('(.*"k8s.io/kubectl/pkg/cmd/util")', import_replace)
|
||||
|
||||
|
||||
def string_flag_replace(match, file, line_number):
|
||||
"""Replace a cmd.Flags().String("...", "", "...") with an internationalization
|
||||
"""
|
||||
sys.stdout.write('{}i18n.T("{})"))\n'.format(match.group(1), match.group(2)))
|
||||
|
||||
STRING_FLAG_MATCH = MatchHandler('(\s+cmd\.Flags\(\).String\("[^"]*", "[^"]*", )"([^"]*)"\)', string_flag_replace)
|
||||
|
||||
|
||||
def long_string_replace(match, file, line_number):
|
||||
return '{}i18n.T({}){}'.format(match.group(1), match.group(2), match.group(3))
|
||||
|
||||
LONG_DESC_MATCH = MatchHandler('(LongDesc\()(`[^`]+`)([^\n]\n)', long_string_replace)
|
||||
|
||||
EXAMPLE_MATCH = MatchHandler('(Examples\()(`[^`]+`)([^\n]\n)', long_string_replace)
|
||||
|
||||
def replace(filename, matchers, multiline_matchers):
|
||||
"""Given a file and a set of matchers, run those matchers
|
||||
across the file and replace it with the results.
|
||||
"""
|
||||
# Run all the matchers
|
||||
line_number = 0
|
||||
for line in fileinput.input(filename, inplace=True):
|
||||
line_number += 1
|
||||
matched = False
|
||||
for matcher in matchers:
|
||||
match = matcher.regex.match(line)
|
||||
if match:
|
||||
matcher.replace_fn(match, filename, line_number)
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
with open(filename, 'r') as datafile:
|
||||
content = datafile.read()
|
||||
for matcher in multiline_matchers:
|
||||
match = matcher.regex.search(content)
|
||||
while match:
|
||||
rep = matcher.replace_fn(match, filename, 0)
|
||||
# Escape back references in the replacement string
|
||||
# (And escape for Python)
|
||||
# (And escape for regex)
|
||||
rep = re.sub('\\\\(\\d)', '\\\\\\\\\\1', rep)
|
||||
content = matcher.regex.sub(rep, content, 1)
|
||||
match = matcher.regex.search(content)
|
||||
sys.stdout.write(content)
|
||||
|
||||
# gofmt the file again
|
||||
from subprocess import call
|
||||
call(["goimports", "-w", filename])
|
||||
|
||||
replace(sys.argv[1], [SHORT_MATCH, IMPORT_MATCH, STRING_FLAG_MATCH], [LONG_DESC_MATCH, EXAMPLE_MATCH])
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- sig-cli-maintainers
|
||||
reviewers:
|
||||
- sig-cli-reviewers
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+2920
File diff suppressed because it is too large
Load Diff
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+5077
File diff suppressed because it is too large
Load Diff
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+5077
File diff suppressed because it is too large
Load Diff
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
# Test translations for unit tests.
|
||||
# Copyright (C) 2016
|
||||
# This file is distributed under the same license as the Kubernetes package.
|
||||
# FIRST AUTHOR brendan.d.burns@gmail.com, 2016.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gettext-go-examples-hello\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL\n"
|
||||
"POT-Creation-Date: 2021-07-07 20:15+0200\n"
|
||||
"PO-Revision-Date: 2017-01-29 22:54-0800\n"
|
||||
"Last-Translator: Brendan Burns <brendan.d.burns@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 1.6.10\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/delete_cluster.go#L38
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster.go:42
|
||||
msgid "Delete the specified cluster from the kubeconfig"
|
||||
msgstr "Supprimer le cluster spécifié du kubeconfig"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/delete_context.go#L38
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context.go:42
|
||||
msgid "Delete the specified context from the kubeconfig"
|
||||
msgstr "Supprimer le contexte spécifié du kubeconfig"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/get_contexts.go#L62
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go:72
|
||||
msgid "Describe one or many contexts"
|
||||
msgstr "Décrire un ou plusieurs contextes"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/get_clusters.go#L40
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters.go:41
|
||||
msgid "Display clusters defined in the kubeconfig"
|
||||
msgstr "Afficher les cluster définis dans kubeconfig"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/view.go#L64
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/view.go:81
|
||||
msgid "Display merged kubeconfig settings or a specified kubeconfig file"
|
||||
msgstr ""
|
||||
"Afficher les paramètres fusionnés de kubeconfig ou d'un fichier kubeconfig "
|
||||
"spécifié"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/config.go#L39
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/config.go:42
|
||||
msgid "Modify kubeconfig files"
|
||||
msgstr "Modifier des fichiers kubeconfig"
|
||||
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate.go:135
|
||||
msgid "Update the annotations on a resource"
|
||||
msgstr "Mettre à jour les annotations d'une ressource"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/apply.go#L98
|
||||
#~ msgid "Apply a configuration to a resource by filename or stdin"
|
||||
#~ msgstr ""
|
||||
#~ "Appliquer une configuration à une ressource par nom de fichier ou depuis "
|
||||
#~ "stdin"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/current_context.go#L48
|
||||
#~ msgid "Displays the current-context"
|
||||
#~ msgstr "Affiche le contexte actuel"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_cluster.go#L67
|
||||
#~ msgid "Sets a cluster entry in kubeconfig"
|
||||
#~ msgstr "Définit un cluster dans kubeconfig"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_context.go#L57
|
||||
#~ msgid "Sets a context entry in kubeconfig"
|
||||
#~ msgstr "Définit un contexte dans kubeconfig"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_authinfo.go#L103
|
||||
#~ msgid "Sets a user entry in kubeconfig"
|
||||
#~ msgstr "Définit un utilisateur dans kubeconfig"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/set.go#L59
|
||||
#~ msgid "Sets an individual value in a kubeconfig file"
|
||||
#~ msgstr "Définit une valeur individuelle dans un fichier kubeconfig"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/use_context.go#L48
|
||||
#~ msgid "Sets the current-context in a kubeconfig file"
|
||||
#~ msgstr "Définit le contexte courant dans un fichier kubeconfig"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/unset.go#L47
|
||||
#~ msgid "Unsets an individual value in a kubeconfig file"
|
||||
#~ msgstr "Supprime une valeur individuelle dans un fichier kubeconfig"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "watch is only supported on individual resources and resource collections "
|
||||
#~ "- %d resources were found"
|
||||
#~ msgid_plural ""
|
||||
#~ "watch is only supported on individual resources and resource collections "
|
||||
#~ "- %d resources were found"
|
||||
#~ msgstr[0] ""
|
||||
#~ "watch n'est compatible qu'avec les ressources individuelles et les "
|
||||
#~ "collections de ressources. - %d ressource a été trouvée. "
|
||||
#~ msgstr[1] ""
|
||||
#~ "watch n'est compatible qu'avec les ressources individuelles et les "
|
||||
#~ "collections de ressources. - %d ressources ont été trouvées. "
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+3249
File diff suppressed because it is too large
Load Diff
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+3365
File diff suppressed because it is too large
Load Diff
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
# Test translations for unit tests.
|
||||
# Copyright (C) 2017
|
||||
# This file is distributed under the same license as the Kubernetes package.
|
||||
# FIRST AUTHOR ianyrchoi@gmail.com, 2018.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gettext-go-examples-hello\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL\n"
|
||||
"POT-Creation-Date: 2021-07-07 20:15+0200\n"
|
||||
"PO-Revision-Date: 2018-04-03 06:05+0900\n"
|
||||
"Last-Translator: Ian Y. Choi <ianyrchoi@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: ko_KR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/delete_cluster.go#L38
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster.go:42
|
||||
msgid "Delete the specified cluster from the kubeconfig"
|
||||
msgstr "kubeconfig에서 지정된 클러스터를 삭제합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/delete_context.go#L38
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context.go:42
|
||||
msgid "Delete the specified context from the kubeconfig"
|
||||
msgstr "kubeconfig에서 지정된 컨텍스트를 삭제합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/get_contexts.go#L62
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go:72
|
||||
msgid "Describe one or many contexts"
|
||||
msgstr "하나 또는 여러 컨텍스트를 설명합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/get_clusters.go#L40
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters.go:41
|
||||
msgid "Display clusters defined in the kubeconfig"
|
||||
msgstr "kubeconfig에 정의된 클러스터를 표시합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/view.go#L64
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/view.go:81
|
||||
msgid "Display merged kubeconfig settings or a specified kubeconfig file"
|
||||
msgstr "병합된 kubeconfig 설정 또는 지정된 kubeconfig 파일을 표시합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/config.go#L39
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/config.go:42
|
||||
msgid "Modify kubeconfig files"
|
||||
msgstr "kubeconfig 파일을 수정합니다"
|
||||
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate.go:135
|
||||
msgid "Update the annotations on a resource"
|
||||
msgstr "자원에 대한 주석을 업데이트합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/masterpkg/kubectl/cmd/apply.go#L98
|
||||
#~ msgid "Apply a configuration to a resource by filename or stdin"
|
||||
#~ msgstr "구성을 파일 이름 또는 stdin에 의한 자원에 적용합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/current_context.go#L48
|
||||
#~ msgid "Displays the current-context"
|
||||
#~ msgstr "현재-컨텍스트를 표시합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_cluster.go#L67
|
||||
#~ msgid "Sets a cluster entry in kubeconfig"
|
||||
#~ msgstr "kubeconfig에서 클러스터 항목을 설정합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_context.go#L57
|
||||
#~ msgid "Sets a context entry in kubeconfig"
|
||||
#~ msgstr "kubeconfig에서 컨텍스트 항목을 설정합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/create_authinfo.go#L103
|
||||
#~ msgid "Sets a user entry in kubeconfig"
|
||||
#~ msgstr "kubeconfig에서 사용자 항목을 설정합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/set.go#L59
|
||||
#~ msgid "Sets an individual value in a kubeconfig file"
|
||||
#~ msgstr "kubeconfig 파일에서 단일값을 설정합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/use_context.go#L48
|
||||
#~ msgid "Sets the current-context in a kubeconfig file"
|
||||
#~ msgstr "kubeconfig 파일에서 현재-컨텍스트를 설정합니다"
|
||||
|
||||
# https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/config/unset.go#L47
|
||||
#~ msgid "Unsets an individual value in a kubeconfig file"
|
||||
#~ msgstr "kubeconfig 파일에서 단일값 설정을 해제합니다"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "watch is only supported on individual resources and resource collections "
|
||||
#~ "- %d resources were found"
|
||||
#~ msgid_plural ""
|
||||
#~ "watch is only supported on individual resources and resource collections "
|
||||
#~ "- %d resources were found"
|
||||
#~ msgstr[0] ""
|
||||
#~ "watch는 단일 리소스와 리소스 모음만을 지원합니다 - %d 개 자원을 발견하였습"
|
||||
#~ "니다"
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+3250
File diff suppressed because it is too large
Load Diff
+3291
File diff suppressed because it is too large
Load Diff
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+3236
File diff suppressed because it is too large
Load Diff
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
# Test translations for unit tests.
|
||||
# Copyright (C) 2017
|
||||
# This file is distributed under the same license as the Kubernetes package.
|
||||
# FIRST AUTHOR warmchang@outlook.com, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: hello-world\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL\n"
|
||||
"POT-Creation-Date: 2021-07-07 20:15+0200\n"
|
||||
"PO-Revision-Date: 2017-06-02 09:13+0800\n"
|
||||
"Last-Translator: William Chang <warmchang@outlook.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: zh\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.2\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster.go:42
|
||||
msgid "Delete the specified cluster from the kubeconfig"
|
||||
msgstr "刪除 kubeconfig 檔案中指定的叢集(cluster)"
|
||||
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context.go:42
|
||||
msgid "Delete the specified context from the kubeconfig"
|
||||
msgstr "刪除 kubeconfig 檔案中指定的 context"
|
||||
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go:72
|
||||
msgid "Describe one or many contexts"
|
||||
msgstr "描述一個或多個 context"
|
||||
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters.go:41
|
||||
msgid "Display clusters defined in the kubeconfig"
|
||||
msgstr "顯示 kubeconfig 檔案中定義的叢集(cluster)"
|
||||
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/view.go:81
|
||||
msgid "Display merged kubeconfig settings or a specified kubeconfig file"
|
||||
msgstr "顯示合併的 kubeconfig 配置或一個指定的 kubeconfig 檔案"
|
||||
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/config/config.go:42
|
||||
msgid "Modify kubeconfig files"
|
||||
msgstr "修改 kubeconfig 檔案"
|
||||
|
||||
#: staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate.go:135
|
||||
msgid "Update the annotations on a resource"
|
||||
msgstr "更新一個資源的注解(annotations)"
|
||||
|
||||
#~ msgid "Apply a configuration to a resource by filename or stdin"
|
||||
#~ msgstr "通過檔案名或標準輸入流(stdin)對資源進行配置"
|
||||
|
||||
#~ msgid "Displays the current-context"
|
||||
#~ msgstr "顯示目前的 context"
|
||||
|
||||
#~ msgid "Sets a cluster entry in kubeconfig"
|
||||
#~ msgstr "設置 kubeconfig 檔案中的一個叢集(cluster)條目"
|
||||
|
||||
#~ msgid "Sets a context entry in kubeconfig"
|
||||
#~ msgstr "設置 kubeconfig 檔案中的一個 context 條目"
|
||||
|
||||
#~ msgid "Sets a user entry in kubeconfig"
|
||||
#~ msgstr "設置 kubeconfig 檔案中的一個使用者條目"
|
||||
|
||||
#~ msgid "Sets an individual value in a kubeconfig file"
|
||||
#~ msgstr "設置 kubeconfig 檔案中的一個值"
|
||||
|
||||
#~ msgid "Sets the current-context in a kubeconfig file"
|
||||
#~ msgstr "設置 kubeconfig 檔案中的目前 context"
|
||||
|
||||
#~ msgid "Unsets an individual value in a kubeconfig file"
|
||||
#~ msgstr "取消設置 kubeconfig 檔案中的一個值"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "watch is only supported on individual resources and resource collections "
|
||||
#~ "- %d resources were found"
|
||||
#~ msgid_plural ""
|
||||
#~ "watch is only supported on individual resources and resource collections "
|
||||
#~ "- %d resources were found"
|
||||
#~ msgstr[0] "一次只能 watch 一個資源或資料集合 - 找到了 %d 個資源"
|
||||
#~ msgstr[1] "一次只能 watch 一個資源或資料集合 - 找到了 %d 個資源"
|
||||
BIN
Binary file not shown.
+28
@@ -0,0 +1,28 @@
|
||||
# Test translations for unit tests.
|
||||
# Copyright (C) 2016
|
||||
# This file is distributed under the same license as the Kubernetes package.
|
||||
# FIRST AUTHOR brendan.d.burns@gmail.com, 2016.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gettext-go-examples-hello\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-12-12 20:03+0000\n"
|
||||
"PO-Revision-Date: 2016-12-13 21:35-0800\n"
|
||||
"Last-Translator: Brendan Burns <brendan.d.burns@gmail.com>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 1.6.10\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: en\n"
|
||||
|
||||
msgid "test_plural"
|
||||
msgid_plural "test_plural"
|
||||
msgstr[0] "there was %d item"
|
||||
msgstr[1] "there were %d items"
|
||||
|
||||
msgid "test_string"
|
||||
msgstr "foo"
|
||||
BIN
Binary file not shown.
+28
@@ -0,0 +1,28 @@
|
||||
# Test translations for unit tests.
|
||||
# Copyright (C) 2016
|
||||
# This file is distributed under the same license as the Kubernetes package.
|
||||
# FIRST AUTHOR brendan.d.burns@gmail.com, 2016.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gettext-go-examples-hello\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-12-12 20:03+0000\n"
|
||||
"PO-Revision-Date: 2016-12-13 22:12-0800\n"
|
||||
"Last-Translator: Brendan Burns <brendan.d.burns@gmail.com>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 1.6.10\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: en\n"
|
||||
|
||||
msgid "test_plural"
|
||||
msgid_plural "test_plural"
|
||||
msgstr[0] "there was %d item"
|
||||
msgstr[1] "there were %d items"
|
||||
|
||||
msgid "test_string"
|
||||
msgstr "baz"
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
Copyright 2016 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 interrupt
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// terminationSignals are signals that cause the program to exit in the
|
||||
// supported platforms (linux, darwin, windows).
|
||||
var terminationSignals = []os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT}
|
||||
|
||||
// Handler guarantees execution of notifications after a critical section (the function passed
|
||||
// to a Run method), even in the presence of process termination. It guarantees exactly once
|
||||
// invocation of the provided notify functions.
|
||||
type Handler struct {
|
||||
notify []func()
|
||||
final func(os.Signal)
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// Chain creates a new handler that invokes all notify functions when the critical section exits
|
||||
// and then invokes the optional handler's notifications. This allows critical sections to be
|
||||
// nested without losing exactly once invocations. Notify functions can invoke any cleanup needed
|
||||
// but should not exit (which is the responsibility of the parent handler).
|
||||
func Chain(handler *Handler, notify ...func()) *Handler {
|
||||
if handler == nil {
|
||||
return New(nil, notify...)
|
||||
}
|
||||
return New(handler.Signal, append(notify, handler.Close)...)
|
||||
}
|
||||
|
||||
// New creates a new handler that guarantees all notify functions are run after the critical
|
||||
// section exits (or is interrupted by the OS), then invokes the final handler. If no final
|
||||
// handler is specified, the default final is `os.Exit(1)`. A handler can only be used for
|
||||
// one critical section.
|
||||
func New(final func(os.Signal), notify ...func()) *Handler {
|
||||
return &Handler{
|
||||
final: final,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
// Close executes all the notification handlers if they have not yet been executed.
|
||||
func (h *Handler) Close() {
|
||||
h.once.Do(func() {
|
||||
for _, fn := range h.notify {
|
||||
fn()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Signal is called when an os.Signal is received, and guarantees that all notifications
|
||||
// are executed, then the final handler is executed. This function should only be called once
|
||||
// per Handler instance.
|
||||
func (h *Handler) Signal(s os.Signal) {
|
||||
h.once.Do(func() {
|
||||
for _, fn := range h.notify {
|
||||
fn()
|
||||
}
|
||||
if h.final == nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
h.final(s)
|
||||
})
|
||||
}
|
||||
|
||||
// Run ensures that any notifications are invoked after the provided fn exits (even if the
|
||||
// process is interrupted by an OS termination signal). Notifications are only invoked once
|
||||
// per Handler instance, so calling Run more than once will not behave as the user expects.
|
||||
func (h *Handler) Run(fn func() error) error {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, terminationSignals...)
|
||||
defer func() {
|
||||
signal.Stop(ch)
|
||||
close(ch)
|
||||
}()
|
||||
go func() {
|
||||
sig, ok := <-ch
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h.Signal(sig)
|
||||
}()
|
||||
defer h.Close()
|
||||
return fn()
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- apelisse
|
||||
reviewers:
|
||||
- apelisse
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
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 openapi is a collection of libraries for fetching the openapi spec
|
||||
// from a Kubernetes server and then indexing the type definitions.
|
||||
// The openapi spec contains the object model definitions and extensions metadata
|
||||
// such as the patchStrategy and patchMergeKey for creating patches.
|
||||
package openapi
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
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 openapi
|
||||
|
||||
import (
|
||||
openapi_v2 "github.com/google/gnostic-models/openapiv2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// OpenAPIResourcesGetter represents a function to return
|
||||
// OpenAPI V2 resource specifications. Used for lazy-loading
|
||||
// these resource specifications.
|
||||
type OpenAPIResourcesGetter interface {
|
||||
OpenAPISchema() (Resources, error)
|
||||
}
|
||||
|
||||
// Resources interface describe a resources provider, that can give you
|
||||
// resource based on group-version-kind.
|
||||
type Resources interface {
|
||||
LookupResource(gvk schema.GroupVersionKind) proto.Schema
|
||||
GetConsumes(gvk schema.GroupVersionKind, operation string) []string
|
||||
}
|
||||
|
||||
// groupVersionKindExtensionKey is the key used to lookup the
|
||||
// GroupVersionKind value for an object definition from the
|
||||
// definition's "extensions" map.
|
||||
const groupVersionKindExtensionKey = "x-kubernetes-group-version-kind"
|
||||
|
||||
// document is an implementation of `Resources`. It looks for
|
||||
// resources in an openapi Schema.
|
||||
type document struct {
|
||||
// Maps gvk to model name
|
||||
resources map[schema.GroupVersionKind]string
|
||||
models proto.Models
|
||||
doc *openapi_v2.Document
|
||||
}
|
||||
|
||||
var _ Resources = &document{}
|
||||
|
||||
// NewOpenAPIData creates a new `Resources` out of the openapi document
|
||||
func NewOpenAPIData(doc *openapi_v2.Document) (Resources, error) {
|
||||
models, err := proto.NewOpenAPIData(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resources := map[schema.GroupVersionKind]string{}
|
||||
for _, modelName := range models.ListModels() {
|
||||
model := models.LookupModel(modelName)
|
||||
if model == nil {
|
||||
panic("ListModels returns a model that can't be looked-up.")
|
||||
}
|
||||
gvkList := parseGroupVersionKind(model)
|
||||
for _, gvk := range gvkList {
|
||||
if len(gvk.Kind) > 0 {
|
||||
resources[gvk] = modelName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &document{
|
||||
resources: resources,
|
||||
models: models,
|
||||
doc: doc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *document) LookupResource(gvk schema.GroupVersionKind) proto.Schema {
|
||||
modelName, found := d.resources[gvk]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return d.models.LookupModel(modelName)
|
||||
}
|
||||
|
||||
func (d *document) GetConsumes(gvk schema.GroupVersionKind, operation string) []string {
|
||||
for _, path := range d.doc.GetPaths().GetPath() {
|
||||
for _, ex := range path.GetValue().GetPatch().GetVendorExtension() {
|
||||
if ex.GetValue().GetYaml() == "" ||
|
||||
ex.GetName() != "x-kubernetes-group-version-kind" {
|
||||
continue
|
||||
}
|
||||
|
||||
var value map[string]string
|
||||
err := yaml.Unmarshal([]byte(ex.GetValue().GetYaml()), &value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if value["group"] == gvk.Group && value["kind"] == gvk.Kind && value["version"] == gvk.Version {
|
||||
switch operation {
|
||||
case "GET":
|
||||
return path.GetValue().GetGet().GetConsumes()
|
||||
case "PATCH":
|
||||
return path.GetValue().GetPatch().GetConsumes()
|
||||
case "HEAD":
|
||||
return path.GetValue().GetHead().GetConsumes()
|
||||
case "PUT":
|
||||
return path.GetValue().GetPut().GetConsumes()
|
||||
case "POST":
|
||||
return path.GetValue().GetPost().GetConsumes()
|
||||
case "OPTIONS":
|
||||
return path.GetValue().GetOptions().GetConsumes()
|
||||
case "DELETE":
|
||||
return path.GetValue().GetDelete().GetConsumes()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one.
|
||||
func parseGroupVersionKind(s proto.Schema) []schema.GroupVersionKind {
|
||||
extensions := s.GetExtensions()
|
||||
|
||||
gvkListResult := []schema.GroupVersionKind{}
|
||||
|
||||
// Get the extensions
|
||||
gvkExtension, ok := extensions[groupVersionKindExtensionKey]
|
||||
if !ok {
|
||||
return []schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
// gvk extension must be a list of at least 1 element.
|
||||
gvkList, ok := gvkExtension.([]interface{})
|
||||
if !ok {
|
||||
return []schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
for _, gvk := range gvkList {
|
||||
// gvk extension list must be a map with group, version, and
|
||||
// kind fields
|
||||
gvkMap, ok := gvk.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
group, ok := gvkMap["group"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
version, ok := gvkMap["version"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
kind, ok := gvkMap["kind"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
gvkListResult = append(gvkListResult, schema.GroupVersionKind{
|
||||
Group: group,
|
||||
Version: version,
|
||||
Kind: kind,
|
||||
})
|
||||
}
|
||||
|
||||
return gvkListResult
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
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 openapi
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
openapi_v2 "github.com/google/gnostic-models/openapiv2"
|
||||
"k8s.io/client-go/discovery"
|
||||
)
|
||||
|
||||
// CachedOpenAPIGetter fetches the openapi schema once and then caches it in memory
|
||||
type CachedOpenAPIGetter struct {
|
||||
openAPIClient discovery.OpenAPISchemaInterface
|
||||
|
||||
// Cached results
|
||||
sync.Once
|
||||
openAPISchema *openapi_v2.Document
|
||||
err error
|
||||
}
|
||||
|
||||
var _ discovery.OpenAPISchemaInterface = &CachedOpenAPIGetter{}
|
||||
|
||||
// NewOpenAPIGetter returns an object to return OpenAPIDatas which reads
|
||||
// from a server, and then stores in memory for subsequent invocations
|
||||
func NewOpenAPIGetter(openAPIClient discovery.OpenAPISchemaInterface) *CachedOpenAPIGetter {
|
||||
return &CachedOpenAPIGetter{
|
||||
openAPIClient: openAPIClient,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAPISchema implements OpenAPISchemaInterface.
|
||||
func (g *CachedOpenAPIGetter) OpenAPISchema() (*openapi_v2.Document, error) {
|
||||
g.Do(func() {
|
||||
g.openAPISchema, g.err = g.openAPIClient.OpenAPISchema()
|
||||
})
|
||||
|
||||
// Return the saved result.
|
||||
return g.openAPISchema, g.err
|
||||
}
|
||||
|
||||
type CachedOpenAPIParser struct {
|
||||
openAPIClient discovery.OpenAPISchemaInterface
|
||||
|
||||
// Cached results
|
||||
sync.Once
|
||||
openAPIResources Resources
|
||||
err error
|
||||
}
|
||||
|
||||
func NewOpenAPIParser(openAPIClient discovery.OpenAPISchemaInterface) *CachedOpenAPIParser {
|
||||
return &CachedOpenAPIParser{
|
||||
openAPIClient: openAPIClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CachedOpenAPIParser) Parse() (Resources, error) {
|
||||
p.Do(func() {
|
||||
oapi, err := p.openAPIClient.OpenAPISchema()
|
||||
if err != nil {
|
||||
p.err = err
|
||||
return
|
||||
}
|
||||
p.openAPIResources, p.err = NewOpenAPIData(oapi)
|
||||
})
|
||||
|
||||
return p.openAPIResources, p.err
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2016 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 templates
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CommandGroup struct {
|
||||
Message string
|
||||
Commands []*cobra.Command
|
||||
}
|
||||
|
||||
type CommandGroups []CommandGroup
|
||||
|
||||
func (g CommandGroups) Add(c *cobra.Command) {
|
||||
for _, group := range g {
|
||||
c.AddCommand(group.Commands...)
|
||||
}
|
||||
}
|
||||
|
||||
func (g CommandGroups) Has(c *cobra.Command) bool {
|
||||
for _, group := range g {
|
||||
for _, command := range group.Commands {
|
||||
if command == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AddAdditionalCommands(g CommandGroups, message string, cmds []*cobra.Command) CommandGroups {
|
||||
group := CommandGroup{Message: message}
|
||||
for _, c := range cmds {
|
||||
// Don't show commands that have no short description
|
||||
if !g.Has(c) && len(c.Short) != 0 {
|
||||
group.Commands = append(group.Commands, c)
|
||||
}
|
||||
}
|
||||
if len(group.Commands) == 0 {
|
||||
return g
|
||||
}
|
||||
return append(g, group)
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-wordwrap"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
const offset = 10
|
||||
|
||||
// HelpFlagPrinter is a printer that
|
||||
// processes the help flag and print
|
||||
// it to i/o writer
|
||||
type HelpFlagPrinter struct {
|
||||
wrapLimit uint
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// NewHelpFlagPrinter will initialize a HelpFlagPrinter given the
|
||||
// i/o writer
|
||||
func NewHelpFlagPrinter(out io.Writer, wrapLimit uint) *HelpFlagPrinter {
|
||||
return &HelpFlagPrinter{
|
||||
wrapLimit: wrapLimit,
|
||||
out: out,
|
||||
}
|
||||
}
|
||||
|
||||
// PrintHelpFlag will beautify the help flags and print it out to p.out
|
||||
func (p *HelpFlagPrinter) PrintHelpFlag(flag *flag.Flag) {
|
||||
formatBuf := new(bytes.Buffer)
|
||||
writeFlag(formatBuf, flag)
|
||||
|
||||
wrappedStr := formatBuf.String()
|
||||
flagAndUsage := strings.Split(formatBuf.String(), "\n")
|
||||
flagStr := flagAndUsage[0]
|
||||
|
||||
// if the flag usage is longer than one line, wrap it again
|
||||
if len(flagAndUsage) > 1 {
|
||||
nextLines := strings.Join(flagAndUsage[1:], " ")
|
||||
wrappedUsages := wordwrap.WrapString(nextLines, p.wrapLimit-offset)
|
||||
wrappedStr = flagStr + "\n" + wrappedUsages
|
||||
}
|
||||
appendTabStr := strings.ReplaceAll(wrappedStr, "\n", "\n\t")
|
||||
|
||||
fmt.Fprint(p.out, appendTabStr+"\n\n")
|
||||
}
|
||||
|
||||
// writeFlag will output the help flag based
|
||||
// on the format provided by getFlagFormat to i/o writer
|
||||
func writeFlag(out io.Writer, f *flag.Flag) {
|
||||
deprecated := ""
|
||||
if f.Deprecated != "" {
|
||||
deprecated = fmt.Sprintf(" (DEPRECATED: %s)", f.Deprecated)
|
||||
}
|
||||
fmt.Fprintf(out, getFlagFormat(f), f.Shorthand, f.Name, f.DefValue, f.Usage, deprecated)
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright 2016 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 templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
const linebreak = "\n"
|
||||
|
||||
// ASCIIRenderer implements blackfriday.Renderer
|
||||
var _ blackfriday.Renderer = &ASCIIRenderer{}
|
||||
|
||||
// ASCIIRenderer is a blackfriday.Renderer intended for rendering markdown
|
||||
// documents as plain text, well suited for human reading on terminals.
|
||||
type ASCIIRenderer struct {
|
||||
Indentation string
|
||||
|
||||
listItemCount uint
|
||||
listLevel uint
|
||||
}
|
||||
|
||||
// render markdown to text
|
||||
func (r *ASCIIRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
||||
switch node.Type {
|
||||
case blackfriday.Text:
|
||||
raw := string(node.Literal)
|
||||
lines := strings.Split(raw, linebreak)
|
||||
for _, line := range lines {
|
||||
trimmed := strings.Trim(line, " \n\t")
|
||||
if len(trimmed) > 0 && trimmed[0] != '_' {
|
||||
w.Write([]byte(" "))
|
||||
}
|
||||
w.Write([]byte(trimmed))
|
||||
}
|
||||
case blackfriday.HorizontalRule, blackfriday.Hardbreak:
|
||||
w.Write([]byte(linebreak + "----------" + linebreak))
|
||||
case blackfriday.Code, blackfriday.CodeBlock:
|
||||
w.Write([]byte(linebreak))
|
||||
lines := []string{}
|
||||
for _, line := range strings.Split(string(node.Literal), linebreak) {
|
||||
trimmed := strings.Trim(line, " \t")
|
||||
// Adding 4 times of indentation will let blackfriday to accept
|
||||
// this literal as Code or CodeBlock again in next invocation
|
||||
indented := strings.Repeat(r.Indentation, 4) + trimmed
|
||||
lines = append(lines, indented)
|
||||
}
|
||||
w.Write([]byte(strings.Join(lines, linebreak)))
|
||||
case blackfriday.Image:
|
||||
w.Write(node.LinkData.Destination)
|
||||
case blackfriday.Link:
|
||||
w.Write([]byte(" "))
|
||||
w.Write(node.LinkData.Destination)
|
||||
case blackfriday.Paragraph:
|
||||
if r.listLevel == 0 {
|
||||
w.Write([]byte(linebreak))
|
||||
}
|
||||
case blackfriday.List:
|
||||
if entering {
|
||||
w.Write([]byte(linebreak))
|
||||
r.listLevel++
|
||||
} else {
|
||||
r.listLevel--
|
||||
r.listItemCount = 0
|
||||
}
|
||||
case blackfriday.Item:
|
||||
if entering {
|
||||
r.listItemCount++
|
||||
for i := 0; uint(i) < r.listLevel; i++ {
|
||||
w.Write([]byte(r.Indentation))
|
||||
}
|
||||
if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
|
||||
w.Write([]byte(fmt.Sprintf("%d. ", r.listItemCount)))
|
||||
} else {
|
||||
w.Write([]byte("* "))
|
||||
}
|
||||
} else {
|
||||
w.Write([]byte(linebreak))
|
||||
}
|
||||
default:
|
||||
normalText(w, node.Literal)
|
||||
}
|
||||
return blackfriday.GoToNext
|
||||
}
|
||||
|
||||
func normalText(w io.Writer, text []byte) {
|
||||
w.Write([]byte(strings.Trim(string(text), " \n\t")))
|
||||
}
|
||||
|
||||
// RenderHeader writes document preamble and TOC if requested.
|
||||
func (r *ASCIIRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) {
|
||||
|
||||
}
|
||||
|
||||
// RenderFooter writes document footer.
|
||||
func (r *ASCIIRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) {
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright 2016 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 templates
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const Indentation = ` `
|
||||
|
||||
// LongDesc normalizes a command's long description to follow the conventions.
|
||||
func LongDesc(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return normalizer{s}.heredoc().markdown().trim().string
|
||||
}
|
||||
|
||||
// Examples normalizes a command's examples to follow the conventions.
|
||||
func Examples(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return normalizer{s}.trim().indent().string
|
||||
}
|
||||
|
||||
// Normalize perform all required normalizations on a given command.
|
||||
func Normalize(cmd *cobra.Command) *cobra.Command {
|
||||
if len(cmd.Long) > 0 {
|
||||
cmd.Long = LongDesc(cmd.Long)
|
||||
}
|
||||
if len(cmd.Example) > 0 {
|
||||
cmd.Example = Examples(cmd.Example)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NormalizeAll perform all required normalizations in the entire command tree.
|
||||
func NormalizeAll(cmd *cobra.Command) *cobra.Command {
|
||||
if cmd.HasSubCommands() {
|
||||
for _, subCmd := range cmd.Commands() {
|
||||
NormalizeAll(subCmd)
|
||||
}
|
||||
}
|
||||
Normalize(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type normalizer struct {
|
||||
string
|
||||
}
|
||||
|
||||
func (s normalizer) markdown() normalizer {
|
||||
bytes := []byte(s.string)
|
||||
formatted := blackfriday.Run(bytes, blackfriday.WithExtensions(blackfriday.NoIntraEmphasis), blackfriday.WithRenderer(&ASCIIRenderer{Indentation: Indentation}))
|
||||
s.string = string(formatted)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s normalizer) heredoc() normalizer {
|
||||
s.string = heredoc.Doc(s.string)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s normalizer) trim() normalizer {
|
||||
s.string = strings.TrimSpace(s.string)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s normalizer) indent() normalizer {
|
||||
indentedLines := []string{}
|
||||
for _, line := range strings.Split(s.string, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
indented := Indentation + trimmed
|
||||
indentedLines = append(indentedLines, indented)
|
||||
}
|
||||
s.string = strings.Join(indentedLines, "\n")
|
||||
return s
|
||||
}
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
Copyright 2016 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 templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/kubectl/pkg/util/term"
|
||||
)
|
||||
|
||||
type FlagExposer interface {
|
||||
ExposeFlags(cmd *cobra.Command, flags ...string) FlagExposer
|
||||
}
|
||||
|
||||
func ActsAsRootCommand(cmd *cobra.Command, filters []string, groups ...CommandGroup) FlagExposer {
|
||||
if cmd == nil {
|
||||
panic("nil root command")
|
||||
}
|
||||
templater := &templater{
|
||||
RootCmd: cmd,
|
||||
UsageTemplate: MainUsageTemplate(),
|
||||
HelpTemplate: MainHelpTemplate(),
|
||||
CommandGroups: groups,
|
||||
Filtered: filters,
|
||||
}
|
||||
cmd.SetFlagErrorFunc(templater.FlagErrorFunc())
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SetUsageFunc(templater.UsageFunc())
|
||||
cmd.SetHelpFunc(templater.HelpFunc())
|
||||
return templater
|
||||
}
|
||||
|
||||
func UseOptionsTemplates(cmd *cobra.Command) {
|
||||
templater := &templater{
|
||||
UsageTemplate: OptionsUsageTemplate(),
|
||||
HelpTemplate: OptionsHelpTemplate(),
|
||||
}
|
||||
cmd.SetUsageFunc(templater.UsageFunc())
|
||||
cmd.SetHelpFunc(templater.HelpFunc())
|
||||
}
|
||||
|
||||
type templater struct {
|
||||
UsageTemplate string
|
||||
HelpTemplate string
|
||||
RootCmd *cobra.Command
|
||||
CommandGroups
|
||||
Filtered []string
|
||||
}
|
||||
|
||||
func (templater *templater) FlagErrorFunc(exposedFlags ...string) func(*cobra.Command, error) error {
|
||||
return func(c *cobra.Command, err error) error {
|
||||
c.SilenceUsage = true
|
||||
switch c.CalledAs() {
|
||||
case "options":
|
||||
return fmt.Errorf("%s\nRun '%s' without flags.", err, c.CommandPath())
|
||||
default:
|
||||
return fmt.Errorf("%s\nSee '%s --help' for usage.", err, c.CommandPath())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (templater *templater) ExposeFlags(cmd *cobra.Command, flags ...string) FlagExposer {
|
||||
cmd.SetUsageFunc(templater.UsageFunc(flags...))
|
||||
return templater
|
||||
}
|
||||
|
||||
func (templater *templater) HelpFunc() func(*cobra.Command, []string) {
|
||||
return func(c *cobra.Command, s []string) {
|
||||
t := template.New("help")
|
||||
t.Funcs(templater.templateFuncs())
|
||||
template.Must(t.Parse(templater.HelpTemplate))
|
||||
out := term.NewResponsiveWriter(c.OutOrStdout())
|
||||
err := t.Execute(out, c)
|
||||
if err != nil {
|
||||
c.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (templater *templater) UsageFunc(exposedFlags ...string) func(*cobra.Command) error {
|
||||
return func(c *cobra.Command) error {
|
||||
t := template.New("usage")
|
||||
t.Funcs(templater.templateFuncs(exposedFlags...))
|
||||
template.Must(t.Parse(templater.UsageTemplate))
|
||||
out := term.NewResponsiveWriter(c.OutOrStderr())
|
||||
return t.Execute(out, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (templater *templater) templateFuncs(exposedFlags ...string) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"trim": strings.TrimSpace,
|
||||
"trimRight": func(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) },
|
||||
"trimLeft": func(s string) string { return strings.TrimLeftFunc(s, unicode.IsSpace) },
|
||||
"gt": cobra.Gt,
|
||||
"eq": cobra.Eq,
|
||||
"rpad": rpad,
|
||||
"appendIfNotPresent": appendIfNotPresent,
|
||||
"flagsNotIntersected": flagsNotIntersected,
|
||||
"visibleFlags": visibleFlags,
|
||||
"flagsUsages": flagsUsages,
|
||||
"cmdGroups": templater.cmdGroups,
|
||||
"cmdGroupsString": templater.cmdGroupsString,
|
||||
"rootCmd": templater.rootCmdName,
|
||||
"isRootCmd": templater.isRootCmd,
|
||||
"optionsCmdFor": templater.optionsCmdFor,
|
||||
"usageLine": templater.usageLine,
|
||||
"reverseParentsNames": templater.reverseParentsNames,
|
||||
"exposed": func(c *cobra.Command) *flag.FlagSet {
|
||||
exposed := flag.NewFlagSet("exposed", flag.ContinueOnError)
|
||||
if len(exposedFlags) > 0 {
|
||||
for _, name := range exposedFlags {
|
||||
if flag := c.Flags().Lookup(name); flag != nil {
|
||||
exposed.AddFlag(flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return exposed
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (templater *templater) cmdGroups(c *cobra.Command, all []*cobra.Command) []CommandGroup {
|
||||
if len(templater.CommandGroups) > 0 && c == templater.RootCmd {
|
||||
all = filter(all, templater.Filtered...)
|
||||
return AddAdditionalCommands(templater.CommandGroups, "Other Commands:", all)
|
||||
}
|
||||
all = filter(all, "options")
|
||||
return []CommandGroup{
|
||||
{
|
||||
Message: "Available Commands:",
|
||||
Commands: all,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *templater) cmdGroupsString(c *cobra.Command) string {
|
||||
groups := []string{}
|
||||
for _, cmdGroup := range t.cmdGroups(c, c.Commands()) {
|
||||
cmds := []string{cmdGroup.Message}
|
||||
for _, cmd := range cmdGroup.Commands {
|
||||
if cmd.IsAvailableCommand() {
|
||||
cmds = append(cmds, " "+rpad(cmd.Name(), cmd.NamePadding())+" "+cmd.Short)
|
||||
}
|
||||
}
|
||||
groups = append(groups, strings.Join(cmds, "\n"))
|
||||
}
|
||||
return strings.Join(groups, "\n\n")
|
||||
}
|
||||
|
||||
func (t *templater) rootCmdName(c *cobra.Command) string {
|
||||
return t.rootCmd(c).CommandPath()
|
||||
}
|
||||
|
||||
func (t *templater) reverseParentsNames(c *cobra.Command) []string {
|
||||
reverseParentsNames := []string{}
|
||||
parents := t.parents(c)
|
||||
for i := len(parents) - 1; i >= 0; i-- {
|
||||
reverseParentsNames = append(reverseParentsNames, parents[i].Name())
|
||||
}
|
||||
return reverseParentsNames
|
||||
}
|
||||
|
||||
func (t *templater) isRootCmd(c *cobra.Command) bool {
|
||||
return t.rootCmd(c) == c
|
||||
}
|
||||
|
||||
func (t *templater) parents(c *cobra.Command) []*cobra.Command {
|
||||
parents := []*cobra.Command{c}
|
||||
for current := c; !t.isRootCmd(current) && current.HasParent(); {
|
||||
current = current.Parent()
|
||||
parents = append(parents, current)
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
func (t *templater) rootCmd(c *cobra.Command) *cobra.Command {
|
||||
if c != nil && !c.HasParent() {
|
||||
return c
|
||||
}
|
||||
if t.RootCmd == nil {
|
||||
panic("nil root cmd")
|
||||
}
|
||||
return t.RootCmd
|
||||
}
|
||||
|
||||
func (t *templater) optionsCmdFor(c *cobra.Command) string {
|
||||
if !c.Runnable() {
|
||||
return ""
|
||||
}
|
||||
rootCmdStructure := t.parents(c)
|
||||
for i := len(rootCmdStructure) - 1; i >= 0; i-- {
|
||||
cmd := rootCmdStructure[i]
|
||||
if _, _, err := cmd.Find([]string{"options"}); err == nil {
|
||||
return cmd.CommandPath() + " options"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *templater) usageLine(c *cobra.Command) string {
|
||||
usage := c.UseLine()
|
||||
suffix := "[options]"
|
||||
if c.HasFlags() && !strings.Contains(usage, suffix) {
|
||||
usage += " " + suffix
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
// flagsUsages will print out the kubectl help flags
|
||||
func flagsUsages(f *flag.FlagSet) (string, error) {
|
||||
flagBuf := new(bytes.Buffer)
|
||||
wrapLimit, err := term.GetWordWrapperLimit()
|
||||
if err != nil {
|
||||
wrapLimit = 0
|
||||
}
|
||||
printer := NewHelpFlagPrinter(flagBuf, wrapLimit)
|
||||
|
||||
f.VisitAll(func(flag *flag.Flag) {
|
||||
if flag.Hidden {
|
||||
return
|
||||
}
|
||||
printer.PrintHelpFlag(flag)
|
||||
})
|
||||
|
||||
return flagBuf.String(), nil
|
||||
}
|
||||
|
||||
// getFlagFormat will output the flag format
|
||||
func getFlagFormat(f *flag.Flag) string {
|
||||
var format string
|
||||
format = "--%s=%s:\n%s%s"
|
||||
if f.Value.Type() == "string" {
|
||||
format = "--%s='%s':\n%s%s"
|
||||
}
|
||||
|
||||
if len(f.Shorthand) > 0 {
|
||||
format = " -%s, " + format
|
||||
} else {
|
||||
format = " %s" + format
|
||||
}
|
||||
|
||||
return format
|
||||
}
|
||||
|
||||
func rpad(s string, padding int) string {
|
||||
template := fmt.Sprintf("%%-%ds", padding)
|
||||
return fmt.Sprintf(template, s)
|
||||
}
|
||||
|
||||
func appendIfNotPresent(s, stringToAppend string) string {
|
||||
if strings.Contains(s, stringToAppend) {
|
||||
return s
|
||||
}
|
||||
return s + " " + stringToAppend
|
||||
}
|
||||
|
||||
func flagsNotIntersected(l *flag.FlagSet, r *flag.FlagSet) *flag.FlagSet {
|
||||
f := flag.NewFlagSet("notIntersected", flag.ContinueOnError)
|
||||
l.VisitAll(func(flag *flag.Flag) {
|
||||
if r.Lookup(flag.Name) == nil {
|
||||
f.AddFlag(flag)
|
||||
}
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
func visibleFlags(l *flag.FlagSet) *flag.FlagSet {
|
||||
hidden := "help"
|
||||
f := flag.NewFlagSet("visible", flag.ContinueOnError)
|
||||
l.VisitAll(func(flag *flag.Flag) {
|
||||
if flag.Name != hidden {
|
||||
f.AddFlag(flag)
|
||||
}
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
func filter(cmds []*cobra.Command, names ...string) []*cobra.Command {
|
||||
out := []*cobra.Command{}
|
||||
for _, c := range cmds {
|
||||
if c.Hidden {
|
||||
continue
|
||||
}
|
||||
skip := false
|
||||
for _, name := range names {
|
||||
if name == c.Name() {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
Copyright 2016 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 templates
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
// SectionVars is the help template section that declares variables to be used in the template.
|
||||
SectionVars = `{{$isRootCmd := isRootCmd .}}` +
|
||||
`{{$rootCmd := rootCmd .}}` +
|
||||
`{{$visibleFlags := visibleFlags (flagsNotIntersected .LocalFlags .PersistentFlags)}}` +
|
||||
`{{$explicitlyExposedFlags := exposed .}}` +
|
||||
`{{$optionsCmdFor := optionsCmdFor .}}` +
|
||||
`{{$usageLine := usageLine .}}` +
|
||||
`{{$reverseParentsNames := reverseParentsNames .}}`
|
||||
|
||||
// SectionAliases is the help template section that displays command aliases.
|
||||
SectionAliases = `{{if gt .Aliases 0}}Aliases:
|
||||
{{.NameAndAliases}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionExamples is the help template section that displays command examples.
|
||||
SectionExamples = `{{if .HasExample}}Examples:
|
||||
{{trimRight .Example}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionSubcommands is the help template section that displays the command's subcommands.
|
||||
SectionSubcommands = `{{if .HasAvailableSubCommands}}{{cmdGroupsString .}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionFlags is the help template section that displays the command's flags.
|
||||
SectionFlags = `{{ if or $visibleFlags.HasFlags $explicitlyExposedFlags.HasFlags}}Options:
|
||||
{{ if $visibleFlags.HasFlags}}{{trimRight (flagsUsages $visibleFlags)}}{{end}}{{ if $explicitlyExposedFlags.HasFlags}}{{ if $visibleFlags.HasFlags}}
|
||||
{{end}}{{trimRight (flagsUsages $explicitlyExposedFlags)}}{{end}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionUsage is the help template section that displays the command's usage.
|
||||
SectionUsage = `{{if and .Runnable (ne .UseLine "") (ne .UseLine $rootCmd)}}Usage:
|
||||
{{$usageLine}}
|
||||
|
||||
{{end}}`
|
||||
|
||||
// SectionTipsHelp is the help template section that displays the '--help' hint.
|
||||
SectionTipsHelp = `{{if .HasSubCommands}}Use "{{range $reverseParentsNames}}{{.}} {{end}}<command> --help" for more information about a given command.
|
||||
{{end}}`
|
||||
|
||||
// SectionTipsGlobalOptions is the help template section that displays the 'options' hint for displaying global flags.
|
||||
SectionTipsGlobalOptions = `{{if $optionsCmdFor}}Use "{{$optionsCmdFor}}" for a list of global command-line options (applies to all commands).
|
||||
{{end}}`
|
||||
)
|
||||
|
||||
// MainHelpTemplate if the template for 'help' used by most commands.
|
||||
func MainHelpTemplate() string {
|
||||
return `{{with or .Long .Short }}{{. | trim}}{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
|
||||
}
|
||||
|
||||
// MainUsageTemplate if the template for 'usage' used by most commands.
|
||||
func MainUsageTemplate() string {
|
||||
sections := []string{
|
||||
"\n\n",
|
||||
SectionVars,
|
||||
SectionAliases,
|
||||
SectionExamples,
|
||||
SectionSubcommands,
|
||||
SectionFlags,
|
||||
SectionUsage,
|
||||
SectionTipsHelp,
|
||||
SectionTipsGlobalOptions,
|
||||
}
|
||||
return strings.TrimRightFunc(strings.Join(sections, ""), unicode.IsSpace)
|
||||
}
|
||||
|
||||
// OptionsHelpTemplate if the template for 'help' used by the 'options' command.
|
||||
func OptionsHelpTemplate() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// OptionsUsageTemplate if the template for 'usage' used by the 'options' command.
|
||||
func OptionsUsageTemplate() string {
|
||||
return `{{ if .HasInheritedFlags}}The following options can be passed to any command:
|
||||
|
||||
{{flagsUsages .InheritedFlags}}{{end}}`
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
Copyright 2016 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 term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/moby/term"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
)
|
||||
|
||||
// TerminalSize represents the width and height of a terminal.
|
||||
// It is the same as staging/src/k8s.io/client-go/tools/remotecommand.TerminalSize.
|
||||
// Copied to decouple the packages. Terminal-related package should not depend on API client and vice versa.
|
||||
type TerminalSize struct {
|
||||
Width uint16
|
||||
Height uint16
|
||||
}
|
||||
|
||||
// TerminalSizeQueue is capable of returning terminal resize events as they occur.
|
||||
// It is the same as staging/src/k8s.io/client-go/tools/remotecommand.TerminalSizeQueue.
|
||||
// Copied to decouple the packages. Terminal-related package should not depend on API client and vice versa.
|
||||
type TerminalSizeQueue interface {
|
||||
// Next returns the new terminal size after the terminal has been resized. It returns nil when
|
||||
// monitoring has been stopped.
|
||||
Next() *TerminalSize
|
||||
}
|
||||
|
||||
// GetSize returns the current size of the user's terminal. If it isn't a terminal,
|
||||
// nil is returned.
|
||||
func (t TTY) GetSize() *TerminalSize {
|
||||
outFd, isTerminal := term.GetFdInfo(t.Out)
|
||||
if !isTerminal {
|
||||
return nil
|
||||
}
|
||||
return GetSize(outFd)
|
||||
}
|
||||
|
||||
// GetSize returns the current size of the terminal associated with fd.
|
||||
func GetSize(fd uintptr) *TerminalSize {
|
||||
winsize, err := term.GetWinsize(fd)
|
||||
if err != nil {
|
||||
runtime.HandleError(fmt.Errorf("unable to get terminal size: %v", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &TerminalSize{Width: winsize.Width, Height: winsize.Height}
|
||||
}
|
||||
|
||||
// MonitorSize monitors the terminal's size. It returns a TerminalSizeQueue primed with
|
||||
// initialSizes, or nil if there's no TTY present.
|
||||
func (t *TTY) MonitorSize(initialSizes ...*TerminalSize) TerminalSizeQueue {
|
||||
outFd, isTerminal := term.GetFdInfo(t.Out)
|
||||
if !isTerminal {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sizeQueue = &sizeQueue{
|
||||
t: *t,
|
||||
// make it buffered so we can send the initial terminal sizes without blocking, prior to starting
|
||||
// the streaming below
|
||||
resizeChan: make(chan TerminalSize, len(initialSizes)),
|
||||
stopResizing: make(chan struct{}),
|
||||
}
|
||||
|
||||
t.sizeQueue.monitorSize(outFd, initialSizes...)
|
||||
|
||||
return t.sizeQueue
|
||||
}
|
||||
|
||||
// sizeQueue implements remotecommand.TerminalSizeQueue
|
||||
type sizeQueue struct {
|
||||
t TTY
|
||||
// resizeChan receives a Size each time the user's terminal is resized.
|
||||
resizeChan chan TerminalSize
|
||||
stopResizing chan struct{}
|
||||
}
|
||||
|
||||
// make sure sizeQueue implements the TerminalSizeQueue interface
|
||||
var _ TerminalSizeQueue = &sizeQueue{}
|
||||
|
||||
// monitorSize primes resizeChan with initialSizes and then monitors for resize events. With each
|
||||
// new event, it sends the current terminal size to resizeChan.
|
||||
func (s *sizeQueue) monitorSize(outFd uintptr, initialSizes ...*TerminalSize) {
|
||||
// send the initial sizes
|
||||
for i := range initialSizes {
|
||||
if initialSizes[i] != nil {
|
||||
s.resizeChan <- *initialSizes[i]
|
||||
}
|
||||
}
|
||||
|
||||
resizeEvents := make(chan TerminalSize, 1)
|
||||
|
||||
monitorResizeEvents(outFd, resizeEvents, s.stopResizing)
|
||||
|
||||
// listen for resize events in the background
|
||||
go func() {
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
for {
|
||||
select {
|
||||
case size, ok := <-resizeEvents:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
// try to send the size to resizeChan, but don't block
|
||||
case s.resizeChan <- size:
|
||||
// send successful
|
||||
default:
|
||||
// unable to send / no-op
|
||||
}
|
||||
case <-s.stopResizing:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Next returns the new terminal size after the terminal has been resized. It returns nil when
|
||||
// monitoring has been stopped.
|
||||
func (s *sizeQueue) Next() *TerminalSize {
|
||||
size, ok := <-s.resizeChan
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &size
|
||||
}
|
||||
|
||||
// stop stops the background goroutine that is monitoring for terminal resizes.
|
||||
func (s *sizeQueue) stop() {
|
||||
close(s.stopResizing)
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
Copyright 2016 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 term
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
)
|
||||
|
||||
// monitorResizeEvents spawns a goroutine that waits for SIGWINCH signals (these indicate the
|
||||
// terminal has resized). After receiving a SIGWINCH, this gets the terminal size and tries to send
|
||||
// it to the resizeEvents channel. The goroutine stops when the stop channel is closed.
|
||||
func monitorResizeEvents(fd uintptr, resizeEvents chan<- TerminalSize, stop chan struct{}) {
|
||||
go func() {
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
winch := make(chan os.Signal, 1)
|
||||
signal.Notify(winch, unix.SIGWINCH)
|
||||
defer signal.Stop(winch)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-winch:
|
||||
size := GetSize(fd)
|
||||
if size == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// try to send size
|
||||
select {
|
||||
case resizeEvents <- *size:
|
||||
// success
|
||||
default:
|
||||
// not sent
|
||||
}
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2016 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 term
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
)
|
||||
|
||||
// monitorResizeEvents spawns a goroutine that periodically gets the terminal size and tries to send
|
||||
// it to the resizeEvents channel if the size has changed. The goroutine stops when the stop channel
|
||||
// is closed.
|
||||
func monitorResizeEvents(fd uintptr, resizeEvents chan<- TerminalSize, stop chan struct{}) {
|
||||
go func() {
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
size := GetSize(fd)
|
||||
if size == nil {
|
||||
return
|
||||
}
|
||||
lastSize := *size
|
||||
|
||||
for {
|
||||
// see if we need to stop running
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
size := GetSize(fd)
|
||||
if size == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if size.Height != lastSize.Height || size.Width != lastSize.Width {
|
||||
lastSize.Height = size.Height
|
||||
lastSize.Width = size.Width
|
||||
resizeEvents <- *size
|
||||
}
|
||||
|
||||
// sleep to avoid hot looping
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
Copyright 2016 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 term
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
|
||||
"github.com/moby/term"
|
||||
|
||||
"k8s.io/kubectl/pkg/util/interrupt"
|
||||
)
|
||||
|
||||
// SafeFunc is a function to be invoked by TTY.
|
||||
type SafeFunc func() error
|
||||
|
||||
// TTY helps invoke a function and preserve the state of the terminal, even if the process is
|
||||
// terminated during execution. It also provides support for terminal resizing for remote command
|
||||
// execution/attachment.
|
||||
type TTY struct {
|
||||
// In is a reader representing stdin. It is a required field.
|
||||
In io.Reader
|
||||
// Out is a writer representing stdout. It must be set to support terminal resizing. It is an
|
||||
// optional field.
|
||||
Out io.Writer
|
||||
// Raw is true if the terminal should be set raw.
|
||||
Raw bool
|
||||
// TryDev indicates the TTY should try to open /dev/tty if the provided input
|
||||
// is not a file descriptor.
|
||||
TryDev bool
|
||||
// Parent is an optional interrupt handler provided to this function - if provided
|
||||
// it will be invoked after the terminal state is restored. If it is not provided,
|
||||
// a signal received during the TTY will result in os.Exit(0) being invoked.
|
||||
Parent *interrupt.Handler
|
||||
|
||||
// sizeQueue is set after a call to MonitorSize() and is used to monitor SIGWINCH signals when the
|
||||
// user's terminal resizes.
|
||||
sizeQueue *sizeQueue
|
||||
}
|
||||
|
||||
// IsTerminalIn returns true if t.In is a terminal. Does not check /dev/tty
|
||||
// even if TryDev is set.
|
||||
func (t TTY) IsTerminalIn() bool {
|
||||
return printers.IsTerminal(t.In)
|
||||
}
|
||||
|
||||
// IsTerminalOut returns true if t.Out is a terminal. Does not check /dev/tty
|
||||
// even if TryDev is set.
|
||||
func (t TTY) IsTerminalOut() bool {
|
||||
return printers.IsTerminal(t.Out)
|
||||
}
|
||||
|
||||
// IsTerminal returns whether the passed object is a terminal or not.
|
||||
// Deprecated: use printers.IsTerminal instead.
|
||||
var IsTerminal = printers.IsTerminal
|
||||
|
||||
// AllowsColorOutput returns true if the specified writer is a terminal and
|
||||
// the process environment indicates color output is supported and desired.
|
||||
// Deprecated: use printers.AllowsColorOutput instead.
|
||||
var AllowsColorOutput = printers.AllowsColorOutput
|
||||
|
||||
// Safe invokes the provided function and will attempt to ensure that when the
|
||||
// function returns (or a termination signal is sent) that the terminal state
|
||||
// is reset to the condition it was in prior to the function being invoked. If
|
||||
// t.Raw is true the terminal will be put into raw mode prior to calling the function.
|
||||
// If the input file descriptor is not a TTY and TryDev is true, the /dev/tty file
|
||||
// will be opened (if available).
|
||||
func (t TTY) Safe(fn SafeFunc) error {
|
||||
inFd, isTerminal := term.GetFdInfo(t.In)
|
||||
|
||||
if !isTerminal && t.TryDev {
|
||||
if f, err := os.Open("/dev/tty"); err == nil {
|
||||
defer f.Close()
|
||||
inFd = f.Fd()
|
||||
isTerminal = term.IsTerminal(inFd)
|
||||
}
|
||||
}
|
||||
if !isTerminal {
|
||||
return fn()
|
||||
}
|
||||
|
||||
var state *term.State
|
||||
var err error
|
||||
if t.Raw {
|
||||
state, err = term.MakeRaw(inFd)
|
||||
} else {
|
||||
state, err = term.SaveState(inFd)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return interrupt.Chain(t.Parent, func() {
|
||||
if t.sizeQueue != nil {
|
||||
t.sizeQueue.stop()
|
||||
}
|
||||
|
||||
term.RestoreTerminal(inFd, state)
|
||||
}).Run(fn)
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
Copyright 2016 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 term
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
wordwrap "github.com/mitchellh/go-wordwrap"
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
type wordWrapWriter struct {
|
||||
limit uint
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
// NewResponsiveWriter creates a Writer that detects the column width of the
|
||||
// terminal we are in, and adjusts every line width to fit and use recommended
|
||||
// terminal sizes for better readability. Does proper word wrapping automatically.
|
||||
//
|
||||
// if terminal width >= 120 columns use 120 columns
|
||||
// if terminal width >= 100 columns use 100 columns
|
||||
// if terminal width >= 80 columns use 80 columns
|
||||
//
|
||||
// In case we're not in a terminal or if it's smaller than 80 columns width,
|
||||
// doesn't do any wrapping.
|
||||
func NewResponsiveWriter(w io.Writer) io.Writer {
|
||||
file, ok := w.(*os.File)
|
||||
if !ok {
|
||||
return w
|
||||
}
|
||||
fd := file.Fd()
|
||||
if !term.IsTerminal(fd) {
|
||||
return w
|
||||
}
|
||||
|
||||
terminalSize := GetSize(fd)
|
||||
if terminalSize == nil {
|
||||
return w
|
||||
}
|
||||
limit := getTerminalLimitWidth(terminalSize)
|
||||
|
||||
return NewWordWrapWriter(w, limit)
|
||||
}
|
||||
|
||||
// NewWordWrapWriter is a Writer that supports a limit of characters on every line
|
||||
// and does auto word wrapping that respects that limit.
|
||||
func NewWordWrapWriter(w io.Writer, limit uint) io.Writer {
|
||||
return &wordWrapWriter{
|
||||
limit: limit,
|
||||
writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
func getTerminalLimitWidth(terminalSize *TerminalSize) uint {
|
||||
var limit uint
|
||||
switch {
|
||||
case terminalSize.Width >= 120:
|
||||
limit = 120
|
||||
case terminalSize.Width >= 100:
|
||||
limit = 100
|
||||
case terminalSize.Width >= 80:
|
||||
limit = 80
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func GetWordWrapperLimit() (uint, error) {
|
||||
stdout := os.Stdout
|
||||
fd := stdout.Fd()
|
||||
if !term.IsTerminal(fd) {
|
||||
return 0, errors.New("file descriptor is not a terminal")
|
||||
}
|
||||
terminalSize := GetSize(fd)
|
||||
if terminalSize == nil {
|
||||
return 0, errors.New("terminal size is nil")
|
||||
}
|
||||
return getTerminalLimitWidth(terminalSize), nil
|
||||
}
|
||||
|
||||
func (w wordWrapWriter) Write(p []byte) (nn int, err error) {
|
||||
if w.limit == 0 {
|
||||
return w.writer.Write(p)
|
||||
}
|
||||
original := string(p)
|
||||
wrapped := wordwrap.WrapString(original, w.limit)
|
||||
return w.writer.Write([]byte(wrapped))
|
||||
}
|
||||
|
||||
// NewPunchCardWriter is a NewWordWrapWriter that limits the line width to 80 columns.
|
||||
func NewPunchCardWriter(w io.Writer) io.Writer {
|
||||
return NewWordWrapWriter(w, 80)
|
||||
}
|
||||
|
||||
type maxWidthWriter struct {
|
||||
maxWidth uint
|
||||
currentWidth uint
|
||||
written uint
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
// NewMaxWidthWriter is a Writer that supports a limit of characters on every
|
||||
// line, but doesn't do any word wrapping automatically.
|
||||
func NewMaxWidthWriter(w io.Writer, maxWidth uint) io.Writer {
|
||||
return &maxWidthWriter{
|
||||
maxWidth: maxWidth,
|
||||
writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (m maxWidthWriter) Write(p []byte) (nn int, err error) {
|
||||
for _, b := range p {
|
||||
if m.currentWidth == m.maxWidth {
|
||||
m.writer.Write([]byte{'\n'})
|
||||
m.currentWidth = 0
|
||||
}
|
||||
if b == '\n' {
|
||||
m.currentWidth = 0
|
||||
}
|
||||
_, err := m.writer.Write([]byte{b})
|
||||
if err != nil {
|
||||
return int(m.written), err
|
||||
}
|
||||
m.written++
|
||||
m.currentWidth++
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
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 validation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
ejson "github.com/exponent-io/jsonpath"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// Schema is an interface that knows how to validate an API object serialized to a byte array.
|
||||
type Schema interface {
|
||||
ValidateBytes(data []byte) error
|
||||
}
|
||||
|
||||
// NullSchema always validates bytes.
|
||||
type NullSchema struct{}
|
||||
|
||||
// ValidateBytes never fails for NullSchema.
|
||||
func (NullSchema) ValidateBytes(data []byte) error { return nil }
|
||||
|
||||
// NoDoubleKeySchema is a schema that disallows double keys.
|
||||
type NoDoubleKeySchema struct{}
|
||||
|
||||
// ValidateBytes validates bytes.
|
||||
func (NoDoubleKeySchema) ValidateBytes(data []byte) error {
|
||||
var list []error
|
||||
if err := validateNoDuplicateKeys(data, "metadata", "labels"); err != nil {
|
||||
list = append(list, err)
|
||||
}
|
||||
if err := validateNoDuplicateKeys(data, "metadata", "annotations"); err != nil {
|
||||
list = append(list, err)
|
||||
}
|
||||
return utilerrors.NewAggregate(list)
|
||||
}
|
||||
|
||||
func validateNoDuplicateKeys(data []byte, path ...string) error {
|
||||
r := ejson.NewDecoder(bytes.NewReader(data))
|
||||
// This is Go being unfriendly. The 'path ...string' comes in as a
|
||||
// []string, and SeekTo takes ...interface{}, so we can't just pass
|
||||
// the path straight in, we have to copy it. *sigh*
|
||||
ifacePath := []interface{}{}
|
||||
for ix := range path {
|
||||
ifacePath = append(ifacePath, path[ix])
|
||||
}
|
||||
found, err := r.SeekTo(ifacePath...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for {
|
||||
tok, err := r.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
if t.String() == "}" {
|
||||
return nil
|
||||
}
|
||||
case ejson.KeyString:
|
||||
if seen[string(t)] {
|
||||
return fmt.Errorf("duplicate key: %s", string(t))
|
||||
}
|
||||
seen[string(t)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConjunctiveSchema encapsulates a schema list.
|
||||
type ConjunctiveSchema []Schema
|
||||
|
||||
// ValidateBytes validates bytes per a ConjunctiveSchema.
|
||||
func (c ConjunctiveSchema) ValidateBytes(data []byte) error {
|
||||
var list []error
|
||||
schemas := []Schema(c)
|
||||
for ix := range schemas {
|
||||
if err := schemas[ix].ValidateBytes(data); err != nil {
|
||||
list = append(list, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(list)
|
||||
}
|
||||
|
||||
func NewParamVerifyingSchema(s Schema, verifier resource.Verifier, directive string) Schema {
|
||||
return ¶mVerifyingSchema{
|
||||
schema: s,
|
||||
verifier: verifier,
|
||||
directive: directive,
|
||||
}
|
||||
}
|
||||
|
||||
// paramVerifyingSchema only performs validation
|
||||
// based on the fieldValidation query param
|
||||
// being unsupported by the apiserver, because
|
||||
// server-side validation will be performed instead
|
||||
// of client-side validation.
|
||||
type paramVerifyingSchema struct {
|
||||
schema Schema
|
||||
verifier resource.Verifier
|
||||
directive string
|
||||
}
|
||||
|
||||
// ValidateBytes validates bytes per a ParamVerifyingSchema
|
||||
func (c *paramVerifyingSchema) ValidateBytes(data []byte) error {
|
||||
obj, err := parse(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvk, errs := getObjectKind(obj)
|
||||
if errs != nil {
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
err = c.verifier.HasSupport(gvk)
|
||||
if resource.IsParamUnsupportedError(err) {
|
||||
switch c.directive {
|
||||
case metav1.FieldValidationStrict:
|
||||
return c.schema.ValidateBytes(data)
|
||||
case metav1.FieldValidationWarn:
|
||||
klog.Warningf("cannot perform warn validation if server-side field validation is unsupported, skipping validation")
|
||||
default:
|
||||
// can't be reached
|
||||
klog.Warningf("unexpected field validation directive: %s, skipping validation", c.directive)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
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 validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/kube-openapi/pkg/util/proto/validation"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
)
|
||||
|
||||
// schemaValidation validates the object against an OpenAPI schema.
|
||||
type schemaValidation struct {
|
||||
resourcesGetter openapi.OpenAPIResourcesGetter
|
||||
}
|
||||
|
||||
// NewSchemaValidation creates a new Schema that can be used
|
||||
// to validate objects.
|
||||
func NewSchemaValidation(resourcesGetter openapi.OpenAPIResourcesGetter) Schema {
|
||||
return &schemaValidation{
|
||||
resourcesGetter: resourcesGetter,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateBytes will validates the object against using the Resources
|
||||
// object.
|
||||
func (v *schemaValidation) ValidateBytes(data []byte) error {
|
||||
obj, err := parse(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvk, errs := getObjectKind(obj)
|
||||
if errs != nil {
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
if (gvk == schema.GroupVersionKind{Version: "v1", Kind: "List"}) {
|
||||
return utilerrors.NewAggregate(v.validateList(obj))
|
||||
}
|
||||
return utilerrors.NewAggregate(v.validateResource(obj, gvk))
|
||||
}
|
||||
|
||||
func (v *schemaValidation) validateList(object interface{}) []error {
|
||||
fields, ok := object.(map[string]interface{})
|
||||
if !ok || fields == nil {
|
||||
return []error{errors.New("invalid object to validate")}
|
||||
}
|
||||
|
||||
allErrors := []error{}
|
||||
if _, ok := fields["items"].([]interface{}); !ok {
|
||||
return []error{errors.New("invalid object to validate")}
|
||||
}
|
||||
for _, item := range fields["items"].([]interface{}) {
|
||||
if gvk, errs := getObjectKind(item); errs != nil {
|
||||
allErrors = append(allErrors, errs...)
|
||||
} else {
|
||||
allErrors = append(allErrors, v.validateResource(item, gvk)...)
|
||||
}
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func (v *schemaValidation) validateResource(obj interface{}, gvk schema.GroupVersionKind) []error {
|
||||
// This lazy-loads the OpenAPI V2 specifications, caching the specs.
|
||||
resources, err := v.resourcesGetter.OpenAPISchema()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
resource := resources.LookupResource(gvk)
|
||||
if resource == nil {
|
||||
// resource is not present, let's just skip validation.
|
||||
return nil
|
||||
}
|
||||
|
||||
return validation.ValidateModel(obj, resource, gvk.Kind)
|
||||
}
|
||||
|
||||
func parse(data []byte) (interface{}, error) {
|
||||
var obj interface{}
|
||||
out, err := yaml.ToJSON(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(out, &obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func getObjectKind(object interface{}) (schema.GroupVersionKind, []error) {
|
||||
var listErrors []error
|
||||
fields, ok := object.(map[string]interface{})
|
||||
if !ok || fields == nil {
|
||||
listErrors = append(listErrors, errors.New("invalid object to validate"))
|
||||
return schema.GroupVersionKind{}, listErrors
|
||||
}
|
||||
|
||||
var group string
|
||||
var version string
|
||||
apiVersion := fields["apiVersion"]
|
||||
if apiVersion == nil {
|
||||
listErrors = append(listErrors, errors.New("apiVersion not set"))
|
||||
} else if _, ok := apiVersion.(string); !ok {
|
||||
listErrors = append(listErrors, errors.New("apiVersion isn't string type"))
|
||||
} else {
|
||||
gv, err := schema.ParseGroupVersion(apiVersion.(string))
|
||||
if err != nil {
|
||||
listErrors = append(listErrors, err)
|
||||
} else {
|
||||
group = gv.Group
|
||||
version = gv.Version
|
||||
}
|
||||
}
|
||||
kind := fields["kind"]
|
||||
if kind == nil {
|
||||
listErrors = append(listErrors, errors.New("kind not set"))
|
||||
} else if _, ok := kind.(string); !ok {
|
||||
listErrors = append(listErrors, errors.New("kind isn't string type"))
|
||||
}
|
||||
if listErrors != nil {
|
||||
return schema.GroupVersionKind{}, listErrors
|
||||
}
|
||||
|
||||
return schema.GroupVersionKind{Group: group, Version: version, Kind: kind.(string)}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user