-
Notifications
You must be signed in to change notification settings - Fork 273
Expand file tree
/
Copy pathscenarios.go
More file actions
357 lines (316 loc) · 16.8 KB
/
Copy pathscenarios.go
File metadata and controls
357 lines (316 loc) · 16.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package encryption
import (
"context"
"fmt"
mathrand "math/rand/v2"
"strings"
"testing"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/rand"
configv1 "github.com/openshift/api/config/v1"
)
type BasicScenario struct {
Namespace string
LabelSelector string
EncryptionConfigSecretName string
EncryptionConfigSecretNamespace string
OperatorNamespace string
TargetGRs []schema.GroupResource
AssertFunc func(t testing.TB, clientSet ClientSet, expectedMode configv1.EncryptionType, namespace, labelSelector string)
}
// EncryptionProvider pairs an encryption config with an optional setup function
// that ensures prerequisites (secrets, credentials, infrastructure) are in place.
type EncryptionProvider struct {
configv1.APIServerEncryption
// Setup is called once before the provider is first used. May be nil.
Setup func(ctx context.Context, t testing.TB)
}
func TestEncryptionTypeIdentity(ctx context.Context, t testing.TB, scenario BasicScenario) {
e := NewE(t, PrintEventsOnFailure(scenario.OperatorNamespace))
clientSet := SetAndWaitForEncryptionType(ctx, e, EncryptionProvider{APIServerEncryption: configv1.APIServerEncryption{Type: configv1.EncryptionTypeIdentity}}, scenario.TargetGRs, scenario.Namespace, scenario.LabelSelector)
scenario.AssertFunc(e, clientSet, configv1.EncryptionTypeIdentity, scenario.Namespace, scenario.LabelSelector)
}
func TestEncryptionTypeUnset(ctx context.Context, t testing.TB, scenario BasicScenario) {
e := NewE(t, PrintEventsOnFailure(scenario.OperatorNamespace))
clientSet := SetAndWaitForEncryptionType(ctx, e, EncryptionProvider{}, scenario.TargetGRs, scenario.Namespace, scenario.LabelSelector)
scenario.AssertFunc(e, clientSet, configv1.EncryptionTypeIdentity, scenario.Namespace, scenario.LabelSelector)
}
func resolveProvider(t testing.TB, defaultType configv1.EncryptionType, providers []EncryptionProvider) EncryptionProvider {
t.Helper()
if len(providers) > 1 {
t.Fatalf("expected at most one provider, got %d", len(providers))
}
if len(providers) == 1 {
return providers[0]
}
return EncryptionProvider{APIServerEncryption: configv1.APIServerEncryption{Type: defaultType}}
}
func TestEncryptionTypeAESCBC(ctx context.Context, t testing.TB, scenario BasicScenario, providers ...EncryptionProvider) {
provider := resolveProvider(t, configv1.EncryptionTypeAESCBC, providers)
e := NewE(t, PrintEventsOnFailure(scenario.OperatorNamespace))
clientSet := SetAndWaitForEncryptionType(ctx, e, provider, scenario.TargetGRs, scenario.Namespace, scenario.LabelSelector)
scenario.AssertFunc(e, clientSet, provider.Type, scenario.Namespace, scenario.LabelSelector)
AssertEncryptionConfig(e, clientSet, scenario.EncryptionConfigSecretName, scenario.EncryptionConfigSecretNamespace, scenario.TargetGRs)
}
func TestEncryptionTypeAESGCM(ctx context.Context, t testing.TB, scenario BasicScenario, providers ...EncryptionProvider) {
provider := resolveProvider(t, configv1.EncryptionTypeAESGCM, providers)
e := NewE(t, PrintEventsOnFailure(scenario.OperatorNamespace))
clientSet := SetAndWaitForEncryptionType(ctx, e, provider, scenario.TargetGRs, scenario.Namespace, scenario.LabelSelector)
scenario.AssertFunc(e, clientSet, provider.Type, scenario.Namespace, scenario.LabelSelector)
AssertEncryptionConfig(e, clientSet, scenario.EncryptionConfigSecretName, scenario.EncryptionConfigSecretNamespace, scenario.TargetGRs)
}
func TestEncryptionTypeKMS(ctx context.Context, t testing.TB, scenario BasicScenario, providers ...EncryptionProvider) {
provider := resolveProvider(t, configv1.EncryptionTypeKMS, providers)
e := NewE(t, PrintEventsOnFailure(scenario.OperatorNamespace))
clientSet := SetAndWaitForEncryptionType(ctx, e, provider, scenario.TargetGRs, scenario.Namespace, scenario.LabelSelector)
scenario.AssertFunc(e, clientSet, provider.Type, scenario.Namespace, scenario.LabelSelector)
AssertEncryptionConfig(e, clientSet, scenario.EncryptionConfigSecretName, scenario.EncryptionConfigSecretNamespace, scenario.TargetGRs)
}
func TestEncryptionType(ctx context.Context, t testing.TB, scenario BasicScenario, provider EncryptionProvider) {
switch provider.Type {
case configv1.EncryptionTypeAESCBC:
TestEncryptionTypeAESCBC(ctx, t, scenario, provider)
case configv1.EncryptionTypeAESGCM:
TestEncryptionTypeAESGCM(ctx, t, scenario, provider)
case configv1.EncryptionTypeKMS:
TestEncryptionTypeKMS(ctx, t, scenario, provider)
case configv1.EncryptionTypeIdentity, "":
TestEncryptionTypeIdentity(ctx, t, scenario)
default:
t.Fatalf("Unknown encryption type: %s", provider.Type)
}
}
type OnOffScenario struct {
BasicScenario
CreateResourceFunc func(t testing.TB, clientSet ClientSet, namespace string) runtime.Object
AssertResourceEncryptedFunc func(t testing.TB, clientSet ClientSet, resource runtime.Object)
AssertResourceNotEncryptedFunc func(t testing.TB, clientSet ClientSet, resource runtime.Object)
ResourceFunc func(t testing.TB, namespace string) runtime.Object
ResourceName string
EncryptionProvider EncryptionProvider
}
type testStep struct {
name string
testFunc func(testing.TB)
}
func TestEncryptionTurnOnAndOff(ctx context.Context, t testing.TB, scenario OnOffScenario) {
scenarios := []testStep{
{name: fmt.Sprintf("CreateAndStore%s", scenario.ResourceName), testFunc: func(t testing.TB) {
e := NewE(t)
scenario.CreateResourceFunc(e, GetClients(e), scenario.Namespace)
}},
{name: fmt.Sprintf("On%s", strings.ToUpper(string(scenario.EncryptionProvider.Type))), testFunc: func(t testing.TB) { TestEncryptionType(ctx, t, scenario.BasicScenario, scenario.EncryptionProvider) }},
{name: fmt.Sprintf("Assert%sEncrypted", scenario.ResourceName), testFunc: func(t testing.TB) {
e := NewE(t)
scenario.AssertResourceEncryptedFunc(e, GetClients(e), scenario.ResourceFunc(e, scenario.Namespace))
}},
{name: "OffIdentity", testFunc: func(t testing.TB) { TestEncryptionTypeIdentity(ctx, t, scenario.BasicScenario) }},
{name: fmt.Sprintf("Assert%sNotEncrypted", scenario.ResourceName), testFunc: func(t testing.TB) {
e := NewE(t)
scenario.AssertResourceNotEncryptedFunc(e, GetClients(e), scenario.ResourceFunc(e, scenario.Namespace))
}},
{name: fmt.Sprintf("On%sSecond", strings.ToUpper(string(scenario.EncryptionProvider.Type))), testFunc: func(t testing.TB) { TestEncryptionType(ctx, t, scenario.BasicScenario, scenario.EncryptionProvider) }},
{name: fmt.Sprintf("Assert%sEncryptedSecond", scenario.ResourceName), testFunc: func(t testing.TB) {
e := NewE(t)
scenario.AssertResourceEncryptedFunc(e, GetClients(e), scenario.ResourceFunc(e, scenario.Namespace))
}},
{name: "OffIdentitySecond", testFunc: func(t testing.TB) { TestEncryptionTypeIdentity(ctx, t, scenario.BasicScenario) }},
{name: fmt.Sprintf("Assert%sNotEncryptedSecond", scenario.ResourceName), testFunc: func(t testing.TB) {
e := NewE(t)
scenario.AssertResourceNotEncryptedFunc(e, GetClients(e), scenario.ResourceFunc(e, scenario.Namespace))
}},
}
// run scenarios
for _, testScenario := range scenarios {
t.Logf("=== STEP: %s ===", testScenario.name)
testScenario.testFunc(t)
if t.Failed() {
t.Errorf("stopping the test as %q scenario failed", testScenario.name)
return
}
}
}
// ProvidersMigrationScenario defines a test scenario for migrating encryption
// between multiple providers.
//
// See TestEncryptionProvidersMigration for more details.
type ProvidersMigrationScenario struct {
BasicScenario
CreateResourceFunc func(t testing.TB, clientSet ClientSet, namespace string) runtime.Object
AssertResourceEncryptedFunc func(t testing.TB, clientSet ClientSet, resource runtime.Object)
AssertResourceNotEncryptedFunc func(t testing.TB, clientSet ClientSet, resource runtime.Object)
ResourceFunc func(t testing.TB, namespace string) runtime.Object
ResourceName string
// EncryptionProviders is the list of encryption providers to migrate through.
// The test will migrate through each provider in order, then always end by
// switching to identity (off) to verify the resource is re-written unencrypted.
EncryptionProviders []EncryptionProvider
}
// ShuffleEncryptionProviders returns a new slice with the providers in random order,
// leaving the original slice unchanged. Use this to test different migration orderings.
func ShuffleEncryptionProviders(providers []EncryptionProvider) []EncryptionProvider {
shuffled := make([]EncryptionProvider, len(providers))
copy(shuffled, providers)
mathrand.Shuffle(len(shuffled), func(i, j int) {
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
})
return shuffled
}
// TestEncryptionProvidersMigration tests migration between given encryption providers.
// It creates a resource, migrates through each provider,
// verifies the resource is encrypted after each migration, and finally
// switches to identity (off).
func TestEncryptionProvidersMigration(ctx context.Context, t testing.TB, scenario ProvidersMigrationScenario) {
if len(scenario.EncryptionProviders) < 2 {
t.Fatalf("ProvidersMigrationScenario requires at least 2 encryption providers, got %d", len(scenario.EncryptionProviders))
}
for _, provider := range scenario.EncryptionProviders {
if provider.Type == configv1.EncryptionTypeIdentity || provider.Type == "" {
t.Fatalf("Unsupported encryption provider %q passed", provider.Type)
}
}
// step 1: create the resource
scenarios := []testStep{
{name: fmt.Sprintf("CreateAndStore%s", scenario.ResourceName), testFunc: func(t testing.TB) {
e := NewE(t)
scenario.CreateResourceFunc(e, GetClients(e), scenario.Namespace)
}},
}
// step 2: migrate through each provider in sequence
for i, provider := range scenario.EncryptionProviders {
prefix := "EncryptWith"
if i > 0 {
prefix = "MigrateTo"
}
scenarios = append(scenarios,
testStep{name: fmt.Sprintf("%s%s", prefix, strings.ToUpper(string(provider.Type))), testFunc: func(t testing.TB) {
TestEncryptionType(ctx, t, scenario.BasicScenario, provider)
}},
testStep{name: fmt.Sprintf("Assert%sEncrypted", scenario.ResourceName), testFunc: func(t testing.TB) {
e := NewE(t)
scenario.AssertResourceEncryptedFunc(e, GetClients(e), scenario.ResourceFunc(e, scenario.Namespace))
}},
)
}
// step 3: switch to identity (off) to verify the resource is re-written unencrypted
scenarios = append(scenarios, testStep{name: fmt.Sprintf("OffIdentityAndAssert%sNotEncrypted", scenario.ResourceName), testFunc: func(t testing.TB) {
TestEncryptionTypeIdentity(ctx, t, scenario.BasicScenario)
e := NewE(t)
scenario.AssertResourceNotEncryptedFunc(e, GetClients(e), scenario.ResourceFunc(e, scenario.Namespace))
}})
// run scenarios
for _, testScenario := range scenarios {
t.Logf("=== STEP: %s ===", testScenario.name)
testScenario.testFunc(t)
if t.Failed() {
t.Errorf("stopping the test as %q scenario failed", testScenario.name)
return
}
}
}
type RotationScenario struct {
BasicScenario
CreateResourceFunc func(t testing.TB, clientSet ClientSet, namespace string) runtime.Object
GetRawResourceFunc func(t testing.TB, clientSet ClientSet, namespace string) string
UnsupportedConfigFunc UpdateUnsupportedConfigFunc
EncryptionProvider EncryptionProvider
}
// TestEncryptionRotation first encrypts data with aescbc key
// then it forces a key rotation by setting the "encyrption.Reason" in the operator's configuration file
func TestEncryptionRotation(ctx context.Context, t testing.TB, scenario RotationScenario) {
// test data
ns := scenario.Namespace
labelSelector := scenario.LabelSelector
// step 1: create the desired resource
e := NewE(t)
clientSet := GetClients(e)
scenario.CreateResourceFunc(e, GetClients(e), ns)
// step 2: run provided encryption scenario
TestEncryptionType(ctx, t, scenario.BasicScenario, scenario.EncryptionProvider)
// step 3: take samples
rawEncryptedResourceWithKey1 := scenario.GetRawResourceFunc(e, clientSet, ns)
// step 4: force key rotation and wait for migration to complete
lastMigratedKeyMeta, err := GetLastKeyMeta(t, clientSet.Kube, ns, labelSelector)
require.NoError(e, err)
require.NoError(e, ForceKeyRotation(e, scenario.UnsupportedConfigFunc, fmt.Sprintf("test-key-rotation-%s", rand.String(4))))
WaitForNextMigratedKey(e, clientSet.Kube, lastMigratedKeyMeta, scenario.TargetGRs, ns, labelSelector)
scenario.AssertFunc(e, clientSet, scenario.EncryptionProvider.Type, ns, labelSelector)
// step 5: verify if the provided resource was encrypted with a different key (step 2 vs step 4)
rawEncryptedResourceWithKey2 := scenario.GetRawResourceFunc(e, clientSet, ns)
if rawEncryptedResourceWithKey1 == rawEncryptedResourceWithKey2 {
t.Errorf("expected the resource to has a different content after a key rotation,\ncontentBeforeRotation %s\ncontentAfterRotation %s", rawEncryptedResourceWithKey1, rawEncryptedResourceWithKey2)
}
// TODO: assert conditions - operator and encryption migration controller must report status as active not progressing, and not failing for all scenarios
}
// InvalidImageRecoveryScenario tests that an invalid KMS plugin image causes
// the cluster to degrade and that correcting the image restores normal operation.
//
// Arda's scenario:
// 1. Enable KMS with invalid image
// 2. Attempt to switch to aescbc (cluster should be stuck on KMS)
// 3. See degraded
// 4. Update with KMS with valid image
// 5. See everything works
type InvalidImageRecoveryScenario struct {
BasicScenario
// InvalidImageProvider is the KMS EncryptionProvider configured with an invalid
// (non-existent or broken) KMS plugin image. Enabling this should cause degradation.
InvalidImageProvider EncryptionProvider
// ValidImageProvider is the KMS EncryptionProvider configured with the correct
// KMS plugin image. Applying this after degradation should restore the cluster.
ValidImageProvider EncryptionProvider
// WaitForDegraded should block until the operator reports a degraded condition.
WaitForDegraded func(ctx context.Context, t testing.TB)
// WaitForRecovery should block until the operator reports a healthy condition
// and encryption is fully operational.
WaitForRecovery func(ctx context.Context, t testing.TB)
}
// TestEncryptionInvalidImageRecovery tests that:
// 1. Enabling KMS with an invalid plugin image causes degradation
// 2. The cluster remains stuck and cannot migrate to another mode (e.g. aescbc)
// 3. Fixing the KMS config with a valid image restores the cluster
func TestEncryptionInvalidImageRecovery(ctx context.Context, t testing.TB, scenario InvalidImageRecoveryScenario) {
e := NewE(t, PrintEventsOnFailure(scenario.OperatorNamespace))
require.NotNil(t, scenario.WaitForDegraded, "WaitForDegraded must not be nil")
require.NotNil(t, scenario.WaitForRecovery, "WaitForRecovery must not be nil")
require.Equal(t, configv1.EncryptionTypeKMS, scenario.InvalidImageProvider.Type, "InvalidImageProvider must be KMS type")
require.Equal(t, configv1.EncryptionTypeKMS, scenario.ValidImageProvider.Type, "ValidImageProvider must be KMS type")
cs := GetClients(e)
// step 1: enable KMS with invalid image (set config but don't wait for completion)
t.Log("Setting KMS encryption with invalid plugin image")
if scenario.InvalidImageProvider.Setup != nil {
scenario.InvalidImageProvider.Setup(ctx, e)
}
apiServer, err := cs.ApiServerConfig.Get(ctx, "cluster", metav1.GetOptions{})
require.NoError(t, err)
apiServer.Spec.Encryption = scenario.InvalidImageProvider.APIServerEncryption
_, err = cs.ApiServerConfig.Update(ctx, apiServer, metav1.UpdateOptions{})
require.NoError(t, err)
// step 2: attempt to switch to aescbc — cluster should remain stuck on KMS
t.Log("Attempting to switch to aescbc (cluster should remain stuck on KMS)")
apiServer, err = cs.ApiServerConfig.Get(ctx, "cluster", metav1.GetOptions{})
require.NoError(t, err)
apiServer.Spec.Encryption = configv1.APIServerEncryption{Type: configv1.EncryptionTypeAESCBC}
_, err = cs.ApiServerConfig.Update(ctx, apiServer, metav1.UpdateOptions{})
require.NoError(t, err)
// step 3: wait for degraded
t.Log("Waiting for operator to report degraded status")
scenario.WaitForDegraded(ctx, e)
// step 4: fix the config with valid KMS image
t.Log("Updating KMS encryption with valid plugin image to recover")
if scenario.ValidImageProvider.Setup != nil {
scenario.ValidImageProvider.Setup(ctx, e)
}
apiServer, err = cs.ApiServerConfig.Get(ctx, "cluster", metav1.GetOptions{})
require.NoError(t, err)
apiServer.Spec.Encryption = scenario.ValidImageProvider.APIServerEncryption
_, err = cs.ApiServerConfig.Update(ctx, apiServer, metav1.UpdateOptions{})
require.NoError(t, err)
// step 5: wait for recovery — everything should work
t.Log("Waiting for operator to recover with valid KMS image")
scenario.WaitForRecovery(ctx, e)
t.Log("Invalid image recovery test passed")
}