...

Source file src/edge-infra.dev/pkg/edge/iam/ctl/providerctl/provider_encryption_controller_test.go

Documentation: edge-infra.dev/pkg/edge/iam/ctl/providerctl

     1  package providerctl_test
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path/filepath"
     7  	"reflect"
     8  	"testing"
     9  	"time"
    10  
    11  	goext "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/suite"
    14  	corev1 "k8s.io/api/core/v1"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/runtime"
    17  	"k8s.io/apimachinery/pkg/types"
    18  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    19  	ctrl "sigs.k8s.io/controller-runtime"
    20  
    21  	edgeConditions "edge-infra.dev/pkg/k8s/runtime/conditions"
    22  	"edge-infra.dev/pkg/lib/fog"
    23  
    24  	"k8s.io/apimachinery/pkg/api/errors"
    25  	"sigs.k8s.io/controller-runtime/pkg/client"
    26  
    27  	"edge-infra.dev/test"
    28  	"edge-infra.dev/test/framework"
    29  	"edge-infra.dev/test/framework/k8s"
    30  
    31  	"edge-infra.dev/pkg/edge/constants"
    32  	api "edge-infra.dev/pkg/edge/iam/api/v1alpha1"
    33  	"edge-infra.dev/pkg/edge/iam/ctl/providerctl"
    34  	"edge-infra.dev/pkg/k8s/runtime/controller"
    35  	unstructuredutil "edge-infra.dev/pkg/k8s/unstructured"
    36  	"edge-infra.dev/test/framework/k8s/envtest"
    37  )
    38  
    39  // bazel test pkg/edge/iam/ctl/providerctl:providerctl_test
    40  type Suite struct {
    41  	*framework.Framework
    42  	*k8s.K8s
    43  	ctx     context.Context
    44  	timeout time.Duration
    45  	tick    time.Duration
    46  }
    47  
    48  func TestProviderEncryptionReconciler(t *testing.T) {
    49  	// test setup
    50  	testEnv := envtest.Setup()
    51  	ctrl.SetLogger(fog.New())
    52  	cfg, opts := controller.ProcessOptions(controller.WithCfg(testEnv.Config), controller.WithMetricsAddress("0"))
    53  	opts.Scheme = createScheme()
    54  	mgr, err := ctrl.NewManager(cfg, opts)
    55  	if err != nil {
    56  		t.Errorf("unable to create  manager %v", err)
    57  	}
    58  
    59  	// get manifest path
    60  	cwd, err := os.Getwd()
    61  	if err != nil {
    62  		t.Errorf("error getting working directory: %v", err)
    63  	}
    64  
    65  	// go up 4 directories so we can go into /config
    66  	parentDir := filepath.Dir(cwd)
    67  	for i := 0; i < 4; i++ {
    68  		parentDir = filepath.Dir(parentDir)
    69  	}
    70  
    71  	// set env variables
    72  	os.Setenv("IAM_CLUSTER_ID", "123")
    73  	os.Setenv("IAM_ENCRYPTION_ENABLED", "true")
    74  	os.Setenv("IAM_ENV_TEST", "true")
    75  	os.Setenv("IAM_ENCRYPTION_KEY", "my-32bit-super-extra-secret-key!")
    76  	os.Setenv("IAM_ORGANIZATION_ID", "4efa46628b914711bce36939abcfd084")
    77  	os.Setenv("IAM_ORGANIZATION_NAME", "dev-ex")
    78  	os.Setenv("IAM_SITE_ID", "dev-site")
    79  
    80  	f := framework.New("providerctl").Component("providerctl")
    81  	s := &Suite{
    82  		Framework: f,
    83  		ctx:       context.Background(),
    84  		timeout:   60 * time.Second,
    85  		tick:      50 * time.Millisecond,
    86  	}
    87  
    88  	resmaps, err := providerctl.CreateResmaps([]string{"test/test_manifests.yaml"})
    89  	assert.NoError(t, err)
    90  	assert.NotEmpty(t, resmaps)
    91  
    92  	// match key to target in createProviderObj
    93  	resmaps["kind"] = resmaps["test"]
    94  
    95  	// create provider reconciler
    96  	r := &providerctl.ProviderReconciler{
    97  		Name:    "provider-controller",
    98  		Client:  mgr.GetClient(),
    99  		Scheme:  mgr.GetScheme(),
   100  		Resmaps: resmaps,
   101  	}
   102  	err = r.SetupWithManager(mgr)
   103  	test.NoError(err)
   104  
   105  	k := k8s.New(testEnv.Config, k8s.WithCtrlManager(mgr), k8s.WithKonfigKonnector())
   106  	s.K8s = k
   107  
   108  	f.Register(k)
   109  
   110  	suite.Run(t, s)
   111  
   112  	t.Cleanup(func() {
   113  		f.NoError(testEnv.Stop())
   114  	})
   115  }
   116  
   117  // confirm external secret created
   118  func (s *Suite) TestReconcileExternalSecretCreation() {
   119  	namespace := &corev1.Namespace{
   120  		TypeMeta: metav1.TypeMeta{
   121  			Kind:       "Namespace",
   122  			APIVersion: "v1",
   123  		},
   124  		ObjectMeta: metav1.ObjectMeta{
   125  			Name: "edge-iam",
   126  		},
   127  	}
   128  	s.Require().NoError(s.Client.Create(s.ctx, namespace))
   129  
   130  	// create private-key-secret
   131  	secret := &corev1.Secret{}
   132  	secret.ObjectMeta = metav1.ObjectMeta{
   133  		Name:      "private-key-secret",
   134  		Namespace: "edge-iam",
   135  	}
   136  	secret.Data = map[string][]byte{
   137  		"private_key":    []byte("key"),
   138  		"private_key_id": []byte("key-id"),
   139  	}
   140  	s.Require().NoError(s.Client.Create(s.ctx, secret))
   141  
   142  	// create challenge-secret
   143  	challengeSecret := &corev1.Secret{}
   144  	challengeSecret.ObjectMeta = metav1.ObjectMeta{
   145  		Name:      "challenge-secret",
   146  		Namespace: "edge-iam",
   147  	}
   148  	challengeSecret.Data = map[string][]byte{
   149  		"secret": []byte("secret"),
   150  	}
   151  	s.Require().NoError(s.Client.Create(s.ctx, challengeSecret))
   152  
   153  	// create initial provider obj w/ spec.encryption.version = 1
   154  	provider := createProviderObj("1", "1")
   155  	s.Require().NoError(s.Client.Create(s.ctx, provider))
   156  
   157  	// initially provider's status is empty before reconciliation, so saving it for later
   158  	emptyStatus := provider.Status
   159  
   160  	// check the if our first encryption key exists
   161  	var externalSecret = &goext.ExternalSecret{}
   162  	s.Require().Eventually(func() bool {
   163  		err := s.Client.Get(s.ctx, types.NamespacedName{
   164  			Name:      providerctl.EncryptionKeySecretPrefix + "1",
   165  			Namespace: "edge-iam",
   166  		}, externalSecret)
   167  		return err == nil
   168  	}, s.timeout, s.tick, "expected external secret was never found")
   169  
   170  	// grab provider
   171  	providerObj := &api.Provider{}
   172  	s.Require().Eventually(func() bool {
   173  		err := s.Client.Get(s.ctx, types.NamespacedName{
   174  			Name:      "provider",
   175  			Namespace: "edge-iam",
   176  		}, providerObj)
   177  		// confirm provider exists & status not empty
   178  		return !reflect.DeepEqual(providerObj.Status, emptyStatus) && err == nil
   179  	}, s.timeout, s.tick, "expected provider object with non-empty status was never found")
   180  
   181  	// update provider to version 2
   182  	providerObj.Spec.Encryption.Version = "2"
   183  	s.Require().NoError(s.Client.Update(s.ctx, providerObj))
   184  
   185  	// check for the new v2 external secret
   186  	var externalSecret2 = &goext.ExternalSecret{}
   187  	s.Require().Eventually(func() bool {
   188  		err := s.Client.Get(s.ctx, types.NamespacedName{
   189  			Name:      providerctl.EncryptionKeySecretPrefix + "2",
   190  			Namespace: "edge-iam",
   191  		}, externalSecret2)
   192  		return err == nil
   193  	}, s.timeout, s.tick, "expected second version of external secret was never found")
   194  
   195  	// check provider to confirm version is updated
   196  	providerObj2 := &api.Provider{}
   197  	s.Require().Eventually(func() bool {
   198  		err := s.Client.Get(s.ctx, types.NamespacedName{
   199  			Name:      "provider",
   200  			Namespace: "edge-iam",
   201  		}, providerObj2)
   202  		return !reflect.DeepEqual(providerObj2.Status, emptyStatus) && err == nil
   203  	}, s.timeout, s.tick, "expected second provider object with non-empty status was never found")
   204  
   205  	s.Require().True(providerObj2.Spec.Encryption.Version == "2")
   206  
   207  	// update status to pretend we successfully updated the databases with version 2
   208  	gen := providerObj2.GetGeneration()
   209  	newCondition := metav1.Condition{
   210  		Type:               "DatabaseUpdated",
   211  		Status:             metav1.ConditionTrue,
   212  		Reason:             "EncryptionRotationSucceeded",
   213  		Message:            "successfully updated databases to version: 2",
   214  		LastTransitionTime: metav1.Now(),
   215  		ObservedGeneration: gen,
   216  	}
   217  	patch := client.MergeFrom(providerObj2.DeepCopy())
   218  	edgeConditions.Set(providerObj2, &newCondition)
   219  
   220  	s.Require().NoError(s.Client.Status().Patch(s.ctx, providerObj2, patch))
   221  
   222  	// check provider again to confirm status is updated
   223  	providerObj3 := &api.Provider{}
   224  	s.Require().Eventually(func() bool {
   225  		err := s.Client.Get(s.ctx, types.NamespacedName{
   226  			Name:      "provider",
   227  			Namespace: "edge-iam",
   228  		}, providerObj3)
   229  		return !reflect.DeepEqual(providerObj3.Status, emptyStatus) && err == nil
   230  	}, s.timeout, s.tick, "expected second provider object with non-empty status was never found")
   231  
   232  	// confirming new status was updated, should have 2 conditions now:
   233  	// DatabaseUpdated and InstallationSucceeded
   234  	s.Require().Equal(len(providerObj3.Status.Conditions), 2)
   235  
   236  	// check that the v1 external secret was deleted
   237  	var externalSecret3 = &goext.ExternalSecret{}
   238  	s.Require().Eventually(func() bool {
   239  		err := s.Client.Get(s.ctx, types.NamespacedName{
   240  			Name:      providerctl.EncryptionKeySecretPrefix + "1",
   241  			Namespace: "edge-iam",
   242  		}, externalSecret3)
   243  
   244  		// should have been deleted if spec & status version are the both version 2
   245  		if err != nil {
   246  			if errors.IsNotFound(err) {
   247  				return true
   248  			}
   249  		}
   250  		return false
   251  	}, s.timeout, s.tick, "external secret was found or errored, when it should have been deleted")
   252  }
   253  
   254  // test if the created external secret matches the expected
   255  func TestCreateEncryptionExternalSecret(t *testing.T) {
   256  	externalSecret := &goext.ExternalSecret{
   257  		TypeMeta: metav1.TypeMeta{
   258  			APIVersion: goext.ExtSecretGroupVersionKind.GroupVersion().String(),
   259  			Kind:       goext.ExtSecretGroupVersionKind.Kind,
   260  		},
   261  		ObjectMeta: metav1.ObjectMeta{
   262  			Name:      providerctl.EncryptionKeySecretPrefix + "1",
   263  			Namespace: "edge-iam",
   264  			Labels: map[string]string{
   265  				constants.PlatformComponent: "edge-iam",
   266  			},
   267  		},
   268  		Spec: goext.ExternalSecretSpec{
   269  			DataFrom: []goext.ExternalSecretDataFromRemoteRef{
   270  				{
   271  					Extract: &goext.ExternalSecretDataRemoteRef{
   272  						Key:     providerctl.EncryptionKeySecretPrefix + "123",
   273  						Version: "1",
   274  					},
   275  				},
   276  			},
   277  			RefreshInterval: &metav1.Duration{
   278  				Duration: time.Minute,
   279  			},
   280  			SecretStoreRef: goext.SecretStoreRef{
   281  				Name: "gcp-provider",
   282  				Kind: "ClusterSecretStore",
   283  			},
   284  			Target: goext.ExternalSecretTarget{
   285  				Name:           providerctl.EncryptionKeySecretPrefix + "1",
   286  				CreationPolicy: goext.CreatePolicyOwner,
   287  			},
   288  		},
   289  	}
   290  
   291  	uobj, err := unstructuredutil.ToUnstructured(externalSecret)
   292  	if err != nil {
   293  		t.Error("Unable to create unstructured external secret")
   294  	}
   295  
   296  	extSec, err := providerctl.CreateEncryptionExternalSecret("1", providerctl.EncryptionKeySecretPrefix+"123", providerctl.EncryptionKeySecretPrefix+"1")
   297  	if err != nil {
   298  		t.Error("Unable to create encryption external secret")
   299  	}
   300  
   301  	assert.Equal(t, uobj, extSec)
   302  }
   303  
   304  func TestDoesStatusMatchSpecVersion(t *testing.T) {
   305  	// create provider where db's version == provider.encryption.version
   306  	provider := createProviderObj("1", "1")
   307  	_, versionMatch := providerctl.DoesStatusMatchSpecVersion(*provider)
   308  	if !versionMatch {
   309  		t.Errorf("Error checking the status where the provider & db's versions match")
   310  	}
   311  
   312  	// create provider where db's version != the provider.encryption.version
   313  	provider = createProviderObj("1", "2")
   314  	_, versionMatch = providerctl.DoesStatusMatchSpecVersion(*provider)
   315  	if versionMatch {
   316  		t.Errorf("Error checking the status where the provider & db's versions do not match")
   317  	}
   318  }
   319  
   320  func createProviderObj(specVersion string, statusVersion string) *api.Provider {
   321  	statusMessage := "successfully updated databases to version: " + statusVersion
   322  	providerObj := &api.Provider{
   323  		TypeMeta: metav1.TypeMeta{
   324  			APIVersion: "iam.edge-infra.dev/v1alpha1",
   325  			Kind:       "Provider",
   326  		},
   327  		ObjectMeta: metav1.ObjectMeta{
   328  			Name:      "provider",
   329  			Namespace: "edge-iam",
   330  		},
   331  		Spec: api.ProviderSpec{
   332  			Encryption: api.EncryptionFields{
   333  				Version: specVersion,
   334  			},
   335  			Issuer: "http://localhost:8080",
   336  			Target: "kind",
   337  			Barcode: api.BarcodeSpec{
   338  				Expire: "4320h",
   339  				Role:   false,
   340  			},
   341  		},
   342  		Status: api.ProviderStatus{
   343  			Conditions: []metav1.Condition{
   344  				{
   345  					Type:    "DatabaseUpdated",
   346  					Status:  metav1.ConditionTrue,
   347  					Reason:  "EncryptionRotationSucceeded",
   348  					Message: statusMessage,
   349  				},
   350  			},
   351  		},
   352  	}
   353  	return providerObj
   354  }
   355  
   356  func createScheme() *runtime.Scheme {
   357  	scheme := runtime.NewScheme()
   358  	utilruntime.Must(api.AddToScheme(scheme))
   359  	utilruntime.Must(goext.AddToScheme(scheme))
   360  	utilruntime.Must(corev1.AddToScheme(scheme))
   361  	return scheme
   362  }
   363  

View as plain text