diff --git a/docs/examples/aspnetapp.yaml b/docs/examples/aspnetapp.yaml index b7cae86f5..36ffac8b2 100644 --- a/docs/examples/aspnetapp.yaml +++ b/docs/examples/aspnetapp.yaml @@ -9,7 +9,7 @@ spec: - image: "mcr.microsoft.com/dotnet/samples:aspnetapp" name: aspnetapp-image ports: - - containerPort: 80 + - containerPort: 8080 protocol: TCP --- @@ -24,7 +24,7 @@ spec: ports: - protocol: TCP port: 80 - targetPort: 80 + targetPort: 8080 --- diff --git a/pkg/k8scontext/context.go b/pkg/k8scontext/context.go index f23c83131..4ec1bae75 100644 --- a/pkg/k8scontext/context.go +++ b/pkg/k8scontext/context.go @@ -107,7 +107,7 @@ func NewContext(kubeClient kubernetes.Interface, crdClient versioned.Interface, informers: &informerCollection, ingressSecretsMap: utils.NewThreadsafeMultimap(), Caches: &cacheCollection, - CertificateSecretStore: NewSecretStore(), + CertificateSecretStore: NewSecretStore(kubeClient), Work: make(chan events.Event, workBuffer), CacheSynced: make(chan interface{}), diff --git a/pkg/k8scontext/handlers_test.go b/pkg/k8scontext/handlers_test.go index 70b540eed..9f68bd6e9 100644 --- a/pkg/k8scontext/handlers_test.go +++ b/pkg/k8scontext/handlers_test.go @@ -12,7 +12,7 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" diff --git a/pkg/k8scontext/ingress_handlers_test.go b/pkg/k8scontext/ingress_handlers_test.go index 114787c52..8938a7e9b 100644 --- a/pkg/k8scontext/ingress_handlers_test.go +++ b/pkg/k8scontext/ingress_handlers_test.go @@ -9,7 +9,7 @@ import ( "context" "time" - "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/k8scontext/k8scontext_suite_test.go b/pkg/k8scontext/k8scontext_suite_test.go index 77c18771f..c1622e973 100644 --- a/pkg/k8scontext/k8scontext_suite_test.go +++ b/pkg/k8scontext/k8scontext_suite_test.go @@ -3,6 +3,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. // -------------------------------------------------------------------------------------------- +//go:build unittest // +build unittest package k8scontext @@ -11,7 +12,7 @@ import ( "flag" "testing" - "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" "k8s.io/klog/v2" ) diff --git a/pkg/k8scontext/k8scontext_test.go b/pkg/k8scontext/k8scontext_test.go index f0cb0727b..0c3aa558e 100644 --- a/pkg/k8scontext/k8scontext_test.go +++ b/pkg/k8scontext/k8scontext_test.go @@ -12,7 +12,7 @@ import ( "github.com/getlantern/deepcopy" "github.com/knative/pkg/apis/istio/v1alpha3" - "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" networking "k8s.io/api/networking/v1" @@ -73,7 +73,6 @@ var _ = ginkgo.Describe("K8scontext", func() { } } case <-time.After(1 * time.Second): - break } if len(exists) == len(resourceList) { diff --git a/pkg/k8scontext/secrets_handlers.go b/pkg/k8scontext/secrets_handlers.go index 2ac66e0c7..18461646f 100644 --- a/pkg/k8scontext/secrets_handlers.go +++ b/pkg/k8scontext/secrets_handlers.go @@ -18,7 +18,12 @@ import ( // secret resource handlers func (h handlers) secretAdd(obj interface{}) { - sec := obj.(*v1.Secret) + sec, ok := obj.(*v1.Secret) + if !ok { + klog.Error("error decoding object, invalid type") + return + } + if _, exists := namespacesToIgnore[sec.Namespace]; exists { return } @@ -42,7 +47,12 @@ func (h handlers) secretAdd(obj interface{}) { } func (h handlers) secretUpdate(oldObj, newObj interface{}) { - sec := newObj.(*v1.Secret) + sec, ok := newObj.(*v1.Secret) + if !ok { + klog.Error("error decoding object, invalid type") + return + } + if _, exists := namespacesToIgnore[sec.Namespace]; exists { return } @@ -70,6 +80,11 @@ func (h handlers) secretUpdate(oldObj, newObj interface{}) { func (h handlers) secretDelete(obj interface{}) { sec, ok := obj.(*v1.Secret) + if !ok { + klog.Error("error decoding object, invalid type") + return + } + if _, exists := namespacesToIgnore[sec.Namespace]; exists { return } diff --git a/pkg/k8scontext/secrets_handlers_test.go b/pkg/k8scontext/secrets_handlers_test.go index b29cfcbdc..a3bfa86ac 100644 --- a/pkg/k8scontext/secrets_handlers_test.go +++ b/pkg/k8scontext/secrets_handlers_test.go @@ -12,7 +12,7 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes" testclient "k8s.io/client-go/kubernetes/fake" diff --git a/pkg/k8scontext/secretstore.go b/pkg/k8scontext/secretstore.go index a083d8163..834834b31 100644 --- a/pkg/k8scontext/secretstore.go +++ b/pkg/k8scontext/secretstore.go @@ -7,20 +7,18 @@ package k8scontext import ( "bytes" + "context" "os" "os/exec" "sync" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" "github.com/Azure/application-gateway-kubernetes-ingress/pkg/controllererrors" -) - -const ( - recognizedSecretType = "kubernetes.io/tls" - tlsKey = "tls.key" - tlsCrt = "tls.crt" + "github.com/Azure/application-gateway-kubernetes-ingress/pkg/utils" ) // SecretsKeeper is the interface definition for secret store @@ -33,13 +31,15 @@ type SecretsKeeper interface { // SecretsStore maintains a cache of the deployment secrets. type SecretsStore struct { conversionSync sync.Mutex + Client kubernetes.Interface Cache cache.ThreadSafeStore } // NewSecretStore creates a new SecretsKeeper object -func NewSecretStore() SecretsKeeper { +func NewSecretStore(client kubernetes.Interface) SecretsKeeper { return &SecretsStore{ - Cache: cache.NewThreadSafeStore(cache.Indexers{}, cache.Indices{}), + Cache: cache.NewThreadSafeStore(cache.Indexers{}, cache.Indices{}), + Client: client, } } @@ -50,9 +50,33 @@ func (s *SecretsStore) GetPfxCertificate(secretKey string) []byte { return cert } } + + if cert, err := s.GetFromCluster(secretKey); err == nil { + return cert + } return nil } +func (s *SecretsStore) GetFromCluster(secretKey string) ([]byte, error) { + secretNamespace, secretName, err := utils.ParseNamespacedName(secretKey) + if err != nil { + return nil, err + } + + secret, err := s.Client.CoreV1().Secrets(secretNamespace).Get(context.Background(), secretName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + if err := s.ConvertSecret(secretKey, secret); err != nil { + return nil, err + } + + certInterface, _ := s.Cache.Get(secretKey) + cert, _ := certInterface.([]byte) + return cert, nil +} + func (s *SecretsStore) delete(secretKey string) { s.conversionSync.Lock() defer s.conversionSync.Unlock() @@ -66,14 +90,14 @@ func (s *SecretsStore) ConvertSecret(secretKey string, secret *v1.Secret) error defer s.conversionSync.Unlock() // check if this is a secret with the correct type - if secret.Type != recognizedSecretType { + if secret.Type != v1.SecretTypeTLS { return controllererrors.NewErrorf( controllererrors.ErrorUnknownSecretType, "secret [%v] is not type kubernetes.io/tls", secretKey, ) } - if len(secret.Data[tlsKey]) == 0 || len(secret.Data[tlsCrt]) == 0 { + if len(secret.Data[v1.TLSCertKey]) == 0 || len(secret.Data[v1.TLSPrivateKeyKey]) == 0 { return controllererrors.NewErrorf( controllererrors.ErrorMalformedSecret, "secret [%v] is malformed, tls.key or tls.crt is not defined", secretKey, diff --git a/pkg/k8scontext/secretstore_test.go b/pkg/k8scontext/secretstore_test.go index 1f12dc18e..120e3fcca 100644 --- a/pkg/k8scontext/secretstore_test.go +++ b/pkg/k8scontext/secretstore_test.go @@ -6,42 +6,58 @@ package k8scontext import ( - "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + testclient "k8s.io/client-go/kubernetes/fake" "github.com/Azure/application-gateway-kubernetes-ingress/pkg/controllererrors" "github.com/Azure/application-gateway-kubernetes-ingress/pkg/tests" ) var _ = ginkgo.Describe("Testing K8sContext.SecretStore", func() { - secretsStore := NewSecretStore() - ginkgo.Context("Test ConvertSecret function", func() { - secret := v1.Secret{} - ginkgo.It("Should have returned an error - unrecognized type of secret", func() { - err := secretsStore.ConvertSecret("someKey", &secret) - Expect(err.(*controllererrors.Error).Code).To(Equal(controllererrors.ErrorUnknownSecretType)) - }) - ginkgo.It("", func() { - malformed := secret - malformed.Type = recognizedSecretType - err := secretsStore.ConvertSecret("someKey", &malformed) - Expect(err.(*controllererrors.Error).Code).To(Equal(controllererrors.ErrorMalformedSecret)) - }) - ginkgo.It("", func() { - malformed := secret - malformed.Type = recognizedSecretType - malformed.Data = make(map[string][]byte) - malformed.Data[tlsKey] = []byte("X") - malformed.Data[tlsCrt] = []byte("Y") - err := secretsStore.ConvertSecret("someKey", &malformed) - Expect(err.(*controllererrors.Error).Code).To(Equal(controllererrors.ErrorExportingWithOpenSSL)) - }) - ginkgo.It("", func() { + secretsStore := NewSecretStore(nil) + + ginkgo.DescribeTable("when converting certificate to PFX", + func(secret *v1.Secret, expectedError controllererrors.ErrorCode) { + err := secretsStore.ConvertSecret("someKey", secret) + Expect(err.(*controllererrors.Error).Code).To(Equal(expectedError)) + }, + ginkgo.Entry("no type in secret", &v1.Secret{}, controllererrors.ErrorUnknownSecretType), + ginkgo.Entry("unrecognized type of secret", &v1.Secret{Type: v1.SecretTypeOpaque}, controllererrors.ErrorUnknownSecretType), + ginkgo.Entry("malformed data", &v1.Secret{Type: v1.SecretTypeTLS, Data: map[string][]byte{}}, controllererrors.ErrorMalformedSecret), + ginkgo.Entry("invalid data", &v1.Secret{Type: v1.SecretTypeTLS, Data: map[string][]byte{ + v1.TLSCertKey: []byte("X"), + v1.TLSPrivateKeyKey: []byte("X"), + }}, controllererrors.ErrorExportingWithOpenSSL), + ) + + ginkgo.When("certificate gets stored", func() { + ginkgo.It("should be retrivable with the secret key", func() { err := secretsStore.ConvertSecret("someKey", tests.NewSecretTestFixture()) Expect(err).ToNot(HaveOccurred()) actual := secretsStore.GetPfxCertificate("someKey") Expect(len(actual)).To(BeNumerically(">", 0)) }) }) + + ginkgo.When("certificate is no cached", func() { + ginkgo.It("should get it from the api-server", func() { + secret := tests.NewSecretTestFixture() + var client kubernetes.Interface = testclient.NewSimpleClientset(secret) + secretsStore := NewSecretStore(client) + + actual := secretsStore.GetPfxCertificate(secret.Namespace + "/" + secret.Name) + Expect(len(actual)).To(BeNumerically(">", 0)) + }) + + ginkgo.It("should return nil if secret does not exist", func() { + var client kubernetes.Interface = testclient.NewSimpleClientset() + secretsStore := NewSecretStore(client) + + actual := secretsStore.GetPfxCertificate("someKey") + Expect(actual).To(BeNil()) + }) + }) }) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 3b0037cb6..39b927b8a 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -9,8 +9,8 @@ import ( "crypto/md5" "encoding/json" "fmt" - "os" "math/rand" + "os" "strings" "time" @@ -97,3 +97,11 @@ func RemoveDuplicateStrings(list []string) []string { return result } + +func ParseNamespacedName(namespacedName string) (string, string, error) { + split := strings.Split(namespacedName, "/") + if len(split) != 2 { + return "", "", fmt.Errorf("invalid namespaced name %s", namespacedName) + } + return split[0], split[1], nil +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 47d259b6a..0079a9d5a 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -3,6 +3,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. // -------------------------------------------------------------------------------------------- +//go:build unittest // +build unittest package utils @@ -20,83 +21,94 @@ func TestUtils(t *testing.T) { } var _ = Describe("Utils", func() { - Describe("Testing `utils` helpers", func() { - - Context("Testing the Kubernetes namespace generator", func() { - It("Given a namespace and resource it should return the Kubernetes resource identifier.", func() { - Expect(GetResourceKey("default", "pod")).To(Equal("default/pod")) - }) + Context("Testing the Kubernetes namespace generator", func() { + It("Given a namespace and resource it should return the Kubernetes resource identifier.", func() { + Expect(GetResourceKey("default", "pod")).To(Equal("default/pod")) }) + }) - Context("Test GetLastChunkOfSlashed", func() { - It("Should return the last slice of a string split on a slash.", func() { - Expect(GetLastChunkOfSlashed("a/b/c")).To(Equal("c")) - }) + Context("Test GetLastChunkOfSlashed", func() { + It("Should return the last slice of a string split on a slash.", func() { + Expect(GetLastChunkOfSlashed("a/b/c")).To(Equal("c")) + }) - It("Should return the full string when there are no slashes.", func() { - Expect(GetLastChunkOfSlashed("abc")).To(Equal("abc")) - }) + It("Should return the full string when there are no slashes.", func() { + Expect(GetLastChunkOfSlashed("abc")).To(Equal("abc")) }) + }) - Context("Test SaveToFile", func() { - It("should return the path to the temp file and no error", func() { - pathToFile, err := SaveToFile("blah", []byte("content")) - Expect(err).ToNot(HaveOccurred()) - Expect(pathToFile).To(ContainSubstring("blah")) - }) + Context("Test SaveToFile", func() { + It("should return the path to the temp file and no error", func() { + pathToFile, err := SaveToFile("blah", []byte("content")) + Expect(err).ToNot(HaveOccurred()) + Expect(pathToFile).To(ContainSubstring("blah")) }) + }) - Context("Test PrettyJSON", func() { - It("should return pretty JSON and no error", func() { - prettyJSON, err := PrettyJSON([]byte("{\"name\":\"baba yaga\"}"), "--prefix--") - Expect(err).ToNot(HaveOccurred()) - Expect(prettyJSON).To(Equal([]byte(`{ + Context("Test PrettyJSON", func() { + It("should return pretty JSON and no error", func() { + prettyJSON, err := PrettyJSON([]byte("{\"name\":\"baba yaga\"}"), "--prefix--") + Expect(err).ToNot(HaveOccurred()) + Expect(prettyJSON).To(Equal([]byte(`{ --prefix-- "name": "baba yaga" --prefix--}`))) - }) }) + }) - Context("Test GetHashCode", func() { - It("should generate a deterministic hash", func() { - hashcode := GetHashCode([]string{"testing hash generation"}) - Expect(hashcode).To(Equal("28a37ff7b783ffb4696dfb7774331163")) - }) + Context("Test GetHashCode", func() { + It("should generate a deterministic hash", func() { + hashcode := GetHashCode([]string{"testing hash generation"}) + Expect(hashcode).To(Equal("28a37ff7b783ffb4696dfb7774331163")) }) + }) - Context("Test RandStringRunes", func() { - It("should generate n length string", func() { - Expect(len(RandStringRunes(10))).To(Equal(10)) - }) + Context("Test RandStringRunes", func() { + It("should generate n length string", func() { + Expect(len(RandStringRunes(10))).To(Equal(10)) + }) + + It("should not fail when n = 0", func() { + Expect(len(RandStringRunes(0))).To(Equal(0)) + }) + }) + + DescribeTable("Test RemoveDuplicateStrings", + func(input []string, expected []string) { + Expect(RemoveDuplicateStrings(input)).To(Equal(expected)) + }, + Entry( + "Should remove duplicate strings", + []string{"1", "1", "2", "3", "4", "3", "5", "1"}, + []string{"1", "2", "3", "4", "5"}, + ), + Entry( + "Should handle slices with no duplicates", + []string{"1", "1", "2", "3", "4", "3", "5", "1"}, + []string{"1", "2", "3", "4", "5"}, + ), + Entry( + "Should return empty slice if input is empty", + []string{}, + []string{}, + ), + Entry( + "Should return nil if input is nil", + []string(nil), + []string(nil), + ), + ) - It("should not fail when n = 0", func() { - Expect(len(RandStringRunes(0))).To(Equal(0)) - }) + Context("Test ParseNamespacedName", func() { + It("should return namespace and name", func() { + namespace, name, err := ParseNamespacedName("namespace/name") + Expect(err).ToNot(HaveOccurred()) + Expect(namespace).To(Equal("namespace")) + Expect(name).To(Equal("name")) }) - DescribeTable("Test RemoveDuplicateStrings", - func(input []string, expected []string) { - Expect(RemoveDuplicateStrings(input)).To(Equal(expected)) - }, - Entry( - "Should remove duplicate strings", - []string{"1", "1", "2", "3", "4", "3", "5", "1"}, - []string{"1", "2", "3", "4", "5"}, - ), - Entry( - "Should handle slices with no duplicates", - []string{"1", "1", "2", "3", "4", "3", "5", "1"}, - []string{"1", "2", "3", "4", "5"}, - ), - Entry( - "Should return empty slice if input is empty", - []string{}, - []string{}, - ), - Entry( - "Should return nil if input is nil", - []string(nil), - []string(nil), - ), - ) + It("should return error when namespaced name is invalid", func() { + _, _, err := ParseNamespacedName("namespace") + Expect(err).To(HaveOccurred()) + }) }) })