diff --git a/cmd/cli/namespace_add.go b/cmd/cli/namespace_add.go index 5d924edb9e..872ed77b39 100644 --- a/cmd/cli/namespace_add.go +++ b/cmd/cli/namespace_add.go @@ -16,16 +16,18 @@ import ( ) const namespaceAddDescription = ` -This command will join a namespace or a set of namespaces -to the mesh. All services in joined namespaces will be part -of the mesh. +This command will add a namespace or a set of namespaces +to the mesh. Optionally, the namespaces can be configured +for automatic sidecar injection which enables pods in the +added namespaces to be injected with a sidecar upon creation. ` type namespaceAddCmd struct { - out io.Writer - namespaces []string - meshName string - clientSet kubernetes.Interface + out io.Writer + namespaces []string + meshName string + enableSidecarInjection bool + clientSet kubernetes.Interface } func newNamespaceAdd(out io.Writer) *cobra.Command { @@ -57,6 +59,7 @@ func newNamespaceAdd(out io.Writer) *cobra.Command { //add mesh name flag f := cmd.Flags() f.StringVar(&namespaceAdd.meshName, "mesh-name", "osm", "Name of the service mesh") + f.BoolVar(&namespaceAdd.enableSidecarInjection, "enable-sidecar-injection", false, "Enable automatic sidecar injection") return cmd } @@ -78,10 +81,35 @@ func (a *namespaceAddCmd) run() error { if len(list.Items) != 0 { fmt.Fprintf(a.out, "Namespace [%s] already has [%s] installed and cannot be added to mesh [%s]\n", ns, constants.OSMControllerName, a.meshName) } else { - patch := `{"metadata":{"labels":{"` + constants.OSMKubeResourceMonitorAnnotation + `":"` + a.meshName + `"}}}` + var patch string + if a.enableSidecarInjection { + // Patch the namespace with the monitoring label and sidecar injection annotation + patch = fmt.Sprintf(` +{ + "metadata": { + "labels": { + "%s": "%s" + }, + "annotations": { + "%s": "enabled" + } + } +}`, constants.OSMKubeResourceMonitorAnnotation, a.meshName, constants.SidecarInjectionAnnotation) + } else { + // Patch the namespace with monitoring label + patch = fmt.Sprintf(` +{ + "metadata": { + "labels": { + "%s": "%s" + } + } +}`, constants.OSMKubeResourceMonitorAnnotation, a.meshName) + } + _, err := a.clientSet.CoreV1().Namespaces().Patch(ctx, ns, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}, "") if err != nil { - return errors.Errorf("Could not label namespace [%s]: %v", ns, err) + return errors.Errorf("Could not add namespace [%s] to mesh [%s]: %v", ns, a.meshName, err) } fmt.Fprintf(a.out, "Namespace [%s] successfully added to mesh [%s]\n", ns, a.meshName) diff --git a/cmd/cli/namespace_remove.go b/cmd/cli/namespace_remove.go index d3ef76fb6e..d23317d407 100644 --- a/cmd/cli/namespace_remove.go +++ b/cmd/cli/namespace_remove.go @@ -17,7 +17,6 @@ import ( const namespaceRemoveDescription = ` This command will remove a namespace from the mesh. All services in this namespace will be removed from the mesh. - ` type namespaceRemoveCmd struct { @@ -73,12 +72,25 @@ func (r *namespaceRemoveCmd) run() error { val, exists := namespace.ObjectMeta.Labels[constants.OSMKubeResourceMonitorAnnotation] if exists { if val == r.meshName { - patch := `{"metadata":{"labels":{"$patch":"delete", "` + constants.OSMKubeResourceMonitorAnnotation + `":"` + r.meshName + `"}}}` + // Setting null for a key in a map removes only that specific key, which is the desired behavior. + // Even if the key does not exist, there will be no side effects with setting the key to null, which + // will result in the same behavior as if the key were present - the key being removed. + patch := fmt.Sprintf(` +{ + "metadata": { + "labels": { + "%s": null + }, + "annotations": { + "%s": null + } + } +}`, constants.OSMKubeResourceMonitorAnnotation, constants.SidecarInjectionAnnotation) _, err = r.clientSet.CoreV1().Namespaces().Patch(ctx, r.namespace, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}, "") if err != nil { - return errors.Errorf("Could not remove label from namespace %s: %v", r.namespace, err) + return errors.Errorf("Could not remove namespace [%s] from mesh [%s]: %v", r.namespace, r.meshName, err) } fmt.Fprintf(r.out, "Namespace [%s] successfully removed from mesh [%s]\n", r.namespace, r.meshName) diff --git a/cmd/cli/namespace_test.go b/cmd/cli/namespace_test.go index 9ec2418b64..83538aa54a 100644 --- a/cmd/cli/namespace_test.go +++ b/cmd/cli/namespace_test.go @@ -32,13 +32,13 @@ var _ = Describe("Running the namespace add command", func() { err error ) - Context("given one namespace as an arg", func() { + Context("given one namespace as an arg without sidecar injection enabled", func() { BeforeEach(func() { out = new(bytes.Buffer) fakeClientSet = fake.NewSimpleClientset() - nsSpec := createNamespaceSpec(testNamespace, "") + nsSpec := createNamespaceSpec(testNamespace, "", false) fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), nsSpec, metav1.CreateOptions{}) namespaceAddCmd := &namespaceAddCmd{ @@ -58,7 +58,62 @@ var _ = Describe("Running the namespace add command", func() { It("should give a message confirming the successful install", func() { Expect(out.String()).To(Equal(fmt.Sprintf("Namespace [%s] successfully added to mesh [%s]\n", testNamespace, testMeshName))) }) + + It("should correctly add a label to the namespace", func() { + ns, err := fakeClientSet.CoreV1().Namespaces().Get(context.TODO(), testNamespace, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(ns.Labels[constants.OSMKubeResourceMonitorAnnotation]).To(Equal(testMeshName)) + }) + + It("should correctly not add an inject annotation to the namespace", func() { + ns, err := fakeClientSet.CoreV1().Namespaces().Get(context.TODO(), testNamespace, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(ns.Annotations).ShouldNot(HaveKey(constants.SidecarInjectionAnnotation)) + }) }) + + Context("given one namespace as an arg with sidecar injection enabled", func() { + + BeforeEach(func() { + out = new(bytes.Buffer) + fakeClientSet = fake.NewSimpleClientset() + + nsSpec := createNamespaceSpec(testNamespace, "", false) + _, err = fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), nsSpec, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + namespaceAddCmd := &namespaceAddCmd{ + out: out, + meshName: testMeshName, + namespaces: []string{testNamespace}, + enableSidecarInjection: true, + clientSet: fakeClientSet, + } + + err = namespaceAddCmd.run() + }) + + It("should not error", func() { + Expect(err).NotTo(HaveOccurred()) + }) + + It("should give a message confirming the successful install", func() { + Expect(out.String()).To(Equal(fmt.Sprintf("Namespace [%s] successfully added to mesh [%s]\n", testNamespace, testMeshName))) + }) + + It("should correctly add a label to the namespace", func() { + ns, err := fakeClientSet.CoreV1().Namespaces().Get(context.TODO(), testNamespace, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(ns.Labels[constants.OSMKubeResourceMonitorAnnotation]).To(Equal(testMeshName)) + }) + + It("should correctly add an inject annotation to the namespace", func() { + ns, err := fakeClientSet.CoreV1().Namespaces().Get(context.TODO(), testNamespace, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(ns.Annotations[constants.SidecarInjectionAnnotation]).To(Equal("enabled")) + }) + }) + Context("given two namespaces as args", func() { var ( @@ -70,10 +125,10 @@ var _ = Describe("Running the namespace add command", func() { fakeClientSet = fake.NewSimpleClientset() testNamespace2 = "namespace2" - nsSpec := createNamespaceSpec(testNamespace, "") + nsSpec := createNamespaceSpec(testNamespace, "", false) fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), nsSpec, metav1.CreateOptions{}) - nsSpec2 := createNamespaceSpec(testNamespace2, "") + nsSpec2 := createNamespaceSpec(testNamespace2, "", false) fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), nsSpec2, metav1.CreateOptions{}) namespaceAddCmd := &namespaceAddCmd{ @@ -93,7 +148,18 @@ var _ = Describe("Running the namespace add command", func() { It("should give a message confirming the successful install", func() { Expect(out.String()).To(Equal(fmt.Sprintf("Namespace [%s] successfully added to mesh [%s]\nNamespace [%s] successfully added to mesh [%s]\n", testNamespace, testMeshName, testNamespace2, testMeshName))) }) + + It("should correctly add a label to all the namespaces", func() { + ns1, err := fakeClientSet.CoreV1().Namespaces().Get(context.TODO(), testNamespace, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(ns1.Labels[constants.OSMKubeResourceMonitorAnnotation]).To(Equal(testMeshName)) + + ns2, err := fakeClientSet.CoreV1().Namespaces().Get(context.TODO(), testNamespace2, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(ns2.Labels[constants.OSMKubeResourceMonitorAnnotation]).To(Equal(testMeshName)) + }) }) + Context("given one namespace with osm-controller installed in it as an arg", func() { BeforeEach(func() { out = new(bytes.Buffer) @@ -102,7 +168,7 @@ var _ = Describe("Running the namespace add command", func() { deploymentSpec := createDeploymentSpec(testNamespace, defaultMeshName) fakeClientSet.AppsV1().Deployments(testNamespace).Create(context.TODO(), deploymentSpec, metav1.CreateOptions{}) - nsSpec := createNamespaceSpec(testNamespace, "") + nsSpec := createNamespaceSpec(testNamespace, "", false) fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), nsSpec, metav1.CreateOptions{}) namespaceAddCmd := &namespaceAddCmd{ @@ -147,7 +213,7 @@ var _ = Describe("Running the namespace add command", func() { It("should error", func() { Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal(fmt.Sprintf("Could not label namespace [%s]: namespaces \"%s\" not found", testNamespace, testNamespace))) + Expect(err.Error()).To(Equal(fmt.Sprintf("Could not add namespace [%s] to mesh [%s]: namespaces \"%s\" not found", testNamespace, testMeshName, testNamespace))) }) }) }) @@ -165,7 +231,7 @@ var _ = Describe("Running the namespace remove command", func() { out = new(bytes.Buffer) fakeClientSet = fake.NewSimpleClientset() - nsSpec := createNamespaceSpec(testNamespace, testMeshName) + nsSpec := createNamespaceSpec(testNamespace, testMeshName, false) fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), nsSpec, metav1.CreateOptions{}) namespaceRemoveCmd := &namespaceRemoveCmd{ @@ -185,6 +251,58 @@ var _ = Describe("Running the namespace remove command", func() { It("should give a message confirming the successful install", func() { Expect(out.String()).To(Equal(fmt.Sprintf("Namespace [%s] successfully removed from mesh [%s]\n", testNamespace, testMeshName))) }) + + It("should correctly remove the label on the namespace", func() { + ns, err := fakeClientSet.CoreV1().Namespaces().Get(context.TODO(), testNamespace, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(ns.Labels).ShouldNot(HaveKey(constants.OSMKubeResourceMonitorAnnotation)) + }) + }) + + Describe("with pre-existing namespace, correct label and annotation", func() { + var ( + out *bytes.Buffer + fakeClientSet kubernetes.Interface + err error + ) + + BeforeEach(func() { + out = new(bytes.Buffer) + fakeClientSet = fake.NewSimpleClientset() + + nsSpec := createNamespaceSpec(testNamespace, testMeshName, true) + _, err = fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), nsSpec, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + namespaceRemoveCmd := &namespaceRemoveCmd{ + out: out, + meshName: testMeshName, + namespace: testNamespace, + clientSet: fakeClientSet, + } + + err = namespaceRemoveCmd.run() + }) + + It("should not error", func() { + Expect(err).NotTo(HaveOccurred()) + }) + + It("should give a message confirming the successful install", func() { + Expect(out.String()).To(Equal(fmt.Sprintf("Namespace [%s] successfully removed from mesh [%s]\n", testNamespace, testMeshName))) + }) + + It("should correctly remove the label on the namespace", func() { + ns, err := fakeClientSet.CoreV1().Namespaces().Get(context.TODO(), testNamespace, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(ns.Labels).ShouldNot(HaveKey(constants.OSMKubeResourceMonitorAnnotation)) + }) + + It("should correctly remove the inject annotation on the namespace", func() { + ns, err := fakeClientSet.CoreV1().Namespaces().Get(context.TODO(), testNamespace, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(ns.Annotations).ShouldNot(HaveKey(constants.SidecarInjectionAnnotation)) + }) }) Describe("with pre-existing namespace and incorrect label", func() { @@ -198,7 +316,7 @@ var _ = Describe("Running the namespace remove command", func() { out = new(bytes.Buffer) fakeClientSet = fake.NewSimpleClientset() - nsSpec := createNamespaceSpec(testNamespace, testMeshName) + nsSpec := createNamespaceSpec(testNamespace, testMeshName, false) fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), nsSpec, metav1.CreateOptions{}) namespaceRemoveCmd := &namespaceRemoveCmd{ @@ -229,7 +347,7 @@ var _ = Describe("Running the namespace remove command", func() { out = new(bytes.Buffer) fakeClientSet = fake.NewSimpleClientset() - nsSpec := createNamespaceSpec(testNamespace, "") + nsSpec := createNamespaceSpec(testNamespace, "", false) fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), nsSpec, metav1.CreateOptions{}) namespaceRemoveCmd := &namespaceRemoveCmd{ @@ -292,7 +410,7 @@ var _ = Describe("Running the namespace list command", func() { // helper function that adds a name space to the clientset addNamespace := func(name, mesh string) { - ns := createNamespaceSpec(name, mesh) + ns := createNamespaceSpec(name, mesh, false) fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) } @@ -394,15 +512,21 @@ var _ = Describe("Running the namespace list command", func() { }) }) -func createNamespaceSpec(namespace, meshName string) *v1.Namespace { +func createNamespaceSpec(namespace, meshName string, enableSideCarInjection bool) *v1.Namespace { labelMap := make(map[string]string) if meshName != "" { labelMap[constants.OSMKubeResourceMonitorAnnotation] = meshName } - return &v1.Namespace{ + ns := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: namespace, Labels: labelMap, }, } + + if enableSideCarInjection { + ns.Annotations = map[string]string{constants.SidecarInjectionAnnotation: "enabled"} + } + + return ns } diff --git a/demo/join-namespaces.sh b/demo/join-namespaces.sh index da3733288c..f3117843b7 100755 --- a/demo/join-namespaces.sh +++ b/demo/join-namespaces.sh @@ -13,10 +13,10 @@ set -aueo pipefail source .env -./bin/osm namespace add "${BOOKBUYER_NAMESPACE:-bookbuyer}" --mesh-name "${MESH_NAME:-osm}" -./bin/osm namespace add "${BOOKSTORE_NAMESPACE:-bookstore}" --mesh-name "${MESH_NAME:-osm}" -./bin/osm namespace add "${BOOKTHIEF_NAMESPACE:-bookthief}" --mesh-name "${MESH_NAME:-osm}" -./bin/osm namespace add "${BOOKWAREHOUSE_NAMESPACE:-bookwarehouse}" --mesh-name "${MESH_NAME:-osm}" +./bin/osm namespace add "${BOOKBUYER_NAMESPACE:-bookbuyer}" --mesh-name "${MESH_NAME:-osm}" --enable-sidecar-injection +./bin/osm namespace add "${BOOKSTORE_NAMESPACE:-bookstore}" --mesh-name "${MESH_NAME:-osm}" --enable-sidecar-injection +./bin/osm namespace add "${BOOKTHIEF_NAMESPACE:-bookthief}" --mesh-name "${MESH_NAME:-osm}" --enable-sidecar-injection +./bin/osm namespace add "${BOOKWAREHOUSE_NAMESPACE:-bookwarehouse}" --mesh-name "${MESH_NAME:-osm}" --enable-sidecar-injection kubectl apply -f - < openservicemesh.io/monitored-by= ``` - All newly created pods in the added namespace(s) will automatically have a proxy sidecar container injected. To prevent specific pods from participating in the mesh, they can easily be labeled to prevent the sidecar injection. See the [Sidecar Injection](patterns/sidecar_injection.md) document for more details. + By default, the `osm namespace add` command does not enable the namespace for automatic sidecar injection. To enable automatic sidecar injection as a part of enrolling a namespace into the mesh, use `osm namespace add --enable-sidecar-injection`. This does the equivalent of the following: + + ```console + $ kubectl label namespace openservicemesh.io/monitored-by= + $ kubectl annotate namespace openservicemesh.io/sidecar-injection=enabled + ``` + + Once a namespace has been onboarded, pods can be enrolled in the mesh by configuring automatic sidecar injection. See the [Sidecar Injection](patterns/sidecar_injection.md) document for more details. For an example on how to onboard and join namespaces to the OSM mesh, please see the following example: - [demo/join-namespaces.sh](/demo/join-namespaces.sh) diff --git a/docs/patterns/sidecar_injection.md b/docs/patterns/sidecar_injection.md index 2bf9082420..b48c96ca92 100644 --- a/docs/patterns/sidecar_injection.md +++ b/docs/patterns/sidecar_injection.md @@ -4,6 +4,57 @@ Services participating in the service mesh communicate via sidecar proxies insta ## Automatic Sidecar Injection Automatic sidecar injection is currently the only way to inject sidecars into the service mesh. Sidecars can be automatically injected into applicable Kubernetes pods using a mutating webhook admission controller provided by OSM. -Each OSM instance is given a unique ID on installation. This ID is used while labeling namespaces as a way to configure OSM to monitor the namespaces. When a namespace is labeled with `openservicemesh.io/monitored-by=`, pods deployed in the monitored namespaces are automatically injected with sidecars by the corresponding OSM instance. +Automatic sidecar injection can be configured per namespace as a part of enrolling a namespace into the mesh, or later using the Kubernetes API. Automatic sidecar injection can be enabled either on a per namespace or per pod basis by annotating the namespace or pod resource with the sidecar injection annotation. Individual pods and namespaces can be explicitly configured to either enable or disable automatic sidecar injection, giving users the flexibility to control sidecar injection on pods and namespaces. -Since sidecars are automatically injected to pods deployed in OSM monitored namespaces, pods that should not be a part of the service mesh but belong to monitored namespaces need to be explicitly annotated to disable automatic sidecar injection. Using the annotation `"openservicemesh.io/sidecar-injection": "disabled"` on the POD will inform OSM to not inject the sidecar on the POD. \ No newline at end of file +### Enabling Automatic Sidecar Injection + +Prerequisites: +- The namespace to which the pods belong must be a monitored namespace that is added to the mesh using the `osm namespace add` command. + +Automatic Sidecar injection can be enabled in the following ways: + +- While enrolling a namespace into the mesh using `osm` cli: `osm namespace add --enable-sidecar-injection` + +- Using `kubectl` to annotate individual namespaces and pods to enable sidecar injection: + + ```console + # Enable sidecar injection on a namespace + $ kubectl annotate namespace openservicemesh.io/sidecar-injection=enabled + ``` + + ```console + # Enable sidecar injection on a pod + $ kubectl annotate pod openservicemesh.io/sidecar-injection=enabled + ``` + +- Setting the sidecar injection annotation to `enabled` in the Kubernetes resource spec for a namespace or pod: + ```yaml + metadata: + name: test + annotations: + 'openservicemesh.io/sidecar-injection': 'enabled' + ``` + + Pods will be injected with a sidecar only if the following conditions are met: + 1. The namespace to which the pod belongs is a monitored namespace. + 2. The pod is explicitly enabled for the sidecar injection, OR the namespace to which the pod belongs is enabled for the sidecar injection and the pod is not explicitly disabled for sidecar injection. + +### Explicitly Disabling Automatic Sidecar Injection on Pods + +Individual pods can be explicitly disabled for sidecar injection. This is useful when a namespace is enabled for sidecar injection but specific pods should not be injected with sidecars. + +- Using `kubectl` to annotate individual pods to disable sidecar injection: + ```console + # Disable sidecar injection on a pod + $ kubectl annotate pod openservicemesh.io/sidecar-injection=disabled + ``` + +- Setting the sidecar injection annotation to `disabled` in the Kubernetes resource spec for the pod: + ```yaml + metadata: + name: test + annotations: + 'openservicemesh.io/sidecar-injection': 'disabled' + ``` + +Automatic sidecar injection is implicitly disabled for a namespace when it is removed from the mesh using the `osm namespace remove` command. \ No newline at end of file diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index c0231a602b..c1ba9eda4d 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -133,4 +133,7 @@ const ( // InitContainerName is the name of the init container InitContainerName = "osm-init" + + // SidecarInjectionAnnotation is the annotation used for sidecar injection + SidecarInjectionAnnotation = "openservicemesh.io/sidecar-injection" ) diff --git a/pkg/injector/types.go b/pkg/injector/types.go index d0d39251b2..cf951769ff 100644 --- a/pkg/injector/types.go +++ b/pkg/injector/types.go @@ -11,9 +11,6 @@ import ( ) const ( - // OSM Annotations - annotationInject = "openservicemesh.io/sidecar-injection" - envoyBootstrapConfigVolume = "envoy-bootstrap-config-volume" ) diff --git a/pkg/injector/webhook.go b/pkg/injector/webhook.go index e409b2a082..24eba5d21e 100644 --- a/pkg/injector/webhook.go +++ b/pkg/injector/webhook.go @@ -223,40 +223,67 @@ func (wh *webhook) isNamespaceAllowed(namespace string) bool { // mustInject determines whether the sidecar must be injected. // -// The sidecar injection is performed when: -// 1. The namespace is annotated for OSM monitoring, and -// 2. The POD is not annotated with sidecar-injection or is set to enabled/yes/true +// The sidecar injection is performed when the namespace is labeled for monitoring and either of the following is true: +// 1. The pod is explicitly annotated with enabled/yes/true for sidecar injection, or +// 2. The namespace is annotated for sidecar injection and the pod is not explicitly annotated with disabled/no/false // -// The sidecar injection is not performed when: -// 1. The namespace is not annotated for OSM monitoring, or -// 2. The POD is annotated with sidecar-injection set to disabled/no/false -// -// The function returns an error when: -// 1. The value of the POD level sidecar-injection annotation is invalid +// The function returns an error when it is unable to determine whether to perform sidecar injection. func (wh *webhook) mustInject(pod *corev1.Pod, namespace string) (bool, error) { // If the request belongs to a namespace we are not monitoring, skip it if !wh.isNamespaceAllowed(namespace) { - log.Info().Msgf("Request belongs to namespace=%s not in the list of monitored namespaces", namespace) + log.Info().Msgf("Request belongs to namespace=%s, not in the list of monitored namespaces", namespace) return false, nil } - // Check if the POD is annotated for injection - inject := strings.ToLower(pod.ObjectMeta.Annotations[annotationInject]) - log.Debug().Msgf("Sidecar injection annotation: '%s:%s'", annotationInject, inject) + // Check if the pod is annotated for injection + podInjectAnnotationExists, podInject, err := isAnnotatedForInjection(pod.Annotations) + if err != nil { + log.Error().Err(err).Msg("Error determining if the pod is enabled for sidecar injection") + return false, err + } + + // Check if the namespace is annotated for injection + ns, err := wh.kubeClient.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{}) + if err != nil { + log.Error().Err(err).Msgf("Error retrieving namespace %s", namespace) + return false, err + } + nsInjectAnnotationExists, nsInject, err := isAnnotatedForInjection(ns.Annotations) + if err != nil { + log.Error().Err(err).Msg("Error determining if namespace %s is enabled for sidecar injection") + return false, err + } + + if podInjectAnnotationExists && podInject { + // Pod is explicitly annotated to enable sidecar injection + return true, nil + } else if nsInjectAnnotationExists && nsInject { + // Namespace is annotated to enable sidecar injection + if !podInjectAnnotationExists || podInject { + // If pod annotation doesn't exist or if an annotation exists to enable injection, enable it + return true, nil + } + } + + // Conditions to inject the sidecar are not met + return false, nil +} + +func isAnnotatedForInjection(annotations map[string]string) (exists bool, enabled bool, err error) { + inject := strings.ToLower(annotations[constants.SidecarInjectionAnnotation]) + log.Trace().Msgf("Sidecar injection annotation: '%s:%s'", constants.SidecarInjectionAnnotation, inject) if inject != "" { + exists = true switch inject { case "enabled", "yes", "true": - return true, nil + enabled = true case "disabled", "no", "false": - return false, nil + enabled = false default: - return false, errors.Errorf("Invalid annotion value specified for annotation %q: %s", annotationInject, inject) + err = errors.Errorf("Invalid annotion value specified for annotation %q: %s", constants.SidecarInjectionAnnotation, inject) } } - - // If we reached here, it means the namespace was annotated for OSM to monitor - // and no POD level sidecar injection overrides are present. - return true, nil + return } func toAdmissionError(err error) *v1beta1.AdmissionResponse { diff --git a/pkg/injector/webhook_test.go b/pkg/injector/webhook_test.go index 2ce1a26a27..2392467e9f 100644 --- a/pkg/injector/webhook_test.go +++ b/pkg/injector/webhook_test.go @@ -4,13 +4,17 @@ import ( "context" "time" + "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" "github.com/openservicemesh/osm/pkg/certificate" + "github.com/openservicemesh/osm/pkg/constants" + "github.com/openservicemesh/osm/pkg/namespace" ) var _ = Describe("Test MutatingWebhookConfiguration patch", func() { @@ -93,3 +97,290 @@ func (mc mockCertificate) GetCertificateChain() []byte { return []byte func (mc mockCertificate) GetPrivateKey() []byte { return []byte("key") } func (mc mockCertificate) GetIssuingCA() []byte { return []byte("ca") } func (mc mockCertificate) GetExpiration() time.Time { return time.Now() } + +var _ = Describe("Testing isAnnotatedForInjection", func() { + Context("when the inject annotation is one of enabled/yes/true", func() { + It("should return true to enable sidecar injection", func() { + annotation := map[string]string{constants.SidecarInjectionAnnotation: "enabled"} + exists, enabled, err := isAnnotatedForInjection(annotation) + Expect(exists).To(BeTrue()) + Expect(enabled).To(BeTrue()) + Expect(err).To(BeNil()) + }) + + It("should return true to enable sidecar injection", func() { + annotation := map[string]string{constants.SidecarInjectionAnnotation: "yes"} + exists, enabled, err := isAnnotatedForInjection(annotation) + Expect(exists).To(BeTrue()) + Expect(enabled).To(BeTrue()) + Expect(err).To(BeNil()) + }) + + It("should return true to enable sidecar injection", func() { + annotation := map[string]string{constants.SidecarInjectionAnnotation: "true"} + exists, enabled, err := isAnnotatedForInjection(annotation) + Expect(exists).To(BeTrue()) + Expect(enabled).To(BeTrue()) + Expect(err).To(BeNil()) + }) + }) + + Context("when the inject annotation is one of disabled/no/false", func() { + It("should return false to disable sidecar injection", func() { + annotation := map[string]string{constants.SidecarInjectionAnnotation: "disabled"} + exists, enabled, err := isAnnotatedForInjection(annotation) + Expect(exists).To(BeTrue()) + Expect(enabled).To(BeFalse()) + Expect(err).To(BeNil()) + }) + + It("should return false to disable sidecar injection", func() { + annotation := map[string]string{constants.SidecarInjectionAnnotation: "no"} + exists, enabled, err := isAnnotatedForInjection(annotation) + Expect(exists).To(BeTrue()) + Expect(enabled).To(BeFalse()) + Expect(err).To(BeNil()) + }) + + It("should return false to disable sidecar injection", func() { + annotation := map[string]string{constants.SidecarInjectionAnnotation: "false"} + exists, enabled, err := isAnnotatedForInjection(annotation) + Expect(exists).To(BeTrue()) + Expect(enabled).To(BeFalse()) + Expect(err).To(BeNil()) + }) + }) + + Context("when the inject annotation does not exist", func() { + It("should return false to indicate the annotation does not exist", func() { + annotation := map[string]string{} + exists, enabled, err := isAnnotatedForInjection(annotation) + Expect(exists).To(BeFalse()) + Expect(enabled).To(BeFalse()) + Expect(err).To(BeNil()) + }) + }) + + Context("when an invalid inject annotation is specified", func() { + It("should return an error", func() { + annotation := map[string]string{constants.SidecarInjectionAnnotation: "invalid-value"} + _, _, err := isAnnotatedForInjection(annotation) + Expect(err).To(HaveOccurred()) + }) + }) +}) + +var _ = Describe("Testing mustInject", func() { + var ( + mockCtrl *gomock.Controller + mockNsController *namespace.MockController + fakeClientSet *fake.Clientset + wh *webhook + ) + + mockCtrl = gomock.NewController(GinkgoT()) + mockNsController = namespace.NewMockController(mockCtrl) + fakeClientSet = fake.NewSimpleClientset() + namespace := "test" + + BeforeEach(func() { + fakeClientSet = fake.NewSimpleClientset() + wh = &webhook{ + kubeClient: fakeClientSet, + namespaceController: mockNsController, + } + }) + AfterEach(func() { + err := fakeClientSet.CoreV1().Namespaces().Delete(context.TODO(), namespace, metav1.DeleteOptions{}) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return true when the pod is enabled for sidecar injection", func() { + testNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + _, err := fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), testNamespace, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + podWithInjectAnnotationEnabled := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-with-injection-enabled", + Annotations: map[string]string{ + constants.SidecarInjectionAnnotation: "enabled", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "test-SA", + }, + } + _, err = fakeClientSet.CoreV1().Pods(namespace).Create(context.TODO(), podWithInjectAnnotationEnabled, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + mockNsController.EXPECT().IsMonitoredNamespace(namespace).Return(true).Times(1) + + inject, err := wh.mustInject(podWithInjectAnnotationEnabled, namespace) + + Expect(err).ToNot(HaveOccurred()) + Expect(inject).To(BeTrue()) + }) + + It("should return false when the pod is disabled for sidecar injection", func() { + testNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + _, err := fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), testNamespace, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + podWithInjectAnnotationEnabled := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-with-injection-disabled", + Annotations: map[string]string{ + constants.SidecarInjectionAnnotation: "disabled", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "test-SA", + }, + } + _, err = fakeClientSet.CoreV1().Pods(namespace).Create(context.TODO(), podWithInjectAnnotationEnabled, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + mockNsController.EXPECT().IsMonitoredNamespace(namespace).Return(true).Times(1) + + inject, err := wh.mustInject(podWithInjectAnnotationEnabled, namespace) + + Expect(err).ToNot(HaveOccurred()) + Expect(inject).To(BeFalse()) + }) + + It("should return true when the namespace is enabled for injection and the pod is not explicitly disabled for injection", func() { + testNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + Annotations: map[string]string{ + constants.SidecarInjectionAnnotation: "enabled", + }, + }, + } + _, err := fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), testNamespace, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + podWithInjectAnnotationEnabled := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-with-no-injection-annotation", + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "test-SA", + }, + } + _, err = fakeClientSet.CoreV1().Pods(namespace).Create(context.TODO(), podWithInjectAnnotationEnabled, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + mockNsController.EXPECT().IsMonitoredNamespace(namespace).Return(true).Times(1) + + inject, err := wh.mustInject(podWithInjectAnnotationEnabled, namespace) + + Expect(err).ToNot(HaveOccurred()) + Expect(inject).To(BeTrue()) + }) + + It("should return false when the namespace is enabled for injection and the pod is explicitly disabled for injection", func() { + testNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + Annotations: map[string]string{ + constants.SidecarInjectionAnnotation: "enabled", + }, + }, + } + _, err := fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), testNamespace, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + podWithInjectAnnotationEnabled := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-with-injection-disabled", + Annotations: map[string]string{ + constants.SidecarInjectionAnnotation: "disabled", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "test-SA", + }, + } + _, err = fakeClientSet.CoreV1().Pods(namespace).Create(context.TODO(), podWithInjectAnnotationEnabled, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + mockNsController.EXPECT().IsMonitoredNamespace(namespace).Return(true).Times(1) + + inject, err := wh.mustInject(podWithInjectAnnotationEnabled, namespace) + + Expect(err).ToNot(HaveOccurred()) + Expect(inject).To(BeFalse()) + }) + + It("should return false when the pod's namespace is not being monitored", func() { + testNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + _, err := fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), testNamespace, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + podWithInjectAnnotationEnabled := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-with-injection-enabled", + Annotations: map[string]string{ + constants.SidecarInjectionAnnotation: "enabled", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "test-SA", + }, + } + _, err = fakeClientSet.CoreV1().Pods(namespace).Create(context.TODO(), podWithInjectAnnotationEnabled, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + mockNsController.EXPECT().IsMonitoredNamespace(namespace).Return(false).Times(1) + + inject, err := wh.mustInject(podWithInjectAnnotationEnabled, namespace) + + Expect(err).ToNot(HaveOccurred()) + Expect(inject).To(BeFalse()) + }) + + It("should return an error when an invalid annotation is specified", func() { + testNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + _, err := fakeClientSet.CoreV1().Namespaces().Create(context.TODO(), testNamespace, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + podWithInjectAnnotationEnabled := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-with-injection-enabled", + Annotations: map[string]string{ + constants.SidecarInjectionAnnotation: "invalid-value", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "test-SA", + }, + } + _, err = fakeClientSet.CoreV1().Pods(namespace).Create(context.TODO(), podWithInjectAnnotationEnabled, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + mockNsController.EXPECT().IsMonitoredNamespace(namespace).Return(true).Times(1) + + inject, err := wh.mustInject(podWithInjectAnnotationEnabled, namespace) + + Expect(err).To(HaveOccurred()) + Expect(inject).To(BeFalse()) + }) +})