k8s.io/apiserver@v0.31.1/pkg/admission/config.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package admission
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  
    27  	"k8s.io/klog/v2"
    28  	"sigs.k8s.io/yaml"
    29  
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/serializer"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	"k8s.io/apiserver/pkg/apis/apiserver"
    34  	apiserverv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
    35  )
    36  
    37  func makeAbs(path, base string) (string, error) {
    38  	if filepath.IsAbs(path) {
    39  		return path, nil
    40  	}
    41  	if len(base) == 0 || base == "." {
    42  		cwd, err := os.Getwd()
    43  		if err != nil {
    44  			return "", err
    45  		}
    46  		base = cwd
    47  	}
    48  	return filepath.Join(base, path), nil
    49  }
    50  
    51  // ReadAdmissionConfiguration reads the admission configuration at the specified path.
    52  // It returns the loaded admission configuration if the input file aligns with the required syntax.
    53  // If it does not align with the provided syntax, it returns a default configuration for the enumerated
    54  // set of pluginNames whose config location references the specified configFilePath.
    55  // It does this to preserve backward compatibility when admission control files were opaque.
    56  // It returns an error if the file did not exist.
    57  func ReadAdmissionConfiguration(pluginNames []string, configFilePath string, configScheme *runtime.Scheme) (ConfigProvider, error) {
    58  	if configFilePath == "" {
    59  		return configProvider{config: &apiserver.AdmissionConfiguration{}}, nil
    60  	}
    61  	// a file was provided, so we just read it.
    62  	data, err := os.ReadFile(configFilePath)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("unable to read admission control configuration from %q [%v]", configFilePath, err)
    65  	}
    66  	codecs := serializer.NewCodecFactory(configScheme)
    67  	decoder := codecs.UniversalDecoder()
    68  	decodedObj, err := runtime.Decode(decoder, data)
    69  	// we were able to decode the file successfully
    70  	if err == nil {
    71  		decodedConfig, ok := decodedObj.(*apiserver.AdmissionConfiguration)
    72  		if !ok {
    73  			return nil, fmt.Errorf("unexpected type: %T", decodedObj)
    74  		}
    75  		baseDir := path.Dir(configFilePath)
    76  		for i := range decodedConfig.Plugins {
    77  			if decodedConfig.Plugins[i].Path == "" {
    78  				continue
    79  			}
    80  			// we update relative file paths to absolute paths
    81  			absPath, err := makeAbs(decodedConfig.Plugins[i].Path, baseDir)
    82  			if err != nil {
    83  				return nil, err
    84  			}
    85  			decodedConfig.Plugins[i].Path = absPath
    86  		}
    87  		return configProvider{
    88  			config: decodedConfig,
    89  		}, nil
    90  	}
    91  	// we got an error where the decode wasn't related to a missing type
    92  	if !(runtime.IsMissingVersion(err) || runtime.IsMissingKind(err) || runtime.IsNotRegisteredError(err)) {
    93  		return nil, err
    94  	}
    95  
    96  	// Only tolerate load errors if the file appears to be one of the two legacy plugin configs
    97  	unstructuredData := map[string]interface{}{}
    98  	if err2 := yaml.Unmarshal(data, &unstructuredData); err2 != nil {
    99  		return nil, err
   100  	}
   101  	_, isLegacyImagePolicy := unstructuredData["imagePolicy"]
   102  	_, isLegacyPodNodeSelector := unstructuredData["podNodeSelectorPluginConfig"]
   103  	if !isLegacyImagePolicy && !isLegacyPodNodeSelector {
   104  		return nil, err
   105  	}
   106  
   107  	// convert the legacy format to the new admission control format
   108  	// in order to preserve backwards compatibility, we set plugins that
   109  	// previously read input from a non-versioned file configuration to the
   110  	// current input file.
   111  	legacyPluginsWithUnversionedConfig := sets.NewString("ImagePolicyWebhook", "PodNodeSelector")
   112  	externalConfig := &apiserverv1.AdmissionConfiguration{}
   113  	for _, pluginName := range pluginNames {
   114  		if legacyPluginsWithUnversionedConfig.Has(pluginName) {
   115  			externalConfig.Plugins = append(externalConfig.Plugins,
   116  				apiserverv1.AdmissionPluginConfiguration{
   117  					Name: pluginName,
   118  					Path: configFilePath})
   119  		}
   120  	}
   121  	configScheme.Default(externalConfig)
   122  	internalConfig := &apiserver.AdmissionConfiguration{}
   123  	if err := configScheme.Convert(externalConfig, internalConfig, nil); err != nil {
   124  		return nil, err
   125  	}
   126  	return configProvider{
   127  		config: internalConfig,
   128  	}, nil
   129  }
   130  
   131  type configProvider struct {
   132  	config *apiserver.AdmissionConfiguration
   133  }
   134  
   135  // GetAdmissionPluginConfigurationFor returns a reader that holds the admission plugin configuration.
   136  func GetAdmissionPluginConfigurationFor(pluginCfg apiserver.AdmissionPluginConfiguration) (io.Reader, error) {
   137  	// if there is a nest object, return it directly
   138  	if pluginCfg.Configuration != nil {
   139  		return bytes.NewBuffer(pluginCfg.Configuration.Raw), nil
   140  	}
   141  	// there is nothing nested, so we delegate to path
   142  	if pluginCfg.Path != "" {
   143  		content, err := os.ReadFile(pluginCfg.Path)
   144  		if err != nil {
   145  			klog.Fatalf("Couldn't open admission plugin configuration %s: %#v", pluginCfg.Path, err)
   146  			return nil, err
   147  		}
   148  		return bytes.NewBuffer(content), nil
   149  	}
   150  	// there is no special config at all
   151  	return nil, nil
   152  }
   153  
   154  // ConfigFor returns a reader for the specified plugin.
   155  // If no specific configuration is present, we return a nil reader.
   156  func (p configProvider) ConfigFor(pluginName string) (io.Reader, error) {
   157  	// there is no config, so there is no potential config
   158  	if p.config == nil {
   159  		return nil, nil
   160  	}
   161  	// look for matching plugin and get configuration
   162  	for _, pluginCfg := range p.config.Plugins {
   163  		if pluginName != pluginCfg.Name {
   164  			continue
   165  		}
   166  		pluginConfig, err := GetAdmissionPluginConfigurationFor(pluginCfg)
   167  		if err != nil {
   168  			return nil, err
   169  		}
   170  		return pluginConfig, nil
   171  	}
   172  	// there is no registered config that matches on plugin name.
   173  	return nil, nil
   174  }