Merge pull request #76849 from liggitt/crd_webhook_integration_tests

Fix scale and rollback subresources with admission webhooks, add integration tests
k3s-v1.15.3
Kubernetes Prow Robot 2019-04-22 10:44:16 -07:00 committed by GitHub
commit 716344fd7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1353 additions and 83 deletions

View File

@ -158,24 +158,27 @@ func (r *RollbackREST) New() runtime.Object {
return &apps.DeploymentRollback{}
}
var _ = rest.Creater(&RollbackREST{})
var _ = rest.NamedCreater(&RollbackREST{})
func (r *RollbackREST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
func (r *RollbackREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
rollback, ok := obj.(*apps.DeploymentRollback)
if !ok {
return nil, errors.NewBadRequest(fmt.Sprintf("not a DeploymentRollback: %#v", obj))
}
if errs := appsvalidation.ValidateDeploymentRollback(rollback); len(errs) != 0 {
return nil, errors.NewInvalid(apps.Kind("DeploymentRollback"), rollback.Name, errs)
}
if name != rollback.Name {
return nil, errors.NewBadRequest("name in URL does not match name in DeploymentRollback object")
}
if createValidation != nil {
if err := createValidation(obj.DeepCopyObject()); err != nil {
return nil, err
}
}
if errs := appsvalidation.ValidateDeploymentRollback(rollback); len(errs) != 0 {
return nil, errors.NewInvalid(apps.Kind("DeploymentRollback"), rollback.Name, errs)
}
// Update the Deployment with information in DeploymentRollback to trigger rollback
err := r.rollbackDeployment(ctx, rollback.Name, &rollback.RollbackTo, rollback.UpdatedAnnotations, dryrun.IsDryRun(options.DryRun))
if err != nil {
@ -293,7 +296,15 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
deployment.Spec.Replicas = scale.Spec.Replicas
deployment.ResourceVersion = scale.ResourceVersion
obj, _, err = r.store.Update(ctx, deployment.Name, rest.DefaultUpdatedObjectInfo(deployment), createValidation, updateValidation, false, options)
obj, _, err = r.store.Update(
ctx,
deployment.Name,
rest.DefaultUpdatedObjectInfo(deployment),
toScaleCreateValidation(createValidation),
toScaleUpdateValidation(updateValidation),
false,
options,
)
if err != nil {
return nil, false, err
}
@ -305,6 +316,30 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
return newScale, false, nil
}
func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc {
return func(obj runtime.Object) error {
scale, err := scaleFromDeployment(obj.(*apps.Deployment))
if err != nil {
return err
}
return f(scale)
}
}
func toScaleUpdateValidation(f rest.ValidateObjectUpdateFunc) rest.ValidateObjectUpdateFunc {
return func(obj, old runtime.Object) error {
newScale, err := scaleFromDeployment(obj.(*apps.Deployment))
if err != nil {
return err
}
oldScale, err := scaleFromDeployment(old.(*apps.Deployment))
if err != nil {
return err
}
return f(newScale, oldScale)
}
}
// scaleFromDeployment returns a scale subresource for a deployment.
func scaleFromDeployment(deployment *apps.Deployment) (*autoscaling.Scale, error) {
selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector)

View File

@ -351,7 +351,7 @@ func TestEtcdCreateDeploymentRollback(t *testing.T) {
if _, err := storage.Deployment.Create(ctx, validNewDeployment(), rest.ValidateAllObjectFunc, &metav1.CreateOptions{}); err != nil {
t.Fatalf("%s: unexpected error: %v", k, err)
}
rollbackRespStatus, err := rollbackStorage.Create(ctx, &test.rollback, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
rollbackRespStatus, err := rollbackStorage.Create(ctx, test.rollback.Name, &test.rollback, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
if !test.errOK(err) {
t.Errorf("%s: unexpected error: %v", k, err)
} else if err == nil {
@ -392,7 +392,7 @@ func TestCreateDeploymentRollbackValidation(t *testing.T) {
validationError := fmt.Errorf("admission deny")
alwaysDenyValidationFunc := func(obj runtime.Object) error { return validationError }
_, err := rollbackStorage.Create(ctx, &rollback, alwaysDenyValidationFunc, &metav1.CreateOptions{})
_, err := rollbackStorage.Create(ctx, rollback.Name, &rollback, alwaysDenyValidationFunc, &metav1.CreateOptions{})
if err == nil || validationError != err {
t.Errorf("expected: %v, got: %v", validationError, err)
@ -411,7 +411,7 @@ func TestEtcdCreateDeploymentRollbackNoDeployment(t *testing.T) {
rollbackStorage := storage.Rollback
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), namespace)
_, err := rollbackStorage.Create(ctx, &apps.DeploymentRollback{
_, err := rollbackStorage.Create(ctx, name, &apps.DeploymentRollback{
Name: name,
UpdatedAnnotations: map[string]string{},
RollbackTo: apps.RollbackConfig{Revision: 1},

View File

@ -201,7 +201,15 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
rs.Spec.Replicas = scale.Spec.Replicas
rs.ResourceVersion = scale.ResourceVersion
obj, _, err = r.store.Update(ctx, rs.Name, rest.DefaultUpdatedObjectInfo(rs), createValidation, updateValidation, false, options)
obj, _, err = r.store.Update(
ctx,
rs.Name,
rest.DefaultUpdatedObjectInfo(rs),
toScaleCreateValidation(createValidation),
toScaleUpdateValidation(updateValidation),
false,
options,
)
if err != nil {
return nil, false, err
}
@ -213,6 +221,30 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
return newScale, false, err
}
func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc {
return func(obj runtime.Object) error {
scale, err := scaleFromReplicaSet(obj.(*apps.ReplicaSet))
if err != nil {
return err
}
return f(scale)
}
}
func toScaleUpdateValidation(f rest.ValidateObjectUpdateFunc) rest.ValidateObjectUpdateFunc {
return func(obj, old runtime.Object) error {
newScale, err := scaleFromReplicaSet(obj.(*apps.ReplicaSet))
if err != nil {
return err
}
oldScale, err := scaleFromReplicaSet(old.(*apps.ReplicaSet))
if err != nil {
return err
}
return f(newScale, oldScale)
}
}
// scaleFromReplicaSet returns a scale subresource for a replica set.
func scaleFromReplicaSet(rs *apps.ReplicaSet) (*autoscaling.Scale, error) {
selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector)

View File

@ -188,7 +188,15 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
ss.Spec.Replicas = scale.Spec.Replicas
ss.ResourceVersion = scale.ResourceVersion
obj, _, err = r.store.Update(ctx, ss.Name, rest.DefaultUpdatedObjectInfo(ss), createValidation, updateValidation, false, options)
obj, _, err = r.store.Update(
ctx,
ss.Name,
rest.DefaultUpdatedObjectInfo(ss),
toScaleCreateValidation(createValidation),
toScaleUpdateValidation(updateValidation),
false,
options,
)
if err != nil {
return nil, false, err
}
@ -200,6 +208,30 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
return newScale, false, err
}
func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc {
return func(obj runtime.Object) error {
scale, err := scaleFromStatefulSet(obj.(*apps.StatefulSet))
if err != nil {
return err
}
return f(scale)
}
}
func toScaleUpdateValidation(f rest.ValidateObjectUpdateFunc) rest.ValidateObjectUpdateFunc {
return func(obj, old runtime.Object) error {
newScale, err := scaleFromStatefulSet(obj.(*apps.StatefulSet))
if err != nil {
return err
}
oldScale, err := scaleFromStatefulSet(old.(*apps.StatefulSet))
if err != nil {
return err
}
return f(newScale, oldScale)
}
}
// scaleFromStatefulSet returns a scale subresource for a statefulset.
func scaleFromStatefulSet(ss *apps.StatefulSet) (*autoscaling.Scale, error) {
selector, err := metav1.LabelSelectorAsSelector(ss.Spec.Selector)

View File

@ -183,7 +183,15 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
rc.Spec.Replicas = scale.Spec.Replicas
rc.ResourceVersion = scale.ResourceVersion
obj, _, err = r.store.Update(ctx, rc.Name, rest.DefaultUpdatedObjectInfo(rc), createValidation, updateValidation, false, options)
obj, _, err = r.store.Update(
ctx,
rc.Name,
rest.DefaultUpdatedObjectInfo(rc),
toScaleCreateValidation(createValidation),
toScaleUpdateValidation(updateValidation),
false,
options,
)
if err != nil {
return nil, false, err
}
@ -191,6 +199,21 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
return scaleFromRC(rc), false, nil
}
func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc {
return func(obj runtime.Object) error {
return f(scaleFromRC(obj.(*api.ReplicationController)))
}
}
func toScaleUpdateValidation(f rest.ValidateObjectUpdateFunc) rest.ValidateObjectUpdateFunc {
return func(obj, old runtime.Object) error {
return f(
scaleFromRC(obj.(*api.ReplicationController)),
scaleFromRC(old.(*api.ReplicationController)),
)
}
}
// scaleFromRC returns a scale subresource for a replication controller.
func scaleFromRC(rc *api.ReplicationController) *autoscaling.Scale {
return &autoscaling.Scale{

View File

@ -95,7 +95,15 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
rc.Spec.Replicas = scale.Spec.Replicas
rc.ResourceVersion = scale.ResourceVersion
obj, _, err = r.store.Update(ctx, rc.Name, rest.DefaultUpdatedObjectInfo(rc), createValidation, updateValidation, false, options)
obj, _, err = r.store.Update(
ctx,
rc.Name,
rest.DefaultUpdatedObjectInfo(rc),
toScaleCreateValidation(createValidation),
toScaleUpdateValidation(updateValidation),
false,
options,
)
if err != nil {
return nil, false, errors.NewConflict(extensions.Resource("replicationcontrollers/scale"), scale.Name, err)
}
@ -103,6 +111,21 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
return scaleFromRC(rc), false, nil
}
func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc {
return func(obj runtime.Object) error {
return f(scaleFromRC(obj.(*api.ReplicationController)))
}
}
func toScaleUpdateValidation(f rest.ValidateObjectUpdateFunc) rest.ValidateObjectUpdateFunc {
return func(obj, old runtime.Object) error {
return f(
scaleFromRC(obj.(*api.ReplicationController)),
scaleFromRC(old.(*api.ReplicationController)),
)
}
}
// scaleFromRC returns a scale subresource for a replication controller.
func scaleFromRC(rc *api.ReplicationController) *autoscaling.Scale {
return &autoscaling.Scale{

View File

@ -190,9 +190,9 @@ func (p *PolicyData) EnsureRBACPolicy() genericapiserver.PostStartHookFunc {
case result.Protected && result.Operation != reconciliation.ReconcileNone:
klog.Warningf("skipped reconcile-protected clusterrole.%s/%s with missing permissions: %v", rbac.GroupName, clusterRole.Name, result.MissingRules)
case result.Operation == reconciliation.ReconcileUpdate:
klog.Infof("updated clusterrole.%s/%s with additional permissions: %v", rbac.GroupName, clusterRole.Name, result.MissingRules)
klog.V(2).Infof("updated clusterrole.%s/%s with additional permissions: %v", rbac.GroupName, clusterRole.Name, result.MissingRules)
case result.Operation == reconciliation.ReconcileCreate:
klog.Infof("created clusterrole.%s/%s", rbac.GroupName, clusterRole.Name)
klog.V(2).Infof("created clusterrole.%s/%s", rbac.GroupName, clusterRole.Name)
}
return nil
})
@ -218,11 +218,11 @@ func (p *PolicyData) EnsureRBACPolicy() genericapiserver.PostStartHookFunc {
case result.Protected && result.Operation != reconciliation.ReconcileNone:
klog.Warningf("skipped reconcile-protected clusterrolebinding.%s/%s with missing subjects: %v", rbac.GroupName, clusterRoleBinding.Name, result.MissingSubjects)
case result.Operation == reconciliation.ReconcileUpdate:
klog.Infof("updated clusterrolebinding.%s/%s with additional subjects: %v", rbac.GroupName, clusterRoleBinding.Name, result.MissingSubjects)
klog.V(2).Infof("updated clusterrolebinding.%s/%s with additional subjects: %v", rbac.GroupName, clusterRoleBinding.Name, result.MissingSubjects)
case result.Operation == reconciliation.ReconcileCreate:
klog.Infof("created clusterrolebinding.%s/%s", rbac.GroupName, clusterRoleBinding.Name)
klog.V(2).Infof("created clusterrolebinding.%s/%s", rbac.GroupName, clusterRoleBinding.Name)
case result.Operation == reconciliation.ReconcileRecreate:
klog.Infof("recreated clusterrolebinding.%s/%s", rbac.GroupName, clusterRoleBinding.Name)
klog.V(2).Infof("recreated clusterrolebinding.%s/%s", rbac.GroupName, clusterRoleBinding.Name)
}
return nil
})
@ -249,9 +249,9 @@ func (p *PolicyData) EnsureRBACPolicy() genericapiserver.PostStartHookFunc {
case result.Protected && result.Operation != reconciliation.ReconcileNone:
klog.Warningf("skipped reconcile-protected role.%s/%s in %v with missing permissions: %v", rbac.GroupName, role.Name, namespace, result.MissingRules)
case result.Operation == reconciliation.ReconcileUpdate:
klog.Infof("updated role.%s/%s in %v with additional permissions: %v", rbac.GroupName, role.Name, namespace, result.MissingRules)
klog.V(2).Infof("updated role.%s/%s in %v with additional permissions: %v", rbac.GroupName, role.Name, namespace, result.MissingRules)
case result.Operation == reconciliation.ReconcileCreate:
klog.Infof("created role.%s/%s in %v", rbac.GroupName, role.Name, namespace)
klog.V(2).Infof("created role.%s/%s in %v", rbac.GroupName, role.Name, namespace)
}
return nil
})
@ -279,11 +279,11 @@ func (p *PolicyData) EnsureRBACPolicy() genericapiserver.PostStartHookFunc {
case result.Protected && result.Operation != reconciliation.ReconcileNone:
klog.Warningf("skipped reconcile-protected rolebinding.%s/%s in %v with missing subjects: %v", rbac.GroupName, roleBinding.Name, namespace, result.MissingSubjects)
case result.Operation == reconciliation.ReconcileUpdate:
klog.Infof("updated rolebinding.%s/%s in %v with additional subjects: %v", rbac.GroupName, roleBinding.Name, namespace, result.MissingSubjects)
klog.V(2).Infof("updated rolebinding.%s/%s in %v with additional subjects: %v", rbac.GroupName, roleBinding.Name, namespace, result.MissingSubjects)
case result.Operation == reconciliation.ReconcileCreate:
klog.Infof("created rolebinding.%s/%s in %v", rbac.GroupName, roleBinding.Name, namespace)
klog.V(2).Infof("created rolebinding.%s/%s in %v", rbac.GroupName, roleBinding.Name, namespace)
case result.Operation == reconciliation.ReconcileRecreate:
klog.Infof("recreated rolebinding.%s/%s in %v", rbac.GroupName, roleBinding.Name, namespace)
klog.V(2).Infof("recreated rolebinding.%s/%s in %v", rbac.GroupName, roleBinding.Name, namespace)
}
return nil
})

View File

@ -12,6 +12,7 @@ go_library(
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
@ -22,6 +23,7 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
"//staging/src/k8s.io/client-go/rest:go_default_library",
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
],

View File

@ -19,6 +19,7 @@ package conversion
import (
"fmt"
autoscalingv1 "k8s.io/api/autoscaling/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -26,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/webhook"
typedscheme "k8s.io/client-go/kubernetes/scheme"
)
// CRConverterFactory is the factory for all CR converters.
@ -77,7 +79,20 @@ func (m *CRConverterFactory) NewConverter(crd *apiextensions.CustomResourceDefin
default:
return nil, nil, fmt.Errorf("unknown conversion strategy %q for CRD %s", crd.Spec.Conversion.Strategy, crd.Name)
}
// Determine whether we should expect to be asked to "convert" autoscaling/v1 Scale types
convertScale := false
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
convertScale = crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil
for _, version := range crd.Spec.Versions {
if version.Subresources != nil && version.Subresources.Scale != nil {
convertScale = true
}
}
}
unsafe = &crConverter{
convertScale: convertScale,
validVersions: validVersions,
clusterScoped: crd.Spec.Scope == apiextensions.ClusterScoped,
converter: converter,
@ -96,6 +111,7 @@ type crConverterInterface interface {
// crConverter extends the delegate converter with generic CR conversion behaviour. The delegate will implement the
// user defined conversion strategy given in the CustomResourceDefinition.
type crConverter struct {
convertScale bool
converter crConverterInterface
validVersions map[schema.GroupVersion]bool
clusterScoped bool
@ -114,14 +130,23 @@ func (c *crConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, valu
}
func (c *crConverter) Convert(in, out, context interface{}) error {
// Special-case typed scale conversion if this custom resource supports a scale endpoint
if c.convertScale {
_, isInScale := in.(*autoscalingv1.Scale)
_, isOutScale := out.(*autoscalingv1.Scale)
if isInScale || isOutScale {
return typedscheme.Scheme.Convert(in, out, context)
}
}
unstructIn, ok := in.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("input type %T in not valid for unstructured conversion", in)
return fmt.Errorf("input type %T in not valid for unstructured conversion to %T", in, out)
}
unstructOut, ok := out.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("output type %T in not valid for unstructured conversion", out)
return fmt.Errorf("output type %T in not valid for unstructured conversion from %T", out, in)
}
outGVK := unstructOut.GroupVersionKind()

View File

@ -268,7 +268,15 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
}
cr.SetResourceVersion(scale.ResourceVersion)
obj, _, err = r.store.Update(ctx, cr.GetName(), rest.DefaultUpdatedObjectInfo(cr), createValidation, updateValidation, false, options)
obj, _, err = r.store.Update(
ctx,
cr.GetName(),
rest.DefaultUpdatedObjectInfo(cr),
toScaleCreateValidation(createValidation, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath),
toScaleUpdateValidation(updateValidation, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath),
false,
options,
)
if err != nil {
return nil, false, err
}
@ -281,6 +289,30 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update
return newScale, false, err
}
func toScaleCreateValidation(f rest.ValidateObjectFunc, specReplicasPath, statusReplicasPath, labelSelectorPath string) rest.ValidateObjectFunc {
return func(obj runtime.Object) error {
scale, _, err := scaleFromCustomResource(obj.(*unstructured.Unstructured), specReplicasPath, statusReplicasPath, labelSelectorPath)
if err != nil {
return err
}
return f(scale)
}
}
func toScaleUpdateValidation(f rest.ValidateObjectUpdateFunc, specReplicasPath, statusReplicasPath, labelSelectorPath string) rest.ValidateObjectUpdateFunc {
return func(obj, old runtime.Object) error {
newScale, _, err := scaleFromCustomResource(obj.(*unstructured.Unstructured), specReplicasPath, statusReplicasPath, labelSelectorPath)
if err != nil {
return err
}
oldScale, _, err := scaleFromCustomResource(old.(*unstructured.Unstructured), specReplicasPath, statusReplicasPath, labelSelectorPath)
if err != nil {
return err
}
return f(newScale, oldScale)
}
}
// scaleFromCustomResource returns a scale subresource for a customresource and a bool signalling wether
// the specReplicas value was found.
func scaleFromCustomResource(cr *unstructured.Unstructured, specReplicasPath, statusReplicasPath, labelSelectorPath string) (*autoscalingv1.Scale, bool, error) {
@ -310,6 +342,11 @@ func scaleFromCustomResource(cr *unstructured.Unstructured, specReplicasPath, st
}
scale := &autoscalingv1.Scale{
// Populate apiVersion and kind so conversion recognizes we are already in the desired GVK and doesn't try to convert
TypeMeta: metav1.TypeMeta{
APIVersion: "autoscaling/v1",
Kind: "Scale",
},
ObjectMeta: metav1.ObjectMeta{
Name: cr.GetName(),
Namespace: cr.GetNamespace(),

View File

@ -381,6 +381,10 @@ func TestScaleGet(t *testing.T) {
}
want := &autoscalingv1.Scale{
TypeMeta: metav1.TypeMeta{
Kind: "Scale",
APIVersion: "autoscaling/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: cr.GetName(),
Namespace: metav1.NamespaceDefault,

View File

@ -6,6 +6,10 @@ go_test(
"main_test.go",
"watch_restart_test.go",
],
tags = [
"etcd",
"integration",
],
deps = [
"//pkg/api/testapi:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",

View File

@ -83,6 +83,7 @@ filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//test/integration/apiserver/admissionwebhook:all-srcs",
"//test/integration/apiserver/apply:all-srcs",
],
tags = ["automanaged"],

View File

@ -0,0 +1,48 @@
load("@io_bazel_rules_go//go:def.bzl", "go_test")
go_test(
name = "go_default_test",
srcs = [
"admission_test.go",
"main_test.go",
],
tags = [
"etcd",
"integration",
],
deps = [
"//cmd/kube-apiserver/app/options:go_default_library",
"//staging/src/k8s.io/api/admission/v1beta1:go_default_library",
"//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//staging/src/k8s.io/api/apps/v1beta1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/api/extensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//staging/src/k8s.io/client-go/dynamic:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/util/retry:go_default_library",
"//test/integration/etcd:go_default_library",
"//test/integration/framework:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,947 @@
/*
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 admissionwebhook
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"sort"
"strings"
"sync"
"testing"
"time"
"k8s.io/api/admission/v1beta1"
admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
v1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
dynamic "k8s.io/client-go/dynamic"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/retry"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/test/integration/etcd"
)
const (
testNamespace = "webhook-integration"
mutation = "mutation"
validation = "validation"
)
type testContext struct {
t *testing.T
admissionHolder *holder
client dynamic.Interface
gvr schema.GroupVersionResource
resource metav1.APIResource
resources map[schema.GroupVersionResource]metav1.APIResource
}
type testFunc func(*testContext)
var (
// defaultResourceFuncs holds the default test functions.
// may be overridden for specific resources by customTestFuncs.
defaultResourceFuncs = map[string]testFunc{
"create": testResourceCreate,
"update": testResourceUpdate,
"patch": testResourcePatch,
"delete": testResourceDelete,
"deletecollection": testResourceDeletecollection,
}
// defaultSubresourceFuncs holds default subresource test functions.
// may be overridden for specific resources by customTestFuncs.
defaultSubresourceFuncs = map[string]testFunc{
"update": testSubresourceUpdate,
"patch": testSubresourcePatch,
}
// customTestFuncs holds custom test functions by resource and verb.
customTestFuncs = map[schema.GroupVersionResource]map[string]testFunc{
gvr("", "v1", "namespaces"): {"delete": testNamespaceDelete},
gvr("apps", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback},
gvr("extensions", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback},
}
// excludedResources lists resources / verb combinations that are not yet tested. this set should trend to zero.
excludedResources = map[schema.GroupVersionResource]sets.String{
// TODO: verify non-persisted review objects work with webhook admission in place (and determine whether they should be sent to admission)
gvr("authentication.k8s.io", "v1", "tokenreviews"): sets.NewString("*"),
gvr("authentication.k8s.io", "v1beta1", "tokenreviews"): sets.NewString("*"),
gvr("authorization.k8s.io", "v1", "localsubjectaccessreviews"): sets.NewString("*"),
gvr("authorization.k8s.io", "v1", "subjectaccessreviews"): sets.NewString("*"),
gvr("authorization.k8s.io", "v1", "selfsubjectaccessreviews"): sets.NewString("*"),
gvr("authorization.k8s.io", "v1", "selfsubjectrulesreviews"): sets.NewString("*"),
gvr("authorization.k8s.io", "v1beta1", "localsubjectaccessreviews"): sets.NewString("*"),
gvr("authorization.k8s.io", "v1beta1", "subjectaccessreviews"): sets.NewString("*"),
gvr("authorization.k8s.io", "v1beta1", "selfsubjectaccessreviews"): sets.NewString("*"),
gvr("authorization.k8s.io", "v1beta1", "selfsubjectrulesreviews"): sets.NewString("*"),
// TODO: webhook config objects are not subject to admission, verify CRUD works and webhooks do not observe them
gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): sets.NewString("*"),
gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): sets.NewString("*"),
// TODO: implement custom subresource tests (requires special states or requests)
gvr("", "v1", "bindings"): sets.NewString("create"),
gvr("", "v1", "nodes/proxy"): sets.NewString("*"),
gvr("", "v1", "pods/attach"): sets.NewString("create"),
gvr("", "v1", "pods/binding"): sets.NewString("create"),
gvr("", "v1", "pods/eviction"): sets.NewString("create"),
gvr("", "v1", "pods/exec"): sets.NewString("create"),
gvr("", "v1", "pods/portforward"): sets.NewString("create"),
gvr("", "v1", "pods/proxy"): sets.NewString("*"),
gvr("", "v1", "services/proxy"): sets.NewString("*"),
}
parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{
gvr("extensions", "v1beta1", "replicationcontrollers/scale"): gvr("", "v1", "replicationcontrollers"),
}
)
type holder struct {
lock sync.RWMutex
t *testing.T
expectGVR metav1.GroupVersionResource
expectGVK schema.GroupVersionKind
expectOperation v1beta1.Operation
expectNamespace string
expectName string
expectObject bool
expectOldObject bool
recorded map[string]*v1beta1.AdmissionRequest
}
func (h *holder) reset(t *testing.T) {
h.lock.Lock()
defer h.lock.Unlock()
h.t = t
h.expectGVR = metav1.GroupVersionResource{}
h.expectGVK = schema.GroupVersionKind{}
h.expectOperation = ""
h.expectName = ""
h.expectNamespace = ""
h.expectObject = false
h.expectOldObject = false
h.recorded = map[string]*v1beta1.AdmissionRequest{
mutation: nil,
validation: nil,
}
}
func (h *holder) expect(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject bool) {
// Special-case namespaces, since the object name shows up in request attributes for update/delete requests
if len(namespace) == 0 && gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace" && operation != v1beta1.Create {
namespace = name
}
h.lock.Lock()
defer h.lock.Unlock()
h.expectGVR = metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
h.expectGVK = gvk
h.expectOperation = operation
h.expectName = name
h.expectNamespace = namespace
h.expectObject = object
h.expectOldObject = oldObject
h.recorded = map[string]*v1beta1.AdmissionRequest{
mutation: nil,
validation: nil,
}
}
func (h *holder) record(phase string, request *v1beta1.AdmissionRequest) {
h.lock.Lock()
defer h.lock.Unlock()
// this is useful to turn on if items aren't getting recorded and you need to figure out why
debug := false
if debug {
h.t.Logf("%s %#v %v", request.Operation, request.Resource, request.SubResource)
}
resource := request.Resource
if len(request.SubResource) > 0 {
resource.Resource += "/" + request.SubResource
}
if resource != h.expectGVR {
if debug {
h.t.Log(resource, "!=", h.expectGVR)
}
return
}
if request.Operation != h.expectOperation {
if debug {
h.t.Log(request.Operation, "!=", h.expectOperation)
}
return
}
if request.Namespace != h.expectNamespace {
if debug {
h.t.Log(request.Namespace, "!=", h.expectNamespace)
}
return
}
name := request.Name
if name == "" && request.Object.Object != nil {
name = request.Object.Object.(*unstructured.Unstructured).GetName()
}
if name != h.expectName {
if debug {
h.t.Log(name, "!=", h.expectName)
}
return
}
h.recorded[phase] = request
}
func (h *holder) verify(t *testing.T) {
h.lock.Lock()
defer h.lock.Unlock()
if err := h.verifyRequest(h.recorded[mutation]); err != nil {
t.Errorf("mutation error: %v", err)
}
if err := h.verifyRequest(h.recorded[validation]); err != nil {
t.Errorf("validation error: %v", err)
}
}
func (h *holder) verifyRequest(request *v1beta1.AdmissionRequest) error {
if request == nil {
return fmt.Errorf("no request received")
}
if h.expectObject {
if err := h.verifyObject(request.Object.Object); err != nil {
return fmt.Errorf("object error: %v", err)
}
} else if request.Object.Object != nil {
return fmt.Errorf("unexpected object: %#v", request.Object.Object)
}
if h.expectOldObject {
if err := h.verifyObject(request.OldObject.Object); err != nil {
return fmt.Errorf("old object error: %v", err)
}
} else if request.OldObject.Object != nil {
return fmt.Errorf("unexpected old object: %#v", request.OldObject.Object)
}
return nil
}
func (h *holder) verifyObject(obj runtime.Object) error {
if obj == nil {
return fmt.Errorf("no object sent")
}
if obj.GetObjectKind().GroupVersionKind() != h.expectGVK {
return fmt.Errorf("expected %#v, got %#v", h.expectGVK, obj.GetObjectKind().GroupVersionKind())
}
return nil
}
// TestWebhookV1beta1 tests communication between API server and webhook process.
func TestWebhookV1beta1(t *testing.T) {
// holder communicates expectations to webhooks, and results from webhooks
holder := &holder{t: t}
// set up webhook server
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(localhostCert) {
t.Fatal("Failed to append Cert from PEM")
}
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
if err != nil {
t.Fatalf("Failed to build cert with error: %+v", err)
}
webhookMux := http.NewServeMux()
webhookMux.Handle("/"+mutation, newWebhookHandler(t, holder, mutation))
webhookMux.Handle("/"+validation, newWebhookHandler(t, holder, validation))
webhookServer := httptest.NewUnstartedServer(webhookMux)
webhookServer.TLS = &tls.Config{
RootCAs: roots,
Certificates: []tls.Certificate{cert},
}
webhookServer.StartTLS()
defer webhookServer.Close()
// start API server
master := etcd.StartRealMasterOrDie(t, func(opts *options.ServerRunOptions) {
// turn off admission plugins that add finalizers
opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount", "StorageObjectInUseProtection"}
// force enable all resources so we can check storage.
// TODO: drop these once we stop allowing them to be served.
opts.APIEnablement.RuntimeConfig["extensions/v1beta1/deployments"] = "true"
opts.APIEnablement.RuntimeConfig["extensions/v1beta1/daemonsets"] = "true"
opts.APIEnablement.RuntimeConfig["extensions/v1beta1/replicasets"] = "true"
opts.APIEnablement.RuntimeConfig["extensions/v1beta1/podsecuritypolicies"] = "true"
opts.APIEnablement.RuntimeConfig["extensions/v1beta1/networkpolicies"] = "true"
})
defer master.Cleanup()
if _, err := master.Client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}); err != nil {
t.Fatal(err)
}
if err := createV1beta1MutationWebhook(master.Client, webhookServer.URL+"/"+mutation); err != nil {
t.Fatal(err)
}
if err := createV1beta1ValidationWebhook(master.Client, webhookServer.URL+"/"+validation); err != nil {
t.Fatal(err)
}
// gather resources to test
dynamicClient := master.Dynamic
_, resources, err := master.Client.Discovery().ServerGroupsAndResources()
if err != nil {
t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
}
gvrsToTest := []schema.GroupVersionResource{}
resourcesByGVR := map[schema.GroupVersionResource]metav1.APIResource{}
for _, list := range resources {
defaultGroupVersion, err := schema.ParseGroupVersion(list.GroupVersion)
if err != nil {
t.Errorf("Failed to get GroupVersion for: %+v", list)
continue
}
for _, resource := range list.APIResources {
if resource.Group == "" {
resource.Group = defaultGroupVersion.Group
}
if resource.Version == "" {
resource.Version = defaultGroupVersion.Version
}
gvr := defaultGroupVersion.WithResource(resource.Name)
resourcesByGVR[gvr] = resource
if shouldTestResource(gvr, resource) {
gvrsToTest = append(gvrsToTest, gvr)
}
}
}
sort.SliceStable(gvrsToTest, func(i, j int) bool {
if gvrsToTest[i].Group < gvrsToTest[j].Group {
return true
}
if gvrsToTest[i].Group > gvrsToTest[j].Group {
return false
}
if gvrsToTest[i].Version < gvrsToTest[j].Version {
return true
}
if gvrsToTest[i].Version > gvrsToTest[j].Version {
return false
}
if gvrsToTest[i].Resource < gvrsToTest[j].Resource {
return true
}
if gvrsToTest[i].Resource > gvrsToTest[j].Resource {
return false
}
return true
})
// Test admission on all resources, subresources, and verbs
for _, gvr := range gvrsToTest {
resource := resourcesByGVR[gvr]
t.Run(gvr.Group+"."+gvr.Version+"."+strings.ReplaceAll(resource.Name, "/", "."), func(t *testing.T) {
for _, verb := range []string{"create", "update", "patch", "connect", "delete", "deletecollection"} {
if shouldTestResourceVerb(gvr, resource, verb) {
t.Run(verb, func(t *testing.T) {
holder.reset(t)
testFunc := getTestFunc(gvr, verb)
testFunc(&testContext{
t: t,
admissionHolder: holder,
client: dynamicClient,
gvr: gvr,
resource: resource,
resources: resourcesByGVR,
})
holder.verify(t)
})
}
}
})
}
}
//
// generic resource testing
//
func testResourceCreate(c *testContext) {
stubObj, err := getStubObj(c.gvr, c.resource)
if err != nil {
c.t.Error(err)
return
}
ns := ""
if c.resource.Namespaced {
ns = testNamespace
}
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Create, stubObj.GetName(), ns, true, false)
_, err = c.client.Resource(c.gvr).Namespace(ns).Create(stubObj, metav1.CreateOptions{})
if err != nil {
c.t.Error(err)
return
}
}
func testResourceUpdate(c *testContext) {
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
obj, err := createOrGetResource(c.client, c.gvr, c.resource)
if err != nil {
return err
}
obj.SetAnnotations(map[string]string{"update": "true"})
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true)
_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(obj, metav1.UpdateOptions{})
return err
}); err != nil {
c.t.Error(err)
return
}
}
func testResourcePatch(c *testContext) {
obj, err := createOrGetResource(c.client, c.gvr, c.resource)
if err != nil {
c.t.Error(err)
return
}
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true)
_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
obj.GetName(),
types.MergePatchType,
[]byte(`{"metadata":{"annotations":{"patch":"true"}}}`),
metav1.PatchOptions{})
if err != nil {
c.t.Error(err)
return
}
}
func testResourceDelete(c *testContext) {
obj, err := createOrGetResource(c.client, c.gvr, c.resource)
if err != nil {
c.t.Error(err)
return
}
background := metav1.DeletePropagationBackground
zero := int64(0)
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, false)
err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(obj.GetName(), &metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
if err != nil {
c.t.Error(err)
return
}
// wait for the item to be gone
err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
if errors.IsNotFound(err) {
return true, nil
}
if err == nil {
c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
return false, nil
}
return false, err
})
if err != nil {
c.t.Error(err)
return
}
}
func testResourceDeletecollection(c *testContext) {
obj, err := createOrGetResource(c.client, c.gvr, c.resource)
if err != nil {
c.t.Error(err)
return
}
background := metav1.DeletePropagationBackground
zero := int64(0)
// update the object with a label that matches our selector
_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
obj.GetName(),
types.MergePatchType,
[]byte(`{"metadata":{"labels":{"webhooktest":"true"}}}`),
metav1.PatchOptions{})
if err != nil {
c.t.Error(err)
return
}
// set expectations
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Delete, "", obj.GetNamespace(), false, false)
// delete
err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).DeleteCollection(&metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}, metav1.ListOptions{LabelSelector: "webhooktest=true"})
if err != nil {
c.t.Error(err)
return
}
// wait for the item to be gone
err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
if errors.IsNotFound(err) {
return true, nil
}
if err == nil {
c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
return false, nil
}
return false, err
})
if err != nil {
c.t.Error(err)
return
}
}
func getParentGVR(gvr schema.GroupVersionResource) schema.GroupVersionResource {
parentGVR, found := parentResources[gvr]
// if no special override is found, just drop the subresource
if !found {
parentGVR = gvr
parentGVR.Resource = strings.Split(parentGVR.Resource, "/")[0]
}
return parentGVR
}
func testSubresourceUpdate(c *testContext) {
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
parentGVR := getParentGVR(c.gvr)
parentResource := c.resources[parentGVR]
obj, err := createOrGetResource(c.client, parentGVR, parentResource)
if err != nil {
return err
}
// Save the parent object as what we submit
submitObj := obj
gvrWithoutSubresources := c.gvr
gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
subresources := strings.Split(c.gvr.Resource, "/")[1:]
// If the subresource supports get, fetch that as the object to submit (namespaces/finalize, */scale, etc)
if sets.NewString(c.resource.Verbs...).Has("get") {
submitObj, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{}, subresources...)
if err != nil {
return err
}
}
// Modify the object
submitObj.SetAnnotations(map[string]string{"subresourceupdate": "true"})
// set expectations
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true)
_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Update(
submitObj,
metav1.UpdateOptions{},
subresources...,
)
return err
}); err != nil {
c.t.Error(err)
}
}
func testSubresourcePatch(c *testContext) {
parentGVR := getParentGVR(c.gvr)
parentResource := c.resources[parentGVR]
obj, err := createOrGetResource(c.client, parentGVR, parentResource)
if err != nil {
c.t.Error(err)
return
}
gvrWithoutSubresources := c.gvr
gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
subresources := strings.Split(c.gvr.Resource, "/")[1:]
// set expectations
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true)
_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Patch(
obj.GetName(),
types.MergePatchType,
[]byte(`{"metadata":{"annotations":{"subresourcepatch":"true"}}}`),
metav1.PatchOptions{},
subresources...,
)
if err != nil {
c.t.Error(err)
return
}
}
func unimplemented(c *testContext) {
c.t.Errorf("Test function for %+v has not been implemented...", c.gvr)
}
//
// custom methods
//
// testNamespaceDelete verifies namespace-specific delete behavior:
// - ensures admission is called on first delete (which only sets deletionTimestamp and terminating state)
// - removes finalizer from namespace
// - ensures admission is called on final delete once finalizers are removed
func testNamespaceDelete(c *testContext) {
obj, err := createOrGetResource(c.client, c.gvr, c.resource)
if err != nil {
c.t.Error(err)
return
}
background := metav1.DeletePropagationBackground
zero := int64(0)
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, false)
err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(obj.GetName(), &metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
if err != nil {
c.t.Error(err)
return
}
c.admissionHolder.verify(c.t)
// do the finalization so the namespace can be deleted
obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
if err != nil {
c.t.Error(err)
return
}
err = unstructured.SetNestedField(obj.Object, nil, "spec", "finalizers")
if err != nil {
c.t.Error(err)
return
}
_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(obj, metav1.UpdateOptions{}, "finalize")
if err != nil {
c.t.Error(err)
return
}
// then run the final delete and make sure admission is called again
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, false)
err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(obj.GetName(), &metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
if err != nil {
c.t.Error(err)
return
}
c.admissionHolder.verify(c.t)
// verify namespace is gone
obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
if err == nil || !errors.IsNotFound(err) {
c.t.Errorf("expected namespace to be gone, got %#v, %v", obj, err)
}
}
// testDeploymentRollback verifies rollback-specific behavior:
// - creates a parent deployment
// - creates a rollback object and posts it
func testDeploymentRollback(c *testContext) {
deploymentGVR := gvr("apps", "v1", "deployments")
obj, err := createOrGetResource(c.client, deploymentGVR, c.resources[deploymentGVR])
if err != nil {
c.t.Error(err)
return
}
gvrWithoutSubresources := c.gvr
gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
subresources := strings.Split(c.gvr.Resource, "/")[1:]
c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Create, obj.GetName(), obj.GetNamespace(), true, false)
var rollbackObj runtime.Object
switch c.gvr {
case gvr("apps", "v1beta1", "deployments/rollback"):
rollbackObj = &appsv1beta1.DeploymentRollback{
TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1beta1", Kind: "DeploymentRollback"},
Name: obj.GetName(),
RollbackTo: appsv1beta1.RollbackConfig{Revision: 0},
}
case gvr("extensions", "v1beta1", "deployments/rollback"):
rollbackObj = &extensionsv1beta1.DeploymentRollback{
TypeMeta: metav1.TypeMeta{APIVersion: "extensions/v1beta1", Kind: "DeploymentRollback"},
Name: obj.GetName(),
RollbackTo: extensionsv1beta1.RollbackConfig{Revision: 0},
}
default:
c.t.Errorf("unknown rollback resource %#v", c.gvr)
return
}
rollbackUnstructuredBody, err := runtime.DefaultUnstructuredConverter.ToUnstructured(rollbackObj)
if err != nil {
c.t.Errorf("ToUnstructured failed: %v", err)
return
}
rollbackUnstructuredObj := &unstructured.Unstructured{Object: rollbackUnstructuredBody}
rollbackUnstructuredObj.SetName(obj.GetName())
_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Create(rollbackUnstructuredObj, metav1.CreateOptions{}, subresources...)
if err != nil {
c.t.Error(err)
return
}
}
//
// utility methods
//
func newWebhookHandler(t *testing.T, holder *holder, phase string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
data, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Error(err)
return
}
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
t.Errorf("contentType=%s, expect application/json", contentType)
return
}
review := v1beta1.AdmissionReview{}
if err := json.Unmarshal(data, &review); err != nil {
t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
http.Error(w, err.Error(), 400)
return
}
if review.GetObjectKind().GroupVersionKind() != gvk("admission.k8s.io", "v1beta1", "AdmissionReview") {
t.Errorf("Invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind())
http.Error(w, err.Error(), 400)
return
}
if len(review.Request.Object.Raw) > 0 {
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil {
t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err)
http.Error(w, err.Error(), 400)
return
}
review.Request.Object.Object = u
}
if len(review.Request.OldObject.Raw) > 0 {
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil {
t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err)
http.Error(w, err.Error(), 400)
return
}
review.Request.OldObject.Object = u
}
holder.record(phase, review.Request)
review.Response = &v1beta1.AdmissionResponse{
Allowed: true,
UID: review.Request.UID,
Result: &metav1.Status{Message: "admitted"},
}
// If we're mutating, and have an object, return a patch to exercise conversion
if phase == mutation && len(review.Request.Object.Raw) > 0 {
review.Response.Patch = []byte(`[{"op":"add","path":"/foo","value":"test"}]`)
jsonPatch := v1beta1.PatchTypeJSONPatch
review.Response.PatchType = &jsonPatch
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(review); err != nil {
t.Errorf("Marshal of response failed with error: %v", err)
}
})
}
func getTestFunc(gvr schema.GroupVersionResource, verb string) testFunc {
if f, found := customTestFuncs[gvr][verb]; found {
return f
}
if strings.Contains(gvr.Resource, "/") {
if f, found := defaultSubresourceFuncs[verb]; found {
return f
}
return unimplemented
}
if f, found := defaultResourceFuncs[verb]; found {
return f
}
return unimplemented
}
func getStubObj(gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr]
if !ok {
return nil, fmt.Errorf("no stub data for %#v", gvr)
}
stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := json.Unmarshal([]byte(data.Stub), &stubObj.Object); err != nil {
return nil, fmt.Errorf("error unmarshaling stub for %#v: %v", gvr, err)
}
return stubObj, nil
}
func createOrGetResource(client dynamic.Interface, gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
stubObj, err := getStubObj(gvr, resource)
if err != nil {
return nil, err
}
ns := ""
if resource.Namespaced {
ns = testNamespace
}
obj, err := client.Resource(gvr).Namespace(ns).Get(stubObj.GetName(), metav1.GetOptions{})
if err == nil {
return obj, nil
}
if !errors.IsNotFound(err) {
return nil, err
}
return client.Resource(gvr).Namespace(ns).Create(stubObj, metav1.CreateOptions{})
}
func gvr(group, version, resource string) schema.GroupVersionResource {
return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
}
func gvk(group, version, kind string) schema.GroupVersionKind {
return schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
}
func shouldTestResource(gvr schema.GroupVersionResource, resource metav1.APIResource) bool {
if !sets.NewString(resource.Verbs...).HasAny("create", "update", "patch", "connect", "delete", "deletecollection") {
return false
}
return !excludedResources[gvr].Has("*")
}
func shouldTestResourceVerb(gvr schema.GroupVersionResource, resource metav1.APIResource, verb string) bool {
if !sets.NewString(resource.Verbs...).Has(verb) {
return false
}
return !excludedResources[gvr].Has(verb)
}
//
// webhook registration helpers
//
func createV1beta1ValidationWebhook(client clientset.Interface, endpoint string) error {
fail := admissionv1beta1.Fail
// Attaching Admission webhook to API server
_, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&admissionv1beta1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"},
Webhooks: []admissionv1beta1.Webhook{{
Name: "admission.integration.test",
ClientConfig: admissionv1beta1.WebhookClientConfig{
URL: &endpoint,
CABundle: localhostCert,
},
Rules: []admissionv1beta1.RuleWithOperations{{
Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
Rule: admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
}},
FailurePolicy: &fail,
AdmissionReviewVersions: []string{"v1beta1"},
}},
})
return err
}
func createV1beta1MutationWebhook(client clientset.Interface, endpoint string) error {
fail := admissionv1beta1.Fail
// Attaching Mutation webhook to API server
_, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "mutation.integration.test"},
Webhooks: []admissionv1beta1.Webhook{{
Name: "mutation.integration.test",
ClientConfig: admissionv1beta1.WebhookClientConfig{
URL: &endpoint,
CABundle: localhostCert,
},
Rules: []admissionv1beta1.RuleWithOperations{{
Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
Rule: admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
}},
FailurePolicy: &fail,
AdmissionReviewVersions: []string{"v1beta1"},
}},
})
return err
}
// localhostCert was generated from crypto/tls/generate_cert.go with the following command:
// go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
MIIBjzCCATmgAwIBAgIRAKpi2WmTcFrVjxrl5n5YDUEwDQYJKoZIhvcNAQELBQAw
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC
QQC9fEbRszP3t14Gr4oahV7zFObBI4TfA5i7YnlMXeLinb7MnvT4bkfOJzE6zktn
59zP7UiHs3l4YOuqrjiwM413AgMBAAGjaDBmMA4GA1UdDwEB/wQEAwICpDATBgNV
HSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MC4GA1UdEQQnMCWCC2V4
YW1wbGUuY29thwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUA
A0EAUsVE6KMnza/ZbodLlyeMzdo7EM/5nb5ywyOxgIOCf0OOLHsPS9ueGLQX9HEG
//yjTXuhNcUugExIjM/AIwAZPQ==
-----END CERTIFICATE-----`)
// localhostKey is the private key for localhostCert.
var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAL18RtGzM/e3XgavihqFXvMU5sEjhN8DmLtieUxd4uKdvsye9Phu
R84nMTrOS2fn3M/tSIezeXhg66quOLAzjXcCAwEAAQJBAKcRxH9wuglYLBdI/0OT
BLzfWPZCEw1vZmMR2FF1Fm8nkNOVDPleeVGTWoOEcYYlQbpTmkGSxJ6ya+hqRi6x
goECIQDx3+X49fwpL6B5qpJIJMyZBSCuMhH4B7JevhGGFENi3wIhAMiNJN5Q3UkL
IuSvv03kaPR5XVQ99/UeEetUgGvBcABpAiBJSBzVITIVCGkGc7d+RCf49KTCIklv
bGWObufAR8Ni4QIgWpILjW8dkGg8GOUZ0zaNA6Nvt6TIv2UWGJ4v5PoV98kCIQDx
rIiZs5QbKdycsv9gQJzwQAogC8o04X3Zz3dsoX+h4A==
-----END RSA PRIVATE KEY-----`)

