diff --git a/Dockerfile b/Dockerfile index c1c7e2fd..cfe818f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ COPY . ./ RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} GO111MODULE=on go build -mod vendor -a -o dist/onic ./main.go # For Open source -FROM oraclelinux:7-slim +FROM oraclelinux:8-slim LABEL author="OKE Foundations Team" diff --git a/deploy/manifests/oci-native-ingress-controller/templates/deployment.yaml b/deploy/manifests/oci-native-ingress-controller/templates/deployment.yaml index 86a1e712..2a89a9e8 100644 --- a/deploy/manifests/oci-native-ingress-controller/templates/deployment.yaml +++ b/deploy/manifests/oci-native-ingress-controller/templates/deployment.yaml @@ -43,14 +43,18 @@ spec: defaultMode: 420 secretName: oci-native-ingress-controller-tls securityContext: - {} + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault containers: - name: oci-native-ingress-controller securityContext: allowPrivilegeEscalation: false + capabilities: + drop: + - ALL readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 1000 image: "ghcr.io/oracle/oci-native-ingress-controller:v1.3.9" imagePullPolicy: Always args: diff --git a/deploy/manifests/oci-native-ingress-controller/templates/rbac.yaml b/deploy/manifests/oci-native-ingress-controller/templates/rbac.yaml index 6644d418..c45da6f1 100644 --- a/deploy/manifests/oci-native-ingress-controller/templates/rbac.yaml +++ b/deploy/manifests/oci-native-ingress-controller/templates/rbac.yaml @@ -41,6 +41,9 @@ rules: - apiGroups: [""] resources: [pods/status] verbs: [patch] +- apiGroups: [""] + resources: [serviceaccounts] + verbs: [list, watch] --- # Source: oci-native-ingress-controller/templates/rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 diff --git a/helm/oci-native-ingress-controller/values.yaml b/helm/oci-native-ingress-controller/values.yaml index cdcf0e59..94a32934 100644 --- a/helm/oci-native-ingress-controller/values.yaml +++ b/helm/oci-native-ingress-controller/values.yaml @@ -45,14 +45,18 @@ serviceAccount: podAnnotations: {} -podSecurityContext: {} - # fsGroup: 2000 +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault -securityContext: +securityContext: readOnlyRootFilesystem: true - runAsNonRoot: true allowPrivilegeEscalation: false - runAsUser: 1000 + capabilities: + drop: + - ALL rbac: create: true diff --git a/pkg/controllers/ingress/util.go b/pkg/controllers/ingress/util.go index d4764fe2..309b6cb5 100644 --- a/pkg/controllers/ingress/util.go +++ b/pkg/controllers/ingress/util.go @@ -274,6 +274,8 @@ func GetSSLConfigForBackendSet(namespace string, artifactType string, artifact s if *caBundle.Name != newCertificateName { klog.Infof("Ca bundle for backend set %s needs update. Old name %s, New name %s", *bs.Name, *caBundle.Name, newCertificateName) createCaBundle = true + } else { + caBundleId = caBundle.Id } } else { createCaBundle = true diff --git a/pkg/controllers/ingressclass/ingressclass.go b/pkg/controllers/ingressclass/ingressclass.go index 16110b87..8cd0d690 100644 --- a/pkg/controllers/ingressclass/ingressclass.go +++ b/pkg/controllers/ingressclass/ingressclass.go @@ -357,6 +357,13 @@ func (c *Controller) createLoadBalancer(ctx context.Context, ic *networkingv1.In return nil, err } + defaultTags := getImplicitDefaultTagsForNewLoadBalancer(lb.DefinedTags, definedTags) + klog.Infof("Back-filling default tags %+v in LB %s for IC %s", defaultTags, *lb.Id, ic.Name) + err = updateImplicitDefaultTagsAnnotation(wrapperClient.GetK8Client(), ic, defaultTags) + if err != nil { + return nil, fmt.Errorf("unable to update implicit-default-tags for IC %s: %w", ic.Name, err) + } + return lb, nil } @@ -392,21 +399,34 @@ func (c *Controller) checkForIngressClassParameterUpdates(ctx context.Context, i // check LoadBalancerName, Defined and Freeform tags displayName := util.GetIngressClassLoadBalancerName(ic, icp) - definedTags, err := util.GetIngressClassDefinedTags(ic) + implicitDefaultTags, err := util.GetIngressClassImplicitDefaultTags(ic) if err != nil { return err } + + definedTags, updatedImplicitDefaultTags, err := getUpdatedDefinedAndImplicitDefaultTags(lb.DefinedTags, ic) + if err != nil { + return err + } + freeformTags, err := util.GetIngressClassFreeformTags(ic) if err != nil { return err } - if *lb.DisplayName != displayName || !reflect.DeepEqual(lb.DefinedTags, definedTags) || !reflect.DeepEqual(lb.FreeformTags, freeformTags) { + if *lb.DisplayName != displayName || !isDefinedTagsEqual(lb.DefinedTags, definedTags) || !reflect.DeepEqual(lb.FreeformTags, freeformTags) { _, err = wrapperClient.GetLbClient().UpdateLoadBalancer(context.Background(), *lb.Id, displayName, definedTags, freeformTags) if err != nil { return err } + } + if !isDefinedTagsEqual(implicitDefaultTags, updatedImplicitDefaultTags) { + klog.Infof("Updating implicit default tags %+v in LB %s for IC %s", updatedImplicitDefaultTags, *lb.Id, ic.Name) + err = updateImplicitDefaultTagsAnnotation(wrapperClient.GetK8Client(), ic, updatedImplicitDefaultTags) + if err != nil { + return err + } } // refresh lb, etag information after last call diff --git a/pkg/controllers/ingressclass/util.go b/pkg/controllers/ingressclass/util.go new file mode 100644 index 00000000..375e4da3 --- /dev/null +++ b/pkg/controllers/ingressclass/util.go @@ -0,0 +1,175 @@ +/* + * + * * OCI Native Ingress Controller + * * + * * Copyright (c) 2024 Oracle America, Inc. and its affiliates. + * * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + * + */ + +package ingressclass + +import ( + "encoding/json" + "github.com/oracle/oci-native-ingress-controller/pkg/util" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + "reflect" + "strings" +) + +var ( + tagVariables = []string{ + "${iam.principal.name}", + "${iam.principal.type}", + "${oci.datetime}", + } +) + +func isDefinedTagsEqual(dt1, dt2 util.DefinedTagsType) bool { + return reflect.DeepEqual(getLowerCaseDefinedTags(dt1), getLowerCaseDefinedTags(dt2)) +} + +func getImplicitDefaultTagsForNewLoadBalancer(actualDefinedTags, suppliedDefinedTags util.DefinedTagsType) util.DefinedTagsType { + defaultTags := util.DefinedTagsType{} + lowerCaseSuppliedDefinedTags := getLowerCaseDefinedTags(suppliedDefinedTags) + + klog.Infof("Calculating implicit default tags where actualTags: %+v, suppliedTags: %+v", + actualDefinedTags, suppliedDefinedTags) + + for namespace, _ := range actualDefinedTags { + for key, value := range actualDefinedTags[namespace] { + if !containsDefinedTagIgnoreCase(lowerCaseSuppliedDefinedTags, namespace, key) { + insertDefinedTag(defaultTags, namespace, key, value) + } + } + } + + return defaultTags +} + +func getUpdatedDefinedAndImplicitDefaultTags(actualTags util.DefinedTagsType, + ic *networkingv1.IngressClass) (util.DefinedTagsType, util.DefinedTagsType, error) { + updatedDefinedTags := util.DefinedTagsType{} + updatedDefaultTags := util.DefinedTagsType{} + + definedTags, err := util.GetIngressClassDefinedTags(ic) + if err != nil { + return nil, nil, err + } + + defaultTags, err := util.GetIngressClassImplicitDefaultTags(ic) + if err != nil { + return nil, nil, err + } + + klog.Infof("Calculating defined/default tags where actualTags: %+v, suppliedDefinedTags: %+v, implicitDefaultTags: %+v", + actualTags, definedTags, defaultTags) + + // Preserve default tags if they are present on LB and not overriden in supplied tags + lcDefinedTags := getLowerCaseDefinedTags(definedTags) + lcDefaultTags := getLowerCaseDefinedTags(defaultTags) + for namespace, _ := range actualTags { + for key, value := range actualTags[namespace] { + if !containsDefinedTagIgnoreCase(lcDefinedTags, namespace, key) && + containsDefinedTagIgnoreCase(lcDefaultTags, namespace, key) { + insertDefinedTag(updatedDefinedTags, namespace, key, value) + insertDefinedTag(updatedDefaultTags, namespace, key, value) + } + } + } + + // Add supplied defined tags + // We use only lower-case (namespace, key) pairs to avoid case-related conflicts + // If the supplied tag value has a Tag Variable, and the tag is already present on LB we will not try to update it + lcActualTags := getLowerCaseDefinedTags(actualTags) + lcUpdatedDefinedTags := getLowerCaseDefinedTags(updatedDefinedTags) + for namespace, _ := range lcDefinedTags { + for key, value := range lcDefinedTags[namespace] { + if definedTagValueHasTagVariable(value) && containsDefinedTagIgnoreCase(lcActualTags, namespace, key) { + klog.Infof("Supplied value of Tag %s.%s has tag-variable(s) and is already present on LB, will not be updated", + namespace, key) + insertDefinedTag(lcUpdatedDefinedTags, namespace, key, lcActualTags[namespace][key]) + } else { + insertDefinedTag(lcUpdatedDefinedTags, namespace, key, value) + } + } + } + + klog.Infof("Calculated defined/default tags for IngressClass %s: definedTags: %+v, implicitDefaultTags: %+v", + ic.Name, lcUpdatedDefinedTags, updatedDefaultTags) + return lcUpdatedDefinedTags, updatedDefaultTags, nil +} + +func updateImplicitDefaultTagsAnnotation(client kubernetes.Interface, ic *networkingv1.IngressClass, + defaultTags util.DefinedTagsType) error { + defaultTagsBytes, err := json.Marshal(defaultTags) + if err != nil { + return err + } + + patchError, notComplete := util.PatchIngressClassWithAnnotation(client, ic, + util.IngressClassImplicitDefaultTagsAnnotation, string(defaultTagsBytes)) + if notComplete { + return patchError + } + + return nil +} + +func getLowerCaseDefinedTags(tags util.DefinedTagsType) util.DefinedTagsType { + lowerCaseTags := util.DefinedTagsType{} + + for k, _ := range tags { + lowerCaseTags[strings.ToLower(k)] = map[string]interface{}{} + for ik, iv := range tags[k] { + lowerCaseTags[strings.ToLower(k)][strings.ToLower(ik)] = iv + } + } + + return lowerCaseTags +} + +// Checks if (namespace, key) pair exists in a lower-cased definedTags map, ignore case of (namespace, key) +func containsDefinedTagIgnoreCase(lowerCaseTags util.DefinedTagsType, namespace string, key string) bool { + if lowerCaseTags == nil { + return false + } + + containsNamespace := false + containsKey := false + + _, containsNamespace = lowerCaseTags[strings.ToLower(namespace)] + if containsNamespace { + _, containsKey = lowerCaseTags[strings.ToLower(namespace)][strings.ToLower(key)] + } + + return containsNamespace && containsKey +} + +func insertDefinedTag(definedTags util.DefinedTagsType, namespace string, key string, value interface{}) { + if definedTags == nil { + return + } + + _, ok := definedTags[namespace] + if !ok { + definedTags[namespace] = map[string]interface{}{} + } + + definedTags[namespace][key] = value +} + +func definedTagValueHasTagVariable(value interface{}) bool { + stringValue, ok := value.(string) + if ok { + for _, tagVar := range tagVariables { + if strings.Contains(stringValue, tagVar) { + return true + } + } + } + + return false +} diff --git a/pkg/controllers/ingressclass/util_test.go b/pkg/controllers/ingressclass/util_test.go new file mode 100644 index 00000000..f3a34dcf --- /dev/null +++ b/pkg/controllers/ingressclass/util_test.go @@ -0,0 +1,101 @@ +/* + * + * * OCI Native Ingress Controller + * * + * * Copyright (c) 2024 Oracle America, Inc. and its affiliates. + * * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + * + */ + +package ingressclass + +import ( + . "github.com/onsi/gomega" + "github.com/oracle/oci-native-ingress-controller/pkg/util" + networkingv1 "k8s.io/api/networking/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" +) + +func TestIsDefinedTagsEqual(t *testing.T) { + RegisterTestingT(t) + + emptyTags := util.DefinedTagsType{} + dt1 := util.DefinedTagsType{"N1": {"k1": "v1"}, "n2": {"K2": "V2", "k3": "v3"}} + dt2 := util.DefinedTagsType{"n1": {"K1": "v1"}, "N2": {"k2": "V2", "K3": "v3"}} + dt3 := util.DefinedTagsType{"n1": {"k1": "v1"}, "n2": {"k2": "v2", "k3": "v3"}} + + Expect(isDefinedTagsEqual(nil, emptyTags)).Should(BeTrue()) + Expect(isDefinedTagsEqual(dt1, dt2)).Should(BeTrue()) + Expect(isDefinedTagsEqual(dt1, dt3)).Should(BeFalse()) + Expect(isDefinedTagsEqual(dt2, dt3)).Should(BeFalse()) +} + +func TestGetImplicitDefaultTagsForNewLoadBalancer(t *testing.T) { + RegisterTestingT(t) + + actualDefinedTags := util.DefinedTagsType{"n1": {"k1": "v1", "KI1": "vi1"}, "n2": {"k2": "V2", "K3": "v3"}, "n3": {"k4": "v4"}} + suppliedDefinedTags := util.DefinedTagsType{"N1": {"k1": "v1"}, "n2": {"K2": "V2", "k3": "v3"}} + expectedImplicitDefaultTags := util.DefinedTagsType{"n1": {"KI1": "vi1"}, "n3": {"k4": "v4"}} + + Expect(expectedImplicitDefaultTags).Should(Equal(getImplicitDefaultTagsForNewLoadBalancer(actualDefinedTags, suppliedDefinedTags))) +} + +func TestGetUpdatedDefinedAndImplicitDefaultTags(t *testing.T) { + RegisterTestingT(t) + + actualTags := util.DefinedTagsType{"n1": {"k1": "v1", "KI1": "vi1"}, "n2": {"K2": "V2", "k3": "v3"}, "n3": {"K4": "v5"}} + + ingressClass := &networkingv1.IngressClass{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + util.IngressClassDefinedTagsAnnotation: `{"n1": {"k1": "v1"}, "N2": {"k2": "v3", "K3": "V3"}}`, + util.IngressClassImplicitDefaultTagsAnnotation: `{"n1": {"KI1": "vi1"}, "n2": {"K2": "V2"}, "n3": {"k4": "V4"}}`, + }, + }, + } + + expectedDefinedTags := util.DefinedTagsType{"n1": {"k1": "v1", "ki1": "vi1"}, "n2": {"k2": "v3", "k3": "V3"}, "n3": {"k4": "v5"}} + expectedDefaultTags := util.DefinedTagsType{"n1": {"KI1": "vi1"}, "n3": {"K4": "v5"}} + + definedTags, defaultTags, err := getUpdatedDefinedAndImplicitDefaultTags(actualTags, ingressClass) + Expect(err).To(BeNil()) + Expect(expectedDefinedTags).Should(Equal(definedTags)) + Expect(expectedDefaultTags).Should(Equal(defaultTags)) +} + +func TestGetLowerCaseDefinedTags(t *testing.T) { + RegisterTestingT(t) + + emptyTags := util.DefinedTagsType{} + definedTags := util.DefinedTagsType{"N1": {"k1": "v1"}, "n2": {"K2": "V2", "k3": "v3"}} + expectedTags := util.DefinedTagsType{"n1": {"k1": "v1"}, "n2": {"k2": "V2", "k3": "v3"}} + + Expect(getLowerCaseDefinedTags(nil)).Should(Equal(emptyTags)) + Expect(getLowerCaseDefinedTags(definedTags)).Should(Equal(expectedTags)) +} + +func TestContainsDefinedTagIgnoreCase(t *testing.T) { + RegisterTestingT(t) + + definedTags := util.DefinedTagsType{"n1": {"k1": "v1"}, "n2": {"k2": "V2", "k3": "v3"}} + + Expect(containsDefinedTagIgnoreCase(nil, "namespace", "key")).Should(BeFalse()) + Expect(containsDefinedTagIgnoreCase(definedTags, "N1", "k1")).Should(BeTrue()) + Expect(containsDefinedTagIgnoreCase(definedTags, "n2", "K3")).Should(BeTrue()) + Expect(containsDefinedTagIgnoreCase(definedTags, "n4", "k1")).Should(BeFalse()) + Expect(containsDefinedTagIgnoreCase(definedTags, "n2", "k1")).Should(BeFalse()) +} + +func TestInsertDefinedTag(t *testing.T) { + RegisterTestingT(t) + + definedtags := util.DefinedTagsType{} + insertDefinedTag(definedtags, "n1", "k1", "v1") + insertDefinedTag(definedtags, "n1", "K2", "V2") + insertDefinedTag(definedtags, "N2", "k3", "v3") + insertDefinedTag(definedtags, "n1", "k4", "V4") + + expectedTags := util.DefinedTagsType{"n1": {"k1": "v1", "K2": "V2", "k4": "V4"}, "N2": {"k3": "v3"}} + Expect(expectedTags).Should(Equal(definedtags)) +} diff --git a/pkg/util/util.go b/pkg/util/util.go index d4088992..bb73bd31 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -70,6 +70,7 @@ const ( IngressClassDeleteProtectionEnabledAnnotation = "oci-native-ingress.oraclecloud.com/delete-protection-enabled" IngressClassDefinedTagsAnnotation = "oci-native-ingress.oraclecloud.com/defined-tags" IngressClassFreeformTagsAnnotation = "oci-native-ingress.oraclecloud.com/freeform-tags" + IngressClassImplicitDefaultTagsAnnotation = "oci-native-ingress.oraclecloud.com/implicit-default-tags" IngressHealthCheckProtocolAnnotation = "oci-native-ingress.oraclecloud.com/healthcheck-protocol" IngressHealthCheckPortAnnotation = "oci-native-ingress.oraclecloud.com/healthcheck-port" @@ -104,6 +105,8 @@ const ( WrapperClient = "wrapperClient" ) +type DefinedTagsType = map[string]map[string]interface{} + var ErrIngressClassNotReady = errors.New("ingress class not ready") func GetIngressClassCompartmentId(p *v1beta1.IngressClassParameters, defaultCompartment string) string { @@ -191,13 +194,13 @@ func GetIngressClassDeleteProtectionEnabled(ic *networkingv1.IngressClass) bool return result } -func GetIngressClassDefinedTags(ic *networkingv1.IngressClass) (map[string]map[string]interface{}, error) { +func GetIngressClassDefinedTags(ic *networkingv1.IngressClass) (DefinedTagsType, error) { annotation := IngressClassDefinedTagsAnnotation value, ok := ic.Annotations[annotation] // value of defined tags can only be strings for now, but we will allow anything that fits the type // specified by LoadBalancer.DefinedTags - definedTags := map[string]map[string]interface{}{} + definedTags := DefinedTagsType{} if !ok || strings.TrimSpace(value) == "" { return definedTags, nil @@ -211,6 +214,24 @@ func GetIngressClassDefinedTags(ic *networkingv1.IngressClass) (map[string]map[s return definedTags, nil } +func GetIngressClassImplicitDefaultTags(ic *networkingv1.IngressClass) (DefinedTagsType, error) { + annotation := IngressClassImplicitDefaultTagsAnnotation + value, ok := ic.Annotations[annotation] + + defaultTags := DefinedTagsType{} + + if !ok || strings.TrimSpace(value) == "" { + return defaultTags, nil + } + + err := json.Unmarshal([]byte(value), &defaultTags) + if err != nil { + return nil, fmt.Errorf("error parsing value %s for annotation %s: %w", value, annotation, err) + } + + return defaultTags, nil +} + func GetIngressClassFreeformTags(ic *networkingv1.IngressClass) (map[string]string, error) { annotation := IngressClassFreeformTagsAnnotation value, ok := ic.Annotations[annotation] @@ -552,10 +573,16 @@ func GetTimeDifferenceInSeconds(startTime, endTime int64) float64 { } func PatchIngressClassWithAnnotation(client kubernetes.Interface, ic *networkingv1.IngressClass, annotationName string, annotationValue string) (error, bool) { + patchMap := map[string]map[string]map[string]string{ + "metadata": {"annotations": {annotationName: annotationValue}}, + } + patchBytes, err := json.Marshal(patchMap) + if err != nil { + return err, true + } - patchBytes := []byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"}}}`, annotationName, annotationValue)) - - err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + klog.Infof("Will try patching IngressClass %s for annotation %s: %s", ic.Name, annotationName, annotationValue) + err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { _, err := client.NetworkingV1().IngressClasses().Patch(context.TODO(), ic.Name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}) return err })