diff --git a/.sonarcloud.properties b/.sonarcloud.properties index f40a39f2e8..df1bdabccd 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -1,6 +1,6 @@ sonar.projectKey=keptn_lifecycle-toolkit sonar.projectName=lifecycle-toolkit -sonar.cpd.exclusions=**/test_*.go,\ +sonar.cpd.exclusions= **/*_test.go,\ scheduler/test/e2e/fake/**/*.go,\ operator/apis/lifecycle/v1alpha1/**/*.go,\ operator/apis/lifecycle/v1alpha2/**/*.go,\ diff --git a/Makefile b/Makefile index cac691cdac..57b6f5d13e 100644 --- a/Makefile +++ b/Makefile @@ -33,10 +33,13 @@ $(HELMIFY): $(LOCALBIN) .PHONY: integration-test #these tests should run on a real cluster! integration-test: # to run a single test by name use --test eg. --test=expose-keptn-metric kubectl kuttl test --start-kind=false ./test/integration/ --config=kuttl-test.yaml + kubectl kuttl test --start-kind=false ./test/testcertificate/ --config=kuttl-test.yaml + .PHONY: integration-test-local #these tests should run on a real cluster! integration-test-local: install-prometheus kubectl kuttl test --start-kind=false ./test/integration/ --config=kuttl-test-local.yaml + kubectl kuttl test --start-kind=false ./test/testcertificate/ --config=kuttl-test-local.yaml .PHONY: load-test load-test: diff --git a/metrics-operator/cmd/certificates/certificatehandler.go b/metrics-operator/cmd/certificates/certificatehandler.go new file mode 100644 index 0000000000..ba9450fad9 --- /dev/null +++ b/metrics-operator/cmd/certificates/certificatehandler.go @@ -0,0 +1,22 @@ +package certificates + +import ( + "crypto/x509" + "encoding/pem" +) + +//go:generate moq -pkg fake -skip-ensure -out ./fake/certificatehandler_mock.go . ICertificateHandler +type ICertificateHandler interface { + Decode(data []byte) (p *pem.Block, rest []byte) + Parse(der []byte) (*x509.Certificate, error) +} + +type defaultCertificateHandler struct { +} + +func (c defaultCertificateHandler) Decode(data []byte) (p *pem.Block, rest []byte) { + return pem.Decode(data) +} +func (c defaultCertificateHandler) Parse(der []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(der) +} diff --git a/metrics-operator/cmd/certificates/fake/certificatehandler_mock.go b/metrics-operator/cmd/certificates/fake/certificatehandler_mock.go new file mode 100644 index 0000000000..45a5eacbae --- /dev/null +++ b/metrics-operator/cmd/certificates/fake/certificatehandler_mock.go @@ -0,0 +1,116 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package fake + +import ( + "crypto/x509" + "encoding/pem" + "sync" +) + +// ICertificateHandlerMock is a mock implementation of certificates.ICertificateHandler. +// +// func TestSomethingThatUsesICertificateHandler(t *testing.T) { +// +// // make and configure a mocked certificates.ICertificateHandler +// mockedICertificateHandler := &ICertificateHandlerMock{ +// DecodeFunc: func(data []byte) (*pem.Block, []byte) { +// panic("mock out the Decode method") +// }, +// ParseFunc: func(der []byte) (*x509.Certificate, error) { +// panic("mock out the Parse method") +// }, +// } +// +// // use mockedICertificateHandler in code that requires certificates.ICertificateHandler +// // and then make assertions. +// +// } +type ICertificateHandlerMock struct { + // DecodeFunc mocks the Decode method. + DecodeFunc func(data []byte) (*pem.Block, []byte) + + // ParseFunc mocks the Parse method. + ParseFunc func(der []byte) (*x509.Certificate, error) + + // calls tracks calls to the methods. + calls struct { + // Decode holds details about calls to the Decode method. + Decode []struct { + // Data is the data argument value. + Data []byte + } + // Parse holds details about calls to the Parse method. + Parse []struct { + // Der is the der argument value. + Der []byte + } + } + lockDecode sync.RWMutex + lockParse sync.RWMutex +} + +// Decode calls DecodeFunc. +func (mock *ICertificateHandlerMock) Decode(data []byte) (*pem.Block, []byte) { + if mock.DecodeFunc == nil { + panic("ICertificateHandlerMock.DecodeFunc: method is nil but ICertificateHandler.Decode was just called") + } + callInfo := struct { + Data []byte + }{ + Data: data, + } + mock.lockDecode.Lock() + mock.calls.Decode = append(mock.calls.Decode, callInfo) + mock.lockDecode.Unlock() + return mock.DecodeFunc(data) +} + +// DecodeCalls gets all the calls that were made to Decode. +// Check the length with: +// +// len(mockedICertificateHandler.DecodeCalls()) +func (mock *ICertificateHandlerMock) DecodeCalls() []struct { + Data []byte +} { + var calls []struct { + Data []byte + } + mock.lockDecode.RLock() + calls = mock.calls.Decode + mock.lockDecode.RUnlock() + return calls +} + +// Parse calls ParseFunc. +func (mock *ICertificateHandlerMock) Parse(der []byte) (*x509.Certificate, error) { + if mock.ParseFunc == nil { + panic("ICertificateHandlerMock.ParseFunc: method is nil but ICertificateHandler.Parse was just called") + } + callInfo := struct { + Der []byte + }{ + Der: der, + } + mock.lockParse.Lock() + mock.calls.Parse = append(mock.calls.Parse, callInfo) + mock.lockParse.Unlock() + return mock.ParseFunc(der) +} + +// ParseCalls gets all the calls that were made to Parse. +// Check the length with: +// +// len(mockedICertificateHandler.ParseCalls()) +func (mock *ICertificateHandlerMock) ParseCalls() []struct { + Der []byte +} { + var calls []struct { + Der []byte + } + mock.lockParse.RLock() + calls = mock.calls.Parse + mock.lockParse.RUnlock() + return calls +} diff --git a/metrics-operator/cmd/certificates/watcher.go b/metrics-operator/cmd/certificates/watcher.go index 426da57f87..3621679ccc 100644 --- a/metrics-operator/cmd/certificates/watcher.go +++ b/metrics-operator/cmd/certificates/watcher.go @@ -3,8 +3,6 @@ package certificates import ( "bytes" "context" - "crypto/x509" - "encoding/pem" "errors" "fmt" "os" @@ -16,13 +14,13 @@ import ( corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/manager" ) const ( certificateRenewalInterval = 6 * time.Hour ServerKey = "tls.key" ServerCert = "tls.crt" + CertThreshold = 5 * time.Minute ) type CertificateWatcher struct { @@ -31,16 +29,20 @@ type CertificateWatcher struct { certificateDirectory string namespace string certificateSecretName string - Log logr.Logger + certificateTreshold time.Duration + ICertificateHandler + Log logr.Logger } -func NewCertificateWatcher(mgr manager.Manager, namespace string, secretName string, log logr.Logger) *CertificateWatcher { +func NewCertificateWatcher(reader client.Reader, certDir string, namespace string, secretName string, log logr.Logger) *CertificateWatcher { return &CertificateWatcher{ - apiReader: mgr.GetAPIReader(), + apiReader: reader, fs: afero.NewOsFs(), - certificateDirectory: mgr.GetWebhookServer().CertDir, + certificateDirectory: certDir, namespace: namespace, certificateSecretName: secretName, + ICertificateHandler: defaultCertificateHandler{}, + certificateTreshold: CertThreshold, Log: log, } } @@ -75,7 +77,7 @@ func (watcher *CertificateWatcher) updateCertificatesFromSecret() error { } for _, filename := range []string{ServerCert, ServerKey} { - if _, err = watcher.ensureCertificateFile(secret, filename); err != nil { + if err = watcher.ensureCertificateFile(secret, filename); err != nil { return err } } @@ -88,22 +90,18 @@ func (watcher *CertificateWatcher) updateCertificatesFromSecret() error { return nil } -func (watcher *CertificateWatcher) ensureCertificateFile(secret corev1.Secret, filename string) (bool, error) { +func (watcher *CertificateWatcher) ensureCertificateFile(secret corev1.Secret, filename string) error { f := filepath.Join(watcher.certificateDirectory, filename) - data, err := afero.ReadFile(watcher.fs, f) if os.IsNotExist(err) || !bytes.Equal(data, secret.Data[filename]) { - if err := afero.WriteFile(watcher.fs, f, secret.Data[filename], 0666); err != nil { - return false, err - } - } else { - return false, err + return afero.WriteFile(watcher.fs, f, secret.Data[filename], 0666) } - return true, nil + return err + } func (watcher *CertificateWatcher) WaitForCertificates() { - for threshold := time.Now().Add(5 * time.Minute); time.Now().Before(threshold); { + for threshold := time.Now().Add(watcher.certificateTreshold); time.Now().Before(threshold); { if err := watcher.updateCertificatesFromSecret(); err != nil { if k8serrors.IsNotFound(err) { @@ -120,10 +118,10 @@ func (watcher *CertificateWatcher) WaitForCertificates() { } func (watcher *CertificateWatcher) ValidateCertificateExpiration(certData []byte, renewalThreshold time.Duration, now time.Time) (bool, error) { - if block, _ := pem.Decode(certData); block == nil { + if block, _ := watcher.Decode(certData); block == nil { watcher.Log.Error(errors.New("can't decode PEM file"), "failed to parse certificate") return false, nil - } else if cert, err := x509.ParseCertificate(block.Bytes); err != nil { + } else if cert, err := watcher.Parse(block.Bytes); err != nil { watcher.Log.Error(err, "failed to parse certificate") return false, err } else if now.After(cert.NotAfter.Add(-renewalThreshold)) { diff --git a/metrics-operator/cmd/certificates/watcher_test.go b/metrics-operator/cmd/certificates/watcher_test.go new file mode 100644 index 0000000000..4f4301e313 --- /dev/null +++ b/metrics-operator/cmd/certificates/watcher_test.go @@ -0,0 +1,318 @@ +package certificates + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-logr/logr/testr" + "github.com/keptn/lifecycle-toolkit/metrics-operator/cmd/certificates/fake" + fakeclient "github.com/keptn/lifecycle-toolkit/metrics-operator/controllers/common/fake" + "github.com/pkg/errors" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const CACERT = `-----BEGIN CERTIFICATE----- +MIICPTCCAeKgAwIBAgIRAMIV/0UqFGHgKSYOWBdx/KcwCgYIKoZIzj0EAwIwczEL +MAkGA1UEBhMCQVQxCzAJBgNVBAgTAktMMRMwEQYDVQQHEwpLbGFnZW5mdXJ0MQ4w +DAYDVQQKEwVLZXB0bjEZMBcGA1UECxMQTGlmZWN5Y2xlVG9vbGtpdDEXMBUGA1UE +AwwOKi5rZXB0bi1ucy5zdmMwHhcNMjMwNDE5MTEwNDUzWhcNMjQwNDE4MTEwNDUz +WjBzMQswCQYDVQQGEwJBVDELMAkGA1UECBMCS0wxEzARBgNVBAcTCktsYWdlbmZ1 +cnQxDjAMBgNVBAoTBUtlcHRuMRkwFwYDVQQLExBMaWZlY3ljbGVUb29sa2l0MRcw +FQYDVQQDDA4qLmtlcHRuLW5zLnN2YzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA +BPxAP4JTJfwKz/P32dXuyfVi7kinQPebSYwF/gRAUcN0dCAi6GnxbI2OXlcU0guD +zHXv3VRh3EX2fiNszcfKaCajVzBVMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAK +BggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQUGe/8XYV1HsZs +nWsyrOCCGr/sQDAKBggqhkjOPQQDAgNJADBGAiEAkcPaCANDXW5Uillrof0VrnPw +ow49D22Gsrh7YM+vmTQCIQDU1L5IT0Zz+bdIyFSsDnEUXZDeydNv56DoSLh+358Y +aw== +-----END CERTIFICATE-----` + +const CAKEY = `-----BEGIN PRIVATE KEY----- +MHcCAQEEII5SAqBxINKatksyu2mTvLZZhfEOpNinYJDwlQjkfreboAoGCCqGSM49 +AwEHoUQDQgAE/EA/glMl/ArP8/fZ1e7J9WLuSKdA95tJjAX+BEBRw3R0ICLoafFs +jY5eVxTSC4PMde/dVGHcRfZ+I2zNx8poJg== +-----END PRIVATE KEY-----` + +const uniqueIDPEM = `-----BEGIN CERTIFICATE----- +MIIFsDCCBJigAwIBAgIIrOyC1ydafZMwDQYJKoZIhvcNAQEFBQAwgY4xgYswgYgG +A1UEAx6BgABNAGkAYwByAG8AcwBvAGYAdAAgAEYAbwByAGUAZgByAG8AbgB0ACAA +VABNAEcAIABIAFQAVABQAFMAIABJAG4AcwBwAGUAYwB0AGkAbwBuACAAQwBlAHIA +dABpAGYAaQBjAGEAdABpAG8AbgAgAEEAdQB0AGgAbwByAGkAdAB5MB4XDTE0MDEx +ODAwNDEwMFoXDTE1MTExNTA5Mzc1NlowgZYxCzAJBgNVBAYTAklEMRAwDgYDVQQI +EwdqYWthcnRhMRIwEAYDVQQHEwlJbmRvbmVzaWExHDAaBgNVBAoTE3N0aG9ub3Jl +aG90ZWxyZXNvcnQxHDAaBgNVBAsTE3N0aG9ub3JlaG90ZWxyZXNvcnQxJTAjBgNV +BAMTHG1haWwuc3Rob25vcmVob3RlbHJlc29ydC5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCvuu0qpI+Ko2X84Twkf84cRD/rgp6vpgc5Ebejx/D4 +PEVON5edZkazrMGocK/oQqIlRxx/lefponN/chlGcllcVVPWTuFjs8k+Aat6T1qp +4iXxZekAqX+U4XZMIGJD3PckPL6G2RQSlF7/LhGCsRNRdKpMWSTbou2Ma39g52Kf +gsl3SK/GwLiWpxpcSkNQD1hugguEIsQYLxbeNwpcheXZtxbBGguPzQ7rH8c5vuKU +BkMOzaiNKLzHbBdFSrua8KWwCJg76Vdq/q36O9GlW6YgG3i+A4pCJjXWerI1lWwX +Ktk5V+SvUHGey1bkDuZKJ6myMk2pGrrPWCT7jP7WskChAgMBAAGBCQBCr1dgEleo +cKOCAfswggH3MIHDBgNVHREEgbswgbiCHG1haWwuc3Rob25vcmVob3RlbHJlc29y +dC5jb22CIGFzaGNoc3ZyLnN0aG9ub3JlaG90ZWxyZXNvcnQuY29tgiRBdXRvRGlz +Y292ZXIuc3Rob25vcmVob3RlbHJlc29ydC5jb22CHEF1dG9EaXNjb3Zlci5ob3Rl +bHJlc29ydC5jb22CCEFTSENIU1ZSghdzdGhvbm9yZWhvdGVscmVzb3J0LmNvbYIP +aG90ZWxyZXNvcnQuY29tMCEGCSsGAQQBgjcUAgQUHhIAVwBlAGIAUwBlAHIAdgBl +AHIwHQYDVR0OBBYEFMAC3UR4FwAdGekbhMgnd6lMejtbMAsGA1UdDwQEAwIFoDAT +BgNVHSUEDDAKBggrBgEFBQcDATAJBgNVHRMEAjAAMIG/BgNVHQEEgbcwgbSAFGfF +6xihk+gJJ5TfwvtWe1UFnHLQoYGRMIGOMYGLMIGIBgNVBAMegYAATQBpAGMAcgBv +AHMAbwBmAHQAIABGAG8AcgBlAGYAcgBvAG4AdAAgAFQATQBHACAASABUAFQAUABT +ACAASQBuAHMAcABlAGMAdABpAG8AbgAgAEMAZQByAHQAaQBmAGkAYwBhAHQAaQBv +AG4AIABBAHUAdABoAG8AcgBpAHQAeYIIcKhXEmBXr0IwDQYJKoZIhvcNAQEFBQAD +ggEBABlSxyCMr3+ANr+WmPSjyN5YCJBgnS0IFCwJAzIYP87bcTye/U8eQ2+E6PqG +Q7Huj7nfHEw9qnGo+HNyPp1ad3KORzXDb54c6xEoi+DeuPzYHPbn4c3hlH49I0aQ +eWW2w4RslSWpLvO6Y7Lboyz2/Thk/s2kd4RHxkkWpH2ltPqJuYYg3X6oM5+gIFHJ +WGnh+ojZ5clKvS5yXh3Wkj78M6sb32KfcBk0Hx6NkCYPt60ODYmWtvqwtw6r73u5 +TnTYWRNvo2svX69TriL+CkHY9O1Hkwf2It5zHl3gNiKTJVaak8AuEz/CKWZneovt +yYLwhUhg3PX5Co1VKYE+9TxloiE= +-----END CERTIFICATE-----` + +var ERR_BAD_CERT = errors.New("bad cert") + +var emptySecret = v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "my-cert", + }, +} + +var goodSecret = v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "my-cert", + }, + Data: map[string][]byte{ + ServerCert: []byte(CACERT), + ServerKey: []byte(CAKEY), + }, +} + +func TestCertificateWatcher_ValidateCertificateExpiration(t *testing.T) { + + tests := []struct { + name string + certHandler ICertificateHandler + certData []byte + renewalThreshold time.Duration + now time.Time + want bool + wantErr error + }{ + { + name: "certificate cannot be decoded", + certHandler: &fake.ICertificateHandlerMock{ + DecodeFunc: func(data []byte) (p *pem.Block, rest []byte) { + return nil, nil //fake a failure in the decoding + }, + ParseFunc: nil, + }, + want: false, + }, + { + name: "certificate cannot be parsed", + certHandler: &fake.ICertificateHandlerMock{ + DecodeFunc: func(data []byte) (p *pem.Block, rest []byte) { + return &pem.Block{Type: "test", Bytes: []byte("testdata")}, nil + }, + ParseFunc: func(der []byte) (*x509.Certificate, error) { + return nil, ERR_BAD_CERT + }, + }, + want: false, + wantErr: ERR_BAD_CERT, + }, + { + name: "good certificate - unexpired", + certData: []byte(uniqueIDPEM), + certHandler: defaultCertificateHandler{}, + want: true, + }, + { + name: "good certificate - expired", + certData: []byte(uniqueIDPEM), + now: time.Now(), //setting up now makes sure that the threshold is passed + certHandler: defaultCertificateHandler{}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + watcher := &CertificateWatcher{ + ICertificateHandler: tt.certHandler, + Log: testr.New(t), + } + got, err := watcher.ValidateCertificateExpiration(tt.certData, tt.renewalThreshold, tt.now) + if tt.wantErr != nil { + require.Error(t, err) + t.Log("want:", tt.wantErr, "got:", err) + require.True(t, errors.Is(tt.wantErr, err)) + } + require.Equal(t, got, tt.want) + }) + } +} + +func TestCertificateWatcher_ensureCertificateFile(t *testing.T) { + + certdir := t.TempDir() + f := filepath.Join(certdir, ServerCert) + err := os.WriteFile(f, goodSecret.Data[ServerCert], 0666) + require.Nil(t, err) + baddir := t.TempDir() + f = filepath.Join(baddir, ServerCert) + err = os.WriteFile(f, goodSecret.Data[ServerKey], 0666) + require.Nil(t, err) + tests := []struct { + name string + fs afero.Fs + secret v1.Secret + filename string + certDir string + wantErr bool + err string + }{ + { + name: "if good cert exist in fs no error", + secret: goodSecret, + certDir: certdir, + filename: ServerCert, + wantErr: false, + }, + + { + name: "if unexisting file name, we expect a file system error", + secret: emptySecret, + filename: "$%&/())=$§%/=", + certDir: baddir, + wantErr: true, + err: "no such file or directory", + }, + + { + name: "wrong file content is replaced with updated cert", + certDir: baddir, + secret: goodSecret, + filename: ServerCert, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + watcher := &CertificateWatcher{ + fs: afero.NewOsFs(), + certificateDirectory: tt.certDir, + } + err := watcher.ensureCertificateFile(tt.secret, tt.filename) + if !tt.wantErr { + require.Nil(t, err) + f = filepath.Join(tt.certDir, ServerCert) + data, err := os.ReadFile(f) + if err != nil { + panic(err) + } + if !bytes.Equal(data, tt.secret.Data[tt.filename]) { + t.Errorf("ensureCertificateFile()data %v was not replaced with %v", data, tt.secret.Data[tt.filename]) + } + } else { + require.Contains(t, err.Error(), tt.err) + } + }) + } +} + +func TestCertificateWatcher_updateCertificatesFromSecret(t *testing.T) { + + oldDir := t.TempDir() + os.Remove(oldDir) + + tests := []struct { + name string + apiReader client.Reader + certificateDirectory string + namespace string + certificateSecretName string + wantErr error + }{ + { + name: "certificate not found", + apiReader: fakeclient.NewClient(), + certificateDirectory: t.TempDir(), + namespace: "default", + certificateSecretName: "my-cert", + wantErr: errors.New("secrets \"my-cert\" not found"), + }, + { + name: "outdated certificate found, nothing in dir", + apiReader: fakeclient.NewClient(&emptySecret), + certificateDirectory: t.TempDir(), + namespace: "default", + certificateSecretName: "my-cert", + wantErr: errors.New("certificate is outdated"), + }, + + { + name: "outdated certificate found, not existing in dir", + apiReader: fakeclient.NewClient(&emptySecret), + certificateDirectory: oldDir, + namespace: "default", + certificateSecretName: "my-cert", + wantErr: errors.New("certificate is outdated"), + }, + { + name: "good certificate - not stored", + apiReader: fakeclient.NewClient(&goodSecret), + certificateDirectory: t.TempDir(), + namespace: "default", + certificateSecretName: "my-cert", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + watcher := &CertificateWatcher{ + apiReader: tt.apiReader, + fs: afero.NewOsFs(), + certificateDirectory: tt.certificateDirectory, + namespace: tt.namespace, + certificateSecretName: tt.certificateSecretName, + ICertificateHandler: defaultCertificateHandler{}, + Log: testr.New(t), + } + err := watcher.updateCertificatesFromSecret() + if tt.wantErr == nil { + require.Nil(t, err) + } else { + require.NotNil(t, err) + require.Contains(t, err.Error(), tt.wantErr.Error()) + } + }) + } +} + +func TestNewCertificateWatcher(t *testing.T) { + logger := testr.New(t) + client := fakeclient.NewClient() + want := &CertificateWatcher{ + apiReader: client, + fs: afero.NewOsFs(), + namespace: "default", + certificateSecretName: "my-secret", + certificateDirectory: "test", + certificateTreshold: CertThreshold, + ICertificateHandler: defaultCertificateHandler{}, + Log: testr.New(t), + } + got := NewCertificateWatcher(client, "test", "default", "my-secret", logger) + require.EqualValues(t, got, want) + +} diff --git a/metrics-operator/cmd/webhook/builder.go b/metrics-operator/cmd/webhook/builder.go index b8b78ed068..311acff4ac 100644 --- a/metrics-operator/cmd/webhook/builder.go +++ b/metrics-operator/cmd/webhook/builder.go @@ -69,7 +69,7 @@ func (builder Builder) Run(webhookManager manager.Manager) error { builder.GetManagerProvider().SetupWebhookServer(webhookManager) certificates. - NewCertificateWatcher(webhookManager, builder.namespace, secretCertsName, ctrl.Log.WithName("Webhook Cert Manager")). + NewCertificateWatcher(webhookManager.GetAPIReader(), webhookManager.GetWebhookServer().CertDir, builder.namespace, secretCertsName, ctrl.Log.WithName("Webhook Cert Manager")). WaitForCertificates() signalHandler := ctrl.SetupSignalHandler() diff --git a/operator/cmd/certificates/certificatehandler.go b/operator/cmd/certificates/certificatehandler.go new file mode 100644 index 0000000000..ba9450fad9 --- /dev/null +++ b/operator/cmd/certificates/certificatehandler.go @@ -0,0 +1,22 @@ +package certificates + +import ( + "crypto/x509" + "encoding/pem" +) + +//go:generate moq -pkg fake -skip-ensure -out ./fake/certificatehandler_mock.go . ICertificateHandler +type ICertificateHandler interface { + Decode(data []byte) (p *pem.Block, rest []byte) + Parse(der []byte) (*x509.Certificate, error) +} + +type defaultCertificateHandler struct { +} + +func (c defaultCertificateHandler) Decode(data []byte) (p *pem.Block, rest []byte) { + return pem.Decode(data) +} +func (c defaultCertificateHandler) Parse(der []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(der) +} diff --git a/operator/cmd/certificates/fake/certificatehandler_mock.go b/operator/cmd/certificates/fake/certificatehandler_mock.go new file mode 100644 index 0000000000..45a5eacbae --- /dev/null +++ b/operator/cmd/certificates/fake/certificatehandler_mock.go @@ -0,0 +1,116 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package fake + +import ( + "crypto/x509" + "encoding/pem" + "sync" +) + +// ICertificateHandlerMock is a mock implementation of certificates.ICertificateHandler. +// +// func TestSomethingThatUsesICertificateHandler(t *testing.T) { +// +// // make and configure a mocked certificates.ICertificateHandler +// mockedICertificateHandler := &ICertificateHandlerMock{ +// DecodeFunc: func(data []byte) (*pem.Block, []byte) { +// panic("mock out the Decode method") +// }, +// ParseFunc: func(der []byte) (*x509.Certificate, error) { +// panic("mock out the Parse method") +// }, +// } +// +// // use mockedICertificateHandler in code that requires certificates.ICertificateHandler +// // and then make assertions. +// +// } +type ICertificateHandlerMock struct { + // DecodeFunc mocks the Decode method. + DecodeFunc func(data []byte) (*pem.Block, []byte) + + // ParseFunc mocks the Parse method. + ParseFunc func(der []byte) (*x509.Certificate, error) + + // calls tracks calls to the methods. + calls struct { + // Decode holds details about calls to the Decode method. + Decode []struct { + // Data is the data argument value. + Data []byte + } + // Parse holds details about calls to the Parse method. + Parse []struct { + // Der is the der argument value. + Der []byte + } + } + lockDecode sync.RWMutex + lockParse sync.RWMutex +} + +// Decode calls DecodeFunc. +func (mock *ICertificateHandlerMock) Decode(data []byte) (*pem.Block, []byte) { + if mock.DecodeFunc == nil { + panic("ICertificateHandlerMock.DecodeFunc: method is nil but ICertificateHandler.Decode was just called") + } + callInfo := struct { + Data []byte + }{ + Data: data, + } + mock.lockDecode.Lock() + mock.calls.Decode = append(mock.calls.Decode, callInfo) + mock.lockDecode.Unlock() + return mock.DecodeFunc(data) +} + +// DecodeCalls gets all the calls that were made to Decode. +// Check the length with: +// +// len(mockedICertificateHandler.DecodeCalls()) +func (mock *ICertificateHandlerMock) DecodeCalls() []struct { + Data []byte +} { + var calls []struct { + Data []byte + } + mock.lockDecode.RLock() + calls = mock.calls.Decode + mock.lockDecode.RUnlock() + return calls +} + +// Parse calls ParseFunc. +func (mock *ICertificateHandlerMock) Parse(der []byte) (*x509.Certificate, error) { + if mock.ParseFunc == nil { + panic("ICertificateHandlerMock.ParseFunc: method is nil but ICertificateHandler.Parse was just called") + } + callInfo := struct { + Der []byte + }{ + Der: der, + } + mock.lockParse.Lock() + mock.calls.Parse = append(mock.calls.Parse, callInfo) + mock.lockParse.Unlock() + return mock.ParseFunc(der) +} + +// ParseCalls gets all the calls that were made to Parse. +// Check the length with: +// +// len(mockedICertificateHandler.ParseCalls()) +func (mock *ICertificateHandlerMock) ParseCalls() []struct { + Der []byte +} { + var calls []struct { + Der []byte + } + mock.lockParse.RLock() + calls = mock.calls.Parse + mock.lockParse.RUnlock() + return calls +} diff --git a/operator/cmd/certificates/watcher.go b/operator/cmd/certificates/watcher.go index 9c992ea27d..3621679ccc 100644 --- a/operator/cmd/certificates/watcher.go +++ b/operator/cmd/certificates/watcher.go @@ -3,8 +3,6 @@ package certificates import ( "bytes" "context" - "crypto/x509" - "encoding/pem" "errors" "fmt" "os" @@ -16,14 +14,13 @@ import ( corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/manager" ) -// TODO: refactor code below to be testable and also tested: https://github.com/keptn/lifecycle-toolkit/issues/640 const ( certificateRenewalInterval = 6 * time.Hour ServerKey = "tls.key" ServerCert = "tls.crt" + CertThreshold = 5 * time.Minute ) type CertificateWatcher struct { @@ -32,16 +29,20 @@ type CertificateWatcher struct { certificateDirectory string namespace string certificateSecretName string - Log logr.Logger + certificateTreshold time.Duration + ICertificateHandler + Log logr.Logger } -func NewCertificateWatcher(mgr manager.Manager, namespace string, secretName string, log logr.Logger) *CertificateWatcher { +func NewCertificateWatcher(reader client.Reader, certDir string, namespace string, secretName string, log logr.Logger) *CertificateWatcher { return &CertificateWatcher{ - apiReader: mgr.GetAPIReader(), + apiReader: reader, fs: afero.NewOsFs(), - certificateDirectory: mgr.GetWebhookServer().CertDir, + certificateDirectory: certDir, namespace: namespace, certificateSecretName: secretName, + ICertificateHandler: defaultCertificateHandler{}, + certificateTreshold: CertThreshold, Log: log, } } @@ -76,7 +77,7 @@ func (watcher *CertificateWatcher) updateCertificatesFromSecret() error { } for _, filename := range []string{ServerCert, ServerKey} { - if _, err = watcher.ensureCertificateFile(secret, filename); err != nil { + if err = watcher.ensureCertificateFile(secret, filename); err != nil { return err } } @@ -89,22 +90,18 @@ func (watcher *CertificateWatcher) updateCertificatesFromSecret() error { return nil } -func (watcher *CertificateWatcher) ensureCertificateFile(secret corev1.Secret, filename string) (bool, error) { +func (watcher *CertificateWatcher) ensureCertificateFile(secret corev1.Secret, filename string) error { f := filepath.Join(watcher.certificateDirectory, filename) - data, err := afero.ReadFile(watcher.fs, f) if os.IsNotExist(err) || !bytes.Equal(data, secret.Data[filename]) { - if err := afero.WriteFile(watcher.fs, f, secret.Data[filename], 0666); err != nil { - return false, err - } - } else { - return false, err + return afero.WriteFile(watcher.fs, f, secret.Data[filename], 0666) } - return true, nil + return err + } func (watcher *CertificateWatcher) WaitForCertificates() { - for threshold := time.Now().Add(5 * time.Minute); time.Now().Before(threshold); { + for threshold := time.Now().Add(watcher.certificateTreshold); time.Now().Before(threshold); { if err := watcher.updateCertificatesFromSecret(); err != nil { if k8serrors.IsNotFound(err) { @@ -121,10 +118,10 @@ func (watcher *CertificateWatcher) WaitForCertificates() { } func (watcher *CertificateWatcher) ValidateCertificateExpiration(certData []byte, renewalThreshold time.Duration, now time.Time) (bool, error) { - if block, _ := pem.Decode(certData); block == nil { + if block, _ := watcher.Decode(certData); block == nil { watcher.Log.Error(errors.New("can't decode PEM file"), "failed to parse certificate") return false, nil - } else if cert, err := x509.ParseCertificate(block.Bytes); err != nil { + } else if cert, err := watcher.Parse(block.Bytes); err != nil { watcher.Log.Error(err, "failed to parse certificate") return false, err } else if now.After(cert.NotAfter.Add(-renewalThreshold)) { diff --git a/operator/cmd/certificates/watcher_test.go b/operator/cmd/certificates/watcher_test.go new file mode 100644 index 0000000000..0b393332d8 --- /dev/null +++ b/operator/cmd/certificates/watcher_test.go @@ -0,0 +1,318 @@ +package certificates + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-logr/logr/testr" + "github.com/keptn/lifecycle-toolkit/operator/cmd/certificates/fake" + fakeclient "github.com/keptn/lifecycle-toolkit/operator/controllers/common/fake" + "github.com/pkg/errors" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const CACERT = `-----BEGIN CERTIFICATE----- +MIICPTCCAeKgAwIBAgIRAMIV/0UqFGHgKSYOWBdx/KcwCgYIKoZIzj0EAwIwczEL +MAkGA1UEBhMCQVQxCzAJBgNVBAgTAktMMRMwEQYDVQQHEwpLbGFnZW5mdXJ0MQ4w +DAYDVQQKEwVLZXB0bjEZMBcGA1UECxMQTGlmZWN5Y2xlVG9vbGtpdDEXMBUGA1UE +AwwOKi5rZXB0bi1ucy5zdmMwHhcNMjMwNDE5MTEwNDUzWhcNMjQwNDE4MTEwNDUz +WjBzMQswCQYDVQQGEwJBVDELMAkGA1UECBMCS0wxEzARBgNVBAcTCktsYWdlbmZ1 +cnQxDjAMBgNVBAoTBUtlcHRuMRkwFwYDVQQLExBMaWZlY3ljbGVUb29sa2l0MRcw +FQYDVQQDDA4qLmtlcHRuLW5zLnN2YzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA +BPxAP4JTJfwKz/P32dXuyfVi7kinQPebSYwF/gRAUcN0dCAi6GnxbI2OXlcU0guD +zHXv3VRh3EX2fiNszcfKaCajVzBVMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAK +BggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQUGe/8XYV1HsZs +nWsyrOCCGr/sQDAKBggqhkjOPQQDAgNJADBGAiEAkcPaCANDXW5Uillrof0VrnPw +ow49D22Gsrh7YM+vmTQCIQDU1L5IT0Zz+bdIyFSsDnEUXZDeydNv56DoSLh+358Y +aw== +-----END CERTIFICATE-----` + +const CAKEY = `-----BEGIN PRIVATE KEY----- +MHcCAQEEII5SAqBxINKatksyu2mTvLZZhfEOpNinYJDwlQjkfreboAoGCCqGSM49 +AwEHoUQDQgAE/EA/glMl/ArP8/fZ1e7J9WLuSKdA95tJjAX+BEBRw3R0ICLoafFs +jY5eVxTSC4PMde/dVGHcRfZ+I2zNx8poJg== +-----END PRIVATE KEY-----` + +const uniqueIDPEM = `-----BEGIN CERTIFICATE----- +MIIFsDCCBJigAwIBAgIIrOyC1ydafZMwDQYJKoZIhvcNAQEFBQAwgY4xgYswgYgG +A1UEAx6BgABNAGkAYwByAG8AcwBvAGYAdAAgAEYAbwByAGUAZgByAG8AbgB0ACAA +VABNAEcAIABIAFQAVABQAFMAIABJAG4AcwBwAGUAYwB0AGkAbwBuACAAQwBlAHIA +dABpAGYAaQBjAGEAdABpAG8AbgAgAEEAdQB0AGgAbwByAGkAdAB5MB4XDTE0MDEx +ODAwNDEwMFoXDTE1MTExNTA5Mzc1NlowgZYxCzAJBgNVBAYTAklEMRAwDgYDVQQI +EwdqYWthcnRhMRIwEAYDVQQHEwlJbmRvbmVzaWExHDAaBgNVBAoTE3N0aG9ub3Jl +aG90ZWxyZXNvcnQxHDAaBgNVBAsTE3N0aG9ub3JlaG90ZWxyZXNvcnQxJTAjBgNV +BAMTHG1haWwuc3Rob25vcmVob3RlbHJlc29ydC5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCvuu0qpI+Ko2X84Twkf84cRD/rgp6vpgc5Ebejx/D4 +PEVON5edZkazrMGocK/oQqIlRxx/lefponN/chlGcllcVVPWTuFjs8k+Aat6T1qp +4iXxZekAqX+U4XZMIGJD3PckPL6G2RQSlF7/LhGCsRNRdKpMWSTbou2Ma39g52Kf +gsl3SK/GwLiWpxpcSkNQD1hugguEIsQYLxbeNwpcheXZtxbBGguPzQ7rH8c5vuKU +BkMOzaiNKLzHbBdFSrua8KWwCJg76Vdq/q36O9GlW6YgG3i+A4pCJjXWerI1lWwX +Ktk5V+SvUHGey1bkDuZKJ6myMk2pGrrPWCT7jP7WskChAgMBAAGBCQBCr1dgEleo +cKOCAfswggH3MIHDBgNVHREEgbswgbiCHG1haWwuc3Rob25vcmVob3RlbHJlc29y +dC5jb22CIGFzaGNoc3ZyLnN0aG9ub3JlaG90ZWxyZXNvcnQuY29tgiRBdXRvRGlz +Y292ZXIuc3Rob25vcmVob3RlbHJlc29ydC5jb22CHEF1dG9EaXNjb3Zlci5ob3Rl +bHJlc29ydC5jb22CCEFTSENIU1ZSghdzdGhvbm9yZWhvdGVscmVzb3J0LmNvbYIP +aG90ZWxyZXNvcnQuY29tMCEGCSsGAQQBgjcUAgQUHhIAVwBlAGIAUwBlAHIAdgBl +AHIwHQYDVR0OBBYEFMAC3UR4FwAdGekbhMgnd6lMejtbMAsGA1UdDwQEAwIFoDAT +BgNVHSUEDDAKBggrBgEFBQcDATAJBgNVHRMEAjAAMIG/BgNVHQEEgbcwgbSAFGfF +6xihk+gJJ5TfwvtWe1UFnHLQoYGRMIGOMYGLMIGIBgNVBAMegYAATQBpAGMAcgBv +AHMAbwBmAHQAIABGAG8AcgBlAGYAcgBvAG4AdAAgAFQATQBHACAASABUAFQAUABT +ACAASQBuAHMAcABlAGMAdABpAG8AbgAgAEMAZQByAHQAaQBmAGkAYwBhAHQAaQBv +AG4AIABBAHUAdABoAG8AcgBpAHQAeYIIcKhXEmBXr0IwDQYJKoZIhvcNAQEFBQAD +ggEBABlSxyCMr3+ANr+WmPSjyN5YCJBgnS0IFCwJAzIYP87bcTye/U8eQ2+E6PqG +Q7Huj7nfHEw9qnGo+HNyPp1ad3KORzXDb54c6xEoi+DeuPzYHPbn4c3hlH49I0aQ +eWW2w4RslSWpLvO6Y7Lboyz2/Thk/s2kd4RHxkkWpH2ltPqJuYYg3X6oM5+gIFHJ +WGnh+ojZ5clKvS5yXh3Wkj78M6sb32KfcBk0Hx6NkCYPt60ODYmWtvqwtw6r73u5 +TnTYWRNvo2svX69TriL+CkHY9O1Hkwf2It5zHl3gNiKTJVaak8AuEz/CKWZneovt +yYLwhUhg3PX5Co1VKYE+9TxloiE= +-----END CERTIFICATE-----` + +var ERR_BAD_CERT = errors.New("bad cert") + +var emptySecret = v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "my-cert", + }, +} + +var goodSecret = v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "my-cert", + }, + Data: map[string][]byte{ + ServerCert: []byte(CACERT), + ServerKey: []byte(CAKEY), + }, +} + +func TestCertificateWatcher_ValidateCertificateExpiration(t *testing.T) { + + tests := []struct { + name string + certHandler ICertificateHandler + certData []byte + renewalThreshold time.Duration + now time.Time + want bool + wantErr error + }{ + { + name: "certificate cannot be decoded", + certHandler: &fake.ICertificateHandlerMock{ + DecodeFunc: func(data []byte) (p *pem.Block, rest []byte) { + return nil, nil //fake a failure in the decoding + }, + ParseFunc: nil, + }, + want: false, + }, + { + name: "certificate cannot be parsed", + certHandler: &fake.ICertificateHandlerMock{ + DecodeFunc: func(data []byte) (p *pem.Block, rest []byte) { + return &pem.Block{Type: "test", Bytes: []byte("testdata")}, nil + }, + ParseFunc: func(der []byte) (*x509.Certificate, error) { + return nil, ERR_BAD_CERT + }, + }, + want: false, + wantErr: ERR_BAD_CERT, + }, + { + name: "good certificate - unexpired", + certData: []byte(uniqueIDPEM), + certHandler: defaultCertificateHandler{}, + want: true, + }, + { + name: "good certificate - expired", + certData: []byte(uniqueIDPEM), + now: time.Now(), //setting up now makes sure that the threshold is passed + certHandler: defaultCertificateHandler{}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + watcher := &CertificateWatcher{ + ICertificateHandler: tt.certHandler, + Log: testr.New(t), + } + got, err := watcher.ValidateCertificateExpiration(tt.certData, tt.renewalThreshold, tt.now) + if tt.wantErr != nil { + require.Error(t, err) + t.Log("want:", tt.wantErr, "got:", err) + require.True(t, errors.Is(tt.wantErr, err)) + } + require.Equal(t, got, tt.want) + }) + } +} + +func TestCertificateWatcher_ensureCertificateFile(t *testing.T) { + + certdir := t.TempDir() + f := filepath.Join(certdir, ServerCert) + err := os.WriteFile(f, goodSecret.Data[ServerCert], 0666) + require.Nil(t, err) + baddir := t.TempDir() + f = filepath.Join(baddir, ServerCert) + err = os.WriteFile(f, goodSecret.Data[ServerKey], 0666) + require.Nil(t, err) + tests := []struct { + name string + fs afero.Fs + secret v1.Secret + filename string + certDir string + wantErr bool + err string + }{ + { + name: "if good cert exist in fs no error", + secret: goodSecret, + certDir: certdir, + filename: ServerCert, + wantErr: false, + }, + + { + name: "if unexisting file name, we expect a file system error", + secret: emptySecret, + filename: "$%&/())=$§%/=", + certDir: baddir, + wantErr: true, + err: "no such file or directory", + }, + + { + name: "wrong file content is replaced with updated cert", + certDir: baddir, + secret: goodSecret, + filename: ServerCert, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + watcher := &CertificateWatcher{ + fs: afero.NewOsFs(), + certificateDirectory: tt.certDir, + } + err := watcher.ensureCertificateFile(tt.secret, tt.filename) + if !tt.wantErr { + require.Nil(t, err) + f = filepath.Join(tt.certDir, ServerCert) + data, err := os.ReadFile(f) + if err != nil { + panic(err) + } + if !bytes.Equal(data, tt.secret.Data[tt.filename]) { + t.Errorf("ensureCertificateFile()data %v was not replaced with %v", data, tt.secret.Data[tt.filename]) + } + } else { + require.Contains(t, err.Error(), tt.err) + } + }) + } +} + +func TestCertificateWatcher_updateCertificatesFromSecret(t *testing.T) { + + oldDir := t.TempDir() + os.Remove(oldDir) + + tests := []struct { + name string + apiReader client.Reader + certificateDirectory string + namespace string + certificateSecretName string + wantErr error + }{ + { + name: "certificate not found", + apiReader: fakeclient.NewClient(), + certificateDirectory: t.TempDir(), + namespace: "default", + certificateSecretName: "my-cert", + wantErr: errors.New("secrets \"my-cert\" not found"), + }, + { + name: "outdated certificate found, nothing in dir", + apiReader: fakeclient.NewClient(&emptySecret), + certificateDirectory: t.TempDir(), + namespace: "default", + certificateSecretName: "my-cert", + wantErr: errors.New("certificate is outdated"), + }, + + { + name: "outdated certificate found, not existing in dir", + apiReader: fakeclient.NewClient(&emptySecret), + certificateDirectory: oldDir, + namespace: "default", + certificateSecretName: "my-cert", + wantErr: errors.New("certificate is outdated"), + }, + { + name: "good certificate - not stored", + apiReader: fakeclient.NewClient(&goodSecret), + certificateDirectory: t.TempDir(), + namespace: "default", + certificateSecretName: "my-cert", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + watcher := &CertificateWatcher{ + apiReader: tt.apiReader, + fs: afero.NewOsFs(), + certificateDirectory: tt.certificateDirectory, + namespace: tt.namespace, + certificateSecretName: tt.certificateSecretName, + ICertificateHandler: defaultCertificateHandler{}, + Log: testr.New(t), + } + err := watcher.updateCertificatesFromSecret() + if tt.wantErr == nil { + require.Nil(t, err) + } else { + require.NotNil(t, err) + require.Contains(t, err.Error(), tt.wantErr.Error()) + } + }) + } +} + +func TestNewCertificateWatcher(t *testing.T) { + logger := testr.New(t) + client := fakeclient.NewClient() + want := &CertificateWatcher{ + apiReader: client, + fs: afero.NewOsFs(), + namespace: "default", + certificateSecretName: "my-secret", + certificateDirectory: "test", + certificateTreshold: CertThreshold, + ICertificateHandler: defaultCertificateHandler{}, + Log: testr.New(t), + } + got := NewCertificateWatcher(client, "test", "default", "my-secret", logger) + require.EqualValues(t, got, want) + +} diff --git a/operator/cmd/webhook/builder.go b/operator/cmd/webhook/builder.go index ef742b58e4..d87b0ef591 100644 --- a/operator/cmd/webhook/builder.go +++ b/operator/cmd/webhook/builder.go @@ -72,7 +72,7 @@ func (builder Builder) Run(webhookManager manager.Manager) error { builder.GetManagerProvider().SetupWebhookServer(webhookManager) certificates. - NewCertificateWatcher(webhookManager, builder.namespace, webhooks.SecretCertsName, ctrl.Log.WithName("Webhook Cert Manager")). + NewCertificateWatcher(webhookManager.GetAPIReader(), webhookManager.GetWebhookServer().CertDir, builder.namespace, webhooks.SecretCertsName, ctrl.Log.WithName("Webhook Cert Manager")). WaitForCertificates() webhookManager.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{ diff --git a/test/testcertificate/cert-recreates/00-assert.yaml b/test/testcertificate/cert-recreates/00-assert.yaml new file mode 100644 index 0000000000..53a3c423ac --- /dev/null +++ b/test/testcertificate/cert-recreates/00-assert.yaml @@ -0,0 +1,22 @@ +# certificate is recreated there are operators instances available +apiVersion: v1 +kind: Secret +metadata: + name: klt-certs + namespace: keptn-lifecycle-toolkit-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lifecycle-operator + namespace: keptn-lifecycle-toolkit-system +status: + readyReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: metrics-operator + namespace: keptn-lifecycle-toolkit-system +status: + readyReplicas: 1 diff --git a/test/testcertificate/cert-recreates/00-teststep.yaml b/test/testcertificate/cert-recreates/00-teststep.yaml new file mode 100644 index 0000000000..dd15873612 --- /dev/null +++ b/test/testcertificate/cert-recreates/00-teststep.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1 +kind: TestStep +commands: # first scenario: the toolkit is restarted after removing the certificate + - script: kubectl delete secret klt-certs -n keptn-lifecycle-toolkit-system + - script: kubectl rollout restart deployment -n keptn-lifecycle-toolkit-system -l control-plane=lifecycle-operator + - script: kubectl rollout restart deployment -n keptn-lifecycle-toolkit-system -l control-plane=metrics-operator diff --git a/test/testcertificate/cert-recreates/01-assert.yaml b/test/testcertificate/cert-recreates/01-assert.yaml new file mode 100644 index 0000000000..7348173bb1 --- /dev/null +++ b/test/testcertificate/cert-recreates/01-assert.yaml @@ -0,0 +1,22 @@ +# certificate is recreated and there are operators instances available +apiVersion: v1 +kind: Secret +metadata: + name: klt-certs + namespace: keptn-lifecycle-toolkit-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lifecycle-operator + namespace: keptn-lifecycle-toolkit-system +status: + readyReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: metrics-operator + namespace: keptn-lifecycle-toolkit-system +status: + readyReplicas: 1 diff --git a/test/testcertificate/cert-recreates/01-delete.yaml b/test/testcertificate/cert-recreates/01-delete.yaml new file mode 100644 index 0000000000..1ca271c25a --- /dev/null +++ b/test/testcertificate/cert-recreates/01-delete.yaml @@ -0,0 +1,6 @@ +# second scenario: certificate is removed with no restart +apiVersion: v1 +kind: Secret +metadata: + name: klt-certs + namespace: keptn-lifecycle-toolkit-system diff --git a/test/testcertificate/cert-recreates/02-assert.yaml b/test/testcertificate/cert-recreates/02-assert.yaml new file mode 100644 index 0000000000..ca2a907616 --- /dev/null +++ b/test/testcertificate/cert-recreates/02-assert.yaml @@ -0,0 +1,24 @@ +# certificate is recreated and there are operators instances available +apiVersion: v1 +kind: Secret +metadata: + name: klt-certs + namespace: keptn-lifecycle-toolkit-system + annotations: + mycert: "true" # make sure this is the latest secret +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lifecycle-operator + namespace: keptn-lifecycle-toolkit-system +status: + readyReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: metrics-operator + namespace: keptn-lifecycle-toolkit-system +status: + readyReplicas: 1 diff --git a/test/testcertificate/cert-recreates/02-install.yaml b/test/testcertificate/cert-recreates/02-install.yaml new file mode 100644 index 0000000000..28abe03683 --- /dev/null +++ b/test/testcertificate/cert-recreates/02-install.yaml @@ -0,0 +1,13 @@ +# third scenario: certificate is invalid/expired +apiVersion: v1 +kind: Secret +metadata: + name: klt-certs + namespace: keptn-lifecycle-toolkit-system + annotations: + mycert: "true" +data: + # yamllint disable rule:line-length + tls.crt: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNkekNDQWg2Z0F3SUJBZ0lRZUpBWkJMcmxCY2VqNzh3cm4wV1ZIekFLQmdncWhrak9QUVFEQWpDQmlURUwKa0ZGY1FpY29hdE8yRFJnPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t" + # yamllint disable rule:line-length + tls.key: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNkekNDQWg2Z0F3SUJBZ0lRZUpBWkJMcmxCY2VqNzh3cm4wV1ZIekFLQmdncWhrak9QUVFEQWpDQmlURUwKa0ZGY1FpY29hdE8yRFJnPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t" diff --git a/test/testcertificate/cert-recreates/02-teststep.yaml b/test/testcertificate/cert-recreates/02-teststep.yaml new file mode 100644 index 0000000000..1553671775 --- /dev/null +++ b/test/testcertificate/cert-recreates/02-teststep.yaml @@ -0,0 +1,12 @@ +apiVersion: kuttl.dev/v1 +kind: TestStep +commands: + - script: | # make sure secret is updated from the bad one to a proper one + str1=$(kubectl get secret klt-certs -n keptn-lifecycle-toolkit-system -o=go-template='{{index .data "tls.crt"}}') + str2="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNkekNDQWg2Z0F3SUJBZ0lRZUpBWkJMcmxCY2VqNzh3cm4wV1ZIekFLQmdncWhrak9QUVFEQWpDQmlURUwKa0ZGY1FpY29hdE8yRFJnPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t" + if [ "$str1" == "$str2" ]; then + echo "Strings are equal" $str1 + exit 1 + else + echo "Strings are not equal" $str1 "!=" $str2 + fi