mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-20 18:31:15 +00:00
Merge pull request #112050 from nilekhc/kms-hot-reload
Implements hot reload of the KMS `EncryptionConfiguration`
This commit is contained in:
commit
e62cfabf93
@ -402,7 +402,7 @@ func buildGenericConfig(
|
|||||||
} else {
|
} else {
|
||||||
s.Etcd.StorageConfig.Transport.TracerProvider = oteltrace.NewNoopTracerProvider()
|
s.Etcd.StorageConfig.Transport.TracerProvider = oteltrace.NewNoopTracerProvider()
|
||||||
}
|
}
|
||||||
if lastErr = s.Etcd.Complete(genericConfig.StorageObjectCountTracker, genericConfig.DrainedNotify()); lastErr != nil {
|
if lastErr = s.Etcd.Complete(genericConfig.StorageObjectCountTracker, genericConfig.DrainedNotify(), genericConfig.AddPostStartHook); lastErr != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ func setUp(t *testing.T) (*etcd3testing.EtcdTestServer, Config, *assert.Assertio
|
|||||||
etcdOptions := options.NewEtcdOptions(storageConfig)
|
etcdOptions := options.NewEtcdOptions(storageConfig)
|
||||||
// unit tests don't need watch cache and it leaks lots of goroutines with etcd testing functions during unit tests
|
// unit tests don't need watch cache and it leaks lots of goroutines with etcd testing functions during unit tests
|
||||||
etcdOptions.EnableWatchCache = false
|
etcdOptions.EnableWatchCache = false
|
||||||
if err := etcdOptions.Complete(config.GenericConfig.StorageObjectCountTracker, config.GenericConfig.DrainedNotify()); err != nil {
|
if err := etcdOptions.Complete(config.GenericConfig.StorageObjectCountTracker, config.GenericConfig.DrainedNotify(), config.GenericConfig.AddPostStartHook); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err := etcdOptions.ApplyWithStorageFactoryTo(storageFactory, config.GenericConfig)
|
err := etcdOptions.ApplyWithStorageFactoryTo(storageFactory, config.GenericConfig)
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -27,6 +28,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -59,6 +61,7 @@ const (
|
|||||||
kmsPluginHealthzPositiveTTL = 20 * time.Second
|
kmsPluginHealthzPositiveTTL = 20 * time.Second
|
||||||
kmsAPIVersionV1 = "v1"
|
kmsAPIVersionV1 = "v1"
|
||||||
kmsAPIVersionV2 = "v2"
|
kmsAPIVersionV2 = "v2"
|
||||||
|
kmsReloadHealthCheckName = "kms-providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type kmsPluginHealthzResponse struct {
|
type kmsPluginHealthzResponse struct {
|
||||||
@ -85,7 +88,7 @@ type kmsv2PluginProbe struct {
|
|||||||
type kmsHealthChecker []healthz.HealthChecker
|
type kmsHealthChecker []healthz.HealthChecker
|
||||||
|
|
||||||
func (k kmsHealthChecker) Name() string {
|
func (k kmsHealthChecker) Name() string {
|
||||||
return "kms-providers"
|
return kmsReloadHealthCheckName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k kmsHealthChecker) Check(req *http.Request) error {
|
func (k kmsHealthChecker) Check(req *http.Request) error {
|
||||||
@ -113,25 +116,51 @@ func (h *kmsv2PluginProbe) toHealthzCheck(idx int) healthz.HealthChecker {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EncryptionConfiguration represents the parsed and normalized encryption configuration for the apiserver.
|
||||||
|
type EncryptionConfiguration struct {
|
||||||
|
// Transformers is a list of value.Transformer that will be used to encrypt and decrypt data.
|
||||||
|
Transformers map[schema.GroupResource]value.Transformer
|
||||||
|
|
||||||
|
// HealthChecks is a list of healthz.HealthChecker that will be used to check the health of the encryption providers.
|
||||||
|
HealthChecks []healthz.HealthChecker
|
||||||
|
|
||||||
|
// EncryptionFileContentHash is the hash of the encryption config file.
|
||||||
|
EncryptionFileContentHash string
|
||||||
|
|
||||||
|
// KMSCloseGracePeriod is the duration we will wait before closing old transformers.
|
||||||
|
// We wait for any in-flight requests to finish by using the duration which is longer than their timeout.
|
||||||
|
KMSCloseGracePeriod time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
// LoadEncryptionConfig parses and validates the encryption config specified by filepath.
|
// LoadEncryptionConfig parses and validates the encryption config specified by filepath.
|
||||||
// It may launch multiple go routines whose lifecycle is controlled by stopCh.
|
// It may launch multiple go routines whose lifecycle is controlled by stopCh.
|
||||||
// If reload is true, or KMS v2 plugins are used with no KMS v1 plugins, the returned slice of health checkers will always be of length 1.
|
// If reload is true, or KMS v2 plugins are used with no KMS v1 plugins, the returned slice of health checkers will always be of length 1.
|
||||||
func LoadEncryptionConfig(filepath string, reload bool, stopCh <-chan struct{}) (map[schema.GroupResource]value.Transformer, []healthz.HealthChecker, error) {
|
func LoadEncryptionConfig(filepath string, reload bool, stopCh <-chan struct{}) (*EncryptionConfiguration, error) {
|
||||||
config, err := loadConfig(filepath, reload)
|
config, contentHash, err := loadConfig(filepath, reload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("error while parsing file: %w", err)
|
return nil, fmt.Errorf("error while parsing file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
transformers, kmsHealthChecks, kmsUsed, err := getTransformerOverridesAndKMSPluginHealthzCheckers(config, stopCh)
|
transformers, kmsHealthChecks, kmsUsed, err := getTransformerOverridesAndKMSPluginHealthzCheckers(config, stopCh)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("error while building transformers: %w", err)
|
return nil, fmt.Errorf("error while building transformers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if reload || (kmsUsed.v2Used && !kmsUsed.v1Used) {
|
if reload || (kmsUsed.v2Used && !kmsUsed.v1Used) {
|
||||||
kmsHealthChecks = []healthz.HealthChecker{kmsHealthChecker(kmsHealthChecks)}
|
kmsHealthChecks = []healthz.HealthChecker{kmsHealthChecker(kmsHealthChecks)}
|
||||||
}
|
}
|
||||||
|
|
||||||
return transformers, kmsHealthChecks, nil
|
// KMSTimeout is the duration we will wait before closing old transformers.
|
||||||
|
// The way we calculate is as follows:
|
||||||
|
// 1. Sum all timeouts across all KMS plugins. (check kmsPrefixTransformer for differences between v1 and v2)
|
||||||
|
// 2. Multiply that by 2 (to allow for some buffer)
|
||||||
|
// The reason we sum all timeout is because kmsHealthChecker() will run all health checks serially
|
||||||
|
return &EncryptionConfiguration{
|
||||||
|
Transformers: transformers,
|
||||||
|
HealthChecks: kmsHealthChecks,
|
||||||
|
EncryptionFileContentHash: contentHash,
|
||||||
|
KMSCloseGracePeriod: 2 * kmsUsed.kmsTimeoutSum,
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTransformerOverridesAndKMSPluginHealthzCheckers(config *apiserverconfig.EncryptionConfiguration, stopCh <-chan struct{}) (map[schema.GroupResource]value.Transformer, []healthz.HealthChecker, *kmsState, error) {
|
func getTransformerOverridesAndKMSPluginHealthzCheckers(config *apiserverconfig.EncryptionConfiguration, stopCh <-chan struct{}) (map[schema.GroupResource]value.Transformer, []healthz.HealthChecker, *kmsState, error) {
|
||||||
@ -168,6 +197,8 @@ func getTransformerOverridesAndKMSPluginProbes(config *apiserverconfig.Encryptio
|
|||||||
kmsUsed.v1Used = kmsUsed.v1Used || used.v1Used
|
kmsUsed.v1Used = kmsUsed.v1Used || used.v1Used
|
||||||
kmsUsed.v2Used = kmsUsed.v2Used || used.v2Used
|
kmsUsed.v2Used = kmsUsed.v2Used || used.v2Used
|
||||||
|
|
||||||
|
kmsUsed.kmsTimeoutSum += used.kmsTimeoutSum
|
||||||
|
|
||||||
// For each resource, create a list of providers to use
|
// For each resource, create a list of providers to use
|
||||||
for _, resource := range resourceConfig.Resources {
|
for _, resource := range resourceConfig.Resources {
|
||||||
resource := resource
|
resource := resource
|
||||||
@ -262,19 +293,20 @@ func isKMSv2ProviderHealthy(name string, response *envelopekmsv2.StatusResponse)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(filepath string, reload bool) (*apiserverconfig.EncryptionConfiguration, error) {
|
// loadConfig parses the encryption configuration file at filepath and returns the parsed config and hash of the file.
|
||||||
|
func loadConfig(filepath string, reload bool) (*apiserverconfig.EncryptionConfiguration, string, error) {
|
||||||
f, err := os.Open(filepath)
|
f, err := os.Open(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error opening encryption provider configuration file %q: %w", filepath, err)
|
return nil, "", fmt.Errorf("error opening encryption provider configuration file %q: %w", filepath, err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
data, err := io.ReadAll(f)
|
data, err := io.ReadAll(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not read contents: %w", err)
|
return nil, "", fmt.Errorf("could not read contents: %w", err)
|
||||||
}
|
}
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil, fmt.Errorf("encryption provider configuration file %q is empty", filepath)
|
return nil, "", fmt.Errorf("encryption provider configuration file %q is empty", filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
@ -284,14 +316,14 @@ func loadConfig(filepath string, reload bool) (*apiserverconfig.EncryptionConfig
|
|||||||
|
|
||||||
configObj, gvk, err := codecs.UniversalDecoder().Decode(data, nil, nil)
|
configObj, gvk, err := codecs.UniversalDecoder().Decode(data, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
config, ok := configObj.(*apiserverconfig.EncryptionConfiguration)
|
config, ok := configObj.(*apiserverconfig.EncryptionConfiguration)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("got unexpected config type: %v", gvk)
|
return nil, "", fmt.Errorf("got unexpected config type: %v", gvk)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, validation.ValidateEncryptionConfiguration(config, reload).ToAggregate()
|
return config, computeEncryptionConfigHash(data), validation.ValidateEncryptionConfiguration(config, reload).ToAggregate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func prefixTransformersAndProbes(config apiserverconfig.ResourceConfiguration, stopCh <-chan struct{}) ([]value.PrefixTransformer, []healthChecker, *kmsState, error) {
|
func prefixTransformersAndProbes(config apiserverconfig.ResourceConfiguration, stopCh <-chan struct{}) ([]value.PrefixTransformer, []healthChecker, *kmsState, error) {
|
||||||
@ -324,6 +356,9 @@ func prefixTransformersAndProbes(config apiserverconfig.ResourceConfiguration, s
|
|||||||
probes = append(probes, probe)
|
probes = append(probes, probe)
|
||||||
kmsUsed.v1Used = kmsUsed.v1Used || used.v1Used
|
kmsUsed.v1Used = kmsUsed.v1Used || used.v1Used
|
||||||
kmsUsed.v2Used = kmsUsed.v2Used || used.v2Used
|
kmsUsed.v2Used = kmsUsed.v2Used || used.v2Used
|
||||||
|
|
||||||
|
// calculate the maximum timeout for all KMS providers
|
||||||
|
kmsUsed.kmsTimeoutSum += used.kmsTimeoutSum
|
||||||
}
|
}
|
||||||
|
|
||||||
case provider.Identity != nil:
|
case provider.Identity != nil:
|
||||||
@ -459,6 +494,7 @@ var (
|
|||||||
|
|
||||||
type kmsState struct {
|
type kmsState struct {
|
||||||
v1Used, v2Used bool
|
v1Used, v2Used bool
|
||||||
|
kmsTimeoutSum time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func kmsPrefixTransformer(config *apiserverconfig.KMSConfiguration, stopCh <-chan struct{}) (value.PrefixTransformer, healthChecker, *kmsState, error) {
|
func kmsPrefixTransformer(config *apiserverconfig.KMSConfiguration, stopCh <-chan struct{}) (value.PrefixTransformer, healthChecker, *kmsState, error) {
|
||||||
@ -483,7 +519,11 @@ func kmsPrefixTransformer(config *apiserverconfig.KMSConfiguration, stopCh <-cha
|
|||||||
|
|
||||||
transformer := envelopePrefixTransformer(config, envelopeService, kmsTransformerPrefixV1)
|
transformer := envelopePrefixTransformer(config, envelopeService, kmsTransformerPrefixV1)
|
||||||
|
|
||||||
return transformer, probe, &kmsState{v1Used: true}, nil
|
return transformer, probe, &kmsState{
|
||||||
|
v1Used: true,
|
||||||
|
// for v1 we will do encrypt and decrypt for health check. Since these are serial operations, we will double the timeout.
|
||||||
|
kmsTimeoutSum: 2 * config.Timeout.Duration,
|
||||||
|
}, nil
|
||||||
|
|
||||||
case kmsAPIVersionV2:
|
case kmsAPIVersionV2:
|
||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.KMSv2) {
|
if !utilfeature.DefaultFeatureGate.Enabled(features.KMSv2) {
|
||||||
@ -509,7 +549,10 @@ func kmsPrefixTransformer(config *apiserverconfig.KMSConfiguration, stopCh <-cha
|
|||||||
Prefix: []byte(kmsTransformerPrefixV2 + kmsName + ":"),
|
Prefix: []byte(kmsTransformerPrefixV2 + kmsName + ":"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return transformer, probe, &kmsState{v2Used: true}, nil
|
return transformer, probe, &kmsState{
|
||||||
|
v2Used: true,
|
||||||
|
kmsTimeoutSum: config.Timeout.Duration,
|
||||||
|
}, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return value.PrefixTransformer{}, nil, nil, fmt.Errorf("could not configure KMS plugin %q, unsupported KMS API version %q", kmsName, config.APIVersion)
|
return value.PrefixTransformer{}, nil, nil, fmt.Errorf("could not configure KMS plugin %q, unsupported KMS API version %q", kmsName, config.APIVersion)
|
||||||
@ -555,3 +598,133 @@ func (u unionTransformers) TransformFromStorage(ctx context.Context, data []byte
|
|||||||
func (u unionTransformers) TransformToStorage(ctx context.Context, data []byte, dataCtx value.Context) (out []byte, err error) {
|
func (u unionTransformers) TransformToStorage(ctx context.Context, data []byte, dataCtx value.Context) (out []byte, err error) {
|
||||||
return u[0].TransformToStorage(ctx, data, dataCtx)
|
return u[0].TransformToStorage(ctx, data, dataCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// computeEncryptionConfigHash returns the expected hash for an encryption config file that has been loaded as bytes.
|
||||||
|
// We use a hash instead of the raw file contents when tracking changes to avoid holding any encryption keys in memory outside of their associated transformers.
|
||||||
|
// This hash must be used in-memory and not externalized to the process because it has no cross-release stability guarantees.
|
||||||
|
func computeEncryptionConfigHash(data []byte) string {
|
||||||
|
return fmt.Sprintf("%x", sha256.Sum256(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ healthz.HealthChecker = &DynamicTransformers{}
|
||||||
|
|
||||||
|
// DynamicTransformers holds transformers that may be dynamically updated via a single external actor, likely a controller.
|
||||||
|
// This struct must avoid locks (even read write locks) as it is inline to all calls to storage.
|
||||||
|
type DynamicTransformers struct {
|
||||||
|
transformTracker *atomic.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type transformTracker struct {
|
||||||
|
transformerOverrides map[schema.GroupResource]value.Transformer
|
||||||
|
kmsPluginHealthzCheck healthz.HealthChecker
|
||||||
|
closeTransformers context.CancelFunc
|
||||||
|
kmsCloseGracePeriod time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDynamicTransformers returns transformers, health checks for kms providers and an ability to close transformers.
|
||||||
|
func NewDynamicTransformers(
|
||||||
|
transformerOverrides map[schema.GroupResource]value.Transformer,
|
||||||
|
kmsPluginHealthzCheck healthz.HealthChecker,
|
||||||
|
closeTransformers context.CancelFunc,
|
||||||
|
kmsCloseGracePeriod time.Duration,
|
||||||
|
) *DynamicTransformers {
|
||||||
|
dynamicTransformers := &DynamicTransformers{
|
||||||
|
transformTracker: &atomic.Value{},
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker := &transformTracker{
|
||||||
|
transformerOverrides: transformerOverrides,
|
||||||
|
kmsPluginHealthzCheck: kmsPluginHealthzCheck,
|
||||||
|
closeTransformers: closeTransformers,
|
||||||
|
kmsCloseGracePeriod: kmsCloseGracePeriod,
|
||||||
|
}
|
||||||
|
dynamicTransformers.transformTracker.Store(tracker)
|
||||||
|
|
||||||
|
return dynamicTransformers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check implements healthz.HealthChecker
|
||||||
|
func (d *DynamicTransformers) Check(req *http.Request) error {
|
||||||
|
return d.transformTracker.Load().(*transformTracker).kmsPluginHealthzCheck.Check(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements healthz.HealthChecker
|
||||||
|
func (d *DynamicTransformers) Name() string {
|
||||||
|
return kmsReloadHealthCheckName
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformerForResource returns the transformer for the given resource.
|
||||||
|
func (d *DynamicTransformers) TransformerForResource(resource schema.GroupResource) value.Transformer {
|
||||||
|
return &resourceTransformer{
|
||||||
|
resource: resource,
|
||||||
|
transformTracker: d.transformTracker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the transformer overrides. This method is not go routine safe and must only be called by the same, single caller throughout the lifetime of this object.
|
||||||
|
func (d *DynamicTransformers) Set(
|
||||||
|
transformerOverrides map[schema.GroupResource]value.Transformer,
|
||||||
|
closeTransformers context.CancelFunc,
|
||||||
|
kmsPluginHealthzCheck healthz.HealthChecker,
|
||||||
|
kmsCloseGracePeriod time.Duration,
|
||||||
|
) {
|
||||||
|
// store new values
|
||||||
|
newTransformTracker := &transformTracker{
|
||||||
|
transformerOverrides: transformerOverrides,
|
||||||
|
closeTransformers: closeTransformers,
|
||||||
|
kmsPluginHealthzCheck: kmsPluginHealthzCheck,
|
||||||
|
kmsCloseGracePeriod: kmsCloseGracePeriod,
|
||||||
|
}
|
||||||
|
|
||||||
|
// update new transformer overrides
|
||||||
|
oldTransformTracker := d.transformTracker.Swap(newTransformTracker).(*transformTracker)
|
||||||
|
|
||||||
|
// close old transformers once we wait for grpc request to finish any in-flight requests.
|
||||||
|
// by the time we spawn this go routine, the new transformers have already been set and will be used for new requests.
|
||||||
|
// if the server starts shutting down during sleep duration then the transformers will correctly closed early because their lifetime is tied to the api-server drain notifier.
|
||||||
|
go func() {
|
||||||
|
time.Sleep(oldTransformTracker.kmsCloseGracePeriod)
|
||||||
|
oldTransformTracker.closeTransformers()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ value.Transformer = &resourceTransformer{}
|
||||||
|
|
||||||
|
type resourceTransformer struct {
|
||||||
|
resource schema.GroupResource
|
||||||
|
transformTracker *atomic.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resourceTransformer) TransformFromStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, bool, error) {
|
||||||
|
return r.transformer().TransformFromStorage(ctx, data, dataCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resourceTransformer) TransformToStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, error) {
|
||||||
|
return r.transformer().TransformToStorage(ctx, data, dataCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resourceTransformer) transformer() value.Transformer {
|
||||||
|
transformer := r.transformTracker.Load().(*transformTracker).transformerOverrides[r.resource]
|
||||||
|
if transformer == nil {
|
||||||
|
return identity.NewEncryptCheckTransformer()
|
||||||
|
}
|
||||||
|
return transformer
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceTransformers interface {
|
||||||
|
TransformerForResource(resource schema.GroupResource) value.Transformer
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ResourceTransformers = &DynamicTransformers{}
|
||||||
|
var _ ResourceTransformers = &StaticTransformers{}
|
||||||
|
|
||||||
|
type StaticTransformers map[schema.GroupResource]value.Transformer
|
||||||
|
|
||||||
|
// StaticTransformers
|
||||||
|
func (s StaticTransformers) TransformerForResource(resource schema.GroupResource) value.Transformer {
|
||||||
|
transformer := s[resource]
|
||||||
|
if transformer == nil {
|
||||||
|
return identity.NewEncryptCheckTransformer()
|
||||||
|
}
|
||||||
|
return transformer
|
||||||
|
}
|
||||||
|
@ -114,7 +114,7 @@ func newMockErrorEnvelopeKMSv2Service(endpoint string, timeout time.Duration) (e
|
|||||||
|
|
||||||
func TestLegacyConfig(t *testing.T) {
|
func TestLegacyConfig(t *testing.T) {
|
||||||
legacyV1Config := "testdata/valid-configs/legacy.yaml"
|
legacyV1Config := "testdata/valid-configs/legacy.yaml"
|
||||||
legacyConfigObject, err := loadConfig(legacyV1Config, false)
|
legacyConfigObject, _, err := loadConfig(legacyV1Config, false)
|
||||||
cacheSize := int32(10)
|
cacheSize := int32(10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, legacyV1Config)
|
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, legacyV1Config)
|
||||||
@ -177,48 +177,48 @@ func TestEncryptionProviderConfigCorrect(t *testing.T) {
|
|||||||
// Transforms data using one of them, and tries to untransform using the others.
|
// Transforms data using one of them, and tries to untransform using the others.
|
||||||
// Repeats this for all possible combinations.
|
// Repeats this for all possible combinations.
|
||||||
correctConfigWithIdentityFirst := "testdata/valid-configs/identity-first.yaml"
|
correctConfigWithIdentityFirst := "testdata/valid-configs/identity-first.yaml"
|
||||||
identityFirstTransformerOverrides, _, err := LoadEncryptionConfig(correctConfigWithIdentityFirst, false, ctx.Done())
|
identityFirstEncryptionConfiguration, err := LoadEncryptionConfig(correctConfigWithIdentityFirst, false, ctx.Done())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithIdentityFirst)
|
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithIdentityFirst)
|
||||||
}
|
}
|
||||||
|
|
||||||
correctConfigWithAesGcmFirst := "testdata/valid-configs/aes-gcm-first.yaml"
|
correctConfigWithAesGcmFirst := "testdata/valid-configs/aes-gcm-first.yaml"
|
||||||
aesGcmFirstTransformerOverrides, _, err := LoadEncryptionConfig(correctConfigWithAesGcmFirst, false, ctx.Done())
|
aesGcmFirstEncryptionConfiguration, err := LoadEncryptionConfig(correctConfigWithAesGcmFirst, false, ctx.Done())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithAesGcmFirst)
|
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithAesGcmFirst)
|
||||||
}
|
}
|
||||||
|
|
||||||
correctConfigWithAesCbcFirst := "testdata/valid-configs/aes-cbc-first.yaml"
|
correctConfigWithAesCbcFirst := "testdata/valid-configs/aes-cbc-first.yaml"
|
||||||
aesCbcFirstTransformerOverrides, _, err := LoadEncryptionConfig(correctConfigWithAesCbcFirst, false, ctx.Done())
|
aesCbcFirstEncryptionConfiguration, err := LoadEncryptionConfig(correctConfigWithAesCbcFirst, false, ctx.Done())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithAesCbcFirst)
|
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithAesCbcFirst)
|
||||||
}
|
}
|
||||||
|
|
||||||
correctConfigWithSecretboxFirst := "testdata/valid-configs/secret-box-first.yaml"
|
correctConfigWithSecretboxFirst := "testdata/valid-configs/secret-box-first.yaml"
|
||||||
secretboxFirstTransformerOverrides, _, err := LoadEncryptionConfig(correctConfigWithSecretboxFirst, false, ctx.Done())
|
secretboxFirstEncryptionConfiguration, err := LoadEncryptionConfig(correctConfigWithSecretboxFirst, false, ctx.Done())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithSecretboxFirst)
|
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithSecretboxFirst)
|
||||||
}
|
}
|
||||||
|
|
||||||
correctConfigWithKMSFirst := "testdata/valid-configs/kms-first.yaml"
|
correctConfigWithKMSFirst := "testdata/valid-configs/kms-first.yaml"
|
||||||
kmsFirstTransformerOverrides, _, err := LoadEncryptionConfig(correctConfigWithKMSFirst, false, ctx.Done())
|
kmsFirstEncryptionConfiguration, err := LoadEncryptionConfig(correctConfigWithKMSFirst, false, ctx.Done())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithKMSFirst)
|
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithKMSFirst)
|
||||||
}
|
}
|
||||||
|
|
||||||
correctConfigWithKMSv2First := "testdata/valid-configs/kmsv2-first.yaml"
|
correctConfigWithKMSv2First := "testdata/valid-configs/kmsv2-first.yaml"
|
||||||
kmsv2FirstTransformerOverrides, _, err := LoadEncryptionConfig(correctConfigWithKMSv2First, false, ctx.Done())
|
kmsv2FirstEncryptionConfiguration, err := LoadEncryptionConfig(correctConfigWithKMSv2First, false, ctx.Done())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithKMSv2First)
|
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithKMSv2First)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick the transformer for any of the returned resources.
|
// Pick the transformer for any of the returned resources.
|
||||||
identityFirstTransformer := identityFirstTransformerOverrides[schema.ParseGroupResource("secrets")]
|
identityFirstTransformer := identityFirstEncryptionConfiguration.Transformers[schema.ParseGroupResource("secrets")]
|
||||||
aesGcmFirstTransformer := aesGcmFirstTransformerOverrides[schema.ParseGroupResource("secrets")]
|
aesGcmFirstTransformer := aesGcmFirstEncryptionConfiguration.Transformers[schema.ParseGroupResource("secrets")]
|
||||||
aesCbcFirstTransformer := aesCbcFirstTransformerOverrides[schema.ParseGroupResource("secrets")]
|
aesCbcFirstTransformer := aesCbcFirstEncryptionConfiguration.Transformers[schema.ParseGroupResource("secrets")]
|
||||||
secretboxFirstTransformer := secretboxFirstTransformerOverrides[schema.ParseGroupResource("secrets")]
|
secretboxFirstTransformer := secretboxFirstEncryptionConfiguration.Transformers[schema.ParseGroupResource("secrets")]
|
||||||
kmsFirstTransformer := kmsFirstTransformerOverrides[schema.ParseGroupResource("secrets")]
|
kmsFirstTransformer := kmsFirstEncryptionConfiguration.Transformers[schema.ParseGroupResource("secrets")]
|
||||||
kmsv2FirstTransformer := kmsv2FirstTransformerOverrides[schema.ParseGroupResource("secrets")]
|
kmsv2FirstTransformer := kmsv2FirstEncryptionConfiguration.Transformers[schema.ParseGroupResource("secrets")]
|
||||||
|
|
||||||
dataCtx := value.DefaultContext([]byte(sampleContextText))
|
dataCtx := value.DefaultContext([]byte(sampleContextText))
|
||||||
originalText := []byte(sampleText)
|
originalText := []byte(sampleText)
|
||||||
@ -256,6 +256,222 @@ func TestEncryptionProviderConfigCorrect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestKMSMaxTimeout(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv2, true)()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
expectedTimeout time.Duration
|
||||||
|
config apiserverconfig.EncryptionConfiguration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default timeout",
|
||||||
|
config: apiserverconfig.EncryptionConfiguration{
|
||||||
|
Resources: []apiserverconfig.ResourceConfiguration{
|
||||||
|
{
|
||||||
|
Resources: []string{"secrets"},
|
||||||
|
Providers: []apiserverconfig.ProviderConfiguration{
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "kms",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
// default timeout is 3s
|
||||||
|
// this will be set automatically if not provided in config file
|
||||||
|
Duration: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/testprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTimeout: 6 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with v1 provider",
|
||||||
|
config: apiserverconfig.EncryptionConfiguration{
|
||||||
|
Resources: []apiserverconfig.ResourceConfiguration{
|
||||||
|
{
|
||||||
|
Resources: []string{"secrets"},
|
||||||
|
Providers: []apiserverconfig.ProviderConfiguration{
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "kms",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
// default timeout is 3s
|
||||||
|
// this will be set automatically if not provided in config file
|
||||||
|
Duration: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/testprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resources: []string{"configmaps"},
|
||||||
|
Providers: []apiserverconfig.ProviderConfiguration{
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "kms",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
// default timeout is 3s
|
||||||
|
// this will be set automatically if not provided in config file
|
||||||
|
Duration: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/testprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTimeout: 12 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with v2 provider",
|
||||||
|
config: apiserverconfig.EncryptionConfiguration{
|
||||||
|
Resources: []apiserverconfig.ResourceConfiguration{
|
||||||
|
{
|
||||||
|
Resources: []string{"secrets"},
|
||||||
|
Providers: []apiserverconfig.ProviderConfiguration{
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "kms",
|
||||||
|
APIVersion: "v2",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: 15 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/testprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "new-kms",
|
||||||
|
APIVersion: "v2",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: 5 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/anothertestprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resources: []string{"configmaps"},
|
||||||
|
Providers: []apiserverconfig.ProviderConfiguration{
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "another-kms",
|
||||||
|
APIVersion: "v2",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: 10 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/testprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "yet-another-kms",
|
||||||
|
APIVersion: "v2",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: 2 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/anothertestprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTimeout: 32 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with v1 and v2 provider",
|
||||||
|
config: apiserverconfig.EncryptionConfiguration{
|
||||||
|
Resources: []apiserverconfig.ResourceConfiguration{
|
||||||
|
{
|
||||||
|
Resources: []string{"secrets"},
|
||||||
|
Providers: []apiserverconfig.ProviderConfiguration{
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "kms",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: 1 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/testprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "another-kms",
|
||||||
|
APIVersion: "v2",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: 1 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/anothertestprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resources: []string{"configmaps"},
|
||||||
|
Providers: []apiserverconfig.ProviderConfiguration{
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "kms",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: 4 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/testprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
KMS: &apiserverconfig.KMSConfiguration{
|
||||||
|
Name: "yet-another-kms",
|
||||||
|
APIVersion: "v1",
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: 2 * time.Second,
|
||||||
|
},
|
||||||
|
Endpoint: "unix:///tmp/anothertestprovider.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTimeout: 15 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
cacheSize := int32(1000)
|
||||||
|
for _, resource := range testCase.config.Resources {
|
||||||
|
for _, provider := range resource.Providers {
|
||||||
|
if provider.KMS != nil {
|
||||||
|
provider.KMS.CacheSize = &cacheSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, kmsUsed, _ := getTransformerOverridesAndKMSPluginHealthzCheckers(&testCase.config, testContext(t).Done())
|
||||||
|
if kmsUsed == nil {
|
||||||
|
t.Fatal("kmsUsed should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if kmsUsed.kmsTimeoutSum != testCase.expectedTimeout {
|
||||||
|
t.Fatalf("expected timeout %v, got %v", testCase.expectedTimeout, kmsUsed.kmsTimeoutSum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestKMSPluginHealthz(t *testing.T) {
|
func TestKMSPluginHealthz(t *testing.T) {
|
||||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv2, true)()
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv2, true)()
|
||||||
|
|
||||||
@ -323,7 +539,7 @@ func TestKMSPluginHealthz(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
config, err := loadConfig(tt.config, false)
|
config, _, err := loadConfig(tt.config, false)
|
||||||
if errStr := errString(err); errStr != tt.wantErr {
|
if errStr := errString(err); errStr != tt.wantErr {
|
||||||
t.Fatalf("unexpected error state got=%s want=%s", errStr, tt.wantErr)
|
t.Fatalf("unexpected error state got=%s want=%s", errStr, tt.wantErr)
|
||||||
}
|
}
|
||||||
@ -541,14 +757,14 @@ func getTransformerFromEncryptionConfig(t *testing.T, encryptionConfigPath strin
|
|||||||
ctx := testContext(t)
|
ctx := testContext(t)
|
||||||
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
transformers, _, err := LoadEncryptionConfig(encryptionConfigPath, false, ctx.Done())
|
encryptionConfiguration, err := LoadEncryptionConfig(encryptionConfigPath, false, ctx.Done())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(transformers) != 1 {
|
if len(encryptionConfiguration.Transformers) != 1 {
|
||||||
t.Fatalf("input config does not have exactly one resource: %s", encryptionConfigPath)
|
t.Fatalf("input config does not have exactly one resource: %s", encryptionConfigPath)
|
||||||
}
|
}
|
||||||
for _, transformer := range transformers {
|
for _, transformer := range encryptionConfiguration.Transformers {
|
||||||
return transformer
|
return transformer
|
||||||
}
|
}
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
@ -602,3 +818,12 @@ func errString(err error) string {
|
|||||||
|
|
||||||
return err.Error()
|
return err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestComputeEncryptionConfigHash(t *testing.T) {
|
||||||
|
// hash the empty string to be sure that sha256 is being used
|
||||||
|
expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
sum := computeEncryptionConfigHash([]byte(""))
|
||||||
|
if expect != sum {
|
||||||
|
t.Errorf("expected hash %q but got %q", expect, sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,265 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apiserver/pkg/server/healthz"
|
||||||
|
"k8s.io/apiserver/pkg/server/options/encryptionconfig"
|
||||||
|
"k8s.io/client-go/util/workqueue"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// workqueueKey is the dummy key used to process change in encryption config file.
|
||||||
|
const workqueueKey = "key"
|
||||||
|
|
||||||
|
// DynamicKMSEncryptionConfigContent which can dynamically handle changes in encryption config file.
|
||||||
|
type DynamicKMSEncryptionConfigContent struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
// filePath is the path of the file to read.
|
||||||
|
filePath string
|
||||||
|
|
||||||
|
// lastLoadedEncryptionConfigHash stores last successfully read encryption config file content.
|
||||||
|
lastLoadedEncryptionConfigHash string
|
||||||
|
|
||||||
|
// queue for processing changes in encryption config file.
|
||||||
|
queue workqueue.RateLimitingInterface
|
||||||
|
|
||||||
|
// dynamicTransformers updates the transformers when encryption config file changes.
|
||||||
|
dynamicTransformers *encryptionconfig.DynamicTransformers
|
||||||
|
|
||||||
|
// stopCh used here is a lifecycle signal of genericapiserver already drained while shutting down.
|
||||||
|
stopCh <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDynamicKMSEncryptionConfiguration returns controller that dynamically reacts to changes in encryption config file.
|
||||||
|
func NewDynamicKMSEncryptionConfiguration(
|
||||||
|
name, filePath string,
|
||||||
|
dynamicTransformers *encryptionconfig.DynamicTransformers,
|
||||||
|
configContentHash string,
|
||||||
|
stopCh <-chan struct{},
|
||||||
|
) *DynamicKMSEncryptionConfigContent {
|
||||||
|
encryptionConfig := &DynamicKMSEncryptionConfigContent{
|
||||||
|
name: name,
|
||||||
|
filePath: filePath,
|
||||||
|
lastLoadedEncryptionConfigHash: configContentHash,
|
||||||
|
dynamicTransformers: dynamicTransformers,
|
||||||
|
stopCh: stopCh,
|
||||||
|
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), fmt.Sprintf("%s-hot-reload", name)),
|
||||||
|
}
|
||||||
|
encryptionConfig.queue.Add(workqueueKey)
|
||||||
|
|
||||||
|
return encryptionConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the controller and blocks until stopCh is closed.
|
||||||
|
func (d *DynamicKMSEncryptionConfigContent) Run(ctx context.Context) {
|
||||||
|
defer utilruntime.HandleCrash()
|
||||||
|
defer d.queue.ShutDown()
|
||||||
|
|
||||||
|
klog.InfoS("Starting controller", "name", d.name)
|
||||||
|
defer klog.InfoS("Shutting down controller", "name", d.name)
|
||||||
|
|
||||||
|
// start worker for processing content
|
||||||
|
go wait.Until(d.runWorker, time.Second, ctx.Done())
|
||||||
|
|
||||||
|
// start the loop that watches the encryption config file until stopCh is closed.
|
||||||
|
go wait.Until(func() {
|
||||||
|
if err := d.watchEncryptionConfigFile(ctx.Done()); err != nil {
|
||||||
|
// if there is an error while setting up or handling the watches, this will ensure that we will process the config file.
|
||||||
|
defer d.queue.Add(workqueueKey)
|
||||||
|
klog.ErrorS(err, "Failed to watch encryption config file, will retry later")
|
||||||
|
}
|
||||||
|
}, time.Second, ctx.Done())
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DynamicKMSEncryptionConfigContent) watchEncryptionConfigFile(stopCh <-chan struct{}) error {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating fsnotify watcher: %w", err)
|
||||||
|
}
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
|
if err = watcher.Add(d.filePath); err != nil {
|
||||||
|
return fmt.Errorf("error adding watch for file %s: %w", d.filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.Events:
|
||||||
|
if err := d.handleWatchEvent(event, watcher); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case err := <-watcher.Errors:
|
||||||
|
return fmt.Errorf("received fsnotify error: %w", err)
|
||||||
|
case <-stopCh:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DynamicKMSEncryptionConfigContent) handleWatchEvent(event fsnotify.Event, watcher *fsnotify.Watcher) error {
|
||||||
|
// This should be executed after restarting the watch (if applicable) to ensure no file event will be missing.
|
||||||
|
defer d.queue.Add(workqueueKey)
|
||||||
|
|
||||||
|
// return if file has not been removed or renamed.
|
||||||
|
if event.Op&(fsnotify.Remove|fsnotify.Rename) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := watcher.Remove(d.filePath); err != nil {
|
||||||
|
klog.V(2).InfoS("Failed to remove file watch, it may have been deleted", "file", d.filePath, "err", err)
|
||||||
|
}
|
||||||
|
if err := watcher.Add(d.filePath); err != nil {
|
||||||
|
return fmt.Errorf("error adding watch for file %s: %w", d.filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runWorker to process file content
|
||||||
|
func (d *DynamicKMSEncryptionConfigContent) runWorker() {
|
||||||
|
for d.processNextWorkItem() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processNextWorkItem processes file content when there is a message in the queue.
|
||||||
|
func (d *DynamicKMSEncryptionConfigContent) processNextWorkItem() bool {
|
||||||
|
// key here is dummy item in the queue to trigger file content processing.
|
||||||
|
key, quit := d.queue.Get()
|
||||||
|
if quit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer d.queue.Done(key)
|
||||||
|
|
||||||
|
var (
|
||||||
|
updatedEffectiveConfig bool
|
||||||
|
err error
|
||||||
|
encryptionConfiguration *encryptionconfig.EncryptionConfiguration
|
||||||
|
configChanged bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// get context to close the new transformers.
|
||||||
|
ctx, closeTransformers := wait.ContextForChannel(d.stopCh)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// TODO: increment success metric when updatedEffectiveConfig=true
|
||||||
|
|
||||||
|
if !updatedEffectiveConfig {
|
||||||
|
// avoid leaking if we're not using the newly constructed transformers (due to an error or them not being changed)
|
||||||
|
closeTransformers()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// TODO: increment failure metric
|
||||||
|
utilruntime.HandleError(fmt.Errorf("error processing encryption config file %s: %v", d.filePath, err))
|
||||||
|
// add dummy item back to the queue to trigger file content processing.
|
||||||
|
d.queue.AddRateLimited(key)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
encryptionConfiguration, configChanged, err = d.processEncryptionConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !configChanged {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(encryptionConfiguration.HealthChecks) != 1 {
|
||||||
|
err = fmt.Errorf("unexpected number of healthz checks: %d. Should have only one", len(encryptionConfiguration.HealthChecks))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// get healthz checks for all new KMS plugins.
|
||||||
|
if err = d.validateNewTransformersHealth(ctx, encryptionConfiguration.HealthChecks[0], encryptionConfiguration.KMSCloseGracePeriod); err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// update transformers.
|
||||||
|
// when reload=true there must always be one healthz check.
|
||||||
|
d.dynamicTransformers.Set(
|
||||||
|
encryptionConfiguration.Transformers,
|
||||||
|
closeTransformers,
|
||||||
|
encryptionConfiguration.HealthChecks[0],
|
||||||
|
encryptionConfiguration.KMSCloseGracePeriod,
|
||||||
|
)
|
||||||
|
|
||||||
|
// update local copy of recent config content once update is successful.
|
||||||
|
d.lastLoadedEncryptionConfigHash = encryptionConfiguration.EncryptionFileContentHash
|
||||||
|
klog.V(2).InfoS("Loaded new kms encryption config content", "name", d.name)
|
||||||
|
|
||||||
|
updatedEffectiveConfig = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadEncryptionConfig processes the next set of content from the file.
|
||||||
|
func (d *DynamicKMSEncryptionConfigContent) processEncryptionConfig(ctx context.Context) (
|
||||||
|
encryptionConfiguration *encryptionconfig.EncryptionConfiguration,
|
||||||
|
configChanged bool,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
// this code path will only execute if reload=true. So passing true explicitly.
|
||||||
|
encryptionConfiguration, err = encryptionconfig.LoadEncryptionConfig(d.filePath, true, ctx.Done())
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if encryptionConfig is different from the current. Do nothing if they are the same.
|
||||||
|
if encryptionConfiguration.EncryptionFileContentHash == d.lastLoadedEncryptionConfigHash {
|
||||||
|
klog.V(4).InfoS("Encryption config has not changed", "name", d.name)
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return encryptionConfiguration, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DynamicKMSEncryptionConfigContent) validateNewTransformersHealth(
|
||||||
|
ctx context.Context,
|
||||||
|
kmsPluginHealthzCheck healthz.HealthChecker,
|
||||||
|
kmsPluginCloseGracePeriod time.Duration,
|
||||||
|
) error {
|
||||||
|
// test if new transformers are healthy
|
||||||
|
var healthCheckError error
|
||||||
|
|
||||||
|
if kmsPluginCloseGracePeriod < 10*time.Second {
|
||||||
|
kmsPluginCloseGracePeriod = 10 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
pollErr := wait.PollImmediate(100*time.Millisecond, kmsPluginCloseGracePeriod, func() (bool, error) {
|
||||||
|
// create a fake http get request to health check endpoint
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("/healthz/%s", kmsPluginHealthzCheck.Name()), nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
healthCheckError = kmsPluginHealthzCheck.Check(req)
|
||||||
|
return healthCheckError == nil, nil
|
||||||
|
})
|
||||||
|
if pollErr != nil {
|
||||||
|
return fmt.Errorf("health check for new transformers failed, polling error %v: %w", pollErr, healthCheckError)
|
||||||
|
}
|
||||||
|
klog.V(2).InfoS("Health check succeeded")
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProcessEncryptionConfig(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
filePath string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty config file",
|
||||||
|
filePath: "testdata/empty_config.yaml",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
d := NewDynamicKMSEncryptionConfiguration(
|
||||||
|
testCase.name,
|
||||||
|
testCase.filePath,
|
||||||
|
nil,
|
||||||
|
"",
|
||||||
|
ctx.Done(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, _, err := d.processEncryptionConfig(ctx)
|
||||||
|
if testCase.expectError && err == nil {
|
||||||
|
t.Fatalf("expected error but got none")
|
||||||
|
}
|
||||||
|
if !testCase.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatchEncryptionConfigFile(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
generateEvent func(filePath string, cancel context.CancelFunc)
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "file not renamed or removed",
|
||||||
|
expectError: false,
|
||||||
|
generateEvent: func(filePath string, cancel context.CancelFunc) {
|
||||||
|
os.Chtimes(filePath, time.Now(), time.Now())
|
||||||
|
|
||||||
|
// wait for the event to be handled
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
cancel()
|
||||||
|
os.Remove(filePath)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file renamed",
|
||||||
|
expectError: true,
|
||||||
|
generateEvent: func(filePath string, cancel context.CancelFunc) {
|
||||||
|
os.Rename(filePath, filePath+"1")
|
||||||
|
|
||||||
|
// wait for the event to be handled
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
os.Remove(filePath + "1")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file removed",
|
||||||
|
expectError: true,
|
||||||
|
generateEvent: func(filePath string, cancel context.CancelFunc) {
|
||||||
|
// allow watcher handle to start
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
os.Remove(filePath)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
testFilePath := copyFileForTest(t, "testdata/ec_config.yaml")
|
||||||
|
|
||||||
|
d := NewDynamicKMSEncryptionConfiguration(
|
||||||
|
testCase.name,
|
||||||
|
testFilePath,
|
||||||
|
nil,
|
||||||
|
"",
|
||||||
|
ctx.Done(),
|
||||||
|
)
|
||||||
|
|
||||||
|
errs := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
err := d.watchEncryptionConfigFile(d.stopCh)
|
||||||
|
errs <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
testCase.generateEvent(d.filePath, cancel)
|
||||||
|
|
||||||
|
err := <-errs
|
||||||
|
if testCase.expectError && err == nil {
|
||||||
|
t.Fatalf("expected error but got none")
|
||||||
|
}
|
||||||
|
if !testCase.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFileForTest(t *testing.T, srcFilePath string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// get directory from source file path
|
||||||
|
srcDir := filepath.Dir(srcFilePath)
|
||||||
|
|
||||||
|
// get file name from source file path
|
||||||
|
srcFileName := filepath.Base(srcFilePath)
|
||||||
|
|
||||||
|
// set new file path
|
||||||
|
dstFilePath := filepath.Join(srcDir, "test_"+srcFileName)
|
||||||
|
|
||||||
|
// copy src file to dst file
|
||||||
|
r, err := os.Open(srcFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open source file: %v", err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
w, err := os.Create(dstFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create destination file: %v", err)
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
// copy the file
|
||||||
|
_, err = io.Copy(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to copy file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to close destination file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dstFilePath
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
kind: EncryptionConfiguration
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- kms:
|
||||||
|
name: foo
|
||||||
|
endpoint: unix:///tmp/testprovider.sock
|
@ -27,15 +27,16 @@ import (
|
|||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
"k8s.io/apiserver/pkg/server"
|
"k8s.io/apiserver/pkg/server"
|
||||||
"k8s.io/apiserver/pkg/server/healthz"
|
"k8s.io/apiserver/pkg/server/healthz"
|
||||||
"k8s.io/apiserver/pkg/server/options/encryptionconfig"
|
"k8s.io/apiserver/pkg/server/options/encryptionconfig"
|
||||||
|
kmsconfigcontroller "k8s.io/apiserver/pkg/server/options/encryptionconfig/controller"
|
||||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||||
storagefactory "k8s.io/apiserver/pkg/storage/storagebackend/factory"
|
storagefactory "k8s.io/apiserver/pkg/storage/storagebackend/factory"
|
||||||
"k8s.io/apiserver/pkg/storage/value"
|
|
||||||
flowcontrolrequest "k8s.io/apiserver/pkg/util/flowcontrol/request"
|
flowcontrolrequest "k8s.io/apiserver/pkg/util/flowcontrol/request"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
@ -64,7 +65,7 @@ type EtcdOptions struct {
|
|||||||
|
|
||||||
// complete guards fields that must be initialized via Complete before the Apply methods can be used.
|
// complete guards fields that must be initialized via Complete before the Apply methods can be used.
|
||||||
complete bool
|
complete bool
|
||||||
transformerOverrides map[schema.GroupResource]value.Transformer
|
resourceTransformers encryptionconfig.ResourceTransformers
|
||||||
kmsPluginHealthzChecks []healthz.HealthChecker
|
kmsPluginHealthzChecks []healthz.HealthChecker
|
||||||
|
|
||||||
// SkipHealthEndpoints, when true, causes the Apply methods to not set up health endpoints.
|
// SkipHealthEndpoints, when true, causes the Apply methods to not set up health endpoints.
|
||||||
@ -125,7 +126,7 @@ func (s *EtcdOptions) Validate() []error {
|
|||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddEtcdFlags adds flags related to etcd storage for a specific APIServer to the specified FlagSet
|
// AddFlags adds flags related to etcd storage for a specific APIServer to the specified FlagSet
|
||||||
func (s *EtcdOptions) AddFlags(fs *pflag.FlagSet) {
|
func (s *EtcdOptions) AddFlags(fs *pflag.FlagSet) {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return
|
return
|
||||||
@ -213,7 +214,11 @@ func (s *EtcdOptions) AddFlags(fs *pflag.FlagSet) {
|
|||||||
// Complete must be called exactly once before using any of the Apply methods. It is responsible for setting
|
// Complete must be called exactly once before using any of the Apply methods. It is responsible for setting
|
||||||
// up objects that must be created once and reused across multiple invocations such as storage transformers.
|
// up objects that must be created once and reused across multiple invocations such as storage transformers.
|
||||||
// This method mutates the receiver (EtcdOptions). It must never mutate the inputs.
|
// This method mutates the receiver (EtcdOptions). It must never mutate the inputs.
|
||||||
func (s *EtcdOptions) Complete(storageObjectCountTracker flowcontrolrequest.StorageObjectCountTracker, stopCh <-chan struct{}) error {
|
func (s *EtcdOptions) Complete(
|
||||||
|
storageObjectCountTracker flowcontrolrequest.StorageObjectCountTracker,
|
||||||
|
stopCh <-chan struct{},
|
||||||
|
addPostStartHook func(name string, hook server.PostStartHookFunc) error,
|
||||||
|
) error {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -223,12 +228,56 @@ func (s *EtcdOptions) Complete(storageObjectCountTracker flowcontrolrequest.Stor
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(s.EncryptionProviderConfigFilepath) != 0 {
|
if len(s.EncryptionProviderConfigFilepath) != 0 {
|
||||||
transformerOverrides, kmsPluginHealthzChecks, err := encryptionconfig.LoadEncryptionConfig(s.EncryptionProviderConfigFilepath, s.EncryptionProviderConfigAutomaticReload, stopCh)
|
ctx, closeTransformers := wait.ContextForChannel(stopCh)
|
||||||
|
|
||||||
|
encryptionConfiguration, err := encryptionconfig.LoadEncryptionConfig(s.EncryptionProviderConfigFilepath, s.EncryptionProviderConfigAutomaticReload, ctx.Done())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// in case of error, we want to close partially initialized (if any) transformers
|
||||||
|
closeTransformers()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.transformerOverrides = transformerOverrides
|
|
||||||
s.kmsPluginHealthzChecks = kmsPluginHealthzChecks
|
// enable kms hot reload controller only if the config file is set to be automatically reloaded
|
||||||
|
if s.EncryptionProviderConfigAutomaticReload {
|
||||||
|
// with reload=true we will always have 1 health check
|
||||||
|
if len(encryptionConfiguration.HealthChecks) != 1 {
|
||||||
|
// in case of error, we want to close partially initialized (if any) transformers
|
||||||
|
closeTransformers()
|
||||||
|
return fmt.Errorf("failed to start kms encryption config hot reload controller. only 1 health check should be available when reload is enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicTransformers := encryptionconfig.NewDynamicTransformers(encryptionConfiguration.Transformers, encryptionConfiguration.HealthChecks[0], closeTransformers, encryptionConfiguration.KMSCloseGracePeriod)
|
||||||
|
|
||||||
|
s.resourceTransformers = dynamicTransformers
|
||||||
|
s.kmsPluginHealthzChecks = []healthz.HealthChecker{dynamicTransformers}
|
||||||
|
|
||||||
|
// add post start hook to start hot reload controller
|
||||||
|
// adding this hook here will ensure that it gets configured exactly once
|
||||||
|
err = addPostStartHook(
|
||||||
|
"start-encryption-provider-config-automatic-reload",
|
||||||
|
func(hookContext server.PostStartHookContext) error {
|
||||||
|
kmsConfigController := kmsconfigcontroller.NewDynamicKMSEncryptionConfiguration(
|
||||||
|
"kms-encryption-config",
|
||||||
|
s.EncryptionProviderConfigFilepath,
|
||||||
|
dynamicTransformers,
|
||||||
|
encryptionConfiguration.EncryptionFileContentHash,
|
||||||
|
ctx.Done(),
|
||||||
|
)
|
||||||
|
|
||||||
|
go kmsConfigController.Run(ctx)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// in case of error, we want to close partially initialized (if any) transformers
|
||||||
|
closeTransformers()
|
||||||
|
return fmt.Errorf("failed to add post start hook for kms encryption config hot reload controller: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.resourceTransformers = encryptionconfig.StaticTransformers(encryptionConfiguration.Transformers)
|
||||||
|
s.kmsPluginHealthzChecks = encryptionConfiguration.HealthChecks
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.StorageConfig.StorageObjectCountTracker = storageObjectCountTracker
|
s.StorageConfig.StorageObjectCountTracker = storageObjectCountTracker
|
||||||
@ -263,10 +312,10 @@ func (s *EtcdOptions) ApplyWithStorageFactoryTo(factory serverstorage.StorageFac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.transformerOverrides) > 0 {
|
if s.resourceTransformers != nil {
|
||||||
factory = &transformerStorageFactory{
|
factory = &transformerStorageFactory{
|
||||||
delegate: factory,
|
delegate: factory,
|
||||||
transformerOverrides: s.transformerOverrides,
|
resourceTransformers: s.resourceTransformers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,7 +449,7 @@ var _ serverstorage.StorageFactory = &transformerStorageFactory{}
|
|||||||
|
|
||||||
type transformerStorageFactory struct {
|
type transformerStorageFactory struct {
|
||||||
delegate serverstorage.StorageFactory
|
delegate serverstorage.StorageFactory
|
||||||
transformerOverrides map[schema.GroupResource]value.Transformer
|
resourceTransformers encryptionconfig.ResourceTransformers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *transformerStorageFactory) NewConfig(resource schema.GroupResource) (*storagebackend.ConfigForResource, error) {
|
func (t *transformerStorageFactory) NewConfig(resource schema.GroupResource) (*storagebackend.ConfigForResource, error) {
|
||||||
@ -409,14 +458,9 @@ func (t *transformerStorageFactory) NewConfig(resource schema.GroupResource) (*s
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
transformer, ok := t.transformerOverrides[resource]
|
|
||||||
if !ok {
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
configCopy := *config
|
configCopy := *config
|
||||||
resourceConfig := configCopy.Config
|
resourceConfig := configCopy.Config
|
||||||
resourceConfig.Transformer = transformer
|
resourceConfig.Transformer = t.resourceTransformers.TransformerForResource(resource)
|
||||||
configCopy.Config = resourceConfig
|
configCopy.Config = resourceConfig
|
||||||
|
|
||||||
return &configCopy, nil
|
return &configCopy, nil
|
||||||
|
@ -306,7 +306,7 @@ func TestKMSHealthzEndpoint(t *testing.T) {
|
|||||||
EncryptionProviderConfigAutomaticReload: tc.reload,
|
EncryptionProviderConfigAutomaticReload: tc.reload,
|
||||||
SkipHealthEndpoints: tc.skipHealth,
|
SkipHealthEndpoints: tc.skipHealth,
|
||||||
}
|
}
|
||||||
if err := etcdOptions.Complete(serverConfig.StorageObjectCountTracker, serverConfig.DrainedNotify()); err != nil {
|
if err := etcdOptions.Complete(serverConfig.StorageObjectCountTracker, serverConfig.DrainedNotify(), serverConfig.AddPostStartHook); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := etcdOptions.ApplyTo(serverConfig); err != nil {
|
if err := etcdOptions.ApplyTo(serverConfig); err != nil {
|
||||||
@ -345,7 +345,7 @@ func TestReadinessCheck(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
serverConfig := server.NewConfig(codecs)
|
serverConfig := server.NewConfig(codecs)
|
||||||
etcdOptions := &EtcdOptions{SkipHealthEndpoints: tc.skipHealth}
|
etcdOptions := &EtcdOptions{SkipHealthEndpoints: tc.skipHealth}
|
||||||
if err := etcdOptions.Complete(serverConfig.StorageObjectCountTracker, serverConfig.DrainedNotify()); err != nil {
|
if err := etcdOptions.Complete(serverConfig.StorageObjectCountTracker, serverConfig.DrainedNotify(), serverConfig.AddPostStartHook); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := etcdOptions.ApplyTo(serverConfig); err != nil {
|
if err := etcdOptions.ApplyTo(serverConfig); err != nil {
|
||||||
|
@ -101,7 +101,7 @@ func (o *RecommendedOptions) AddFlags(fs *pflag.FlagSet) {
|
|||||||
// ApplyTo adds RecommendedOptions to the server configuration.
|
// ApplyTo adds RecommendedOptions to the server configuration.
|
||||||
// pluginInitializers can be empty, it is only need for additional initializers.
|
// pluginInitializers can be empty, it is only need for additional initializers.
|
||||||
func (o *RecommendedOptions) ApplyTo(config *server.RecommendedConfig) error {
|
func (o *RecommendedOptions) ApplyTo(config *server.RecommendedConfig) error {
|
||||||
if err := o.Etcd.Complete(config.Config.StorageObjectCountTracker, config.Config.DrainedNotify()); err != nil {
|
if err := o.Etcd.Complete(config.Config.StorageObjectCountTracker, config.Config.DrainedNotify(), config.Config.AddPostStartHook); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := o.Etcd.ApplyTo(&config.Config); err != nil {
|
if err := o.Etcd.ApplyTo(&config.Config); err != nil {
|
||||||
|
@ -94,8 +94,7 @@ resources:
|
|||||||
- name: key1
|
- name: key1
|
||||||
secret: c2VjcmV0IGlzIHNlY3VyZQ==
|
secret: c2VjcmV0IGlzIHNlY3VyZQ==
|
||||||
`
|
`
|
||||||
|
test, err := newTransformTest(t, encryptionConfig, false, "", false)
|
||||||
test, err := newTransformTest(t, encryptionConfig, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to start Kube API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
t.Fatalf("failed to start Kube API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
||||||
}
|
}
|
||||||
|
@ -26,13 +26,16 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/cryptobyte"
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apiserver/pkg/storage/value"
|
"k8s.io/apiserver/pkg/storage/value"
|
||||||
aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes"
|
aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes"
|
||||||
mock "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/testing/v1beta1"
|
mock "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/testing/v1beta1"
|
||||||
@ -128,7 +131,7 @@ resources:
|
|||||||
}
|
}
|
||||||
defer pluginMock.CleanUp()
|
defer pluginMock.CleanUp()
|
||||||
|
|
||||||
test, err := newTransformTest(t, encryptionConfig, false)
|
test, err := newTransformTest(t, encryptionConfig, false, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
||||||
}
|
}
|
||||||
@ -276,6 +279,475 @@ resources:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestECHotReload is an integration test that verifies hot reload of KMS encryption config works.
|
||||||
|
// This test asserts following scenarios:
|
||||||
|
// 1. start at 'kms-provider'
|
||||||
|
// 2. create some secrets
|
||||||
|
// 3. add 'new-kms-provider' as write KMS (this is okay because we only have 1 API server)
|
||||||
|
// 4. wait for config to be observed
|
||||||
|
// 5. run storage migration on secrets
|
||||||
|
// 6. confirm that secrets have the new prefix
|
||||||
|
// 7. remove 'kms-provider'
|
||||||
|
// 8. wait for config to be observed
|
||||||
|
// 9. confirm that reads still work
|
||||||
|
// 10. confirm that cluster wide secret read still works
|
||||||
|
// 11. confirm that api server can restart with last applied encryption config
|
||||||
|
func TestEncryptionConfigHotReload(t *testing.T) {
|
||||||
|
encryptionConfig := `
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- kms:
|
||||||
|
name: kms-provider
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@kms-provider.sock
|
||||||
|
`
|
||||||
|
pluginMock, err := mock.NewBase64Plugin("@kms-provider.sock")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create mock of KMS Plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go pluginMock.Start()
|
||||||
|
if err := mock.WaitForBase64PluginToBeUp(pluginMock); err != nil {
|
||||||
|
t.Fatalf("Failed start plugin, err: %v", err)
|
||||||
|
}
|
||||||
|
defer pluginMock.CleanUp()
|
||||||
|
|
||||||
|
var restarted bool
|
||||||
|
test, err := newTransformTest(t, encryptionConfig, true, "", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if !restarted {
|
||||||
|
test.cleanUp()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
test.secret, err = test.createSecret(testSecret, testNamespace)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test secret, error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new secret in default namespace. This is to assert cluster wide read works after hot reload.
|
||||||
|
_, err = test.createSecret(fmt.Sprintf("%s-%s", testSecret, "1"), "default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test secret in default namespace, error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = test.createConfigMap(testConfigmap, testNamespace)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test configmap, error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if hot reload controller is healthy
|
||||||
|
mustBeHealthy(t, "/poststarthook/start-encryption-provider-config-automatic-reload", "ok", test.kubeAPIServer.ClientConfig)
|
||||||
|
|
||||||
|
encryptionConfigWithNewProvider := `
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- kms:
|
||||||
|
name: new-kms-provider-for-secrets
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@new-kms-provider.sock
|
||||||
|
- kms:
|
||||||
|
name: kms-provider
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@kms-provider.sock
|
||||||
|
- resources:
|
||||||
|
- configmaps
|
||||||
|
providers:
|
||||||
|
- kms:
|
||||||
|
name: new-kms-provider-for-configmaps
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@new-kms-provider.sock
|
||||||
|
- identity: {}
|
||||||
|
`
|
||||||
|
// start new KMS Plugin
|
||||||
|
newPluginMock, err := mock.NewBase64Plugin("@new-kms-provider.sock")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create mock of KMS Plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go newPluginMock.Start()
|
||||||
|
if err := mock.WaitForBase64PluginToBeUp(newPluginMock); err != nil {
|
||||||
|
t.Fatalf("Failed start plugin, err: %v", err)
|
||||||
|
}
|
||||||
|
defer newPluginMock.CleanUp()
|
||||||
|
|
||||||
|
// update encryption config
|
||||||
|
if err := os.WriteFile(path.Join(test.configDir, encryptionConfigFileName), []byte(encryptionConfigWithNewProvider), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to update encryption config, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantPrefixForSecrets := "k8s:enc:kms:v1:new-kms-provider-for-secrets:"
|
||||||
|
|
||||||
|
// implementing this brute force approach instead of fancy channel notification to avoid test specific code in prod.
|
||||||
|
// wait for config to be observed
|
||||||
|
verifyIfKMSTransformersSwapped(t, wantPrefixForSecrets, test)
|
||||||
|
|
||||||
|
// run storage migration
|
||||||
|
// get secrets
|
||||||
|
secretsList, err := test.restClient.CoreV1().Secrets("").List(
|
||||||
|
context.TODO(),
|
||||||
|
metav1.ListOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to list secrets, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, secret := range secretsList.Items {
|
||||||
|
// update secret
|
||||||
|
_, err = test.restClient.CoreV1().Secrets(secret.Namespace).Update(
|
||||||
|
context.TODO(),
|
||||||
|
&secret,
|
||||||
|
metav1.UpdateOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to update secret, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get configmaps
|
||||||
|
configmapsList, err := test.restClient.CoreV1().ConfigMaps("").List(
|
||||||
|
context.TODO(),
|
||||||
|
metav1.ListOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to list configmaps, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, configmap := range configmapsList.Items {
|
||||||
|
// update configmap
|
||||||
|
_, err = test.restClient.CoreV1().ConfigMaps(configmap.Namespace).Update(
|
||||||
|
context.TODO(),
|
||||||
|
&configmap,
|
||||||
|
metav1.UpdateOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to update configmap, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert that resources has new prefix
|
||||||
|
secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace)
|
||||||
|
rawEnvelope, err := test.getRawSecretFromETCD()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert secret
|
||||||
|
if !bytes.HasPrefix(rawEnvelope, []byte(wantPrefixForSecrets)) {
|
||||||
|
t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefixForSecrets, rawEnvelope)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConfigmapEnvelope, err := test.readRawRecordFromETCD(test.getETCDPathForResource(test.storageConfig.Prefix, "", "configmaps", testConfigmap, testNamespace))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read %s from etcd: %v", test.getETCDPathForResource(test.storageConfig.Prefix, "", "configmaps", testConfigmap, testNamespace), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert prefix for configmap
|
||||||
|
wantPrefixForConfigmaps := "k8s:enc:kms:v1:new-kms-provider-for-configmaps:"
|
||||||
|
if !bytes.HasPrefix(rawConfigmapEnvelope.Kvs[0].Value, []byte(wantPrefixForConfigmaps)) {
|
||||||
|
t.Fatalf("expected configmap to be prefixed with %s, but got %s", wantPrefixForConfigmaps, rawConfigmapEnvelope.Kvs[0].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove old KMS provider
|
||||||
|
encryptionConfigWithoutOldProvider := `
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- kms:
|
||||||
|
name: new-kms-provider-for-secrets
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@new-kms-provider.sock
|
||||||
|
- resources:
|
||||||
|
- configmaps
|
||||||
|
providers:
|
||||||
|
- kms:
|
||||||
|
name: new-kms-provider-for-configmaps
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@new-kms-provider.sock
|
||||||
|
`
|
||||||
|
|
||||||
|
// update encryption config and wait for hot reload
|
||||||
|
if err := os.WriteFile(path.Join(test.configDir, encryptionConfigFileName), []byte(encryptionConfigWithoutOldProvider), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to update encryption config, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for config to be observed
|
||||||
|
verifyIfKMSTransformersSwapped(t, wantPrefixForSecrets, test)
|
||||||
|
|
||||||
|
// confirm that reading secrets still works
|
||||||
|
_, err = test.restClient.CoreV1().Secrets(testNamespace).Get(
|
||||||
|
context.TODO(),
|
||||||
|
testSecret,
|
||||||
|
metav1.GetOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read secret, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure cluster wide secrets read still works
|
||||||
|
_, err = test.restClient.CoreV1().Secrets("").List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to list secrets, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure cluster wide configmaps read still works
|
||||||
|
_, err = test.restClient.CoreV1().ConfigMaps("").List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to list configmaps, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// restart kube-apiserver with last applied encryption config and assert that server can start
|
||||||
|
previousConfigDir := test.configDir
|
||||||
|
test.shutdownAPIServer()
|
||||||
|
restarted = true
|
||||||
|
test, err = newTransformTest(t, "", true, previousConfigDir, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
||||||
|
}
|
||||||
|
test.cleanUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptionConfigHotReloadFileWatch(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
fileUpdateMethod string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
fileUpdateMethod: "truncate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileUpdateMethod: "deleteAndCreate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileUpdateMethod: "move",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileUpdateMethod: "symLink",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.fileUpdateMethod, func(t *testing.T) {
|
||||||
|
encryptionConfig := `
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- kms:
|
||||||
|
name: kms-provider
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@kms-provider.sock
|
||||||
|
`
|
||||||
|
pluginMock, err := mock.NewBase64Plugin("@kms-provider.sock")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create mock of KMS Plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go pluginMock.Start()
|
||||||
|
if err := mock.WaitForBase64PluginToBeUp(pluginMock); err != nil {
|
||||||
|
t.Fatalf("Failed start plugin, err: %v", err)
|
||||||
|
}
|
||||||
|
defer pluginMock.CleanUp()
|
||||||
|
|
||||||
|
var test *transformTest
|
||||||
|
if tc.fileUpdateMethod == "symLink" {
|
||||||
|
test, err = newTransformTest(t, encryptionConfig, true, "", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
test, err = newTransformTest(t, encryptionConfig, true, "", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer test.cleanUp()
|
||||||
|
|
||||||
|
test.secret, err = test.createSecret(testSecret, testNamespace)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test secret, error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if hot reload controller is healthy
|
||||||
|
mustBeHealthy(t, "/poststarthook/start-encryption-provider-config-automatic-reload", "ok", test.kubeAPIServer.ClientConfig)
|
||||||
|
|
||||||
|
encryptionConfigWithNewProvider := `
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- kms:
|
||||||
|
name: new-kms-provider-for-secrets
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@new-kms-provider.sock
|
||||||
|
- kms:
|
||||||
|
name: kms-provider
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@kms-provider.sock
|
||||||
|
- resources:
|
||||||
|
- configmaps
|
||||||
|
providers:
|
||||||
|
- kms:
|
||||||
|
name: new-kms-provider-for-configmaps
|
||||||
|
cachesize: 1000
|
||||||
|
endpoint: unix:///@new-kms-provider.sock
|
||||||
|
- identity: {}
|
||||||
|
`
|
||||||
|
// start new KMS Plugin
|
||||||
|
newPluginMock, err := mock.NewBase64Plugin("@new-kms-provider.sock")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create mock of KMS Plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go newPluginMock.Start()
|
||||||
|
if err := mock.WaitForBase64PluginToBeUp(newPluginMock); err != nil {
|
||||||
|
t.Fatalf("Failed start plugin, err: %v", err)
|
||||||
|
}
|
||||||
|
defer newPluginMock.CleanUp()
|
||||||
|
|
||||||
|
switch tc.fileUpdateMethod {
|
||||||
|
case "truncate":
|
||||||
|
// update encryption config
|
||||||
|
// os.WriteFile truncates the file before writing
|
||||||
|
if err := os.WriteFile(path.Join(test.configDir, encryptionConfigFileName), []byte(encryptionConfigWithNewProvider), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to update encryption config, err: %v", err)
|
||||||
|
}
|
||||||
|
case "deleteAndCreate":
|
||||||
|
// update encryption config
|
||||||
|
// os.Remove deletes the file before creating a new one
|
||||||
|
if err := os.Remove(path.Join(test.configDir, encryptionConfigFileName)); err != nil {
|
||||||
|
t.Fatalf("failed to remove encryption config, err: %v", err)
|
||||||
|
}
|
||||||
|
file, err := os.Create(path.Join(test.configDir, encryptionConfigFileName))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create encryption config, err: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := file.Write([]byte(encryptionConfigWithNewProvider)); err != nil {
|
||||||
|
t.Fatalf("failed to write encryption config, err: %v", err)
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close encryption config, err: %v", err)
|
||||||
|
}
|
||||||
|
case "move":
|
||||||
|
// update encryption config
|
||||||
|
// write new config to a temp file
|
||||||
|
if err := os.WriteFile(path.Join(test.configDir, encryptionConfigFileName+".tmp"), []byte(encryptionConfigWithNewProvider), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config to tmp file, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// move the temp file to the original file
|
||||||
|
if err := os.Rename(path.Join(test.configDir, encryptionConfigFileName+".tmp"), path.Join(test.configDir, encryptionConfigFileName)); err != nil {
|
||||||
|
t.Fatalf("failed to move encryption config, err: %v", err)
|
||||||
|
}
|
||||||
|
case "symLink":
|
||||||
|
// update encryption config
|
||||||
|
// write new config in a parent directory.
|
||||||
|
if err := os.WriteFile(path.Join(test.configParentDir, encryptionConfigFileName), []byte(encryptionConfigWithNewProvider), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to update encryption config, err: %v", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("unknown file update method: %s", tc.fileUpdateMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantPrefix := "k8s:enc:kms:v1:new-kms-provider-for-secrets:"
|
||||||
|
|
||||||
|
// implementing this brute force approach instead of fancy channel notification to avoid test specific code in prod.
|
||||||
|
// wait for config to be observed
|
||||||
|
verifyIfKMSTransformersSwapped(t, wantPrefix, test)
|
||||||
|
|
||||||
|
// run storage migration
|
||||||
|
// get secrets
|
||||||
|
secretsList, err := test.restClient.CoreV1().Secrets("").List(
|
||||||
|
context.TODO(),
|
||||||
|
metav1.ListOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to list secrets, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, secret := range secretsList.Items {
|
||||||
|
// update secret
|
||||||
|
_, err = test.restClient.CoreV1().Secrets(secret.Namespace).Update(
|
||||||
|
context.TODO(),
|
||||||
|
&secret,
|
||||||
|
metav1.UpdateOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to update secret, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert that resources has new prefix
|
||||||
|
secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace)
|
||||||
|
rawEnvelope, err := test.getRawSecretFromETCD()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert secret
|
||||||
|
if !bytes.HasPrefix(rawEnvelope, []byte(wantPrefix)) {
|
||||||
|
t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, rawEnvelope)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyIfKMSTransformersSwapped(t *testing.T, wantPrefix string, test *transformTest) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var swapErr error
|
||||||
|
// delete and recreate same secret flakes, so create a new secret with a different index until new prefix is observed
|
||||||
|
// generate a random int to be used in secret name
|
||||||
|
idx := rand.Intn(100000)
|
||||||
|
|
||||||
|
pollErr := wait.PollImmediate(time.Second, wait.ForeverTestTimeout, func() (bool, error) {
|
||||||
|
// create secret
|
||||||
|
secretName := fmt.Sprintf("secret-%d", idx)
|
||||||
|
_, err := test.createSecret(secretName, "default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test secret, error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawEnvelope, err := test.readRawRecordFromETCD(test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", secretName, "default"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read %s from etcd: %v", test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", secretName, "default"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check prefix
|
||||||
|
if !bytes.HasPrefix(rawEnvelope.Kvs[0].Value, []byte(wantPrefix)) {
|
||||||
|
idx++
|
||||||
|
|
||||||
|
swapErr = fmt.Errorf("expected secret to be prefixed with %s, but got %s", wantPrefix, rawEnvelope.Kvs[0].Value)
|
||||||
|
|
||||||
|
// return nil error to continue polling till timeout
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
if pollErr == wait.ErrWaitTimeout {
|
||||||
|
t.Fatalf("failed to verify if kms transformers swapped, err: %v", swapErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestKMSHealthz(t *testing.T) {
|
func TestKMSHealthz(t *testing.T) {
|
||||||
encryptionConfig := `
|
encryptionConfig := `
|
||||||
kind: EncryptionConfiguration
|
kind: EncryptionConfiguration
|
||||||
@ -317,22 +789,21 @@ resources:
|
|||||||
t.Fatalf("Failed to start KMS Plugin #2: err: %v", err)
|
t.Fatalf("Failed to start KMS Plugin #2: err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
test, err := newTransformTest(t, encryptionConfig, false)
|
test, err := newTransformTest(t, encryptionConfig, false, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start kube-apiserver, error: %v", err)
|
t.Fatalf("Failed to start kube-apiserver, error: %v", err)
|
||||||
}
|
}
|
||||||
defer test.cleanUp()
|
defer test.cleanUp()
|
||||||
|
|
||||||
// Name of the healthz check is calculated based on a constant "kms-provider-" + position of the
|
// Name of the healthz check is always "kms-provider-0" and it covers all kms plugins.
|
||||||
// provider in the config.
|
|
||||||
|
|
||||||
// Stage 1 - Since all kms-plugins are guaranteed to be up, healthz checks for:
|
// Stage 1 - Since all kms-plugins are guaranteed to be up, healthz checks for:
|
||||||
// healthz/kms-provider-0 and /healthz/kms-provider-1 should be OK.
|
// healthz/kms-provider-0 and /healthz/kms-provider-1 should be OK.
|
||||||
mustBeHealthy(t, "/kms-provider-0", "ok", test.kubeAPIServer.ClientConfig)
|
mustBeHealthy(t, "/kms-provider-0", "ok", test.kubeAPIServer.ClientConfig)
|
||||||
mustBeHealthy(t, "/kms-provider-1", "ok", test.kubeAPIServer.ClientConfig)
|
mustBeHealthy(t, "/kms-provider-1", "ok", test.kubeAPIServer.ClientConfig)
|
||||||
|
|
||||||
// Stage 2 - kms-plugin for provider-1 is down. Therefore, expect the health check for provider-1
|
// Stage 2 - kms-plugin for provider-1 is down. Therefore, expect the healthz check
|
||||||
// to fail, but provider-2 should still be OK
|
// to fail and report that provider-1 is down
|
||||||
pluginMock1.EnterFailedState()
|
pluginMock1.EnterFailedState()
|
||||||
mustBeUnHealthy(t, "/kms-provider-0",
|
mustBeUnHealthy(t, "/kms-provider-0",
|
||||||
"internal server error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled",
|
"internal server error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled",
|
||||||
@ -396,7 +867,7 @@ resources:
|
|||||||
t.Fatalf("Failed to start KMS Plugin #2: err: %v", err)
|
t.Fatalf("Failed to start KMS Plugin #2: err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
test, err := newTransformTest(t, encryptionConfig, true)
|
test, err := newTransformTest(t, encryptionConfig, true, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start kube-apiserver, error: %v", err)
|
t.Fatalf("Failed to start kube-apiserver, error: %v", err)
|
||||||
}
|
}
|
||||||
@ -412,7 +883,7 @@ resources:
|
|||||||
// to fail and report that provider-1 is down
|
// to fail and report that provider-1 is down
|
||||||
pluginMock1.EnterFailedState()
|
pluginMock1.EnterFailedState()
|
||||||
mustBeUnHealthy(t, "/kms-providers",
|
mustBeUnHealthy(t, "/kms-providers",
|
||||||
"internal server error: kms-provider-0: rpc error: code = FailedPrecondition desc = failed precondition - key disabled",
|
"internal server error: kms-provider-0: failed to perform encrypt section of the healthz check for KMS Provider provider-1, error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled",
|
||||||
test.kubeAPIServer.ClientConfig)
|
test.kubeAPIServer.ClientConfig)
|
||||||
pluginMock1.ExitFailedState()
|
pluginMock1.ExitFailedState()
|
||||||
|
|
||||||
@ -420,7 +891,7 @@ resources:
|
|||||||
// to succeed now, but provider-2 is now down.
|
// to succeed now, but provider-2 is now down.
|
||||||
pluginMock2.EnterFailedState()
|
pluginMock2.EnterFailedState()
|
||||||
mustBeUnHealthy(t, "/kms-providers",
|
mustBeUnHealthy(t, "/kms-providers",
|
||||||
"internal server error: kms-provider-1: rpc error: code = FailedPrecondition desc = failed precondition - key disabled",
|
"internal server error: kms-provider-1: failed to perform encrypt section of the healthz check for KMS Provider provider-2, error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled",
|
||||||
test.kubeAPIServer.ClientConfig)
|
test.kubeAPIServer.ClientConfig)
|
||||||
pluginMock2.ExitFailedState()
|
pluginMock2.ExitFailedState()
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ resources:
|
|||||||
}
|
}
|
||||||
defer pluginMock.CleanUp()
|
defer pluginMock.CleanUp()
|
||||||
|
|
||||||
test, err := newTransformTest(t, encryptionConfig, false)
|
test, err := newTransformTest(t, encryptionConfig, false, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
||||||
}
|
}
|
||||||
@ -253,7 +253,7 @@ resources:
|
|||||||
t.Fatalf("Failed to start KMS Plugin #2: err: %v", err)
|
t.Fatalf("Failed to start KMS Plugin #2: err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
test, err := newTransformTest(t, encryptionConfig, false)
|
test, err := newTransformTest(t, encryptionConfig, false, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start kube-apiserver, error: %v", err)
|
t.Fatalf("Failed to start kube-apiserver, error: %v", err)
|
||||||
}
|
}
|
||||||
@ -341,7 +341,7 @@ resources:
|
|||||||
}
|
}
|
||||||
t.Cleanup(pluginMock.CleanUp)
|
t.Cleanup(pluginMock.CleanUp)
|
||||||
|
|
||||||
test, err := newTransformTest(t, encryptionConfig, false)
|
test, err := newTransformTest(t, encryptionConfig, false, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ func TestSecretsShouldBeTransformed(t *testing.T) {
|
|||||||
// TODO: add secretbox
|
// TODO: add secretbox
|
||||||
}
|
}
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
test, err := newTransformTest(t, tt.transformerConfigContent, false)
|
test, err := newTransformTest(t, tt.transformerConfigContent, false, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
test.cleanUp()
|
test.cleanUp()
|
||||||
t.Errorf("failed to setup test for envelop %s, error was %v", tt.transformerPrefix, err)
|
t.Errorf("failed to setup test for envelop %s, error was %v", tt.transformerPrefix, err)
|
||||||
@ -120,7 +120,7 @@ func BenchmarkAESCBCEnvelopeWrite(b *testing.B) {
|
|||||||
|
|
||||||
func runBenchmark(b *testing.B, transformerConfig string) {
|
func runBenchmark(b *testing.B, transformerConfig string) {
|
||||||
b.StopTimer()
|
b.StopTimer()
|
||||||
test, err := newTransformTest(b, transformerConfig, false)
|
test, err := newTransformTest(b, transformerConfig, false, "", false)
|
||||||
defer test.cleanUp()
|
defer test.cleanUp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("failed to setup benchmark for config %s, error was %v", transformerConfig, err)
|
b.Fatalf("failed to setup benchmark for config %s, error was %v", transformerConfig, err)
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -56,6 +57,7 @@ const (
|
|||||||
encryptionConfigFileName = "encryption.conf"
|
encryptionConfigFileName = "encryption.conf"
|
||||||
testNamespace = "secret-encryption-test"
|
testNamespace = "secret-encryption-test"
|
||||||
testSecret = "test-secret"
|
testSecret = "test-secret"
|
||||||
|
testConfigmap = "test-configmap"
|
||||||
metricsPrefix = "apiserver_storage_"
|
metricsPrefix = "apiserver_storage_"
|
||||||
configMapKey = "foo"
|
configMapKey = "foo"
|
||||||
configMapVal = "bar"
|
configMapVal = "bar"
|
||||||
@ -73,6 +75,7 @@ type transformTest struct {
|
|||||||
logger kubeapiservertesting.Logger
|
logger kubeapiservertesting.Logger
|
||||||
storageConfig *storagebackend.Config
|
storageConfig *storagebackend.Config
|
||||||
configDir string
|
configDir string
|
||||||
|
configParentDir string
|
||||||
transformerConfig string
|
transformerConfig string
|
||||||
kubeAPIServer kubeapiservertesting.TestServer
|
kubeAPIServer kubeapiservertesting.TestServer
|
||||||
restClient *kubernetes.Clientset
|
restClient *kubernetes.Clientset
|
||||||
@ -80,7 +83,7 @@ type transformTest struct {
|
|||||||
secret *corev1.Secret
|
secret *corev1.Secret
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTransformTest(l kubeapiservertesting.Logger, transformerConfigYAML string, reload bool) (*transformTest, error) {
|
func newTransformTest(l kubeapiservertesting.Logger, transformerConfigYAML string, reload bool, configDir string, ecSymLink bool) (*transformTest, error) {
|
||||||
e := transformTest{
|
e := transformTest{
|
||||||
logger: l,
|
logger: l,
|
||||||
transformerConfig: transformerConfigYAML,
|
transformerConfig: transformerConfigYAML,
|
||||||
@ -88,10 +91,14 @@ func newTransformTest(l kubeapiservertesting.Logger, transformerConfigYAML strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if transformerConfigYAML != "" {
|
// create config dir with provided config yaml
|
||||||
if e.configDir, err = e.createEncryptionConfig(); err != nil {
|
if transformerConfigYAML != "" && configDir == "" {
|
||||||
|
if e.configDir, e.configParentDir, err = e.createEncryptionConfig(ecSymLink); err != nil {
|
||||||
return nil, fmt.Errorf("error while creating KubeAPIServer encryption config: %v", err)
|
return nil, fmt.Errorf("error while creating KubeAPIServer encryption config: %v", err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// configDir already exists. api-server must be restarting with existing encryption config
|
||||||
|
e.configDir = configDir
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.kubeAPIServer, err = kubeapiservertesting.StartTestServer(l, nil, e.getEncryptionOptions(reload), e.storageConfig); err != nil {
|
if e.kubeAPIServer, err = kubeapiservertesting.StartTestServer(l, nil, e.getEncryptionOptions(reload), e.storageConfig); err != nil {
|
||||||
@ -121,6 +128,11 @@ func newTransformTest(l kubeapiservertesting.Logger, transformerConfigYAML strin
|
|||||||
|
|
||||||
func (e *transformTest) cleanUp() {
|
func (e *transformTest) cleanUp() {
|
||||||
os.RemoveAll(e.configDir)
|
os.RemoveAll(e.configDir)
|
||||||
|
os.RemoveAll(e.configParentDir)
|
||||||
|
e.shutdownAPIServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *transformTest) shutdownAPIServer() {
|
||||||
e.restClient.CoreV1().Namespaces().Delete(context.TODO(), e.ns.Name, *metav1.NewDeleteOptions(0))
|
e.restClient.CoreV1().Namespaces().Delete(context.TODO(), e.ns.Name, *metav1.NewDeleteOptions(0))
|
||||||
e.kubeAPIServer.TearDownFn()
|
e.kubeAPIServer.TearDownFn()
|
||||||
}
|
}
|
||||||
@ -250,20 +262,40 @@ func (e *transformTest) getEncryptionOptions(reload bool) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *transformTest) createEncryptionConfig() (string, error) {
|
func (e *transformTest) createEncryptionConfig(ecSymLink bool) (string, string, error) {
|
||||||
tempDir, err := os.MkdirTemp("", "secrets-encryption-test")
|
tempDir, err := os.MkdirTemp("", "secrets-encryption-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create temp directory: %v", err)
|
return "", "", fmt.Errorf("failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ecSymLink {
|
||||||
|
// create another temp dir
|
||||||
|
parentTempDir, err := os.MkdirTemp("", "secrets-encryption-symlink-test")
|
||||||
|
if err != nil {
|
||||||
|
return tempDir, "", fmt.Errorf("failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create config file
|
||||||
|
if err := os.WriteFile(filepath.Join(parentTempDir, encryptionConfigFileName), []byte(e.transformerConfig), 0644); err != nil {
|
||||||
|
return tempDir, parentTempDir, fmt.Errorf("failed to write encryption config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create symlink
|
||||||
|
if err := os.Symlink(filepath.Join(parentTempDir, encryptionConfigFileName), filepath.Join(tempDir, encryptionConfigFileName)); err != nil {
|
||||||
|
return tempDir, parentTempDir, fmt.Errorf("failed to create symlink: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempDir, parentTempDir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptionConfig := path.Join(tempDir, encryptionConfigFileName)
|
encryptionConfig := path.Join(tempDir, encryptionConfigFileName)
|
||||||
|
|
||||||
if err := os.WriteFile(encryptionConfig, []byte(e.transformerConfig), 0644); err != nil {
|
if err := os.WriteFile(encryptionConfig, []byte(e.transformerConfig), 0644); err != nil {
|
||||||
os.RemoveAll(tempDir)
|
os.RemoveAll(tempDir)
|
||||||
return "", fmt.Errorf("error while writing encryption config: %v", err)
|
return tempDir, "", fmt.Errorf("error while writing encryption config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tempDir, nil
|
return tempDir, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *transformTest) getEncryptionConfig() (*apiserverconfigv1.ProviderConfiguration, error) {
|
func (e *transformTest) getEncryptionConfig() (*apiserverconfigv1.ProviderConfiguration, error) {
|
||||||
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -1534,6 +1534,7 @@ k8s.io/apiserver/pkg/server/httplog
|
|||||||
k8s.io/apiserver/pkg/server/mux
|
k8s.io/apiserver/pkg/server/mux
|
||||||
k8s.io/apiserver/pkg/server/options
|
k8s.io/apiserver/pkg/server/options
|
||||||
k8s.io/apiserver/pkg/server/options/encryptionconfig
|
k8s.io/apiserver/pkg/server/options/encryptionconfig
|
||||||
|
k8s.io/apiserver/pkg/server/options/encryptionconfig/controller
|
||||||
k8s.io/apiserver/pkg/server/resourceconfig
|
k8s.io/apiserver/pkg/server/resourceconfig
|
||||||
k8s.io/apiserver/pkg/server/routes
|
k8s.io/apiserver/pkg/server/routes
|
||||||
k8s.io/apiserver/pkg/server/storage
|
k8s.io/apiserver/pkg/server/storage
|
||||||
|
Loading…
Reference in New Issue
Block a user