1095 lines
32 KiB
Go
1095 lines
32 KiB
Go
/*
|
|
Copyright 2024 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 storageversionmigrator
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
clientv3 "go.etcd.io/etcd/client/v3"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
svmv1alpha1 "k8s.io/api/storagemigration/v1alpha1"
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
|
crdintegration "k8s.io/apiextensions-apiserver/test/integration"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
|
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
|
|
endpointsdiscovery "k8s.io/apiserver/pkg/endpoints/discovery"
|
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
|
"k8s.io/client-go/discovery"
|
|
cacheddiscovery "k8s.io/client-go/discovery/cached/memory"
|
|
"k8s.io/client-go/dynamic"
|
|
"k8s.io/client-go/informers"
|
|
clientset "k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/metadata"
|
|
"k8s.io/client-go/metadata/metadatainformer"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/restmapper"
|
|
"k8s.io/client-go/util/cert"
|
|
"k8s.io/client-go/util/keyutil"
|
|
utiltesting "k8s.io/client-go/util/testing"
|
|
"k8s.io/controller-manager/pkg/informerfactory"
|
|
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
|
"k8s.io/kubernetes/cmd/kube-controller-manager/names"
|
|
"k8s.io/kubernetes/pkg/controller/garbagecollector"
|
|
"k8s.io/kubernetes/pkg/controller/storageversionmigrator"
|
|
"k8s.io/kubernetes/test/images/agnhost/crd-conversion-webhook/converter"
|
|
"k8s.io/kubernetes/test/integration"
|
|
"k8s.io/kubernetes/test/integration/etcd"
|
|
"k8s.io/kubernetes/test/integration/framework"
|
|
"k8s.io/kubernetes/test/utils"
|
|
utilnet "k8s.io/utils/net"
|
|
"k8s.io/utils/ptr"
|
|
)
|
|
|
|
const (
|
|
secretKey = "api_key"
|
|
secretVal = "086a7ffc-0225-11e8-ba89-0ed5f89f718b" // Fake value for testing.
|
|
secretName = "test-secret"
|
|
triggerSecretName = "trigger-for-svm"
|
|
svmName = "test-svm"
|
|
secondSVMName = "second-test-svm"
|
|
auditPolicyFileName = "audit-policy.yaml"
|
|
auditLogFileName = "audit.log"
|
|
encryptionConfigFileName = "encryption.conf"
|
|
metricPrefix = "apiserver_encryption_config_controller_automatic_reload_success_total"
|
|
defaultNamespace = "default"
|
|
crdName = "testcrd"
|
|
crdGroup = "stable.example.com"
|
|
servicePort = int32(9443)
|
|
webhookHandler = "crdconvert"
|
|
)
|
|
|
|
var (
|
|
resources = map[string]string{
|
|
"auditPolicy": `
|
|
apiVersion: audit.k8s.io/v1
|
|
kind: Policy
|
|
omitStages:
|
|
- "RequestReceived"
|
|
rules:
|
|
- level: Metadata
|
|
resources:
|
|
- group: ""
|
|
resources: ["secrets"]
|
|
verbs: ["patch"]
|
|
`,
|
|
"initialEncryptionConfig": `
|
|
kind: EncryptionConfiguration
|
|
apiVersion: apiserver.config.k8s.io/v1
|
|
resources:
|
|
- resources:
|
|
- secrets
|
|
providers:
|
|
- aescbc:
|
|
keys:
|
|
- name: key1
|
|
secret: c2VjcmV0IGlzIHNlY3VyZQ==
|
|
`,
|
|
"updatedEncryptionConfig": `
|
|
kind: EncryptionConfiguration
|
|
apiVersion: apiserver.config.k8s.io/v1
|
|
resources:
|
|
- resources:
|
|
- secrets
|
|
providers:
|
|
- aescbc:
|
|
keys:
|
|
- name: key2
|
|
secret: c2VjcmV0IGlzIHNlY3VyZSwgaXMgaXQ/
|
|
- aescbc:
|
|
keys:
|
|
- name: key1
|
|
secret: c2VjcmV0IGlzIHNlY3VyZQ==
|
|
`,
|
|
}
|
|
|
|
v1CRDVersion = []apiextensionsv1.CustomResourceDefinitionVersion{
|
|
{
|
|
Name: "v1",
|
|
Served: true,
|
|
Storage: true,
|
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
|
Type: "object",
|
|
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
|
"hostPort": {Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
v2CRDVersion = []apiextensionsv1.CustomResourceDefinitionVersion{
|
|
{
|
|
Name: "v2",
|
|
Served: true,
|
|
Storage: false,
|
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
|
Type: "object",
|
|
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
|
"host": {Type: "string"},
|
|
"port": {Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "v1",
|
|
Served: true,
|
|
Storage: true,
|
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
|
Type: "object",
|
|
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
|
"hostPort": {Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
v2StorageCRDVersion = []apiextensionsv1.CustomResourceDefinitionVersion{
|
|
{
|
|
Name: "v1",
|
|
Served: true,
|
|
Storage: false,
|
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
|
Type: "object",
|
|
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
|
"hostPort": {Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "v2",
|
|
Served: true,
|
|
Storage: true,
|
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
|
Type: "object",
|
|
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
|
"host": {Type: "string"},
|
|
"port": {Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
v1NotServingCRDVersion = []apiextensionsv1.CustomResourceDefinitionVersion{
|
|
{
|
|
Name: "v1",
|
|
Served: false,
|
|
Storage: false,
|
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
|
Type: "object",
|
|
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
|
"hostPort": {Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "v2",
|
|
Served: true,
|
|
Storage: true,
|
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
|
Type: "object",
|
|
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
|
"host": {Type: "string"},
|
|
"port": {Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
type svmTest struct {
|
|
policyFile *os.File
|
|
logFile *os.File
|
|
client clientset.Interface
|
|
clientConfig *rest.Config
|
|
dynamicClient *dynamic.DynamicClient
|
|
discoveryClient *discovery.DiscoveryClient
|
|
storageConfig *storagebackend.Config
|
|
server *kubeapiservertesting.TestServer
|
|
apiextensionsclient *apiextensionsclientset.Clientset
|
|
filePathForEncryptionConfig string
|
|
}
|
|
|
|
func svmSetup(ctx context.Context, t *testing.T) *svmTest {
|
|
t.Helper()
|
|
|
|
filePathForEncryptionConfig, err := createEncryptionConfig(t, resources["initialEncryptionConfig"])
|
|
if err != nil {
|
|
t.Fatalf("failed to create encryption config: %v", err)
|
|
}
|
|
|
|
policyFile, logFile := setupAudit(t)
|
|
apiServerFlags := []string{
|
|
"--encryption-provider-config", filepath.Join(filePathForEncryptionConfig, encryptionConfigFileName),
|
|
"--encryption-provider-config-automatic-reload=true",
|
|
"--disable-admission-plugins", "ServiceAccount",
|
|
"--audit-policy-file", policyFile.Name(),
|
|
"--audit-log-version", "audit.k8s.io/v1",
|
|
"--audit-log-mode", "blocking",
|
|
"--audit-log-path", logFile.Name(),
|
|
}
|
|
storageConfig := framework.SharedEtcd()
|
|
server := kubeapiservertesting.StartTestServerOrDie(t, nil, apiServerFlags, storageConfig)
|
|
|
|
clientSet, err := clientset.NewForConfig(server.ClientConfig)
|
|
if err != nil {
|
|
t.Fatalf("error in create clientset: %v", err)
|
|
}
|
|
|
|
discoveryClient := cacheddiscovery.NewMemCacheClient(clientSet.Discovery())
|
|
rvDiscoveryClient, err := discovery.NewDiscoveryClientForConfig(server.ClientConfig)
|
|
if err != nil {
|
|
t.Fatalf("failed to create discovery client: %v", err)
|
|
}
|
|
restMapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
|
|
restMapper.Reset()
|
|
metadataClient, err := metadata.NewForConfig(server.ClientConfig)
|
|
if err != nil {
|
|
t.Fatalf("failed to create metadataClient: %v", err)
|
|
}
|
|
dynamicClient, err := dynamic.NewForConfig(server.ClientConfig)
|
|
if err != nil {
|
|
t.Fatalf("error in create dynamic client: %v", err)
|
|
}
|
|
sharedInformers := informers.NewSharedInformerFactory(clientSet, 0)
|
|
metadataInformers := metadatainformer.NewSharedInformerFactory(metadataClient, 0)
|
|
alwaysStarted := make(chan struct{})
|
|
close(alwaysStarted)
|
|
|
|
gc, err := garbagecollector.NewGarbageCollector(
|
|
ctx,
|
|
clientSet,
|
|
metadataClient,
|
|
restMapper,
|
|
garbagecollector.DefaultIgnoredResources(),
|
|
informerfactory.NewInformerFactory(sharedInformers, metadataInformers),
|
|
alwaysStarted,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("error while creating garbage collector: %v", err)
|
|
|
|
}
|
|
startGC := func() {
|
|
syncPeriod := 5 * time.Second
|
|
go wait.Until(func() {
|
|
restMapper.Reset()
|
|
}, syncPeriod, ctx.Done())
|
|
go gc.Run(ctx, 1)
|
|
go gc.Sync(ctx, clientSet.Discovery(), syncPeriod)
|
|
}
|
|
|
|
svmController := storageversionmigrator.NewSVMController(
|
|
ctx,
|
|
clientSet,
|
|
dynamicClient,
|
|
sharedInformers.Storagemigration().V1alpha1().StorageVersionMigrations(),
|
|
names.StorageVersionMigratorController,
|
|
restMapper,
|
|
gc.GetDependencyGraphBuilder(),
|
|
)
|
|
|
|
rvController := storageversionmigrator.NewResourceVersionController(
|
|
ctx,
|
|
clientSet,
|
|
rvDiscoveryClient,
|
|
metadataClient,
|
|
sharedInformers.Storagemigration().V1alpha1().StorageVersionMigrations(),
|
|
restMapper,
|
|
)
|
|
|
|
// Start informer and controllers
|
|
sharedInformers.Start(ctx.Done())
|
|
startGC()
|
|
go svmController.Run(ctx)
|
|
go rvController.Run(ctx)
|
|
|
|
svmTest := &svmTest{
|
|
storageConfig: storageConfig,
|
|
server: server,
|
|
client: clientSet,
|
|
clientConfig: server.ClientConfig,
|
|
dynamicClient: dynamicClient,
|
|
discoveryClient: rvDiscoveryClient,
|
|
policyFile: policyFile,
|
|
logFile: logFile,
|
|
filePathForEncryptionConfig: filePathForEncryptionConfig,
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
server.TearDownFn()
|
|
utiltesting.CloseAndRemove(t, svmTest.logFile)
|
|
utiltesting.CloseAndRemove(t, svmTest.policyFile)
|
|
err = os.RemoveAll(svmTest.filePathForEncryptionConfig)
|
|
if err != nil {
|
|
t.Errorf("error while removing temp directory: %v", err)
|
|
}
|
|
})
|
|
|
|
return svmTest
|
|
}
|
|
|
|
func createEncryptionConfig(t *testing.T, encryptionConfig string) (
|
|
filePathForEncryptionConfig string,
|
|
err error,
|
|
) {
|
|
t.Helper()
|
|
tempDir, err := os.MkdirTemp("", svmName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temp directory: %w", err)
|
|
}
|
|
|
|
if err = os.WriteFile(filepath.Join(tempDir, encryptionConfigFileName), []byte(encryptionConfig), 0644); err != nil {
|
|
err = os.RemoveAll(tempDir)
|
|
if err != nil {
|
|
t.Errorf("error while removing temp directory: %v", err)
|
|
}
|
|
return tempDir, fmt.Errorf("error while writing encryption config: %w", err)
|
|
}
|
|
|
|
return tempDir, nil
|
|
}
|
|
|
|
func (svm *svmTest) createSecret(ctx context.Context, t *testing.T, name, namespace string) (*corev1.Secret, error) {
|
|
t.Helper()
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
},
|
|
Data: map[string][]byte{
|
|
secretKey: []byte(secretVal),
|
|
},
|
|
}
|
|
|
|
return svm.client.CoreV1().Secrets(secret.Namespace).Create(ctx, secret, metav1.CreateOptions{})
|
|
}
|
|
|
|
func (svm *svmTest) getRawSecretFromETCD(t *testing.T, name, namespace string) ([]byte, error) {
|
|
t.Helper()
|
|
secretETCDPath := svm.getETCDPathForResource(t, svm.storageConfig.Prefix, "", "secrets", name, namespace)
|
|
etcdResponse, err := svm.readRawRecordFromETCD(t, secretETCDPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read %s from etcd: %w", secretETCDPath, err)
|
|
}
|
|
return etcdResponse.Kvs[0].Value, nil
|
|
}
|
|
|
|
func (svm *svmTest) getETCDPathForResource(t *testing.T, storagePrefix, group, resource, name, namespaceName string) string {
|
|
t.Helper()
|
|
groupResource := resource
|
|
if group != "" {
|
|
groupResource = fmt.Sprintf("%s/%s", group, resource)
|
|
}
|
|
if namespaceName == "" {
|
|
return fmt.Sprintf("/%s/%s/%s", storagePrefix, groupResource, name)
|
|
}
|
|
return fmt.Sprintf("/%s/%s/%s/%s", storagePrefix, groupResource, namespaceName, name)
|
|
}
|
|
|
|
func (svm *svmTest) readRawRecordFromETCD(t *testing.T, path string) (*clientv3.GetResponse, error) {
|
|
t.Helper()
|
|
rawClient, etcdClient, err := integration.GetEtcdClients(svm.server.ServerOpts.Etcd.StorageConfig.Transport)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create etcd client: %w", err)
|
|
}
|
|
// kvClient is a wrapper around rawClient and to avoid leaking goroutines we need to
|
|
// close the client (which we can do by closing rawClient).
|
|
defer func() {
|
|
if err := rawClient.Close(); err != nil {
|
|
t.Errorf("error closing rawClient: %v", err)
|
|
}
|
|
}()
|
|
|
|
response, err := etcdClient.Get(context.Background(), path, clientv3.WithPrefix())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve secret from etcd %w", err)
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (svm *svmTest) getRawCRFromETCD(t *testing.T, name, namespace, crdGroup, crdName string) ([]byte, error) {
|
|
t.Helper()
|
|
crdETCDPath := svm.getETCDPathForResource(t, svm.storageConfig.Prefix, crdGroup, crdName, name, namespace)
|
|
etcdResponse, err := svm.readRawRecordFromETCD(t, crdETCDPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read %s from etcd: %v", crdETCDPath, err)
|
|
}
|
|
return etcdResponse.Kvs[0].Value, nil
|
|
}
|
|
|
|
func (svm *svmTest) updateFile(t *testing.T, configDir, filename string, newContent []byte) {
|
|
t.Helper()
|
|
// Create a temporary file
|
|
tempFile, err := os.CreateTemp(configDir, "tempfile")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := tempFile.Close(); err != nil {
|
|
t.Errorf("error closing tempFile: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Write the new content to the temporary file
|
|
_, err = tempFile.Write(newContent)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Atomically replace the original file with the temporary file
|
|
err = os.Rename(tempFile.Name(), filepath.Join(configDir, filename))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (svm *svmTest) createSVMResource(ctx context.Context, t *testing.T, name string, gvr svmv1alpha1.GroupVersionResource) (
|
|
*svmv1alpha1.StorageVersionMigration,
|
|
error,
|
|
) {
|
|
t.Helper()
|
|
svmResource := &svmv1alpha1.StorageVersionMigration{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Spec: svmv1alpha1.StorageVersionMigrationSpec{
|
|
Resource: svmv1alpha1.GroupVersionResource{
|
|
Group: gvr.Group,
|
|
Version: gvr.Version,
|
|
Resource: gvr.Resource,
|
|
},
|
|
},
|
|
}
|
|
|
|
return svm.client.StoragemigrationV1alpha1().
|
|
StorageVersionMigrations().
|
|
Create(ctx, svmResource, metav1.CreateOptions{})
|
|
}
|
|
|
|
func (svm *svmTest) getSVM(ctx context.Context, t *testing.T, name string) (
|
|
*svmv1alpha1.StorageVersionMigration,
|
|
error,
|
|
) {
|
|
t.Helper()
|
|
return svm.client.StoragemigrationV1alpha1().
|
|
StorageVersionMigrations().
|
|
Get(ctx, name, metav1.GetOptions{})
|
|
}
|
|
|
|
func setupAudit(t *testing.T) (
|
|
policyFile *os.File,
|
|
logFile *os.File,
|
|
) {
|
|
t.Helper()
|
|
// prepare audit policy file
|
|
policyFile, err := os.CreateTemp("", auditPolicyFileName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create audit policy file: %v", err)
|
|
}
|
|
if _, err := policyFile.Write([]byte(resources["auditPolicy"])); err != nil {
|
|
t.Fatalf("Failed to write audit policy file: %v", err)
|
|
}
|
|
|
|
// prepare audit log file
|
|
logFile, err = os.CreateTemp("", auditLogFileName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create audit log file: %v", err)
|
|
}
|
|
|
|
return policyFile, logFile
|
|
}
|
|
|
|
func (svm *svmTest) getAutomaticReloadSuccessTotal(ctx context.Context, t *testing.T) int {
|
|
t.Helper()
|
|
|
|
copyConfig := rest.CopyConfig(svm.server.ClientConfig)
|
|
copyConfig.GroupVersion = &schema.GroupVersion{}
|
|
copyConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer()
|
|
rc, err := rest.RESTClientFor(copyConfig)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create REST client: %v", err)
|
|
}
|
|
|
|
body, err := rc.Get().AbsPath("/metrics").DoRaw(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
metricRegex := regexp.MustCompile(fmt.Sprintf(`%s{.*} (\d+)`, metricPrefix))
|
|
for _, line := range strings.Split(string(body), "\n") {
|
|
if strings.HasPrefix(line, metricPrefix) {
|
|
matches := metricRegex.FindStringSubmatch(line)
|
|
if len(matches) == 2 {
|
|
metricValue, err := strconv.Atoi(matches[1])
|
|
if err != nil {
|
|
t.Fatalf("Failed to convert metric value to integer: %v", err)
|
|
}
|
|
return metricValue
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func (svm *svmTest) isEncryptionConfigFileUpdated(ctx context.Context, t *testing.T, metricBeforeUpdate int) bool {
|
|
t.Helper()
|
|
|
|
err := wait.PollUntilContextTimeout(
|
|
ctx,
|
|
500*time.Millisecond,
|
|
wait.ForeverTestTimeout,
|
|
true,
|
|
func(ctx context.Context) (bool, error) {
|
|
metric := svm.getAutomaticReloadSuccessTotal(ctx, t)
|
|
return metric == (metricBeforeUpdate + 1), nil
|
|
},
|
|
)
|
|
|
|
return err == nil
|
|
}
|
|
|
|
// waitForResourceMigration checks following conditions:
|
|
// 1. The svm resource has SuccessfullyMigrated condition.
|
|
// 2. The audit log contains patch events for the given secret.
|
|
func (svm *svmTest) waitForResourceMigration(
|
|
ctx context.Context,
|
|
t *testing.T,
|
|
svmName, name string,
|
|
expectedEvents int,
|
|
) bool {
|
|
t.Helper()
|
|
|
|
var isMigrated bool
|
|
err := wait.PollUntilContextTimeout(
|
|
ctx,
|
|
500*time.Millisecond,
|
|
wait.ForeverTestTimeout,
|
|
true,
|
|
func(ctx context.Context) (bool, error) {
|
|
svmResource, err := svm.getSVM(ctx, t, svmName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get SVM resource: %v", err)
|
|
}
|
|
if svmResource.Status.ResourceVersion == "" {
|
|
return false, nil
|
|
}
|
|
|
|
if storageversionmigrator.IsConditionTrue(svmResource, svmv1alpha1.MigrationSucceeded) {
|
|
isMigrated = true
|
|
}
|
|
|
|
// We utilize the LastSyncResourceVersion of the Garbage Collector (GC) to ensure that the cache is up-to-date before proceeding with the migration.
|
|
// However, in a quiet cluster, the GC may not be updated unless there is some activity or the watch receives a bookmark event after every 10 minutes.
|
|
// To expedite the update of the GC cache, we create a dummy secret and then promptly delete it.
|
|
// This action forces the GC to refresh its cache, enabling us to proceed with the migration.
|
|
_, err = svm.createSecret(ctx, t, triggerSecretName, defaultNamespace)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create secret: %v", err)
|
|
}
|
|
err = svm.client.CoreV1().Secrets(defaultNamespace).Delete(ctx, triggerSecretName, metav1.DeleteOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to delete secret: %v", err)
|
|
}
|
|
|
|
stream, err := os.Open(svm.logFile.Name())
|
|
if err != nil {
|
|
t.Fatalf("Failed to open audit log file: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := stream.Close(); err != nil {
|
|
t.Errorf("error while closing audit log file: %v", err)
|
|
}
|
|
}()
|
|
|
|
missingReport, err := utils.CheckAuditLines(
|
|
stream,
|
|
[]utils.AuditEvent{
|
|
{
|
|
Level: auditinternal.LevelMetadata,
|
|
Stage: auditinternal.StageResponseComplete,
|
|
RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=storage-version-migrator-controller", defaultNamespace, name),
|
|
Verb: "patch",
|
|
Code: 200,
|
|
User: "system:apiserver",
|
|
Resource: "secrets",
|
|
Namespace: "default",
|
|
AuthorizeDecision: "allow",
|
|
RequestObject: false,
|
|
ResponseObject: false,
|
|
},
|
|
},
|
|
auditv1.SchemeGroupVersion,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to check audit log: %v", err)
|
|
}
|
|
if (len(missingReport.MissingEvents) != 0) && (expectedEvents < missingReport.NumEventsChecked) {
|
|
isMigrated = false
|
|
}
|
|
|
|
return isMigrated, nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return isMigrated
|
|
}
|
|
|
|
func (svm *svmTest) createCRD(
|
|
t *testing.T,
|
|
name, group string,
|
|
certCtx *certContext,
|
|
crdVersions []apiextensionsv1.CustomResourceDefinitionVersion,
|
|
) *apiextensionsv1.CustomResourceDefinition {
|
|
t.Helper()
|
|
pluralName := name + "s"
|
|
listKind := name + "List"
|
|
|
|
crd := &apiextensionsv1.CustomResourceDefinition{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: pluralName + "." + group,
|
|
},
|
|
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
|
Group: group,
|
|
Names: apiextensionsv1.CustomResourceDefinitionNames{
|
|
Kind: name,
|
|
ListKind: listKind,
|
|
Plural: pluralName,
|
|
Singular: name,
|
|
},
|
|
Scope: apiextensionsv1.NamespaceScoped,
|
|
Versions: crdVersions,
|
|
Conversion: &apiextensionsv1.CustomResourceConversion{
|
|
Strategy: apiextensionsv1.WebhookConverter,
|
|
Webhook: &apiextensionsv1.WebhookConversion{
|
|
ClientConfig: &apiextensionsv1.WebhookClientConfig{
|
|
CABundle: certCtx.signingCert,
|
|
URL: ptr.To(
|
|
fmt.Sprintf("https://127.0.0.1:%d/%s", servicePort, webhookHandler),
|
|
),
|
|
},
|
|
ConversionReviewVersions: []string{"v1", "v2"},
|
|
},
|
|
},
|
|
PreserveUnknownFields: false,
|
|
},
|
|
}
|
|
|
|
apiextensionsclient, err := apiextensionsclientset.NewForConfig(svm.clientConfig)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create apiextensions client: %v", err)
|
|
}
|
|
svm.apiextensionsclient = apiextensionsclient
|
|
|
|
etcd.CreateTestCRDs(t, apiextensionsclient, false, crd)
|
|
return crd
|
|
}
|
|
|
|
func (svm *svmTest) updateCRD(
|
|
ctx context.Context,
|
|
t *testing.T,
|
|
crdName string,
|
|
updatesCRDVersions []apiextensionsv1.CustomResourceDefinitionVersion,
|
|
expectedServingVersions []string,
|
|
expectedStorageVersion string,
|
|
) {
|
|
t.Helper()
|
|
|
|
var err error
|
|
crd, err := crdintegration.UpdateV1CustomResourceDefinitionWithRetry(svm.apiextensionsclient, crdName, func(c *apiextensionsv1.CustomResourceDefinition) {
|
|
c.Spec.Versions = updatesCRDVersions
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to update CRD: %v", err)
|
|
}
|
|
|
|
svm.waitForCRDUpdate(ctx, t, crd.Spec.Names.Kind, expectedServingVersions, expectedStorageVersion)
|
|
}
|
|
|
|
func (svm *svmTest) waitForCRDUpdate(
|
|
ctx context.Context,
|
|
t *testing.T,
|
|
crdKind string,
|
|
expectedServingVersions []string,
|
|
expectedStorageVersion string,
|
|
) {
|
|
t.Helper()
|
|
|
|
err := wait.PollUntilContextTimeout(
|
|
ctx,
|
|
500*time.Millisecond,
|
|
wait.ForeverTestTimeout,
|
|
true,
|
|
func(ctx context.Context) (bool, error) {
|
|
apiGroups, _, err := svm.discoveryClient.ServerGroupsAndResources()
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to get server groups and resources: %w", err)
|
|
}
|
|
for _, api := range apiGroups {
|
|
if api.Name == crdGroup {
|
|
var servingVersions []string
|
|
for _, apiVersion := range api.Versions {
|
|
servingVersions = append(servingVersions, apiVersion.Version)
|
|
}
|
|
sort.Strings(servingVersions)
|
|
|
|
// Check if the serving versions are as expected
|
|
if reflect.DeepEqual(expectedServingVersions, servingVersions) {
|
|
expectedHash := endpointsdiscovery.StorageVersionHash(crdGroup, expectedStorageVersion, crdKind)
|
|
resourceList, err := svm.discoveryClient.ServerResourcesForGroupVersion(crdGroup + "/" + api.PreferredVersion.Version)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to get server resources for group version: %w", err)
|
|
}
|
|
|
|
// Check if the storage version is as expected
|
|
for _, resource := range resourceList.APIResources {
|
|
if resource.Kind == crdKind {
|
|
if resource.StorageVersionHash == expectedHash {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to update a CRD: Name: %s, Err: %v", crdName, err)
|
|
}
|
|
}
|
|
|
|
func (svm *svmTest) createCR(ctx context.Context, t *testing.T, crName, version string) *unstructured.Unstructured {
|
|
t.Helper()
|
|
|
|
crdResource := schema.GroupVersionResource{
|
|
Group: crdGroup,
|
|
Version: version,
|
|
Resource: crdName + "s",
|
|
}
|
|
|
|
crdUnstructured := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": crdResource.GroupVersion().String(),
|
|
"kind": crdName,
|
|
"metadata": map[string]interface{}{
|
|
"name": crName,
|
|
"namespace": defaultNamespace,
|
|
},
|
|
},
|
|
}
|
|
|
|
crdUnstructured, err := svm.dynamicClient.Resource(crdResource).Namespace(defaultNamespace).Create(ctx, crdUnstructured, metav1.CreateOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create CR: %v", err)
|
|
}
|
|
|
|
return crdUnstructured
|
|
}
|
|
|
|
func (svm *svmTest) getCR(ctx context.Context, t *testing.T, crName, version string) *unstructured.Unstructured {
|
|
t.Helper()
|
|
|
|
crdResource := schema.GroupVersionResource{
|
|
Group: crdGroup,
|
|
Version: version,
|
|
Resource: crdName + "s",
|
|
}
|
|
|
|
cr, err := svm.dynamicClient.Resource(crdResource).Namespace(defaultNamespace).Get(ctx, crName, metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to get CR: %v", err)
|
|
}
|
|
|
|
return cr
|
|
}
|
|
|
|
func (svm *svmTest) listCR(ctx context.Context, t *testing.T, version string) error {
|
|
t.Helper()
|
|
|
|
crdResource := schema.GroupVersionResource{
|
|
Group: crdGroup,
|
|
Version: version,
|
|
Resource: crdName + "s",
|
|
}
|
|
|
|
_, err := svm.dynamicClient.Resource(crdResource).Namespace(defaultNamespace).List(ctx, metav1.ListOptions{})
|
|
|
|
return err
|
|
}
|
|
|
|
func (svm *svmTest) deleteCR(ctx context.Context, t *testing.T, name, version string) {
|
|
t.Helper()
|
|
crdResource := schema.GroupVersionResource{
|
|
Group: crdGroup,
|
|
Version: version,
|
|
Resource: crdName + "s",
|
|
}
|
|
err := svm.dynamicClient.Resource(crdResource).Namespace(defaultNamespace).Delete(ctx, name, metav1.DeleteOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to delete CR: %v", err)
|
|
}
|
|
}
|
|
|
|
func (svm *svmTest) createConversionWebhook(ctx context.Context, t *testing.T, certCtx *certContext) context.CancelFunc {
|
|
t.Helper()
|
|
http.HandleFunc(fmt.Sprintf("/%s", webhookHandler), converter.ServeExampleConvert)
|
|
|
|
block, _ := pem.Decode(certCtx.key)
|
|
if block == nil {
|
|
panic("failed to parse PEM block containing the key")
|
|
}
|
|
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse private key: %v", err)
|
|
}
|
|
|
|
blockCer, _ := pem.Decode(certCtx.cert)
|
|
if blockCer == nil {
|
|
panic("failed to parse PEM block containing the key")
|
|
}
|
|
webhookCert, err := x509.ParseCertificate(blockCer.Bytes)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse certificate: %v", err)
|
|
}
|
|
|
|
server := &http.Server{
|
|
Addr: fmt.Sprintf("127.0.0.1:%d", servicePort),
|
|
TLSConfig: &tls.Config{
|
|
Certificates: []tls.Certificate{
|
|
{
|
|
Certificate: [][]byte{webhookCert.Raw},
|
|
PrivateKey: key,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
go func() {
|
|
// skipping error handling here because this always returns a non-nil error.
|
|
// after Server.Shutdown, the returned error is ErrServerClosed.
|
|
_ = server.ListenAndServeTLS("", "")
|
|
|
|
}()
|
|
|
|
serverCtx, cancel := context.WithCancel(ctx)
|
|
go func(ctx context.Context, t *testing.T) {
|
|
<-ctx.Done()
|
|
// Context was cancelled, shutdown the server
|
|
if err := server.Shutdown(context.Background()); err != nil {
|
|
t.Logf("Failed to shutdown server: %v", err)
|
|
}
|
|
}(serverCtx, t)
|
|
|
|
return cancel
|
|
}
|
|
|
|
type certContext struct {
|
|
cert []byte
|
|
key []byte
|
|
signingCert []byte
|
|
}
|
|
|
|
func (svm *svmTest) setupServerCert(t *testing.T) *certContext {
|
|
t.Helper()
|
|
certDir, err := os.MkdirTemp("", "test-e2e-server-cert")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a temp dir for cert generation %v", err)
|
|
}
|
|
defer func(path string) {
|
|
err := os.RemoveAll(path)
|
|
if err != nil {
|
|
t.Fatalf("Failed to remove temp dir %v", err)
|
|
}
|
|
}(certDir)
|
|
signingKey, err := utils.NewPrivateKey()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create CA private key %v", err)
|
|
}
|
|
signingCert, err := cert.NewSelfSignedCACert(cert.Config{CommonName: "e2e-server-cert-ca"}, signingKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create CA cert for apiserver %v", err)
|
|
}
|
|
caCertFile, err := os.CreateTemp(certDir, "ca.crt")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a temp file for ca cert generation %v", err)
|
|
}
|
|
defer utiltesting.CloseAndRemove(&testing.T{}, caCertFile)
|
|
if err := os.WriteFile(caCertFile.Name(), utils.EncodeCertPEM(signingCert), 0644); err != nil {
|
|
t.Fatalf("Failed to write CA cert %v", err)
|
|
}
|
|
key, err := utils.NewPrivateKey()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create private key for %v", err)
|
|
}
|
|
signedCert, err := utils.NewSignedCert(
|
|
&cert.Config{
|
|
CommonName: "127.0.0.1",
|
|
AltNames: cert.AltNames{
|
|
IPs: []net.IP{utilnet.ParseIPSloppy("127.0.0.1")},
|
|
},
|
|
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
},
|
|
key, signingCert, signingKey,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create cert%v", err)
|
|
}
|
|
certFile, err := os.CreateTemp(certDir, "server.crt")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a temp file for cert generation %v", err)
|
|
}
|
|
defer utiltesting.CloseAndRemove(&testing.T{}, certFile)
|
|
keyFile, err := os.CreateTemp(certDir, "server.key")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a temp file for key generation %v", err)
|
|
}
|
|
if err = os.WriteFile(certFile.Name(), utils.EncodeCertPEM(signedCert), 0600); err != nil {
|
|
t.Fatalf("Failed to write cert file %v", err)
|
|
}
|
|
privateKeyPEM, err := keyutil.MarshalPrivateKeyToPEM(key)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal key %v", err)
|
|
}
|
|
if err = os.WriteFile(keyFile.Name(), privateKeyPEM, 0644); err != nil {
|
|
t.Fatalf("Failed to write key file %v", err)
|
|
}
|
|
defer utiltesting.CloseAndRemove(&testing.T{}, keyFile)
|
|
return &certContext{
|
|
cert: utils.EncodeCertPEM(signedCert),
|
|
key: privateKeyPEM,
|
|
signingCert: utils.EncodeCertPEM(signingCert),
|
|
}
|
|
}
|
|
|
|
func (svm *svmTest) isCRStoredAtVersion(t *testing.T, version, crName string) bool {
|
|
t.Helper()
|
|
|
|
data, err := svm.getRawCRFromETCD(t, crName, defaultNamespace, crdGroup, crdName+"s")
|
|
if err != nil {
|
|
t.Fatalf("Failed to get CR from etcd: %v", err)
|
|
}
|
|
|
|
// parse data to unstructured.Unstructured
|
|
obj := &unstructured.Unstructured{}
|
|
err = obj.UnmarshalJSON(data)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal data to unstructured: %v", err)
|
|
}
|
|
|
|
return obj.GetAPIVersion() == fmt.Sprintf("%s/%s", crdGroup, version)
|
|
}
|
|
|
|
func (svm *svmTest) isCRDMigrated(ctx context.Context, t *testing.T, crdSVMName string) bool {
|
|
t.Helper()
|
|
|
|
err := wait.PollUntilContextTimeout(
|
|
ctx,
|
|
500*time.Millisecond,
|
|
1*time.Minute,
|
|
true,
|
|
func(ctx context.Context) (bool, error) {
|
|
triggerCR := svm.createCR(ctx, t, "triggercr", "v1")
|
|
svm.deleteCR(ctx, t, triggerCR.GetName(), "v1")
|
|
svmResource, err := svm.getSVM(ctx, t, crdSVMName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get SVM resource: %v", err)
|
|
}
|
|
if svmResource.Status.ResourceVersion == "" {
|
|
return false, nil
|
|
}
|
|
|
|
if storageversionmigrator.IsConditionTrue(svmResource, svmv1alpha1.MigrationSucceeded) {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
},
|
|
)
|
|
return err == nil
|
|
}
|
|
|
|
type versions struct {
|
|
generation int64
|
|
rv string
|
|
isRVUpdated bool
|
|
}
|
|
|
|
func (svm *svmTest) validateRVAndGeneration(ctx context.Context, t *testing.T, crVersions map[string]versions) {
|
|
t.Helper()
|
|
|
|
for crName, version := range crVersions {
|
|
// get CR from etcd
|
|
data, err := svm.getRawCRFromETCD(t, crName, defaultNamespace, crdGroup, crdName+"s")
|
|
if err != nil {
|
|
t.Fatalf("Failed to get CR from etcd: %v", err)
|
|
}
|
|
|
|
// parse data to unstructured.Unstructured
|
|
obj := &unstructured.Unstructured{}
|
|
err = obj.UnmarshalJSON(data)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal data to unstructured: %v", err)
|
|
}
|
|
|
|
// validate resourceVersion and generation
|
|
crVersion := svm.getCR(ctx, t, crName, "v2").GetResourceVersion()
|
|
if version.isRVUpdated && crVersion == version.rv {
|
|
t.Fatalf("ResourceVersion of CR %s should not be equal. Expected: %s, Got: %s", crName, version.rv, crVersion)
|
|
}
|
|
if obj.GetGeneration() != version.generation {
|
|
t.Fatalf("Generation of CR %s should be equal. Expected: %d, Got: %d", crName, version.generation, obj.GetGeneration())
|
|
}
|
|
}
|
|
}
|