Remove alpha InitializerConfiguration types, Initializers admission plugin
Kubernetes-commit: dc1fa870bff65c20f48a83ea3af54adb3f526e28
This commit is contained in:
parent
8ed8dcb5c9
commit
123cf8011f
File diff suppressed because it is too large
Load Diff
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
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 configuration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
type InitializerConfigurationLister interface {
|
||||
List(opts metav1.ListOptions) (*v1alpha1.InitializerConfigurationList, error)
|
||||
}
|
||||
|
||||
type InitializerConfigurationManager struct {
|
||||
*poller
|
||||
}
|
||||
|
||||
func NewInitializerConfigurationManager(c InitializerConfigurationLister) *InitializerConfigurationManager {
|
||||
getFn := func() (runtime.Object, error) {
|
||||
list, err := c.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) || errors.IsForbidden(err) {
|
||||
klog.V(5).Infof("Initializers are disabled due to an error: %v", err)
|
||||
return nil, ErrDisabled
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return mergeInitializerConfigurations(list), nil
|
||||
}
|
||||
return &InitializerConfigurationManager{
|
||||
newPoller(getFn),
|
||||
}
|
||||
}
|
||||
|
||||
// Initializers returns the merged InitializerConfiguration.
|
||||
func (im *InitializerConfigurationManager) Initializers() (*v1alpha1.InitializerConfiguration, error) {
|
||||
configuration, err := im.poller.configuration()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
initializerConfiguration, ok := configuration.(*v1alpha1.InitializerConfiguration)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected type %v, got type %v", reflect.TypeOf(initializerConfiguration), reflect.TypeOf(configuration))
|
||||
}
|
||||
return initializerConfiguration, nil
|
||||
}
|
||||
|
||||
func (im *InitializerConfigurationManager) Run(stopCh <-chan struct{}) {
|
||||
im.poller.Run(stopCh)
|
||||
}
|
||||
|
||||
func mergeInitializerConfigurations(initializerConfigurationList *v1alpha1.InitializerConfigurationList) *v1alpha1.InitializerConfiguration {
|
||||
configurations := initializerConfigurationList.Items
|
||||
sort.SliceStable(configurations, InitializerConfigurationSorter(configurations).ByName)
|
||||
var ret v1alpha1.InitializerConfiguration
|
||||
for _, c := range configurations {
|
||||
ret.Initializers = append(ret.Initializers, c.Initializers...)
|
||||
}
|
||||
return &ret
|
||||
}
|
||||
|
||||
type InitializerConfigurationSorter []v1alpha1.InitializerConfiguration
|
||||
|
||||
func (a InitializerConfigurationSorter) ByName(i, j int) bool {
|
||||
return a[i].Name < a[j].Name
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
/*
|
||||
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 configuration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type mockLister struct {
|
||||
invoked int
|
||||
successes int
|
||||
failures int
|
||||
configurationList v1alpha1.InitializerConfigurationList
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func newMockLister(successes, failures int, configurationList v1alpha1.InitializerConfigurationList, t *testing.T) *mockLister {
|
||||
return &mockLister{
|
||||
failures: failures,
|
||||
successes: successes,
|
||||
configurationList: configurationList,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
// The first List will be successful; the next m.failures List will
|
||||
// fail; the next m.successes List will be successful
|
||||
// List should only be called 1+m.failures+m.successes times.
|
||||
func (m *mockLister) List(options metav1.ListOptions) (*v1alpha1.InitializerConfigurationList, error) {
|
||||
m.invoked++
|
||||
if m.invoked == 1 {
|
||||
return &m.configurationList, nil
|
||||
}
|
||||
if m.invoked <= 1+m.failures {
|
||||
return nil, fmt.Errorf("some error")
|
||||
}
|
||||
if m.invoked <= 1+m.failures+m.successes {
|
||||
return &m.configurationList, nil
|
||||
}
|
||||
m.t.Fatalf("unexpected call to List, should only be called %d times", 1+m.successes+m.failures)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var _ InitializerConfigurationLister = &mockLister{}
|
||||
|
||||
func TestConfiguration(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
failures int
|
||||
// note that the first call to mockLister is always a success.
|
||||
successes int
|
||||
expectReady bool
|
||||
}{
|
||||
{
|
||||
name: "number of failures hasn't reached failureThreshold",
|
||||
failures: defaultFailureThreshold - 1,
|
||||
expectReady: true,
|
||||
},
|
||||
{
|
||||
name: "number of failures just reaches failureThreshold",
|
||||
failures: defaultFailureThreshold,
|
||||
expectReady: false,
|
||||
},
|
||||
{
|
||||
name: "number of failures exceeds failureThreshold",
|
||||
failures: defaultFailureThreshold + 1,
|
||||
expectReady: false,
|
||||
},
|
||||
{
|
||||
name: "number of failures exceeds failureThreshold, but then get another success",
|
||||
failures: defaultFailureThreshold + 1,
|
||||
successes: 1,
|
||||
expectReady: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
mock := newMockLister(c.successes, c.failures, v1alpha1.InitializerConfigurationList{}, t)
|
||||
manager := NewInitializerConfigurationManager(mock)
|
||||
manager.interval = 1 * time.Millisecond
|
||||
for i := 0; i < 1+c.successes+c.failures; i++ {
|
||||
manager.sync()
|
||||
}
|
||||
_, err := manager.Initializers()
|
||||
if err != nil && c.expectReady {
|
||||
t.Errorf("case %s, expect ready, got: %v", c.name, err)
|
||||
}
|
||||
if err == nil && !c.expectReady {
|
||||
t.Errorf("case %s, expect not ready", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeInitializerConfigurations(t *testing.T) {
|
||||
configurationsList := v1alpha1.InitializerConfigurationList{
|
||||
Items: []v1alpha1.InitializerConfiguration{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "provider_2",
|
||||
},
|
||||
Initializers: []v1alpha1.Initializer{
|
||||
{
|
||||
Name: "initializer_a",
|
||||
},
|
||||
{
|
||||
Name: "initializer_b",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "provider_1",
|
||||
},
|
||||
Initializers: []v1alpha1.Initializer{
|
||||
{
|
||||
Name: "initializer_c",
|
||||
},
|
||||
{
|
||||
Name: "initializer_d",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected := &v1alpha1.InitializerConfiguration{
|
||||
Initializers: []v1alpha1.Initializer{
|
||||
{
|
||||
Name: "initializer_c",
|
||||
},
|
||||
{
|
||||
Name: "initializer_d",
|
||||
},
|
||||
{
|
||||
Name: "initializer_a",
|
||||
},
|
||||
{
|
||||
Name: "initializer_b",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got := mergeInitializerConfigurations(&configurationsList)
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %#v, got: %#v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
type disabledInitializerConfigLister struct{}
|
||||
|
||||
func (l *disabledInitializerConfigLister) List(options metav1.ListOptions) (*v1alpha1.InitializerConfigurationList, error) {
|
||||
return nil, errors.NewNotFound(schema.GroupResource{Group: "admissionregistration", Resource: "initializerConfigurations"}, "")
|
||||
}
|
||||
func TestInitializerConfigDisabled(t *testing.T) {
|
||||
manager := NewInitializerConfigurationManager(&disabledInitializerConfigLister{})
|
||||
manager.sync()
|
||||
_, err := manager.Initializers()
|
||||
if err.Error() != ErrDisabled.Error() {
|
||||
t.Errorf("expected %v, got %v", ErrDisabled, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,369 +0,0 @@
|
|||
/*
|
||||
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 initialization
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/validation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/configuration"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
// Name of admission plug-in
|
||||
PluginName = "Initializers"
|
||||
)
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewInitializer(), nil
|
||||
})
|
||||
}
|
||||
|
||||
type initializerOptions struct {
|
||||
Initializers []string
|
||||
}
|
||||
|
||||
// InitializationConfig specifies initialization config
|
||||
type InitializationConfig interface {
|
||||
Run(stopCh <-chan struct{})
|
||||
Initializers() (*v1alpha1.InitializerConfiguration, error)
|
||||
}
|
||||
|
||||
type initializer struct {
|
||||
config InitializationConfig
|
||||
authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
// NewInitializer creates a new initializer plugin which assigns newly created resources initializers
|
||||
// based on configuration loaded from the admission API group.
|
||||
// FUTURE: this may be moved to the storage layer of the apiserver, but for now this is an alpha feature
|
||||
// that can be disabled.
|
||||
func NewInitializer() admission.Interface {
|
||||
return &initializer{}
|
||||
}
|
||||
|
||||
// ValidateInitialization implements the InitializationValidator interface.
|
||||
func (i *initializer) ValidateInitialization() error {
|
||||
if i.config == nil {
|
||||
return fmt.Errorf("the Initializer admission plugin requires a Kubernetes client to be provided")
|
||||
}
|
||||
if i.authorizer == nil {
|
||||
return fmt.Errorf("the Initializer admission plugin requires an authorizer to be provided")
|
||||
}
|
||||
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.Initializers) {
|
||||
if err := utilfeature.DefaultMutableFeatureGate.Set(string(features.Initializers) + "=true"); err != nil {
|
||||
klog.Errorf("error enabling Initializers feature as part of admission plugin setup: %v", err)
|
||||
} else {
|
||||
klog.Infof("enabled Initializers feature as part of admission plugin setup")
|
||||
}
|
||||
}
|
||||
|
||||
i.config.Run(wait.NeverStop)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetExternalKubeClientSet implements the WantsExternalKubeClientSet interface.
|
||||
func (i *initializer) SetExternalKubeClientSet(client clientset.Interface) {
|
||||
i.config = configuration.NewInitializerConfigurationManager(client.AdmissionregistrationV1alpha1().InitializerConfigurations())
|
||||
}
|
||||
|
||||
// SetAuthorizer implements the WantsAuthorizer interface.
|
||||
func (i *initializer) SetAuthorizer(a authorizer.Authorizer) {
|
||||
i.authorizer = a
|
||||
}
|
||||
|
||||
var initializerFieldPath = field.NewPath("metadata", "initializers")
|
||||
|
||||
// readConfig holds requests instead of failing them if the server is not yet initialized
|
||||
// or is unresponsive. It formats the returned error for client use if necessary.
|
||||
func (i *initializer) readConfig(a admission.Attributes) (*v1alpha1.InitializerConfiguration, error) {
|
||||
// read initializers from config
|
||||
config, err := i.config.Initializers()
|
||||
if err == nil {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// if initializer configuration is disabled, fail open
|
||||
if err == configuration.ErrDisabled {
|
||||
return &v1alpha1.InitializerConfiguration{}, nil
|
||||
}
|
||||
|
||||
e := errors.NewServerTimeout(a.GetResource().GroupResource(), "create", 1)
|
||||
if err == configuration.ErrNotReady {
|
||||
e.ErrStatus.Message = fmt.Sprintf("Waiting for initialization configuration to load: %v", err)
|
||||
e.ErrStatus.Reason = "LoadingConfiguration"
|
||||
e.ErrStatus.Details.Causes = append(e.ErrStatus.Details.Causes, metav1.StatusCause{
|
||||
Type: "InitializerConfigurationPending",
|
||||
Message: "The server is waiting for the initializer configuration to be loaded.",
|
||||
})
|
||||
} else {
|
||||
e.ErrStatus.Message = fmt.Sprintf("Unable to refresh the initializer configuration: %v", err)
|
||||
e.ErrStatus.Reason = "LoadingConfiguration"
|
||||
e.ErrStatus.Details.Causes = append(e.ErrStatus.Details.Causes, metav1.StatusCause{
|
||||
Type: "InitializerConfigurationFailure",
|
||||
Message: "An error has occurred while refreshing the initializer configuration, no resources can be created until a refresh succeeds.",
|
||||
})
|
||||
}
|
||||
return nil, e
|
||||
}
|
||||
|
||||
// Admit checks for create requests to add initializers, or update request to enforce invariants.
|
||||
// The admission controller fails open if the object doesn't have ObjectMeta (can't be initialized).
|
||||
// A client with sufficient permission ("initialize" verb on resource) can specify its own initializers
|
||||
// or an empty initializers struct (which bypasses initialization). Only clients with the initialize verb
|
||||
// can update objects that have not completed initialization. Sub resources can still be modified on
|
||||
// resources that are undergoing initialization.
|
||||
// TODO: once this logic is ready for beta, move it into the REST storage layer.
|
||||
func (i *initializer) Admit(a admission.Attributes) (err error) {
|
||||
switch a.GetOperation() {
|
||||
case admission.Create, admission.Update:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: should sub-resource action should be denied until the object is initialized?
|
||||
if len(a.GetSubresource()) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch a.GetOperation() {
|
||||
case admission.Create:
|
||||
accessor, err := meta.Accessor(a.GetObject())
|
||||
if err != nil {
|
||||
// objects without meta accessor cannot be checked for initialization, and it is possible to make calls
|
||||
// via our API that don't have ObjectMeta
|
||||
return nil
|
||||
}
|
||||
existing := accessor.GetInitializers()
|
||||
if existing != nil {
|
||||
klog.V(5).Infof("Admin bypassing initialization for %s", a.GetResource())
|
||||
|
||||
// it must be possible for some users to bypass initialization - for now, check the initialize operation
|
||||
if err := i.canInitialize(a, "create with initializers denied"); err != nil {
|
||||
return err
|
||||
}
|
||||
// allow administrators to bypass initialization by setting an empty initializers struct
|
||||
if len(existing.Pending) == 0 && existing.Result == nil {
|
||||
accessor.SetInitializers(nil)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
klog.V(5).Infof("Checking initialization for %s", a.GetResource())
|
||||
|
||||
config, err := i.readConfig(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Mirror pods are exempt from initialization because they are created and initialized
|
||||
// on the Kubelet before they appear in the API.
|
||||
// TODO: once this moves to REST storage layer, this becomes a pod specific concern
|
||||
if a.GetKind().GroupKind() == v1.SchemeGroupVersion.WithKind("Pod").GroupKind() {
|
||||
accessor, err := meta.Accessor(a.GetObject())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
annotations := accessor.GetAnnotations()
|
||||
if _, isMirror := annotations[v1.MirrorPodAnnotationKey]; isMirror {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
names := findInitializers(config, a.GetResource())
|
||||
if len(names) == 0 {
|
||||
klog.V(5).Infof("No initializers needed")
|
||||
return nil
|
||||
}
|
||||
|
||||
klog.V(5).Infof("Found initializers for %s: %v", a.GetResource(), names)
|
||||
accessor.SetInitializers(newInitializers(names))
|
||||
}
|
||||
|
||||
case admission.Update:
|
||||
accessor, err := meta.Accessor(a.GetObject())
|
||||
if err != nil {
|
||||
// objects without meta accessor cannot be checked for initialization, and it is possible to make calls
|
||||
// via our API that don't have ObjectMeta
|
||||
return nil
|
||||
}
|
||||
updated := accessor.GetInitializers()
|
||||
|
||||
// controllers deployed with an empty initializers.pending have their initializers set to nil
|
||||
// but should be able to update without changing their manifest
|
||||
if updated != nil && len(updated.Pending) == 0 && updated.Result == nil {
|
||||
accessor.SetInitializers(nil)
|
||||
updated = nil
|
||||
}
|
||||
|
||||
existingAccessor, err := meta.Accessor(a.GetOldObject())
|
||||
if err != nil {
|
||||
// if the old object does not have an accessor, but the new one does, error out
|
||||
return fmt.Errorf("initialized resources must be able to set initializers (%T): %v", a.GetOldObject(), err)
|
||||
}
|
||||
existing := existingAccessor.GetInitializers()
|
||||
|
||||
// updates on initialized resources are allowed
|
||||
if updated == nil && existing == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
klog.V(5).Infof("Modifying uninitialized resource %s", a.GetResource())
|
||||
|
||||
// because we are called before validation, we need to ensure the update transition is valid.
|
||||
if errs := validation.ValidateInitializersUpdate(updated, existing, initializerFieldPath); len(errs) > 0 {
|
||||
return errors.NewInvalid(a.GetKind().GroupKind(), a.GetName(), errs)
|
||||
}
|
||||
|
||||
// caller must have the ability to mutate un-initialized resources
|
||||
if err := i.canInitialize(a, "update to uninitialized resource denied"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: restrict initialization list changes to specific clients?
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *initializer) canInitialize(a admission.Attributes, message string) error {
|
||||
// caller must have the ability to mutate un-initialized resources
|
||||
decision, reason, err := i.authorizer.Authorize(authorizer.AttributesRecord{
|
||||
Name: a.GetName(),
|
||||
ResourceRequest: true,
|
||||
User: a.GetUserInfo(),
|
||||
Verb: "initialize",
|
||||
Namespace: a.GetNamespace(),
|
||||
APIGroup: a.GetResource().Group,
|
||||
APIVersion: a.GetResource().Version,
|
||||
Resource: a.GetResource().Resource,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if decision != authorizer.DecisionAllow {
|
||||
return errors.NewForbidden(a.GetResource().GroupResource(), a.GetName(), fmt.Errorf("%s: %s", message, reason))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handles returns true if this admission controller can handle the given operation
|
||||
// where operation can be one of CREATE, UPDATE, DELETE, or CONNECT
|
||||
func (i *initializer) Handles(op admission.Operation) bool {
|
||||
return op == admission.Create || op == admission.Update
|
||||
}
|
||||
|
||||
// newInitializers populates an Initializers struct.
|
||||
func newInitializers(names []string) *metav1.Initializers {
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
var init []metav1.Initializer
|
||||
for _, name := range names {
|
||||
init = append(init, metav1.Initializer{Name: name})
|
||||
}
|
||||
return &metav1.Initializers{
|
||||
Pending: init,
|
||||
}
|
||||
}
|
||||
|
||||
// findInitializers returns the list of initializer names that apply to a config. It returns an empty list
|
||||
// if no initializers apply.
|
||||
func findInitializers(initializers *v1alpha1.InitializerConfiguration, gvr schema.GroupVersionResource) []string {
|
||||
var names []string
|
||||
for _, init := range initializers.Initializers {
|
||||
if !matchRule(init.Rules, gvr) {
|
||||
continue
|
||||
}
|
||||
names = append(names, init.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// matchRule returns true if any rule matches the provided group version resource.
|
||||
func matchRule(rules []v1alpha1.Rule, gvr schema.GroupVersionResource) bool {
|
||||
for _, rule := range rules {
|
||||
if !hasGroup(rule.APIGroups, gvr.Group) {
|
||||
return false
|
||||
}
|
||||
if !hasVersion(rule.APIVersions, gvr.Version) {
|
||||
return false
|
||||
}
|
||||
if !hasResource(rule.Resources, gvr.Resource) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(rules) > 0
|
||||
}
|
||||
|
||||
func hasGroup(groups []string, group string) bool {
|
||||
if groups[0] == "*" {
|
||||
return true
|
||||
}
|
||||
for _, g := range groups {
|
||||
if g == group {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasVersion(versions []string, version string) bool {
|
||||
if versions[0] == "*" {
|
||||
return true
|
||||
}
|
||||
for _, v := range versions {
|
||||
if v == version {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasResource(resources []string, resource string) bool {
|
||||
if resources[0] == "*" || resources[0] == "*/*" {
|
||||
return true
|
||||
}
|
||||
for _, r := range resources {
|
||||
if strings.Contains(r, "/") {
|
||||
continue
|
||||
}
|
||||
if r == resource {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
/*
|
||||
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 initialization
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
"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"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
func newInitializer(name string, rules ...v1alpha1.Rule) *v1alpha1.InitializerConfiguration {
|
||||
return addInitializer(&v1alpha1.InitializerConfiguration{}, name, rules...)
|
||||
}
|
||||
|
||||
func addInitializer(base *v1alpha1.InitializerConfiguration, name string, rules ...v1alpha1.Rule) *v1alpha1.InitializerConfiguration {
|
||||
base.Initializers = append(base.Initializers, v1alpha1.Initializer{
|
||||
Name: name,
|
||||
Rules: rules,
|
||||
})
|
||||
return base
|
||||
}
|
||||
|
||||
func TestFindInitializers(t *testing.T) {
|
||||
type args struct {
|
||||
initializers *v1alpha1.InitializerConfiguration
|
||||
gvr schema.GroupVersionResource
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
gvr: schema.GroupVersionResource{},
|
||||
initializers: newInitializer("1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "everything",
|
||||
args: args{
|
||||
gvr: schema.GroupVersionResource{},
|
||||
initializers: newInitializer("1", v1alpha1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}}),
|
||||
},
|
||||
want: []string{"1"},
|
||||
},
|
||||
{
|
||||
name: "empty group",
|
||||
args: args{
|
||||
gvr: schema.GroupVersionResource{},
|
||||
initializers: newInitializer("1", v1alpha1.Rule{APIGroups: []string{""}, APIVersions: []string{"*"}, Resources: []string{"*"}}),
|
||||
},
|
||||
want: []string{"1"},
|
||||
},
|
||||
{
|
||||
name: "pod",
|
||||
args: args{
|
||||
gvr: schema.GroupVersionResource{Resource: "pods"},
|
||||
initializers: addInitializer(
|
||||
newInitializer("1", v1alpha1.Rule{APIGroups: []string{""}, APIVersions: []string{"*"}, Resources: []string{"pods"}}),
|
||||
"2", v1alpha1.Rule{APIGroups: []string{""}, APIVersions: []string{"*"}, Resources: []string{"pods"}},
|
||||
),
|
||||
},
|
||||
want: []string{"1", "2"},
|
||||
},
|
||||
{
|
||||
name: "multiple matches",
|
||||
args: args{
|
||||
gvr: schema.GroupVersionResource{Resource: "pods"},
|
||||
initializers: newInitializer("1", v1alpha1.Rule{APIGroups: []string{""}, APIVersions: []string{"*"}, Resources: []string{"pods"}}),
|
||||
},
|
||||
want: []string{"1"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := findInitializers(tt.args.initializers, tt.args.gvr); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("findInitializers() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
accept bool
|
||||
}
|
||||
|
||||
func (f *fakeAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if f.accept {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
return authorizer.DecisionNoOpinion, "denied", nil
|
||||
}
|
||||
|
||||
func TestAdmitUpdate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
oldInitializers *metav1.Initializers
|
||||
newInitializers *metav1.Initializers
|
||||
verifyUpdatedObj func(runtime.Object) (pass bool, reason string)
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "updates on initialized resources are allowed",
|
||||
oldInitializers: nil,
|
||||
newInitializers: nil,
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
name: "updates on initialized resources are allowed",
|
||||
oldInitializers: &metav1.Initializers{Pending: []metav1.Initializer{{Name: "init.k8s.io"}}},
|
||||
newInitializers: &metav1.Initializers{},
|
||||
verifyUpdatedObj: func(obj runtime.Object) (bool, string) {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return false, "cannot get accessor"
|
||||
}
|
||||
if accessor.GetInitializers() != nil {
|
||||
return false, "expect nil initializers"
|
||||
}
|
||||
return true, ""
|
||||
},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
name: "initializers may not be set once initialized",
|
||||
oldInitializers: nil,
|
||||
newInitializers: &metav1.Initializers{Pending: []metav1.Initializer{{Name: "init.k8s.io"}}},
|
||||
err: "field is immutable once initialization has completed",
|
||||
},
|
||||
{
|
||||
name: "empty initializer list is treated as nil initializer",
|
||||
oldInitializers: nil,
|
||||
newInitializers: &metav1.Initializers{},
|
||||
verifyUpdatedObj: func(obj runtime.Object) (bool, string) {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return false, "cannot get accessor"
|
||||
}
|
||||
if accessor.GetInitializers() != nil {
|
||||
return false, "expect nil initializers"
|
||||
}
|
||||
return true, ""
|
||||
},
|
||||
err: "",
|
||||
},
|
||||
}
|
||||
|
||||
plugin := initializer{
|
||||
config: nil,
|
||||
authorizer: &fakeAuthorizer{true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
oldObj := &v1.Pod{}
|
||||
oldObj.Initializers = tc.oldInitializers
|
||||
newObj := &v1.Pod{}
|
||||
newObj.Initializers = tc.newInitializers
|
||||
a := admission.NewAttributesRecord(newObj, oldObj, schema.GroupVersionKind{}, "", "foo", schema.GroupVersionResource{}, "", admission.Update, false, nil)
|
||||
err := plugin.Admit(a)
|
||||
switch {
|
||||
case tc.err == "" && err != nil:
|
||||
t.Errorf("%q: unexpected error: %v", tc.name, err)
|
||||
case tc.err != "" && err == nil:
|
||||
t.Errorf("%q: unexpected no error, expected %s", tc.name, tc.err)
|
||||
case tc.err != "" && err != nil && !strings.Contains(err.Error(), tc.err):
|
||||
t.Errorf("%q: expected %s, got %v", tc.name, tc.err, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -28,7 +28,6 @@ import (
|
|||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/initializer"
|
||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/initialization"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||
|
|
@ -79,8 +78,8 @@ func NewAdmissionOptions() *AdmissionOptions {
|
|||
// admission plugins. The apiserver always runs the validating ones
|
||||
// after all the mutating ones, so their relative order in this list
|
||||
// doesn't matter.
|
||||
RecommendedPluginOrder: []string{lifecycle.PluginName, initialization.PluginName, mutatingwebhook.PluginName, validatingwebhook.PluginName},
|
||||
DefaultOffPlugins: sets.NewString(initialization.PluginName),
|
||||
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, validatingwebhook.PluginName},
|
||||
DefaultOffPlugins: sets.NewString(),
|
||||
}
|
||||
server.RegisterAllAdmissionPlugins(options.Plugins)
|
||||
return options
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package server
|
|||
// This file exists to force the desired plugin implementations to be linked into genericapi pkg.
|
||||
import (
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/initialization"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||
|
|
@ -28,7 +27,6 @@ import (
|
|||
// RegisterAllAdmissionPlugins registers all admission plugins
|
||||
func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
lifecycle.Register(plugins)
|
||||
initialization.Register(plugins)
|
||||
validatingwebhook.Register(plugins)
|
||||
mutatingwebhook.Register(plugins)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue