add set cmd with cloneset support (#15)
* add set cmd with cloneset support * Format import Format import
This commit is contained in:
parent
424998d5e5
commit
89c461e93e
|
|
@ -10,7 +10,7 @@ So, `kubectl-kruise` was created.
|
|||
The development of `kubectl-kruise` is in progress, if you wanna to experience it, you can clone it and make it:
|
||||
|
||||
```
|
||||
make build && cp kubectl-kruise /usr/local/bin
|
||||
make build && cp bin/kubectl-kruise /usr/local/bin
|
||||
|
||||
```
|
||||
|
||||
|
|
|
|||
30
go.mod
30
go.mod
|
|
@ -3,16 +3,17 @@ module github.com/openkruise/kruise-tools
|
|||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/coreos/bbolt v1.3.3 // indirect
|
||||
github.com/coreos/etcd v3.3.17+incompatible // indirect
|
||||
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450 // indirect
|
||||
github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/googleapis/gnostic v0.4.0
|
||||
github.com/kr/pretty v0.2.0 // indirect
|
||||
github.com/lithammer/dedent v1.1.0
|
||||
github.com/openkruise/kruise-api v0.8.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect
|
||||
github.com/stretchr/testify v1.5.1
|
||||
golang.org/x/text v0.3.3
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.21.0
|
||||
k8s.io/apimachinery v0.21.0
|
||||
k8s.io/cli-runtime v0.21.0
|
||||
|
|
@ -21,18 +22,17 @@ require (
|
|||
k8s.io/klog v1.0.0
|
||||
k8s.io/kubectl v0.21.0
|
||||
sigs.k8s.io/controller-runtime v0.6.3
|
||||
sigs.k8s.io/structured-merge-diff v1.0.1 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v2 v2.0.1 // indirect
|
||||
vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc // indirect
|
||||
sigs.k8s.io/kustomize v2.0.3+incompatible
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
||||
|
||||
// Replace to match K8s 1.18.6
|
||||
replace (
|
||||
k8s.io/api => k8s.io/api v0.18.6
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.18.6
|
||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.18.6
|
||||
k8s.io/client-go => k8s.io/client-go v0.18.6
|
||||
k8s.io/component-base => k8s.io/component-base v0.18.6
|
||||
k8s.io/kubectl v0.21.0 => k8s.io/kubectl v0.18.6
|
||||
k8s.io/metrics => k8s.io/metrics v0.18.6
|
||||
k8s.io/api => k8s.io/api v0.18.4
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.18.4
|
||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.18.4
|
||||
k8s.io/client-go => k8s.io/client-go v0.18.4
|
||||
k8s.io/component-base => k8s.io/component-base v0.18.4
|
||||
k8s.io/kubectl v0.21.0 => k8s.io/kubectl v0.18.4
|
||||
k8s.io/metrics => k8s.io/metrics v0.18.4
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
krollout "github.com/openkruise/kruise-tools/pkg/cmd/rollout"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
kset "github.com/openkruise/kruise-tools/pkg/cmd/set"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
|
|
@ -374,6 +375,12 @@ func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
|||
ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err}
|
||||
|
||||
groups := templates.CommandGroups{
|
||||
{
|
||||
Message: "Basic Commands:",
|
||||
Commands: []*cobra.Command{
|
||||
kset.NewCmdSet(f, ioStreams),
|
||||
},
|
||||
},
|
||||
{
|
||||
Message: "CloneSet Commands:",
|
||||
Commands: []*cobra.Command{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
# Note: The set package is still in development progress
|
||||
Update cloneset 'sample' with a new environment variable
|
||||
* [x] kubectl-kruise set env cloneset/sample STORAGE_DIR=/local
|
||||
|
||||
List the environment variables defined on a deployments 'sample-build'
|
||||
* [x] kubectl-kruise set env cloneset/sample --list
|
||||
|
||||
List the environment variables defined on all pods
|
||||
* [x] kubectl-kruise set env pods --all --list
|
||||
|
||||
Output modified deployment in YAML, and does not alter the object on the server
|
||||
* [x] kubectl-kruise set env cloneset/sample STORAGE_DIR=/data -o yaml
|
||||
|
||||
Update all containers in all replication controllers in the project to have ENV=prod
|
||||
* [x] kubectl-kruise set env rc --all ENV=prod
|
||||
|
||||
Import environment from a secret
|
||||
* [x] kubectl-kruise set env --from=secret/mysecret deployment/myapp
|
||||
|
||||
Import environment from a config map with a prefix
|
||||
* [x] kubectl-kruise set env --from=configmap/myconfigmap --prefix=MYSQL_ deployment/myapp
|
||||
|
||||
Import specific keys from a config map
|
||||
* [x] kubectl-kruise set env --keys=my-example-key --from=configmap/myconfigmap deployment/myapp
|
||||
|
||||
Remove the environment variable ENV from container 'c1' in all deployment configs
|
||||
* [x] kubectl-kruise set env deployments --all --containers="c1" ENV-
|
||||
|
||||
Remove the environment variable ENV from a deployment definition on disk and update the deployment config on the server
|
||||
* [x] kubectl-kruise set env -f deploy.json ENV-
|
||||
|
||||
Set some of the local shell environment into a deployment config on the server
|
||||
* [x] env | grep XDG_VTNR | kubectl-kruise set env -e - deployment/nginx-deployment
|
||||
|
||||
Set a deployment's nginx container image to 'nginx:1.9.1', and its busybox container image to 'busybox'.
|
||||
* [x] kubectl-kruise set image cloneset/sample busybox=busybox nginx=nginx:1.9.1
|
||||
|
||||
Update all deployments' and rc's nginx container's image to 'nginx:1.9.1'
|
||||
* [x] kubectl-kruise set image cloneset,rc nginx=nginx:1.9.1 --all
|
||||
|
||||
Update image of all containers of daemonset abc to 'nginx:1.9.1'
|
||||
* [x] kubectl-kruise set image cloneset sample *=nginx:1.9.1
|
||||
|
||||
Print result (in yaml format) of updating nginx container image from local file, without hitting the server
|
||||
* [x] kubectl-kruise set image -f path/to/file.yaml nginx=nginx:1.9.1 --local -o yaml
|
||||
|
||||
Set a deployments nginx container cpu limits to "200m" and memory to "512Mi"
|
||||
* [x] kubectl-kruise set resources cloneset sample -c=nginx --limits=cpu=200m,memory=512Mi
|
||||
|
||||
Set the resource request and limits for all containers in nginx
|
||||
* [x] kubectl-kruise set resources cloneset sample --limits=cpu=200m,memory=512Mi --requests=cpu=100m,memory=256Mi
|
||||
|
||||
Remove the resource requests for resources on containers in nginx
|
||||
* [x] kubectl-kruise set resources cloneset sample --limits=cpu=0,memory=0 --requests=cpu=0,memory=0
|
||||
|
||||
Print the result (in yaml format) of updating nginx container limits from a local, without hitting the server
|
||||
* [x] kubectl-kruise set resources -f path/to/file.yaml --limits=cpu=200m,memory=512Mi --local -o yaml
|
||||
|
||||
Set Deployment nginx-deployment's ServiceAccount to serviceaccount1
|
||||
* [x] kubectl-kruise set serviceaccount cloneset sample serviceaccount1
|
||||
|
||||
Print the result (in yaml format) of updated nginx deployment with serviceaccount from local file, without hitting apiserver
|
||||
* [x] kubectl-kruise set sa -f nginx-deployment.yaml serviceaccount1 --local --dry-run=client -o yaml
|
||||
|
||||
Update a ClusterRoleBinding for serviceaccount1
|
||||
* [x] kubectl set subject clusterrolebinding admin --serviceaccount=namespace:serviceaccount1
|
||||
|
||||
Update a RoleBinding for user1, user2, and group1
|
||||
* [x] kubectl set subject rolebinding admin --user=user1 --user=user2 --group=group1
|
||||
|
||||
Print the result (in yaml format) of updating rolebinding subjects from a local, without hitting the server
|
||||
* [x] kubectl rolebinding admin --role=admin --user=admin -o yaml --dry-run=client | kubectl set subject --local -f - --user=foo -o yaml
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
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 env provides functions to incorporate environment variables into set env.
|
||||
package env
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
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 env
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
var argumentEnvironment = regexp.MustCompile("(?ms)^(.+)\\=(.*)$")
|
||||
var validArgumentEnvironment = regexp.MustCompile("(?ms)^(\\w+)\\=(.*)$")
|
||||
|
||||
// IsEnvironmentArgument checks whether a string is an environment argument, that is, whether it matches the "anycharacters=anycharacters" pattern.
|
||||
func IsEnvironmentArgument(s string) bool {
|
||||
return argumentEnvironment.MatchString(s)
|
||||
}
|
||||
|
||||
// IsValidEnvironmentArgument checks whether a string is a valid environment argument, that is, whether it matches the "wordcharacters=anycharacters" pattern. Word characters can be letters, numbers, and underscores.
|
||||
func IsValidEnvironmentArgument(s string) bool {
|
||||
return validArgumentEnvironment.MatchString(s)
|
||||
}
|
||||
|
||||
// SplitEnvironmentFromResources separates resources from environment arguments.
|
||||
// Resources must come first. Arguments may have the "DASH-" syntax.
|
||||
func SplitEnvironmentFromResources(args []string) (resources, envArgs []string, ok bool) {
|
||||
first := true
|
||||
for _, s := range args {
|
||||
// this method also has to understand env removal syntax, i.e. KEY-
|
||||
isEnv := IsEnvironmentArgument(s) || strings.HasSuffix(s, "-")
|
||||
switch {
|
||||
case first && isEnv:
|
||||
first = false
|
||||
fallthrough
|
||||
case !first && isEnv:
|
||||
envArgs = append(envArgs, s)
|
||||
case first && !isEnv:
|
||||
resources = append(resources, s)
|
||||
case !first && !isEnv:
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
return resources, envArgs, true
|
||||
}
|
||||
|
||||
// parseIntoEnvVar parses the list of key-value pairs into kubernetes EnvVar.
|
||||
// envVarType is for making errors more specific to user intentions.
|
||||
func parseIntoEnvVar(spec []string, defaultReader io.Reader, envVarType string) ([]corev1.EnvVar, []string, error) {
|
||||
env := []corev1.EnvVar{}
|
||||
exists := sets.NewString()
|
||||
var remove []string
|
||||
for _, envSpec := range spec {
|
||||
switch {
|
||||
case !IsValidEnvironmentArgument(envSpec) && !strings.HasSuffix(envSpec, "-"):
|
||||
return nil, nil, fmt.Errorf("%ss must be of the form key=value and can only contain letters, numbers, and underscores", envVarType)
|
||||
case envSpec == "-":
|
||||
if defaultReader == nil {
|
||||
return nil, nil, fmt.Errorf("when '-' is used, STDIN must be open")
|
||||
}
|
||||
fileEnv, err := readEnv(defaultReader, envVarType)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
env = append(env, fileEnv...)
|
||||
case strings.Contains(envSpec, "="):
|
||||
parts := strings.SplitN(envSpec, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, nil, fmt.Errorf("invalid %s: %v", envVarType, envSpec)
|
||||
}
|
||||
exists.Insert(parts[0])
|
||||
env = append(env, corev1.EnvVar{
|
||||
Name: parts[0],
|
||||
Value: parts[1],
|
||||
})
|
||||
case strings.HasSuffix(envSpec, "-"):
|
||||
remove = append(remove, envSpec[:len(envSpec)-1])
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown %s: %v", envVarType, envSpec)
|
||||
}
|
||||
}
|
||||
for _, removeLabel := range remove {
|
||||
if _, found := exists[removeLabel]; found {
|
||||
return nil, nil, fmt.Errorf("can not both modify and remove the same %s in the same command", envVarType)
|
||||
}
|
||||
}
|
||||
return env, remove, nil
|
||||
}
|
||||
|
||||
// ParseEnv parses the elements of the first argument looking for environment variables in key=value form and, if one of those values is "-", it also scans the reader.
|
||||
// The same environment variable cannot be both modified and removed in the same command.
|
||||
func ParseEnv(spec []string, defaultReader io.Reader) ([]corev1.EnvVar, []string, error) {
|
||||
return parseIntoEnvVar(spec, defaultReader, "environment variable")
|
||||
}
|
||||
|
||||
func readEnv(r io.Reader, envVarType string) ([]corev1.EnvVar, error) {
|
||||
env := []corev1.EnvVar{}
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
envSpec := scanner.Text()
|
||||
if pos := strings.Index(envSpec, "#"); pos != -1 {
|
||||
envSpec = envSpec[:pos]
|
||||
}
|
||||
|
||||
if strings.Contains(envSpec, "=") {
|
||||
parts := strings.SplitN(envSpec, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid %s: %v", envVarType, envSpec)
|
||||
}
|
||||
env = append(env, corev1.EnvVar{
|
||||
Name: parts[0],
|
||||
Value: parts[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
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 env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ExampleIsEnvironmentArgument_true() {
|
||||
test := "returns=true"
|
||||
fmt.Println(IsEnvironmentArgument(test))
|
||||
// Output: true
|
||||
}
|
||||
|
||||
func ExampleIsEnvironmentArgument_false() {
|
||||
test := "returnsfalse"
|
||||
fmt.Println(IsEnvironmentArgument(test))
|
||||
// Output: false
|
||||
}
|
||||
|
||||
func ExampleIsValidEnvironmentArgument_true() {
|
||||
test := "wordcharacters=true"
|
||||
fmt.Println(IsValidEnvironmentArgument(test))
|
||||
// Output: true
|
||||
}
|
||||
|
||||
func ExampleIsValidEnvironmentArgument_false() {
|
||||
test := "not$word^characters=test"
|
||||
fmt.Println(IsValidEnvironmentArgument(test))
|
||||
// Output: false
|
||||
}
|
||||
|
||||
func ExampleSplitEnvironmentFromResources() {
|
||||
args := []string{`resource`, "ENV\\=ARG", `ONE\=MORE`, `DASH-`}
|
||||
fmt.Println(SplitEnvironmentFromResources(args))
|
||||
// Output: [resource] [ENV\=ARG ONE\=MORE DASH-] true
|
||||
}
|
||||
|
||||
func ExampleParseEnv_good() {
|
||||
r := strings.NewReader("FROM=READER")
|
||||
ss := []string{"ENV=VARIABLE", "AND=ANOTHER", "REMOVE-", "-"}
|
||||
fmt.Println(ParseEnv(ss, r))
|
||||
// Output:
|
||||
// [{ENV VARIABLE nil} {AND ANOTHER nil} {FROM READER nil}] [REMOVE] <nil>
|
||||
}
|
||||
|
||||
func ExampleParseEnv_bad() {
|
||||
var r io.Reader
|
||||
bad := []string{"This not in the key=value format."}
|
||||
fmt.Println(ParseEnv(bad, r))
|
||||
// Output:
|
||||
// [] [] environment variables must be of the form key=value and can only contain letters, numbers, and underscores
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
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 env
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// ResourceStore defines a new resource store data structure.
|
||||
type ResourceStore struct {
|
||||
SecretStore map[string]*corev1.Secret
|
||||
ConfigMapStore map[string]*corev1.ConfigMap
|
||||
}
|
||||
|
||||
// NewResourceStore returns a pointer to a new resource store data structure.
|
||||
func NewResourceStore() *ResourceStore {
|
||||
return &ResourceStore{
|
||||
SecretStore: make(map[string]*corev1.Secret),
|
||||
ConfigMapStore: make(map[string]*corev1.ConfigMap),
|
||||
}
|
||||
}
|
||||
|
||||
// getSecretRefValue returns the value of a secret in the supplied namespace
|
||||
func getSecretRefValue(client kubernetes.Interface, namespace string, store *ResourceStore, secretSelector *corev1.SecretKeySelector) (string, error) {
|
||||
secret, ok := store.SecretStore[secretSelector.Name]
|
||||
if !ok {
|
||||
var err error
|
||||
secret, err = client.CoreV1().Secrets(namespace).Get(context.TODO(), secretSelector.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
store.SecretStore[secretSelector.Name] = secret
|
||||
}
|
||||
if data, ok := secret.Data[secretSelector.Key]; ok {
|
||||
return string(data), nil
|
||||
}
|
||||
return "", fmt.Errorf("key %s not found in secret %s", secretSelector.Key, secretSelector.Name)
|
||||
|
||||
}
|
||||
|
||||
// getConfigMapRefValue returns the value of a configmap in the supplied namespace
|
||||
func getConfigMapRefValue(client kubernetes.Interface, namespace string, store *ResourceStore, configMapSelector *corev1.ConfigMapKeySelector) (string, error) {
|
||||
configMap, ok := store.ConfigMapStore[configMapSelector.Name]
|
||||
if !ok {
|
||||
var err error
|
||||
configMap, err = client.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapSelector.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
store.ConfigMapStore[configMapSelector.Name] = configMap
|
||||
}
|
||||
if data, ok := configMap.Data[configMapSelector.Key]; ok {
|
||||
return string(data), nil
|
||||
}
|
||||
return "", fmt.Errorf("key %s not found in config map %s", configMapSelector.Key, configMapSelector.Name)
|
||||
}
|
||||
|
||||
// getFieldRef returns the value of the supplied path in the given object
|
||||
func getFieldRef(obj runtime.Object, from *corev1.EnvVarSource) (string, error) {
|
||||
return extractFieldPathAsString(obj, from.FieldRef.FieldPath)
|
||||
}
|
||||
|
||||
// extractFieldPathAsString extracts the field from the given object
|
||||
// and returns it as a string. The object must be a pointer to an
|
||||
// API type.
|
||||
func extractFieldPathAsString(obj interface{}, fieldPath string) (string, error) {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if path, subscript, ok := splitMaybeSubscriptedPath(fieldPath); ok {
|
||||
switch path {
|
||||
case "metadata.annotations":
|
||||
if errs := validation.IsQualifiedName(strings.ToLower(subscript)); len(errs) != 0 {
|
||||
return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";"))
|
||||
}
|
||||
return accessor.GetAnnotations()[subscript], nil
|
||||
case "metadata.labels":
|
||||
if errs := validation.IsQualifiedName(subscript); len(errs) != 0 {
|
||||
return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";"))
|
||||
}
|
||||
return accessor.GetLabels()[subscript], nil
|
||||
default:
|
||||
return "", fmt.Errorf("fieldPath %q does not support subscript", fieldPath)
|
||||
}
|
||||
}
|
||||
|
||||
switch fieldPath {
|
||||
case "metadata.annotations":
|
||||
return formatMap(accessor.GetAnnotations()), nil
|
||||
case "metadata.labels":
|
||||
return formatMap(accessor.GetLabels()), nil
|
||||
case "metadata.name":
|
||||
return accessor.GetName(), nil
|
||||
case "metadata.namespace":
|
||||
return accessor.GetNamespace(), nil
|
||||
case "metadata.uid":
|
||||
return string(accessor.GetUID()), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unsupported fieldPath: %v", fieldPath)
|
||||
}
|
||||
|
||||
// splitMaybeSubscriptedPath checks whether the specified fieldPath is
|
||||
// subscripted, and
|
||||
// - if yes, this function splits the fieldPath into path and subscript, and
|
||||
// returns (path, subscript, true).
|
||||
// - if no, this function returns (fieldPath, "", false).
|
||||
//
|
||||
// Example inputs and outputs:
|
||||
// - "metadata.annotations['myKey']" --> ("metadata.annotations", "myKey", true)
|
||||
// - "metadata.annotations['a[b]c']" --> ("metadata.annotations", "a[b]c", true)
|
||||
// - "metadata.labels['']" --> ("metadata.labels", "", true)
|
||||
// - "metadata.labels" --> ("metadata.labels", "", false)
|
||||
func splitMaybeSubscriptedPath(fieldPath string) (string, string, bool) {
|
||||
if !strings.HasSuffix(fieldPath, "']") {
|
||||
return fieldPath, "", false
|
||||
}
|
||||
s := strings.TrimSuffix(fieldPath, "']")
|
||||
parts := strings.SplitN(s, "['", 2)
|
||||
if len(parts) < 2 {
|
||||
return fieldPath, "", false
|
||||
}
|
||||
if len(parts[0]) == 0 {
|
||||
return fieldPath, "", false
|
||||
}
|
||||
return parts[0], parts[1], true
|
||||
}
|
||||
|
||||
// formatMap formats map[string]string to a string.
|
||||
func formatMap(m map[string]string) (fmtStr string) {
|
||||
// output with keys in sorted order to provide stable output
|
||||
keys := sets.NewString()
|
||||
for key := range m {
|
||||
keys.Insert(key)
|
||||
}
|
||||
for _, key := range keys.List() {
|
||||
fmtStr += fmt.Sprintf("%v=%q\n", key, m[key])
|
||||
}
|
||||
fmtStr = strings.TrimSuffix(fmtStr, "\n")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getResourceFieldRef returns the value of a resource in the given container
|
||||
func getResourceFieldRef(from *corev1.EnvVarSource, container *corev1.Container) (string, error) {
|
||||
return extractContainerResourceValue(from.ResourceFieldRef, container)
|
||||
}
|
||||
|
||||
// ExtractContainerResourceValue extracts the value of a resource
|
||||
// in an already known container
|
||||
func extractContainerResourceValue(fs *corev1.ResourceFieldSelector, container *corev1.Container) (string, error) {
|
||||
divisor := resource.Quantity{}
|
||||
if divisor.Cmp(fs.Divisor) == 0 {
|
||||
divisor = resource.MustParse("1")
|
||||
} else {
|
||||
divisor = fs.Divisor
|
||||
}
|
||||
|
||||
switch fs.Resource {
|
||||
case "limits.cpu":
|
||||
return convertResourceCPUToString(container.Resources.Limits.Cpu(), divisor)
|
||||
case "limits.memory":
|
||||
return convertResourceMemoryToString(container.Resources.Limits.Memory(), divisor)
|
||||
case "limits.ephemeral-storage":
|
||||
return convertResourceEphemeralStorageToString(container.Resources.Limits.StorageEphemeral(), divisor)
|
||||
case "requests.cpu":
|
||||
return convertResourceCPUToString(container.Resources.Requests.Cpu(), divisor)
|
||||
case "requests.memory":
|
||||
return convertResourceMemoryToString(container.Resources.Requests.Memory(), divisor)
|
||||
case "requests.ephemeral-storage":
|
||||
return convertResourceEphemeralStorageToString(container.Resources.Requests.StorageEphemeral(), divisor)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Unsupported container resource : %v", fs.Resource)
|
||||
}
|
||||
|
||||
// convertResourceCPUToString converts cpu value to the format of divisor and returns
|
||||
// ceiling of the value.
|
||||
func convertResourceCPUToString(cpu *resource.Quantity, divisor resource.Quantity) (string, error) {
|
||||
c := int64(math.Ceil(float64(cpu.MilliValue()) / float64(divisor.MilliValue())))
|
||||
return strconv.FormatInt(c, 10), nil
|
||||
}
|
||||
|
||||
// convertResourceMemoryToString converts memory value to the format of divisor and returns
|
||||
// ceiling of the value.
|
||||
func convertResourceMemoryToString(memory *resource.Quantity, divisor resource.Quantity) (string, error) {
|
||||
m := int64(math.Ceil(float64(memory.Value()) / float64(divisor.Value())))
|
||||
return strconv.FormatInt(m, 10), nil
|
||||
}
|
||||
|
||||
// convertResourceEphemeralStorageToString converts ephemeral storage value to the format of divisor and returns
|
||||
// ceiling of the value.
|
||||
func convertResourceEphemeralStorageToString(ephemeralStorage *resource.Quantity, divisor resource.Quantity) (string, error) {
|
||||
m := int64(math.Ceil(float64(ephemeralStorage.Value()) / float64(divisor.Value())))
|
||||
return strconv.FormatInt(m, 10), nil
|
||||
}
|
||||
|
||||
// GetEnvVarRefValue returns the value referenced by the supplied EnvVarSource given the other supplied information.
|
||||
func GetEnvVarRefValue(kc kubernetes.Interface, ns string, store *ResourceStore, from *corev1.EnvVarSource, obj runtime.Object, c *corev1.Container) (string, error) {
|
||||
if from.SecretKeyRef != nil {
|
||||
return getSecretRefValue(kc, ns, store, from.SecretKeyRef)
|
||||
}
|
||||
|
||||
if from.ConfigMapKeyRef != nil {
|
||||
return getConfigMapRefValue(kc, ns, store, from.ConfigMapKeyRef)
|
||||
}
|
||||
|
||||
if from.FieldRef != nil {
|
||||
return getFieldRef(obj, from)
|
||||
}
|
||||
|
||||
if from.ResourceFieldRef != nil {
|
||||
return getResourceFieldRef(from, c)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid valueFrom")
|
||||
}
|
||||
|
||||
// GetEnvVarRefString returns a text description of whichever field is set within the supplied EnvVarSource argument.
|
||||
func GetEnvVarRefString(from *corev1.EnvVarSource) string {
|
||||
if from.ConfigMapKeyRef != nil {
|
||||
return fmt.Sprintf("configmap %s, key %s", from.ConfigMapKeyRef.Name, from.ConfigMapKeyRef.Key)
|
||||
}
|
||||
|
||||
if from.SecretKeyRef != nil {
|
||||
return fmt.Sprintf("secret %s, key %s", from.SecretKeyRef.Name, from.SecretKeyRef.Key)
|
||||
}
|
||||
|
||||
if from.FieldRef != nil {
|
||||
return fmt.Sprintf("field path %s", from.FieldRef.FieldPath)
|
||||
}
|
||||
|
||||
if from.ResourceFieldRef != nil {
|
||||
containerPrefix := ""
|
||||
if from.ResourceFieldRef.ContainerName != "" {
|
||||
containerPrefix = fmt.Sprintf("%s/", from.ResourceFieldRef.ContainerName)
|
||||
}
|
||||
return fmt.Sprintf("resource field %s%s", containerPrefix, from.ResourceFieldRef.Resource)
|
||||
}
|
||||
|
||||
return "invalid valueFrom"
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// selectContainers allows one or more containers to be matched against a string or wildcard
|
||||
func selectContainers(containers []v1.Container, spec string) ([]*v1.Container, []*v1.Container) {
|
||||
var out []*v1.Container
|
||||
var skipped []*v1.Container
|
||||
for i, c := range containers {
|
||||
if selectString(c.Name, spec) {
|
||||
out = append(out, &containers[i])
|
||||
} else {
|
||||
skipped = append(skipped, &containers[i])
|
||||
}
|
||||
}
|
||||
return out, skipped
|
||||
}
|
||||
|
||||
// selectString returns true if the provided string matches spec, where spec is a string with
|
||||
// a non-greedy '*' wildcard operator.
|
||||
// TODO: turn into a regex and handle greedy matches and backtracking.
|
||||
func selectString(s, spec string) bool {
|
||||
if spec == "*" {
|
||||
return true
|
||||
}
|
||||
if !strings.Contains(spec, "*") {
|
||||
return s == spec
|
||||
}
|
||||
|
||||
pos := 0
|
||||
match := true
|
||||
parts := strings.Split(spec, "*")
|
||||
for i, part := range parts {
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
next := strings.Index(s[pos:], part)
|
||||
switch {
|
||||
// next part not in string
|
||||
case next < pos:
|
||||
fallthrough
|
||||
// first part does not match start of string
|
||||
case i == 0 && pos != 0:
|
||||
fallthrough
|
||||
// last part does not exactly match remaining part of string
|
||||
case i == (len(parts)-1) && len(s) != (len(part)+next):
|
||||
match = false
|
||||
break
|
||||
default:
|
||||
pos = next
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// Patch represents the result of a mutation to an object.
|
||||
type Patch struct {
|
||||
Info *resource.Info
|
||||
Err error
|
||||
|
||||
Before []byte
|
||||
After []byte
|
||||
Patch []byte
|
||||
}
|
||||
|
||||
// PatchFn is a function type that accepts an info object and returns a byte slice.
|
||||
// Implementations of PatchFn should update the object and return it encoded.
|
||||
type PatchFn func(runtime.Object) ([]byte, error)
|
||||
|
||||
// CalculatePatch calls the mutation function on the provided info object, and generates a strategic merge patch for
|
||||
// the changes in the object. Encoder must be able to encode the info into the appropriate destination type.
|
||||
// This function returns whether the mutation function made any change in the original object.
|
||||
func CalculatePatch(patch *Patch, encoder runtime.Encoder, mutateFn PatchFn) bool {
|
||||
patch.Before, patch.Err = runtime.Encode(encoder, patch.Info.Object)
|
||||
patch.After, patch.Err = mutateFn(patch.Info.Object)
|
||||
if patch.Err != nil {
|
||||
return true
|
||||
}
|
||||
if patch.After == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
patch.Patch, patch.Err = strategicpatch.CreateTwoWayMergePatch(patch.Before, patch.After, patch.Info.Object)
|
||||
return true
|
||||
}
|
||||
|
||||
// CalculatePatches calculates patches on each provided info object. If the provided mutateFn
|
||||
// makes no change in an object, the object is not included in the final list of patches.
|
||||
func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn PatchFn) []*Patch {
|
||||
var patches []*Patch
|
||||
for _, info := range infos {
|
||||
patch := &Patch{Info: info}
|
||||
if CalculatePatch(patch, encoder, mutateFn) {
|
||||
patches = append(patches, patch)
|
||||
}
|
||||
}
|
||||
return patches
|
||||
}
|
||||
|
||||
func findEnv(env []v1.EnvVar, name string) (v1.EnvVar, bool) {
|
||||
for _, e := range env {
|
||||
if e.Name == name {
|
||||
return e, true
|
||||
}
|
||||
}
|
||||
return v1.EnvVar{}, false
|
||||
}
|
||||
|
||||
func updateEnv(existing []v1.EnvVar, env []v1.EnvVar, remove []string) []v1.EnvVar {
|
||||
var out []v1.EnvVar
|
||||
covered := sets.NewString(remove...)
|
||||
for _, e := range existing {
|
||||
if covered.Has(e.Name) {
|
||||
continue
|
||||
}
|
||||
newer, ok := findEnv(env, e.Name)
|
||||
if ok {
|
||||
covered.Insert(e.Name)
|
||||
out = append(out, newer)
|
||||
continue
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
for _, e := range env {
|
||||
if covered.Has(e.Name) {
|
||||
continue
|
||||
}
|
||||
covered.Insert(e.Name)
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
setLong = templates.LongDesc(`
|
||||
Configure application resources
|
||||
|
||||
These commands help you make changes to existing application resources.`)
|
||||
)
|
||||
|
||||
// NewCmdSet returns an initialized Command instance for 'set' sub command
|
||||
func NewCmdSet(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "set SUBCOMMAND",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Set specific features on objects"),
|
||||
Long: setLong,
|
||||
Run: cmdutil.DefaultSubCommandRun(streams.ErrOut),
|
||||
}
|
||||
|
||||
// add subcommands
|
||||
cmd.AddCommand(NewCmdImage(f, streams))
|
||||
cmd.AddCommand(NewCmdResources(f, streams))
|
||||
cmd.AddCommand(NewCmdSelector(f, streams))
|
||||
cmd.AddCommand(NewCmdSubject(f, streams))
|
||||
cmd.AddCommand(NewCmdServiceAccount(f, streams))
|
||||
cmd.AddCommand(NewCmdEnv(f, streams))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -0,0 +1,699 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
appsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/openkruise/kruise-tools/pkg/api"
|
||||
"github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
envutil "k8s.io/kubectl/pkg/cmd/set/env"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
)
|
||||
|
||||
var (
|
||||
validEnvNameRegexp = regexp.MustCompile("[^a-zA-Z0-9_]")
|
||||
envResources = `
|
||||
pod (po), replicationcontroller (rc), deployment (deploy), daemonset (ds), job, replicaset (rs), cloneset (cs)`
|
||||
|
||||
envLong = templates.LongDesc(`
|
||||
Update environment variables on a pod template.
|
||||
|
||||
List environment variable definitions in one or more pods, pod templates.
|
||||
Add, update, or remove container environment variable definitions in one or
|
||||
more pod templates (within replication controllers or deployment configurations).
|
||||
View or modify the environment variable definitions on all containers in the
|
||||
specified pods or pod templates, or just those that match a wildcard.
|
||||
|
||||
If "--env -" is passed, environment variables can be read from STDIN using the standard env
|
||||
syntax.
|
||||
|
||||
Possible resources include (case insensitive):
|
||||
` + envResources)
|
||||
|
||||
envExample = templates.Examples(`
|
||||
# Update cloneset 'sample' with a new environment variable
|
||||
kubectl-kruise set env cloneset/sample STORAGE_DIR=/local
|
||||
|
||||
# List the environment variables defined on a cloneset 'sample'
|
||||
kubectl-kruise set env cloneset/sample --list
|
||||
|
||||
# List the environment variables defined on all pods
|
||||
kubectl-kruise set env pods --all --list
|
||||
|
||||
# Output modified cloneset in YAML, and does not alter the object on the server
|
||||
kubectl-kruise set env cloneset/sample STORAGE_DIR=/data -o yaml
|
||||
|
||||
# Update all containers in all replication controllers in the project to have ENV=prod
|
||||
kubectl-kruise set env rc --all ENV=prod
|
||||
|
||||
# Import environment from a secret
|
||||
kubectl-kruise set env --from=secret/mysecret cloneset/sample
|
||||
|
||||
# Import environment from a config map with a prefix
|
||||
kubectl-kruise set env --from=configmap/myconfigmap --prefix=MYSQL_ cloneset/sample
|
||||
|
||||
# Import specific keys from a config map
|
||||
kubectl-kruise set env --keys=my-example-key --from=configmap/myconfigmap cloneset/sample
|
||||
|
||||
# Remove the environment variable ENV from container 'c1' in all deployment configs
|
||||
kubectl-kruise set env clonesets --all --containers="c1" ENV-
|
||||
|
||||
# Remove the environment variable ENV from a deployment definition on disk and
|
||||
# update the deployment config on the server
|
||||
kubectl-kruise set env -f deploy.json ENV-
|
||||
|
||||
# Set some of the local shell environment into a deployment config on the server
|
||||
env | grep RAILS_ | kubectl-kruise set env -e - cloneset/sample`)
|
||||
)
|
||||
|
||||
// EnvOptions holds values for 'set env' command-lone options
|
||||
type EnvOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
resource.FilenameOptions
|
||||
|
||||
EnvParams []string
|
||||
All bool
|
||||
Resolve bool
|
||||
List bool
|
||||
Local bool
|
||||
Overwrite bool
|
||||
ContainerSelector string
|
||||
Selector string
|
||||
From string
|
||||
Prefix string
|
||||
Keys []string
|
||||
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
|
||||
envArgs []string
|
||||
resources []string
|
||||
output string
|
||||
dryRunStrategy cmdutil.DryRunStrategy
|
||||
dryRunVerifier *resource.DryRunVerifier
|
||||
builder func() *resource.Builder
|
||||
updatePodSpecForObject polymorphichelpers.UpdatePodSpecForObjectFunc
|
||||
namespace string
|
||||
enforceNamespace bool
|
||||
clientset *kubernetes.Clientset
|
||||
resRef api.ResourceRef
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
type control struct {
|
||||
client client.Client
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
// NewEnvOptions returns an EnvOptions indicating all containers in the selected
|
||||
// pod templates are selected by default and allowing environment to be overwritten
|
||||
func NewEnvOptions(streams genericclioptions.IOStreams) *EnvOptions {
|
||||
return &EnvOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("env updated").WithTypeSetter(scheme.Scheme),
|
||||
|
||||
ContainerSelector: "*",
|
||||
Overwrite: true,
|
||||
|
||||
IOStreams: streams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdEnv implements the OpenShift cli env command
|
||||
func NewCmdEnv(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewEnvOptions(streams)
|
||||
cmd := &cobra.Command{
|
||||
Use: "env RESOURCE/NAME KEY_1=VAL_1 ... KEY_N=VAL_N",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Update environment variables on a pod template",
|
||||
Long: envLong,
|
||||
Example: fmt.Sprintf(envExample),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunEnv(f))
|
||||
},
|
||||
}
|
||||
usage := "the resource to update the env"
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||
cmd.Flags().StringVarP(&o.ContainerSelector, "containers", "c", o.ContainerSelector, "The names of containers in the selected pod templates to change - may use wildcards")
|
||||
cmd.Flags().StringVarP(&o.From, "from", "", "", "The name of a resource from which to inject environment variables")
|
||||
cmd.Flags().StringVarP(&o.Prefix, "prefix", "", "", "Prefix to append to variable names")
|
||||
cmd.Flags().StringArrayVarP(&o.EnvParams, "env", "e", o.EnvParams, "Specify a key-value pair for an environment variable to set into each container.")
|
||||
cmd.Flags().StringSliceVarP(&o.Keys, "keys", "", o.Keys, "Comma-separated list of keys to import from specified resource")
|
||||
cmd.Flags().BoolVar(&o.List, "list", o.List, "If true, display the environment and any changes in the standard format. this flag will removed when we have kubectl view env.")
|
||||
cmd.Flags().BoolVar(&o.Resolve, "resolve", o.Resolve, "If true, show secret or configmap references when listing variables")
|
||||
cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on")
|
||||
cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set env will NOT contact api-server but run locally.")
|
||||
cmd.Flags().BoolVar(&o.All, "all", o.All, "If true, select all resources in the namespace of the specified resource types")
|
||||
cmd.Flags().BoolVar(&o.Overwrite, "overwrite", o.Overwrite, "If true, allow environment to be overwritten, otherwise reject updates that overwrite existing environment.")
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func validateNoOverwrites(existing []v1.EnvVar, env []v1.EnvVar) error {
|
||||
for _, e := range env {
|
||||
if current, exists := findEnv(existing, e.Name); exists && current.Value != e.Value {
|
||||
return fmt.Errorf("'%s' already has a value (%s), and --overwrite is false", current.Name, current.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func keyToEnvName(key string) string {
|
||||
return strings.ToUpper(validEnvNameRegexp.ReplaceAllString(key, "_"))
|
||||
}
|
||||
|
||||
func contains(key string, keyList []string) bool {
|
||||
if len(keyList) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, k := range keyList {
|
||||
if k == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Complete completes all required options
|
||||
func (o *EnvOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
if o.All && len(o.Selector) > 0 {
|
||||
return fmt.Errorf("cannot set --all and --selector at the same time")
|
||||
}
|
||||
ok := false
|
||||
o.resources, o.envArgs, ok = envutil.SplitEnvironmentFromResources(args)
|
||||
if !ok {
|
||||
return fmt.Errorf("all resources must be specified before environment changes: %s", strings.Join(args, " "))
|
||||
}
|
||||
|
||||
o.updatePodSpecForObject = polymorphichelpers.UpdatePodSpecForObjectFn
|
||||
o.output = cmdutil.GetFlagString(cmd, "output")
|
||||
var err error
|
||||
o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discoveryClient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.dryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)
|
||||
|
||||
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy)
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = printer.PrintObj
|
||||
|
||||
o.clientset, err = f.KubernetesClientSet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.builder = f.NewBuilder
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure provided values for EnvOptions are valid
|
||||
func (o *EnvOptions) Validate() error {
|
||||
if o.Local && o.dryRunStrategy == cmdutil.DryRunServer {
|
||||
return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?")
|
||||
}
|
||||
if len(o.Filenames) == 0 && len(o.resources) < 1 {
|
||||
return fmt.Errorf("one or more resources must be specified as <resource> <name> or <resource>/<name>")
|
||||
}
|
||||
if o.List && len(o.output) > 0 {
|
||||
return fmt.Errorf("--list and --output may not be specified together")
|
||||
}
|
||||
if len(o.Keys) > 0 && len(o.From) == 0 {
|
||||
return fmt.Errorf("when specifying --keys, a configmap or secret must be provided with --from")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunEnv contains all the necessary functionality for the OpenShift cli env command
|
||||
func (o *EnvOptions) RunEnv(f cmdutil.Factory) error {
|
||||
env, remove, err := envutil.ParseEnv(append(o.EnvParams, o.envArgs...), o.In)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(o.From) != 0 {
|
||||
b := o.builder().
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
LocalParam(o.Local).
|
||||
ContinueOnError().
|
||||
NamespaceParam(o.namespace).DefaultNamespace().
|
||||
FilenameParam(o.enforceNamespace, &o.FilenameOptions).
|
||||
Flatten()
|
||||
if !o.Local {
|
||||
b = b.
|
||||
LabelSelectorParam(o.Selector).
|
||||
ResourceTypeOrNameArgs(o.All, o.From).
|
||||
Latest()
|
||||
}
|
||||
|
||||
infos, err := b.Do().Infos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
switch from := info.Object.(type) {
|
||||
case *v1.Secret:
|
||||
for key := range from.Data {
|
||||
if contains(key, o.Keys) {
|
||||
envVar := v1.EnvVar{
|
||||
Name: keyToEnvName(key),
|
||||
ValueFrom: &v1.EnvVarSource{
|
||||
SecretKeyRef: &v1.SecretKeySelector{
|
||||
LocalObjectReference: v1.LocalObjectReference{
|
||||
Name: from.Name,
|
||||
},
|
||||
Key: key,
|
||||
},
|
||||
},
|
||||
}
|
||||
env = append(env, envVar)
|
||||
}
|
||||
}
|
||||
case *v1.ConfigMap:
|
||||
for key := range from.Data {
|
||||
if contains(key, o.Keys) {
|
||||
envVar := v1.EnvVar{
|
||||
Name: keyToEnvName(key),
|
||||
ValueFrom: &v1.EnvVarSource{
|
||||
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
|
||||
LocalObjectReference: v1.LocalObjectReference{
|
||||
Name: from.Name,
|
||||
},
|
||||
Key: key,
|
||||
},
|
||||
},
|
||||
}
|
||||
env = append(env, envVar)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported resource specified in --from")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(o.Prefix) != 0 {
|
||||
for i := range env {
|
||||
env[i].Name = fmt.Sprintf("%s%s", o.Prefix, env[i].Name)
|
||||
}
|
||||
}
|
||||
|
||||
b := o.builder().
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
LocalParam(o.Local).
|
||||
ContinueOnError().
|
||||
NamespaceParam(o.namespace).DefaultNamespace().
|
||||
FilenameParam(o.enforceNamespace, &o.FilenameOptions).
|
||||
Flatten()
|
||||
|
||||
if !o.Local {
|
||||
b.LabelSelectorParam(o.Selector).
|
||||
ResourceTypeOrNameArgs(o.All, o.resources...).
|
||||
Latest()
|
||||
}
|
||||
|
||||
infos, err := b.Do().Infos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(infos) == 0{
|
||||
return nil
|
||||
}
|
||||
|
||||
switch infos[0].Object.(type){
|
||||
case *appsv1alpha1.CloneSet:
|
||||
cfg, err := f.ToRESTConfig()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemeGet := api.GetScheme()
|
||||
mapper, err := apiutil.NewDiscoveryRESTMapper(cfg)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctrl := &control{}
|
||||
|
||||
if ctrl.client, err = client.New(cfg, client.Options{Scheme: schemeGet, Mapper: mapper}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctrl.cache, err = cache.New(cfg, cache.Options{Scheme: schemeGet, Mapper: mapper}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cs := &appsv1alpha1.CloneSet{}
|
||||
if err := ctrl.client.Get(context.TODO(), types.NamespacedName{Namespace: o.namespace, Name: infos[0].Name}, cs); err != nil {
|
||||
return fmt.Errorf("failed to get %v of %v: %v", o.namespace, infos[0].Name, err)
|
||||
}
|
||||
|
||||
resolutionErrorsEncountered := false
|
||||
containers, _ := selectContainers(cs.Spec.Template.Spec.Containers, o.ContainerSelector)
|
||||
|
||||
objName, err := meta.NewAccessor().Name(cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvks, _, err := scheme.Scheme.ObjectKinds(cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objKind := cs.GetObjectKind().GroupVersionKind().Kind
|
||||
if len(objKind) == 0 {
|
||||
for _, gvk := range gvks {
|
||||
if len(gvk.Kind) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal {
|
||||
continue
|
||||
}
|
||||
|
||||
objKind = gvk.Kind
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(containers) == 0 {
|
||||
if gvks, _, err := scheme.Scheme.ObjectKinds(cs); err == nil {
|
||||
objKind := cs.GetObjectKind().GroupVersionKind().Kind
|
||||
if len(objKind) == 0 {
|
||||
for _, gvk := range gvks {
|
||||
if len(gvk.Kind) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal {
|
||||
continue
|
||||
}
|
||||
|
||||
objKind = gvk.Kind
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(o.ErrOut, "warning: %s/%s does not have any containers matching %q\n", objKind, objName, o.ContainerSelector)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, c := range containers {
|
||||
if !o.Overwrite {
|
||||
if err := validateNoOverwrites(c.Env, env); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.Env = updateEnv(c.Env, env, remove)
|
||||
if o.List {
|
||||
resolveErrors := map[string][]string{}
|
||||
store := envutil.NewResourceStore()
|
||||
|
||||
fmt.Fprintf(o.Out, "# %s %s, container %s\n", objKind, objName, c.Name)
|
||||
for _, env := range c.Env {
|
||||
// Print the simple value
|
||||
if env.ValueFrom == nil {
|
||||
fmt.Fprintf(o.Out, "%s=%s\n", env.Name, env.Value)
|
||||
continue
|
||||
}
|
||||
|
||||
// Print the reference version
|
||||
if !o.Resolve {
|
||||
fmt.Fprintf(o.Out, "# %s from %s\n", env.Name, envutil.GetEnvVarRefString(env.ValueFrom))
|
||||
continue
|
||||
}
|
||||
|
||||
value, err := envutil.GetEnvVarRefValue(o.clientset, o.namespace, store, env.ValueFrom, cs, c)
|
||||
// Print the resolved value
|
||||
if err == nil {
|
||||
fmt.Fprintf(o.Out, "%s=%s\n", env.Name, value)
|
||||
continue
|
||||
}
|
||||
|
||||
// Print the reference version and save the resolve error
|
||||
fmt.Fprintf(o.Out, "# %s from %s\n", env.Name, envutil.GetEnvVarRefString(env.ValueFrom))
|
||||
errString := err.Error()
|
||||
resolveErrors[errString] = append(resolveErrors[errString], env.Name)
|
||||
resolutionErrorsEncountered = true
|
||||
}
|
||||
|
||||
// Print any resolution errors
|
||||
var errs []string
|
||||
for err, vars := range resolveErrors {
|
||||
sort.Strings(vars)
|
||||
errs = append(errs, fmt.Sprintf("error retrieving reference for %s: %v", strings.Join(vars, ", "), err))
|
||||
}
|
||||
sort.Strings(errs)
|
||||
for _, err := range errs {
|
||||
_, _ = fmt.Fprintln(o.ErrOut, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := ctrl.client.Update(context.TODO(), cs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resolutionErrorsEncountered {
|
||||
return errors.New("failed to retrieve valueFrom references")
|
||||
}
|
||||
|
||||
if o.List {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(o.Out, "%s env updated\n", infos[0].ObjectName())
|
||||
|
||||
return nil
|
||||
default:
|
||||
patches := CalculatePatches(infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) {
|
||||
_, err := o.updatePodSpecForObject(obj, func(spec *v1.PodSpec) error {
|
||||
resolutionErrorsEncountered := false
|
||||
containers, _ := selectContainers(spec.Containers, o.ContainerSelector)
|
||||
objName, err := meta.NewAccessor().Name(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvks, _, err := scheme.Scheme.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objKind := obj.GetObjectKind().GroupVersionKind().Kind
|
||||
if len(objKind) == 0 {
|
||||
for _, gvk := range gvks {
|
||||
if len(gvk.Kind) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal {
|
||||
continue
|
||||
}
|
||||
|
||||
objKind = gvk.Kind
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(containers) == 0 {
|
||||
if gvks, _, err := scheme.Scheme.ObjectKinds(obj); err == nil {
|
||||
objKind := obj.GetObjectKind().GroupVersionKind().Kind
|
||||
if len(objKind) == 0 {
|
||||
for _, gvk := range gvks {
|
||||
if len(gvk.Kind) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal {
|
||||
continue
|
||||
}
|
||||
|
||||
objKind = gvk.Kind
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(o.ErrOut, "warning: %s/%s does not have any containers matching %q\n", objKind, objName, o.ContainerSelector)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, c := range containers {
|
||||
if !o.Overwrite {
|
||||
if err := validateNoOverwrites(c.Env, env); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.Env = updateEnv(c.Env, env, remove)
|
||||
if o.List {
|
||||
resolveErrors := map[string][]string{}
|
||||
store := envutil.NewResourceStore()
|
||||
|
||||
fmt.Fprintf(o.Out, "# %s %s, container %s\n", objKind, objName, c.Name)
|
||||
for _, env := range c.Env {
|
||||
// Print the simple value
|
||||
if env.ValueFrom == nil {
|
||||
fmt.Fprintf(o.Out, "%s=%s\n", env.Name, env.Value)
|
||||
continue
|
||||
}
|
||||
|
||||
// Print the reference version
|
||||
if !o.Resolve {
|
||||
fmt.Fprintf(o.Out, "# %s from %s\n", env.Name, envutil.GetEnvVarRefString(env.ValueFrom))
|
||||
continue
|
||||
}
|
||||
|
||||
value, err := envutil.GetEnvVarRefValue(o.clientset, o.namespace, store, env.ValueFrom, obj, c)
|
||||
// Print the resolved value
|
||||
if err == nil {
|
||||
fmt.Fprintf(o.Out, "%s=%s\n", env.Name, value)
|
||||
continue
|
||||
}
|
||||
|
||||
// Print the reference version and save the resolve error
|
||||
fmt.Fprintf(o.Out, "# %s from %s\n", env.Name, envutil.GetEnvVarRefString(env.ValueFrom))
|
||||
errString := err.Error()
|
||||
resolveErrors[errString] = append(resolveErrors[errString], env.Name)
|
||||
resolutionErrorsEncountered = true
|
||||
}
|
||||
|
||||
// Print any resolution errors
|
||||
var errs []string
|
||||
for err, vars := range resolveErrors {
|
||||
sort.Strings(vars)
|
||||
errs = append(errs, fmt.Sprintf("error retrieving reference for %s: %v", strings.Join(vars, ", "), err))
|
||||
}
|
||||
sort.Strings(errs)
|
||||
for _, err := range errs {
|
||||
_, _ = fmt.Fprintln(o.ErrOut, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if resolutionErrorsEncountered {
|
||||
return errors.New("failed to retrieve valueFrom references")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
return runtime.Encode(scheme.DefaultJSONEncoder(), obj)
|
||||
}
|
||||
return nil, err
|
||||
})
|
||||
|
||||
if o.List {
|
||||
return nil
|
||||
}
|
||||
|
||||
var allErrs []error
|
||||
|
||||
for _, patch := range patches {
|
||||
info := patch.Info
|
||||
if patch.Err != nil {
|
||||
name := info.ObjectName()
|
||||
allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err))
|
||||
continue
|
||||
}
|
||||
|
||||
// no changes
|
||||
if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if o.Local || o.dryRunStrategy == cmdutil.DryRunClient {
|
||||
if err := o.PrintObj(info.Object, o.Out); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if o.dryRunStrategy == cmdutil.DryRunServer {
|
||||
if err := o.dryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
actual, err := resource.
|
||||
NewHelper(info.Client, info.Mapping).
|
||||
DryRun(o.dryRunStrategy == cmdutil.DryRunServer).
|
||||
Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, fmt.Errorf("failed to patch env update to pod template: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
// make sure arguments to set or replace environment variables are set
|
||||
// before returning a successful message
|
||||
if len(env) == 0 && len(o.envArgs) == 0 {
|
||||
return fmt.Errorf("at least one environment variable must be provided")
|
||||
}
|
||||
|
||||
if err := o.PrintObj(actual, o.Out); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(allErrs)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,668 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
func TestSetEnvLocal(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: ""},
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
|
||||
outputFormat := "name"
|
||||
|
||||
streams, _, buf, bufErr := genericclioptions.NewTestIOStreams()
|
||||
opts := NewEnvOptions(streams)
|
||||
opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme)
|
||||
opts.FilenameOptions = resource.FilenameOptions{
|
||||
Filenames: []string{"../../../testdata/controller.yaml"},
|
||||
}
|
||||
opts.Local = true
|
||||
|
||||
err := opts.Complete(tf, NewCmdEnv(tf, streams), []string{"env=prod"})
|
||||
assert.NoError(t, err)
|
||||
err = opts.Validate()
|
||||
assert.NoError(t, err)
|
||||
err = opts.RunEnv(f)
|
||||
assert.NoError(t, err)
|
||||
if bufErr.Len() > 0 {
|
||||
t.Errorf("unexpected error: %s", string(bufErr.String()))
|
||||
}
|
||||
if !strings.Contains(buf.String(), "replicationcontroller/cassandra") {
|
||||
t.Errorf("did not set env: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEnvLocalNamespace(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: ""},
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
|
||||
outputFormat := "yaml"
|
||||
|
||||
streams, _, buf, bufErr := genericclioptions.NewTestIOStreams()
|
||||
opts := NewEnvOptions(streams)
|
||||
opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme)
|
||||
opts.FilenameOptions = resource.FilenameOptions{
|
||||
Filenames: []string{"../../../testdata/set/namespaced-resource.yaml"},
|
||||
}
|
||||
opts.Local = true
|
||||
|
||||
err := opts.Complete(tf, NewCmdEnv(tf, streams), []string{"env=prod"})
|
||||
assert.NoError(t, err)
|
||||
err = opts.Validate()
|
||||
assert.NoError(t, err)
|
||||
err = opts.RunEnv()
|
||||
assert.NoError(t, err)
|
||||
if bufErr.Len() > 0 {
|
||||
t.Errorf("unexpected error: %s", string(bufErr.String()))
|
||||
}
|
||||
if !strings.Contains(buf.String(), "namespace: existing-ns") {
|
||||
t.Errorf("did not set env: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMultiResourcesEnvLocal(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: ""},
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
|
||||
|
||||
outputFormat := "name"
|
||||
streams, _, buf, bufErr := genericclioptions.NewTestIOStreams()
|
||||
opts := NewEnvOptions(streams)
|
||||
opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme)
|
||||
opts.FilenameOptions = resource.FilenameOptions{
|
||||
Filenames: []string{"../../../testdata/set/multi-resource-yaml.yaml"},
|
||||
}
|
||||
opts.Local = true
|
||||
|
||||
err := opts.Complete(tf, NewCmdEnv(tf, streams), []string{"env=prod"})
|
||||
assert.NoError(t, err)
|
||||
err = opts.Validate()
|
||||
assert.NoError(t, err)
|
||||
err = opts.RunEnv()
|
||||
assert.NoError(t, err)
|
||||
if bufErr.Len() > 0 {
|
||||
t.Errorf("unexpected error: %s", string(bufErr.String()))
|
||||
}
|
||||
expectedOut := "replicationcontroller/first-rc\nreplicationcontroller/second-rc\n"
|
||||
if buf.String() != expectedOut {
|
||||
t.Errorf("expected out:\n%s\nbut got:\n%s", expectedOut, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEnvRemote(t *testing.T) {
|
||||
inputs := []struct {
|
||||
name string
|
||||
object runtime.Object
|
||||
groupVersion schema.GroupVersion
|
||||
path string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "test extensions.v1beta1 replicaset",
|
||||
object: &extensionsv1beta1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test apps.v1beta2 replicaset",
|
||||
object: &appsv1beta2.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test appsv1 replicaset",
|
||||
object: &appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test extensions.v1beta1 daemonset",
|
||||
object: &extensionsv1beta1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.DaemonSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test appsv1beta2 daemonset",
|
||||
object: &appsv1beta2.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.DaemonSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test appsv1 daemonset",
|
||||
object: &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test extensions.v1beta1 deployment",
|
||||
object: &extensionsv1beta1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test appsv1beta1 deployment",
|
||||
object: &appsv1beta1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test appsv1beta2 deployment",
|
||||
object: &appsv1beta2.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test appsv1 deployment",
|
||||
object: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test appsv1beta1 statefulset",
|
||||
object: &appsv1beta1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta1.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test appsv1beta2 statefulset",
|
||||
object: &appsv1beta2.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test appsv1 statefulset",
|
||||
object: &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test batchv1 Job",
|
||||
object: &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: batchv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/jobs/nginx",
|
||||
args: []string{"job", "nginx", "env=prod"},
|
||||
},
|
||||
{
|
||||
name: "test corev1 replication controller",
|
||||
object: &corev1.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Template: &corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: corev1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicationcontrollers/nginx",
|
||||
args: []string{"replicationcontroller", "nginx", "env=prod"},
|
||||
},
|
||||
}
|
||||
for _, input := range inputs {
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: input.groupVersion,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == input.path && m == http.MethodGet:
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
case p == input.path && m == http.MethodPatch:
|
||||
stream, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assert.Contains(t, string(bytes), `"value":`+`"`+"prod"+`"`, fmt.Sprintf("env not updated for %#v", input.object))
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
default:
|
||||
t.Errorf("%s: unexpected request: %s %#v\n%#v", "image", req.Method, req.URL, req)
|
||||
return nil, fmt.Errorf("unexpected request")
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
outputFormat := "yaml"
|
||||
streams := genericclioptions.NewTestIOStreamsDiscard()
|
||||
opts := NewEnvOptions(streams)
|
||||
opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme)
|
||||
opts.Local = false
|
||||
opts.IOStreams = streams
|
||||
err := opts.Complete(tf, NewCmdEnv(tf, streams), input.args)
|
||||
assert.NoError(t, err)
|
||||
err = opts.RunEnv()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEnvFromResource(t *testing.T) {
|
||||
mockConfigMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testconfigmap"},
|
||||
Data: map[string]string{
|
||||
"env": "prod",
|
||||
"test-key": "testValue",
|
||||
"test-key-two": "testValueTwo",
|
||||
},
|
||||
}
|
||||
|
||||
mockSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testsecret"},
|
||||
Data: map[string][]byte{
|
||||
"env": []byte("prod"),
|
||||
"test-key": []byte("testValue"),
|
||||
"test-key-two": []byte("testValueTwo"),
|
||||
},
|
||||
}
|
||||
|
||||
inputs := []struct {
|
||||
name string
|
||||
args []string
|
||||
from string
|
||||
keys []string
|
||||
assertIncludes []string
|
||||
assertExcludes []string
|
||||
}{
|
||||
{
|
||||
name: "test from configmap",
|
||||
args: []string{"deployment", "nginx"},
|
||||
from: "configmap/testconfigmap",
|
||||
keys: []string{},
|
||||
assertIncludes: []string{
|
||||
`{"name":"ENV","valueFrom":{"configMapKeyRef":{"key":"env","name":"testconfigmap"}}}`,
|
||||
`{"name":"TEST_KEY","valueFrom":{"configMapKeyRef":{"key":"test-key","name":"testconfigmap"}}}`,
|
||||
`{"name":"TEST_KEY_TWO","valueFrom":{"configMapKeyRef":{"key":"test-key-two","name":"testconfigmap"}}}`,
|
||||
},
|
||||
assertExcludes: []string{},
|
||||
},
|
||||
{
|
||||
name: "test from secret",
|
||||
args: []string{"deployment", "nginx"},
|
||||
from: "secret/testsecret",
|
||||
keys: []string{},
|
||||
assertIncludes: []string{
|
||||
`{"name":"ENV","valueFrom":{"secretKeyRef":{"key":"env","name":"testsecret"}}}`,
|
||||
`{"name":"TEST_KEY","valueFrom":{"secretKeyRef":{"key":"test-key","name":"testsecret"}}}`,
|
||||
`{"name":"TEST_KEY_TWO","valueFrom":{"secretKeyRef":{"key":"test-key-two","name":"testsecret"}}}`,
|
||||
},
|
||||
assertExcludes: []string{},
|
||||
},
|
||||
{
|
||||
name: "test from configmap with keys",
|
||||
args: []string{"deployment", "nginx"},
|
||||
from: "configmap/testconfigmap",
|
||||
keys: []string{"env", "test-key-two"},
|
||||
assertIncludes: []string{
|
||||
`{"name":"ENV","valueFrom":{"configMapKeyRef":{"key":"env","name":"testconfigmap"}}}`,
|
||||
`{"name":"TEST_KEY_TWO","valueFrom":{"configMapKeyRef":{"key":"test-key-two","name":"testconfigmap"}}}`,
|
||||
},
|
||||
assertExcludes: []string{`{"name":"TEST_KEY","valueFrom":{"configMapKeyRef":{"key":"test-key","name":"testconfigmap"}}}`},
|
||||
},
|
||||
{
|
||||
name: "test from secret with keys",
|
||||
args: []string{"deployment", "nginx"},
|
||||
from: "secret/testsecret",
|
||||
keys: []string{"env", "test-key-two"},
|
||||
assertIncludes: []string{
|
||||
`{"name":"ENV","valueFrom":{"secretKeyRef":{"key":"env","name":"testsecret"}}}`,
|
||||
`{"name":"TEST_KEY_TWO","valueFrom":{"secretKeyRef":{"key":"test-key-two","name":"testsecret"}}}`,
|
||||
},
|
||||
assertExcludes: []string{`{"name":"TEST_KEY","valueFrom":{"secretKeyRef":{"key":"test-key","name":"testsecret"}}}`},
|
||||
},
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
mockDeployment := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/configmaps/testconfigmap" && m == http.MethodGet:
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockConfigMap)}, nil
|
||||
case p == "/namespaces/test/secrets/testsecret" && m == http.MethodGet:
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockSecret)}, nil
|
||||
case p == "/namespaces/test/deployments/nginx" && m == http.MethodGet:
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockDeployment)}, nil
|
||||
case p == "/namespaces/test/deployments/nginx" && m == http.MethodPatch:
|
||||
stream, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, include := range input.assertIncludes {
|
||||
assert.Contains(t, string(bytes), include)
|
||||
}
|
||||
for _, exclude := range input.assertExcludes {
|
||||
assert.NotContains(t, string(bytes), exclude)
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockDeployment)}, nil
|
||||
default:
|
||||
t.Errorf("%s: unexpected request: %#v\n%#v", input.name, req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
outputFormat := "yaml"
|
||||
streams := genericclioptions.NewTestIOStreamsDiscard()
|
||||
opts := NewEnvOptions(streams)
|
||||
opts.From = input.from
|
||||
opts.Keys = input.keys
|
||||
opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme)
|
||||
opts.Local = false
|
||||
opts.IOStreams = streams
|
||||
err := opts.Complete(tf, NewCmdEnv(tf, streams), input.args)
|
||||
assert.NoError(t, err)
|
||||
err = opts.RunEnv()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||
kresource "github.com/openkruise/kruise-tools/pkg/resource"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/klog"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// SetImageOptions ImageOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
|
||||
// referencing the cmd.Flags()
|
||||
type SetImageOptions struct {
|
||||
resource.FilenameOptions
|
||||
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
|
||||
Infos []*resource.Info
|
||||
Selector string
|
||||
DryRunStrategy cmdutil.DryRunStrategy
|
||||
DryRunVerifier *resource.DryRunVerifier
|
||||
All bool
|
||||
Output string
|
||||
Local bool
|
||||
ResolveImage ImageResolver
|
||||
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
Recorder genericclioptions.Recorder
|
||||
|
||||
UpdatePodSpecForObject polymorphichelpers.UpdatePodSpecForObjectFunc
|
||||
Resources []string
|
||||
ContainerImages map[string]string
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
var (
|
||||
imageResources = `
|
||||
pod (po), replicationcontroller (rc), deployment (deploy), daemonset (ds), replicaset (rs), cloneset(cs)`
|
||||
|
||||
imageLong = templates.LongDesc(`
|
||||
Update existing container image(s) of resources.
|
||||
|
||||
Possible resources include (case insensitive):
|
||||
` + imageResources)
|
||||
|
||||
imageExample = templates.Examples(`
|
||||
# Set a deployment's nginx container image to 'nginx:1.9.1', and its busybox container image to 'busybox'.
|
||||
kubectl-kruise set image cloneset/sample busybox=busybox nginx=nginx:1.9.1
|
||||
|
||||
# Update all deployments' and rc's nginx container's image to 'nginx:1.9.1'
|
||||
kubectl-kruise set image cloneset,rc nginx=nginx:1.9.1 --all
|
||||
|
||||
# Update image of all containers of cloneset sample to 'nginx:1.9.1'
|
||||
kubectl-kruise set image cloneset sample *=nginx:1.9.1
|
||||
|
||||
# Print result (in yaml format) of updating nginx container image from local file, without hitting the server
|
||||
kubectl-kruise set image -f path/to/file.yaml nginx=nginx:1.9.1 --local -o yaml`)
|
||||
)
|
||||
|
||||
// NewImageOptions returns an initialized SetImageOptions instance
|
||||
func NewImageOptions(streams genericclioptions.IOStreams) *SetImageOptions {
|
||||
return &SetImageOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("image updated").WithTypeSetter(scheme.Scheme),
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
|
||||
Recorder: genericclioptions.NoopRecorder{},
|
||||
|
||||
IOStreams: streams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdImage returns an initialized Command instance for the 'set image' sub command
|
||||
func NewCmdImage(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewImageOptions(streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "image (-f FILENAME | TYPE NAME) CONTAINER_NAME_1=CONTAINER_IMAGE_1 ... CONTAINER_NAME_N=CONTAINER_IMAGE_N",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Update image of a pod template"),
|
||||
Long: imageLong,
|
||||
Example: imageExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
|
||||
usage := "identifying the resource to get from a server."
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||
cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources, including uninitialized ones, in the namespace of the specified resource types")
|
||||
cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on, not including uninitialized ones, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
||||
cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set image will NOT contact api-server but run locally.")
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all required options
|
||||
func (o *SetImageOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
err = o.RecordFlags.Complete(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.UpdatePodSpecForObject = polymorphichelpers.UpdatePodSpecForObjectFn
|
||||
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discoveryClient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)
|
||||
o.Output = cmdutil.GetFlagString(cmd, "output")
|
||||
o.ResolveImage = resolveImageFunc
|
||||
|
||||
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.PrintObj = printer.PrintObj
|
||||
|
||||
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Resources, o.ContainerImages, err = getResourcesAndImages(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder := f.NewBuilder().
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
LocalParam(o.Local).
|
||||
ContinueOnError().
|
||||
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||
Flatten()
|
||||
|
||||
if !o.Local {
|
||||
builder.LabelSelectorParam(o.Selector).
|
||||
ResourceTypeOrNameArgs(o.All, o.Resources...).
|
||||
Latest()
|
||||
} else {
|
||||
// if a --local flag was provided, and a resource was specified in the form
|
||||
// <resource>/<name>, fail immediately as --local cannot query the api server
|
||||
// for the specified resource.
|
||||
if len(o.Resources) > 0 {
|
||||
return resource.LocalResourceError
|
||||
}
|
||||
}
|
||||
|
||||
o.Infos, err = builder.Do().Infos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure provided values in SetImageOptions are valid
|
||||
func (o *SetImageOptions) Validate() error {
|
||||
var errors []error
|
||||
if o.All && len(o.Selector) > 0 {
|
||||
errors = append(errors, fmt.Errorf("cannot set --all and --selector at the same time"))
|
||||
}
|
||||
if len(o.Resources) < 1 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
|
||||
errors = append(errors, fmt.Errorf("one or more resources must be specified as <resource> <name> or <resource>/<name>"))
|
||||
}
|
||||
if len(o.ContainerImages) < 1 {
|
||||
errors = append(errors, fmt.Errorf("at least one image update is required"))
|
||||
} else if len(o.ContainerImages) > 1 && hasWildcardKey(o.ContainerImages) {
|
||||
errors = append(errors, fmt.Errorf("all containers are already specified by *, but saw more than one container_name=container_image pairs"))
|
||||
}
|
||||
if o.Local && o.DryRunStrategy == cmdutil.DryRunServer {
|
||||
errors = append(errors, fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?"))
|
||||
}
|
||||
return utilerrors.NewAggregate(errors)
|
||||
}
|
||||
|
||||
// Run performs the execution of 'set image' sub command
|
||||
func (o *SetImageOptions) Run() error {
|
||||
allErrs := []error{}
|
||||
|
||||
patches := CalculatePatches(o.Infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) {
|
||||
_, err := o.UpdatePodSpecForObject(obj, func(spec *corev1.PodSpec) error {
|
||||
for name, image := range o.ContainerImages {
|
||||
resolvedImageName, err := o.ResolveImage(image)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, fmt.Errorf("error: unable to resolve image %q for container %q: %v", image, name, err))
|
||||
if name == "*" {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
initContainerFound := setImage(spec.InitContainers, name, resolvedImageName)
|
||||
containerFound := setImage(spec.Containers, name, resolvedImageName)
|
||||
if !containerFound && !initContainerFound {
|
||||
allErrs = append(allErrs, fmt.Errorf("error: unable to find container named %q", name))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// record this change (for rollout history)
|
||||
if err := o.Recorder.Record(obj); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
}
|
||||
|
||||
return runtime.Encode(scheme.DefaultJSONEncoder(), obj)
|
||||
})
|
||||
|
||||
for _, patch := range patches {
|
||||
info := patch.Info
|
||||
if patch.Err != nil {
|
||||
name := info.ObjectName()
|
||||
allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err))
|
||||
continue
|
||||
}
|
||||
|
||||
// no changes
|
||||
if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if o.Local || o.DryRunStrategy == cmdutil.DryRunClient {
|
||||
if err := o.PrintObj(info.Object, o.Out); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if o.DryRunStrategy == cmdutil.DryRunServer {
|
||||
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// patch the change
|
||||
actual, err := kresource.
|
||||
NewHelper(info.Client, info.Mapping).
|
||||
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
|
||||
Patch(info.Namespace, info.Name, types.MergePatchType, patch.Patch, nil)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, fmt.Errorf("failed to patch image update to pod template: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := o.PrintObj(actual, o.Out); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
|
||||
}
|
||||
return utilerrors.NewAggregate(allErrs)
|
||||
}
|
||||
|
||||
func setImage(containers []corev1.Container, containerName string, image string) bool {
|
||||
containerFound := false
|
||||
// Find the container to update, and update its image
|
||||
for i, c := range containers {
|
||||
if c.Name == containerName || containerName == "*" {
|
||||
containerFound = true
|
||||
containers[i].Image = image
|
||||
}
|
||||
}
|
||||
return containerFound
|
||||
}
|
||||
|
||||
// getResourcesAndImages retrieves resources and container name:images pair from given args
|
||||
func getResourcesAndImages(args []string) (resources []string, containerImages map[string]string, err error) {
|
||||
pairType := "image"
|
||||
resources, imageArgs, err := cmdutil.GetResourcesAndPairs(args, pairType)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
containerImages, _, err = cmdutil.ParsePairs(imageArgs, pairType, false)
|
||||
return
|
||||
}
|
||||
|
||||
func hasWildcardKey(containerImages map[string]string) bool {
|
||||
_, ok := containerImages["*"]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ImageResolver is a func that receives an image name, and
|
||||
// resolves it to an appropriate / compatible image name.
|
||||
// Adds flexibility for future image resolving methods.
|
||||
type ImageResolver func(in string) (string, error)
|
||||
|
||||
// implements ImageResolver
|
||||
func resolveImageFunc(in string) (string, error) {
|
||||
return in, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,776 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
func TestImageLocal(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: ""},
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
|
||||
|
||||
outputFormat := "name"
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdImage(tf, streams)
|
||||
cmd.SetOutput(buf)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
cmd.Flags().Set("local", "true")
|
||||
|
||||
opts := SetImageOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
FilenameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"../../../testdata/controller.yaml"}},
|
||||
Local: true,
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := opts.Complete(tf, cmd, []string{"cassandra=thingy"})
|
||||
if err == nil {
|
||||
err = opts.Validate()
|
||||
}
|
||||
if err == nil {
|
||||
err = opts.Run()
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "replicationcontroller/cassandra") {
|
||||
t.Errorf("did not set image: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetImageValidation(t *testing.T) {
|
||||
printFlags := genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
imageOptions *SetImageOptions
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "test resource < 1 and filenames empty",
|
||||
imageOptions: &SetImageOptions{PrintFlags: printFlags},
|
||||
expectErr: "[one or more resources must be specified as <resource> <name> or <resource>/<name>, at least one image update is required]",
|
||||
},
|
||||
{
|
||||
name: "test containerImages < 1",
|
||||
imageOptions: &SetImageOptions{
|
||||
PrintFlags: printFlags,
|
||||
Resources: []string{"a", "b", "c"},
|
||||
|
||||
FilenameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"testFile"},
|
||||
},
|
||||
},
|
||||
expectErr: "at least one image update is required",
|
||||
},
|
||||
{
|
||||
name: "test containerImages > 1 and all containers are already specified by *",
|
||||
imageOptions: &SetImageOptions{
|
||||
PrintFlags: printFlags,
|
||||
Resources: []string{"a", "b", "c"},
|
||||
FilenameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"testFile"},
|
||||
},
|
||||
ContainerImages: map[string]string{
|
||||
"test": "test",
|
||||
"*": "test",
|
||||
},
|
||||
},
|
||||
expectErr: "all containers are already specified by *, but saw more than one container_name=container_image pairs",
|
||||
},
|
||||
{
|
||||
name: "success case",
|
||||
imageOptions: &SetImageOptions{
|
||||
PrintFlags: printFlags,
|
||||
Resources: []string{"a", "b", "c"},
|
||||
FilenameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"testFile"},
|
||||
},
|
||||
ContainerImages: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
expectErr: "",
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
err := testCase.imageOptions.Validate()
|
||||
if err != nil {
|
||||
if err.Error() != testCase.expectErr {
|
||||
t.Errorf("[%s]:expect err:%s got err:%s", testCase.name, testCase.expectErr, err.Error())
|
||||
}
|
||||
}
|
||||
if err == nil && (testCase.expectErr != "") {
|
||||
t.Errorf("[%s]:expect err:%s got err:%v", testCase.name, testCase.expectErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMultiResourcesImageLocal(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: ""},
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
|
||||
|
||||
outputFormat := "name"
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdImage(tf, streams)
|
||||
cmd.SetOutput(buf)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
cmd.Flags().Set("local", "true")
|
||||
|
||||
opts := SetImageOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
FilenameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"../../../testdata/set/multi-resource-yaml.yaml"}},
|
||||
Local: true,
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := opts.Complete(tf, cmd, []string{"*=thingy"})
|
||||
if err == nil {
|
||||
err = opts.Validate()
|
||||
}
|
||||
if err == nil {
|
||||
err = opts.Run()
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
expectedOut := "replicationcontroller/first-rc\nreplicationcontroller/second-rc\n"
|
||||
if buf.String() != expectedOut {
|
||||
t.Errorf("expected out:\n%s\nbut got:\n%s", expectedOut, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetImageRemote(t *testing.T) {
|
||||
inputs := []struct {
|
||||
name string
|
||||
object runtime.Object
|
||||
groupVersion schema.GroupVersion
|
||||
path string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "set image extensionsv1beta1 ReplicaSet",
|
||||
object: &extensionsv1beta1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta2 ReplicaSet",
|
||||
object: &appsv1beta2.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1 ReplicaSet",
|
||||
object: &appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image extensionsv1beta1 DaemonSet",
|
||||
object: &extensionsv1beta1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.DaemonSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta2 DaemonSet",
|
||||
object: &appsv1beta2.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.DaemonSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1 DaemonSet",
|
||||
object: &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image extensionsv1beta1 Deployment",
|
||||
object: &extensionsv1beta1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta1 Deployment",
|
||||
object: &appsv1beta1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta2 Deployment",
|
||||
object: &appsv1beta2.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1 Deployment",
|
||||
object: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta1 StatefulSet",
|
||||
object: &appsv1beta1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta1.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta2 StatefulSet",
|
||||
object: &appsv1beta2.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1 StatefulSet",
|
||||
object: &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image batchv1 Job",
|
||||
object: &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: batchv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/jobs/nginx",
|
||||
args: []string{"job", "nginx", "*=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set image corev1.ReplicationController",
|
||||
object: &corev1.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Template: &corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: corev1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicationcontrollers/nginx",
|
||||
args: []string{"replicationcontroller", "nginx", "*=thingy"},
|
||||
},
|
||||
}
|
||||
for _, input := range inputs {
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: input.groupVersion,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == input.path && m == http.MethodGet:
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
case p == input.path && m == http.MethodPatch:
|
||||
stream, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assert.Contains(t, string(bytes), `"image":`+`"`+"thingy"+`"`, fmt.Sprintf("image not updated for %#v", input.object))
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
default:
|
||||
t.Errorf("%s: unexpected request: %s %#v\n%#v", "image", req.Method, req.URL, req)
|
||||
return nil, fmt.Errorf("unexpected request")
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
outputFormat := "yaml"
|
||||
|
||||
streams := genericclioptions.NewTestIOStreamsDiscard()
|
||||
cmd := NewCmdImage(tf, streams)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
opts := SetImageOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
|
||||
Local: false,
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := opts.Complete(tf, cmd, input.args)
|
||||
assert.NoError(t, err)
|
||||
err = opts.Run()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetImageRemoteWithSpecificContainers(t *testing.T) {
|
||||
inputs := []struct {
|
||||
name string
|
||||
object runtime.Object
|
||||
groupVersion schema.GroupVersion
|
||||
path string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "set container image only",
|
||||
object: &extensionsv1beta1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", "nginx=thingy"},
|
||||
},
|
||||
{
|
||||
name: "set initContainer image only",
|
||||
object: &appsv1beta2.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", "nginx=thingy"},
|
||||
},
|
||||
}
|
||||
for _, input := range inputs {
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: input.groupVersion,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == input.path && m == http.MethodGet:
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
case p == input.path && m == http.MethodPatch:
|
||||
stream, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assert.Contains(t, string(bytes), `"image":"`+"thingy"+`","name":`+`"nginx"`, fmt.Sprintf("image not updated for %#v", input.object))
|
||||
assert.NotContains(t, string(bytes), `"image":"`+"thingy"+`","name":`+`"busybox"`, fmt.Sprintf("image updated for %#v", input.object))
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
default:
|
||||
t.Errorf("%s: unexpected request: %s %#v\n%#v", "image", req.Method, req.URL, req)
|
||||
return nil, fmt.Errorf("unexpected request")
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
outputFormat := "yaml"
|
||||
|
||||
streams := genericclioptions.NewTestIOStreamsDiscard()
|
||||
cmd := NewCmdImage(tf, streams)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
opts := SetImageOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
|
||||
Local: false,
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := opts.Complete(tf, cmd, input.args)
|
||||
assert.NoError(t, err)
|
||||
err = opts.Run()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,430 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
appsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/openkruise/kruise-tools/pkg/api"
|
||||
"github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/klog"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
)
|
||||
|
||||
var (
|
||||
resourcesLong = templates.LongDesc(`
|
||||
Specify compute resource requirements (cpu, memory) for any resource that defines a pod template. If a pod is successfully scheduled, it is guaranteed the amount of resource requested, but may burst up to its specified limits.
|
||||
|
||||
for each compute resource, if a limit is specified and a request is omitted, the request will default to the limit.
|
||||
|
||||
Possible resources include (case insensitive): %s.`)
|
||||
|
||||
resourcesExample = templates.Examples(`
|
||||
# Set a deployments nginx container cpu limits to "200m" and memory to "512Mi"
|
||||
kubectl-kruise set resources cloneset sample -c=nginx --limits=cpu=200m,memory=512Mi
|
||||
|
||||
# Set the resource request and limits for all containers in nginx
|
||||
kubectl-kruise set resources cloneset sample --limits=cpu=200m,memory=512Mi --requests=cpu=100m,memory=256Mi
|
||||
|
||||
# Remove the resource requests for resources on containers in nginx
|
||||
kubectl-kruise set resources cloneset sample --limits=cpu=0,memory=0 --requests=cpu=0,memory=0
|
||||
|
||||
# Print the result (in yaml format) of updating nginx container limits from a local, without hitting the server
|
||||
kubectl-kruise set resources -f path/to/file.yaml --limits=cpu=200m,memory=512Mi --local -o yaml`)
|
||||
)
|
||||
|
||||
// SetResourcesOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
|
||||
// referencing the cmd.Flags
|
||||
type SetResourcesOptions struct {
|
||||
resource.FilenameOptions
|
||||
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
|
||||
Infos []*resource.Info
|
||||
Selector string
|
||||
ContainerSelector string
|
||||
Output string
|
||||
All bool
|
||||
Local bool
|
||||
|
||||
DryRunStrategy cmdutil.DryRunStrategy
|
||||
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
Recorder genericclioptions.Recorder
|
||||
|
||||
Limits string
|
||||
Requests string
|
||||
ResourceRequirements corev1.ResourceRequirements
|
||||
|
||||
UpdatePodSpecForObject polymorphichelpers.UpdatePodSpecForObjectFunc
|
||||
Resources []string
|
||||
DryRunVerifier *resource.DryRunVerifier
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewResourcesOptions returns a ResourcesOptions indicating all containers in the selected
|
||||
// pod templates are selected by default.
|
||||
func NewResourcesOptions(streams genericclioptions.IOStreams) *SetResourcesOptions {
|
||||
return &SetResourcesOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("resource requirements updated").WithTypeSetter(scheme.Scheme),
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
|
||||
Recorder: genericclioptions.NoopRecorder{},
|
||||
|
||||
ContainerSelector: "*",
|
||||
|
||||
IOStreams: streams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdResources returns initialized Command instance for the 'set resources' sub command
|
||||
func NewCmdResources(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewResourcesOptions(streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "resources (-f FILENAME | TYPE NAME) ([--limits=LIMITS & --requests=REQUESTS]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Update resource requests/limits on objects with pod templates"),
|
||||
Long: fmt.Sprintf(resourcesLong, cmdutil.SuggestAPIResources("kubectl")),
|
||||
Example: resourcesExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run(f))
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
|
||||
//usage := "Filename, directory, or URL to a file identifying the resource to get from the server"
|
||||
//kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage)
|
||||
usage := "identifying the resource to get from a server."
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||
cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources, including uninitialized ones, in the namespace of the specified resource types")
|
||||
cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on, not including uninitialized ones,supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
||||
cmd.Flags().StringVarP(&o.ContainerSelector, "containers", "c", o.ContainerSelector, "The names of containers in the selected pod templates to change, all containers are selected by default - may use wildcards")
|
||||
cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set resources will NOT contact api-server but run locally.")
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().StringVar(&o.Limits, "limits", o.Limits, "The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi'. Note that server side components may assign requests depending on the server configuration, such as limit ranges.")
|
||||
cmd.Flags().StringVar(&o.Requests, "requests", o.Requests, "The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi'. Note that server side components may assign requests depending on the server configuration, such as limit ranges.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all required options
|
||||
func (o *SetResourcesOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
err = o.RecordFlags.Complete(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.UpdatePodSpecForObject = polymorphichelpers.UpdatePodSpecForObjectFn
|
||||
o.Output = cmdutil.GetFlagString(cmd, "output")
|
||||
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discoveryClient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)
|
||||
|
||||
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = printer.PrintObj
|
||||
|
||||
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder := f.NewBuilder().
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
LocalParam(o.Local).
|
||||
ContinueOnError().
|
||||
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||
Flatten()
|
||||
|
||||
if !o.Local {
|
||||
builder.LabelSelectorParam(o.Selector).
|
||||
ResourceTypeOrNameArgs(o.All, args...).
|
||||
Latest()
|
||||
} else {
|
||||
// if a --local flag was provided, and a resource was specified in the form
|
||||
// <resource>/<name>, fail immediately as --local cannot query the api server
|
||||
// for the specified resource.
|
||||
// TODO: this should be in the builder - if someone specifies tuples, fail when
|
||||
// local is true
|
||||
if len(args) > 0 {
|
||||
return resource.LocalResourceError
|
||||
}
|
||||
}
|
||||
|
||||
o.Infos, err = builder.Do().Infos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure that provided values in ResourcesOptions are valid
|
||||
func (o *SetResourcesOptions) Validate() error {
|
||||
var err error
|
||||
if o.Local && o.DryRunStrategy == cmdutil.DryRunServer {
|
||||
return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?")
|
||||
}
|
||||
if o.All && len(o.Selector) > 0 {
|
||||
return fmt.Errorf("cannot set --all and --selector at the same time")
|
||||
}
|
||||
if len(o.Limits) == 0 && len(o.Requests) == 0 {
|
||||
return fmt.Errorf("you must specify an update to requests or limits (in the form of --requests/--limits)")
|
||||
}
|
||||
|
||||
o.ResourceRequirements, err = generateversioned.HandleResourceRequirementsV1(map[string]string{"limits": o.Limits, "requests": o.Requests})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run performs the execution of 'set resources' sub command
|
||||
func (o *SetResourcesOptions) Run(f cmdutil.Factory) error {
|
||||
|
||||
if len(o.Infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch o.Infos[0].Object.(type) {
|
||||
case *appsv1alpha1.CloneSet:
|
||||
var allErrs []error
|
||||
transformed := false
|
||||
|
||||
cfg, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemeGet := api.GetScheme()
|
||||
mapper, err := apiutil.NewDiscoveryRESTMapper(cfg)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctrl := &control{}
|
||||
|
||||
if ctrl.client, err = client.New(cfg, client.Options{Scheme: schemeGet, Mapper: mapper}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctrl.cache, err = cache.New(cfg, cache.Options{Scheme: schemeGet, Mapper: mapper}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cs := &appsv1alpha1.CloneSet{}
|
||||
if err := ctrl.client.Get(context.TODO(), types.NamespacedName{Namespace: o.Infos[0].Namespace, Name: o.Infos[0].Name}, cs); err != nil {
|
||||
return fmt.Errorf("failed to get %v of %v: %v", o.Infos[0].Namespace, o.Infos[0].Name, err)
|
||||
}
|
||||
|
||||
containers, _ := selectContainers(cs.Spec.Template.Spec.Containers, o.ContainerSelector)
|
||||
|
||||
_, err = meta.NewAccessor().Name(cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvks, _, err := scheme.Scheme.ObjectKinds(cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objKind := cs.GetObjectKind().GroupVersionKind().Kind
|
||||
if len(objKind) == 0 {
|
||||
for _, gvk := range gvks {
|
||||
if len(gvk.Kind) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal {
|
||||
continue
|
||||
}
|
||||
|
||||
objKind = gvk.Kind
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(containers) != 0 {
|
||||
for i := range containers {
|
||||
if len(o.Limits) != 0 && len(containers[i].Resources.Limits) == 0 {
|
||||
containers[i].Resources.Limits = make(corev1.ResourceList)
|
||||
}
|
||||
for key, value := range o.ResourceRequirements.Limits {
|
||||
containers[i].Resources.Limits[key] = value
|
||||
}
|
||||
|
||||
if len(o.Requests) != 0 && len(containers[i].Resources.Requests) == 0 {
|
||||
containers[i].Resources.Requests = make(corev1.ResourceList)
|
||||
}
|
||||
for key, value := range o.ResourceRequirements.Requests {
|
||||
containers[i].Resources.Requests[key] = value
|
||||
}
|
||||
transformed = true
|
||||
}
|
||||
} else {
|
||||
allErrs = append(allErrs, fmt.Errorf("error: unable to find container named %s", o.ContainerSelector))
|
||||
}
|
||||
if !transformed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// record this change (for rollout history)
|
||||
if err := o.Recorder.Record(cs); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
}
|
||||
|
||||
if err := ctrl.client.Update(context.TODO(), cs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(o.Out, "%s resource requirements updated\n", o.Infos[0].ObjectName())
|
||||
|
||||
return utilerrors.NewAggregate(allErrs)
|
||||
|
||||
default:
|
||||
|
||||
var allErrs []error
|
||||
patches := CalculatePatches(o.Infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) {
|
||||
transformed := false
|
||||
_, err := o.UpdatePodSpecForObject(obj, func(spec *corev1.PodSpec) error {
|
||||
containers, _ := selectContainers(spec.Containers, o.ContainerSelector)
|
||||
if len(containers) != 0 {
|
||||
for i := range containers {
|
||||
if len(o.Limits) != 0 && len(containers[i].Resources.Limits) == 0 {
|
||||
containers[i].Resources.Limits = make(corev1.ResourceList)
|
||||
}
|
||||
for key, value := range o.ResourceRequirements.Limits {
|
||||
containers[i].Resources.Limits[key] = value
|
||||
}
|
||||
|
||||
if len(o.Requests) != 0 && len(containers[i].Resources.Requests) == 0 {
|
||||
containers[i].Resources.Requests = make(corev1.ResourceList)
|
||||
}
|
||||
for key, value := range o.ResourceRequirements.Requests {
|
||||
containers[i].Resources.Requests[key] = value
|
||||
}
|
||||
transformed = true
|
||||
}
|
||||
} else {
|
||||
allErrs = append(allErrs, fmt.Errorf("error: unable to find container named %s", o.ContainerSelector))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !transformed {
|
||||
return nil, nil
|
||||
}
|
||||
// record this change (for rollout history)
|
||||
if err := o.Recorder.Record(obj); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
}
|
||||
|
||||
return runtime.Encode(scheme.DefaultJSONEncoder(), obj)
|
||||
})
|
||||
|
||||
for _, patch := range patches {
|
||||
info := patch.Info
|
||||
name := info.ObjectName()
|
||||
if patch.Err != nil {
|
||||
allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err))
|
||||
continue
|
||||
}
|
||||
|
||||
//no changes
|
||||
if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if o.Local || o.DryRunStrategy == cmdutil.DryRunClient {
|
||||
if err := o.PrintObj(info.Object, o.Out); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if o.DryRunStrategy == cmdutil.DryRunServer {
|
||||
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||
allErrs = append(allErrs, fmt.Errorf("failed to patch resources update to pod template %v", err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
actual, err := resource.
|
||||
NewHelper(info.Client, info.Mapping).
|
||||
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
|
||||
Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, fmt.Errorf("failed to patch resources update to pod template %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := o.PrintObj(actual, o.Out); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(allErrs)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,518 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
func TestResourcesLocal(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: ""},
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
|
||||
|
||||
outputFormat := "name"
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdResources(tf, streams)
|
||||
cmd.SetOutput(buf)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
cmd.Flags().Set("local", "true")
|
||||
|
||||
opts := SetResourcesOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
FilenameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"../../../testdata/controller.yaml"}},
|
||||
Local: true,
|
||||
Limits: "cpu=200m,memory=512Mi",
|
||||
Requests: "cpu=200m,memory=512Mi",
|
||||
ContainerSelector: "*",
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
err := opts.Complete(tf, cmd, []string{})
|
||||
if err == nil {
|
||||
err = opts.Validate()
|
||||
}
|
||||
if err == nil {
|
||||
err = opts.Run()
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "replicationcontroller/cassandra") {
|
||||
t.Errorf("did not set resources: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMultiResourcesLimitsLocal(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: ""},
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
|
||||
|
||||
outputFormat := "name"
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdResources(tf, streams)
|
||||
cmd.SetOutput(buf)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
cmd.Flags().Set("local", "true")
|
||||
|
||||
opts := SetResourcesOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
FilenameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"../../../testdata/set/multi-resource-yaml.yaml"}},
|
||||
Local: true,
|
||||
Limits: "cpu=200m,memory=512Mi",
|
||||
Requests: "cpu=200m,memory=512Mi",
|
||||
ContainerSelector: "*",
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
err := opts.Complete(tf, cmd, []string{})
|
||||
if err == nil {
|
||||
err = opts.Validate()
|
||||
}
|
||||
if err == nil {
|
||||
err = opts.Run()
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
expectedOut := "replicationcontroller/first-rc\nreplicationcontroller/second-rc\n"
|
||||
if buf.String() != expectedOut {
|
||||
t.Errorf("expected out:\n%s\nbut got:\n%s", expectedOut, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetResourcesRemote(t *testing.T) {
|
||||
inputs := []struct {
|
||||
name string
|
||||
object runtime.Object
|
||||
groupVersion schema.GroupVersion
|
||||
path string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "set image extensionsv1beta1 ReplicaSet",
|
||||
object: &extensionsv1beta1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta2 ReplicaSet",
|
||||
object: &appsv1beta2.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1 ReplicaSet",
|
||||
object: &appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image extensionsv1beta1 DaemonSet",
|
||||
object: &extensionsv1beta1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.DaemonSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta2 DaemonSet",
|
||||
object: &appsv1beta2.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.DaemonSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1 DaemonSet",
|
||||
object: &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image extensionsv1beta1 Deployment",
|
||||
object: &extensionsv1beta1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: extensionsv1beta1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta1 Deployment",
|
||||
object: &appsv1beta1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta2 Deployment",
|
||||
object: &appsv1beta2.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1 Deployment",
|
||||
object: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta1 StatefulSet",
|
||||
object: &appsv1beta1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta1.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1beta2 StatefulSet",
|
||||
object: &appsv1beta2.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image appsv1 StatefulSet",
|
||||
object: &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image batchv1 Job",
|
||||
object: &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: batchv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/jobs/nginx",
|
||||
args: []string{"job", "nginx"},
|
||||
},
|
||||
{
|
||||
name: "set image corev1.ReplicationController",
|
||||
object: &corev1.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Template: &corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: corev1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicationcontrollers/nginx",
|
||||
args: []string{"replicationcontroller", "nginx"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, input := range inputs {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: input.groupVersion,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == input.path && m == http.MethodGet:
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
case p == input.path && m == http.MethodPatch:
|
||||
stream, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assert.Contains(t, string(bytes), "200m", fmt.Sprintf("resources not updated for %#v", input.object))
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
default:
|
||||
t.Errorf("%s: unexpected request: %s %#v\n%#v", "resources", req.Method, req.URL, req)
|
||||
return nil, fmt.Errorf("unexpected request")
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
outputFormat := "yaml"
|
||||
|
||||
streams := genericclioptions.NewTestIOStreamsDiscard()
|
||||
cmd := NewCmdResources(tf, streams)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
opts := SetResourcesOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
|
||||
Limits: "cpu=200m,memory=512Mi",
|
||||
ContainerSelector: "*",
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := opts.Complete(tf, cmd, input.args)
|
||||
if err == nil {
|
||||
err = opts.Validate()
|
||||
}
|
||||
if err == nil {
|
||||
err = opts.Run()
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/klog"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// SetSelectorOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
|
||||
// referencing the cmd.Flags()
|
||||
type SetSelectorOptions struct {
|
||||
// Bound
|
||||
ResourceBuilderFlags *genericclioptions.ResourceBuilderFlags
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
dryRunStrategy cmdutil.DryRunStrategy
|
||||
dryRunVerifier *resource.DryRunVerifier
|
||||
|
||||
// set by args
|
||||
resources []string
|
||||
selector *metav1.LabelSelector
|
||||
resourceVersion string
|
||||
|
||||
// computed
|
||||
WriteToServer bool
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
Recorder genericclioptions.Recorder
|
||||
ResourceFinder genericclioptions.ResourceFinder
|
||||
|
||||
// set at initialization
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
var (
|
||||
selectorLong = templates.LongDesc(`
|
||||
Set the selector on a resource. Note that the new selector will overwrite the old selector if the resource had one prior to the invocation
|
||||
of 'set selector'.
|
||||
|
||||
A selector must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[1]d characters.
|
||||
If --resource-version is specified, then updates will use this resource version, otherwise the existing resource-version will be used.
|
||||
Note: currently selectors can only be set on Service objects.`)
|
||||
selectorExample = templates.Examples(`
|
||||
# set the labels and selector before creating a deployment/service pair.
|
||||
kubectl create service clusterip my-svc --clusterip="None" -o yaml --dry-run=client | kubectl-kruise set selector --local -f - 'environment=qa' -o yaml | kubectl create -f -
|
||||
kubectl create cloneset sample -o yaml --dry-run=client | kubectl label --local -f - environment=qa -o yaml | kubectl create -f -`)
|
||||
)
|
||||
|
||||
// NewSelectorOptions returns an initialized SelectorOptions instance
|
||||
func NewSelectorOptions(streams genericclioptions.IOStreams) *SetSelectorOptions {
|
||||
return &SetSelectorOptions{
|
||||
ResourceBuilderFlags: genericclioptions.NewResourceBuilderFlags().
|
||||
WithScheme(scheme.Scheme).
|
||||
WithAll(false).
|
||||
WithLocal(false).
|
||||
WithLatest(),
|
||||
PrintFlags: genericclioptions.NewPrintFlags("selector updated").WithTypeSetter(scheme.Scheme),
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
|
||||
Recorder: genericclioptions.NoopRecorder{},
|
||||
|
||||
IOStreams: streams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdSelector is the "set selector" command.
|
||||
func NewCmdSelector(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewSelectorOptions(streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "selector (-f FILENAME | TYPE NAME) EXPRESSIONS [--resource-version=version]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Set the selector on a resource"),
|
||||
Long: fmt.Sprintf(selectorLong, validation.LabelValueMaxLength),
|
||||
Example: selectorExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunSelector())
|
||||
},
|
||||
}
|
||||
|
||||
o.ResourceBuilderFlags.AddFlags(cmd.Flags())
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
|
||||
cmd.Flags().StringVarP(&o.resourceVersion, "resource-version", "", o.resourceVersion, "If non-empty, the selectors update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.")
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete assigns the SelectorOptions from args.
|
||||
func (o *SetSelectorOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
o.RecordFlags.Complete(cmd)
|
||||
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discoveryClient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.dryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)
|
||||
|
||||
o.resources, o.selector, err = getResourcesAndSelector(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.ResourceFinder = o.ResourceBuilderFlags.ToBuilder(f, o.resources)
|
||||
o.WriteToServer = !(*o.ResourceBuilderFlags.Local || o.dryRunStrategy == cmdutil.DryRunClient)
|
||||
|
||||
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy)
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = printer.PrintObj
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate basic inputs
|
||||
func (o *SetSelectorOptions) Validate() error {
|
||||
if o.selector == nil {
|
||||
return fmt.Errorf("one selector is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunSelector executes the command.
|
||||
func (o *SetSelectorOptions) RunSelector() error {
|
||||
r := o.ResourceFinder.Do()
|
||||
|
||||
return r.Visit(func(info *resource.Info, err error) error {
|
||||
patch := &Patch{Info: info}
|
||||
|
||||
if len(o.resourceVersion) != 0 {
|
||||
// ensure resourceVersion is always sent in the patch by clearing it from the starting JSON
|
||||
accessor, err := meta.Accessor(info.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessor.SetResourceVersion("")
|
||||
}
|
||||
|
||||
CalculatePatch(patch, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) {
|
||||
|
||||
if len(o.resourceVersion) != 0 {
|
||||
accessor, err := meta.Accessor(info.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessor.SetResourceVersion(o.resourceVersion)
|
||||
}
|
||||
|
||||
selectErr := updateSelectorForObject(info.Object, *o.selector)
|
||||
if selectErr != nil {
|
||||
return nil, selectErr
|
||||
}
|
||||
|
||||
// record this change (for rollout history)
|
||||
if err := o.Recorder.Record(patch.Info.Object); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
}
|
||||
|
||||
return runtime.Encode(scheme.DefaultJSONEncoder(), info.Object)
|
||||
})
|
||||
|
||||
if patch.Err != nil {
|
||||
return patch.Err
|
||||
}
|
||||
if !o.WriteToServer {
|
||||
return o.PrintObj(info.Object, o.Out)
|
||||
}
|
||||
if o.dryRunStrategy == cmdutil.DryRunServer {
|
||||
if err := o.dryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
actual, err := resource.
|
||||
NewHelper(info.Client, info.Mapping).
|
||||
DryRun(o.dryRunStrategy == cmdutil.DryRunServer).
|
||||
Patch(info.Namespace, info.Name, types.MergePatchType, patch.Patch, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return o.PrintObj(actual, o.Out)
|
||||
})
|
||||
}
|
||||
|
||||
func updateSelectorForObject(obj runtime.Object, selector metav1.LabelSelector) error {
|
||||
copyOldSelector := func() (map[string]string, error) {
|
||||
if len(selector.MatchExpressions) > 0 {
|
||||
return nil, fmt.Errorf("match expression %v not supported on this object", selector.MatchExpressions)
|
||||
}
|
||||
dst := make(map[string]string)
|
||||
for label, value := range selector.MatchLabels {
|
||||
dst[label] = value
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
var err error
|
||||
switch t := obj.(type) {
|
||||
case *corev1.Service:
|
||||
t.Spec.Selector, err = copyOldSelector()
|
||||
default:
|
||||
err = fmt.Errorf("setting a selector is only supported for Services")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// getResourcesAndSelector retrieves resources and the selector expression from the given args (assuming selectors the last arg)
|
||||
func getResourcesAndSelector(args []string) (resources []string, selector *metav1.LabelSelector, err error) {
|
||||
if len(args) == 0 {
|
||||
return []string{}, nil, nil
|
||||
}
|
||||
resources = args[:len(args)-1]
|
||||
selector, err = metav1.ParseToLabelSelector(args[len(args)-1])
|
||||
return resources, selector, err
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
"k8s.io/api/core/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
func TestUpdateSelectorForObjectTypes(t *testing.T) {
|
||||
before := metav1.LabelSelector{MatchLabels: map[string]string{"fee": "true"},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "foo",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"on", "yes"},
|
||||
},
|
||||
}}
|
||||
|
||||
rc := v1.ReplicationController{}
|
||||
ser := v1.Service{}
|
||||
dep := extensionsv1beta1.Deployment{Spec: extensionsv1beta1.DeploymentSpec{Selector: &before}}
|
||||
ds := extensionsv1beta1.DaemonSet{Spec: extensionsv1beta1.DaemonSetSpec{Selector: &before}}
|
||||
rs := extensionsv1beta1.ReplicaSet{Spec: extensionsv1beta1.ReplicaSetSpec{Selector: &before}}
|
||||
job := batchv1.Job{Spec: batchv1.JobSpec{Selector: &before}}
|
||||
pvc := v1.PersistentVolumeClaim{Spec: v1.PersistentVolumeClaimSpec{Selector: &before}}
|
||||
sa := v1.ServiceAccount{}
|
||||
type args struct {
|
||||
obj runtime.Object
|
||||
selector metav1.LabelSelector
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "rc",
|
||||
args: args{
|
||||
obj: &rc,
|
||||
selector: metav1.LabelSelector{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "ser",
|
||||
args: args{
|
||||
obj: &ser,
|
||||
selector: metav1.LabelSelector{},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{name: "dep",
|
||||
args: args{
|
||||
obj: &dep,
|
||||
selector: metav1.LabelSelector{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "ds",
|
||||
args: args{
|
||||
obj: &ds,
|
||||
selector: metav1.LabelSelector{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "rs",
|
||||
args: args{
|
||||
obj: &rs,
|
||||
selector: metav1.LabelSelector{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "job",
|
||||
args: args{
|
||||
obj: &job,
|
||||
selector: metav1.LabelSelector{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "pvc - no updates",
|
||||
args: args{
|
||||
obj: &pvc,
|
||||
selector: metav1.LabelSelector{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "sa - no selector",
|
||||
args: args{
|
||||
obj: &sa,
|
||||
selector: metav1.LabelSelector{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if err := updateSelectorForObject(tt.args.obj, tt.args.selector); (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNewSelectorValuesForObject(t *testing.T) {
|
||||
ser := v1.Service{}
|
||||
type args struct {
|
||||
obj runtime.Object
|
||||
selector metav1.LabelSelector
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "empty",
|
||||
args: args{
|
||||
obj: &ser,
|
||||
selector: metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{name: "label-only",
|
||||
args: args{
|
||||
obj: &ser,
|
||||
selector: metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"b": "u"},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if err := updateSelectorForObject(tt.args.obj, tt.args.selector); (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
}
|
||||
|
||||
assert.EqualValues(t, tt.args.selector.MatchLabels, ser.Spec.Selector, tt.name)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateOldSelectorValuesForObject(t *testing.T) {
|
||||
ser := v1.Service{Spec: v1.ServiceSpec{Selector: map[string]string{"fee": "true"}}}
|
||||
type args struct {
|
||||
obj runtime.Object
|
||||
selector metav1.LabelSelector
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "empty",
|
||||
args: args{
|
||||
obj: &ser,
|
||||
selector: metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{name: "label-only",
|
||||
args: args{
|
||||
obj: &ser,
|
||||
selector: metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"fee": "false", "x": "y"},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{name: "expr-only - err",
|
||||
args: args{
|
||||
obj: &ser,
|
||||
selector: metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "a",
|
||||
Operator: "In",
|
||||
Values: []string{"x", "y"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "both - err",
|
||||
args: args{
|
||||
obj: &ser,
|
||||
selector: metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"b": "u"},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "a",
|
||||
Operator: "In",
|
||||
Values: []string{"x", "y"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
err := updateSelectorForObject(tt.args.obj, tt.args.selector)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
} else if !tt.wantErr {
|
||||
assert.EqualValues(t, tt.args.selector.MatchLabels, ser.Spec.Selector, tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetResourcesAndSelector(t *testing.T) {
|
||||
type args struct {
|
||||
args []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantResources []string
|
||||
wantSelector *metav1.LabelSelector
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic match",
|
||||
args: args{args: []string{"rc/foo", "healthy=true"}},
|
||||
wantResources: []string{"rc/foo"},
|
||||
wantErr: false,
|
||||
wantSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"healthy": "true"},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "basic expression",
|
||||
args: args{args: []string{"rc/foo", "buildType notin (debug, test)"}},
|
||||
wantResources: []string{"rc/foo"},
|
||||
wantErr: false,
|
||||
wantSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "buildType",
|
||||
Operator: "NotIn",
|
||||
Values: []string{"debug", "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "selector error",
|
||||
args: args{args: []string{"rc/foo", "buildType notthis (debug, test)"}},
|
||||
wantResources: []string{"rc/foo"},
|
||||
wantErr: true,
|
||||
wantSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no resource and selector",
|
||||
args: args{args: []string{}},
|
||||
wantResources: []string{},
|
||||
wantErr: false,
|
||||
wantSelector: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
gotResources, gotSelector, err := getResourcesAndSelector(tt.args.args)
|
||||
if err != nil {
|
||||
if !tt.wantErr {
|
||||
t.Errorf("%q. getResourcesAndSelector() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(gotResources, tt.wantResources) {
|
||||
t.Errorf("%q. getResourcesAndSelector() gotResources = %v, want %v", tt.name, gotResources, tt.wantResources)
|
||||
}
|
||||
if !reflect.DeepEqual(gotSelector, tt.wantSelector) {
|
||||
t.Errorf("%q. getResourcesAndSelector() gotSelector = %v, want %v", tt.name, gotSelector, tt.wantSelector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectorTest(t *testing.T) {
|
||||
info := &resource.Info{
|
||||
Object: &v1.Service{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"},
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "some-ns", Name: "cassandra"},
|
||||
},
|
||||
}
|
||||
|
||||
labelToSet, err := metav1.ParseToLabelSelector("environment=qa")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
iostreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
o := &SetSelectorOptions{
|
||||
selector: labelToSet,
|
||||
ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(info),
|
||||
Recorder: genericclioptions.NoopRecorder{},
|
||||
PrintObj: (&printers.NamePrinter{}).PrintObj,
|
||||
IOStreams: iostreams,
|
||||
}
|
||||
|
||||
if err := o.RunSelector(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "service/cassandra") {
|
||||
t.Errorf("did not set selector: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/klog"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
serviceaccountResources = `
|
||||
replicationcontroller (rc), deployment (deploy), daemonset (ds), job, replicaset (rs), statefulset, cloneset (cs)`
|
||||
|
||||
serviceaccountLong = templates.LongDesc(i18n.T(`
|
||||
Update ServiceAccount of pod template resources.
|
||||
|
||||
Possible resources (case insensitive) can be:
|
||||
` + serviceaccountResources))
|
||||
|
||||
serviceaccountExample = templates.Examples(i18n.T(`
|
||||
# Set Deployment nginx-deployment's ServiceAccount to serviceaccount1
|
||||
kubectl-kruise set serviceaccount cloneset sample serviceaccount1
|
||||
|
||||
# Print the result (in yaml format) of updated cloneset with serviceaccount from local file, without hitting apiserver
|
||||
kubectl-kruise set sa -f CloneSet.yaml serviceaccount1 --local --dry-run=client -o yaml
|
||||
`))
|
||||
)
|
||||
|
||||
// SetServiceAccountOptions encapsulates the data required to perform the operation.
|
||||
type SetServiceAccountOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
|
||||
fileNameOptions resource.FilenameOptions
|
||||
dryRunStrategy cmdutil.DryRunStrategy
|
||||
dryRunVerifier *resource.DryRunVerifier
|
||||
shortOutput bool
|
||||
all bool
|
||||
output string
|
||||
local bool
|
||||
updatePodSpecForObject polymorphichelpers.UpdatePodSpecForObjectFunc
|
||||
infos []*resource.Info
|
||||
serviceAccountName string
|
||||
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
Recorder genericclioptions.Recorder
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewSetServiceAccountOptions returns an initialized SetServiceAccountOptions instance
|
||||
func NewSetServiceAccountOptions(streams genericclioptions.IOStreams) *SetServiceAccountOptions {
|
||||
return &SetServiceAccountOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("serviceaccount updated").WithTypeSetter(scheme.Scheme),
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
|
||||
Recorder: genericclioptions.NoopRecorder{},
|
||||
|
||||
IOStreams: streams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdServiceAccount returns the "set serviceaccount" command.
|
||||
func NewCmdServiceAccount(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewSetServiceAccountOptions(streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "serviceaccount (-f FILENAME | TYPE NAME) SERVICE_ACCOUNT",
|
||||
DisableFlagsInUseLine: true,
|
||||
Aliases: []string{"sa"},
|
||||
Short: i18n.T("Update ServiceAccount of a resource"),
|
||||
Long: serviceaccountLong,
|
||||
Example: serviceaccountExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
|
||||
usage := "identifying the resource to get from a server."
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.fileNameOptions, usage)
|
||||
cmd.Flags().BoolVar(&o.all, "all", o.all, "Select all resources, including uninitialized ones, in the namespace of the specified resource types")
|
||||
cmd.Flags().BoolVar(&o.local, "local", o.local, "If true, set serviceaccount will NOT contact api-server but run locally.")
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete configures serviceAccountConfig from command line args.
|
||||
func (o *SetServiceAccountOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
o.RecordFlags.Complete(cmd)
|
||||
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.shortOutput = cmdutil.GetFlagString(cmd, "output") == "name"
|
||||
o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if o.local && o.dryRunStrategy == cmdutil.DryRunServer {
|
||||
return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?")
|
||||
}
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discoveryClient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.dryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)
|
||||
o.output = cmdutil.GetFlagString(cmd, "output")
|
||||
o.updatePodSpecForObject = polymorphichelpers.UpdatePodSpecForObjectFn
|
||||
|
||||
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy)
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = printer.PrintObj
|
||||
|
||||
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return errors.New("serviceaccount is required")
|
||||
}
|
||||
o.serviceAccountName = args[len(args)-1]
|
||||
resources := args[:len(args)-1]
|
||||
builder := f.NewBuilder().
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
LocalParam(o.local).
|
||||
ContinueOnError().
|
||||
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||
FilenameParam(enforceNamespace, &o.fileNameOptions).
|
||||
Flatten()
|
||||
if !o.local {
|
||||
builder.ResourceTypeOrNameArgs(o.all, resources...).
|
||||
Latest()
|
||||
}
|
||||
o.infos, err = builder.Do().Infos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run creates and applies the patch either locally or calling apiserver.
|
||||
func (o *SetServiceAccountOptions) Run() error {
|
||||
var patchErrs []error
|
||||
patchFn := func(obj runtime.Object) ([]byte, error) {
|
||||
_, err := o.updatePodSpecForObject(obj, func(podSpec *corev1.PodSpec) error {
|
||||
podSpec.ServiceAccountName = o.serviceAccountName
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// record this change (for rollout history)
|
||||
if err := o.Recorder.Record(obj); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
}
|
||||
|
||||
return runtime.Encode(scheme.DefaultJSONEncoder(), obj)
|
||||
}
|
||||
|
||||
patches := CalculatePatches(o.infos, scheme.DefaultJSONEncoder(), patchFn)
|
||||
for _, patch := range patches {
|
||||
info := patch.Info
|
||||
name := info.ObjectName()
|
||||
if patch.Err != nil {
|
||||
patchErrs = append(patchErrs, fmt.Errorf("error: %s %v\n", name, patch.Err))
|
||||
continue
|
||||
}
|
||||
if o.local || o.dryRunStrategy == cmdutil.DryRunClient {
|
||||
if err := o.PrintObj(info.Object, o.Out); err != nil {
|
||||
patchErrs = append(patchErrs, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if o.dryRunStrategy == cmdutil.DryRunServer {
|
||||
if err := o.dryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||
patchErrs = append(patchErrs, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
actual, err := resource.
|
||||
NewHelper(info.Client, info.Mapping).
|
||||
DryRun(o.dryRunStrategy == cmdutil.DryRunServer).
|
||||
Patch(info.Namespace, info.Name, types.MergePatchType, patch.Patch, nil)
|
||||
if err != nil {
|
||||
patchErrs = append(patchErrs, fmt.Errorf("failed to patch ServiceAccountName %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := o.PrintObj(actual, o.Out); err != nil {
|
||||
patchErrs = append(patchErrs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(patchErrs)
|
||||
}
|
||||
|
|
@ -0,0 +1,407 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceAccount = "serviceaccount1"
|
||||
serviceAccountMissingErrString = "serviceaccount is required"
|
||||
resourceMissingErrString = `You must provide one or more resources by argument or filename.
|
||||
Example resource specifications include:
|
||||
'-f rsrc.yaml'
|
||||
'--filename=rsrc.json'
|
||||
'<resource> <name>'
|
||||
'<resource>'`
|
||||
)
|
||||
|
||||
func TestSetServiceAccountLocal(t *testing.T) {
|
||||
inputs := []struct {
|
||||
yaml string
|
||||
apiGroup string
|
||||
}{
|
||||
{yaml: "../../../testdata/set/replication.yaml", apiGroup: ""},
|
||||
{yaml: "../../../testdata/set/daemon.yaml", apiGroup: "extensions"},
|
||||
{yaml: "../../../testdata/set/redis-slave.yaml", apiGroup: "extensions"},
|
||||
{yaml: "../../../testdata/set/job.yaml", apiGroup: "batch"},
|
||||
{yaml: "../../../testdata/set/deployment.yaml", apiGroup: "extensions"},
|
||||
}
|
||||
|
||||
for i, input := range inputs {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
|
||||
outputFormat := "yaml"
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdServiceAccount(tf, streams)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
cmd.Flags().Set("local", "true")
|
||||
saConfig := SetServiceAccountOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
fileNameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{input.yaml}},
|
||||
local: true,
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := saConfig.Complete(tf, cmd, []string{serviceAccount})
|
||||
assert.NoError(t, err)
|
||||
err = saConfig.Run()
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, buf.String(), "serviceAccountName: "+serviceAccount, fmt.Sprintf("serviceaccount not updated for %s", input.yaml))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetServiceAccountMultiLocal(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: ""},
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
|
||||
|
||||
outputFormat := "name"
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdServiceAccount(tf, streams)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
cmd.Flags().Set("local", "true")
|
||||
opts := SetServiceAccountOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
fileNameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"../../../testdata/set/multi-resource-yaml.yaml"}},
|
||||
local: true,
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
err := opts.Complete(tf, cmd, []string{serviceAccount})
|
||||
if err == nil {
|
||||
err = opts.Run()
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
expectedOut := "replicationcontroller/first-rc\nreplicationcontroller/second-rc\n"
|
||||
if buf.String() != expectedOut {
|
||||
t.Errorf("expected out:\n%s\nbut got:\n%s", expectedOut, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetServiceAccountRemote(t *testing.T) {
|
||||
inputs := []struct {
|
||||
object runtime.Object
|
||||
groupVersion schema.GroupVersion
|
||||
path string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
object: &extensionsv1beta1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1beta2.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1beta2.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.ReplicaSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicasets/nginx",
|
||||
args: []string{"replicaset", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &extensionsv1beta1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1beta2.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/daemonsets/nginx",
|
||||
args: []string{"daemonset", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &extensionsv1beta1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: extensionsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1beta1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: appsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1beta2.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/deployments/nginx",
|
||||
args: []string{"deployment", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1beta1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: appsv1beta1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1beta2.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: appsv1beta2.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groupVersion: appsv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/statefulsets/nginx",
|
||||
args: []string{"statefulset", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: batchv1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/jobs/nginx",
|
||||
args: []string{"job", "nginx", serviceAccount},
|
||||
},
|
||||
{
|
||||
object: &corev1.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nginx"},
|
||||
},
|
||||
groupVersion: corev1.SchemeGroupVersion,
|
||||
path: "/namespaces/test/replicationcontrollers/nginx",
|
||||
args: []string{"replicationcontroller", "nginx", serviceAccount},
|
||||
},
|
||||
}
|
||||
for i, input := range inputs {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: input.groupVersion,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == input.path && m == http.MethodGet:
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
case p == input.path && m == http.MethodPatch:
|
||||
stream, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assert.Contains(t, string(bytes), `"serviceAccountName":`+`"`+serviceAccount+`"`, fmt.Sprintf("serviceaccount not updated for %#v", input.object))
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil
|
||||
default:
|
||||
t.Errorf("%s: unexpected request: %s %#v\n%#v", "serviceaccount", req.Method, req.URL, req)
|
||||
return nil, fmt.Errorf("unexpected request")
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
outputFormat := "yaml"
|
||||
|
||||
streams := genericclioptions.NewTestIOStreamsDiscard()
|
||||
cmd := NewCmdServiceAccount(tf, streams)
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
saConfig := SetServiceAccountOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
|
||||
local: false,
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := saConfig.Complete(tf, cmd, input.args)
|
||||
assert.NoError(t, err)
|
||||
err = saConfig.Run()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceAccountValidation(t *testing.T) {
|
||||
inputs := []struct {
|
||||
name string
|
||||
args []string
|
||||
errorString string
|
||||
}{
|
||||
{name: "test service account missing", args: []string{}, errorString: serviceAccountMissingErrString},
|
||||
{name: "test service account resource missing", args: []string{serviceAccount}, errorString: resourceMissingErrString},
|
||||
}
|
||||
for _, input := range inputs {
|
||||
t.Run(input.name, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
|
||||
outputFormat := ""
|
||||
|
||||
streams := genericclioptions.NewTestIOStreamsDiscard()
|
||||
cmd := NewCmdServiceAccount(tf, streams)
|
||||
|
||||
saConfig := &SetServiceAccountOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := saConfig.Complete(tf, cmd, input.args)
|
||||
assert.EqualError(t, err, input.errorString)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func objBody(obj runtime.Object) io.ReadCloser {
|
||||
return cmdtesting.BytesBody([]byte(runtime.EncodeOrDie(scheme.DefaultJSONEncoder(), obj)))
|
||||
}
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
subjectLong = templates.LongDesc(`
|
||||
Update User, Group or ServiceAccount in a RoleBinding/ClusterRoleBinding.`)
|
||||
|
||||
subjectExample = templates.Examples(`
|
||||
# Update a ClusterRoleBinding for serviceaccount1
|
||||
kubectl-kruise set subject clusterrolebinding admin --serviceaccount=namespace:serviceaccount1
|
||||
|
||||
# Update a RoleBinding for user1, user2, and group1
|
||||
kubectl-kruise set subject rolebinding admin --user=user1 --user=user2 --group=group1
|
||||
|
||||
# Print the result (in yaml format) of updating rolebinding subjects from a local, without hitting the server
|
||||
kubectl create rolebinding admin --role=admin --user=admin -o yaml --dry-run=client | kubectl-kruise set subject --local -f - --user=foo -o yaml`)
|
||||
)
|
||||
|
||||
type updateSubjects func(existings []rbacv1.Subject, targets []rbacv1.Subject) (bool, []rbacv1.Subject)
|
||||
|
||||
// SubjectOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
|
||||
// referencing the cmd.Flags
|
||||
type SubjectOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
|
||||
resource.FilenameOptions
|
||||
|
||||
Infos []*resource.Info
|
||||
Selector string
|
||||
ContainerSelector string
|
||||
Output string
|
||||
All bool
|
||||
DryRunStrategy cmdutil.DryRunStrategy
|
||||
DryRunVerifier *resource.DryRunVerifier
|
||||
Local bool
|
||||
|
||||
Users []string
|
||||
Groups []string
|
||||
ServiceAccounts []string
|
||||
|
||||
namespace string
|
||||
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewSubjectOptions returns an initialized SubjectOptions instance
|
||||
func NewSubjectOptions(streams genericclioptions.IOStreams) *SubjectOptions {
|
||||
return &SubjectOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("subjects updated").WithTypeSetter(scheme.Scheme),
|
||||
|
||||
IOStreams: streams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdSubject returns the "new subject" sub command
|
||||
func NewCmdSubject(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewSubjectOptions(streams)
|
||||
cmd := &cobra.Command{
|
||||
Use: "subject (-f FILENAME | TYPE NAME) [--user=username] [--group=groupname] [--serviceaccount=namespace:serviceaccountname] [--dry-run=server|client|none]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Update User, Group or ServiceAccount in a RoleBinding/ClusterRoleBinding"),
|
||||
Long: subjectLong,
|
||||
Example: subjectExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run(addSubjects))
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "the resource to update the subjects")
|
||||
cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources, including uninitialized ones, in the namespace of the specified resource types")
|
||||
cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on, not including uninitialized ones, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
||||
cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set subject will NOT contact api-server but run locally.")
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().StringArrayVar(&o.Users, "user", o.Users, "Usernames to bind to the role")
|
||||
cmd.Flags().StringArrayVar(&o.Groups, "group", o.Groups, "Groups to bind to the role")
|
||||
cmd.Flags().StringArrayVar(&o.ServiceAccounts, "serviceaccount", o.ServiceAccounts, "Service accounts to bind to the role")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all required options
|
||||
func (o *SubjectOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
o.Output = cmdutil.GetFlagString(cmd, "output")
|
||||
var err error
|
||||
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discoveryClient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)
|
||||
|
||||
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = printer.PrintObj
|
||||
|
||||
var enforceNamespace bool
|
||||
o.namespace, enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builder := f.NewBuilder().
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
LocalParam(o.Local).
|
||||
ContinueOnError().
|
||||
NamespaceParam(o.namespace).DefaultNamespace().
|
||||
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||
Flatten()
|
||||
|
||||
if o.Local {
|
||||
// if a --local flag was provided, and a resource was specified in the form
|
||||
// <resource>/<name>, fail immediately as --local cannot query the api server
|
||||
// for the specified resource.
|
||||
if len(args) > 0 {
|
||||
return resource.LocalResourceError
|
||||
}
|
||||
} else {
|
||||
builder = builder.
|
||||
LabelSelectorParam(o.Selector).
|
||||
ResourceTypeOrNameArgs(o.All, args...).
|
||||
Latest()
|
||||
}
|
||||
|
||||
o.Infos, err = builder.Do().Infos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure provided values in SubjectOptions are valid
|
||||
func (o *SubjectOptions) Validate() error {
|
||||
if o.Local && o.DryRunStrategy == cmdutil.DryRunServer {
|
||||
return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?")
|
||||
}
|
||||
if o.All && len(o.Selector) > 0 {
|
||||
return fmt.Errorf("cannot set --all and --selector at the same time")
|
||||
}
|
||||
if len(o.Users) == 0 && len(o.Groups) == 0 && len(o.ServiceAccounts) == 0 {
|
||||
return fmt.Errorf("you must specify at least one value of user, group or serviceaccount")
|
||||
}
|
||||
|
||||
for _, sa := range o.ServiceAccounts {
|
||||
tokens := strings.Split(sa, ":")
|
||||
if len(tokens) != 2 || tokens[1] == "" {
|
||||
return fmt.Errorf("serviceaccount must be <namespace>:<name>")
|
||||
}
|
||||
|
||||
for _, info := range o.Infos {
|
||||
_, ok := info.Object.(*rbacv1.ClusterRoleBinding)
|
||||
if ok && tokens[0] == "" {
|
||||
return fmt.Errorf("serviceaccount must be <namespace>:<name>, namespace must be specified")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run performs the execution of "set subject" sub command
|
||||
func (o *SubjectOptions) Run(fn updateSubjects) error {
|
||||
patches := CalculatePatches(o.Infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) {
|
||||
var subjects []rbacv1.Subject
|
||||
for _, user := range sets.NewString(o.Users...).List() {
|
||||
subject := rbacv1.Subject{
|
||||
Kind: rbacv1.UserKind,
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Name: user,
|
||||
}
|
||||
subjects = append(subjects, subject)
|
||||
}
|
||||
for _, group := range sets.NewString(o.Groups...).List() {
|
||||
subject := rbacv1.Subject{
|
||||
Kind: rbacv1.GroupKind,
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Name: group,
|
||||
}
|
||||
subjects = append(subjects, subject)
|
||||
}
|
||||
for _, sa := range sets.NewString(o.ServiceAccounts...).List() {
|
||||
tokens := strings.Split(sa, ":")
|
||||
namespace := tokens[0]
|
||||
name := tokens[1]
|
||||
if len(namespace) == 0 {
|
||||
namespace = o.namespace
|
||||
}
|
||||
subject := rbacv1.Subject{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
}
|
||||
subjects = append(subjects, subject)
|
||||
}
|
||||
|
||||
transformed, err := updateSubjectForObject(obj, subjects, fn)
|
||||
if transformed && err == nil {
|
||||
// TODO: switch UpdatePodSpecForObject to work on v1.PodSpec
|
||||
return runtime.Encode(scheme.DefaultJSONEncoder(), obj)
|
||||
}
|
||||
return nil, err
|
||||
})
|
||||
|
||||
var allErrs []error
|
||||
for _, patch := range patches {
|
||||
info := patch.Info
|
||||
name := info.ObjectName()
|
||||
if patch.Err != nil {
|
||||
allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err))
|
||||
continue
|
||||
}
|
||||
|
||||
//no changes
|
||||
if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if o.Local || o.DryRunStrategy == cmdutil.DryRunClient {
|
||||
if err := o.PrintObj(info.Object, o.Out); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if o.DryRunStrategy == cmdutil.DryRunServer {
|
||||
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
actual, err := resource.
|
||||
NewHelper(info.Client, info.Mapping).
|
||||
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
|
||||
Patch(info.Namespace, info.Name, types.MergePatchType, patch.Patch, nil)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, fmt.Errorf("failed to patch subjects to rolebinding: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := o.PrintObj(actual, o.Out); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(allErrs)
|
||||
}
|
||||
|
||||
//Note: the obj mutates in the function
|
||||
func updateSubjectForObject(obj runtime.Object, subjects []rbacv1.Subject, fn updateSubjects) (bool, error) {
|
||||
switch t := obj.(type) {
|
||||
case *rbacv1.RoleBinding:
|
||||
transformed, result := fn(t.Subjects, subjects)
|
||||
t.Subjects = result
|
||||
return transformed, nil
|
||||
case *rbacv1.ClusterRoleBinding:
|
||||
transformed, result := fn(t.Subjects, subjects)
|
||||
t.Subjects = result
|
||||
return transformed, nil
|
||||
default:
|
||||
return false, fmt.Errorf("setting subjects is only supported for RoleBinding/ClusterRoleBinding")
|
||||
}
|
||||
}
|
||||
|
||||
func addSubjects(existings []rbacv1.Subject, targets []rbacv1.Subject) (bool, []rbacv1.Subject) {
|
||||
transformed := false
|
||||
updated := existings
|
||||
for _, item := range targets {
|
||||
if !contain(existings, item) {
|
||||
updated = append(updated, item)
|
||||
transformed = true
|
||||
}
|
||||
}
|
||||
return transformed, updated
|
||||
}
|
||||
|
||||
func contain(slice []rbacv1.Subject, item rbacv1.Subject) bool {
|
||||
for _, v := range slice {
|
||||
if v == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tests := map[string]struct {
|
||||
options *SubjectOptions
|
||||
expectErr bool
|
||||
}{
|
||||
"test-missing-subjects": {
|
||||
options: &SubjectOptions{
|
||||
Users: []string{},
|
||||
Groups: []string{},
|
||||
ServiceAccounts: []string{},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-invalid-serviceaccounts": {
|
||||
options: &SubjectOptions{
|
||||
Users: []string{},
|
||||
Groups: []string{},
|
||||
ServiceAccounts: []string{"foo"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-serviceaccounts-name": {
|
||||
options: &SubjectOptions{
|
||||
Users: []string{},
|
||||
Groups: []string{},
|
||||
ServiceAccounts: []string{"foo:"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-serviceaccounts-namespace": {
|
||||
options: &SubjectOptions{
|
||||
Infos: []*resource.Info{
|
||||
{
|
||||
Object: &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "clusterrolebinding",
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "role",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []string{},
|
||||
Groups: []string{},
|
||||
ServiceAccounts: []string{":foo"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-valid-case": {
|
||||
options: &SubjectOptions{
|
||||
Infos: []*resource.Info{
|
||||
{
|
||||
Object: &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "rolebinding",
|
||||
Namespace: "one",
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "role",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []string{"foo"},
|
||||
Groups: []string{"foo"},
|
||||
ServiceAccounts: []string{"ns:foo"},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
err := test.options.Validate()
|
||||
if test.expectErr && err != nil {
|
||||
continue
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSubjectForObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
obj runtime.Object
|
||||
subjects []rbacv1.Subject
|
||||
expected []rbacv1.Subject
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
Name: "invalid object type",
|
||||
obj: &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "role",
|
||||
Namespace: "one",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
Name: "add resource with users in rolebinding",
|
||||
obj: &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "rolebinding",
|
||||
Namespace: "one",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
subjects: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
expected: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
Name: "add resource with groups in rolebinding",
|
||||
obj: &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "rolebinding",
|
||||
Namespace: "one",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Group",
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
subjects: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Group",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Group",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
expected: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Group",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Group",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
Name: "add resource with serviceaccounts in rolebinding",
|
||||
obj: &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "rolebinding",
|
||||
Namespace: "one",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
expected: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
Name: "add resource with serviceaccounts in clusterrolebinding",
|
||||
obj: &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "clusterrolebinding",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Group",
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Group",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if _, err := updateSubjectForObject(tt.obj, tt.subjects, addSubjects); (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. updateSubjectForObject() error = %v, wantErr %v", tt.Name, err, tt.wantErr)
|
||||
}
|
||||
|
||||
want := tt.expected
|
||||
var got []rbacv1.Subject
|
||||
switch t := tt.obj.(type) {
|
||||
case *rbacv1.RoleBinding:
|
||||
got = t.Subjects
|
||||
case *rbacv1.ClusterRoleBinding:
|
||||
got = t.Subjects
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%q. updateSubjectForObject() failed", tt.Name)
|
||||
t.Errorf("Got: %v", got)
|
||||
t.Errorf("Want: %v", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSubject(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
existing []rbacv1.Subject
|
||||
subjects []rbacv1.Subject
|
||||
expected []rbacv1.Subject
|
||||
wantChange bool
|
||||
}{
|
||||
{
|
||||
Name: "add resource with users",
|
||||
existing: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
subjects: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
wantChange: false,
|
||||
},
|
||||
{
|
||||
Name: "add resource with serviceaccounts",
|
||||
existing: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "two",
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "a",
|
||||
},
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "one",
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: "two",
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
wantChange: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
changed := false
|
||||
got := []rbacv1.Subject{}
|
||||
if changed, got = addSubjects(tt.existing, tt.subjects); (changed != false) != tt.wantChange {
|
||||
t.Errorf("%q. addSubjects() changed = %v, wantChange = %v", tt.Name, changed, tt.wantChange)
|
||||
}
|
||||
|
||||
want := tt.expected
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%q. addSubjects() failed", tt.Name)
|
||||
t.Errorf("Got: %v", got)
|
||||
t.Errorf("Want: %v", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
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 set
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
clientcmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func TestLocalAndDryRunFlags(t *testing.T) {
|
||||
f := clientcmdutil.NewFactory(genericclioptions.NewTestConfigFlags())
|
||||
setCmd := NewCmdSet(f, genericclioptions.NewTestIOStreamsDiscard())
|
||||
ensureLocalAndDryRunFlagsOnChildren(t, setCmd, "")
|
||||
}
|
||||
|
||||
func ensureLocalAndDryRunFlagsOnChildren(t *testing.T, c *cobra.Command, prefix string) {
|
||||
for _, cmd := range c.Commands() {
|
||||
name := prefix + cmd.Name()
|
||||
if localFlag := cmd.Flag("local"); localFlag == nil {
|
||||
t.Errorf("Command %s does not implement the --local flag", name)
|
||||
}
|
||||
if dryRunFlag := cmd.Flag("dry-run"); dryRunFlag == nil {
|
||||
t.Errorf("Command %s does not implement the --dry-run flag", name)
|
||||
}
|
||||
ensureLocalAndDryRunFlagsOnChildren(t, cmd, name+".")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
/*
|
||||
Copyright 2020 The Kruise Authors.
|
||||
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.
|
||||
|
|
@ -20,43 +17,42 @@ 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/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
"k8s.io/kubectl/pkg/validation"
|
||||
)
|
||||
|
||||
type Factory interface {
|
||||
genericclioptions.RESTClientGetter
|
||||
}
|
||||
|
||||
type factoryImpl struct {
|
||||
clientGetter genericclioptions.RESTClientGetter
|
||||
}
|
||||
|
||||
func (f *factoryImpl) ToRESTConfig() (*rest.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 NewFactory(clientGetter genericclioptions.RESTClientGetter) Factory {
|
||||
if clientGetter == nil {
|
||||
panic("attempt to instantiate client_access_factory with nil clientGetter")
|
||||
}
|
||||
|
||||
f := &factoryImpl{
|
||||
clientGetter: clientGetter,
|
||||
}
|
||||
|
||||
return f
|
||||
|
||||
// DynamicClient returns a dynamic client ready for use
|
||||
DynamicClient() (dynamic.Interface, error)
|
||||
|
||||
// KubernetesClientSet gives you back an external clientset
|
||||
KubernetesClientSet() (*kubernetes.Clientset, error)
|
||||
|
||||
// RESTClient 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
|
||||
|
||||
// ClientForMapping 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)
|
||||
|
||||
// UnstructuredClientForMapping Returns a RESTClient for working with Unstructured objects.
|
||||
UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error)
|
||||
|
||||
// Validator Returns a schema that can validate objects stored on disk.
|
||||
Validator(validate bool) (validation.Schema, error)
|
||||
// OpenAPISchema returns the parsed openapi schema definition
|
||||
OpenAPISchema() (openapi.Resources, error)
|
||||
// OpenAPIGetter returns a getter for the openapi schema document
|
||||
OpenAPIGetter() discovery.OpenAPISchemaInterface
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
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 (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"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"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
type factoryImpl struct {
|
||||
clientGetter genericclioptions.RESTClientGetter
|
||||
}
|
||||
|
||||
func NewFactory(clientGetter genericclioptions.RESTClientGetter) *factoryImpl {
|
||||
if clientGetter == nil {
|
||||
panic("attempt to instantiate client_access_factory with nil clientGetter")
|
||||
}
|
||||
|
||||
f := &factoryImpl{
|
||||
clientGetter: clientGetter,
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = setKubernetesDefaults(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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) 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) OpenAPIGetter() discovery.OpenAPISchemaInterface {
|
||||
discovery, err := f.clientGetter.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
f.getter.Do(func() {
|
||||
f.openAPIGetter = openapi.NewOpenAPIGetter(discovery)
|
||||
})
|
||||
|
||||
return f.openAPIGetter
|
||||
}
|
||||
|
||||
*/
|
||||
|
|
@ -21,6 +21,8 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
|
|
@ -65,3 +67,7 @@ func CheckErr(err error) {
|
|||
}
|
||||
fatal(msg, DefaultErrorExitCode)
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package polymorphichelpers
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
|
|
@ -32,6 +33,9 @@ import (
|
|||
|
||||
func updatePodSpecForObject(obj runtime.Object, fn func(*v1.PodSpec) error) (bool, error) {
|
||||
switch t := obj.(type) {
|
||||
|
||||
case *kruiseappsv1alpha1.CloneSet:
|
||||
return true, fn(&t.Spec.Template.Spec)
|
||||
case *v1.Pod:
|
||||
return true, fn(&t.Spec)
|
||||
// ReplicationController
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// TODO require negotiatedSerializer. leaving it optional lets us plumb current behavior and deal with the difference after major plumbing is complete
|
||||
func (clientConfigFn ClientConfigFunc) clientForGroupVersion(gv schema.GroupVersion, negotiatedSerializer runtime.NegotiatedSerializer) (RESTClient, error) {
|
||||
cfg, err := clientConfigFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if negotiatedSerializer != nil {
|
||||
cfg.ContentConfig.NegotiatedSerializer = negotiatedSerializer
|
||||
}
|
||||
cfg.GroupVersion = &gv
|
||||
if len(gv.Group) == 0 {
|
||||
cfg.APIPath = "/api"
|
||||
} else {
|
||||
cfg.APIPath = "/apis"
|
||||
}
|
||||
|
||||
return rest.RESTClientFor(cfg)
|
||||
}
|
||||
|
||||
func (clientConfigFn ClientConfigFunc) unstructuredClientForGroupVersion(gv schema.GroupVersion) (RESTClient, error) {
|
||||
cfg, err := clientConfigFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.ContentConfig = UnstructuredPlusDefaultContentConfig()
|
||||
cfg.GroupVersion = &gv
|
||||
if len(gv.Group) == 0 {
|
||||
cfg.APIPath = "/api"
|
||||
} else {
|
||||
cfg.APIPath = "/apis"
|
||||
}
|
||||
|
||||
return rest.RESTClientFor(cfg)
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
// CRDGetter is a function that can download the list of GVK for all
|
||||
// CRDs.
|
||||
type CRDGetter func() ([]schema.GroupKind, error)
|
||||
|
||||
func CRDFromDynamic(client dynamic.Interface) CRDGetter {
|
||||
return func() ([]schema.GroupKind, error) {
|
||||
list, err := client.Resource(schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1beta1",
|
||||
Resource: "customresourcedefinitions",
|
||||
}).List(context.TODO(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list CRDs: %v", err)
|
||||
}
|
||||
if list == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gks := []schema.GroupKind{}
|
||||
|
||||
// We need to parse the list to get the gvk, I guess that's fine.
|
||||
for _, crd := range (*list).Items {
|
||||
// Look for group, version, and kind
|
||||
group, _, _ := unstructured.NestedString(crd.Object, "spec", "group")
|
||||
kind, _, _ := unstructured.NestedString(crd.Object, "spec", "names", "kind")
|
||||
|
||||
gks = append(gks, schema.GroupKind{
|
||||
Group: group,
|
||||
Kind: kind,
|
||||
})
|
||||
}
|
||||
|
||||
return gks, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CRDFinder keeps a cache of known CRDs and finds a given GVK in the
|
||||
// list.
|
||||
type CRDFinder interface {
|
||||
HasCRD(gvk schema.GroupKind) (bool, error)
|
||||
}
|
||||
|
||||
func NewCRDFinder(getter CRDGetter) CRDFinder {
|
||||
return &crdFinder{
|
||||
getter: getter,
|
||||
}
|
||||
}
|
||||
|
||||
type crdFinder struct {
|
||||
getter CRDGetter
|
||||
cache *[]schema.GroupKind
|
||||
}
|
||||
|
||||
func (f *crdFinder) cacheCRDs() error {
|
||||
if f.cache != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := f.getter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.cache = &list
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *crdFinder) findCRD(gvk schema.GroupKind) bool {
|
||||
for _, crd := range *f.cache {
|
||||
if reflect.DeepEqual(gvk, crd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *crdFinder) HasCRD(gvk schema.GroupKind) (bool, error) {
|
||||
if err := f.cacheCRDs(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return f.findCRD(gvk), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func TestCacheCRDFinder(t *testing.T) {
|
||||
called := 0
|
||||
getter := func() ([]schema.GroupKind, error) {
|
||||
called += 1
|
||||
return nil, nil
|
||||
}
|
||||
finder := NewCRDFinder(getter)
|
||||
if called != 0 {
|
||||
t.Fatalf("Creating the finder shouldn't call the getter, has called = %v", called)
|
||||
}
|
||||
_, err := finder.HasCRD(schema.GroupKind{Group: "", Kind: "Pod"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to call HasCRD: %v", err)
|
||||
}
|
||||
if called != 1 {
|
||||
t.Fatalf("First call should call the getter, has called = %v", called)
|
||||
}
|
||||
|
||||
_, err = finder.HasCRD(schema.GroupKind{Group: "", Kind: "Pod"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to call HasCRD: %v", err)
|
||||
}
|
||||
if called != 1 {
|
||||
t.Fatalf("Second call should NOT call the getter, has called = %v", called)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRDFinderErrors(t *testing.T) {
|
||||
getter := func() ([]schema.GroupKind, error) {
|
||||
return nil, errors.New("not working")
|
||||
}
|
||||
finder := NewCRDFinder(getter)
|
||||
found, err := finder.HasCRD(schema.GroupKind{Group: "", Kind: "Pod"})
|
||||
if found == true {
|
||||
t.Fatalf("Found the CRD with non-working getter function")
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatalf("Error in getter should be reported")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRDFinder(t *testing.T) {
|
||||
getter := func() ([]schema.GroupKind, error) {
|
||||
return []schema.GroupKind{
|
||||
{
|
||||
Group: "crd.com",
|
||||
Kind: "MyCRD",
|
||||
},
|
||||
{
|
||||
Group: "crd.com",
|
||||
Kind: "MyNewCRD",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
finder := NewCRDFinder(getter)
|
||||
|
||||
if found, _ := finder.HasCRD(schema.GroupKind{Group: "crd.com", Kind: "MyCRD"}); !found {
|
||||
t.Fatalf("Failed to find CRD MyCRD")
|
||||
}
|
||||
if found, _ := finder.HasCRD(schema.GroupKind{Group: "crd.com", Kind: "Random"}); found {
|
||||
t.Fatalf("Found crd Random that doesn't exist")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
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 resource assists clients in dealing with RESTful objects that match the
|
||||
// Kubernetes API conventions. The Helper object provides simple CRUD operations
|
||||
// on resources. The Visitor interface makes it easy to deal with multiple resources
|
||||
// in bulk for retrieval and operation. The Builder object simplifies converting
|
||||
// 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 // import "k8s.io/cli-runtime/pkg/resource"
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
// VerifyDryRun returns nil if a resource group-version-kind supports
|
||||
// server-side dry-run. Otherwise, an error is returned.
|
||||
func VerifyDryRun(gvk schema.GroupVersionKind, dynamicClient dynamic.Interface, discoveryClient discovery.DiscoveryInterface) error {
|
||||
verifier := NewDryRunVerifier(dynamicClient, discoveryClient)
|
||||
return verifier.HasSupport(gvk)
|
||||
}
|
||||
|
||||
func NewDryRunVerifier(dynamicClient dynamic.Interface, discoveryClient discovery.DiscoveryInterface) *DryRunVerifier {
|
||||
return &DryRunVerifier{
|
||||
finder: NewCRDFinder(CRDFromDynamic(dynamicClient)),
|
||||
openAPIGetter: discoveryClient,
|
||||
}
|
||||
}
|
||||
|
||||
func hasGVKExtension(extensions []*openapi_v2.NamedAny, gvk schema.GroupVersionKind) bool {
|
||||
for _, extension := range extensions {
|
||||
if extension.GetValue().GetYaml() == "" ||
|
||||
extension.GetName() != "x-kubernetes-group-version-kind" {
|
||||
continue
|
||||
}
|
||||
var value map[string]string
|
||||
err := yaml.Unmarshal([]byte(extension.GetValue().GetYaml()), &value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if value["group"] == gvk.Group && value["kind"] == gvk.Kind && value["version"] == gvk.Version {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DryRunVerifier verifies if a given group-version-kind supports DryRun
|
||||
// against the current server. Sending dryRun requests to apiserver that
|
||||
// don't support it will result in objects being unwillingly persisted.
|
||||
//
|
||||
// It reads the OpenAPI to see if the given GVK supports dryRun. If the
|
||||
// GVK can not be found, we assume that CRDs will have the same level of
|
||||
// support as "namespaces", and non-CRDs will not be supported. We
|
||||
// delay the check for CRDs as much as possible though, since it
|
||||
// requires an extra round-trip to the server.
|
||||
type DryRunVerifier struct {
|
||||
finder CRDFinder
|
||||
openAPIGetter discovery.OpenAPISchemaInterface
|
||||
}
|
||||
|
||||
// HasSupport verifies if the given gvk supports DryRun. An error is
|
||||
// returned if it doesn't.
|
||||
func (v *DryRunVerifier) HasSupport(gvk schema.GroupVersionKind) error {
|
||||
oapi, err := v.openAPIGetter.OpenAPISchema()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download openapi: %v", err)
|
||||
}
|
||||
supports, err := supportsDryRun(oapi, gvk)
|
||||
if err != nil {
|
||||
// We assume that we couldn't find the type, then check for namespace:
|
||||
supports, _ = supportsDryRun(oapi, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})
|
||||
// If namespace supports dryRun, then we will support dryRun for CRDs only.
|
||||
if supports {
|
||||
supports, err = v.finder.HasCRD(gvk.GroupKind())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check CRD: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !supports {
|
||||
return fmt.Errorf("%v doesn't support dry-run", gvk)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// supportsDryRun is a method that let's us look in the OpenAPI if the
|
||||
// specific group-version-kind supports the dryRun query parameter for
|
||||
// the PATCH end-point.
|
||||
func supportsDryRun(doc *openapi_v2.Document, gvk schema.GroupVersionKind) (bool, error) {
|
||||
for _, path := range doc.GetPaths().GetPath() {
|
||||
// Is this describing the gvk we're looking for?
|
||||
if !hasGVKExtension(path.GetValue().GetPatch().GetVendorExtension(), gvk) {
|
||||
continue
|
||||
}
|
||||
for _, param := range path.GetValue().GetPatch().GetParameters() {
|
||||
if param.GetParameter().GetNonBodyParameter().GetQueryParameterSubSchema().GetName() == "dryRun" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.New("couldn't find GVK in openapi")
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
openapitesting "k8s.io/kube-openapi/pkg/util/proto/testing"
|
||||
)
|
||||
|
||||
func TestSupportsDryRun(t *testing.T) {
|
||||
doc, err := fakeSchema.OpenAPISchema()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get OpenAPI Schema: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
gvk schema.GroupVersionKind
|
||||
success bool
|
||||
supports bool
|
||||
}{
|
||||
{
|
||||
gvk: schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
success: true,
|
||||
supports: true,
|
||||
},
|
||||
{
|
||||
gvk: schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "UnknownKind",
|
||||
},
|
||||
success: false,
|
||||
supports: false,
|
||||
},
|
||||
{
|
||||
gvk: schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "NodeProxyOptions",
|
||||
},
|
||||
success: true,
|
||||
supports: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
supports, err := supportsDryRun(doc, test.gvk)
|
||||
if supports != test.supports || ((err == nil) != test.success) {
|
||||
errStr := "nil"
|
||||
if test.success == false {
|
||||
errStr = "err"
|
||||
}
|
||||
t.Errorf("SupportsDryRun(doc, %v) = (%v, %v), expected (%v, %v)",
|
||||
test.gvk,
|
||||
supports, err,
|
||||
test.supports, errStr,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fakeSchema = openapitesting.Fake{Path: filepath.Join("..", "..", "artifacts", "openapi", "swagger.json")}
|
||||
|
||||
func TestDryRunVerifier(t *testing.T) {
|
||||
dryRunVerifier := DryRunVerifier{
|
||||
finder: NewCRDFinder(func() ([]schema.GroupKind, error) {
|
||||
return []schema.GroupKind{
|
||||
{
|
||||
Group: "crd.com",
|
||||
Kind: "MyCRD",
|
||||
},
|
||||
{
|
||||
Group: "crd.com",
|
||||
Kind: "MyNewCRD",
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
openAPIGetter: &fakeSchema,
|
||||
}
|
||||
|
||||
err := dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "NodeProxyOptions"})
|
||||
if err == nil {
|
||||
t.Fatalf("NodeProxyOptions doesn't support dry-run, yet no error found")
|
||||
}
|
||||
|
||||
err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"})
|
||||
if err != nil {
|
||||
t.Fatalf("Pod should support dry-run: %v", err)
|
||||
}
|
||||
|
||||
err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "MyCRD"})
|
||||
if err != nil {
|
||||
t.Fatalf("MyCRD should support dry-run: %v", err)
|
||||
}
|
||||
|
||||
err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "Random"})
|
||||
if err == nil {
|
||||
t.Fatalf("Random doesn't support dry-run, yet no error found")
|
||||
}
|
||||
}
|
||||
|
||||
type EmptyOpenAPI struct{}
|
||||
|
||||
func (EmptyOpenAPI) OpenAPISchema() (*openapi_v2.Document, error) {
|
||||
return &openapi_v2.Document{}, nil
|
||||
}
|
||||
|
||||
func TestDryRunVerifierNoOpenAPI(t *testing.T) {
|
||||
dryRunVerifier := DryRunVerifier{
|
||||
finder: NewCRDFinder(func() ([]schema.GroupKind, error) {
|
||||
return []schema.GroupKind{
|
||||
{
|
||||
Group: "crd.com",
|
||||
Kind: "MyCRD",
|
||||
},
|
||||
{
|
||||
Group: "crd.com",
|
||||
Kind: "MyNewCRD",
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
openAPIGetter: EmptyOpenAPI{},
|
||||
}
|
||||
|
||||
err := dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"})
|
||||
if err == nil {
|
||||
t.Fatalf("Pod doesn't support dry-run, yet no error found")
|
||||
}
|
||||
|
||||
err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "MyCRD"})
|
||||
if err == nil {
|
||||
t.Fatalf("MyCRD doesn't support dry-run, yet no error found")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/restmapper"
|
||||
)
|
||||
|
||||
// FakeCategoryExpander is for testing only
|
||||
var FakeCategoryExpander restmapper.CategoryExpander = restmapper.SimpleCategoryExpander{
|
||||
Expansions: map[string][]schema.GroupResource{
|
||||
"all": {
|
||||
{Group: "", Resource: "pods"},
|
||||
{Group: "", Resource: "replicationcontrollers"},
|
||||
{Group: "", Resource: "services"},
|
||||
{Group: "apps", Resource: "statefulsets"},
|
||||
{Group: "autoscaling", Resource: "horizontalpodautoscalers"},
|
||||
{Group: "batch", Resource: "jobs"},
|
||||
{Group: "batch", Resource: "cronjobs"},
|
||||
{Group: "extensions", Resource: "daemonsets"},
|
||||
{Group: "extensions", Resource: "deployments"},
|
||||
{Group: "extensions", Resource: "replicasets"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
var metadataAccessor = meta.NewAccessor()
|
||||
|
||||
// Helper provides methods for retrieving or mutating a RESTful
|
||||
// resource.
|
||||
type Helper struct {
|
||||
// The name of this resource as the server would recognize it
|
||||
Resource string
|
||||
// A RESTClient capable of mutating this resource.
|
||||
RESTClient RESTClient
|
||||
// True if the resource type is scoped to namespaces
|
||||
NamespaceScoped bool
|
||||
// If true, then use server-side dry-run to not persist changes to storage
|
||||
// for verbs and resources that support server-side dry-run.
|
||||
//
|
||||
// Note this should only be used against an apiserver with dry-run enabled,
|
||||
// and on resources that support dry-run. If the apiserver or the resource
|
||||
// does not support dry-run, then the change will be persisted to storage.
|
||||
ServerDryRun bool
|
||||
}
|
||||
|
||||
// NewHelper creates a Helper from a ResourceMapping
|
||||
func NewHelper(client RESTClient, mapping *meta.RESTMapping) *Helper {
|
||||
return &Helper{
|
||||
Resource: mapping.Resource.Resource,
|
||||
RESTClient: client,
|
||||
NamespaceScoped: mapping.Scope.Name() == meta.RESTScopeNameNamespace,
|
||||
}
|
||||
}
|
||||
|
||||
// DryRun, if true, will use server-side dry-run to not persist changes to storage.
|
||||
// Otherwise, changes will be persisted to storage.
|
||||
func (m *Helper) DryRun(dryRun bool) *Helper {
|
||||
m.ServerDryRun = dryRun
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Helper) Get(namespace, name string, export bool) (runtime.Object, error) {
|
||||
req := m.RESTClient.Get().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
Name(name)
|
||||
if export {
|
||||
// TODO: I should be part of GetOptions
|
||||
req.Param("export", strconv.FormatBool(export))
|
||||
}
|
||||
return req.Do(context.TODO()).Get()
|
||||
}
|
||||
|
||||
func (m *Helper) List(namespace, apiVersion string, export bool, options *metav1.ListOptions) (runtime.Object, error) {
|
||||
req := m.RESTClient.Get().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
VersionedParams(options, metav1.ParameterCodec)
|
||||
if export {
|
||||
// TODO: I should be part of ListOptions
|
||||
req.Param("export", strconv.FormatBool(export))
|
||||
}
|
||||
return req.Do(context.TODO()).Get()
|
||||
}
|
||||
|
||||
func (m *Helper) Watch(namespace, apiVersion string, options *metav1.ListOptions) (watch.Interface, error) {
|
||||
options.Watch = true
|
||||
return m.RESTClient.Get().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
VersionedParams(options, metav1.ParameterCodec).
|
||||
Watch(context.TODO())
|
||||
}
|
||||
|
||||
func (m *Helper) WatchSingle(namespace, name, resourceVersion string) (watch.Interface, error) {
|
||||
return m.RESTClient.Get().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
VersionedParams(&metav1.ListOptions{
|
||||
ResourceVersion: resourceVersion,
|
||||
Watch: true,
|
||||
FieldSelector: fields.OneTermEqualSelector("metadata.name", name).String(),
|
||||
}, metav1.ParameterCodec).
|
||||
Watch(context.TODO())
|
||||
}
|
||||
|
||||
func (m *Helper) Delete(namespace, name string) (runtime.Object, error) {
|
||||
return m.DeleteWithOptions(namespace, name, nil)
|
||||
}
|
||||
|
||||
func (m *Helper) DeleteWithOptions(namespace, name string, options *metav1.DeleteOptions) (runtime.Object, error) {
|
||||
if options == nil {
|
||||
options = &metav1.DeleteOptions{}
|
||||
}
|
||||
if m.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
|
||||
return m.RESTClient.Delete().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
Name(name).
|
||||
Body(options).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
}
|
||||
|
||||
func (m *Helper) Create(namespace string, modify bool, obj runtime.Object) (runtime.Object, error) {
|
||||
return m.CreateWithOptions(namespace, modify, obj, nil)
|
||||
}
|
||||
|
||||
func (m *Helper) CreateWithOptions(namespace string, modify bool, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
if options == nil {
|
||||
options = &metav1.CreateOptions{}
|
||||
}
|
||||
if m.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
if modify {
|
||||
// Attempt to version the object based on client logic.
|
||||
version, err := metadataAccessor.ResourceVersion(obj)
|
||||
if err != nil {
|
||||
// We don't know how to clear the version on this object, so send it to the server as is
|
||||
return m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
|
||||
}
|
||||
if version != "" {
|
||||
if err := metadataAccessor.SetResourceVersion(obj, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
|
||||
}
|
||||
|
||||
func (m *Helper) createResource(c RESTClient, resource, namespace string, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
return c.Post().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(resource).
|
||||
VersionedParams(options, metav1.ParameterCodec).
|
||||
Body(obj).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
}
|
||||
func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte, options *metav1.PatchOptions) (runtime.Object, error) {
|
||||
if options == nil {
|
||||
options = &metav1.PatchOptions{}
|
||||
}
|
||||
if m.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
return m.RESTClient.Patch(pt). //*rest.Request
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
Name(name).
|
||||
VersionedParams(options, metav1.ParameterCodec).
|
||||
Body(data).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
}
|
||||
|
||||
func (m *Helper) Replace(namespace, name string, overwrite bool, obj runtime.Object) (runtime.Object, error) {
|
||||
c := m.RESTClient
|
||||
var options = &metav1.UpdateOptions{}
|
||||
if m.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
|
||||
// Attempt to version the object based on client logic.
|
||||
version, err := metadataAccessor.ResourceVersion(obj)
|
||||
if err != nil {
|
||||
// We don't know how to version this object, so send it to the server as is
|
||||
return m.replaceResource(c, m.Resource, namespace, name, obj, options)
|
||||
}
|
||||
if version == "" && overwrite {
|
||||
// Retrieve the current version of the object to overwrite the server object
|
||||
serverObj, err := c.Get().NamespaceIfScoped(namespace, m.NamespaceScoped).Resource(m.Resource).Name(name).Do(context.TODO()).Get()
|
||||
if err != nil {
|
||||
// The object does not exist, but we want it to be created
|
||||
return m.replaceResource(c, m.Resource, namespace, name, obj, options)
|
||||
}
|
||||
serverVersion, err := metadataAccessor.ResourceVersion(serverObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := metadataAccessor.SetResourceVersion(obj, serverVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return m.replaceResource(c, m.Resource, namespace, name, obj, options)
|
||||
}
|
||||
|
||||
func (m *Helper) replaceResource(c RESTClient, resource, namespace, name string, obj runtime.Object, options *metav1.UpdateOptions) (runtime.Object, error) {
|
||||
return c.Put().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(resource).
|
||||
Name(name).
|
||||
VersionedParams(options, metav1.ParameterCodec).
|
||||
Body(obj).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
}
|
||||
|
|
@ -0,0 +1,631 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
|
||||
// TODO we need to remove this linkage and create our own scheme
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
func objBody(obj runtime.Object) io.ReadCloser {
|
||||
return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(corev1Codec, obj))))
|
||||
}
|
||||
|
||||
func header() http.Header {
|
||||
header := http.Header{}
|
||||
header.Set("Content-Type", runtime.ContentTypeJSON)
|
||||
return header
|
||||
}
|
||||
|
||||
// splitPath returns the segments for a URL path.
|
||||
func splitPath(path string) []string {
|
||||
path = strings.Trim(path, "/")
|
||||
if path == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(path, "/")
|
||||
}
|
||||
|
||||
// V1DeepEqualSafePodSpec returns a PodSpec which is ready to be used with apiequality.Semantic.DeepEqual
|
||||
func V1DeepEqualSafePodSpec() corev1.PodSpec {
|
||||
grace := int64(30)
|
||||
return corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyAlways,
|
||||
DNSPolicy: corev1.DNSClusterFirst,
|
||||
TerminationGracePeriodSeconds: &grace,
|
||||
SecurityContext: &corev1.PodSecurityContext{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
Err bool
|
||||
Req func(*http.Request) bool
|
||||
Resp *http.Response
|
||||
HttpErr error
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
HttpErr: errors.New("failure"),
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Header: header(),
|
||||
Body: objBody(&metav1.Status{Status: metav1.StatusFailure}),
|
||||
},
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
name: "test3pkg/kubectl/genericclioptions/resource/helper_test.go",
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: header(),
|
||||
Body: objBody(&metav1.Status{Status: metav1.StatusSuccess}),
|
||||
},
|
||||
Req: func(req *http.Request) bool {
|
||||
if req.Method != "DELETE" {
|
||||
t.Errorf("unexpected method: %#v", req)
|
||||
return false
|
||||
}
|
||||
parts := splitPath(req.URL.Path)
|
||||
if len(parts) < 3 {
|
||||
t.Errorf("expected URL path to have 3 parts: %s", req.URL.Path)
|
||||
return false
|
||||
}
|
||||
if parts[1] != "bar" {
|
||||
t.Errorf("url doesn't contain namespace: %#v", req)
|
||||
return false
|
||||
}
|
||||
if parts[2] != "foo" {
|
||||
t.Errorf("url doesn't contain name: %#v", req)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &fake.RESTClient{
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Resp: tt.Resp,
|
||||
Err: tt.HttpErr,
|
||||
}
|
||||
modifier := &Helper{
|
||||
RESTClient: client,
|
||||
NamespaceScoped: true,
|
||||
}
|
||||
_, err := modifier.Delete("bar", "foo")
|
||||
if (err != nil) != tt.Err {
|
||||
t.Errorf("unexpected error: %t %v", tt.Err, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if tt.Req != nil && !tt.Req(client.Req) {
|
||||
t.Errorf("unexpected request: %#v", client.Req)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperCreate(t *testing.T) {
|
||||
expectPost := func(req *http.Request) bool {
|
||||
if req.Method != "POST" {
|
||||
t.Errorf("unexpected method: %#v", req)
|
||||
return false
|
||||
}
|
||||
parts := splitPath(req.URL.Path)
|
||||
if parts[1] != "bar" {
|
||||
t.Errorf("url doesn't contain namespace: %#v", req)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
Resp *http.Response
|
||||
HttpErr error
|
||||
Modify bool
|
||||
Object runtime.Object
|
||||
|
||||
ExpectObject runtime.Object
|
||||
Err bool
|
||||
Req func(*http.Request) bool
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
HttpErr: errors.New("failure"),
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
name: "test1",
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Header: header(),
|
||||
Body: objBody(&metav1.Status{Status: metav1.StatusFailure}),
|
||||
},
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
name: "test1",
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: header(),
|
||||
Body: objBody(&metav1.Status{Status: metav1.StatusSuccess}),
|
||||
},
|
||||
Object: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
||||
ExpectObject: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
||||
Req: expectPost,
|
||||
},
|
||||
{
|
||||
name: "test1",
|
||||
Modify: false,
|
||||
Object: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||
ExpectObject: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||
Resp: &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})},
|
||||
Req: expectPost,
|
||||
},
|
||||
{
|
||||
name: "test1",
|
||||
Modify: true,
|
||||
Object: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"},
|
||||
Spec: V1DeepEqualSafePodSpec(),
|
||||
},
|
||||
ExpectObject: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
||||
Spec: V1DeepEqualSafePodSpec(),
|
||||
},
|
||||
Resp: &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})},
|
||||
Req: expectPost,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &fake.RESTClient{
|
||||
GroupVersion: corev1GV,
|
||||
NegotiatedSerializer: scheme.Codecs,
|
||||
Resp: tt.Resp,
|
||||
Err: tt.HttpErr,
|
||||
}
|
||||
modifier := &Helper{
|
||||
RESTClient: client,
|
||||
NamespaceScoped: true,
|
||||
}
|
||||
_, err := modifier.Create("bar", tt.Modify, tt.Object)
|
||||
if (err != nil) != tt.Err {
|
||||
t.Errorf("%d: unexpected error: %t %v", i, tt.Err, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if tt.Req != nil && !tt.Req(client.Req) {
|
||||
t.Errorf("%d: unexpected request: %#v", i, client.Req)
|
||||
}
|
||||
body, err := ioutil.ReadAll(client.Req.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: unexpected error: %#v", i, err)
|
||||
}
|
||||
t.Logf("got body: %s", string(body))
|
||||
expect := []byte{}
|
||||
if tt.ExpectObject != nil {
|
||||
expect = []byte(runtime.EncodeOrDie(corev1Codec, tt.ExpectObject))
|
||||
}
|
||||
if !reflect.DeepEqual(expect, body) {
|
||||
t.Errorf("%d: unexpected body: %s (expected %s)", i, string(body), string(expect))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperGet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
Err bool
|
||||
Req func(*http.Request) bool
|
||||
Resp *http.Response
|
||||
HttpErr error
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
HttpErr: errors.New("failure"),
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
name: "test1",
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Header: header(),
|
||||
Body: objBody(&metav1.Status{Status: metav1.StatusFailure}),
|
||||
},
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
name: "test1",
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: header(),
|
||||
Body: objBody(&corev1.Pod{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}),
|
||||
},
|
||||
Req: func(req *http.Request) bool {
|
||||
if req.Method != "GET" {
|
||||
t.Errorf("unexpected method: %#v", req)
|
||||
return false
|
||||
}
|
||||
parts := splitPath(req.URL.Path)
|
||||
if parts[1] != "bar" {
|
||||
t.Errorf("url doesn't contain namespace: %#v", req)
|
||||
return false
|
||||
}
|
||||
if parts[2] != "foo" {
|
||||
t.Errorf("url doesn't contain name: %#v", req)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &fake.RESTClient{
|
||||
GroupVersion: corev1GV,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Resp: tt.Resp,
|
||||
Err: tt.HttpErr,
|
||||
}
|
||||
modifier := &Helper{
|
||||
RESTClient: client,
|
||||
NamespaceScoped: true,
|
||||
}
|
||||
obj, err := modifier.Get("bar", "foo", false)
|
||||
|
||||
if (err != nil) != tt.Err {
|
||||
t.Errorf("unexpected error: %d %t %v", i, tt.Err, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if obj.(*corev1.Pod).Name != "foo" {
|
||||
t.Errorf("unexpected object: %#v", obj)
|
||||
}
|
||||
if tt.Req != nil && !tt.Req(client.Req) {
|
||||
t.Errorf("unexpected request: %#v", client.Req)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
Err bool
|
||||
Req func(*http.Request) bool
|
||||
Resp *http.Response
|
||||
HttpErr error
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
HttpErr: errors.New("failure"),
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Header: header(),
|
||||
Body: objBody(&metav1.Status{Status: metav1.StatusFailure}),
|
||||
},
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
name: "test3",
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: header(),
|
||||
Body: objBody(&corev1.PodList{
|
||||
Items: []corev1.Pod{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
Req: func(req *http.Request) bool {
|
||||
if req.Method != "GET" {
|
||||
t.Errorf("unexpected method: %#v", req)
|
||||
return false
|
||||
}
|
||||
if req.URL.Path != "/namespaces/bar" {
|
||||
t.Errorf("url doesn't contain name: %#v", req.URL)
|
||||
return false
|
||||
}
|
||||
if req.URL.Query().Get(metav1.LabelSelectorQueryParam(corev1GV.String())) != labels.SelectorFromSet(labels.Set{"foo": "baz"}).String() {
|
||||
t.Errorf("url doesn't contain query parameters: %#v", req.URL)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &fake.RESTClient{
|
||||
GroupVersion: corev1GV,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Resp: tt.Resp,
|
||||
Err: tt.HttpErr,
|
||||
}
|
||||
modifier := &Helper{
|
||||
RESTClient: client,
|
||||
NamespaceScoped: true,
|
||||
}
|
||||
obj, err := modifier.List("bar", corev1GV.String(), false, &metav1.ListOptions{LabelSelector: "foo=baz"})
|
||||
if (err != nil) != tt.Err {
|
||||
t.Errorf("unexpected error: %t %v", tt.Err, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if obj.(*corev1.PodList).Items[0].Name != "foo" {
|
||||
t.Errorf("unexpected object: %#v", obj)
|
||||
}
|
||||
if tt.Req != nil && !tt.Req(client.Req) {
|
||||
t.Errorf("unexpected request: %#v", client.Req)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperListSelectorCombination(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Err bool
|
||||
ErrMsg string
|
||||
FieldSelector string
|
||||
LabelSelector string
|
||||
}{
|
||||
{
|
||||
Name: "No selector",
|
||||
Err: false,
|
||||
},
|
||||
{
|
||||
Name: "Only Label Selector",
|
||||
Err: false,
|
||||
LabelSelector: "foo=baz",
|
||||
},
|
||||
{
|
||||
Name: "Only Field Selector",
|
||||
Err: false,
|
||||
FieldSelector: "xyz=zyx",
|
||||
},
|
||||
{
|
||||
Name: "Both Label and Field Selector",
|
||||
Err: false,
|
||||
LabelSelector: "foo=baz",
|
||||
FieldSelector: "xyz=zyx",
|
||||
},
|
||||
}
|
||||
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: header(),
|
||||
Body: objBody(&corev1.PodList{
|
||||
Items: []corev1.Pod{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
client := &fake.RESTClient{
|
||||
NegotiatedSerializer: scheme.Codecs,
|
||||
Resp: resp,
|
||||
Err: nil,
|
||||
}
|
||||
modifier := &Helper{
|
||||
RESTClient: client,
|
||||
NamespaceScoped: true,
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
_, err := modifier.List("bar",
|
||||
corev1GV.String(),
|
||||
false,
|
||||
&metav1.ListOptions{LabelSelector: tt.LabelSelector, FieldSelector: tt.FieldSelector})
|
||||
if tt.Err {
|
||||
if err == nil {
|
||||
t.Errorf("%q expected error: %q", tt.Name, tt.ErrMsg)
|
||||
}
|
||||
if err != nil && err.Error() != tt.ErrMsg {
|
||||
t.Errorf("%q expected error: %q", tt.Name, tt.ErrMsg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperReplace(t *testing.T) {
|
||||
expectPut := func(path string, req *http.Request) bool {
|
||||
if req.Method != "PUT" {
|
||||
t.Errorf("unexpected method: %#v", req)
|
||||
return false
|
||||
}
|
||||
if req.URL.Path != path {
|
||||
t.Errorf("unexpected url: %v", req.URL)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
Resp *http.Response
|
||||
HTTPClient *http.Client
|
||||
HttpErr error
|
||||
Overwrite bool
|
||||
Object runtime.Object
|
||||
Namespace string
|
||||
NamespaceScoped bool
|
||||
|
||||
ExpectPath string
|
||||
ExpectObject runtime.Object
|
||||
Err bool
|
||||
Req func(string, *http.Request) bool
|
||||
}{
|
||||
{
|
||||
Name: "test1",
|
||||
Namespace: "bar",
|
||||
NamespaceScoped: true,
|
||||
HttpErr: errors.New("failure"),
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Namespace: "bar",
|
||||
NamespaceScoped: true,
|
||||
Object: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Header: header(),
|
||||
Body: objBody(&metav1.Status{Status: metav1.StatusFailure}),
|
||||
},
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Namespace: "bar",
|
||||
NamespaceScoped: true,
|
||||
Object: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
||||
ExpectPath: "/namespaces/bar/foo",
|
||||
ExpectObject: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
||||
Resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: header(),
|
||||
Body: objBody(&metav1.Status{Status: metav1.StatusSuccess}),
|
||||
},
|
||||
Req: expectPut,
|
||||
},
|
||||
// namespace scoped resource
|
||||
{
|
||||
Name: "test4",
|
||||
Namespace: "bar",
|
||||
NamespaceScoped: true,
|
||||
Object: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
||||
Spec: V1DeepEqualSafePodSpec(),
|
||||
},
|
||||
ExpectPath: "/namespaces/bar/foo",
|
||||
ExpectObject: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"},
|
||||
Spec: V1DeepEqualSafePodSpec(),
|
||||
},
|
||||
Overwrite: true,
|
||||
HTTPClient: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
if req.Method == "PUT" {
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})}, nil
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil
|
||||
}),
|
||||
Req: expectPut,
|
||||
},
|
||||
// cluster scoped resource
|
||||
{
|
||||
Name: "test5",
|
||||
Object: &corev1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
||||
},
|
||||
ExpectObject: &corev1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"},
|
||||
},
|
||||
Overwrite: true,
|
||||
ExpectPath: "/foo",
|
||||
HTTPClient: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
if req.Method == "PUT" {
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})}, nil
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil
|
||||
}),
|
||||
Req: expectPut,
|
||||
},
|
||||
{
|
||||
Name: "test6",
|
||||
Namespace: "bar",
|
||||
NamespaceScoped: true,
|
||||
Object: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||
ExpectPath: "/namespaces/bar/foo",
|
||||
ExpectObject: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||
Resp: &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})},
|
||||
Req: expectPut,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
client := &fake.RESTClient{
|
||||
GroupVersion: corev1GV,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: tt.HTTPClient,
|
||||
Resp: tt.Resp,
|
||||
Err: tt.HttpErr,
|
||||
}
|
||||
modifier := &Helper{
|
||||
RESTClient: client,
|
||||
NamespaceScoped: tt.NamespaceScoped,
|
||||
}
|
||||
_, err := modifier.Replace(tt.Namespace, "foo", tt.Overwrite, tt.Object)
|
||||
if (err != nil) != tt.Err {
|
||||
t.Fatalf("unexpected error: %t %v", tt.Err, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if tt.Req != nil && (client.Req == nil || !tt.Req(tt.ExpectPath, client.Req)) {
|
||||
t.Fatalf("unexpected request: %#v", client.Req)
|
||||
}
|
||||
body, err := ioutil.ReadAll(client.Req.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %#v", err)
|
||||
}
|
||||
expect := []byte{}
|
||||
if tt.ExpectObject != nil {
|
||||
expect = []byte(runtime.EncodeOrDie(corev1Codec, tt.ExpectObject))
|
||||
}
|
||||
if !reflect.DeepEqual(expect, body) {
|
||||
t.Fatalf("unexpected body: %s", string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
)
|
||||
|
||||
type RESTClientGetter interface {
|
||||
ToRESTConfig() (*rest.Config, error)
|
||||
ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
|
||||
ToRESTMapper() (meta.RESTMapper, error)
|
||||
}
|
||||
|
||||
type ClientConfigFunc func() (*rest.Config, error)
|
||||
type RESTMapperFunc func() (meta.RESTMapper, error)
|
||||
type CategoryExpanderFunc func() (restmapper.CategoryExpander, error)
|
||||
|
||||
// RESTClient is a client helper for dealing with RESTful resources
|
||||
// in a generic way.
|
||||
type RESTClient interface {
|
||||
Get() *rest.Request
|
||||
Post() *rest.Request
|
||||
Patch(types.PatchType) *rest.Request
|
||||
Delete() *rest.Request
|
||||
Put() *rest.Request
|
||||
}
|
||||
|
||||
// RequestTransform is a function that is given a chance to modify the outgoing request.
|
||||
type RequestTransform func(*rest.Request)
|
||||
|
||||
// NewClientWithOptions wraps the provided RESTClient and invokes each transform on each
|
||||
// newly created request.
|
||||
func NewClientWithOptions(c RESTClient, transforms ...RequestTransform) RESTClient {
|
||||
if len(transforms) == 0 {
|
||||
return c
|
||||
}
|
||||
return &clientOptions{c: c, transforms: transforms}
|
||||
}
|
||||
|
||||
type clientOptions struct {
|
||||
c RESTClient
|
||||
transforms []RequestTransform
|
||||
}
|
||||
|
||||
func (c *clientOptions) modify(req *rest.Request) *rest.Request {
|
||||
for _, transform := range c.transforms {
|
||||
transform(req)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func (c *clientOptions) Get() *rest.Request {
|
||||
return c.modify(c.c.Get())
|
||||
}
|
||||
|
||||
func (c *clientOptions) Post() *rest.Request {
|
||||
return c.modify(c.c.Post())
|
||||
}
|
||||
func (c *clientOptions) Patch(t types.PatchType) *rest.Request {
|
||||
return c.modify(c.c.Patch(t))
|
||||
}
|
||||
func (c *clientOptions) Delete() *rest.Request {
|
||||
return c.modify(c.c.Delete())
|
||||
}
|
||||
func (c *clientOptions) Put() *rest.Request {
|
||||
return c.modify(c.c.Put())
|
||||
}
|
||||
|
||||
// ContentValidator is an interface that knows how to validate an API object serialized to a byte array.
|
||||
type ContentValidator interface {
|
||||
ValidateBytes(data []byte) error
|
||||
}
|
||||
|
||||
// Visitor lets clients walk a list of resources.
|
||||
type Visitor interface {
|
||||
Visit(VisitorFunc) error
|
||||
}
|
||||
|
||||
// VisitorFunc implements the Visitor interface for a matching function.
|
||||
// If there was a problem walking a list of resources, the incoming error
|
||||
// will describe the problem and the function can decide how to handle that error.
|
||||
// A nil returned indicates to accept an error to continue loops even when errors happen.
|
||||
// This is useful for ignoring certain kinds of errors or aggregating errors in some way.
|
||||
type VisitorFunc func(*Info, error) error
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// Mapper is a convenience struct for holding references to the interfaces
|
||||
// needed to create Info for arbitrary objects.
|
||||
type mapper struct {
|
||||
// localFn indicates the call can't make server requests
|
||||
localFn func() bool
|
||||
|
||||
restMapperFn RESTMapperFunc
|
||||
clientFn func(version schema.GroupVersion) (RESTClient, error)
|
||||
decoder runtime.Decoder
|
||||
}
|
||||
|
||||
// InfoForData creates an Info object for the given data. An error is returned
|
||||
// if any of the decoding or client lookup steps fail. Name and namespace will be
|
||||
// set into Info if the mapping's MetadataAccessor can retrieve them.
|
||||
func (m *mapper) infoForData(data []byte, source string) (*Info, error) {
|
||||
obj, gvk, err := m.decoder.Decode(data, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode %q: %v", source, err)
|
||||
}
|
||||
|
||||
name, _ := metadataAccessor.Name(obj)
|
||||
namespace, _ := metadataAccessor.Namespace(obj)
|
||||
resourceVersion, _ := metadataAccessor.ResourceVersion(obj)
|
||||
|
||||
ret := &Info{
|
||||
Source: source,
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
ResourceVersion: resourceVersion,
|
||||
|
||||
Object: obj,
|
||||
}
|
||||
|
||||
if m.localFn == nil || !m.localFn() {
|
||||
restMapper, err := m.restMapperFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to recognize %q: %v", source, err)
|
||||
}
|
||||
ret.Mapping = mapping
|
||||
|
||||
client, err := m.clientFn(gvk.GroupVersion())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err)
|
||||
}
|
||||
ret.Client = client
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// InfoForObject creates an Info object for the given Object. An error is returned
|
||||
// if the object cannot be introspected. Name and namespace will be set into Info
|
||||
// if the mapping's MetadataAccessor can retrieve them.
|
||||
func (m *mapper) infoForObject(obj runtime.Object, typer runtime.ObjectTyper, preferredGVKs []schema.GroupVersionKind) (*Info, error) {
|
||||
groupVersionKinds, _, err := typer.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get type info from the object %q: %v", reflect.TypeOf(obj), err)
|
||||
}
|
||||
|
||||
gvk := groupVersionKinds[0]
|
||||
if len(groupVersionKinds) > 1 && len(preferredGVKs) > 0 {
|
||||
gvk = preferredObjectKind(groupVersionKinds, preferredGVKs)
|
||||
}
|
||||
|
||||
name, _ := metadataAccessor.Name(obj)
|
||||
namespace, _ := metadataAccessor.Namespace(obj)
|
||||
resourceVersion, _ := metadataAccessor.ResourceVersion(obj)
|
||||
ret := &Info{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
ResourceVersion: resourceVersion,
|
||||
|
||||
Object: obj,
|
||||
}
|
||||
|
||||
if m.localFn == nil || !m.localFn() {
|
||||
restMapper, err := m.restMapperFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to recognize %v", err)
|
||||
}
|
||||
ret.Mapping = mapping
|
||||
|
||||
client, err := m.clientFn(gvk.GroupVersion())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err)
|
||||
}
|
||||
ret.Client = client
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// preferredObjectKind picks the possibility that most closely matches the priority list in this order:
|
||||
// GroupVersionKind matches (exact match)
|
||||
// GroupKind matches
|
||||
// Group matches
|
||||
func preferredObjectKind(possibilities []schema.GroupVersionKind, preferences []schema.GroupVersionKind) schema.GroupVersionKind {
|
||||
// Exact match
|
||||
for _, priority := range preferences {
|
||||
for _, possibility := range possibilities {
|
||||
if possibility == priority {
|
||||
return possibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GroupKind match
|
||||
for _, priority := range preferences {
|
||||
for _, possibility := range possibilities {
|
||||
if possibility.GroupKind() == priority.GroupKind() {
|
||||
return possibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group match
|
||||
for _, priority := range preferences {
|
||||
for _, possibility := range possibilities {
|
||||
if possibility.Group == priority.Group {
|
||||
return possibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Just pick the first
|
||||
return possibilities[0]
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
)
|
||||
|
||||
// hold a single instance of the case-sensitive decoder
|
||||
var caseSensitiveJsonIterator = json.CaseSensitiveJsonIterator()
|
||||
|
||||
// metadataValidatingDecoder wraps a decoder and additionally ensures metadata schema fields decode before returning an unstructured object
|
||||
type metadataValidatingDecoder struct {
|
||||
decoder runtime.Decoder
|
||||
}
|
||||
|
||||
func (m *metadataValidatingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
obj, gvk, err := m.decoder.Decode(data, defaults, into)
|
||||
|
||||
// if we already errored, return
|
||||
if err != nil {
|
||||
return obj, gvk, err
|
||||
}
|
||||
|
||||
// if we're not unstructured, return
|
||||
if _, isUnstructured := obj.(runtime.Unstructured); !isUnstructured {
|
||||
return obj, gvk, err
|
||||
}
|
||||
|
||||
// make sure the data can decode into ObjectMeta before we return,
|
||||
// so we don't silently truncate schema errors in metadata later with accesser get/set calls
|
||||
v := &metadataOnlyObject{}
|
||||
if typedErr := caseSensitiveJsonIterator.Unmarshal(data, v); typedErr != nil {
|
||||
return obj, gvk, typedErr
|
||||
}
|
||||
return obj, gvk, err
|
||||
}
|
||||
|
||||
type metadataOnlyObject struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// ErrMatchFunc can be used to filter errors that may not be true failures.
|
||||
type ErrMatchFunc func(error) bool
|
||||
|
||||
// Result contains helper methods for dealing with the outcome of a Builder.
|
||||
type Result struct {
|
||||
err error
|
||||
visitor Visitor
|
||||
|
||||
sources []Visitor
|
||||
singleItemImplied bool
|
||||
targetsSingleItems bool
|
||||
|
||||
mapper *mapper
|
||||
ignoreErrors []utilerrors.Matcher
|
||||
|
||||
// populated by a call to Infos
|
||||
info []*Info
|
||||
}
|
||||
|
||||
// withError allows a fluent style for internal result code.
|
||||
func (r *Result) withError(err error) *Result {
|
||||
r.err = err
|
||||
return r
|
||||
}
|
||||
|
||||
// TargetsSingleItems returns true if any of the builder arguments pointed
|
||||
// to non-list calls (if the user explicitly asked for any object by name).
|
||||
// This includes directories, streams, URLs, and resource name tuples.
|
||||
func (r *Result) TargetsSingleItems() bool {
|
||||
return r.targetsSingleItems
|
||||
}
|
||||
|
||||
// IgnoreErrors will filter errors that occur when by visiting the result
|
||||
// (but not errors that occur by creating the result in the first place),
|
||||
// eliminating any that match fns. This is best used in combination with
|
||||
// Builder.ContinueOnError(), where the visitors accumulate errors and return
|
||||
// them after visiting as a slice of errors. If no errors remain after
|
||||
// filtering, the various visitor methods on Result will return nil for
|
||||
// err.
|
||||
func (r *Result) IgnoreErrors(fns ...ErrMatchFunc) *Result {
|
||||
for _, fn := range fns {
|
||||
r.ignoreErrors = append(r.ignoreErrors, utilerrors.Matcher(fn))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Mapper returns a copy of the builder's mapper.
|
||||
func (r *Result) Mapper() *mapper {
|
||||
return r.mapper
|
||||
}
|
||||
|
||||
// Err returns one or more errors (via a util.ErrorList) that occurred prior
|
||||
// to visiting the elements in the visitor. To see all errors including those
|
||||
// that occur during visitation, invoke Infos().
|
||||
func (r *Result) Err() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
// Visit implements the Visitor interface on the items described in the Builder.
|
||||
// Note that some visitor sources are not traversable more than once, or may
|
||||
// return different results. If you wish to operate on the same set of resources
|
||||
// multiple times, use the Infos() method.
|
||||
func (r *Result) Visit(fn VisitorFunc) error {
|
||||
if r.err != nil {
|
||||
return r.err
|
||||
}
|
||||
err := r.visitor.Visit(fn)
|
||||
return utilerrors.FilterOut(err, r.ignoreErrors...)
|
||||
}
|
||||
|
||||
// IntoSingleItemImplied sets the provided boolean pointer to true if the Builder input
|
||||
// implies a single item, or multiple.
|
||||
func (r *Result) IntoSingleItemImplied(b *bool) *Result {
|
||||
*b = r.singleItemImplied
|
||||
return r
|
||||
}
|
||||
|
||||
// Infos returns an array of all of the resource infos retrieved via traversal.
|
||||
// Will attempt to traverse the entire set of visitors only once, and will return
|
||||
// a cached list on subsequent calls.
|
||||
func (r *Result) Infos() ([]*Info, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.info != nil {
|
||||
return r.info, nil
|
||||
}
|
||||
|
||||
infos := []*Info{}
|
||||
err := r.visitor.Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
infos = append(infos, info)
|
||||
return nil
|
||||
})
|
||||
err = utilerrors.FilterOut(err, r.ignoreErrors...)
|
||||
|
||||
r.info, r.err = infos, err
|
||||
return infos, err
|
||||
}
|
||||
|
||||
// Object returns a single object representing the output of a single visit to all
|
||||
// found resources. If the Builder was a singular context (expected to return a
|
||||
// single resource by user input) and only a single resource was found, the resource
|
||||
// will be returned as is. Otherwise, the returned resources will be part of an
|
||||
// v1.List. The ResourceVersion of the v1.List will be set only if it is identical
|
||||
// across all infos returned.
|
||||
func (r *Result) Object() (runtime.Object, error) {
|
||||
infos, err := r.Infos()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versions := sets.String{}
|
||||
objects := []runtime.Object{}
|
||||
for _, info := range infos {
|
||||
if info.Object != nil {
|
||||
objects = append(objects, info.Object)
|
||||
versions.Insert(info.ResourceVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if len(objects) == 1 {
|
||||
if r.singleItemImplied {
|
||||
return objects[0], nil
|
||||
}
|
||||
// if the item is a list already, don't create another list
|
||||
if meta.IsListType(objects[0]) {
|
||||
return objects[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
version := ""
|
||||
if len(versions) == 1 {
|
||||
version = versions.List()[0]
|
||||
}
|
||||
|
||||
return toV1List(objects, version), err
|
||||
}
|
||||
|
||||
// Compile time check to enforce that list implements the necessary interface
|
||||
var _ metav1.ListInterface = &v1.List{}
|
||||
var _ metav1.ListMetaAccessor = &v1.List{}
|
||||
|
||||
// toV1List takes a slice of Objects + their version, and returns
|
||||
// a v1.List Object containing the objects in the Items field
|
||||
func toV1List(objects []runtime.Object, version string) runtime.Object {
|
||||
raw := []runtime.RawExtension{}
|
||||
for _, o := range objects {
|
||||
raw = append(raw, runtime.RawExtension{Object: o})
|
||||
}
|
||||
return &v1.List{
|
||||
ListMeta: metav1.ListMeta{
|
||||
ResourceVersion: version,
|
||||
},
|
||||
Items: raw,
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceMapping returns a single meta.RESTMapping representing the
|
||||
// resources located by the builder, or an error if more than one
|
||||
// mapping was found.
|
||||
func (r *Result) ResourceMapping() (*meta.RESTMapping, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
mappings := map[schema.GroupVersionResource]*meta.RESTMapping{}
|
||||
for i := range r.sources {
|
||||
m, ok := r.sources[i].(ResourceMapping)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("a resource mapping could not be loaded from %v", reflect.TypeOf(r.sources[i]))
|
||||
}
|
||||
mapping := m.ResourceMapping()
|
||||
mappings[mapping.Resource] = mapping
|
||||
}
|
||||
if len(mappings) != 1 {
|
||||
return nil, fmt.Errorf("expected only a single resource type")
|
||||
}
|
||||
for _, mapping := range mappings {
|
||||
return mapping, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Watch retrieves changes that occur on the server to the specified resource.
|
||||
// It currently supports watching a single source - if the resource source
|
||||
// (selectors or pure types) can be watched, they will be, otherwise the list
|
||||
// will be visited (equivalent to the Infos() call) and if there is a single
|
||||
// resource present, it will be watched, otherwise an error will be returned.
|
||||
func (r *Result) Watch(resourceVersion string) (watch.Interface, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if len(r.sources) != 1 {
|
||||
return nil, fmt.Errorf("you may only watch a single resource or type of resource at a time")
|
||||
}
|
||||
w, ok := r.sources[0].(Watchable)
|
||||
if !ok {
|
||||
info, err := r.Infos()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(info) != 1 {
|
||||
return nil, fmt.Errorf("watch is only supported on individual resources and resource collections - %d resources were found", len(info))
|
||||
}
|
||||
return info[0].Watch(resourceVersion)
|
||||
}
|
||||
return w.Watch(resourceVersion)
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// dynamicCodec is a codec that wraps the standard unstructured codec
|
||||
// with special handling for Status objects.
|
||||
// Deprecated only used by test code and its wrong
|
||||
type dynamicCodec struct{}
|
||||
|
||||
func (dynamicCodec) Decode(data []byte, gvk *schema.GroupVersionKind, obj runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
obj, gvk, err := unstructured.UnstructuredJSONScheme.Decode(data, gvk, obj)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if strings.ToLower(gvk.Kind) == "status" && gvk.Version == "v1" && (gvk.Group == "" || gvk.Group == "meta.k8s.io") {
|
||||
if _, ok := obj.(*metav1.Status); !ok {
|
||||
obj = &metav1.Status{}
|
||||
err := json.Unmarshal(data, obj)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj, gvk, nil
|
||||
}
|
||||
|
||||
func (dynamicCodec) Encode(obj runtime.Object, w io.Writer) error {
|
||||
// There is no need to handle runtime.CacheableObject, as we only
|
||||
// fallback to other encoders here.
|
||||
return unstructured.UnstructuredJSONScheme.Encode(obj, w)
|
||||
}
|
||||
|
||||
// Identifier implements runtime.Encoder interface.
|
||||
func (dynamicCodec) Identifier() runtime.Identifier {
|
||||
return unstructured.UnstructuredJSONScheme.Identifier()
|
||||
}
|
||||
|
||||
// UnstructuredPlusDefaultContentConfig returns a rest.ContentConfig for dynamic types. It includes enough codecs to act as a "normal"
|
||||
// serializer for the rest.client with options, status and the like.
|
||||
func UnstructuredPlusDefaultContentConfig() rest.ContentConfig {
|
||||
// TODO: scheme.Codecs here should become "pkg/apis/server/scheme" which is the minimal core you need
|
||||
// to talk to a kubernetes server
|
||||
jsonInfo, _ := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON)
|
||||
|
||||
jsonInfo.Serializer = dynamicCodec{}
|
||||
jsonInfo.PrettySerializer = nil
|
||||
return rest.ContentConfig{
|
||||
AcceptContentTypes: runtime.ContentTypeJSON,
|
||||
ContentType: runtime.ContentTypeJSON,
|
||||
NegotiatedSerializer: serializer.NegotiatedSerializerWrapper(jsonInfo),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func TestDynamicCodecDecode(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
gvk *schema.GroupVersionKind
|
||||
obj runtime.Object
|
||||
|
||||
expectErr string
|
||||
expectGVK *schema.GroupVersionKind
|
||||
expectObj runtime.Object
|
||||
}{
|
||||
{
|
||||
name: "v1 Status",
|
||||
data: []byte(`{"apiVersion":"v1","kind":"Status"}`),
|
||||
expectGVK: &schema.GroupVersionKind{"", "v1", "Status"},
|
||||
expectObj: &metav1.Status{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Status"}},
|
||||
},
|
||||
{
|
||||
name: "meta.k8s.io/v1 Status",
|
||||
data: []byte(`{"apiVersion":"meta.k8s.io/v1","kind":"Status"}`),
|
||||
expectGVK: &schema.GroupVersionKind{"meta.k8s.io", "v1", "Status"},
|
||||
expectObj: &metav1.Status{TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1", Kind: "Status"}},
|
||||
},
|
||||
{
|
||||
name: "example.com/v1 Status",
|
||||
data: []byte(`{"apiVersion":"example.com/v1","kind":"Status"}`),
|
||||
expectGVK: &schema.GroupVersionKind{"example.com", "v1", "Status"},
|
||||
expectObj: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "example.com/v1", "kind": "Status"}},
|
||||
},
|
||||
{
|
||||
name: "example.com/v1 Foo",
|
||||
data: []byte(`{"apiVersion":"example.com/v1","kind":"Foo"}`),
|
||||
expectGVK: &schema.GroupVersionKind{"example.com", "v1", "Foo"},
|
||||
expectObj: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "example.com/v1", "kind": "Foo"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
obj, gvk, err := dynamicCodec{}.Decode(test.data, test.gvk, test.obj)
|
||||
if (err == nil) != (test.expectErr == "") {
|
||||
t.Fatalf("expected err=%v, got %v", test.expectErr, err)
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), test.expectErr) {
|
||||
t.Fatalf("expected err=%v, got %v", test.expectErr, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(test.expectGVK, gvk) {
|
||||
t.Errorf("expected\n\tgvk=%#v\ngot\n\t%#v", test.expectGVK, gvk)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expectObj, obj) {
|
||||
t.Errorf("expected\n\t%#v\n\t%#v", test.expectObj, obj)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// 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
|
||||
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 bool, limitChunks int64) *Selector {
|
||||
return &Selector{
|
||||
Client: client,
|
||||
Mapping: mapping,
|
||||
Namespace: namespace,
|
||||
LabelSelector: labelSelector,
|
||||
FieldSelector: fieldSelector,
|
||||
Export: export,
|
||||
LimitChunks: limitChunks,
|
||||
}
|
||||
}
|
||||
|
||||
// Visit implements Visitor and uses request chunking by default.
|
||||
func (r *Selector) Visit(fn VisitorFunc) error {
|
||||
var continueToken string
|
||||
for {
|
||||
list, err := NewHelper(r.Client, r.Mapping).List(
|
||||
r.Namespace,
|
||||
r.ResourceMapping().GroupVersionKind.GroupVersion().String(),
|
||||
r.Export,
|
||||
&metav1.ListOptions{
|
||||
LabelSelector: r.LabelSelector,
|
||||
FieldSelector: r.FieldSelector,
|
||||
Limit: r.LimitChunks,
|
||||
Continue: continueToken,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.IsResourceExpired(err) {
|
||||
return err
|
||||
}
|
||||
if errors.IsBadRequest(err) || errors.IsNotFound(err) {
|
||||
if se, ok := err.(*errors.StatusError); ok {
|
||||
// modify the message without hiding this is an API error
|
||||
if len(r.LabelSelector) == 0 && len(r.FieldSelector) == 0 {
|
||||
se.ErrStatus.Message = fmt.Sprintf("Unable to list %q: %v", r.Mapping.Resource, se.ErrStatus.Message)
|
||||
} else {
|
||||
se.ErrStatus.Message = fmt.Sprintf("Unable to find %q that match label selector %q, field selector %q: %v", r.Mapping.Resource, r.LabelSelector, r.FieldSelector, se.ErrStatus.Message)
|
||||
}
|
||||
return se
|
||||
}
|
||||
if len(r.LabelSelector) == 0 && len(r.FieldSelector) == 0 {
|
||||
return fmt.Errorf("Unable to list %q: %v", r.Mapping.Resource, err)
|
||||
}
|
||||
return fmt.Errorf("Unable to find %q that match label selector %q, field selector %q: %v", r.Mapping.Resource, r.LabelSelector, r.FieldSelector, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
resourceVersion, _ := metadataAccessor.ResourceVersion(list)
|
||||
nextContinueToken, _ := metadataAccessor.Continue(list)
|
||||
info := &Info{
|
||||
Client: r.Client,
|
||||
Mapping: r.Mapping,
|
||||
|
||||
Namespace: r.Namespace,
|
||||
ResourceVersion: resourceVersion,
|
||||
|
||||
Object: list,
|
||||
}
|
||||
|
||||
if err := fn(info, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(nextContinueToken) == 0 {
|
||||
return nil
|
||||
}
|
||||
continueToken = nextContinueToken
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Selector) Watch(resourceVersion string) (watch.Interface, error) {
|
||||
return NewHelper(r.Client, r.Mapping).Watch(r.Namespace, r.ResourceMapping().GroupVersionKind.GroupVersion().String(),
|
||||
&metav1.ListOptions{ResourceVersion: resourceVersion, LabelSelector: r.LabelSelector, FieldSelector: r.FieldSelector})
|
||||
}
|
||||
|
||||
// ResourceMapping returns the mapping for this resource and implements ResourceMapping
|
||||
func (r *Selector) ResourceMapping() *meta.RESTMapping {
|
||||
return r.Mapping
|
||||
}
|
||||
|
|
@ -0,0 +1,764 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"sigs.k8s.io/kustomize/pkg/fs"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/cli-runtime/pkg/kustomize"
|
||||
)
|
||||
|
||||
const (
|
||||
constSTDINstr = "STDIN"
|
||||
stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false"
|
||||
)
|
||||
|
||||
// Watchable describes a resource that can be watched for changes that occur on the server,
|
||||
// beginning after the provided resource version.
|
||||
type Watchable interface {
|
||||
Watch(resourceVersion string) (watch.Interface, error)
|
||||
}
|
||||
|
||||
// ResourceMapping allows an object to return the resource mapping associated with
|
||||
// the resource or resources it represents.
|
||||
type ResourceMapping interface {
|
||||
ResourceMapping() *meta.RESTMapping
|
||||
}
|
||||
|
||||
// Info contains temporary info to execute a REST call, or show the results
|
||||
// of an already completed REST call.
|
||||
type Info struct {
|
||||
// Client will only be present if this builder was not local
|
||||
Client RESTClient
|
||||
// Mapping will only be present if this builder was not local
|
||||
Mapping *meta.RESTMapping
|
||||
|
||||
// Namespace will be set if the object is namespaced and has a specified value.
|
||||
Namespace string
|
||||
Name string
|
||||
|
||||
// Optional, Source is the filename or URL to template file (.json or .yaml),
|
||||
// or stdin to use to handle the resource
|
||||
Source string
|
||||
// Optional, this is the most recent value returned by the server if available. It will
|
||||
// typically be in unstructured or internal forms, depending on how the Builder was
|
||||
// defined. If retrieved from the server, the Builder expects the mapping client to
|
||||
// decide the final form. Use the AsVersioned, AsUnstructured, and AsInternal helpers
|
||||
// to alter the object versions.
|
||||
Object runtime.Object
|
||||
// Optional, this is the most recent resource version the server knows about for
|
||||
// this type of resource. It may not match the resource version of the object,
|
||||
// but if set it should be equal to or newer than the resource version of the
|
||||
// object (however the server defines resource version).
|
||||
ResourceVersion string
|
||||
// Optional, should this resource be exported, stripped of cluster-specific and instance specific fields
|
||||
Export bool
|
||||
}
|
||||
|
||||
// Visit implements Visitor
|
||||
func (i *Info) Visit(fn VisitorFunc) error {
|
||||
return fn(i, nil)
|
||||
}
|
||||
|
||||
// Get retrieves the object from the Namespace and Name fields
|
||||
func (i *Info) Get() (err error) {
|
||||
obj, err := NewHelper(i.Client, i.Mapping).Get(i.Namespace, i.Name, i.Export)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) && len(i.Namespace) > 0 && i.Namespace != metav1.NamespaceDefault && i.Namespace != metav1.NamespaceAll {
|
||||
err2 := i.Client.Get().AbsPath("api", "v1", "namespaces", i.Namespace).Do(context.TODO()).Error()
|
||||
if err2 != nil && errors.IsNotFound(err2) {
|
||||
return err2
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
i.Object = obj
|
||||
i.ResourceVersion, _ = metadataAccessor.ResourceVersion(obj)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh updates the object with another object. If ignoreError is set
|
||||
// the Object will be updated even if name, namespace, or resourceVersion
|
||||
// attributes cannot be loaded from the object.
|
||||
func (i *Info) Refresh(obj runtime.Object, ignoreError bool) error {
|
||||
name, err := metadataAccessor.Name(obj)
|
||||
if err != nil {
|
||||
if !ignoreError {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
i.Name = name
|
||||
}
|
||||
namespace, err := metadataAccessor.Namespace(obj)
|
||||
if err != nil {
|
||||
if !ignoreError {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
i.Namespace = namespace
|
||||
}
|
||||
version, err := metadataAccessor.ResourceVersion(obj)
|
||||
if err != nil {
|
||||
if !ignoreError {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
i.ResourceVersion = version
|
||||
}
|
||||
i.Object = obj
|
||||
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", i.Name, i.Namespace)
|
||||
if i.Mapping != nil {
|
||||
mappingInfo := fmt.Sprintf("Resource: %q, GroupVersionKind: %q", i.Mapping.Resource.String(),
|
||||
i.Mapping.GroupVersionKind.String())
|
||||
return fmt.Sprint(mappingInfo, "\n", basicInfo)
|
||||
}
|
||||
return basicInfo
|
||||
}
|
||||
|
||||
// Namespaced returns true if the object belongs to a namespace
|
||||
func (i *Info) Namespaced() bool {
|
||||
if i.Mapping != nil {
|
||||
// if we have RESTMapper info, use it
|
||||
return i.Mapping.Scope.Name() == meta.RESTScopeNameNamespace
|
||||
}
|
||||
// otherwise, use the presence of a namespace in the info as an indicator
|
||||
return len(i.Namespace) > 0
|
||||
}
|
||||
|
||||
// Watch returns server changes to this object after it was retrieved.
|
||||
func (i *Info) Watch(resourceVersion string) (watch.Interface, error) {
|
||||
return NewHelper(i.Client, i.Mapping).WatchSingle(i.Namespace, i.Name, resourceVersion)
|
||||
}
|
||||
|
||||
// ResourceMapping returns the mapping for this resource and implements ResourceMapping
|
||||
func (i *Info) ResourceMapping() *meta.RESTMapping {
|
||||
return i.Mapping
|
||||
}
|
||||
|
||||
// VisitorList implements Visit for the sub visitors it contains. The first error
|
||||
// returned from a child Visitor will terminate iteration.
|
||||
type VisitorList []Visitor
|
||||
|
||||
// Visit implements Visitor
|
||||
func (l VisitorList) Visit(fn VisitorFunc) error {
|
||||
for i := range l {
|
||||
if err := l[i].Visit(fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EagerVisitorList implements Visit for the sub visitors it contains. All errors
|
||||
// will be captured and returned at the end of iteration.
|
||||
type EagerVisitorList []Visitor
|
||||
|
||||
// Visit implements Visitor, and gathers errors that occur during processing until
|
||||
// all sub visitors have been visited.
|
||||
func (l EagerVisitorList) Visit(fn VisitorFunc) error {
|
||||
errs := []error(nil)
|
||||
for i := range l {
|
||||
if err := l[i].Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return nil
|
||||
}
|
||||
if err := fn(info, nil); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
func ValidateSchema(data []byte, schema ContentValidator) error {
|
||||
if schema == nil {
|
||||
return nil
|
||||
}
|
||||
if err := schema.ValidateBytes(data); err != nil {
|
||||
return fmt.Errorf("error validating data: %v; %s", err, stopValidateMessage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// URLVisitor downloads the contents of a URL, and if successful, returns
|
||||
// an info object representing the downloaded object.
|
||||
type URLVisitor struct {
|
||||
URL *url.URL
|
||||
*StreamVisitor
|
||||
HttpAttemptCount int
|
||||
}
|
||||
|
||||
func (v *URLVisitor) Visit(fn VisitorFunc) error {
|
||||
body, err := readHttpWithRetries(httpgetImpl, time.Second, v.URL.String(), v.HttpAttemptCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer body.Close()
|
||||
v.StreamVisitor.Reader = body
|
||||
return v.StreamVisitor.Visit(fn)
|
||||
}
|
||||
|
||||
// readHttpWithRetries tries to http.Get the v.URL retries times before giving up.
|
||||
func readHttpWithRetries(get httpget, duration time.Duration, u string, attempts int) (io.ReadCloser, error) {
|
||||
var err error
|
||||
var body io.ReadCloser
|
||||
if attempts <= 0 {
|
||||
return nil, fmt.Errorf("http attempts must be greater than 0, was %d", attempts)
|
||||
}
|
||||
for i := 0; i < attempts; i++ {
|
||||
var statusCode int
|
||||
var status string
|
||||
if i > 0 {
|
||||
time.Sleep(duration)
|
||||
}
|
||||
|
||||
// Try to get the URL
|
||||
statusCode, status, body, err = get(u)
|
||||
|
||||
// Retry Errors
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Error - Set the error condition from the StatusCode
|
||||
if statusCode != http.StatusOK {
|
||||
err = fmt.Errorf("unable to read URL %q, server reported %s, status code=%d", u, status, statusCode)
|
||||
}
|
||||
|
||||
if statusCode >= 500 && statusCode < 600 {
|
||||
// Retry 500's
|
||||
continue
|
||||
} else {
|
||||
// Don't retry other StatusCodes
|
||||
break
|
||||
}
|
||||
}
|
||||
return body, err
|
||||
}
|
||||
|
||||
// httpget Defines function to retrieve a url and return the results. Exists for unit test stubbing.
|
||||
type httpget func(url string) (int, string, io.ReadCloser, error)
|
||||
|
||||
// httpgetImpl Implements a function to retrieve a url and return the results.
|
||||
func httpgetImpl(url string) (int, string, io.ReadCloser, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return 0, "", nil, err
|
||||
}
|
||||
return resp.StatusCode, resp.Status, resp.Body, nil
|
||||
}
|
||||
|
||||
// DecoratedVisitor will invoke the decorators in order prior to invoking the visitor function
|
||||
// passed to Visit. An error will terminate the visit.
|
||||
type DecoratedVisitor struct {
|
||||
visitor Visitor
|
||||
decorators []VisitorFunc
|
||||
}
|
||||
|
||||
// NewDecoratedVisitor will create a visitor that invokes the provided visitor functions before
|
||||
// the user supplied visitor function is invoked, giving them the opportunity to mutate the Info
|
||||
// object or terminate early with an error.
|
||||
func NewDecoratedVisitor(v Visitor, fn ...VisitorFunc) Visitor {
|
||||
if len(fn) == 0 {
|
||||
return v
|
||||
}
|
||||
return DecoratedVisitor{v, fn}
|
||||
}
|
||||
|
||||
// Visit implements Visitor
|
||||
func (v DecoratedVisitor) Visit(fn VisitorFunc) error {
|
||||
return v.visitor.Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range v.decorators {
|
||||
if err := v.decorators[i](info, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fn(info, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// ContinueOnErrorVisitor visits each item and, if an error occurs on
|
||||
// any individual item, returns an aggregate error after all items
|
||||
// are visited.
|
||||
type ContinueOnErrorVisitor struct {
|
||||
Visitor
|
||||
}
|
||||
|
||||
// Visit returns nil if no error occurs during traversal, a regular
|
||||
// error if one occurs, or if multiple errors occur, an aggregate
|
||||
// error. If the provided visitor fails on any individual item it
|
||||
// will not prevent the remaining items from being visited. An error
|
||||
// returned by the visitor directly may still result in some items
|
||||
// not being visited.
|
||||
func (v ContinueOnErrorVisitor) Visit(fn VisitorFunc) error {
|
||||
errs := []error{}
|
||||
err := v.Visitor.Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return nil
|
||||
}
|
||||
if err := fn(info, nil); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(errs) == 1 {
|
||||
return errs[0]
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
// FlattenListVisitor flattens any objects that runtime.ExtractList recognizes as a list
|
||||
// - has an "Items" public field that is a slice of runtime.Objects or objects satisfying
|
||||
// that interface - into multiple Infos. Returns nil in the case of no errors.
|
||||
// When an error is hit on sub items (for instance, if a List contains an object that does
|
||||
// not have a registered client or resource), returns an aggregate error.
|
||||
type FlattenListVisitor struct {
|
||||
visitor Visitor
|
||||
typer runtime.ObjectTyper
|
||||
mapper *mapper
|
||||
}
|
||||
|
||||
// NewFlattenListVisitor creates a visitor that will expand list style runtime.Objects
|
||||
// into individual items and then visit them individually.
|
||||
func NewFlattenListVisitor(v Visitor, typer runtime.ObjectTyper, mapper *mapper) Visitor {
|
||||
return FlattenListVisitor{v, typer, mapper}
|
||||
}
|
||||
|
||||
func (v FlattenListVisitor) Visit(fn VisitorFunc) error {
|
||||
return v.visitor.Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Object == nil {
|
||||
return fn(info, nil)
|
||||
}
|
||||
if !meta.IsListType(info.Object) {
|
||||
return fn(info, nil)
|
||||
}
|
||||
|
||||
items := []runtime.Object{}
|
||||
itemsToProcess := []runtime.Object{info.Object}
|
||||
|
||||
for i := 0; i < len(itemsToProcess); i++ {
|
||||
currObj := itemsToProcess[i]
|
||||
if !meta.IsListType(currObj) {
|
||||
items = append(items, currObj)
|
||||
continue
|
||||
}
|
||||
|
||||
currItems, err := meta.ExtractList(currObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if errs := runtime.DecodeList(currItems, v.mapper.decoder); len(errs) > 0 {
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
itemsToProcess = append(itemsToProcess, currItems...)
|
||||
}
|
||||
|
||||
// If we have a GroupVersionKind on the list, prioritize that when asking for info on the objects contained in the list
|
||||
var preferredGVKs []schema.GroupVersionKind
|
||||
if info.Mapping != nil && !info.Mapping.GroupVersionKind.Empty() {
|
||||
preferredGVKs = append(preferredGVKs, info.Mapping.GroupVersionKind)
|
||||
}
|
||||
errs := []error{}
|
||||
for i := range items {
|
||||
item, err := v.mapper.infoForObject(items[i], v.typer, preferredGVKs)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
if len(info.ResourceVersion) != 0 {
|
||||
item.ResourceVersion = info.ResourceVersion
|
||||
}
|
||||
if err := fn(item, nil); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func ignoreFile(path string, extensions []string) bool {
|
||||
if len(extensions) == 0 {
|
||||
return false
|
||||
}
|
||||
ext := filepath.Ext(path)
|
||||
for _, s := range extensions {
|
||||
if s == ext {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// FileVisitorForSTDIN return a special FileVisitor just for STDIN
|
||||
func FileVisitorForSTDIN(mapper *mapper, schema ContentValidator) Visitor {
|
||||
return &FileVisitor{
|
||||
Path: constSTDINstr,
|
||||
StreamVisitor: NewStreamVisitor(nil, mapper, constSTDINstr, schema),
|
||||
}
|
||||
}
|
||||
|
||||
// ExpandPathsToFileVisitors will return a slice of FileVisitors that will handle files from the provided path.
|
||||
// After FileVisitors open the files, they will pass an io.Reader to a StreamVisitor to do the reading. (stdin
|
||||
// is also taken care of). Paths argument also accepts a single file, and will return a single visitor
|
||||
func ExpandPathsToFileVisitors(mapper *mapper, paths string, recursive bool, extensions []string, schema ContentValidator) ([]Visitor, error) {
|
||||
var visitors []Visitor
|
||||
err := filepath.Walk(paths, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
if path != paths && !recursive {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Don't check extension if the filepath was passed explicitly
|
||||
if path != paths && ignoreFile(path, extensions) {
|
||||
return nil
|
||||
}
|
||||
|
||||
visitor := &FileVisitor{
|
||||
Path: path,
|
||||
StreamVisitor: NewStreamVisitor(nil, mapper, path, schema),
|
||||
}
|
||||
|
||||
visitors = append(visitors, visitor)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return visitors, nil
|
||||
}
|
||||
|
||||
// FileVisitor is wrapping around a StreamVisitor, to handle open/close files
|
||||
type FileVisitor struct {
|
||||
Path string
|
||||
*StreamVisitor
|
||||
}
|
||||
|
||||
// Visit in a FileVisitor is just taking care of opening/closing files
|
||||
func (v *FileVisitor) Visit(fn VisitorFunc) error {
|
||||
var f *os.File
|
||||
if v.Path == constSTDINstr {
|
||||
f = os.Stdin
|
||||
} else {
|
||||
var err error
|
||||
f, err = os.Open(v.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
// TODO: Consider adding a flag to force to UTF16, apparently some
|
||||
// Windows tools don't write the BOM
|
||||
utf16bom := unicode.BOMOverride(unicode.UTF8.NewDecoder())
|
||||
v.StreamVisitor.Reader = transform.NewReader(f, utf16bom)
|
||||
|
||||
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
|
||||
// a stream decoder method on runtime.Codec to properly handle this.
|
||||
type StreamVisitor struct {
|
||||
io.Reader
|
||||
*mapper
|
||||
|
||||
Source string
|
||||
Schema ContentValidator
|
||||
}
|
||||
|
||||
// NewStreamVisitor is a helper function that is useful when we want to change the fields of the struct but keep calls the same.
|
||||
func NewStreamVisitor(r io.Reader, mapper *mapper, source string, schema ContentValidator) *StreamVisitor {
|
||||
return &StreamVisitor{
|
||||
Reader: r,
|
||||
mapper: mapper,
|
||||
Source: source,
|
||||
Schema: schema,
|
||||
}
|
||||
}
|
||||
|
||||
// Visit implements Visitor over a stream. StreamVisitor is able to distinct multiple resources in one stream.
|
||||
func (v *StreamVisitor) Visit(fn VisitorFunc) error {
|
||||
d := yaml.NewYAMLOrJSONDecoder(v.Reader, 4096)
|
||||
for {
|
||||
ext := runtime.RawExtension{}
|
||||
if err := d.Decode(&ext); err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("error parsing %s: %v", v.Source, err)
|
||||
}
|
||||
// TODO: This needs to be able to handle object in other encodings and schemas.
|
||||
ext.Raw = bytes.TrimSpace(ext.Raw)
|
||||
if len(ext.Raw) == 0 || bytes.Equal(ext.Raw, []byte("null")) {
|
||||
continue
|
||||
}
|
||||
if err := ValidateSchema(ext.Raw, v.Schema); err != nil {
|
||||
return fmt.Errorf("error validating %q: %v", v.Source, err)
|
||||
}
|
||||
info, err := v.infoForData(ext.Raw, v.Source)
|
||||
if err != nil {
|
||||
if fnErr := fn(info, err); fnErr != nil {
|
||||
return fnErr
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := fn(info, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateObjectNamespace(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Object != nil {
|
||||
return metadataAccessor.SetNamespace(info.Object, info.Namespace)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterNamespace omits the namespace if the object is not namespace scoped
|
||||
func FilterNamespace(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Namespaced() {
|
||||
info.Namespace = ""
|
||||
UpdateObjectNamespace(info, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetNamespace ensures that every Info object visited will have a namespace
|
||||
// set. If info.Object is set, it will be mutated as well.
|
||||
func SetNamespace(namespace string) VisitorFunc {
|
||||
return func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Namespaced() {
|
||||
return nil
|
||||
}
|
||||
if len(info.Namespace) == 0 {
|
||||
info.Namespace = namespace
|
||||
UpdateObjectNamespace(info, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RequireNamespace will either set a namespace if none is provided on the
|
||||
// Info object, or if the namespace is set and does not match the provided
|
||||
// value, returns an error. This is intended to guard against administrators
|
||||
// accidentally operating on resources outside their namespace.
|
||||
func RequireNamespace(namespace string) VisitorFunc {
|
||||
return func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Namespaced() {
|
||||
return nil
|
||||
}
|
||||
if len(info.Namespace) == 0 {
|
||||
info.Namespace = namespace
|
||||
UpdateObjectNamespace(info, nil)
|
||||
return nil
|
||||
}
|
||||
if info.Namespace != namespace {
|
||||
return fmt.Errorf("the namespace from the provided object %q does not match the namespace %q. You must pass '--namespace=%s' to perform this operation.", info.Namespace, namespace, info.Namespace)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RetrieveLatest updates the Object on each Info by invoking a standard client
|
||||
// Get.
|
||||
func RetrieveLatest(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if meta.IsListType(info.Object) {
|
||||
return fmt.Errorf("watch is only supported on individual resources and resource collections, but a list of resources is found")
|
||||
}
|
||||
if len(info.Name) == 0 {
|
||||
return nil
|
||||
}
|
||||
if info.Namespaced() && len(info.Namespace) == 0 {
|
||||
return fmt.Errorf("no namespace set on resource %s %q", info.Mapping.Resource, info.Name)
|
||||
}
|
||||
return info.Get()
|
||||
}
|
||||
|
||||
// RetrieveLazy updates the object if it has not been loaded yet.
|
||||
func RetrieveLazy(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Object == nil {
|
||||
return info.Get()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAndRefresh creates an object from input info and refreshes info with that object
|
||||
func CreateAndRefresh(info *Info) error {
|
||||
obj, err := NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.Refresh(obj, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
type FilterFunc func(info *Info, err error) (bool, error)
|
||||
|
||||
type FilteredVisitor struct {
|
||||
visitor Visitor
|
||||
filters []FilterFunc
|
||||
}
|
||||
|
||||
func NewFilteredVisitor(v Visitor, fn ...FilterFunc) Visitor {
|
||||
if len(fn) == 0 {
|
||||
return v
|
||||
}
|
||||
return FilteredVisitor{v, fn}
|
||||
}
|
||||
|
||||
func (v FilteredVisitor) Visit(fn VisitorFunc) error {
|
||||
return v.visitor.Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, filter := range v.filters {
|
||||
ok, err := filter(info, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fn(info, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func FilterByLabelSelector(s labels.Selector) FilterFunc {
|
||||
return func(info *Info, err error) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
a, err := meta.Accessor(info.Object)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !s.Matches(labels.Set(a.GetLabels())) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
type InfoListVisitor []*Info
|
||||
|
||||
func (infos InfoListVisitor) Visit(fn VisitorFunc) error {
|
||||
var err error
|
||||
for _, i := range infos {
|
||||
err = fn(i, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
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 resource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVisitorHttpGet(t *testing.T) {
|
||||
type httpArgs struct {
|
||||
duration time.Duration
|
||||
u string
|
||||
attempts int
|
||||
}
|
||||
|
||||
i := 0
|
||||
tests := []struct {
|
||||
name string
|
||||
httpRetries httpget
|
||||
args httpArgs
|
||||
expectedErr error
|
||||
actualBytes io.ReadCloser
|
||||
actualErr error
|
||||
count int
|
||||
isNotNil bool
|
||||
}{
|
||||
{
|
||||
name: "Test retries on errors",
|
||||
httpRetries: func(url string) (int, string, io.ReadCloser, error) {
|
||||
assert.Equal(t, "hello", url)
|
||||
i++
|
||||
if i > 2 {
|
||||
return 0, "", nil, fmt.Errorf("Failed to get http")
|
||||
}
|
||||
return 0, "", nil, fmt.Errorf("Unexpected error")
|
||||
|
||||
},
|
||||
expectedErr: fmt.Errorf("Failed to get http"),
|
||||
args: httpArgs{
|
||||
duration: 0,
|
||||
u: "hello",
|
||||
attempts: 3,
|
||||
},
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
name: "Test that 500s are retried",
|
||||
httpRetries: func(url string) (int, string, io.ReadCloser, error) {
|
||||
assert.Equal(t, "hello", url)
|
||||
i++
|
||||
return 501, "Status", nil, nil
|
||||
},
|
||||
args: httpArgs{
|
||||
duration: 0,
|
||||
u: "hello",
|
||||
attempts: 3,
|
||||
},
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
name: "Test that 300s are not retried",
|
||||
httpRetries: func(url string) (int, string, io.ReadCloser, error) {
|
||||
assert.Equal(t, "hello", url)
|
||||
i++
|
||||
return 300, "Status", nil, nil
|
||||
|
||||
},
|
||||
args: httpArgs{
|
||||
duration: 0,
|
||||
u: "hello",
|
||||
attempts: 3,
|
||||
},
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
name: "Test attempt count is respected",
|
||||
httpRetries: func(url string) (int, string, io.ReadCloser, error) {
|
||||
assert.Equal(t, "hello", url)
|
||||
i++
|
||||
return 501, "Status", nil, nil
|
||||
|
||||
},
|
||||
args: httpArgs{
|
||||
duration: 0,
|
||||
u: "hello",
|
||||
attempts: 1,
|
||||
},
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
name: "Test attempts less than 1 results in an error",
|
||||
httpRetries: func(url string) (int, string, io.ReadCloser, error) {
|
||||
return 200, "Status", ioutil.NopCloser(new(bytes.Buffer)), nil
|
||||
|
||||
},
|
||||
args: httpArgs{
|
||||
duration: 0,
|
||||
u: "hello",
|
||||
attempts: 0,
|
||||
},
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
name: "Test Success",
|
||||
httpRetries: func(url string) (int, string, io.ReadCloser, error) {
|
||||
assert.Equal(t, "hello", url)
|
||||
i++
|
||||
if i > 1 {
|
||||
return 200, "Status", ioutil.NopCloser(new(bytes.Buffer)), nil
|
||||
}
|
||||
return 501, "Status", nil, nil
|
||||
|
||||
},
|
||||
args: httpArgs{
|
||||
duration: 0,
|
||||
u: "hello",
|
||||
attempts: 3,
|
||||
},
|
||||
count: 2,
|
||||
isNotNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
i = 0
|
||||
actualBytes, actualErr := readHttpWithRetries(tt.httpRetries, tt.args.duration, tt.args.u, tt.args.attempts)
|
||||
|
||||
if tt.isNotNil {
|
||||
assert.Nil(t, actualErr)
|
||||
assert.NotNil(t, actualBytes)
|
||||
} else {
|
||||
if tt.expectedErr != nil {
|
||||
assert.Equal(t, tt.expectedErr, actualErr)
|
||||
} else {
|
||||
assert.Error(t, actualErr)
|
||||
}
|
||||
assert.Nil(t, actualBytes)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.count, i)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenListVisitor(t *testing.T) {
|
||||
b := newDefaultBuilder().
|
||||
FilenameParam(false, &FilenameOptions{Recursive: false, Filenames: []string{"../../artifacts/deeply-nested.yaml"}}).
|
||||
Flatten()
|
||||
|
||||
test := &testVisitor{}
|
||||
|
||||
err := b.Do().Visit(test.Handle)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(test.Infos) != 6 {
|
||||
t.Fatal(spew.Sdump(test.Infos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenListVisitorWithVisitorError(t *testing.T) {
|
||||
b := newDefaultBuilder().
|
||||
FilenameParam(false, &FilenameOptions{Recursive: false, Filenames: []string{"../../artifacts/deeply-nested.yaml"}}).
|
||||
Flatten()
|
||||
|
||||
test := &testVisitor{InjectErr: errors.New("visitor error")}
|
||||
err := b.Do().Visit(test.Handle)
|
||||
if err == nil || !strings.Contains(err.Error(), "visitor error") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(test.Infos) != 6 {
|
||||
t.Fatal(spew.Sdump(test.Infos))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
apiVersion: v1
|
||||
items:
|
||||
- kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: test0
|
||||
data:
|
||||
key1: apple
|
||||
- kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: test1
|
||||
data:
|
||||
key2: apple
|
||||
kind: ConfigMapList
|
||||
metadata: {}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
name: nginx
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: nginx
|
||||
strategy:
|
||||
type: Recreate
|
||||
rollingUpdate: null
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
deployment.kubernetes.io/revision: "1"
|
||||
kubectl.kubernetes.io/last-applied-configuration: '{"kind":"Deployment","apiVersion":"apps/v1","metadata":{"name":"nginx-deployment","creationTimestamp":null,"labels":{"name":"nginx"}},"spec":{"selector":{"matchLabels":{"name":"nginx"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"name":"nginx"}},"spec":{"containers":[{"name":"nginx","image":"nginx","resources":{}}]}},"strategy":{}},"status":{}}'
|
||||
creationTimestamp: "2016-10-24T22:15:06Z"
|
||||
generation: 6
|
||||
labels:
|
||||
name: nginx
|
||||
name: nginx-deployment
|
||||
namespace: test
|
||||
resourceVersion: "355959"
|
||||
selfLink: /apis/extensions/v1beta1/namespaces/test/deployments/nginx-deployment
|
||||
uid: 51ac266e-9a37-11e6-8738-0800270c4edc
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
name: nginx
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 1
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
name: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
imagePullPolicy: Always
|
||||
name: nginx
|
||||
resources: {}
|
||||
terminationMessagePath: /dev/termination-log
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
securityContext: {}
|
||||
terminationGracePeriodSeconds: 30
|
||||
status:
|
||||
availableReplicas: 1
|
||||
observedGeneration: 6
|
||||
replicas: 1
|
||||
updatedReplicas: 1
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"apiVersion":"v1","kind":"ReplicationController","metadata":{"labels":{"name":"test-rc"},"name":"test-rc","namespace":"test"},"spec":{"replicas":1,"template":{"metadata":{"labels":{"name":"test-rc"}},"spec":{"containers":[{"image":"nginx","name":"test-rc","ports":[{"containerPort":80}]}]}}}}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: test-rc
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
containers:
|
||||
- name: test-rc
|
||||
image: nginx
|
||||
args:
|
||||
- -random_flag=%s@domain.com
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"args":"-random_flag=%s@domain.com"}
|
||||
name: test-rc
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
containers:
|
||||
- name: test-rc
|
||||
image: nginx
|
||||
args:
|
||||
- -random_flag=%s@domain.com
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"test":1234}
|
||||
name: test-rc
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
containers:
|
||||
- name: test-rc
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: no-annotation
|
||||
labels:
|
||||
name: no-annotation
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: no-annotation
|
||||
spec:
|
||||
containers:
|
||||
- name: no-annotation
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: no-exist
|
||||
labels:
|
||||
name: no-exist
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: no-exist
|
||||
spec:
|
||||
containers:
|
||||
- name: no-exist
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
apiVersion: v1
|
||||
kind: List
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: test-service
|
||||
labels:
|
||||
name: test-service
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
selector:
|
||||
name: test-rc
|
||||
- apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: test-rc
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
containers:
|
||||
- name: test-rc
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ReplicationController",
|
||||
"metadata": {
|
||||
"name": "test-rc",
|
||||
"labels": {
|
||||
"name": "test-rc"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 1,
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"name": "test-rc"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "test-rc",
|
||||
"image": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: test-rc
|
||||
namespace: test
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
containers:
|
||||
- name: test-rc
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: test-service
|
||||
labels:
|
||||
name: test-service
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
selector:
|
||||
name: test-rc
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: test-rc
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
containers:
|
||||
- name: test-rc
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: test-rc
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test-rc
|
||||
spec:
|
||||
containers:
|
||||
- name: test-rc
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: "unit-test.test.com/v1"
|
||||
kind: Widget
|
||||
metadata:
|
||||
name: "widget"
|
||||
namespace: "test"
|
||||
labels:
|
||||
foo: bar
|
||||
key: "value"
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: "unit-test.test.com/v1"
|
||||
kind: Widget
|
||||
metadata:
|
||||
name: "widget"
|
||||
namespace: "test"
|
||||
annotations:
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"unit-test.test.com/v1\",\"key\":\"value\",\"kind\":\"Widget\",\"metadata\":{\"annotations\":{},\"labels\":{\"foo\":\"bar\"},\"name\":\"widget\",\"namespace\":\"test\"}}\n"
|
||||
labels:
|
||||
foo: bar
|
||||
key: "value"
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: cassandra
|
||||
# The labels will be applied automatically
|
||||
# from the labels in the pod template, if not set
|
||||
# labels:
|
||||
# app: cassandra
|
||||
spec:
|
||||
replicas: 2
|
||||
# The selector will be applied automatically
|
||||
# from the labels in the pod template, if not set.
|
||||
# selector:
|
||||
# app: cassandra
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: cassandra
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- /run.sh
|
||||
resources:
|
||||
limits:
|
||||
cpu: 0.5
|
||||
env:
|
||||
- name: MAX_HEAP_SIZE
|
||||
value: 512M
|
||||
- name: HEAP_NEWSIZE
|
||||
value: 100M
|
||||
- name: CASSANDRA_SEED_PROVIDER
|
||||
value: "io.k8s.cassandra.KubernetesSeedProvider"
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
image: gcr.io/google-samples/cassandra:v13
|
||||
name: cassandra
|
||||
ports:
|
||||
- containerPort: 7000
|
||||
name: intra-node
|
||||
- containerPort: 7001
|
||||
name: tls-intra-node
|
||||
- containerPort: 7199
|
||||
name: jmx
|
||||
- containerPort: 9042
|
||||
name: cql
|
||||
volumeMounts:
|
||||
- mountPath: /cassandra_data
|
||||
name: data
|
||||
volumes:
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontend
|
||||
labels:
|
||||
app: guestbook
|
||||
tier: frontend
|
||||
spec:
|
||||
# if your cluster supports it, uncomment the following to automatically create
|
||||
# an external load-balanced IP for the frontend service.
|
||||
# type: LoadBalancer
|
||||
ports:
|
||||
- port: 80
|
||||
selector:
|
||||
app: guestbook
|
||||
tier: frontend
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,26 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: redis-master
|
||||
labels:
|
||||
app: redis
|
||||
role: master
|
||||
tier: backend
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
role: master
|
||||
tier: backend
|
||||
spec:
|
||||
containers:
|
||||
- name: master
|
||||
image: docker.io/library/redis:5.0.5-alpine
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 100Mi
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis-master
|
||||
labels:
|
||||
app: redis
|
||||
role: master
|
||||
tier: backend
|
||||
spec:
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
selector:
|
||||
app: redis
|
||||
role: master
|
||||
tier: backend
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: frontend
|
||||
spec:
|
||||
replicas: 3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: guestbook
|
||||
tier: frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: php-redis
|
||||
image: gcr.io/google_samples/gb-frontend:v4
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 100Mi
|
||||
env:
|
||||
- name: GET_HOSTS_FROM
|
||||
value: dns
|
||||
# If your cluster config does not include a dns service, then to
|
||||
# instead access environment variables to find service host
|
||||
# info, comment out the 'value: dns' line above, and uncomment the
|
||||
# line below:
|
||||
# value: env
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: redis-master
|
||||
labels:
|
||||
app: redis
|
||||
role: master
|
||||
tier: backend
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
role: master
|
||||
tier: backend
|
||||
spec:
|
||||
containers:
|
||||
- name: master
|
||||
image: docker.io/library/redis:5.0.5-alpine
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 100Mi
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: redis-slave
|
||||
labels:
|
||||
app: redis
|
||||
role: slave
|
||||
tier: backend
|
||||
spec:
|
||||
replicas: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
role: slave
|
||||
tier: backend
|
||||
spec:
|
||||
containers:
|
||||
- name: slave
|
||||
image: docker.io/library/redis:5.0.5-alpine
|
||||
# We are only implementing the dns option of:
|
||||
# https://github.com/kubernetes/examples/blob/97c7ed0eb6555a4b667d2877f965d392e00abc45/guestbook/redis-slave/run.sh
|
||||
command: [ "redis-server", "--slaveof", "redis-master", "6379" ]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 100Mi
|
||||
env:
|
||||
- name: GET_HOSTS_FROM
|
||||
value: dns
|
||||
# If your cluster config does not include a dns service, then to
|
||||
# instead access an environment variable to find the master
|
||||
# service's host, comment out the 'value: dns' line above, and
|
||||
# uncomment the line below:
|
||||
# value: env
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: prometheus-node-exporter
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
daemon: prom-node-exp
|
||||
template:
|
||||
metadata:
|
||||
name: prometheus-node-exporter
|
||||
labels:
|
||||
daemon: prom-node-exp
|
||||
spec:
|
||||
containers:
|
||||
- name: c
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- containerPort: 9090
|
||||
hostPort: 9090
|
||||
name: serverport
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
name: nginx-deployment
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
name: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: pi
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
name: pi
|
||||
spec:
|
||||
containers:
|
||||
- name: pi
|
||||
image: perl
|
||||
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
|
||||
restartPolicy: Never
|
||||
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: first-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
app: mock
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mock
|
||||
spec:
|
||||
containers:
|
||||
- name: mock-container
|
||||
image: k8s.gcr.io/pause:3.2
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: second-rc
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
app: mock
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mock
|
||||
spec:
|
||||
containers:
|
||||
- name: mock-container
|
||||
image: k8s.gcr.io/pause:3.2
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: namespaced-rc
|
||||
namespace: existing-ns
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
app: mock
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mock
|
||||
spec:
|
||||
containers:
|
||||
- name: mock-container
|
||||
image: k8s.gcr.io/pause:3.2
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
metadata:
|
||||
name: redis-slave
|
||||
labels:
|
||||
app: redis
|
||||
role: slave
|
||||
tier: backend
|
||||
spec:
|
||||
# this replicas value is default
|
||||
# modify it according to your case
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
role: slave
|
||||
tier: backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
role: slave
|
||||
tier: backend
|
||||
spec:
|
||||
containers:
|
||||
- name: slave
|
||||
image: docker.io/library/redis:5.0.5-alpine
|
||||
# We are only implementing the dns option of:
|
||||
# https://github.com/kubernetes/examples/blob/97c7ed0eb6555a4b667d2877f965d392e00abc45/guestbook/redis-slave/run.sh
|
||||
command: [ "redis-server", "--slaveof", "redis-master", "6379" ]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 100Mi
|
||||
env:
|
||||
- name: GET_HOSTS_FROM
|
||||
value: dns
|
||||
# If your cluster config does not include a dns service, then to
|
||||
# instead access an environment variable to find the master
|
||||
# service's host, comment out the 'value: dns' line above, and
|
||||
# uncomment the line below.
|
||||
# value: env
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
Loading…
Reference in New Issue