diff --git a/project.clj b/project.clj index 056f52e..a987345 100644 --- a/project.clj +++ b/project.clj @@ -7,6 +7,7 @@ [org.clojure/data.codec "0.1.1"] [nano-id "0.9.3"] [cli-matic "0.3.8"] + [cheshire "5.9.0"] [io.forward/yaml "1.0.9"] [brosenan/lambdakube "0.7.0"]] :plugins [[lein-auto "0.1.3"]] diff --git a/src/kube_deploy/blocks.clj b/src/kube_deploy/blocks.clj new file mode 100644 index 0000000..4ab7301 --- /dev/null +++ b/src/kube_deploy/blocks.clj @@ -0,0 +1,136 @@ +(ns kube-deploy.blocks + (:require [clojure.java.io :as io] + [kube-deploy.helpers :as lkh] + + [lambdakube.core :as lk])) + +(def resources-mini + {:requests + {:cpu "100m" :memory "64Mi"} + :limits + {:cpu "200m" :memory "256Mi"}}) + +(def resources-small + {:requests + {:cpu "100m" :memory "100Mi"} + :limits + {:cpu "200m" :memory "400Mi"}}) + +(def resources-medium + {:requests + {:cpu "100m" :memory "500Mi"} + :limits + {:cpu "200m" :memory "1Gi"}}) + + +(defn postgres + ([kube-injector] + (postgres kube-injector {})) + ([kube-injector settings] + (let [app-name (get settings :app-name "postgres") + app-pod-name (str app-name "-pod") + app-rule-name (str app-name "-rule") + app-container-name (str app-name "-container") + app-dependencies (get settings :dependencies []) + app-containers 1 + app-labels (get settings :labels {:app "postgres"}) + app-image (get settings :image "postgres") + app-resources (get settings :resources resources-small)] + (-> kube-injector + (lk/rule :postgres-master-rule [] + (fn [] + (-> (lk/pod + app-pod-name + app-labels + ) + (lk/add-container app-container-name app-image + (-> {} + (lkh/add-to-spec :resources app-resources) + (lkh/add-readiness-probe-cmd ["pg_isready"]) + (lkh/add-liveness-probe-cmd ["pg_isready"]) + (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]))) + (lk/stateful-set 1) + + (lk/add-volume-claim-template :postgres-volume + {:accessModes ["ReadWriteOnce"] + :resources {:requests {:storage "1Gi"}}} + {:bar "/var/lib/postgresql/data"}) + (lk/expose-cluster-ip app-pod-name + (lk/port :postgres :postgres 5432 5432))))))))) + + +(defn django-gunicorn + ([kube-injector] + (django-gunicorn kube-injector {})) + ([kube-injector settings] + (let [app-name (get settings :app-name "django-gunicorn") + app-pod-name (str app-name "-pod") + app-rule-name (str app-name "-rule") + app-gunicorn-container-name (str app-name "-container") + app-nginx-container-name (str app-name "-nginx-container") + app-dependencies (get settings :dependencies []) + app-containers 1 + app-labels (get settings :labels {:app "django-gunicorn"}) + app-image (get settings :image "python:3") + app-resources (get settings :resources resources-small)] + + (-> kube-injector + (lk/rule (keyword (str app-name "-rule")) app-dependencies + (fn [] + (-> (lk/pod + app-pod-name + app-labels + {:imagePullSecrets [{:name "docker-registry"}]}) + ; Add containers to the pod, one for frontend assets and one for backend + (lk/add-container + app-gunicorn-container-name + app-image + {:resources app-resources + :imagePullPolicy "Always" + :command (get settings :command ["/usr/bin/gunicorn" + "wsgi" + "--workers" "2" + "-k" "gevent" + "--worker-connections" "100" + "--max-requests" "300" + "--keep-alive" "1000" + "-b" "0.0.0.0:5000" + "--chdir=/app"]) + :ports [{:containerPort 5000 :name "gunicorn-port"}] + :envFrom [{:secretRef {:name "environment"}}] + #_(lkh/add-readiness-probe-cmd ["wget" "127.0.0.1:5000"]) + #_(lkh/add-liveness-probe-cmd ["wget" "127.0.0.1:5000"])}) + + (lk/add-volume "static-files" {:emptyDir {}} + {app-pod-name "/app/staticfiles"}) + + ; order does matter here, deployment after initcontainers + ;(lku/wait-for-service-port postgres :postgres) + ;(lku/wait-for-service-port redis :redis) + + ; wrap in a deployment + (lk/deployment app-containers) + (lk/expose-cluster-ip app-gunicorn-container-name + (lk/port app-pod-name :gunicorn-port 5000 5000)) + (lk/expose-cluster-ip app-nginx-container-name + (lk/port app-pod-name :nginx-port 80 80))))) + + ; inject the ingress required to serve to gunicorn app + (lk/rule (keyword (str app-name "-ingress-rule")) [] + (fn [] + (let [ingress-name (str app-name "-ingress")] + (-> (lkh/ingress ingress-name {:app :nginx-ingress :env :production :tier :ingress}) + (lkh/add-spec :tls [{:hosts [(:ingress-host settings)] + :secretName "letsencrypt-production"}]) + (lkh/add-spec :rules [{:host (:ingress-host settings) + :http {:paths + [{:path "/" + :backend + {:serviceName "gunicorn-port" + :servicePort 5000}} + {:path "/static/" + :backend + {:serviceName "nginx-port" + :servicePort 80}}]}}]) + (lk/add-annotation :certmanager.k8s.io/cluster-issuer "letsencrypt-production") + (lk/add-annotation :kubernetes.io/ingress.class "nginx"))))))))) diff --git a/src/kube_deploy/core.clj b/src/kube_deploy/core.clj index ce0b79d..28e5f05 100644 --- a/src/kube_deploy/core.clj +++ b/src/kube_deploy/core.clj @@ -3,17 +3,18 @@ [clojure.java.shell :as sh] [cli-matic.core :refer [run-cmd]] [yaml.core :as yaml] + [cheshire.core :as json] [kube-deploy.from-yaml :as lky] [kube-deploy.data :as data] [kube-deploy.helpers :as lkh] [lambdakube.core :as lk] + [kube-deploy.blocks :as block] [lambdakube.util :as lku])) (def conf (atom {})) (def services (atom {})) (reset! services (clojure.edn/read-string (slurp "config.edn"))) - -(def config {:num-be-slaves 2 :num-fe-replicas 2}) +(def deployment-config {:num-be-slaves 2 :num-fe-replicas 2}) (def resources-mini {:requests @@ -34,6 +35,24 @@ {:cpu "200m" :memory "1Gi"}}) ; will not be needed soon + +(defn kube-get-namespaces [kube-config] + (into #{} (->> (lkh/kube-namespaces kube-config) + (map :metadata) + (map :name)))) + +(defn kube-does-namespace-exists [namespace] + (some namespace + (into #{} (->> (lkh/kube-namespaces "/home/oly/.kube/dke.yml") + (map :metadata) + (map :name))))) + + + + +; will not be needed soon + + (defn kube-apply ([content file] (kube-apply content file nil nil)) @@ -84,6 +103,23 @@ (lk/expose-cluster-ip :redis-slave (lk/port :redis :redis 6379 6379))))))) +(defn mosquitto-mqtt [kube-injector] + (-> kube-injector + (lk/rule :mosquitto-master-rule [] + (fn [] + (-> (lk/pod + :mosquitto-master + {:app :footprint :role :master :env :production :tier :database}) + (lk/add-container :mosquitto "eclipse-mosquitto" + (-> {} + (lkh/add-to-spec :resources resources-mini) + (lkh/add-readiness-probe-cmd ["pg_isready"]) + (lkh/add-liveness-probe-cmd ["pg_isready"]) + (lkh/add-to-spec :envFrom [{:secretRef {:name "environment"}}]))) + (lk/deployment 1) + (lk/expose-cluster-ip :mosquitto-master + (lk/port :mosquitto :mosquitto 1883 1883))))))) + (defn database [kube-injector] (-> kube-injector (lk/rule :postgres-master-rule [] @@ -320,17 +356,18 @@ (-> (lkh/secrets (keyword name) data)))))) (defn make-registry-secrets [kube-injector name data] - (-> kube-injector - (lk/rule - :secrets-registry [] - (fn [] - (lkh/secrets-registry name {} (lkh/encode-secret-map data)))))) + (if (nil? (:.dockerconfigjson data)) + kube-injector + (-> kube-injector + (lk/rule + :secrets-registry [] + (fn [] + (lkh/secrets-registry name {} (lkh/encode-secret-map data))))))) (defn make-namespace [kube-injector name] - (-> kube-injector - (lk/rule :namespace [] - (fn [] - (lkh/kube-namespace (keyword name)))))) + (lk/rule kube-injector :namespace [] + (fn [] + (lkh/kube-namespace (keyword name))))) (defn make-environment-secrets [kube-injector name data] (-> kube-injector @@ -403,7 +440,7 @@ (add-config "scheduler-tasks" (get-in @conf [:config])) footprint lk/standard-descs - (lk/get-deployable config) + (lk/get-deployable deployment-config) (lkh/to-yaml-store (str namespace "-store.yaml")) (kube-apply (io/file (str namespace "-deploy.yaml")) "footprint" kube-config))) @@ -414,7 +451,7 @@ (-> (lk/injector) (make-namespace namespace) ;lk/standard-descs - (lk/get-deployable config) + (lk/get-deployable deployment-config) (lkh/to-yaml-store (str namespace "-store.yaml")) (kube-apply (io/file (str namespace "-deploy.yaml")) nil kube-config)) @@ -427,18 +464,65 @@ redis hackspace lk/standard-descs - (lk/get-deployable config) + (lk/get-deployable deployment-config) (lkh/to-yaml-store (str namespace "-store.yaml")) (kube-apply (io/file (str namespace "-deploy.yaml")) namespace kube-config))) -(defn deploy [{:keys [service]}] - (let [namespace (get-in data/services [(keyword service) :namespace]) - config (get-in data/services [(keyword service) :config])] +;(map lk/injector 'database) + +(defn create-namespace-if-not-exists [{:keys [config namespace blocks]}] + (when-not (kube-does-namespace-exists (keyword namespace)) + (println (str "Namespace " namespace " not found creating")) + (-> (lk/injector) + (make-namespace namespace) + (lk/get-deployable deployment-config) + (lkh/to-yaml-store (str namespace "-namespace.yaml")) + (kube-apply (io/file (str namespace "-deploy.yaml")) nil config)))) + + +(defn apply-blocks [kube-injector func settings] + (if (= 0 (count func)) + kube-injector + (recur + (case (first func) + "database" (database kube-injector) + "postgres" (block/postgres kube-injector) + "django-gunicorn" (block/django-gunicorn kube-injector (get-in settings [(keyword (first func))])) + kube-injector) + (rest func) + settings + ))) + + +(defn build [{:keys [config namespace blocks] :as service}] + (println "Deploying" config namespace) + (if (.exists (io/file (str namespace "-config.edn"))) + (do (reset! conf (clojure.edn/read-string (slurp (str namespace "-config.edn")))) + (create-namespace-if-not-exists service) + (let [injector (lk/injector)] + (-> injector + ;(make-environment-secrets :environment (:vars @conf)) + ;(make-registry-secrets :docker-registry {:.dockerconfigjson (:registry @conf)}) + ;; make-ingress-hackspace + ;; database + ;; redis + ;; hackspace + (apply-blocks blocks @conf) + lk/standard-descs + (lk/get-deployable deployment-config) + (lkh/to-yaml-store (str namespace "-store.yaml")) + (kube-apply (io/file (str namespace "-deploy.yaml")) namespace config)))) + (println "No config found " (str namespace "-config.yaml")))) + +(defn deploy [{:keys [namespace] :as ns-config}] + (let [config (get-in data/services [(keyword namespace) :config]) + blocks (get-in data/services [(keyword namespace) :blocks])] (cond - (= service "hackspace") (hackspace-deployment "maidstone-hackspace" "/home/oly/.kube/dke.yml") - (= service "footprint-production") (footprint-deployment "footprint-production" "/home/oly/.kube/ake.yml") - (= service "footprint-stage") (footprint-deployment "footprint" "/home/oly/.kube/ake.yml") - :else (println (str "Service " service " not found."))))) + (= namespace "hackspace") (hackspace-deployment "maidstone-hackspace" "/home/oly/.kube/dke.yml") + (= namespace "footprint-production") (footprint-deployment "footprint-production" "/home/oly/.kube/ake.yml") + (= namespace "footprint-stage") (footprint-deployment "footprint" "/home/oly/.kube/ake.yml") + (= (nil? blocks)) (build {:namespace namespace :config config :blocks blocks}) + :else (println (str "Namespace " namespace " not found."))))) (def CONFIGURATION {:app {:command "Kubernetes deploy" @@ -447,7 +531,7 @@ ; :global-opts [] :commands [{:command "deploy" :description "Deploy app out to cluster" - :opts [{:option "service" :as "Service to deploy" :type :string :spec string?}] + :opts [{:option "namespace" :as "Namespace to deploy" :type :string :spec string?}] :runs deploy} {:command "secrets" :description "Display secrests" diff --git a/src/kube_deploy/helpers.clj b/src/kube_deploy/helpers.clj index 308f504..feb1645 100644 --- a/src/kube_deploy/helpers.clj +++ b/src/kube_deploy/helpers.clj @@ -1,7 +1,21 @@ (ns kube-deploy.helpers (:require [lambdakube.core :as lk] + [cheshire.core :as json] + [clojure.java.shell :as sh] + [clojure.data.codec.base64 :as b64])) +(defn kube-namespaces + ([] + (kube-namespaces nil)) + ([kube-config] + (let [config-param (if (nil? kube-config) nil (str "--kubeconfig=" kube-config))] + (let [res (apply sh/sh (remove nil? ["kubectl" config-param "get" "namespaces" "-o=json"]))] + (if (= (:exit res) 0) + (-> (json/parse-string (:out res) true) + :items) + (throw (Exception. (:err res)))))))) + (defn kube-namespace ([name] {:apiVersion "v1"