From 6794a24f0fb16c2d9cc8c2083c8f9b2108c07cef Mon Sep 17 00:00:00 2001 From: Michael Jerger Date: Tue, 20 Feb 2024 15:23:59 +0100 Subject: [PATCH] add namespace to ingress --- doc/Architecture.md | 17 ++ src/main/cljc/dda/c4k_common/ingress.cljc | 149 +++++++----------- .../c4k_common/ingress/ingress_internal.cljc | 95 +++++++++++ src/main/resources/namespace/namespace.yaml | 4 + .../ingress/ingress_internal_test.cljc | 133 ++++++++++++++++ .../cljc/dda/c4k_common/ingress_test.cljc | 92 ++--------- 6 files changed, 317 insertions(+), 173 deletions(-) create mode 100644 src/main/cljc/dda/c4k_common/ingress/ingress_internal.cljc create mode 100644 src/main/resources/namespace/namespace.yaml create mode 100644 src/test/cljc/dda/c4k_common/ingress/ingress_internal_test.cljc diff --git a/doc/Architecture.md b/doc/Architecture.md index cb8d505..45a8e08 100644 --- a/doc/Architecture.md +++ b/doc/Architecture.md @@ -49,3 +49,20 @@ C4Context Rel(app-backup, app-db-storage, "*dbc") ``` + +# Layout of a component on example of namespace + + +```mermaid +classDiagram + + class namespace { + config? // the external representation + default-config // static defaults + generate(config, auth) seq + } + class namespace-internal { + config? // the internal representation + generate-namespace(config) map + } +``` \ No newline at end of file diff --git a/src/main/cljc/dda/c4k_common/ingress.cljc b/src/main/cljc/dda/c4k_common/ingress.cljc index a0cc521..b963038 100644 --- a/src/main/cljc/dda/c4k_common/ingress.cljc +++ b/src/main/cljc/dda/c4k_common/ingress.cljc @@ -3,117 +3,76 @@ [clojure.spec.alpha :as s] #?(:clj [orchestra.core :refer [defn-spec]] :cljs [orchestra.core :refer-macros [defn-spec]]) - #?(:cljs [dda.c4k-common.macros :refer-macros [inline-resources]]) - [dda.c4k-common.yaml :as yaml] - [dda.c4k-common.common :as cm] - [dda.c4k-common.predicate :as pred])) + [dda.c4k-common.namespace :as ns] + [dda.c4k-common.ingress.ingress-internal :as int])) -(s/def ::issuer pred/letsencrypt-issuer?) -(s/def ::service-name string?) -(s/def ::app-name string?) -(s/def ::ingress-name string?) -(s/def ::cert-name string?) -(s/def ::service-port pos-int?) -(s/def ::fqdns (s/coll-of pred/fqdn-string?)) -(s/def ::average-rate pos-int?) -(s/def ::burst-rate pos-int?) +(s/def ::issuer ::int/issuer) +(s/def ::service-name ::int/service-name) +(s/def ::app-name ::int/app-name) +(s/def ::ingress-name ::int/ingress-name) +(s/def ::cert-name ::int/cert-name) +(s/def ::service-port ::int/service-port) +(s/def ::fqdns ::int/fqdns) +(s/def ::average-rate ::int/average-rate) +(s/def ::burst-rate ::int/burst-rate) (def simple-ingress? (s/keys :req-un [::fqdns ::service-name ::service-port] - :opt-un [::issuer ::average-rate])) + :opt-un [::issuer ::average-rate ::ns/namespace])) (def ingress? (s/keys :req-un [::fqdns ::app-name ::ingress-name ::service-name ::service-port] - :opt-un [::issuer ::cert-name ::rate-limit-name])) + :opt-un [::issuer ::cert-name ::rate-limit-name ::ns/namespace])) (def certificate? (s/keys :req-un [::fqdns ::app-name ::cert-name] - :opt-un [::issuer])) + :opt-un [::issuer ::ns/namespace])) (def rate-limit-config? (s/keys :req-un [::rate-limit-name ::average-rate ::burst-rate])) -(def simple-ingress-defaults {:issuer "staging" - :average-rate 10}) +(def default-config + (merge ns/default-config + {:issuer "staging" + :average-rate 10})) -#?(:cljs - (defmethod yaml/load-resource :ingress [resource-name] - (get (inline-resources "ingress") resource-name))) -(defn-spec generate-host-rule pred/map-or-seq? - [service-name ::service-name - service-port ::service-port - fqdn pred/fqdn-string?] - (-> - (yaml/load-as-edn "ingress/host-rule.yaml") - (cm/replace-all-matching-values-by-new-value "FQDN" fqdn) - (cm/replace-all-matching-values-by-new-value "SERVICE_PORT" service-port) - (cm/replace-all-matching-values-by-new-value "SERVICE_NAME" service-name))) - -(defn-spec generate-rate-limit-middleware pred/map-or-seq? - [config rate-limit-config?] - (let [{:keys [rate-limit-name average-rate burst-rate]} config] - (-> - (yaml/load-as-edn "ingress/middleware-ratelimit.yaml") - (assoc-in [:metadata :name] (str rate-limit-name "-ratelimit")) - (assoc-in [:spec :rateLimit :average] average-rate) - (assoc-in [:spec :rateLimit :burst] burst-rate)))) - -(defn-spec generate-certificate pred/map-or-seq? +(defn-spec generate-certificate map? [config certificate?] - (let [{:keys [cert-name issuer fqdns app-name] - :or {issuer "staging"}} config - letsencrypt-issuer (name issuer)] - (-> - (yaml/load-as-edn "ingress/certificate.yaml") - (assoc-in [:metadata :name] cert-name) - (assoc-in [:metadata :labels :app.kubernetes.part-of] app-name) - (assoc-in [:spec :secretName] cert-name) - (assoc-in [:spec :commonName] (first fqdns)) - (assoc-in [:spec :dnsNames] fqdns) - (assoc-in [:spec :issuerRef :name] letsencrypt-issuer)))) + (let [final-config (merge default-config + config)] + (int/generate-certificate final-config))) + -(defn-spec generate-ingress pred/map-or-seq? +(defn-spec generate-ingress map? [config ingress?] - (let [{:keys [ingress-name cert-name service-name service-port fqdns app-name rate-limit-name]} config] - (-> - (yaml/load-as-edn "ingress/ingress.yaml") - (assoc-in [:metadata :name] ingress-name) - (assoc-in [:metadata :labels :app.kubernetes.part-of] app-name) - (assoc-in [:metadata :annotations] - {:traefik.ingress.kubernetes.io/router.entrypoints - "web, websecure" - :traefik.ingress.kubernetes.io/router.middlewares - (if rate-limit-name - (str "default-redirect-https@kubernetescrd, " rate-limit-name "-ratelimit@kubernetescrd") - "default-redirect-https@kubernetescrd") - :metallb.universe.tf/address-pool "public"}) - (assoc-in [:spec :tls 0 :secretName] cert-name) - (assoc-in [:spec :tls 0 :hosts] fqdns) - (assoc-in [:spec :rules] - (mapv (partial generate-host-rule service-name service-port) fqdns))))) + (let [final-config (merge default-config + config)] + (int/generate-ingress final-config))) + + +(defn-spec generate-ingress-and-cert seq? + [config simple-ingress?] + (let [{:keys [service-name]} config + final-config (merge {:app-name service-name + :ingress-name service-name + :cert-name service-name} + default-config + config)] + [(int/generate-certificate final-config) + (int/generate-ingress final-config)])) -(defn-spec generate-ingress-and-cert any? - [simple-ingress-config simple-ingress?] - (let [{:keys [service-name]} simple-ingress-config - config (merge {:app-name service-name - :ingress-name service-name - :cert-name service-name} - simple-ingress-defaults - simple-ingress-config)] - [(generate-certificate config) - (generate-ingress config)])) -(defn-spec generate-simple-ingress any? - [simple-ingress-config simple-ingress?] - (let [{:keys [service-name]} simple-ingress-config - config (merge {:app-name service-name - :ingress-name service-name - :cert-name service-name - :rate-limit-name service-name} - simple-ingress-defaults - simple-ingress-config) - {:keys [average-rate]} config] - [(generate-certificate config) - (generate-rate-limit-middleware {:rate-limit-name service-name - :average-rate average-rate - :burst-rate average-rate}) - (generate-ingress config)])) \ No newline at end of file +(defn-spec generate-simple-ingress seq? + [config simple-ingress?] + (let [{:keys [service-name]} config + final-config (merge {:app-name service-name + :ingress-name service-name + :cert-name service-name + :rate-limit-name service-name} + default-config + config) + {:keys [average-rate]} final-config] + [(int/generate-certificate final-config) + (int/generate-rate-limit-middleware {:rate-limit-name service-name + :average-rate average-rate + :burst-rate average-rate}) + (int/generate-ingress final-config)])) \ No newline at end of file diff --git a/src/main/cljc/dda/c4k_common/ingress/ingress_internal.cljc b/src/main/cljc/dda/c4k_common/ingress/ingress_internal.cljc new file mode 100644 index 0000000..e725029 --- /dev/null +++ b/src/main/cljc/dda/c4k_common/ingress/ingress_internal.cljc @@ -0,0 +1,95 @@ +(ns dda.c4k-common.ingress.ingress-internal + (:require + [clojure.spec.alpha :as s] + #?(:clj [orchestra.core :refer [defn-spec]] + :cljs [orchestra.core :refer-macros [defn-spec]]) + #?(:cljs [dda.c4k-common.macros :refer-macros [inline-resources]]) + [dda.c4k-common.yaml :as yaml] + [dda.c4k-common.common :as cm] + [dda.c4k-common.namespace :as ns] + [dda.c4k-common.predicate :as pred])) + +(s/def ::issuer pred/letsencrypt-issuer?) +(s/def ::service-name string?) +(s/def ::app-name string?) +(s/def ::ingress-name string?) +(s/def ::cert-name string?) +(s/def ::service-port pos-int?) +(s/def ::fqdns (s/coll-of pred/fqdn-string?)) +(s/def ::average-rate pos-int?) +(s/def ::burst-rate pos-int?) + +(def ingress? (s/keys :req-un [::ingress-name ::app-name + ::ns/namespace + ::service-name ::service-port + ::issuer ::cert-name + ::fqdns] + :opt-un [::rate-limit-name])) + +(def certificate? (s/keys :req-un [::fqdns ::app-name ::cert-name ::issuer ::ns/namespace])) + +(def rate-limit-config? (s/keys :req-un [::rate-limit-name ::average-rate ::burst-rate])) + + +(defn-spec generate-host-rule map? + [service-name ::service-name + service-port ::service-port + fqdn pred/fqdn-string?] + (-> + (yaml/load-as-edn "ingress/host-rule.yaml") + (cm/replace-all-matching-values-by-new-value "FQDN" fqdn) + (cm/replace-all-matching-values-by-new-value "SERVICE_PORT" service-port) + (cm/replace-all-matching-values-by-new-value "SERVICE_NAME" service-name))) + + +(defn-spec generate-certificate map? + [config certificate?] + (let [{:keys [cert-name issuer fqdns app-name namespace]} config + letsencrypt-issuer (name issuer)] + (-> + (yaml/load-as-edn "ingress/certificate.yaml") + (assoc-in [:metadata :name] cert-name) + (assoc-in [:metadata :namespace] namespace) + (assoc-in [:metadata :labels :app.kubernetes.part-of] app-name) + (assoc-in [:spec :secretName] cert-name) + (assoc-in [:spec :commonName] (first fqdns)) + (assoc-in [:spec :dnsNames] fqdns) + (assoc-in [:spec :issuerRef :name] letsencrypt-issuer)))) + + +(defn-spec generate-rate-limit-middleware map? + [config rate-limit-config?] + (let [{:keys [rate-limit-name average-rate burst-rate]} config] + (-> + (yaml/load-as-edn "ingress/middleware-ratelimit.yaml") + (assoc-in [:metadata :name] (str rate-limit-name "-ratelimit")) + (assoc-in [:spec :rateLimit :average] average-rate) + (assoc-in [:spec :rateLimit :burst] burst-rate)))) + + +(defn-spec generate-ingress map? + [config ingress?] + (let [{:keys [ingress-name cert-name service-name service-port + fqdns app-name rate-limit-name namespace]} config] + (-> + (yaml/load-as-edn "ingress/ingress.yaml") + (assoc-in [:metadata :name] ingress-name) + (assoc-in [:metadata :namespace] namespace) + (assoc-in [:metadata :labels :app.kubernetes.part-of] app-name) + (assoc-in [:metadata :annotations] + {:traefik.ingress.kubernetes.io/router.entrypoints + "web, websecure" + :traefik.ingress.kubernetes.io/router.middlewares + (if rate-limit-name + (str "default-redirect-https@kubernetescrd, " rate-limit-name "-ratelimit@kubernetescrd") + "default-redirect-https@kubernetescrd") + :metallb.universe.tf/address-pool "public"}) + (assoc-in [:spec :tls 0 :secretName] cert-name) + (assoc-in [:spec :tls 0 :hosts] fqdns) + (assoc-in [:spec :rules] + (mapv (partial generate-host-rule service-name service-port) fqdns))))) + + +#?(:cljs + (defmethod yaml/load-resource :ingress [resource-name] + (get (inline-resources "ingress") resource-name))) diff --git a/src/main/resources/namespace/namespace.yaml b/src/main/resources/namespace/namespace.yaml new file mode 100644 index 0000000..cf6d1f2 --- /dev/null +++ b/src/main/resources/namespace/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: default \ No newline at end of file diff --git a/src/test/cljc/dda/c4k_common/ingress/ingress_internal_test.cljc b/src/test/cljc/dda/c4k_common/ingress/ingress_internal_test.cljc new file mode 100644 index 0000000..d7615ff --- /dev/null +++ b/src/test/cljc/dda/c4k_common/ingress/ingress_internal_test.cljc @@ -0,0 +1,133 @@ +(ns dda.c4k-common.ingress.ingress-internal-test + (:require + #?(:clj [clojure.test :refer [deftest is are testing run-tests]] + :cljs [cljs.test :refer-macros [deftest is are testing run-tests]]) + [clojure.spec.test.alpha :as st] + [dda.c4k-common.ingress.ingress-internal :as cut])) + +(st/instrument `cut/generate-host-rule) +(st/instrument `cut/generate-certificate) +(st/instrument `cut/generate-rate-limit-middleware) +(st/instrument `cut/generate-ingress) + + +(deftest should-generate-rule + (is (= {:host "test.com", + :http + {:paths + [{:pathType "Prefix", + :path "/", + :backend + {:service {:name "myservice", :port {:number 3000}}}}]}} + (cut/generate-host-rule "myservice" 3000 "test.com")))) + + +(deftest should-generate-certificate + (is (= {:apiVersion "cert-manager.io/v1", + :kind "Certificate", + :metadata {:name "test-io-cert", + :namespace "default", + :labels {:app.kubernetes.part-of "c4k-common-app"}}, + :spec + {:secretName "test-io-cert", + :commonName "test.de", + :duration "2160h", + :renewBefore "720h", + :dnsNames ["test.de" "test.org" "www.test.de" "www.test.org"], + :issuerRef {:name "prod", :kind "ClusterIssuer"}}} + (cut/generate-certificate {:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"] + :app-name "c4k-common-app" + :cert-name "test-io-cert" + :issuer "prod" + :namespace "default"}))) + (is (= {:apiVersion "cert-manager.io/v1", + :kind "Certificate", + :metadata {:name "test-io-cert", + :namespace "myapp", + :labels {:app.kubernetes.part-of "c4k-common-app"}}, + :spec + {:secretName "test-io-cert", + :commonName "test.de", + :duration "2160h", + :renewBefore "720h", + :dnsNames ["test.de" "test.org" "www.test.de" "www.test.org"], + :issuerRef {:name "prod", :kind "ClusterIssuer"}}} + (cut/generate-certificate {:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"] + :app-name "c4k-common-app" + :cert-name "test-io-cert" + :issuer "prod" + :namespace "myapp"})))) + + +(deftest should-generate-middleware-ratelimit + (is (= {:apiVersion "traefik.containo.us/v1alpha1", + :kind "Middleware", + :metadata {:name "normal-ratelimit"}, + :spec {:rateLimit {:average 10, :burst 5}}} + (cut/generate-rate-limit-middleware {:rate-limit-name "normal" + :average-rate 10, :burst-rate 5})))) + + +(deftest should-generate-ingress + (is (= {:apiVersion "networking.k8s.io/v1", + :kind "Ingress", + :metadata + {:namespace "myapp", + :name "test-io-https-ingress", + :labels {:app.kubernetes.part-of "c4k-common-app"}, + :annotations {:traefik.ingress.kubernetes.io/router.entrypoints + "web, websecure" + :traefik.ingress.kubernetes.io/router.middlewares + "default-redirect-https@kubernetescrd" + :metallb.universe.tf/address-pool "public"}}} + (dissoc (cut/generate-ingress + {:ingress-name "test-io-https-ingress" + :app-name "c4k-common-app" + :namespace "myapp" + :service-name "test-io-service" :service-port 80 + :issuer "prod" :cert-name "noname" + :fqdns ["test.de" "www.test.de" "test-it.de" + "www.test-it.de"]}) :spec))) + (is (= {:name "test-io-https-ingress", + :namespace "default", + :labels {:app.kubernetes.part-of "c4k-common-app"}, + :annotations {:traefik.ingress.kubernetes.io/router.entrypoints + "web, websecure" + :traefik.ingress.kubernetes.io/router.middlewares + "default-redirect-https@kubernetescrd, normal-ratelimit@kubernetescrd", + :metallb.universe.tf/address-pool "public"}} + (:metadata (cut/generate-ingress + { + :ingress-name "test-io-https-ingress" + :app-name "c4k-common-app" + :namespace "default" + :service-name "test-io-service" :service-port 80 + :rate-limit-name "normal" + :issuer "prod" :cert-name "noname" + :fqdns ["test.de"]})))) + (is (= {:tls + [{:hosts + ["test.de" "www.test.de" "test-it.de" "www.test-it.de"], + :secretName "test-io-cert"}] + :rules + [{:host "test.de", + :http + {:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}} + {:host "www.test.de", + :http + {:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}} + {:host "test-it.de", + :http + {:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}} + {:host "www.test-it.de", + :http + {:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}]} + (:spec (cut/generate-ingress { + :ingress-name "test-io-https-ingress" + :app-name "c4k-common-app" + :namespace "default" + :service-name "test-io-service" :service-port 80 + :issuer "prod" :cert-name "test-io-cert" + :fqdns ["test.de" "www.test.de" + "test-it.de" + "www.test-it.de"]}))))) \ No newline at end of file diff --git a/src/test/cljc/dda/c4k_common/ingress_test.cljc b/src/test/cljc/dda/c4k_common/ingress_test.cljc index 126152c..fcc106a 100644 --- a/src/test/cljc/dda/c4k_common/ingress_test.cljc +++ b/src/test/cljc/dda/c4k_common/ingress_test.cljc @@ -5,20 +5,10 @@ [clojure.spec.test.alpha :as st] [dda.c4k-common.ingress :as cut])) -(st/instrument `cut/generate-host-rule) (st/instrument `cut/generate-ingress) (st/instrument `cut/generate-certificate) (st/instrument `cut/generate-ingress-and-cert) - -(deftest should-generate-rule - (is (= {:host "test.com", - :http - {:paths - [{:pathType "Prefix", - :path "/", - :backend - {:service {:name "myservice", :port {:number 3000}}}}]}} - (cut/generate-host-rule "myservice" 3000 "test.com")))) +(st/instrument `cut/generate-simple-ingress) (deftest should-generate-certificate (is (= {:apiVersion "cert-manager.io/v1", @@ -31,84 +21,30 @@ :commonName "test.de", :duration "2160h", :renewBefore "720h", - :dnsNames ["test.de" "test.org" "www.test.de" "www.test.org"], - :issuerRef {:name "prod", :kind "ClusterIssuer"}}} - (cut/generate-certificate {:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"] + :dnsNames ["test.de"], + :issuerRef {:name "staging", :kind "ClusterIssuer"}}} + (cut/generate-certificate {:fqdns ["test.de"] :app-name "c4k-common-app" - :cert-name "test-io-cert" - :issuer "prod"})))) + :cert-name "test-io-cert"})))) -(deftest should-generate-middleware-ratelimit - (is (= {:apiVersion "traefik.containo.us/v1alpha1", - :kind "Middleware", - :metadata {:name "normal-ratelimit"}, - :spec {:rateLimit {:average 10, :burst 5}}} - (cut/generate-rate-limit-middleware {:rate-limit-name "normal" - :average-rate 10, :burst-rate 5})))) (deftest should-generate-ingress - (is (= {:apiVersion "networking.k8s.io/v1", - :kind "Ingress", - :metadata - { - :namespace "default", - :name "test-io-https-ingress", - :labels {:app.kubernetes.part-of "c4k-common-app"}, - :annotations {:traefik.ingress.kubernetes.io/router.entrypoints - "web, websecure" - :traefik.ingress.kubernetes.io/router.middlewares - "default-redirect-https@kubernetescrd" - :metallb.universe.tf/address-pool "public"}}} - (dissoc (cut/generate-ingress - {:issuer "prod" - :service-name "test-io-service" - :app-name "c4k-common-app" - :service-port 80 - :ingress-name "test-io-https-ingress" - :fqdns ["test.de" "www.test.de" "test-it.de" - "www.test-it.de"]}) :spec))) - (is (= { - :name "test-io-https-ingress", + (is (= {:name "test-io-https-ingress", :namespace "default", :labels {:app.kubernetes.part-of "c4k-common-app"}, :annotations {:traefik.ingress.kubernetes.io/router.entrypoints "web, websecure" :traefik.ingress.kubernetes.io/router.middlewares - "default-redirect-https@kubernetescrd, normal-ratelimit@kubernetescrd", + "default-redirect-https@kubernetescrd", :metallb.universe.tf/address-pool "public"}} (:metadata (cut/generate-ingress - {:service-name "test-io-service" - :app-name "c4k-common-app" - :service-port 80 - :ingress-name "test-io-https-ingress" - :rate-limit-name "normal" - :fqdns ["test.de"]})))) - (is (= {:tls - [{:hosts - ["test.de" "www.test.de" "test-it.de" "www.test-it.de"], - :secretName "test-io-cert"}] - :rules - [{:host "test.de", - :http - {:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}} - {:host "www.test.de", - :http - {:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}} - {:host "test-it.de", - :http - {:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}} - {:host "www.test-it.de", - :http - {:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}]} - (:spec (cut/generate-ingress {:issuer "prod" - :app-name "c4k-common-app" - :service-name "test-io-service" - :service-port 80 - :ingress-name "test-io-https-ingress" - :cert-name "test-io-cert" - :fqdns ["test.de" "www.test.de" - "test-it.de" - "www.test-it.de"]}))))) + {:ingress-name "test-io-https-ingress" + :app-name "c4k-common-app" + :service-name "test-io-service" :service-port 80 + :cert-name "myCert" + :fqdns ["test.de"]}))))) + + (deftest should-generate-ingress-and-cert (is (= [{:apiVersion "cert-manager.io/v1", :kind "Certificate",