github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/certloader/loader_test.go (about) 1 package certloader 2 3 import ( 4 "context" 5 "crypto/rand" 6 "crypto/rsa" 7 "crypto/x509" 8 "crypto/x509/pkix" 9 "encoding/pem" 10 "math/big" 11 "testing" 12 "time" 13 14 "github.com/kyma-incubator/compass/components/director/pkg/certloader/automock" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/mock" 17 "github.com/stretchr/testify/require" 18 v1 "k8s.io/api/core/v1" 19 "k8s.io/apimachinery/pkg/runtime" 20 "k8s.io/apimachinery/pkg/watch" 21 ) 22 23 const ( 24 secretName = "secretName" 25 secretCertKey = "tls.crt" 26 secretKeyKey = "tls.key" 27 testCN = "test-common-name" 28 ) 29 30 type testWatch struct { 31 events chan watch.Event 32 } 33 34 func (tw *testWatch) close() { 35 close(tw.events) 36 } 37 38 func (tw *testWatch) putEvent(ev watch.Event) { 39 tw.events <- ev 40 } 41 42 func (tw *testWatch) Stop() {} 43 func (tw *testWatch) ResultChan() <-chan watch.Event { 44 return tw.events 45 } 46 47 func Test_CertificateLoaderWatch(t *testing.T) { 48 config := Config{ 49 ExternalClientCertSecret: "namespace/resource-name", 50 ExternalClientCertCertKey: "tls.crt", 51 ExternalClientCertKeyKey: "tls.key", 52 ExtSvcClientCertSecret: "namespace/resource-name", 53 ExtSvcClientCertCertKey: "tls.crt", 54 ExtSvcClientCertKeyKey: "tls.key"} 55 56 t.Run("should insert secret data on add event", func(t *testing.T) { 57 // given 58 ctx, cancel := context.WithCancel(context.Background()) 59 defer cancel() 60 cache, watcher, secretManagerMock := preparation(ctx, 1, config) 61 certBytes, keyBytes := generateTestCertAndKey(t, testCN) 62 63 // when 64 watcher.putEvent(watch.Event{ 65 Type: watch.Added, 66 Object: &v1.Secret{ 67 Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes}, 68 }, 69 }) 70 71 // then 72 assert.Eventually(t, func() bool { 73 tlsCert := cache.Get() 74 require.NotNil(t, tlsCert) 75 return true 76 }, 2*time.Second, 100*time.Millisecond) 77 cancel() 78 assert.Eventually(t, func() bool { 79 <-ctx.Done() 80 return true 81 }, time.Second, 100*time.Millisecond) 82 secretManagerMock.AssertExpectations(t) 83 }) 84 85 t.Run("should insert secret data on modify event", func(t *testing.T) { 86 // given 87 ctx, cancel := context.WithCancel(context.Background()) 88 defer cancel() 89 cache, watcher, secretManagerMock := preparation(ctx, 1, config) 90 certBytes, keyBytes := generateTestCertAndKey(t, testCN) 91 92 // when 93 watcher.putEvent(watch.Event{ 94 Type: watch.Modified, 95 Object: &v1.Secret{ 96 Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes}, 97 }, 98 }) 99 100 // then 101 assert.Eventually(t, func() bool { 102 tlsCert := cache.Get() 103 require.NotNil(t, tlsCert) 104 return true 105 }, 2*time.Second, 100*time.Millisecond) 106 cancel() 107 assert.Eventually(t, func() bool { 108 <-ctx.Done() 109 return true 110 }, time.Second, 100*time.Millisecond) 111 secretManagerMock.AssertExpectations(t) 112 }) 113 114 t.Run("should not insert secret data if the event object is not secret", func(t *testing.T) { 115 // given 116 ctx, cancel := context.WithCancel(context.Background()) 117 defer cancel() 118 cache, watcher, secretManagerMock := preparation(ctx, 1, config) 119 120 // when 121 watcher.putEvent(watch.Event{ 122 Type: watch.Added, 123 Object: &runtime.Unknown{}, 124 }) 125 126 // then 127 assert.Eventually(t, func() bool { 128 tlsCert := cache.Get() 129 require.Nil(t, tlsCert["resource-name"]) 130 return true 131 }, 2*time.Second, 100*time.Millisecond) 132 cancel() 133 assert.Eventually(t, func() bool { 134 <-ctx.Done() 135 return true 136 }, time.Second, 100*time.Millisecond) 137 secretManagerMock.AssertExpectations(t) 138 }) 139 140 t.Run("should return empty cache after delete event", func(t *testing.T) { 141 // given 142 ctx, cancel := context.WithCancel(context.Background()) 143 defer cancel() 144 cache, watcher, secretManagerMock := preparation(ctx, 1, config) 145 certBytes, keyBytes := generateTestCertAndKey(t, testCN) 146 147 // when 148 watcher.putEvent(watch.Event{ 149 Type: watch.Added, 150 Object: &v1.Secret{ 151 Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes}, 152 }, 153 }) 154 155 assert.Eventually(t, func() bool { 156 tlsCert := cache.Get() 157 require.NotNil(t, tlsCert) 158 return true 159 }, 2*time.Second, 100*time.Millisecond) 160 161 watcher.putEvent(watch.Event{ 162 Type: watch.Deleted, 163 }) 164 165 // then 166 assert.Eventually(t, func() bool { 167 tlsCert := cache.Get() 168 require.Nil(t, tlsCert["resource-name"]) 169 return true 170 }, 2*time.Second, 100*time.Millisecond) 171 cancel() 172 assert.Eventually(t, func() bool { 173 <-ctx.Done() 174 return true 175 }, time.Second, 100*time.Millisecond) 176 secretManagerMock.AssertExpectations(t) 177 }) 178 179 t.Run("should try reconnect when there is error event", func(t *testing.T) { 180 // given 181 ctx, cancel := context.WithCancel(context.Background()) 182 defer cancel() 183 cache, watcher, secretManagerMock := preparation(ctx, 2, config) 184 certBytes, keyBytes := generateTestCertAndKey(t, testCN) 185 186 // when 187 watcher.putEvent(watch.Event{ 188 Type: watch.Error, 189 }) 190 191 watcher.putEvent(watch.Event{ 192 Type: watch.Added, 193 Object: &v1.Secret{ 194 Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes}, 195 }, 196 }) 197 198 // then 199 assert.Eventually(t, func() bool { 200 tlsCert := cache.Get() 201 require.NotNil(t, tlsCert) 202 return true 203 }, 2*time.Second, 100*time.Millisecond) 204 205 watcher.putEvent(watch.Event{ 206 Type: watch.Deleted, 207 }) 208 cancel() 209 assert.Eventually(t, func() bool { 210 <-ctx.Done() 211 return true 212 }, time.Second, 100*time.Millisecond) 213 secretManagerMock.AssertExpectations(t) 214 }) 215 216 t.Run("should try reconnect when event channel is closed", func(t *testing.T) { 217 // given 218 ctx, cancel := context.WithCancel(context.Background()) 219 defer cancel() 220 cache, watcher, secretManagerMock := preparation(ctx, 2, config) 221 certBytes, keyBytes := generateTestCertAndKey(t, testCN) 222 223 // when 224 watcher.close() 225 newWatcher := &testWatch{ 226 events: make(chan watch.Event, 50), 227 } 228 229 secretManagerMock.On("Watch", mock.Anything, mock.AnythingOfType("v1.ListOptions")).Return(newWatcher, nil).Once() 230 231 newWatcher.putEvent(watch.Event{ 232 Type: watch.Added, 233 Object: &v1.Secret{ 234 Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes}, 235 }, 236 }) 237 238 // then 239 assert.Eventually(t, func() bool { 240 tlsCert := cache.Get() 241 require.NotNil(t, tlsCert) 242 return true 243 }, 2*time.Second, 100*time.Millisecond) 244 cancel() 245 assert.Eventually(t, func() bool { 246 <-ctx.Done() 247 return true 248 }, time.Second, 100*time.Millisecond) 249 secretManagerMock.AssertExpectations(t) 250 }) 251 } 252 253 func Test_CertificateParsing(t *testing.T) { 254 ctx := context.Background() 255 config := Config{ 256 ExternalClientCertCertKey: "tls.crt", 257 ExternalClientCertKeyKey: "tls.key", 258 } 259 certBytes, keyBytes := generateTestCertAndKey(t, testCN) 260 invalidCert := "-----BEGIN CERTIFICATE-----\naZOCUHlJ1wKwnYiLnOofB1xyIUZhVLaJy7Ob\n-----END CERTIFICATE-----\n" 261 invalidKey := "-----BEGIN RSA PRIVATE KEY-----\n7qFmWkbkOAM9CUPx5RwSRt45oxlQjvDniZALWqbYxgO5f8cYZsEAyOU1n2DXgiei\n-----END RSA PRIVATE KEY-----\n" 262 263 testCases := []struct { 264 Name string 265 SecretData map[string][]byte 266 ExpectedErrorMsg string 267 Cfg Config 268 }{ 269 { 270 Name: "Successfully get certificate from cache", 271 SecretData: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes}, 272 Cfg: config, 273 }, 274 { 275 Name: "Successfully get ext svc certificate from cache", 276 SecretData: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes}, 277 Cfg: Config{ 278 ExtSvcClientCertCertKey: "tls.crt", 279 ExtSvcClientCertKeyKey: "tls.key", 280 }, 281 }, 282 { 283 Name: "Error when secret data is empty", 284 SecretData: map[string][]byte{}, 285 ExpectedErrorMsg: "There is no certificate data provided", 286 Cfg: config, 287 }, 288 { 289 Name: "Error when certificate data is invalid", 290 SecretData: map[string][]byte{secretCertKey: []byte("invalid"), secretKeyKey: []byte("invalid")}, 291 ExpectedErrorMsg: "Error while decoding certificate pem block", 292 Cfg: config, 293 }, 294 { 295 Name: "Error when parsing certificate", 296 SecretData: map[string][]byte{secretCertKey: []byte(invalidCert), secretKeyKey: []byte("invalid")}, 297 ExpectedErrorMsg: "malformed certificate", 298 Cfg: config, 299 }, 300 { 301 Name: "Error when private key is invalid", 302 SecretData: map[string][]byte{secretCertKey: certBytes, secretKeyKey: []byte("invalid")}, 303 ExpectedErrorMsg: "Error while decoding private key pem block", 304 Cfg: config, 305 }, 306 { 307 Name: "Error when parsing private key", 308 SecretData: map[string][]byte{secretCertKey: certBytes, secretKeyKey: []byte(invalidKey)}, 309 ExpectedErrorMsg: "structure error", 310 Cfg: config, 311 }, 312 } 313 314 for _, testCase := range testCases { 315 t.Run(testCase.Name, func(t *testing.T) { 316 tlsCert, err := parseCertificate(ctx, testCase.SecretData, testCase.Cfg) 317 318 if testCase.ExpectedErrorMsg != "" { 319 require.Error(t, err) 320 require.Contains(t, err.Error(), testCase.ExpectedErrorMsg) 321 require.Nil(t, tlsCert) 322 } else { 323 require.NoError(t, err) 324 require.NotNil(t, tlsCert) 325 } 326 }) 327 } 328 } 329 330 func preparation(ctx context.Context, number int, config Config) (Cache, *testWatch, *automock.Manager) { 331 cache := NewCertificateCache() 332 watcher := &testWatch{ 333 events: make(chan watch.Event, 50), 334 } 335 secretManagerMock := &automock.Manager{} 336 secretManagerMock.On("Watch", mock.Anything, mock.AnythingOfType("v1.ListOptions")).Return(watcher, nil).Times(number) 337 loader := NewCertificateLoader(config, cache, []Manager{secretManagerMock}, []string{secretName}, time.Millisecond) 338 go loader.Run(ctx) 339 340 return cache, watcher, secretManagerMock 341 } 342 343 func generateTestCertAndKey(t *testing.T, commonName string) (crtPem, keyPem []byte) { 344 clientKey, err := rsa.GenerateKey(rand.Reader, 2048) 345 require.NoError(t, err) 346 347 template := &x509.Certificate{ 348 IsCA: true, 349 SerialNumber: big.NewInt(1234), 350 Subject: pkix.Name{ 351 CommonName: commonName, 352 }, 353 NotBefore: time.Now(), 354 NotAfter: time.Now().Add(time.Hour), 355 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 356 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 357 } 358 359 parent := template 360 certRaw, err := x509.CreateCertificate(rand.Reader, template, parent, &clientKey.PublicKey, clientKey) 361 require.NoError(t, err) 362 363 crtPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}) 364 keyPem = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey)}) 365 366 return 367 }