move admission read logic

This commit is contained in:
deads2k
2017-02-06 13:15:16 -05:00
parent 844407e6ca
commit 58992ce8d2
10 changed files with 42 additions and 95 deletions

View File

@@ -0,0 +1,193 @@
/*
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/apimachinery/pkg/apimachinery/announced"
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/apis/apiserver/install"
apiserverv1alpha1 "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
)
var (
groupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
registry = registered.NewOrDie(os.Getenv("KUBE_API_VERSIONS"))
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
func init() {
install.Install(groupFactoryRegistry, registry, scheme)
}
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) (ConfigProvider, error) {
if configFilePath == "" {
return configProvider{config: &apiserver.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 := codecs.UniversalDecoder()
decodedObj, err := runtime.Decode(decoder, data)
// we were able to decode the file successfully
if err == nil {
decodedConfig, ok := decodedObj.(*apiserver.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 configProvider{config: 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 := &apiserverv1alpha1.AdmissionConfiguration{}
for _, pluginName := range pluginNames {
if legacyPluginsWithUnversionedConfig.Has(pluginName) {
externalConfig.Plugins = append(externalConfig.Plugins,
apiserverv1alpha1.AdmissionPluginConfiguration{
Name: pluginName,
Path: configFilePath})
}
}
scheme.Default(externalConfig)
internalConfig := &apiserver.AdmissionConfiguration{}
if err := scheme.Convert(externalConfig, internalConfig, nil); err != nil {
return nil, err
}
return configProvider{config: internalConfig}, nil
}
type configProvider struct {
config *apiserver.AdmissionConfiguration
}
// GetAdmissionPluginConfigurationFor returns a reader that holds the admission plugin configuration.
func GetAdmissionPluginConfigurationFor(pluginCfg apiserver.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 (p configProvider) ConfigFor(pluginName string) (io.Reader, error) {
// there is no config, so there is no potential config
if p.config == nil {
return nil, nil
}
// look for matching plugin and get configuration
for _, pluginCfg := range p.config.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(codecs.LegacyCodec(), obj)
if err != nil {
return nil, err
}
content, err := yaml.JSONToYAML(json)
if err != nil {
return nil, err
}
return content, err
}

View File

@@ -0,0 +1,147 @@
/*
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/apiserver/pkg/apis/apiserver"
)
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 *apiserver.AdmissionConfiguration
PluginNames []string
}{
"v1Alpha1 configuration - path fixup": {
ConfigBody: `{
"apiVersion": "apiserver.k8s.io/v1alpha1",
"kind": "AdmissionConfiguration",
"plugins": [
{"name": "ImagePolicyWebhook", "path": "image-policy-webhook.json"},
{"name": "ResourceQuota"}
]}`,
ExpectedAdmissionConfig: &apiserver.AdmissionConfiguration{
Plugins: []apiserver.AdmissionPluginConfiguration{
{
Name: "ImagePolicyWebhook",
Path: imagePolicyWebhookFile,
},
{
Name: "ResourceQuota",
},
},
},
PluginNames: []string{},
},
"v1Alpha1 configuration - abspath": {
ConfigBody: `{
"apiVersion": "apiserver.k8s.io/v1alpha1",
"kind": "AdmissionConfiguration",
"plugins": [
{"name": "ImagePolicyWebhook", "path": "/tmp/image-policy-webhook.json"},
{"name": "ResourceQuota"}
]}`,
ExpectedAdmissionConfig: &apiserver.AdmissionConfiguration{
Plugins: []apiserver.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: &apiserver.AdmissionConfiguration{
Plugins: []apiserver.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: &apiserver.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.(configProvider).config, testCase.ExpectedAdmissionConfig) {
t.Errorf("%s: Expected:\n\t%#v\nGot:\n\t%#v", testName, testCase.ExpectedAdmissionConfig, config.(configProvider).config)
}
}
}

View File

@@ -16,6 +16,6 @@ limitations under the License.
// +k8s:deepcopy-gen=package,register
// Package api is the internal version of the API.
// Package apiserver is the internal version of the API.
// +groupName=apiserver.k8s.io
package apiserver // import "k8s.io/apiserver/pkg/apis/apiserver"

View File

@@ -21,15 +21,10 @@ import (
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/apis"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
)
func init() {
Install(apis.GroupFactoryRegistry, apis.Registry, apis.Scheme)
}
// Install registers the API group and adds types to a scheme
func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *registered.APIRegistrationManager, scheme *runtime.Scheme) {
if err := announced.NewGroupMetaFactory(

View File

@@ -1,41 +0,0 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apis
import (
"os"
"k8s.io/apimachinery/pkg/apimachinery/announced"
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
// TODO all of these fall out when we move the admission read back into apiserver
// GroupFactoryRegistry is the APIGroupFactoryRegistry (overlaps a bit with Registry, see comments in package for details)
var GroupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
// Registry is an instance of an API registry. This is an interim step to start removing the idea of a global
// API registry.
var Registry = registered.NewOrDie(os.Getenv("KUBE_API_VERSIONS"))
// Scheme is the default instance of runtime.Scheme to which types in the Kubernetes API are already registered.
var Scheme = runtime.NewScheme()
// Codecs provides access to encoding and decoding for the scheme
var Codecs = serializer.NewCodecFactory(Scheme)