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