From b061c20e0ae7560fa7851f11aeb451753f320611 Mon Sep 17 00:00:00 2001 From: Derek Carr Date: Thu, 12 Jan 2017 16:46:47 -0500 Subject: [PATCH 1/3] Add versioned API for AdmissionConfiguration --- pkg/apis/componentconfig/register.go | 2 ++ pkg/apis/componentconfig/types.go | 26 ++++++++++++++++++ pkg/apis/componentconfig/v1alpha1/register.go | 2 ++ pkg/apis/componentconfig/v1alpha1/types.go | 27 +++++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/pkg/apis/componentconfig/register.go b/pkg/apis/componentconfig/register.go index 88f4c3b837..07ae793276 100644 --- a/pkg/apis/componentconfig/register.go +++ b/pkg/apis/componentconfig/register.go @@ -48,6 +48,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &KubeProxyConfiguration{}, &KubeSchedulerConfiguration{}, &KubeletConfiguration{}, + &AdmissionConfiguration{}, ) return nil } @@ -55,3 +56,4 @@ func addKnownTypes(scheme *runtime.Scheme) error { func (obj *KubeProxyConfiguration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } func (obj *KubeSchedulerConfiguration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } func (obj *KubeletConfiguration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } +func (obj *AdmissionConfiguration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } diff --git a/pkg/apis/componentconfig/types.go b/pkg/apis/componentconfig/types.go index 27512380c1..0ac32ecc4b 100644 --- a/pkg/apis/componentconfig/types.go +++ b/pkg/apis/componentconfig/types.go @@ -18,6 +18,7 @@ package componentconfig import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/api" utilconfig "k8s.io/kubernetes/pkg/util/config" ) @@ -828,3 +829,28 @@ type PersistentVolumeRecyclerConfiguration struct { // in a multi-node cluster. IncrementTimeoutHostPath int32 } + +// AdmissionConfiguration provides versioned configuration for admission controllers. +type AdmissionConfiguration struct { + metav1.TypeMeta + + // Plugins allows specifying a configuration per admission control plugin. + Plugins []AdmissionPluginConfiguration +} + +// AdmissionPluginConfiguration provides the configuration for a single plug-in. +type AdmissionPluginConfiguration struct { + // Name is the name of the admission controller. + // It must match the registered admission plugin name. + Name string + + // Path is the path to a configuration file that contains the plugin's + // configuration + // +optional + Path string + + // Configuration is an embedded configuration object to be used as the plugin's + // configuration. If present, it will be used instead of the path to the configuration file. + // +optional + Configuration runtime.Object +} diff --git a/pkg/apis/componentconfig/v1alpha1/register.go b/pkg/apis/componentconfig/v1alpha1/register.go index 94f0f07caf..ef2e0f5ec4 100644 --- a/pkg/apis/componentconfig/v1alpha1/register.go +++ b/pkg/apis/componentconfig/v1alpha1/register.go @@ -37,6 +37,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &KubeProxyConfiguration{}, &KubeSchedulerConfiguration{}, &KubeletConfiguration{}, + &AdmissionConfiguration{}, ) return nil } @@ -44,3 +45,4 @@ func addKnownTypes(scheme *runtime.Scheme) error { func (obj *KubeProxyConfiguration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } func (obj *KubeSchedulerConfiguration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } func (obj *KubeletConfiguration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } +func (obj *AdmissionConfiguration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } diff --git a/pkg/apis/componentconfig/v1alpha1/types.go b/pkg/apis/componentconfig/v1alpha1/types.go index 7c3b22547f..759194361d 100644 --- a/pkg/apis/componentconfig/v1alpha1/types.go +++ b/pkg/apis/componentconfig/v1alpha1/types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/api/v1" ) @@ -578,3 +579,29 @@ type KubeletAnonymousAuthentication struct { // Anonymous requests have a username of system:anonymous, and a group name of system:unauthenticated. Enabled *bool `json:"enabled"` } + +// AdmissionConfiguration provides versioned configuration for admission controllers. +type AdmissionConfiguration struct { + metav1.TypeMeta `json:",inline"` + + // Plugins allows specifying a configuration per admission control plugin. + // +optional + Plugins []AdmissionPluginConfiguration `json:"plugins"` +} + +// AdmissionPluginConfiguration provides the configuration for a single plug-in. +type AdmissionPluginConfiguration struct { + // Name is the name of the admission controller. + // It must match the registered admission plugin name. + Name string `json:"name"` + + // Path is the path to a configuration file that contains the plugin's + // configuration + // +optional + Path string `json:"path"` + + // Configuration is an embedded configuration object to be used as the plugin's + // configuration. If present, it will be used instead of the path to the configuration file. + // +optional + Configuration runtime.RawExtension `json:"configuration"` +} From a2a6b2184f3c767e47f0ab791b7541818cef64eb Mon Sep 17 00:00:00 2001 From: Derek Carr Date: Thu, 12 Jan 2017 16:47:13 -0500 Subject: [PATCH 2/3] Add generated artifacts for AdmissionConfiguration --- .../v1alpha1/zz_generated.conversion.go | 68 ++++++++++++++++++ .../v1alpha1/zz_generated.deepcopy.go | 34 +++++++++ .../componentconfig/zz_generated.deepcopy.go | 37 ++++++++++ pkg/generated/openapi/zz_generated.openapi.go | 70 +++++++++++++++++++ 4 files changed, 209 insertions(+) diff --git a/pkg/apis/componentconfig/v1alpha1/zz_generated.conversion.go b/pkg/apis/componentconfig/v1alpha1/zz_generated.conversion.go index a89819ae9b..9dc04b4984 100644 --- a/pkg/apis/componentconfig/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/componentconfig/v1alpha1/zz_generated.conversion.go @@ -38,6 +38,10 @@ func init() { // Public to allow building arbitrary schemes. func RegisterConversions(scheme *runtime.Scheme) error { return scheme.AddGeneratedConversionFuncs( + Convert_v1alpha1_AdmissionConfiguration_To_componentconfig_AdmissionConfiguration, + Convert_componentconfig_AdmissionConfiguration_To_v1alpha1_AdmissionConfiguration, + Convert_v1alpha1_AdmissionPluginConfiguration_To_componentconfig_AdmissionPluginConfiguration, + Convert_componentconfig_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginConfiguration, Convert_v1alpha1_KubeProxyConfiguration_To_componentconfig_KubeProxyConfiguration, Convert_componentconfig_KubeProxyConfiguration_To_v1alpha1_KubeProxyConfiguration, Convert_v1alpha1_KubeSchedulerConfiguration_To_componentconfig_KubeSchedulerConfiguration, @@ -61,6 +65,70 @@ func RegisterConversions(scheme *runtime.Scheme) error { ) } +func autoConvert_v1alpha1_AdmissionConfiguration_To_componentconfig_AdmissionConfiguration(in *AdmissionConfiguration, out *componentconfig.AdmissionConfiguration, s conversion.Scope) error { + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make([]componentconfig.AdmissionPluginConfiguration, len(*in)) + for i := range *in { + if err := Convert_v1alpha1_AdmissionPluginConfiguration_To_componentconfig_AdmissionPluginConfiguration(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Plugins = nil + } + return nil +} + +func Convert_v1alpha1_AdmissionConfiguration_To_componentconfig_AdmissionConfiguration(in *AdmissionConfiguration, out *componentconfig.AdmissionConfiguration, s conversion.Scope) error { + return autoConvert_v1alpha1_AdmissionConfiguration_To_componentconfig_AdmissionConfiguration(in, out, s) +} + +func autoConvert_componentconfig_AdmissionConfiguration_To_v1alpha1_AdmissionConfiguration(in *componentconfig.AdmissionConfiguration, out *AdmissionConfiguration, s conversion.Scope) error { + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make([]AdmissionPluginConfiguration, len(*in)) + for i := range *in { + if err := Convert_componentconfig_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginConfiguration(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Plugins = nil + } + return nil +} + +func Convert_componentconfig_AdmissionConfiguration_To_v1alpha1_AdmissionConfiguration(in *componentconfig.AdmissionConfiguration, out *AdmissionConfiguration, s conversion.Scope) error { + return autoConvert_componentconfig_AdmissionConfiguration_To_v1alpha1_AdmissionConfiguration(in, out, s) +} + +func autoConvert_v1alpha1_AdmissionPluginConfiguration_To_componentconfig_AdmissionPluginConfiguration(in *AdmissionPluginConfiguration, out *componentconfig.AdmissionPluginConfiguration, s conversion.Scope) error { + out.Name = in.Name + out.Path = in.Path + if err := runtime.Convert_runtime_RawExtension_To_runtime_Object(&in.Configuration, &out.Configuration, s); err != nil { + return err + } + return nil +} + +func Convert_v1alpha1_AdmissionPluginConfiguration_To_componentconfig_AdmissionPluginConfiguration(in *AdmissionPluginConfiguration, out *componentconfig.AdmissionPluginConfiguration, s conversion.Scope) error { + return autoConvert_v1alpha1_AdmissionPluginConfiguration_To_componentconfig_AdmissionPluginConfiguration(in, out, s) +} + +func autoConvert_componentconfig_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginConfiguration(in *componentconfig.AdmissionPluginConfiguration, out *AdmissionPluginConfiguration, s conversion.Scope) error { + out.Name = in.Name + out.Path = in.Path + if err := runtime.Convert_runtime_Object_To_runtime_RawExtension(&in.Configuration, &out.Configuration, s); err != nil { + return err + } + return nil +} + +func Convert_componentconfig_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginConfiguration(in *componentconfig.AdmissionPluginConfiguration, out *AdmissionPluginConfiguration, s conversion.Scope) error { + return autoConvert_componentconfig_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginConfiguration(in, out, s) +} + func autoConvert_v1alpha1_KubeProxyConfiguration_To_componentconfig_KubeProxyConfiguration(in *KubeProxyConfiguration, out *componentconfig.KubeProxyConfiguration, s conversion.Scope) error { out.BindAddress = in.BindAddress out.ClusterCIDR = in.ClusterCIDR diff --git a/pkg/apis/componentconfig/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/componentconfig/v1alpha1/zz_generated.deepcopy.go index aac12afc86..c3bf6ee9c8 100644 --- a/pkg/apis/componentconfig/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/componentconfig/v1alpha1/zz_generated.deepcopy.go @@ -35,6 +35,8 @@ func init() { // to allow building arbitrary schemes. func RegisterDeepCopies(scheme *runtime.Scheme) error { return scheme.AddGeneratedDeepCopyFuncs( + conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_AdmissionConfiguration, InType: reflect.TypeOf(&AdmissionConfiguration{})}, + conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_AdmissionPluginConfiguration, InType: reflect.TypeOf(&AdmissionPluginConfiguration{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_KubeProxyConfiguration, InType: reflect.TypeOf(&KubeProxyConfiguration{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_KubeSchedulerConfiguration, InType: reflect.TypeOf(&KubeSchedulerConfiguration{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_KubeletAnonymousAuthentication, InType: reflect.TypeOf(&KubeletAnonymousAuthentication{})}, @@ -48,6 +50,38 @@ func RegisterDeepCopies(scheme *runtime.Scheme) error { ) } +func DeepCopy_v1alpha1_AdmissionConfiguration(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*AdmissionConfiguration) + out := out.(*AdmissionConfiguration) + *out = *in + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make([]AdmissionPluginConfiguration, len(*in)) + for i := range *in { + if err := DeepCopy_v1alpha1_AdmissionPluginConfiguration(&(*in)[i], &(*out)[i], c); err != nil { + return err + } + } + } + return nil + } +} + +func DeepCopy_v1alpha1_AdmissionPluginConfiguration(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*AdmissionPluginConfiguration) + out := out.(*AdmissionPluginConfiguration) + *out = *in + if newVal, err := c.DeepCopy(&in.Configuration); err != nil { + return err + } else { + out.Configuration = *newVal.(*runtime.RawExtension) + } + return nil + } +} + func DeepCopy_v1alpha1_KubeProxyConfiguration(in interface{}, out interface{}, c *conversion.Cloner) error { { in := in.(*KubeProxyConfiguration) diff --git a/pkg/apis/componentconfig/zz_generated.deepcopy.go b/pkg/apis/componentconfig/zz_generated.deepcopy.go index fd0cb23d28..d6e913e8be 100644 --- a/pkg/apis/componentconfig/zz_generated.deepcopy.go +++ b/pkg/apis/componentconfig/zz_generated.deepcopy.go @@ -36,6 +36,8 @@ func init() { // to allow building arbitrary schemes. func RegisterDeepCopies(scheme *runtime.Scheme) error { return scheme.AddGeneratedDeepCopyFuncs( + conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_componentconfig_AdmissionConfiguration, InType: reflect.TypeOf(&AdmissionConfiguration{})}, + conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_componentconfig_AdmissionPluginConfiguration, InType: reflect.TypeOf(&AdmissionPluginConfiguration{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_componentconfig_IPVar, InType: reflect.TypeOf(&IPVar{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_componentconfig_KubeControllerManagerConfiguration, InType: reflect.TypeOf(&KubeControllerManagerConfiguration{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_componentconfig_KubeProxyConfiguration, InType: reflect.TypeOf(&KubeProxyConfiguration{})}, @@ -54,6 +56,41 @@ func RegisterDeepCopies(scheme *runtime.Scheme) error { ) } +func DeepCopy_componentconfig_AdmissionConfiguration(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*AdmissionConfiguration) + out := out.(*AdmissionConfiguration) + *out = *in + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make([]AdmissionPluginConfiguration, len(*in)) + for i := range *in { + if err := DeepCopy_componentconfig_AdmissionPluginConfiguration(&(*in)[i], &(*out)[i], c); err != nil { + return err + } + } + } + return nil + } +} + +func DeepCopy_componentconfig_AdmissionPluginConfiguration(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*AdmissionPluginConfiguration) + out := out.(*AdmissionPluginConfiguration) + *out = *in + // in.Configuration is kind 'Interface' + if in.Configuration != nil { + if newVal, err := c.DeepCopy(&in.Configuration); err != nil { + return err + } else { + out.Configuration = *newVal.(*runtime.Object) + } + } + return nil + } +} + func DeepCopy_componentconfig_IPVar(in interface{}, out interface{}, c *conversion.Cloner) error { { in := in.(*IPVar) diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index d50fd75f3b..17b052ae3b 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -8812,6 +8812,76 @@ var OpenAPIDefinitions *common.OpenAPIDefinitions = &common.OpenAPIDefinitions{ }, Dependencies: []string{}, }, + "v1alpha1.AdmissionConfiguration": { + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "AdmissionConfiguration provides versioned configuration for admission controllers.", + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "plugins": { + SchemaProps: spec.SchemaProps{ + Description: "Plugins allows specifying a configuration per admission control plugin.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef("#/definitions/v1alpha1.AdmissionPluginConfiguration"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "v1alpha1.AdmissionPluginConfiguration"}, + }, + "v1alpha1.AdmissionPluginConfiguration": { + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "AdmissionPluginConfiguration provides the configuration for a single plug-in.", + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name is the name of the admission controller. It must match the registered admission plugin name.", + Type: []string{"string"}, + Format: "", + }, + }, + "path": { + SchemaProps: spec.SchemaProps{ + Description: "Path is the path to a configuration file that contains the plugin's configuration", + Type: []string{"string"}, + Format: "", + }, + }, + "configuration": { + SchemaProps: spec.SchemaProps{ + Description: "Configuration is an embedded configuration object to be used as the plugin's configuration. If present, it will be used instead of the path to the configuration file.", + Ref: spec.MustCreateRef("#/definitions/runtime.RawExtension"), + }, + }, + }, + Required: []string{"name"}, + }, + }, + Dependencies: []string{ + "runtime.RawExtension"}, + }, "v1alpha1.CertificateSigningRequest": { Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ From 420906bbb86eb0a6ad65f07c8e951a44e2ddd08a Mon Sep 17 00:00:00 2001 From: Derek Carr Date: Thu, 12 Jan 2017 16:47:36 -0500 Subject: [PATCH 3/3] Add support for versioned configuration in admission --- pkg/admission/BUILD | 17 +- pkg/admission/config.go | 176 ++++++++++++++++++ pkg/admission/config_test.go | 148 +++++++++++++++ pkg/admission/plugins.go | 31 ++- plugin/pkg/admission/imagepolicy/admission.go | 1 + .../admission/initialresources/admission.go | 1 + .../admission/podnodeselector/admission.go | 1 + 7 files changed, 355 insertions(+), 20 deletions(-) create mode 100644 pkg/admission/config.go create mode 100644 pkg/admission/config_test.go diff --git a/pkg/admission/BUILD b/pkg/admission/BUILD index 96aaa1d13f..860adb876d 100644 --- a/pkg/admission/BUILD +++ b/pkg/admission/BUILD @@ -13,6 +13,7 @@ go_library( srcs = [ "attributes.go", "chain.go", + "config.go", "errors.go", "handler.go", "interfaces.go", @@ -20,7 +21,12 @@ go_library( ], tags = ["automanaged"], deps = [ + "//pkg/api:go_default_library", "//pkg/api/errors:go_default_library", + "//pkg/apis/componentconfig:go_default_library", + "//pkg/apis/componentconfig/v1alpha1:go_default_library", + "//pkg/util/sets:go_default_library", + "//vendor:github.com/ghodss/yaml", "//vendor:github.com/golang/glog", "//vendor:k8s.io/apimachinery/pkg/api/meta", "//vendor:k8s.io/apimachinery/pkg/runtime", @@ -33,10 +39,17 @@ go_library( go_test( name = "go_default_test", - srcs = ["chain_test.go"], + srcs = [ + "chain_test.go", + "config_test.go", + ], library = ":go_default_library", tags = ["automanaged"], - deps = ["//vendor:k8s.io/apimachinery/pkg/runtime/schema"], + deps = [ + "//pkg/apis/componentconfig:go_default_library", + "//pkg/apis/componentconfig/install:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/runtime/schema", + ], ) filegroup( diff --git a/pkg/admission/config.go b/pkg/admission/config.go new file mode 100644 index 0000000000..daf43e3e9a --- /dev/null +++ b/pkg/admission/config.go @@ -0,0 +1,176 @@ +/* +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 admission + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + + "github.com/ghodss/yaml" + "github.com/golang/glog" + + "bytes" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/componentconfig" + componentconfigv1alpha1 "k8s.io/kubernetes/pkg/apis/componentconfig/v1alpha1" + "k8s.io/kubernetes/pkg/util/sets" + + runtime "k8s.io/apimachinery/pkg/runtime" +) + +func makeAbs(path, base string) (string, error) { + if filepath.IsAbs(path) { + return path, nil + } + if len(base) == 0 || base == "." { + cwd, err := os.Getwd() + if err != nil { + return "", err + } + base = cwd + } + return filepath.Join(base, path), nil +} + +// ReadAdmissionConfiguration reads the admission configuration at the specified path. +// It returns the loaded admission configuration if the input file aligns with the required syntax. +// If it does not align with the provided syntax, it returns a default configuration for the enumerated +// set of pluginNames whose config location references the specified configFilePath. +// It does this to preserve backward compatibility when admission control files were opaque. +// It returns an error if the file did not exist. +func ReadAdmissionConfiguration(pluginNames []string, configFilePath string) (*componentconfig.AdmissionConfiguration, error) { + if configFilePath == "" { + return &componentconfig.AdmissionConfiguration{}, nil + } + // a file was provided, so we just read it. + data, err := ioutil.ReadFile(configFilePath) + if err != nil { + return nil, fmt.Errorf("unable to read admission control configuration from %q [%v]", configFilePath, err) + } + decoder := api.Codecs.UniversalDecoder() + decodedObj, err := runtime.Decode(decoder, data) + // we were able to decode the file successfully + if err == nil { + decodedConfig, ok := decodedObj.(*componentconfig.AdmissionConfiguration) + if !ok { + return nil, fmt.Errorf("unexpected type: %T", decodedObj) + } + baseDir := path.Dir(configFilePath) + for i := range decodedConfig.Plugins { + if decodedConfig.Plugins[i].Path == "" { + continue + } + // we update relative file paths to absolute paths + absPath, err := makeAbs(decodedConfig.Plugins[i].Path, baseDir) + if err != nil { + return nil, err + } + decodedConfig.Plugins[i].Path = absPath + } + return decodedConfig, nil + } + // we got an error where the decode wasn't related to a missing type + if !(runtime.IsMissingVersion(err) || runtime.IsMissingKind(err) || runtime.IsNotRegisteredError(err)) { + return nil, err + } + // convert the legacy format to the new admission control format + // in order to preserve backwards compatibility, we set plugins that + // previously read input from a non-versioned file configuration to the + // current input file. + legacyPluginsWithUnversionedConfig := sets.NewString("ImagePolicyWebhook", "PodNodeSelector") + externalConfig := &componentconfigv1alpha1.AdmissionConfiguration{} + for _, pluginName := range pluginNames { + if legacyPluginsWithUnversionedConfig.Has(pluginName) { + externalConfig.Plugins = append(externalConfig.Plugins, + componentconfigv1alpha1.AdmissionPluginConfiguration{ + Name: pluginName, + Path: configFilePath}) + } + } + api.Scheme.Default(externalConfig) + internalConfig := &componentconfig.AdmissionConfiguration{} + if err := api.Scheme.Convert(externalConfig, internalConfig, nil); err != nil { + return internalConfig, err + } + return internalConfig, nil +} + +// GetAdmissionPluginConfigurationFor returns a reader that holds the admission plugin configuration. +func GetAdmissionPluginConfigurationFor(pluginCfg componentconfig.AdmissionPluginConfiguration) (io.Reader, error) { + // if there is nothing nested in the object, we return the named location + obj := pluginCfg.Configuration + if obj != nil { + // serialize the configuration and build a reader for it + content, err := writeYAML(obj) + if err != nil { + return nil, err + } + return bytes.NewBuffer(content), nil + } + // there is nothing nested, so we delegate to path + if pluginCfg.Path != "" { + content, err := ioutil.ReadFile(pluginCfg.Path) + if err != nil { + glog.Fatalf("Couldn't open admission plugin configuration %s: %#v", pluginCfg.Path, err) + return nil, err + } + return bytes.NewBuffer(content), nil + } + // there is no special config at all + return nil, nil +} + +// GetAdmissionPluginConfiguration takes the admission configuration and returns a reader +// for the specified plugin. If no specific configuration is present, we return a nil reader. +func GetAdmissionPluginConfiguration(cfg *componentconfig.AdmissionConfiguration, pluginName string) (io.Reader, error) { + // there is no config, so there is no potential config + if cfg == nil { + return nil, nil + } + // look for matching plugin and get configuration + for _, pluginCfg := range cfg.Plugins { + if pluginName != pluginCfg.Name { + continue + } + pluginConfig, err := GetAdmissionPluginConfigurationFor(pluginCfg) + if err != nil { + return nil, err + } + return pluginConfig, nil + } + // there is no registered config that matches on plugin name. + return nil, nil +} + +// writeYAML writes the specified object to a byte array as yaml. +func writeYAML(obj runtime.Object) ([]byte, error) { + json, err := runtime.Encode(api.Codecs.LegacyCodec(), obj) + if err != nil { + return nil, err + } + + content, err := yaml.JSONToYAML(json) + if err != nil { + return nil, err + } + return content, err +} diff --git a/pkg/admission/config_test.go b/pkg/admission/config_test.go new file mode 100644 index 0000000000..edccd36256 --- /dev/null +++ b/pkg/admission/config_test.go @@ -0,0 +1,148 @@ +/* +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 admission + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/apis/componentconfig" + _ "k8s.io/kubernetes/pkg/apis/componentconfig/install" +) + +func TestReadAdmissionConfiguration(t *testing.T) { + // create a place holder file to hold per test config + configFile, err := ioutil.TempFile("", "admission-plugin-config") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err = configFile.Close(); err != nil { + t.Fatalf("unexpected err: %v", err) + } + configFileName := configFile.Name() + // the location that will be fixed up to be relative to the test config file. + imagePolicyWebhookFile, err := makeAbs("image-policy-webhook.json", os.TempDir()) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + // individual test scenarios + testCases := map[string]struct { + ConfigBody string + ExpectedAdmissionConfig *componentconfig.AdmissionConfiguration + PluginNames []string + }{ + "v1Alpha1 configuration - path fixup": { + ConfigBody: `{ +"apiVersion": "componentconfig/v1alpha1", +"kind": "AdmissionConfiguration", +"plugins": [ + {"name": "ImagePolicyWebhook", "path": "image-policy-webhook.json"}, + {"name": "ResourceQuota"} +]}`, + ExpectedAdmissionConfig: &componentconfig.AdmissionConfiguration{ + Plugins: []componentconfig.AdmissionPluginConfiguration{ + { + Name: "ImagePolicyWebhook", + Path: imagePolicyWebhookFile, + }, + { + Name: "ResourceQuota", + }, + }, + }, + PluginNames: []string{}, + }, + "v1Alpha1 configuration - abspath": { + ConfigBody: `{ +"apiVersion": "componentconfig/v1alpha1", +"kind": "AdmissionConfiguration", +"plugins": [ + {"name": "ImagePolicyWebhook", "path": "/tmp/image-policy-webhook.json"}, + {"name": "ResourceQuota"} +]}`, + ExpectedAdmissionConfig: &componentconfig.AdmissionConfiguration{ + Plugins: []componentconfig.AdmissionPluginConfiguration{ + { + Name: "ImagePolicyWebhook", + Path: "/tmp/image-policy-webhook.json", + }, + { + Name: "ResourceQuota", + }, + }, + }, + PluginNames: []string{}, + }, + "legacy configuration with using legacy plugins": { + ConfigBody: `{ +"imagePolicy": { + "kubeConfigFile": "/home/user/.kube/config", + "allowTTL": 30, + "denyTTL": 30, + "retryBackoff": 500, + "defaultAllow": true +}, +"podNodeSelectorPluginConfig": { + "clusterDefaultNodeSelector": "" +} +}`, + ExpectedAdmissionConfig: &componentconfig.AdmissionConfiguration{ + Plugins: []componentconfig.AdmissionPluginConfiguration{ + { + Name: "ImagePolicyWebhook", + Path: configFileName, + }, + { + Name: "PodNodeSelector", + Path: configFileName, + }, + }, + }, + PluginNames: []string{"ImagePolicyWebhook", "PodNodeSelector"}, + }, + "legacy configuration not using legacy plugins": { + ConfigBody: `{ +"imagePolicy": { + "kubeConfigFile": "/home/user/.kube/config", + "allowTTL": 30, + "denyTTL": 30, + "retryBackoff": 500, + "defaultAllow": true +}, +"podNodeSelectorPluginConfig": { + "clusterDefaultNodeSelector": "" +} +}`, + ExpectedAdmissionConfig: &componentconfig.AdmissionConfiguration{}, + PluginNames: []string{"NamespaceLifecycle", "InitialResources"}, + }, + } + for testName, testCase := range testCases { + if err = ioutil.WriteFile(configFileName, []byte(testCase.ConfigBody), 0644); err != nil { + t.Fatalf("unexpected err writing temp file: %v", err) + } + config, err := ReadAdmissionConfiguration(testCase.PluginNames, configFileName) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if !reflect.DeepEqual(config, testCase.ExpectedAdmissionConfig) { + t.Errorf("%s: Expected:\n\t%#v\nGot:\n\t%#v", testName, testCase.ExpectedAdmissionConfig, config) + } + } +} diff --git a/pkg/admission/plugins.go b/pkg/admission/plugins.go index 3656e9e950..5f557e55c2 100644 --- a/pkg/admission/plugins.go +++ b/pkg/admission/plugins.go @@ -21,7 +21,6 @@ import ( "fmt" "io" "io/ioutil" - "os" "reflect" "sort" "sync" @@ -115,9 +114,20 @@ func splitStream(config io.Reader) (io.Reader, io.Reader, error) { // NewFromPlugins returns an admission.Interface that will enforce admission control decisions of all // the given plugins. func NewFromPlugins(pluginNames []string, configFilePath string, pluginInitializer PluginInitializer) (Interface, error) { + // load config file path into a componentconfig.AdmissionConfiguration + admissionCfg, err := ReadAdmissionConfiguration(pluginNames, configFilePath) + if err != nil { + return nil, err + } + plugins := []Interface{} for _, pluginName := range pluginNames { - plugin, err := InitPlugin(pluginName, configFilePath, pluginInitializer) + pluginConfig, err := GetAdmissionPluginConfiguration(admissionCfg, pluginName) + if err != nil { + return nil, err + } + + plugin, err := InitPlugin(pluginName, pluginConfig, pluginInitializer) if err != nil { return nil, err } @@ -129,27 +139,12 @@ func NewFromPlugins(pluginNames []string, configFilePath string, pluginInitializ } // InitPlugin creates an instance of the named interface. -func InitPlugin(name string, configFilePath string, pluginInitializer PluginInitializer) (Interface, error) { - var ( - config *os.File - err error - ) - +func InitPlugin(name string, config io.Reader, pluginInitializer PluginInitializer) (Interface, error) { if name == "" { glog.Info("No admission plugin specified.") return nil, nil } - if configFilePath != "" { - config, err = os.Open(configFilePath) - if err != nil { - glog.Fatalf("Couldn't open admission plugin configuration %s: %#v", - configFilePath, err) - } - - defer config.Close() - } - plugin, found, err := getPlugin(name, config) if err != nil { return nil, fmt.Errorf("Couldn't init admission plugin %q: %v", name, err) diff --git a/plugin/pkg/admission/imagepolicy/admission.go b/plugin/pkg/admission/imagepolicy/admission.go index 6eb42dca7f..2907c7102c 100644 --- a/plugin/pkg/admission/imagepolicy/admission.go +++ b/plugin/pkg/admission/imagepolicy/admission.go @@ -214,6 +214,7 @@ func (a *imagePolicyWebhook) admitPod(attributes admission.Attributes, review *v // For additional HTTP configuration, refer to the kubeconfig documentation // http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html. func NewImagePolicyWebhook(configFile io.Reader) (admission.Interface, error) { + // TODO: move this to a versioned configuration file format var config AdmissionConfig d := yaml.NewYAMLOrJSONDecoder(configFile, 4096) err := d.Decode(&config) diff --git a/plugin/pkg/admission/initialresources/admission.go b/plugin/pkg/admission/initialresources/admission.go index 0183d1dc02..0f22a69f01 100644 --- a/plugin/pkg/admission/initialresources/admission.go +++ b/plugin/pkg/admission/initialresources/admission.go @@ -47,6 +47,7 @@ const ( // WARNING: this feature is experimental and will definitely change. func init() { admission.RegisterPlugin("InitialResources", func(config io.Reader) (admission.Interface, error) { + // TODO: remove the usage of flags in favor of reading versioned configuration s, err := newDataSource(*source) if err != nil { return nil, err diff --git a/plugin/pkg/admission/podnodeselector/admission.go b/plugin/pkg/admission/podnodeselector/admission.go index 8275a46cc8..9fbbcecb14 100644 --- a/plugin/pkg/admission/podnodeselector/admission.go +++ b/plugin/pkg/admission/podnodeselector/admission.go @@ -41,6 +41,7 @@ var NamespaceNodeSelectors = []string{"scheduler.alpha.kubernetes.io/node-select func init() { admission.RegisterPlugin("PodNodeSelector", func(config io.Reader) (admission.Interface, error) { + // TODO move this to a versioned configuration file format. pluginConfig := readConfig(config) plugin := NewPodNodeSelector(pluginConfig.PodNodeSelectorPluginConfig) return plugin, nil