View File

@ -0,0 +1,27 @@
/*
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 admissionwebhook
import (
"testing"
"k8s.io/kubernetes/test/integration/framework"
)
func TestMain(m *testing.M) {
framework.EtcdMain(m.Run)
}

View File

@ -28,35 +28,43 @@ import (
// It is exported so that it can be reused across multiple tests.
// It returns a new map on every invocation to prevent different tests from mutating shared state.
func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
return GetEtcdStorageDataForNamespace("etcdstoragepathtestnamespace")
}
// GetEtcdStorageDataForNamespace returns etcd data for all persisted objects.
// It is exported so that it can be reused across multiple tests.
// It returns a new map on every invocation to prevent different tests from mutating shared state.
// Namespaced objects keys are computed for the specified namespace.
func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionResource]StorageData {
etcdStorageData := map[schema.GroupVersionResource]StorageData{
// k8s.io/kubernetes/pkg/api/v1
gvr("", "v1", "configmaps"): {
Stub: `{"data": {"foo": "bar"}, "metadata": {"name": "cm1"}}`,
ExpectedEtcdPath: "/registry/configmaps/etcdstoragepathtestnamespace/cm1",
ExpectedEtcdPath: "/registry/configmaps/" + namespace + "/cm1",
},
gvr("", "v1", "services"): {
Stub: `{"metadata": {"name": "service1"}, "spec": {"externalName": "service1name", "ports": [{"port": 10000, "targetPort": 11000}], "selector": {"test": "data"}}}`,
ExpectedEtcdPath: "/registry/services/specs/etcdstoragepathtestnamespace/service1",
ExpectedEtcdPath: "/registry/services/specs/" + namespace + "/service1",
},
gvr("", "v1", "podtemplates"): {
Stub: `{"metadata": {"name": "pt1name"}, "template": {"metadata": {"labels": {"pt": "01"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container9"}]}}}`,
ExpectedEtcdPath: "/registry/podtemplates/etcdstoragepathtestnamespace/pt1name",
ExpectedEtcdPath: "/registry/podtemplates/" + namespace + "/pt1name",
},
gvr("", "v1", "pods"): {
Stub: `{"metadata": {"name": "pod1"}, "spec": {"containers": [{"image": "fedora:latest", "name": "container7", "resources": {"limits": {"cpu": "1M"}, "requests": {"cpu": "1M"}}}]}}`,
ExpectedEtcdPath: "/registry/pods/etcdstoragepathtestnamespace/pod1",
ExpectedEtcdPath: "/registry/pods/" + namespace + "/pod1",
},
gvr("", "v1", "endpoints"): {
Stub: `{"metadata": {"name": "ep1name"}, "subsets": [{"addresses": [{"hostname": "bar-001", "ip": "192.168.3.1"}], "ports": [{"port": 8000}]}]}`,
ExpectedEtcdPath: "/registry/services/endpoints/etcdstoragepathtestnamespace/ep1name",
ExpectedEtcdPath: "/registry/services/endpoints/" + namespace + "/ep1name",
},
gvr("", "v1", "resourcequotas"): {
Stub: `{"metadata": {"name": "rq1name"}, "spec": {"hard": {"cpu": "5M"}}}`,
ExpectedEtcdPath: "/registry/resourcequotas/etcdstoragepathtestnamespace/rq1name",
ExpectedEtcdPath: "/registry/resourcequotas/" + namespace + "/rq1name",
},
gvr("", "v1", "limitranges"): {
Stub: `{"metadata": {"name": "lr1name"}, "spec": {"limits": [{"type": "Pod"}]}}`,
ExpectedEtcdPath: "/registry/limitranges/etcdstoragepathtestnamespace/lr1name",
ExpectedEtcdPath: "/registry/limitranges/" + namespace + "/lr1name",
},
gvr("", "v1", "namespaces"): {
Stub: `{"metadata": {"name": "namespace1"}, "spec": {"finalizers": ["kubernetes"]}}`,
@ -71,41 +79,41 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
ExpectedEtcdPath: "/registry/persistentvolumes/pv1name",
},
gvr("", "v1", "events"): {
Stub: `{"involvedObject": {"namespace": "etcdstoragepathtestnamespace"}, "message": "some data here", "metadata": {"name": "event1"}}`,
ExpectedEtcdPath: "/registry/events/etcdstoragepathtestnamespace/event1",
Stub: `{"involvedObject": {"namespace": "` + namespace + `"}, "message": "some data here", "metadata": {"name": "event1"}}`,
ExpectedEtcdPath: "/registry/events/" + namespace + "/event1",
},
gvr("", "v1", "persistentvolumeclaims"): {
Stub: `{"metadata": {"name": "pvc1"}, "spec": {"accessModes": ["ReadWriteOnce"], "resources": {"limits": {"storage": "1M"}, "requests": {"storage": "2M"}}, "selector": {"matchLabels": {"pvc": "stuff"}}}}`,
ExpectedEtcdPath: "/registry/persistentvolumeclaims/etcdstoragepathtestnamespace/pvc1",
ExpectedEtcdPath: "/registry/persistentvolumeclaims/" + namespace + "/pvc1",
},
gvr("", "v1", "serviceaccounts"): {
Stub: `{"metadata": {"name": "sa1name"}, "secrets": [{"name": "secret00"}]}`,
ExpectedEtcdPath: "/registry/serviceaccounts/etcdstoragepathtestnamespace/sa1name",
ExpectedEtcdPath: "/registry/serviceaccounts/" + namespace + "/sa1name",
},
gvr("", "v1", "secrets"): {
Stub: `{"data": {"key": "ZGF0YSBmaWxl"}, "metadata": {"name": "secret1"}}`,
ExpectedEtcdPath: "/registry/secrets/etcdstoragepathtestnamespace/secret1",
ExpectedEtcdPath: "/registry/secrets/" + namespace + "/secret1",
},
gvr("", "v1", "replicationcontrollers"): {
Stub: `{"metadata": {"name": "rc1"}, "spec": {"selector": {"new": "stuff"}, "template": {"metadata": {"labels": {"new": "stuff"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container8"}]}}}}`,
ExpectedEtcdPath: "/registry/controllers/etcdstoragepathtestnamespace/rc1",
ExpectedEtcdPath: "/registry/controllers/" + namespace + "/rc1",
},
// --
// k8s.io/kubernetes/pkg/apis/apps/v1beta1
gvr("apps", "v1beta1", "statefulsets"): {
Stub: `{"metadata": {"name": "ss1"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}}}}`,
ExpectedEtcdPath: "/registry/statefulsets/etcdstoragepathtestnamespace/ss1",
ExpectedEtcdPath: "/registry/statefulsets/" + namespace + "/ss1",
ExpectedGVK: gvkP("apps", "v1", "StatefulSet"),
},
gvr("apps", "v1beta1", "deployments"): {
Stub: `{"metadata": {"name": "deployment2"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`,
ExpectedEtcdPath: "/registry/deployments/etcdstoragepathtestnamespace/deployment2",
ExpectedEtcdPath: "/registry/deployments/" + namespace + "/deployment2",
ExpectedGVK: gvkP("apps", "v1", "Deployment"),
},
gvr("apps", "v1beta1", "controllerrevisions"): {
Stub: `{"metadata":{"name":"crs1"},"data":{"name":"abc","namespace":"default","creationTimestamp":null,"Spec":{"Replicas":0,"Selector":{"matchLabels":{"foo":"bar"}},"Template":{"creationTimestamp":null,"labels":{"foo":"bar"},"Spec":{"Volumes":null,"InitContainers":null,"Containers":null,"RestartPolicy":"Always","TerminationGracePeriodSeconds":null,"ActiveDeadlineSeconds":null,"DNSPolicy":"ClusterFirst","NodeSelector":null,"ServiceAccountName":"","AutomountServiceAccountToken":null,"NodeName":"","SecurityContext":null,"ImagePullSecrets":null,"Hostname":"","Subdomain":"","Affinity":null,"SchedulerName":"","Tolerations":null,"HostAliases":null}},"VolumeClaimTemplates":null,"ServiceName":""},"Status":{"ObservedGeneration":null,"Replicas":0}},"revision":0}`,
ExpectedEtcdPath: "/registry/controllerrevisions/etcdstoragepathtestnamespace/crs1",
ExpectedEtcdPath: "/registry/controllerrevisions/" + namespace + "/crs1",
ExpectedGVK: gvkP("apps", "v1", "ControllerRevision"),
},
// --
@ -113,27 +121,27 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/apps/v1beta2
gvr("apps", "v1beta2", "statefulsets"): {
Stub: `{"metadata": {"name": "ss2"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}}}}`,
ExpectedEtcdPath: "/registry/statefulsets/etcdstoragepathtestnamespace/ss2",
ExpectedEtcdPath: "/registry/statefulsets/" + namespace + "/ss2",
ExpectedGVK: gvkP("apps", "v1", "StatefulSet"),
},
gvr("apps", "v1beta2", "deployments"): {
Stub: `{"metadata": {"name": "deployment3"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`,
ExpectedEtcdPath: "/registry/deployments/etcdstoragepathtestnamespace/deployment3",
ExpectedEtcdPath: "/registry/deployments/" + namespace + "/deployment3",
ExpectedGVK: gvkP("apps", "v1", "Deployment"),
},
gvr("apps", "v1beta2", "daemonsets"): {
Stub: `{"metadata": {"name": "ds5"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`,
ExpectedEtcdPath: "/registry/daemonsets/etcdstoragepathtestnamespace/ds5",
ExpectedEtcdPath: "/registry/daemonsets/" + namespace + "/ds5",
ExpectedGVK: gvkP("apps", "v1", "DaemonSet"),
},
gvr("apps", "v1beta2", "replicasets"): {
Stub: `{"metadata": {"name": "rs2"}, "spec": {"selector": {"matchLabels": {"g": "h"}}, "template": {"metadata": {"labels": {"g": "h"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container4"}]}}}}`,
ExpectedEtcdPath: "/registry/replicasets/etcdstoragepathtestnamespace/rs2",
ExpectedEtcdPath: "/registry/replicasets/" + namespace + "/rs2",
ExpectedGVK: gvkP("apps", "v1", "ReplicaSet"),
},
gvr("apps", "v1beta2", "controllerrevisions"): {
Stub: `{"metadata":{"name":"crs2"},"data":{"name":"abc","namespace":"default","creationTimestamp":null,"Spec":{"Replicas":0,"Selector":{"matchLabels":{"foo":"bar"}},"Template":{"creationTimestamp":null,"labels":{"foo":"bar"},"Spec":{"Volumes":null,"InitContainers":null,"Containers":null,"RestartPolicy":"Always","TerminationGracePeriodSeconds":null,"ActiveDeadlineSeconds":null,"DNSPolicy":"ClusterFirst","NodeSelector":null,"ServiceAccountName":"","AutomountServiceAccountToken":null,"NodeName":"","SecurityContext":null,"ImagePullSecrets":null,"Hostname":"","Subdomain":"","Affinity":null,"SchedulerName":"","Tolerations":null,"HostAliases":null}},"VolumeClaimTemplates":null,"ServiceName":""},"Status":{"ObservedGeneration":null,"Replicas":0}},"revision":0}`,
ExpectedEtcdPath: "/registry/controllerrevisions/etcdstoragepathtestnamespace/crs2",
ExpectedEtcdPath: "/registry/controllerrevisions/" + namespace + "/crs2",
ExpectedGVK: gvkP("apps", "v1", "ControllerRevision"),
},
// --
@ -141,37 +149,37 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/apps/v1
gvr("apps", "v1", "daemonsets"): {
Stub: `{"metadata": {"name": "ds6"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`,
ExpectedEtcdPath: "/registry/daemonsets/etcdstoragepathtestnamespace/ds6",
ExpectedEtcdPath: "/registry/daemonsets/" + namespace + "/ds6",
},
gvr("apps", "v1", "deployments"): {
Stub: `{"metadata": {"name": "deployment4"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`,
ExpectedEtcdPath: "/registry/deployments/etcdstoragepathtestnamespace/deployment4",
ExpectedEtcdPath: "/registry/deployments/" + namespace + "/deployment4",
},
gvr("apps", "v1", "statefulsets"): {
Stub: `{"metadata": {"name": "ss3"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}}}}`,
ExpectedEtcdPath: "/registry/statefulsets/etcdstoragepathtestnamespace/ss3",
ExpectedEtcdPath: "/registry/statefulsets/" + namespace + "/ss3",
},
gvr("apps", "v1", "replicasets"): {
Stub: `{"metadata": {"name": "rs3"}, "spec": {"selector": {"matchLabels": {"g": "h"}}, "template": {"metadata": {"labels": {"g": "h"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container4"}]}}}}`,
ExpectedEtcdPath: "/registry/replicasets/etcdstoragepathtestnamespace/rs3",
ExpectedEtcdPath: "/registry/replicasets/" + namespace + "/rs3",
},
gvr("apps", "v1", "controllerrevisions"): {
Stub: `{"metadata":{"name":"crs3"},"data":{"name":"abc","namespace":"default","creationTimestamp":null,"Spec":{"Replicas":0,"Selector":{"matchLabels":{"foo":"bar"}},"Template":{"creationTimestamp":null,"labels":{"foo":"bar"},"Spec":{"Volumes":null,"InitContainers":null,"Containers":null,"RestartPolicy":"Always","TerminationGracePeriodSeconds":null,"ActiveDeadlineSeconds":null,"DNSPolicy":"ClusterFirst","NodeSelector":null,"ServiceAccountName":"","AutomountServiceAccountToken":null,"NodeName":"","SecurityContext":null,"ImagePullSecrets":null,"Hostname":"","Subdomain":"","Affinity":null,"SchedulerName":"","Tolerations":null,"HostAliases":null}},"VolumeClaimTemplates":null,"ServiceName":""},"Status":{"ObservedGeneration":null,"Replicas":0}},"revision":0}`,
ExpectedEtcdPath: "/registry/controllerrevisions/etcdstoragepathtestnamespace/crs3",
ExpectedEtcdPath: "/registry/controllerrevisions/" + namespace + "/crs3",
},
// --
// k8s.io/kubernetes/pkg/apis/autoscaling/v1
gvr("autoscaling", "v1", "horizontalpodautoscalers"): {
Stub: `{"metadata": {"name": "hpa2"}, "spec": {"maxReplicas": 3, "scaleTargetRef": {"kind": "something", "name": "cross"}}}`,
ExpectedEtcdPath: "/registry/horizontalpodautoscalers/etcdstoragepathtestnamespace/hpa2",
ExpectedEtcdPath: "/registry/horizontalpodautoscalers/" + namespace + "/hpa2",
},
// --
// k8s.io/kubernetes/pkg/apis/autoscaling/v2beta1
gvr("autoscaling", "v2beta1", "horizontalpodautoscalers"): {
Stub: `{"metadata": {"name": "hpa1"}, "spec": {"maxReplicas": 3, "scaleTargetRef": {"kind": "something", "name": "cross"}}}`,
ExpectedEtcdPath: "/registry/horizontalpodautoscalers/etcdstoragepathtestnamespace/hpa1",
ExpectedEtcdPath: "/registry/horizontalpodautoscalers/" + namespace + "/hpa1",
ExpectedGVK: gvkP("autoscaling", "v1", "HorizontalPodAutoscaler"),
},
// --
@ -179,7 +187,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/autoscaling/v2beta2
gvr("autoscaling", "v2beta2", "horizontalpodautoscalers"): {
Stub: `{"metadata": {"name": "hpa3"}, "spec": {"maxReplicas": 3, "scaleTargetRef": {"kind": "something", "name": "cross"}}}`,
ExpectedEtcdPath: "/registry/horizontalpodautoscalers/etcdstoragepathtestnamespace/hpa3",
ExpectedEtcdPath: "/registry/horizontalpodautoscalers/" + namespace + "/hpa3",
ExpectedGVK: gvkP("autoscaling", "v1", "HorizontalPodAutoscaler"),
},
// --
@ -187,21 +195,21 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/batch/v1
gvr("batch", "v1", "jobs"): {
Stub: `{"metadata": {"name": "job1"}, "spec": {"manualSelector": true, "selector": {"matchLabels": {"controller-uid": "uid1"}}, "template": {"metadata": {"labels": {"controller-uid": "uid1"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container1"}], "dnsPolicy": "ClusterFirst", "restartPolicy": "Never"}}}}`,
ExpectedEtcdPath: "/registry/jobs/etcdstoragepathtestnamespace/job1",
ExpectedEtcdPath: "/registry/jobs/" + namespace + "/job1",
},
// --
// k8s.io/kubernetes/pkg/apis/batch/v1beta1
gvr("batch", "v1beta1", "cronjobs"): {
Stub: `{"metadata": {"name": "cjv1beta1"}, "spec": {"jobTemplate": {"spec": {"template": {"metadata": {"labels": {"controller-uid": "uid0"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container0"}], "dnsPolicy": "ClusterFirst", "restartPolicy": "Never"}}}}, "schedule": "* * * * *"}}`,
ExpectedEtcdPath: "/registry/cronjobs/etcdstoragepathtestnamespace/cjv1beta1",
ExpectedEtcdPath: "/registry/cronjobs/" + namespace + "/cjv1beta1",
},
// --
// k8s.io/kubernetes/pkg/apis/batch/v2alpha1
gvr("batch", "v2alpha1", "cronjobs"): {
Stub: `{"metadata": {"name": "cjv2alpha1"}, "spec": {"jobTemplate": {"spec": {"template": {"metadata": {"labels": {"controller-uid": "uid0"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container0"}], "dnsPolicy": "ClusterFirst", "restartPolicy": "Never"}}}}, "schedule": "* * * * *"}}`,
ExpectedEtcdPath: "/registry/cronjobs/etcdstoragepathtestnamespace/cjv2alpha1",
ExpectedEtcdPath: "/registry/cronjobs/" + namespace + "/cjv2alpha1",
ExpectedGVK: gvkP("batch", "v1beta1", "CronJob"),
},
// --
@ -216,7 +224,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/coordination/v1
gvr("coordination.k8s.io", "v1", "leases"): {
Stub: `{"metadata": {"name": "leasev1"}, "spec": {"holderIdentity": "holder", "leaseDurationSeconds": 5}}`,
ExpectedEtcdPath: "/registry/leases/etcdstoragepathtestnamespace/leasev1",
ExpectedEtcdPath: "/registry/leases/" + namespace + "/leasev1",
ExpectedGVK: gvkP("coordination.k8s.io", "v1beta1", "Lease"),
},
// --
@ -224,14 +232,14 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/coordination/v1beta1
gvr("coordination.k8s.io", "v1beta1", "leases"): {
Stub: `{"metadata": {"name": "leasev1beta1"}, "spec": {"holderIdentity": "holder", "leaseDurationSeconds": 5}}`,
ExpectedEtcdPath: "/registry/leases/etcdstoragepathtestnamespace/leasev1beta1",
ExpectedEtcdPath: "/registry/leases/" + namespace + "/leasev1beta1",
},
// --
// k8s.io/kubernetes/pkg/apis/events/v1beta1
gvr("events.k8s.io", "v1beta1", "events"): {
Stub: `{"metadata": {"name": "event2"}, "regarding": {"namespace": "etcdstoragepathtestnamespace"}, "note": "some data here", "eventTime": "2017-08-09T15:04:05.000000Z", "reportingInstance": "node-xyz", "reportingController": "k8s.io/my-controller", "action": "DidNothing", "reason": "Laziness"}`,
ExpectedEtcdPath: "/registry/events/etcdstoragepathtestnamespace/event2",
Stub: `{"metadata": {"name": "event2"}, "regarding": {"namespace": "` + namespace + `"}, "note": "some data here", "eventTime": "2017-08-09T15:04:05.000000Z", "reportingInstance": "node-xyz", "reportingController": "k8s.io/my-controller", "action": "DidNothing", "reason": "Laziness"}`,
ExpectedEtcdPath: "/registry/events/" + namespace + "/event2",
ExpectedGVK: gvkP("", "v1", "Event"),
},
// --
@ -239,7 +247,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/extensions/v1beta1
gvr("extensions", "v1beta1", "daemonsets"): {
Stub: `{"metadata": {"name": "ds1"}, "spec": {"selector": {"matchLabels": {"u": "t"}}, "template": {"metadata": {"labels": {"u": "t"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container5"}]}}}}`,
ExpectedEtcdPath: "/registry/daemonsets/etcdstoragepathtestnamespace/ds1",
ExpectedEtcdPath: "/registry/daemonsets/" + namespace + "/ds1",
ExpectedGVK: gvkP("apps", "v1", "DaemonSet"),
},
gvr("extensions", "v1beta1", "podsecuritypolicies"): {
@ -249,21 +257,21 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
},
gvr("extensions", "v1beta1", "ingresses"): {
Stub: `{"metadata": {"name": "ingress1"}, "spec": {"backend": {"serviceName": "service", "servicePort": 5000}}}`,
ExpectedEtcdPath: "/registry/ingress/etcdstoragepathtestnamespace/ingress1",
ExpectedEtcdPath: "/registry/ingress/" + namespace + "/ingress1",
},
gvr("extensions", "v1beta1", "networkpolicies"): {
Stub: `{"metadata": {"name": "np1"}, "spec": {"podSelector": {"matchLabels": {"e": "f"}}}}`,
ExpectedEtcdPath: "/registry/networkpolicies/etcdstoragepathtestnamespace/np1",
ExpectedEtcdPath: "/registry/networkpolicies/" + namespace + "/np1",
ExpectedGVK: gvkP("networking.k8s.io", "v1", "NetworkPolicy"),
},
gvr("extensions", "v1beta1", "deployments"): {
Stub: `{"metadata": {"name": "deployment1"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`,
ExpectedEtcdPath: "/registry/deployments/etcdstoragepathtestnamespace/deployment1",
ExpectedEtcdPath: "/registry/deployments/" + namespace + "/deployment1",
ExpectedGVK: gvkP("apps", "v1", "Deployment"),
},
gvr("extensions", "v1beta1", "replicasets"): {
Stub: `{"metadata": {"name": "rs1"}, "spec": {"selector": {"matchLabels": {"g": "h"}}, "template": {"metadata": {"labels": {"g": "h"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container4"}]}}}}`,
ExpectedEtcdPath: "/registry/replicasets/etcdstoragepathtestnamespace/rs1",
ExpectedEtcdPath: "/registry/replicasets/" + namespace + "/rs1",
ExpectedGVK: gvkP("apps", "v1", "ReplicaSet"),
},
// --
@ -271,7 +279,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/networking/v1beta1
gvr("networking.k8s.io", "v1beta1", "ingresses"): {
Stub: `{"metadata": {"name": "ingress2"}, "spec": {"backend": {"serviceName": "service", "servicePort": 5000}}}`,
ExpectedEtcdPath: "/registry/ingress/etcdstoragepathtestnamespace/ingress2",
ExpectedEtcdPath: "/registry/ingress/" + namespace + "/ingress2",
ExpectedGVK: gvkP("extensions", "v1beta1", "Ingress"),
},
// --
@ -279,14 +287,14 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/networking/v1
gvr("networking.k8s.io", "v1", "networkpolicies"): {
Stub: `{"metadata": {"name": "np2"}, "spec": {"podSelector": {"matchLabels": {"e": "f"}}}}`,
ExpectedEtcdPath: "/registry/networkpolicies/etcdstoragepathtestnamespace/np2",
ExpectedEtcdPath: "/registry/networkpolicies/" + namespace + "/np2",
},
// --
// k8s.io/kubernetes/pkg/apis/policy/v1beta1
gvr("policy", "v1beta1", "poddisruptionbudgets"): {
Stub: `{"metadata": {"name": "pdb1"}, "spec": {"selector": {"matchLabels": {"anokkey": "anokvalue"}}}}`,
ExpectedEtcdPath: "/registry/poddisruptionbudgets/etcdstoragepathtestnamespace/pdb1",
ExpectedEtcdPath: "/registry/poddisruptionbudgets/" + namespace + "/pdb1",
},
gvr("policy", "v1beta1", "podsecuritypolicies"): {
Stub: `{"metadata": {"name": "psp2"}, "spec": {"fsGroup": {"rule": "RunAsAny"}, "privileged": true, "runAsUser": {"rule": "RunAsAny"}, "seLinux": {"rule": "MustRunAs"}, "supplementalGroups": {"rule": "RunAsAny"}}}`,
@ -335,14 +343,14 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/settings/v1alpha1
gvr("settings.k8s.io", "v1alpha1", "podpresets"): {
Stub: `{"metadata": {"name": "podpre1"}, "spec": {"env": [{"name": "FOO"}]}}`,
ExpectedEtcdPath: "/registry/podpresets/etcdstoragepathtestnamespace/podpre1",
ExpectedEtcdPath: "/registry/podpresets/" + namespace + "/podpre1",
},
// --
// k8s.io/kubernetes/pkg/apis/rbac/v1alpha1
gvr("rbac.authorization.k8s.io", "v1alpha1", "roles"): {
Stub: `{"metadata": {"name": "role1"}, "rules": [{"apiGroups": ["v1"], "resources": ["events"], "verbs": ["watch"]}]}`,
ExpectedEtcdPath: "/registry/roles/etcdstoragepathtestnamespace/role1",
ExpectedEtcdPath: "/registry/roles/" + namespace + "/role1",
ExpectedGVK: gvkP("rbac.authorization.k8s.io", "v1", "Role"),
},
gvr("rbac.authorization.k8s.io", "v1alpha1", "clusterroles"): {
@ -352,7 +360,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
},
gvr("rbac.authorization.k8s.io", "v1alpha1", "rolebindings"): {
Stub: `{"metadata": {"name": "roleb1"}, "roleRef": {"apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "somecr"}, "subjects": [{"apiVersion": "rbac.authorization.k8s.io/v1alpha1", "kind": "Group", "name": "system:authenticated"}]}`,
ExpectedEtcdPath: "/registry/rolebindings/etcdstoragepathtestnamespace/roleb1",
ExpectedEtcdPath: "/registry/rolebindings/" + namespace + "/roleb1",
ExpectedGVK: gvkP("rbac.authorization.k8s.io", "v1", "RoleBinding"),
},
gvr("rbac.authorization.k8s.io", "v1alpha1", "clusterrolebindings"): {
@ -365,7 +373,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/rbac/v1beta1
gvr("rbac.authorization.k8s.io", "v1beta1", "roles"): {
Stub: `{"metadata": {"name": "role2"}, "rules": [{"apiGroups": ["v1"], "resources": ["events"], "verbs": ["watch"]}]}`,
ExpectedEtcdPath: "/registry/roles/etcdstoragepathtestnamespace/role2",
ExpectedEtcdPath: "/registry/roles/" + namespace + "/role2",
ExpectedGVK: gvkP("rbac.authorization.k8s.io", "v1", "Role"),
},
gvr("rbac.authorization.k8s.io", "v1beta1", "clusterroles"): {
@ -375,7 +383,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
},
gvr("rbac.authorization.k8s.io", "v1beta1", "rolebindings"): {
Stub: `{"metadata": {"name": "roleb2"}, "roleRef": {"apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "somecr"}, "subjects": [{"apiVersion": "rbac.authorization.k8s.io/v1alpha1", "kind": "Group", "name": "system:authenticated"}]}`,
ExpectedEtcdPath: "/registry/rolebindings/etcdstoragepathtestnamespace/roleb2",
ExpectedEtcdPath: "/registry/rolebindings/" + namespace + "/roleb2",
ExpectedGVK: gvkP("rbac.authorization.k8s.io", "v1", "RoleBinding"),
},
gvr("rbac.authorization.k8s.io", "v1beta1", "clusterrolebindings"): {
@ -388,7 +396,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
// k8s.io/kubernetes/pkg/apis/rbac/v1
gvr("rbac.authorization.k8s.io", "v1", "roles"): {
Stub: `{"metadata": {"name": "role3"}, "rules": [{"apiGroups": ["v1"], "resources": ["events"], "verbs": ["watch"]}]}`,
ExpectedEtcdPath: "/registry/roles/etcdstoragepathtestnamespace/role3",
ExpectedEtcdPath: "/registry/roles/" + namespace + "/role3",
},
gvr("rbac.authorization.k8s.io", "v1", "clusterroles"): {
Stub: `{"metadata": {"name": "crole3"}, "rules": [{"nonResourceURLs": ["/version"], "verbs": ["get"]}]}`,
@ -396,7 +404,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
},
gvr("rbac.authorization.k8s.io", "v1", "rolebindings"): {
Stub: `{"metadata": {"name": "roleb3"}, "roleRef": {"apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "somecr"}, "subjects": [{"apiVersion": "rbac.authorization.k8s.io/v1alpha1", "kind": "Group", "name": "system:authenticated"}]}`,
ExpectedEtcdPath: "/registry/rolebindings/etcdstoragepathtestnamespace/roleb3",
ExpectedEtcdPath: "/registry/rolebindings/" + namespace + "/roleb3",
},
gvr("rbac.authorization.k8s.io", "v1", "clusterrolebindings"): {
Stub: `{"metadata": {"name": "croleb3"}, "roleRef": {"apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "somecr"}, "subjects": [{"apiVersion": "rbac.authorization.k8s.io/v1alpha1", "kind": "Group", "name": "system:authenticated"}]}`,
@ -462,18 +470,18 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData {
},
gvr("cr.bar.com", "v1", "foos"): {
Stub: `{"kind": "Foo", "apiVersion": "cr.bar.com/v1", "metadata": {"name": "cr1foo"}, "color": "blue"}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
ExpectedEtcdPath: "/registry/cr.bar.com/foos/etcdstoragepathtestnamespace/cr1foo",
ExpectedEtcdPath: "/registry/cr.bar.com/foos/" + namespace + "/cr1foo",
},
gvr("custom.fancy.com", "v2", "pants"): {
Stub: `{"kind": "Pant", "apiVersion": "custom.fancy.com/v2", "metadata": {"name": "cr2pant"}, "isFancy": true}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
ExpectedEtcdPath: "/registry/custom.fancy.com/pants/cr2pant",
},
gvr("awesome.bears.com", "v1", "pandas"): {
Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v1", "metadata": {"name": "cr3panda"}, "weight": 100}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v1", "metadata": {"name": "cr3panda"}, "spec":{"replicas": 100}}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
ExpectedEtcdPath: "/registry/awesome.bears.com/pandas/cr3panda",
},
gvr("awesome.bears.com", "v3", "pandas"): {
Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v3", "metadata": {"name": "cr4panda"}, "weight": 300}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v3", "metadata": {"name": "cr4panda"}, "spec":{"replicas": 300}}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
ExpectedEtcdPath: "/registry/awesome.bears.com/pandas/cr4panda",
ExpectedGVK: gvkP("awesome.bears.com", "v1", "Panda"),
},
@ -601,6 +609,14 @@ func GetCustomResourceDefinitionData() []*apiextensionsv1beta1.CustomResourceDef
Plural: "pandas",
Kind: "Panda",
},
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: func() *string { path := ".status.selector"; return &path }(),
},
},
},
},
}

View File

@ -139,13 +139,19 @@ func StartRealMasterOrDie(t *testing.T, configFuncs ...func(*options.ServerRunOp
}()
lastHealth := ""
attempt := 0
if err := wait.PollImmediate(time.Second, time.Minute, func() (done bool, err error) {
// wait for the server to be healthy
result := kubeClient.RESTClient().Get().AbsPath("/healthz").Do()
content, _ := result.Raw()
lastHealth = string(content)
if errResult := result.Error(); errResult != nil {
t.Log(errResult)
attempt++
if attempt < 10 {
t.Log("waiting for server to be healthy")
} else {
t.Log(errResult)
}
return false, nil
}
var status int

View File

@ -78,8 +78,10 @@ go_library(
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
"//test/e2e/framework:go_default_library",
"//test/utils:go_default_library",
"//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/pborman/uuid:go_default_library",
"//vendor/google.golang.org/grpc/grpclog:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/common:go_default_library",
],

View File

@ -27,6 +27,8 @@ import (
"runtime"
"strings"
"github.com/coreos/etcd/clientv3"
"google.golang.org/grpc/grpclog"
"k8s.io/klog"
"k8s.io/kubernetes/pkg/util/env"
@ -109,7 +111,7 @@ func startEtcd() (func(), error) {
"--listen-peer-urls",
"http://127.0.0.1:0",
"--log-package-levels",
"*=DEBUG",
"*=NOTICE", // set to INFO or DEBUG for more logs
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@ -123,6 +125,10 @@ func startEtcd() (func(), error) {
}
}
// Quiet etcd logs for integration tests
// Comment out to get verbose logs if desired
clientv3.SetLogger(grpclog.NewLoggerV2(ioutil.Discard, ioutil.Discard, os.Stderr))
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to run etcd: %v", err)
}