Update go dependencies
This commit is contained in:
parent
14a9e9f3fa
commit
14f4a7b8e8
1349 changed files with 128369 additions and 32627 deletions
28
vendor/k8s.io/cli-runtime/pkg/genericclioptions/builder_flags.go
generated
vendored
28
vendor/k8s.io/cli-runtime/pkg/genericclioptions/builder_flags.go
generated
vendored
|
|
@ -20,7 +20,7 @@ import (
|
|||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/resource"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// ResourceBuilderFlags are flags for finding resources
|
||||
|
|
@ -28,12 +28,11 @@ import (
|
|||
type ResourceBuilderFlags struct {
|
||||
FileNameFlags *FileNameFlags
|
||||
|
||||
LabelSelector *string
|
||||
FieldSelector *string
|
||||
AllNamespaces *bool
|
||||
All *bool
|
||||
Local *bool
|
||||
IncludeUninitialized *bool
|
||||
LabelSelector *string
|
||||
FieldSelector *string
|
||||
AllNamespaces *bool
|
||||
All *bool
|
||||
Local *bool
|
||||
|
||||
Scheme *runtime.Scheme
|
||||
Latest bool
|
||||
|
|
@ -88,12 +87,6 @@ func (o *ResourceBuilderFlags) WithLocal(defaultVal bool) *ResourceBuilderFlags
|
|||
return o
|
||||
}
|
||||
|
||||
// WithUninitialized is using an alpha feature and may be dropped
|
||||
func (o *ResourceBuilderFlags) WithUninitialized(defaultVal bool) *ResourceBuilderFlags {
|
||||
o.IncludeUninitialized = &defaultVal
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *ResourceBuilderFlags) WithScheme(scheme *runtime.Scheme) *ResourceBuilderFlags {
|
||||
o.Scheme = scheme
|
||||
return o
|
||||
|
|
@ -120,7 +113,7 @@ func (o *ResourceBuilderFlags) AddFlags(flagset *pflag.FlagSet) {
|
|||
flagset.StringVar(o.FieldSelector, "field-selector", *o.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.")
|
||||
}
|
||||
if o.AllNamespaces != nil {
|
||||
flagset.BoolVar(o.AllNamespaces, "all-namespaces", *o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
|
||||
flagset.BoolVarP(o.AllNamespaces, "all-namespaces", "A", *o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
|
||||
}
|
||||
if o.All != nil {
|
||||
flagset.BoolVar(o.All, "all", *o.All, "Select all resources in the namespace of the specified resource types")
|
||||
|
|
@ -128,9 +121,6 @@ func (o *ResourceBuilderFlags) AddFlags(flagset *pflag.FlagSet) {
|
|||
if o.Local != nil {
|
||||
flagset.BoolVar(o.Local, "local", *o.Local, "If true, annotation will NOT contact api-server but run locally.")
|
||||
}
|
||||
if o.IncludeUninitialized != nil {
|
||||
flagset.BoolVar(o.IncludeUninitialized, "include-uninitialized", *o.IncludeUninitialized, `If true, the kubectl command applies to uninitialized objects. If explicitly set to false, this flag overrides other flags that make the kubectl commands apply to uninitialized objects, e.g., "--all". Objects with empty metadata.initializers are regarded as initialized.`)
|
||||
}
|
||||
}
|
||||
|
||||
// ToBuilder gives you back a resource finder to visit resources that are located
|
||||
|
|
@ -179,10 +169,6 @@ func (o *ResourceBuilderFlags) ToBuilder(restClientGetter RESTClientGetter, reso
|
|||
}
|
||||
}
|
||||
|
||||
if o.IncludeUninitialized != nil {
|
||||
builder.IncludeUninitialized(*o.IncludeUninitialized)
|
||||
}
|
||||
|
||||
if !o.StopOnFirstError {
|
||||
builder.ContinueOnError()
|
||||
}
|
||||
|
|
|
|||
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/builder_flags_fake.go
generated
vendored
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/builder_flags_fake.go
generated
vendored
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
package genericclioptions
|
||||
|
||||
import (
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/resource"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// NewSimpleResourceFinder builds a super simple ResourceFinder that just iterates over the objects you provided
|
||||
|
|
|
|||
35
vendor/k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go
generated
vendored
35
vendor/k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go
generated
vendored
|
|
@ -21,12 +21,14 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/client-go/discovery"
|
||||
diskcached "k8s.io/client-go/discovery/cached/disk"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
|
@ -92,6 +94,13 @@ type ConfigFlags struct {
|
|||
Username *string
|
||||
Password *string
|
||||
Timeout *string
|
||||
|
||||
clientConfig clientcmd.ClientConfig
|
||||
lock sync.Mutex
|
||||
// If set to true, will use persistent client config and
|
||||
// propagate the config to the places that need it, rather than
|
||||
// loading the config multiple times
|
||||
usePersistentConfig bool
|
||||
}
|
||||
|
||||
// ToRESTConfig implements RESTClientGetter.
|
||||
|
|
@ -106,6 +115,13 @@ func (f *ConfigFlags) ToRESTConfig() (*rest.Config, error) {
|
|||
// Returns an interactive clientConfig if the password flag is enabled,
|
||||
// or a non-interactive clientConfig otherwise.
|
||||
func (f *ConfigFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
if f.usePersistentConfig {
|
||||
return f.toRawKubePersistentConfigLoader()
|
||||
}
|
||||
return f.toRawKubeConfigLoader()
|
||||
}
|
||||
|
||||
func (f *ConfigFlags) toRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
// use the standard defaults for this client command
|
||||
// DEPRECATED: remove and replace with something more accurate
|
||||
|
|
@ -181,6 +197,19 @@ func (f *ConfigFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
|||
return clientConfig
|
||||
}
|
||||
|
||||
// toRawKubePersistentConfigLoader binds config flag values to config overrides
|
||||
// Returns a persistent clientConfig for propagation.
|
||||
func (f *ConfigFlags) toRawKubePersistentConfigLoader() clientcmd.ClientConfig {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
|
||||
if f.clientConfig == nil {
|
||||
f.clientConfig = f.toRawKubeConfigLoader()
|
||||
}
|
||||
|
||||
return f.clientConfig
|
||||
}
|
||||
|
||||
// ToDiscoveryClient implements RESTClientGetter.
|
||||
// Expects the AddFlags method to have been called.
|
||||
// Returns a CachedDiscoveryInterface using a computed RESTConfig.
|
||||
|
|
@ -203,7 +232,7 @@ func (f *ConfigFlags) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, e
|
|||
}
|
||||
|
||||
discoveryCacheDir := computeDiscoverCacheDir(filepath.Join(homedir.HomeDir(), ".kube", "cache", "discovery"), config.Host)
|
||||
return discovery.NewCachedDiscoveryClientForConfig(config, discoveryCacheDir, httpCacheDir, time.Duration(10*time.Minute))
|
||||
return diskcached.NewCachedDiscoveryClientForConfig(config, discoveryCacheDir, httpCacheDir, time.Duration(10*time.Minute))
|
||||
}
|
||||
|
||||
// ToRESTMapper returns a mapper.
|
||||
|
|
@ -285,7 +314,7 @@ func (f *ConfigFlags) WithDeprecatedPasswordFlag() *ConfigFlags {
|
|||
}
|
||||
|
||||
// NewConfigFlags returns ConfigFlags with default values set
|
||||
func NewConfigFlags() *ConfigFlags {
|
||||
func NewConfigFlags(usePersistentConfig bool) *ConfigFlags {
|
||||
impersonateGroup := []string{}
|
||||
insecure := false
|
||||
|
||||
|
|
@ -306,6 +335,8 @@ func NewConfigFlags() *ConfigFlags {
|
|||
BearerToken: stringptr(""),
|
||||
Impersonate: stringptr(""),
|
||||
ImpersonateGroup: &impersonateGroup,
|
||||
|
||||
usePersistentConfig: usePersistentConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/doc.go
generated
vendored
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/doc.go
generated
vendored
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// Package genericclioptions contains flags which can be added to you command, bound, completed, and produce
|
||||
// useful helper functions. Nothing in this package can depend on kube/kube
|
||||
package genericclioptions
|
||||
package genericclioptions // import "k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
|
|
|
|||
10
vendor/k8s.io/cli-runtime/pkg/genericclioptions/filename_flags.go
generated
vendored
10
vendor/k8s.io/cli-runtime/pkg/genericclioptions/filename_flags.go
generated
vendored
|
|
@ -22,7 +22,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/resource"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// Usage of this struct by itself is discouraged.
|
||||
|
|
@ -32,6 +32,7 @@ type FileNameFlags struct {
|
|||
Usage string
|
||||
|
||||
Filenames *[]string
|
||||
Kustomize *string
|
||||
Recursive *bool
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +49,9 @@ func (o *FileNameFlags) ToOptions() resource.FilenameOptions {
|
|||
if o.Filenames != nil {
|
||||
options.Filenames = *o.Filenames
|
||||
}
|
||||
if o.Kustomize != nil {
|
||||
options.Kustomize = *o.Kustomize
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
|
@ -68,4 +72,8 @@ func (o *FileNameFlags) AddFlags(flags *pflag.FlagSet) {
|
|||
}
|
||||
flags.SetAnnotation("filename", cobra.BashCompFilenameExt, annotations)
|
||||
}
|
||||
if o.Kustomize != nil {
|
||||
flags.StringVarP(o.Kustomize, "kustomize", "k", *o.Kustomize,
|
||||
"Process a kustomization directory. This flag can't be used together with -f or -R.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/json_yaml_flags.go
generated
vendored
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/json_yaml_flags.go
generated
vendored
|
|
@ -21,7 +21,7 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
func (f *JSONYamlPrintFlags) AllowedFormats() []string {
|
||||
|
|
|
|||
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/jsonpath_flags.go
generated
vendored
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/jsonpath_flags.go
generated
vendored
|
|
@ -24,7 +24,7 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// templates are logically optional for specifying a format.
|
||||
|
|
|
|||
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/kube_template_flags.go
generated
vendored
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/kube_template_flags.go
generated
vendored
|
|
@ -19,7 +19,7 @@ package genericclioptions
|
|||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// KubeTemplatePrintFlags composes print flags that provide both a JSONPath and a go-template printer.
|
||||
|
|
|
|||
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/name_flags.go
generated
vendored
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/name_flags.go
generated
vendored
|
|
@ -22,7 +22,7 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// NamePrintFlags provides default flags necessary for printing
|
||||
|
|
|
|||
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/print_flags.go
generated
vendored
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/print_flags.go
generated
vendored
|
|
@ -24,7 +24,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
type NoCompatiblePrinterError struct {
|
||||
|
|
|
|||
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/template_flags.go
generated
vendored
2
vendor/k8s.io/cli-runtime/pkg/genericclioptions/template_flags.go
generated
vendored
|
|
@ -24,7 +24,7 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// templates are logically optional for specifying a format.
|
||||
|
|
|
|||
32
vendor/k8s.io/cli-runtime/pkg/kustomize/builder.go
generated
vendored
Normal file
32
vendor/k8s.io/cli-runtime/pkg/kustomize/builder.go
generated
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kustomize
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps"
|
||||
"sigs.k8s.io/kustomize/pkg/commands/build"
|
||||
"sigs.k8s.io/kustomize/pkg/fs"
|
||||
)
|
||||
|
||||
// RunKustomizeBuild runs kustomize build given a filesystem and a path
|
||||
func RunKustomizeBuild(out io.Writer, fSys fs.FileSystem, path string) error {
|
||||
f := k8sdeps.NewFactory()
|
||||
o := build.NewOptions(path, "")
|
||||
return o.RunBuild(out, fSys, f.ResmapF, f.TransformerF)
|
||||
}
|
||||
126
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory.go
generated
vendored
Normal file
126
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/configmapfactory.go
generated
vendored
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
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 configmapandsecret generates configmaps and secrets per generator rules.
|
||||
package configmapandsecret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/api/core/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv"
|
||||
"sigs.k8s.io/kustomize/pkg/ifc"
|
||||
"sigs.k8s.io/kustomize/pkg/types"
|
||||
)
|
||||
|
||||
// ConfigMapFactory makes ConfigMaps.
|
||||
type ConfigMapFactory struct {
|
||||
ldr ifc.Loader
|
||||
}
|
||||
|
||||
// NewConfigMapFactory returns a new ConfigMapFactory.
|
||||
func NewConfigMapFactory(l ifc.Loader) *ConfigMapFactory {
|
||||
return &ConfigMapFactory{ldr: l}
|
||||
}
|
||||
|
||||
func (f *ConfigMapFactory) makeFreshConfigMap(
|
||||
args *types.ConfigMapArgs) *corev1.ConfigMap {
|
||||
cm := &corev1.ConfigMap{}
|
||||
cm.APIVersion = "v1"
|
||||
cm.Kind = "ConfigMap"
|
||||
cm.Name = args.Name
|
||||
cm.Namespace = args.Namespace
|
||||
cm.Data = map[string]string{}
|
||||
return cm
|
||||
}
|
||||
|
||||
// MakeConfigMap returns a new ConfigMap, or nil and an error.
|
||||
func (f *ConfigMapFactory) MakeConfigMap(
|
||||
args *types.ConfigMapArgs, options *types.GeneratorOptions) (*corev1.ConfigMap, error) {
|
||||
var all []kv.Pair
|
||||
var err error
|
||||
cm := f.makeFreshConfigMap(args)
|
||||
|
||||
pairs, err := keyValuesFromEnvFile(f.ldr, args.EnvSource)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf(
|
||||
"env source file: %s",
|
||||
args.EnvSource))
|
||||
}
|
||||
all = append(all, pairs...)
|
||||
|
||||
pairs, err = keyValuesFromLiteralSources(args.LiteralSources)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf(
|
||||
"literal sources %v", args.LiteralSources))
|
||||
}
|
||||
all = append(all, pairs...)
|
||||
|
||||
pairs, err = keyValuesFromFileSources(f.ldr, args.FileSources)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf(
|
||||
"file sources: %v", args.FileSources))
|
||||
}
|
||||
all = append(all, pairs...)
|
||||
|
||||
for _, p := range all {
|
||||
err = addKvToConfigMap(cm, p.Key, p.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options != nil {
|
||||
cm.SetLabels(options.Labels)
|
||||
cm.SetAnnotations(options.Annotations)
|
||||
}
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
// addKvToConfigMap adds the given key and data to the given config map.
|
||||
// Error if key invalid, or already exists.
|
||||
func addKvToConfigMap(configMap *v1.ConfigMap, keyName, data string) error {
|
||||
// Note, the rules for ConfigMap keys are the exact same as the ones for SecretKeys.
|
||||
if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 {
|
||||
return fmt.Errorf("%q is not a valid key name for a ConfigMap: %s", keyName, strings.Join(errs, ";"))
|
||||
}
|
||||
|
||||
keyExistsErrorMsg := "cannot add key %s, another key by that name already exists: %v"
|
||||
|
||||
// If the configmap data contains byte sequences that are all in the UTF-8
|
||||
// range, we will write it to .Data
|
||||
if utf8.Valid([]byte(data)) {
|
||||
if _, entryExists := configMap.Data[keyName]; entryExists {
|
||||
return fmt.Errorf(keyExistsErrorMsg, keyName, configMap.Data)
|
||||
}
|
||||
configMap.Data[keyName] = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherwise, it's BinaryData
|
||||
if configMap.BinaryData == nil {
|
||||
configMap.BinaryData = map[string][]byte{}
|
||||
}
|
||||
if _, entryExists := configMap.BinaryData[keyName]; entryExists {
|
||||
return fmt.Errorf(keyExistsErrorMsg, keyName, configMap.BinaryData)
|
||||
}
|
||||
configMap.BinaryData[keyName] = []byte(data)
|
||||
return nil
|
||||
}
|
||||
107
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv.go
generated
vendored
Normal file
107
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/kv.go
generated
vendored
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package configmapandsecret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv"
|
||||
"sigs.k8s.io/kustomize/pkg/ifc"
|
||||
)
|
||||
|
||||
func keyValuesFromLiteralSources(sources []string) ([]kv.Pair, error) {
|
||||
var kvs []kv.Pair
|
||||
for _, s := range sources {
|
||||
k, v, err := parseLiteralSource(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kvs = append(kvs, kv.Pair{Key: k, Value: v})
|
||||
}
|
||||
return kvs, nil
|
||||
}
|
||||
|
||||
func keyValuesFromFileSources(ldr ifc.Loader, sources []string) ([]kv.Pair, error) {
|
||||
var kvs []kv.Pair
|
||||
for _, s := range sources {
|
||||
k, fPath, err := parseFileSource(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := ldr.Load(fPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kvs = append(kvs, kv.Pair{Key: k, Value: string(content)})
|
||||
}
|
||||
return kvs, nil
|
||||
}
|
||||
|
||||
func keyValuesFromEnvFile(l ifc.Loader, path string) ([]kv.Pair, error) {
|
||||
if path == "" {
|
||||
return nil, nil
|
||||
}
|
||||
content, err := l.Load(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kv.KeyValuesFromLines(content)
|
||||
}
|
||||
|
||||
// parseFileSource parses the source given.
|
||||
//
|
||||
// Acceptable formats include:
|
||||
// 1. source-path: the basename will become the key name
|
||||
// 2. source-name=source-path: the source-name will become the key name and
|
||||
// source-path is the path to the key file.
|
||||
//
|
||||
// Key names cannot include '='.
|
||||
func parseFileSource(source string) (keyName, filePath string, err error) {
|
||||
numSeparators := strings.Count(source, "=")
|
||||
switch {
|
||||
case numSeparators == 0:
|
||||
return path.Base(source), source, nil
|
||||
case numSeparators == 1 && strings.HasPrefix(source, "="):
|
||||
return "", "", fmt.Errorf("key name for file path %v missing", strings.TrimPrefix(source, "="))
|
||||
case numSeparators == 1 && strings.HasSuffix(source, "="):
|
||||
return "", "", fmt.Errorf("file path for key name %v missing", strings.TrimSuffix(source, "="))
|
||||
case numSeparators > 1:
|
||||
return "", "", errors.New("key names or file paths cannot contain '='")
|
||||
default:
|
||||
components := strings.Split(source, "=")
|
||||
return components[0], components[1], nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseLiteralSource parses the source key=val pair into its component pieces.
|
||||
// This functionality is distinguished from strings.SplitN(source, "=", 2) since
|
||||
// it returns an error in the case of empty keys, values, or a missing equals sign.
|
||||
func parseLiteralSource(source string) (keyName, value string, err error) {
|
||||
// leading equal is invalid
|
||||
if strings.Index(source, "=") == 0 {
|
||||
return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source)
|
||||
}
|
||||
// split after the first equal (so values can have the = character)
|
||||
items := strings.SplitN(source, "=", 2)
|
||||
if len(items) != 2 {
|
||||
return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source)
|
||||
}
|
||||
return items[0], strings.Trim(items[1], "\"'"), nil
|
||||
}
|
||||
106
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory.go
generated
vendored
Normal file
106
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret/secretfactory.go
generated
vendored
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
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 configmapandsecret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv"
|
||||
"sigs.k8s.io/kustomize/pkg/ifc"
|
||||
"sigs.k8s.io/kustomize/pkg/types"
|
||||
)
|
||||
|
||||
// SecretFactory makes Secrets.
|
||||
type SecretFactory struct {
|
||||
ldr ifc.Loader
|
||||
}
|
||||
|
||||
// NewSecretFactory returns a new SecretFactory.
|
||||
func NewSecretFactory(ldr ifc.Loader) *SecretFactory {
|
||||
return &SecretFactory{ldr: ldr}
|
||||
}
|
||||
|
||||
func (f *SecretFactory) makeFreshSecret(args *types.SecretArgs) *corev1.Secret {
|
||||
s := &corev1.Secret{}
|
||||
s.APIVersion = "v1"
|
||||
s.Kind = "Secret"
|
||||
s.Name = args.Name
|
||||
s.Namespace = args.Namespace
|
||||
s.Type = corev1.SecretType(args.Type)
|
||||
if s.Type == "" {
|
||||
s.Type = corev1.SecretTypeOpaque
|
||||
}
|
||||
s.Data = map[string][]byte{}
|
||||
return s
|
||||
}
|
||||
|
||||
// MakeSecret returns a new secret.
|
||||
func (f *SecretFactory) MakeSecret(args *types.SecretArgs, options *types.GeneratorOptions) (*corev1.Secret, error) {
|
||||
var all []kv.Pair
|
||||
var err error
|
||||
s := f.makeFreshSecret(args)
|
||||
|
||||
pairs, err := keyValuesFromEnvFile(f.ldr, args.EnvSource)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf(
|
||||
"env source file: %s",
|
||||
args.EnvSource))
|
||||
}
|
||||
all = append(all, pairs...)
|
||||
|
||||
pairs, err = keyValuesFromLiteralSources(args.LiteralSources)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf(
|
||||
"literal sources %v", args.LiteralSources))
|
||||
}
|
||||
all = append(all, pairs...)
|
||||
|
||||
pairs, err = keyValuesFromFileSources(f.ldr, args.FileSources)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf(
|
||||
"file sources: %v", args.FileSources))
|
||||
}
|
||||
all = append(all, pairs...)
|
||||
|
||||
for _, p := range all {
|
||||
err = addKvToSecret(s, p.Key, p.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options != nil {
|
||||
s.SetLabels(options.Labels)
|
||||
s.SetAnnotations(options.Annotations)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func addKvToSecret(secret *corev1.Secret, keyName, data string) error {
|
||||
// Note, the rules for SecretKeys keys are the exact same as the ones for ConfigMap.
|
||||
if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 {
|
||||
return fmt.Errorf("%q is not a valid key name for a Secret: %s", keyName, strings.Join(errs, ";"))
|
||||
}
|
||||
if _, entryExists := secret.Data[keyName]; entryExists {
|
||||
return fmt.Errorf("cannot add key %s, another key by that name already exists", keyName)
|
||||
}
|
||||
secret.Data[keyName] = []byte(data)
|
||||
return nil
|
||||
}
|
||||
76
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/doc.go
generated
vendored
Normal file
76
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// It's possible that kustomize's features will be vendored into
|
||||
// the kubernetes/kubernetes repo and made available to kubectl
|
||||
// commands, while at the same time the kustomize program will
|
||||
// continue to exist as an independent CLI. Vendoring snapshots
|
||||
// would be taken just before a kubectl release.
|
||||
//
|
||||
// This creates a problem in that freestanding-kustomize depends on
|
||||
// (for example):
|
||||
//
|
||||
// https://github.com/kubernetes/apimachinery/
|
||||
// tree/master/pkg/util/yaml
|
||||
//
|
||||
// It vendors that package into
|
||||
// sigs.k8s.io/kustomize/vendor/k8s.io/apimachinery/
|
||||
//
|
||||
// Whereas kubectl-kustomize would have to depend on the "staging"
|
||||
// version of this code, located at
|
||||
//
|
||||
// https://github.com/kubernetes/kubernetes/
|
||||
// blob/master/staging/src/k8s.io/apimachinery/pkg/util/yaml
|
||||
//
|
||||
// which is "vendored" via symlinks:
|
||||
// k8s.io/kubernetes/vendor/k8s.io/apimachinery
|
||||
// is a symlink to
|
||||
// ../../staging/src/k8s.io/apimachinery
|
||||
//
|
||||
// The staging version is the canonical, under-development
|
||||
// version of the code that kubectl depends on, whereas the packages
|
||||
// at kubernetes/apimachinery are periodic snapshots of staging made
|
||||
// for outside tools to depend on.
|
||||
//
|
||||
// apimachinery isn't the only package that poses this problem, just
|
||||
// using it as a specific example.
|
||||
//
|
||||
// The kubectl binary cannot vendor in kustomize code that in
|
||||
// turn vendors in the non-staging packages.
|
||||
//
|
||||
// One way to fix some of this would be to copy code - a hard fork.
|
||||
// This has all the problems associated with a hard forking.
|
||||
//
|
||||
// Another way would be to break the kustomize repo into three:
|
||||
//
|
||||
// (1) kustomize - repo with the main() function,
|
||||
// vendoring (2) and (3).
|
||||
//
|
||||
// (2) kustomize-libs - packages used by (1) with no
|
||||
// apimachinery dependence.
|
||||
//
|
||||
// (3) kustomize-k8sdeps - A thin code layer that depends
|
||||
// on (vendors) apimachinery to provide thin implementations
|
||||
// to interfaces used in (2).
|
||||
//
|
||||
// The kubectl repo would then vendor from (2) only, and have
|
||||
// a local implementation of (3). With that in mind, it's clear
|
||||
// that (3) doesn't have to be a repo; the kustomize version of
|
||||
// the thin layer can live directly in (1).
|
||||
//
|
||||
// This package is the code in (3), meant for kustomize.
|
||||
|
||||
package k8sdeps
|
||||
34
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/factory.go
generated
vendored
Normal file
34
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/factory.go
generated
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
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 k8sdeps provides kustomize factory with k8s dependencies
|
||||
package k8sdeps
|
||||
|
||||
import (
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct"
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer"
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator"
|
||||
"sigs.k8s.io/kustomize/pkg/factory"
|
||||
)
|
||||
|
||||
// NewFactory creates an instance of KustFactory using k8sdeps factories
|
||||
func NewFactory() *factory.KustFactory {
|
||||
return factory.NewKustFactory(
|
||||
kunstruct.NewKunstructuredFactoryImpl(),
|
||||
validator.NewKustValidator(),
|
||||
transformer.NewFactoryImpl(),
|
||||
)
|
||||
}
|
||||
118
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory.go
generated
vendored
Normal file
118
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/factory.go
generated
vendored
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
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 kunstruct
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps/configmapandsecret"
|
||||
"sigs.k8s.io/kustomize/pkg/ifc"
|
||||
"sigs.k8s.io/kustomize/pkg/types"
|
||||
)
|
||||
|
||||
// KunstructuredFactoryImpl hides construction using apimachinery types.
|
||||
type KunstructuredFactoryImpl struct {
|
||||
cmFactory *configmapandsecret.ConfigMapFactory
|
||||
secretFactory *configmapandsecret.SecretFactory
|
||||
}
|
||||
|
||||
var _ ifc.KunstructuredFactory = &KunstructuredFactoryImpl{}
|
||||
|
||||
// NewKunstructuredFactoryImpl returns a factory.
|
||||
func NewKunstructuredFactoryImpl() ifc.KunstructuredFactory {
|
||||
return &KunstructuredFactoryImpl{}
|
||||
}
|
||||
|
||||
// SliceFromBytes returns a slice of Kunstructured.
|
||||
func (kf *KunstructuredFactoryImpl) SliceFromBytes(
|
||||
in []byte) ([]ifc.Kunstructured, error) {
|
||||
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(in), 1024)
|
||||
var result []ifc.Kunstructured
|
||||
var err error
|
||||
for err == nil || isEmptyYamlError(err) {
|
||||
var out unstructured.Unstructured
|
||||
err = decoder.Decode(&out)
|
||||
if err == nil {
|
||||
if len(out.Object) == 0 {
|
||||
continue
|
||||
}
|
||||
err = kf.validate(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, &UnstructAdapter{Unstructured: out})
|
||||
}
|
||||
}
|
||||
if err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isEmptyYamlError(err error) bool {
|
||||
return strings.Contains(err.Error(), "is missing in 'null'")
|
||||
}
|
||||
|
||||
// FromMap returns an instance of Kunstructured.
|
||||
func (kf *KunstructuredFactoryImpl) FromMap(
|
||||
m map[string]interface{}) ifc.Kunstructured {
|
||||
return &UnstructAdapter{Unstructured: unstructured.Unstructured{Object: m}}
|
||||
}
|
||||
|
||||
// MakeConfigMap returns an instance of Kunstructured for ConfigMap
|
||||
func (kf *KunstructuredFactoryImpl) MakeConfigMap(args *types.ConfigMapArgs, options *types.GeneratorOptions) (ifc.Kunstructured, error) {
|
||||
cm, err := kf.cmFactory.MakeConfigMap(args, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewKunstructuredFromObject(cm)
|
||||
}
|
||||
|
||||
// MakeSecret returns an instance of Kunstructured for Secret
|
||||
func (kf *KunstructuredFactoryImpl) MakeSecret(args *types.SecretArgs, options *types.GeneratorOptions) (ifc.Kunstructured, error) {
|
||||
sec, err := kf.secretFactory.MakeSecret(args, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewKunstructuredFromObject(sec)
|
||||
}
|
||||
|
||||
// Set sets loader
|
||||
func (kf *KunstructuredFactoryImpl) Set(ldr ifc.Loader) {
|
||||
kf.cmFactory = configmapandsecret.NewConfigMapFactory(ldr)
|
||||
kf.secretFactory = configmapandsecret.NewSecretFactory(ldr)
|
||||
}
|
||||
|
||||
// validate validates that u has kind and name
|
||||
// except for kind `List`, which doesn't require a name
|
||||
func (kf *KunstructuredFactoryImpl) validate(u unstructured.Unstructured) error {
|
||||
kind := u.GetKind()
|
||||
if kind == "" {
|
||||
return fmt.Errorf("missing kind in object %v", u)
|
||||
} else if strings.HasSuffix(kind, "List") {
|
||||
return nil
|
||||
}
|
||||
if u.GetName() == "" {
|
||||
return fmt.Errorf("missing metadata.name in object %v", u)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
71
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/helper.go
generated
vendored
Normal file
71
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/helper.go
generated
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
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 kunstruct provides unstructured from api machinery and factory for creating unstructured
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseFields(path string) ([]string, error) {
|
||||
if !strings.Contains(path, "[") {
|
||||
return strings.Split(path, "."), nil
|
||||
}
|
||||
|
||||
var fields []string
|
||||
start := 0
|
||||
insideParentheses := false
|
||||
for i := range path {
|
||||
switch path[i] {
|
||||
case '.':
|
||||
if !insideParentheses {
|
||||
fields = append(fields, path[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
case '[':
|
||||
if !insideParentheses {
|
||||
if i == start {
|
||||
start = i + 1
|
||||
} else {
|
||||
fields = append(fields, path[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
insideParentheses = true
|
||||
} else {
|
||||
return nil, fmt.Errorf("nested parentheses are not allowed: %s", path)
|
||||
}
|
||||
case ']':
|
||||
if insideParentheses {
|
||||
fields = append(fields, path[start:i])
|
||||
start = i + 1
|
||||
insideParentheses = false
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid field path %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
if start < len(path)-1 {
|
||||
fields = append(fields, path[start:])
|
||||
}
|
||||
for i, f := range fields {
|
||||
if strings.HasPrefix(f, "\"") || strings.HasPrefix(f, "'") {
|
||||
fields[i] = strings.Trim(f, "\"'")
|
||||
}
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
92
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct.go
generated
vendored
Normal file
92
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kunstruct/kunstruct.go
generated
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
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 kunstruct provides unstructured from api machinery and factory for creating unstructured
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/kustomize/pkg/gvk"
|
||||
"sigs.k8s.io/kustomize/pkg/ifc"
|
||||
)
|
||||
|
||||
var _ ifc.Kunstructured = &UnstructAdapter{}
|
||||
|
||||
// UnstructAdapter wraps unstructured.Unstructured from
|
||||
// https://github.com/kubernetes/apimachinery/blob/master/
|
||||
// pkg/apis/meta/v1/unstructured/unstructured.go
|
||||
// to isolate dependence on apimachinery.
|
||||
type UnstructAdapter struct {
|
||||
unstructured.Unstructured
|
||||
}
|
||||
|
||||
// NewKunstructuredFromObject returns a new instance of Kunstructured.
|
||||
func NewKunstructuredFromObject(obj runtime.Object) (ifc.Kunstructured, error) {
|
||||
// Convert obj to a byte stream, then convert that to JSON (Unstructured).
|
||||
marshaled, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return &UnstructAdapter{}, err
|
||||
}
|
||||
var u unstructured.Unstructured
|
||||
err = u.UnmarshalJSON(marshaled)
|
||||
// creationTimestamp always 'null', remove it
|
||||
u.SetCreationTimestamp(metav1.Time{})
|
||||
return &UnstructAdapter{Unstructured: u}, err
|
||||
}
|
||||
|
||||
// GetGvk returns the Gvk name of the object.
|
||||
func (fs *UnstructAdapter) GetGvk() gvk.Gvk {
|
||||
x := fs.GroupVersionKind()
|
||||
return gvk.Gvk{
|
||||
Group: x.Group,
|
||||
Version: x.Version,
|
||||
Kind: x.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
// Copy provides a copy behind an interface.
|
||||
func (fs *UnstructAdapter) Copy() ifc.Kunstructured {
|
||||
return &UnstructAdapter{*fs.DeepCopy()}
|
||||
}
|
||||
|
||||
// Map returns the unstructured content map.
|
||||
func (fs *UnstructAdapter) Map() map[string]interface{} {
|
||||
return fs.Object
|
||||
}
|
||||
|
||||
// SetMap overrides the unstructured content map.
|
||||
func (fs *UnstructAdapter) SetMap(m map[string]interface{}) {
|
||||
fs.Object = m
|
||||
}
|
||||
|
||||
// GetFieldValue returns value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetFieldValue(path string) (string, error) {
|
||||
fields, err := parseFields(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s, found, err := unstructured.NestedString(
|
||||
fs.UnstructuredContent(), fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return "", fmt.Errorf("no field named '%s'", path)
|
||||
}
|
||||
102
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv.go
generated
vendored
Normal file
102
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/kv/kv.go
generated
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
)
|
||||
|
||||
// Pair represents a <key, value> pair.
|
||||
type Pair struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
|
||||
|
||||
// KeyValuesFromLines parses given content in to a list of key-value pairs.
|
||||
func KeyValuesFromLines(content []byte) ([]Pair, error) {
|
||||
var kvs []Pair
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(content))
|
||||
currentLine := 0
|
||||
for scanner.Scan() {
|
||||
// Process the current line, retrieving a key/value pair if
|
||||
// possible.
|
||||
scannedBytes := scanner.Bytes()
|
||||
kv, err := KeyValuesFromLine(scannedBytes, currentLine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentLine++
|
||||
|
||||
if len(kv.Key) == 0 {
|
||||
// no key means line was empty or a comment
|
||||
continue
|
||||
}
|
||||
|
||||
kvs = append(kvs, kv)
|
||||
}
|
||||
return kvs, nil
|
||||
}
|
||||
|
||||
// KeyValuesFromLine returns a kv with blank key if the line is empty or a comment.
|
||||
// The value will be retrieved from the environment if necessary.
|
||||
func KeyValuesFromLine(line []byte, currentLine int) (Pair, error) {
|
||||
kv := Pair{}
|
||||
|
||||
if !utf8.Valid(line) {
|
||||
return kv, fmt.Errorf("line %d has invalid utf8 bytes : %v", line, string(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 kv, nil
|
||||
}
|
||||
|
||||
data := strings.SplitN(string(line), "=", 2)
|
||||
key := data[0]
|
||||
if errs := validation.IsEnvVarName(key); len(errs) != 0 {
|
||||
return kv, fmt.Errorf("%q is not a valid key name: %s", key, strings.Join(errs, ";"))
|
||||
}
|
||||
|
||||
if len(data) == 2 {
|
||||
kv.Value = data[1]
|
||||
} else {
|
||||
// No value (no `=` in the line) is a signal to obtain the value
|
||||
// from the environment.
|
||||
kv.Value = os.Getenv(key)
|
||||
}
|
||||
kv.Key = key
|
||||
return kv, nil
|
||||
}
|
||||
43
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/factory.go
generated
vendored
Normal file
43
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/factory.go
generated
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
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 transformer provides transformer factory
|
||||
package transformer
|
||||
|
||||
import (
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash"
|
||||
"k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch"
|
||||
"sigs.k8s.io/kustomize/pkg/resource"
|
||||
"sigs.k8s.io/kustomize/pkg/transformers"
|
||||
)
|
||||
|
||||
// FactoryImpl makes patch transformer and name hash transformer
|
||||
type FactoryImpl struct{}
|
||||
|
||||
// NewFactoryImpl makes a new factoryImpl instance
|
||||
func NewFactoryImpl() *FactoryImpl {
|
||||
return &FactoryImpl{}
|
||||
}
|
||||
|
||||
// MakePatchTransformer makes a new patch transformer
|
||||
func (p *FactoryImpl) MakePatchTransformer(slice []*resource.Resource, rf *resource.Factory) (transformers.Transformer, error) {
|
||||
return patch.NewPatchTransformer(slice, rf)
|
||||
}
|
||||
|
||||
// MakeHashTransformer makes a new name hash transformer
|
||||
func (p *FactoryImpl) MakeHashTransformer() transformers.Transformer {
|
||||
return hash.NewNameHashTransformer()
|
||||
}
|
||||
168
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash.go
generated
vendored
Normal file
168
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/hash.go
generated
vendored
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
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 hash
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// KustHash compute hash for unstructured objects
|
||||
type KustHash struct{}
|
||||
|
||||
// NewKustHash returns a KustHash object
|
||||
func NewKustHash() *KustHash {
|
||||
return &KustHash{}
|
||||
}
|
||||
|
||||
// Hash returns a hash of either a ConfigMap or a Secret
|
||||
func (h *KustHash) Hash(m map[string]interface{}) (string, error) {
|
||||
u := unstructured.Unstructured{
|
||||
Object: m,
|
||||
}
|
||||
kind := u.GetKind()
|
||||
switch kind {
|
||||
case "ConfigMap":
|
||||
cm, err := unstructuredToConfigmap(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ConfigMapHash(cm)
|
||||
case "Secret":
|
||||
sec, err := unstructuredToSecret(u)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return SecretHash(sec)
|
||||
default:
|
||||
return "", fmt.Errorf("type %s is supported for hashing in %v", kind, m)
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigMapHash returns a hash of the ConfigMap.
|
||||
// The Data, Kind, and Name are taken into account.
|
||||
func ConfigMapHash(cm *v1.ConfigMap) (string, error) {
|
||||
encoded, err := encodeConfigMap(cm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h, err := encodeHash(hash(encoded))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// SecretHash returns a hash of the Secret.
|
||||
// The Data, Kind, Name, and Type are taken into account.
|
||||
func SecretHash(sec *v1.Secret) (string, error) {
|
||||
encoded, err := encodeSecret(sec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h, err := encodeHash(hash(encoded))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// encodeConfigMap encodes a ConfigMap.
|
||||
// Data, Kind, and Name are taken into account.
|
||||
func encodeConfigMap(cm *v1.ConfigMap) (string, error) {
|
||||
// json.Marshal sorts the keys in a stable order in the encoding
|
||||
m := map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data}
|
||||
if len(cm.BinaryData) > 0 {
|
||||
m["binaryData"] = cm.BinaryData
|
||||
}
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// encodeSecret encodes a Secret.
|
||||
// Data, Kind, Name, and Type are taken into account.
|
||||
func encodeSecret(sec *v1.Secret) (string, error) {
|
||||
// json.Marshal sorts the keys in a stable order in the encoding
|
||||
data, err := json.Marshal(map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// encodeHash extracts the first 40 bits of the hash from the hex string
|
||||
// (1 hex char represents 4 bits), and then maps vowels and vowel-like hex
|
||||
// characters to consonants to prevent bad words from being formed (the theory
|
||||
// is that no vowels makes it really hard to make bad words). Since the string
|
||||
// is hex, the only vowels it can contain are 'a' and 'e'.
|
||||
// We picked some arbitrary consonants to map to from the same character set as GenerateName.
|
||||
// See: https://github.com/kubernetes/apimachinery/blob/dc1f89aff9a7509782bde3b68824c8043a3e58cc/pkg/util/rand/rand.go#L75
|
||||
// If the hex string contains fewer than ten characters, returns an error.
|
||||
func encodeHash(hex string) (string, error) {
|
||||
if len(hex) < 10 {
|
||||
return "", fmt.Errorf("the hex string must contain at least 10 characters")
|
||||
}
|
||||
enc := []rune(hex[:10])
|
||||
for i := range enc {
|
||||
switch enc[i] {
|
||||
case '0':
|
||||
enc[i] = 'g'
|
||||
case '1':
|
||||
enc[i] = 'h'
|
||||
case '3':
|
||||
enc[i] = 'k'
|
||||
case 'a':
|
||||
enc[i] = 'm'
|
||||
case 'e':
|
||||
enc[i] = 't'
|
||||
}
|
||||
}
|
||||
return string(enc), nil
|
||||
}
|
||||
|
||||
// hash hashes `data` with sha256 and returns the hex string
|
||||
func hash(data string) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
|
||||
}
|
||||
|
||||
func unstructuredToConfigmap(u unstructured.Unstructured) (*v1.ConfigMap, error) {
|
||||
marshaled, err := json.Marshal(u.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out v1.ConfigMap
|
||||
err = json.Unmarshal(marshaled, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func unstructuredToSecret(u unstructured.Unstructured) (*v1.Secret, error) {
|
||||
marshaled, err := json.Marshal(u.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out v1.Secret
|
||||
err = json.Unmarshal(marshaled, &out)
|
||||
return &out, err
|
||||
}
|
||||
47
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash.go
generated
vendored
Normal file
47
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/hash/namehash.go
generated
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
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 hash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/pkg/resmap"
|
||||
"sigs.k8s.io/kustomize/pkg/transformers"
|
||||
)
|
||||
|
||||
type nameHashTransformer struct{}
|
||||
|
||||
var _ transformers.Transformer = &nameHashTransformer{}
|
||||
|
||||
// NewNameHashTransformer construct a nameHashTransformer.
|
||||
func NewNameHashTransformer() transformers.Transformer {
|
||||
return &nameHashTransformer{}
|
||||
}
|
||||
|
||||
// Transform appends hash to generated resources.
|
||||
func (o *nameHashTransformer) Transform(m resmap.ResMap) error {
|
||||
for _, res := range m {
|
||||
if res.NeedHashSuffix() {
|
||||
h, err := NewKustHash().Hash(res.Map())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.SetName(fmt.Sprintf("%s-%s", res.GetName(), h))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
174
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch.go
generated
vendored
Normal file
174
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patch.go
generated
vendored
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
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 patch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/evanphx/json-patch"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/kustomize/pkg/gvk"
|
||||
"sigs.k8s.io/kustomize/pkg/resmap"
|
||||
"sigs.k8s.io/kustomize/pkg/resource"
|
||||
"sigs.k8s.io/kustomize/pkg/transformers"
|
||||
)
|
||||
|
||||
// patchTransformer applies patches.
|
||||
type patchTransformer struct {
|
||||
patches []*resource.Resource
|
||||
rf *resource.Factory
|
||||
}
|
||||
|
||||
var _ transformers.Transformer = &patchTransformer{}
|
||||
|
||||
// NewPatchTransformer constructs a patchTransformer.
|
||||
func NewPatchTransformer(
|
||||
slice []*resource.Resource, rf *resource.Factory) (transformers.Transformer, error) {
|
||||
if len(slice) == 0 {
|
||||
return transformers.NewNoOpTransformer(), nil
|
||||
}
|
||||
return &patchTransformer{patches: slice, rf: rf}, nil
|
||||
}
|
||||
|
||||
// Transform apply the patches on top of the base resources.
|
||||
func (pt *patchTransformer) Transform(baseResourceMap resmap.ResMap) error {
|
||||
// Merge and then index the patches by Id.
|
||||
patches, err := pt.mergePatches()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Strategic merge the resources exist in both base and patches.
|
||||
for _, patch := range patches {
|
||||
// Merge patches with base resource.
|
||||
id := patch.Id()
|
||||
matchedIds := baseResourceMap.GetMatchingIds(id.GvknEquals)
|
||||
if len(matchedIds) == 0 {
|
||||
return fmt.Errorf("failed to find an object with %s to apply the patch", id.GvknString())
|
||||
}
|
||||
if len(matchedIds) > 1 {
|
||||
return fmt.Errorf("found multiple objects %#v targeted by patch %#v (ambiguous)", matchedIds, id)
|
||||
}
|
||||
id = matchedIds[0]
|
||||
base := baseResourceMap[id]
|
||||
merged := map[string]interface{}{}
|
||||
versionedObj, err := scheme.Scheme.New(toSchemaGvk(id.Gvk()))
|
||||
baseName := base.GetName()
|
||||
switch {
|
||||
case runtime.IsNotRegisteredError(err):
|
||||
// Use JSON merge patch to handle types w/o schema
|
||||
baseBytes, err := json.Marshal(base.Map())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patchBytes, err := json.Marshal(patch.Map())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mergedBytes, err := jsonpatch.MergePatch(baseBytes, patchBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(mergedBytes, &merged)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
// Use Strategic-Merge-Patch to handle types w/ schema
|
||||
// TODO: Change this to use the new Merge package.
|
||||
// Store the name of the base object, because this name may have been munged.
|
||||
// Apply this name to the patched object.
|
||||
lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
merged, err = strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta(
|
||||
base.Map(),
|
||||
patch.Map(),
|
||||
lookupPatchMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
base.SetName(baseName)
|
||||
baseResourceMap[id].SetMap(merged)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergePatches merge and index patches by Id.
|
||||
// It errors out if there is conflict between patches.
|
||||
func (pt *patchTransformer) mergePatches() (resmap.ResMap, error) {
|
||||
rc := resmap.ResMap{}
|
||||
for ix, patch := range pt.patches {
|
||||
id := patch.Id()
|
||||
existing, found := rc[id]
|
||||
if !found {
|
||||
rc[id] = patch
|
||||
continue
|
||||
}
|
||||
|
||||
versionedObj, err := scheme.Scheme.New(toSchemaGvk(id.Gvk()))
|
||||
if err != nil && !runtime.IsNotRegisteredError(err) {
|
||||
return nil, err
|
||||
}
|
||||
var cd conflictDetector
|
||||
if err != nil {
|
||||
cd = newJMPConflictDetector(pt.rf)
|
||||
} else {
|
||||
cd, err = newSMPConflictDetector(versionedObj, pt.rf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
conflict, err := cd.hasConflict(existing, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conflict {
|
||||
conflictingPatch, err := cd.findConflict(ix, pt.patches)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"conflict between %#v and %#v",
|
||||
conflictingPatch.Map(), patch.Map())
|
||||
}
|
||||
merged, err := cd.mergePatches(existing, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc[id] = merged
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// toSchemaGvk converts to a schema.GroupVersionKind.
|
||||
func toSchemaGvk(x gvk.Gvk) schema.GroupVersionKind {
|
||||
return schema.GroupVersionKind{
|
||||
Group: x.Group,
|
||||
Version: x.Version,
|
||||
Kind: x.Kind,
|
||||
}
|
||||
}
|
||||
137
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patchconflictdetector.go
generated
vendored
Normal file
137
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/transformer/patch/patchconflictdetector.go
generated
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
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 patch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/evanphx/json-patch"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"sigs.k8s.io/kustomize/pkg/resource"
|
||||
)
|
||||
|
||||
type conflictDetector interface {
|
||||
hasConflict(patch1, patch2 *resource.Resource) (bool, error)
|
||||
findConflict(conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error)
|
||||
mergePatches(patch1, patch2 *resource.Resource) (*resource.Resource, error)
|
||||
}
|
||||
|
||||
type jsonMergePatch struct {
|
||||
rf *resource.Factory
|
||||
}
|
||||
|
||||
var _ conflictDetector = &jsonMergePatch{}
|
||||
|
||||
func newJMPConflictDetector(rf *resource.Factory) conflictDetector {
|
||||
return &jsonMergePatch{rf: rf}
|
||||
}
|
||||
|
||||
func (jmp *jsonMergePatch) hasConflict(
|
||||
patch1, patch2 *resource.Resource) (bool, error) {
|
||||
return mergepatch.HasConflicts(patch1.Map(), patch2.Map())
|
||||
}
|
||||
|
||||
func (jmp *jsonMergePatch) findConflict(
|
||||
conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) {
|
||||
for i, patch := range patches {
|
||||
if i == conflictingPatchIdx {
|
||||
continue
|
||||
}
|
||||
if !patches[conflictingPatchIdx].Id().GvknEquals(patch.Id()) {
|
||||
continue
|
||||
}
|
||||
conflict, err := mergepatch.HasConflicts(
|
||||
patch.Map(),
|
||||
patches[conflictingPatchIdx].Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conflict {
|
||||
return patch, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (jmp *jsonMergePatch) mergePatches(
|
||||
patch1, patch2 *resource.Resource) (*resource.Resource, error) {
|
||||
baseBytes, err := json.Marshal(patch1.Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patchBytes, err := json.Marshal(patch2.Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mergedBytes, err := jsonpatch.MergeMergePatches(baseBytes, patchBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mergedMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(mergedBytes, &mergedMap)
|
||||
return jmp.rf.FromMap(mergedMap), err
|
||||
}
|
||||
|
||||
type strategicMergePatch struct {
|
||||
lookupPatchMeta strategicpatch.LookupPatchMeta
|
||||
rf *resource.Factory
|
||||
}
|
||||
|
||||
var _ conflictDetector = &strategicMergePatch{}
|
||||
|
||||
func newSMPConflictDetector(
|
||||
versionedObj runtime.Object,
|
||||
rf *resource.Factory) (conflictDetector, error) {
|
||||
lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj)
|
||||
return &strategicMergePatch{lookupPatchMeta: lookupPatchMeta, rf: rf}, err
|
||||
}
|
||||
|
||||
func (smp *strategicMergePatch) hasConflict(p1, p2 *resource.Resource) (bool, error) {
|
||||
return strategicpatch.MergingMapsHaveConflicts(
|
||||
p1.Map(), p2.Map(), smp.lookupPatchMeta)
|
||||
}
|
||||
|
||||
func (smp *strategicMergePatch) findConflict(
|
||||
conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) {
|
||||
for i, patch := range patches {
|
||||
if i == conflictingPatchIdx {
|
||||
continue
|
||||
}
|
||||
if !patches[conflictingPatchIdx].Id().GvknEquals(patch.Id()) {
|
||||
continue
|
||||
}
|
||||
conflict, err := strategicpatch.MergingMapsHaveConflicts(
|
||||
patch.Map(),
|
||||
patches[conflictingPatchIdx].Map(),
|
||||
smp.lookupPatchMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conflict {
|
||||
return patch, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (smp *strategicMergePatch) mergePatches(patch1, patch2 *resource.Resource) (*resource.Resource, error) {
|
||||
mergeJSONMap, err := strategicpatch.MergeStrategicMergeMapPatchUsingLookupPatchMeta(
|
||||
smp.lookupPatchMeta, patch1.Map(), patch2.Map())
|
||||
return smp.rf.FromMap(mergeJSONMap), err
|
||||
}
|
||||
61
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/validators.go
generated
vendored
Normal file
61
vendor/k8s.io/cli-runtime/pkg/kustomize/k8sdeps/validator/validators.go
generated
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
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 validator provides functions to validate labels, annotations, namespace using apimachinery
|
||||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
v1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// KustValidator validates Labels and annotations by apimachinery
|
||||
type KustValidator struct{}
|
||||
|
||||
// NewKustValidator returns a KustValidator object
|
||||
func NewKustValidator() *KustValidator {
|
||||
return &KustValidator{}
|
||||
}
|
||||
|
||||
// MakeAnnotationValidator returns a MapValidatorFunc using apimachinery.
|
||||
func (v *KustValidator) MakeAnnotationValidator() func(map[string]string) error {
|
||||
return func(x map[string]string) error {
|
||||
errs := apivalidation.ValidateAnnotations(x, field.NewPath("field"))
|
||||
if len(errs) > 0 {
|
||||
return errors.New(errs.ToAggregate().Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MakeLabelValidator returns a MapValidatorFunc using apimachinery.
|
||||
func (v *KustValidator) MakeLabelValidator() func(map[string]string) error {
|
||||
return func(x map[string]string) error {
|
||||
errs := v1validation.ValidateLabels(x, field.NewPath("field"))
|
||||
if len(errs) > 0 {
|
||||
return errors.New(errs.ToAggregate().Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateNamespace validates a string is a valid namespace using apimachinery.
|
||||
func (v *KustValidator) ValidateNamespace(s string) []string {
|
||||
return validation.IsDNS1123Label(s)
|
||||
}
|
||||
19
vendor/k8s.io/cli-runtime/pkg/printers/doc.go
generated
vendored
Normal file
19
vendor/k8s.io/cli-runtime/pkg/printers/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package printers is helper for formatting and printing runtime objects into
|
||||
// primitives io.writer.
|
||||
package printers // import "k8s.io/cli-runtime/pkg/printers"
|
||||
|
|
@ -18,13 +18,13 @@ package printers
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"text/template"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
)
|
||||
|
||||
// GoTemplatePrinter is an implementation of ResourcePrinter which formats data with a Go Template.
|
||||
|
|
@ -73,12 +73,11 @@ type Builder struct {
|
|||
stream bool
|
||||
dir bool
|
||||
|
||||
labelSelector *string
|
||||
fieldSelector *string
|
||||
selectAll bool
|
||||
includeUninitialized bool
|
||||
limitChunks int64
|
||||
requestTransforms []RequestTransform
|
||||
labelSelector *string
|
||||
fieldSelector *string
|
||||
selectAll bool
|
||||
limitChunks int64
|
||||
requestTransforms []RequestTransform
|
||||
|
||||
resources []string
|
||||
|
||||
|
|
@ -131,9 +130,28 @@ func IsUsageError(err error) bool {
|
|||
|
||||
type FilenameOptions struct {
|
||||
Filenames []string
|
||||
Kustomize string
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
func (o *FilenameOptions) validate() []error {
|
||||
var errs []error
|
||||
if len(o.Filenames) > 0 && len(o.Kustomize) > 0 {
|
||||
errs = append(errs, fmt.Errorf("only one of -f or -k can be specified"))
|
||||
}
|
||||
if len(o.Kustomize) > 0 && o.Recursive {
|
||||
errs = append(errs, fmt.Errorf("the -k flag can't be used with -f or -R"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (o *FilenameOptions) RequireFilenameOrKustomize() error {
|
||||
if len(o.Filenames) == 0 && len(o.Kustomize) == 0 {
|
||||
return fmt.Errorf("must specify one of -f and -k")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type resourceTuple struct {
|
||||
Resource string
|
||||
Name string
|
||||
|
|
@ -196,6 +214,10 @@ func (b *Builder) AddError(err error) *Builder {
|
|||
// If ContinueOnError() is set prior to this method, objects on the path that are not
|
||||
// recognized will be ignored (but logged at V(2)).
|
||||
func (b *Builder) FilenameParam(enforceNamespace bool, filenameOptions *FilenameOptions) *Builder {
|
||||
if errs := filenameOptions.validate(); len(errs) > 0 {
|
||||
b.errs = append(b.errs, errs...)
|
||||
return b
|
||||
}
|
||||
recursive := filenameOptions.Recursive
|
||||
paths := filenameOptions.Filenames
|
||||
for _, s := range paths {
|
||||
|
|
@ -216,6 +238,10 @@ func (b *Builder) FilenameParam(enforceNamespace bool, filenameOptions *Filename
|
|||
b.Path(recursive, s)
|
||||
}
|
||||
}
|
||||
if filenameOptions.Kustomize != "" {
|
||||
b.paths = append(b.paths, &KustomizeVisitor{filenameOptions.Kustomize,
|
||||
NewStreamVisitor(nil, b.mapper, filenameOptions.Kustomize, b.schema)})
|
||||
}
|
||||
|
||||
if enforceNamespace {
|
||||
b.RequireNamespace()
|
||||
|
|
@ -441,12 +467,6 @@ func (b *Builder) ExportParam(export bool) *Builder {
|
|||
return b
|
||||
}
|
||||
|
||||
// IncludeUninitialized accepts the include-uninitialized boolean for these resources
|
||||
func (b *Builder) IncludeUninitialized(includeUninitialized bool) *Builder {
|
||||
b.includeUninitialized = includeUninitialized
|
||||
return b
|
||||
}
|
||||
|
||||
// NamespaceParam accepts the namespace that these resources should be
|
||||
// considered under from - used by DefaultNamespace() and RequireNamespace()
|
||||
func (b *Builder) NamespaceParam(namespace string) *Builder {
|
||||
|
|
@ -715,12 +735,12 @@ func (b *Builder) mappingFor(resourceOrKindArg string) (*meta.RESTMapping, error
|
|||
// if we error out here, it is because we could not match a resource or a kind
|
||||
// for the given argument. To maintain consistency with previous behavior,
|
||||
// announce that a resource type could not be found.
|
||||
// if the error is a URL error, then we had trouble doing discovery, so we should return the original
|
||||
// error since it may help a user diagnose what is actually wrong
|
||||
if _, ok := err.(*url.Error); ok {
|
||||
return nil, err
|
||||
// if the error is _not_ a *meta.NoKindMatchError, then we had trouble doing discovery,
|
||||
// so we should return the original error since it may help a user diagnose what is actually wrong
|
||||
if meta.IsNoMatchError(err) {
|
||||
return nil, fmt.Errorf("the server doesn't have a resource type %q", groupResource.Resource)
|
||||
}
|
||||
return nil, fmt.Errorf("the server doesn't have a resource type %q", groupResource.Resource)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mapping, nil
|
||||
|
|
@ -844,7 +864,7 @@ func (b *Builder) visitBySelector() *Result {
|
|||
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
|
||||
selectorNamespace = ""
|
||||
}
|
||||
visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, labelSelector, fieldSelector, b.export, b.includeUninitialized, b.limitChunks))
|
||||
visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, labelSelector, fieldSelector, b.export, b.limitChunks))
|
||||
}
|
||||
if b.continueOnError {
|
||||
result.visitor = EagerVisitorList(visitors)
|
||||
|
|
@ -21,4 +21,4 @@ limitations under the License.
|
|||
// standard command line arguments and parameters into a Visitor that can iterate
|
||||
// over all of the identified resources, whether on the server or on the local
|
||||
// filesystem.
|
||||
package resource
|
||||
package resource // import "k8s.io/cli-runtime/pkg/resource"
|
||||
|
|
@ -138,9 +138,9 @@ func (m *Helper) createResource(c RESTClient, resource, namespace string, obj ru
|
|||
Do().
|
||||
Get()
|
||||
}
|
||||
func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte, options *metav1.UpdateOptions) (runtime.Object, error) {
|
||||
func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte, options *metav1.PatchOptions) (runtime.Object, error) {
|
||||
if options == nil {
|
||||
options = &metav1.UpdateOptions{}
|
||||
options = &metav1.PatchOptions{}
|
||||
}
|
||||
return m.RESTClient.Patch(pt).
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
|
|
@ -27,27 +27,25 @@ import (
|
|||
|
||||
// Selector is a Visitor for resources that match a label selector.
|
||||
type Selector struct {
|
||||
Client RESTClient
|
||||
Mapping *meta.RESTMapping
|
||||
Namespace string
|
||||
LabelSelector string
|
||||
FieldSelector string
|
||||
Export bool
|
||||
IncludeUninitialized bool
|
||||
LimitChunks int64
|
||||
Client RESTClient
|
||||
Mapping *meta.RESTMapping
|
||||
Namespace string
|
||||
LabelSelector string
|
||||
FieldSelector string
|
||||
Export bool
|
||||
LimitChunks int64
|
||||
}
|
||||
|
||||
// NewSelector creates a resource selector which hides details of getting items by their label selector.
|
||||
func NewSelector(client RESTClient, mapping *meta.RESTMapping, namespace, labelSelector, fieldSelector string, export, includeUninitialized bool, limitChunks int64) *Selector {
|
||||
func NewSelector(client RESTClient, mapping *meta.RESTMapping, namespace, labelSelector, fieldSelector string, export bool, limitChunks int64) *Selector {
|
||||
return &Selector{
|
||||
Client: client,
|
||||
Mapping: mapping,
|
||||
Namespace: namespace,
|
||||
LabelSelector: labelSelector,
|
||||
FieldSelector: fieldSelector,
|
||||
Export: export,
|
||||
IncludeUninitialized: includeUninitialized,
|
||||
LimitChunks: limitChunks,
|
||||
Client: client,
|
||||
Mapping: mapping,
|
||||
Namespace: namespace,
|
||||
LabelSelector: labelSelector,
|
||||
FieldSelector: fieldSelector,
|
||||
Export: export,
|
||||
LimitChunks: limitChunks,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,11 +58,10 @@ func (r *Selector) Visit(fn VisitorFunc) error {
|
|||
r.ResourceMapping().GroupVersionKind.GroupVersion().String(),
|
||||
r.Export,
|
||||
&metav1.ListOptions{
|
||||
LabelSelector: r.LabelSelector,
|
||||
FieldSelector: r.FieldSelector,
|
||||
IncludeUninitialized: r.IncludeUninitialized,
|
||||
Limit: r.LimitChunks,
|
||||
Continue: continueToken,
|
||||
LabelSelector: r.LabelSelector,
|
||||
FieldSelector: r.FieldSelector,
|
||||
Limit: r.LimitChunks,
|
||||
Continue: continueToken,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
|
|
@ -38,6 +39,8 @@ import (
|
|||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/cli-runtime/pkg/kustomize"
|
||||
"sigs.k8s.io/kustomize/pkg/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -141,6 +144,18 @@ func (i *Info) Refresh(obj runtime.Object, ignoreError bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ObjectName returns an approximate form of the resource's kind/name.
|
||||
func (i *Info) ObjectName() string {
|
||||
if i.Mapping != nil {
|
||||
return fmt.Sprintf("%s/%s", i.Mapping.Resource.Resource, i.Name)
|
||||
}
|
||||
gvk := i.Object.GetObjectKind().GroupVersionKind()
|
||||
if len(gvk.Group) == 0 {
|
||||
return fmt.Sprintf("%s/%s", strings.ToLower(gvk.Kind), i.Name)
|
||||
}
|
||||
return fmt.Sprintf("%s.%s/%s\n", strings.ToLower(gvk.Kind), gvk.Group, i.Name)
|
||||
}
|
||||
|
||||
// String returns the general purpose string representation
|
||||
func (i *Info) String() string {
|
||||
basicInfo := fmt.Sprintf("Name: %q, Namespace: %q\nObject: %+q", i.Name, i.Namespace, i.Object)
|
||||
|
|
@ -507,6 +522,24 @@ func (v *FileVisitor) Visit(fn VisitorFunc) error {
|
|||
return v.StreamVisitor.Visit(fn)
|
||||
}
|
||||
|
||||
// KustomizeVisitor is wrapper around a StreamVisitor, to handle Kustomization directories
|
||||
type KustomizeVisitor struct {
|
||||
Path string
|
||||
*StreamVisitor
|
||||
}
|
||||
|
||||
// Visit in a KustomizeVisitor gets the output of Kustomize build and save it in the Streamvisitor
|
||||
func (v *KustomizeVisitor) Visit(fn VisitorFunc) error {
|
||||
fSys := fs.MakeRealFS()
|
||||
var out bytes.Buffer
|
||||
err := kustomize.RunKustomizeBuild(&out, fSys, v.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.StreamVisitor.Reader = bytes.NewReader(out.Bytes())
|
||||
return v.StreamVisitor.Visit(fn)
|
||||
}
|
||||
|
||||
// StreamVisitor reads objects from an io.Reader and walks them. A stream visitor can only be
|
||||
// visited once.
|
||||
// TODO: depends on objects being in JSON format before being passed to decode - need to implement
|
||||
Loading…
Add table
Add a link
Reference in a new issue