diff --git a/src/main/cljc/dda/c4k_common/ingress.cljc b/src/main/cljc/dda/c4k_common/ingress.cljc index e900de7..a0cc521 100644 --- a/src/main/cljc/dda/c4k_common/ingress.cljc +++ b/src/main/cljc/dda/c4k_common/ingress.cljc @@ -1,11 +1,9 @@ (ns dda.c4k-common.ingress (:require [clojure.spec.alpha :as s] - #?(:cljs [shadow.resource :as rc]) #?(:clj [orchestra.core :refer [defn-spec]] :cljs [orchestra.core :refer-macros [defn-spec]]) - #?(:clj [clojure.edn :as edn] - :cljs [cljs.reader :as edn]) + #?(: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])) @@ -17,26 +15,28 @@ (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 simple-ingress? (s/keys :req-un [::fqdns ::service-name ::service-port] - :opt-un [::issuer])) + :opt-un [::issuer ::average-rate])) (def ingress? (s/keys :req-un [::fqdns ::app-name ::ingress-name ::service-name ::service-port] - :opt-un [::issuer ::cert-name])) + :opt-un [::issuer ::cert-name ::rate-limit-name])) (def certificate? (s/keys :req-un [::fqdns ::app-name ::cert-name] :opt-un [::issuer])) -; TODO: use this default consistently -(def ingress-defaults {:issuer "staging"}) +(def rate-limit-config? (s/keys :req-un [::rate-limit-name + ::average-rate + ::burst-rate])) + +(def simple-ingress-defaults {:issuer "staging" + :average-rate 10}) #?(:cljs (defmethod yaml/load-resource :ingress [resource-name] - (case resource-name - "ingress/host-rule.yaml" (rc/inline "ingress/host-rule.yaml") - "ingress/certificate.yaml" (rc/inline "ingress/certificate.yaml") - "ingress/ingress.yaml" (rc/inline "ingress/ingress.yaml") - (throw (js/Error. "Undefined Resource!"))))) + (get (inline-resources "ingress") resource-name))) (defn-spec generate-host-rule pred/map-or-seq? [service-name ::service-name @@ -48,16 +48,14 @@ (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-ingress pred/map-or-seq? - [config ingress?] - (let [{:keys [ingress-name cert-name service-name service-port fqdns app-name]} config] +(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/ingress.yaml") - (assoc-in [:metadata :name] ingress-name) - (assoc-in [:metadata :labels :app.kubernetes.part-of] app-name) - (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))))) + (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? [config certificate?] @@ -73,13 +71,49 @@ (assoc-in [:spec :dnsNames] fqdns) (assoc-in [:spec :issuerRef :name] letsencrypt-issuer)))) +(defn-spec generate-ingress pred/map-or-seq? + [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))))) + (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} - ingress-defaults + 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 diff --git a/src/main/resources/ingress/ingress.yaml b/src/main/resources/ingress/ingress.yaml index c958fca..da333b6 100644 --- a/src/main/resources/ingress/ingress.yaml +++ b/src/main/resources/ingress/ingress.yaml @@ -1,10 +1,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: c4k-common-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 diff --git a/src/main/resources/ingress/middleware-ratelimit.yaml b/src/main/resources/ingress/middleware-ratelimit.yaml new file mode 100644 index 0000000..57bdbfd --- /dev/null +++ b/src/main/resources/ingress/middleware-ratelimit.yaml @@ -0,0 +1,8 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: ratelimit +spec: + rateLimit: + average: AVG + burst: BRS \ No newline at end of file diff --git a/src/test/clj/dda/c4k_common/macros_test.clj b/src/test/clj/dda/c4k_common/macros_test.clj index f9f58d5..1f02711 100644 --- a/src/test/clj/dda/c4k_common/macros_test.clj +++ b/src/test/clj/dda/c4k_common/macros_test.clj @@ -4,7 +4,7 @@ [dda.c4k-common.macros :refer [inline-resources]])) (deftest should-count-inline-resources - (is (= 3 (count (inline-resources "ingress"))))) + (is (= 4 (count (inline-resources "ingress"))))) (deftest should-inline-resources (let [resource-path (fn [name] (str "dda/c4k_common/inline_resources_test/" name))] diff --git a/src/test/cljc/dda/c4k_common/ingress_test.cljc b/src/test/cljc/dda/c4k_common/ingress_test.cljc index 3d78d02..126152c 100644 --- a/src/test/cljc/dda/c4k_common/ingress_test.cljc +++ b/src/test/cljc/dda/c4k_common/ingress_test.cljc @@ -20,26 +20,69 @@ {: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"})))) + +(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 - {:name "test-io-https-ingress", + { :namespace "default", + :name "test-io-https-ingress", :labels {:app.kubernetes.part-of "c4k-common-app"}, - :annotations {:traefik.ingress.kubernetes.io/router.entrypoints + :annotations {:traefik.ingress.kubernetes.io/router.entrypoints "web, websecure" - :traefik.ingress.kubernetes.io/router.middlewares + :traefik.ingress.kubernetes.io/router.middlewares "default-redirect-https@kubernetescrd" - :metallb.universe.tf/address-pool "public"}}} + :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))) + {: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", + :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 + {: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"], @@ -66,25 +109,6 @@ :fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}))))) - -(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"})))) - (deftest should-generate-ingress-and-cert (is (= [{:apiVersion "cert-manager.io/v1", :kind "Certificate", @@ -121,4 +145,46 @@ :port {:number 80}}}}]}}]}}] (cut/generate-ingress-and-cert {:fqdns ["test.jit.si"] :service-name "web" - :service-port 80})))) \ No newline at end of file + :service-port 80})))) + +(deftest should-generate-simple-ingress + (is (= [{:apiVersion "cert-manager.io/v1", + :kind "Certificate", + :metadata + {:name "web", + :labels {:app.kubernetes.part-of "web"}, + :namespace "default"}, + :spec + {:secretName "web", + :commonName "test.jit.si", + :duration "2160h", + :renewBefore "720h", + :dnsNames ["test.jit.si"], + :issuerRef {:name "staging", :kind "ClusterIssuer"}}} + {:apiVersion "traefik.containo.us/v1alpha1", + :kind "Middleware", + :metadata {:name "web-ratelimit"}, + :spec {:rateLimit {:average 10, :burst 10}}} + {:apiVersion "networking.k8s.io/v1", + :kind "Ingress", + :metadata + {:name "web", + :namespace "default", + :labels {:app.kubernetes.part-of "web"}, + :annotations + {:traefik.ingress.kubernetes.io/router.entrypoints "web, websecure", + :traefik.ingress.kubernetes.io/router.middlewares + "default-redirect-https@kubernetescrd, web-ratelimit@kubernetescrd", + :metallb.universe.tf/address-pool "public"}}, + :spec + {:tls [{:hosts ["test.jit.si"], :secretName "web"}], + :rules + [{:host "test.jit.si", + :http {:paths [{:path "/", + :pathType "Prefix", + :backend + {:service {:name "web", + :port {:number 80}}}}]}}]}}] + (cut/generate-simple-ingress {:fqdns ["test.jit.si"] + :service-name "web" + :service-port 80})))) \ No newline at end of file diff --git a/src/test/cljs/dda/c4k_common/macros_test.cljs b/src/test/cljs/dda/c4k_common/macros_test.cljs index 1067a3c..cdae418 100644 --- a/src/test/cljs/dda/c4k_common/macros_test.cljs +++ b/src/test/cljs/dda/c4k_common/macros_test.cljs @@ -4,7 +4,7 @@ [dda.c4k-common.macros :refer-macros [inline-resources]])) (deftest should-count-inline-resources - (is (= 3 (count (inline-resources "ingress"))))) + (is (= 4 (count (inline-resources "ingress"))))) (deftest should-inline-resources (let [resource-path (fn [name] (str "dda/c4k_common/inline_resources_test/" name))]