diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab17090 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +.clj-kondo/ +.lsp/ + +# pybuilder +.pybuilder/ +__pycache__/ + +# lein +target/ +.lein-repl-history +.lein-failures +pom.* + +# cljs +.shadow-cljs +.nrepl-* +package-lock.json +node_modules/ +public/js/ + +# ide +.calva + +*.iml +.idea/ + +auth.edn +config.edn diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..0d7852f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,116 @@ +stages: + - build_and_test + - package + - security + - upload + - image + +services: + - docker:19.03.12-dind + +.cljs-job: &cljs + image: domaindrivenarchitecture/shadow-cljs + cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - node_modules/ + - .shadow-cljs/ + - .m2 + before_script: + - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc + - npm install + +.clj-uploadjob: &clj + image: domaindrivenarchitecture/lein + cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - .m2 + before_script: + - mkdir -p /root/.lein + - echo "{:auth {:repository-auth {#\"clojars\" {:username \"${CLOJARS_USER}\" :password \"${CLOJARS_TOKEN_DOMAINDRIVENARCHITECTURE}\" }}}}" > ~/.lein/profiles.clj + +test-cljs: + <<: *cljs + stage: build_and_test + script: + - shadow-cljs compile test + - node target/node-tests.js + +test-clj: + <<: *clj + stage: build_and_test + script: + - lein test + +test-schema: + <<: *clj + stage: build_and_test + script: + - lein uberjar + - java -jar target/uberjar/c4k-website-standalone.jar valid-config.edn valid-auth.edn | kubeconform --kubernetes-version 1.19.0 --strict --skip Certificate - + artifacts: + paths: + - target/uberjar + +report-frontend: + <<: *cljs + stage: package + script: + - mkdir -p target/frontend-build + - shadow-cljs run shadow.cljs.build-report frontend target/frontend-build/build-report.html + artifacts: + paths: + - target/frontend-build/build-report.html + +package-frontend: + <<: *cljs + stage: package + script: + - mkdir -p target/frontend-build + - shadow-cljs release frontend + - cp public/js/main.js target/frontend-build/c4k-website.js + - sha256sum target/frontend-build/c4k-website.js > target/frontend-build/c4k-website.js.sha256 + - sha512sum target/frontend-build/c4k-website.js > target/frontend-build/c4k-website.js.sha512 + artifacts: + paths: + - target/frontend-build + +package-uberjar: + <<: *clj + stage: package + script: + - lein uberjar + - sha256sum target/uberjar/c4k-website-standalone.jar > target/uberjar/c4k-website-standalone.jar.sha256 + - sha512sum target/uberjar/c4k-website-standalone.jar > target/uberjar/c4k-website-standalone.jar.sha512 + artifacts: + paths: + - target/uberjar + +upload-clj-release: + <<: *clj + stage: upload + rules: + - if: '$CI_COMMIT_TAG != null' + script: + - lein deploy + +release: + image: registry.gitlab.com/gitlab-org/release-cli:latest + stage: upload + rules: + - if: '$CI_COMMIT_TAG != null' + artifacts: + paths: + - target/uberjar + - target/frontend-build + script: + - apk --no-cache add curl + - | + release-cli create --name "Release $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG \ + --assets-link "{\"name\":\"c4k-website-standalone.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/c4k-website/-/jobs/${CI_JOB_ID}/artifacts/file/target/uberjar/c4k-website-standalone.jar\"}" \ + --assets-link "{\"name\":\"c4k-website-standalone.jar.sha256\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/c4k-website/-/jobs/${CI_JOB_ID}/artifacts/file/target/uberjar/c4k-website-standalone.jar.sha256\"}" \ + --assets-link "{\"name\":\"c4k-website-standalone.jar.sha512\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/c4k-website/-/jobs/${CI_JOB_ID}/artifacts/file/target/uberjar/c4k-website-standalone.jar.sha512\"}" \ + --assets-link "{\"name\":\"c4k-website.js\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/c4k-website/-/jobs/${CI_JOB_ID}/artifacts/file/target/frontend-build/c4k-website.js\"}" \ + --assets-link "{\"name\":\"c4k-website.js.sha256\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/c4k-website/-/jobs/${CI_JOB_ID}/artifacts/file/target/frontend-build/c4k-website.js.sha256\"}" \ + --assets-link "{\"name\":\"c4k-website.js.sha512\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/c4k-website/-/jobs/${CI_JOB_ID}/artifacts/file/target/frontend-build/c4k-website.js.sha512\"}" \ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/doc/Releasing.md b/doc/Releasing.md new file mode 100644 index 0000000..e3aa755 --- /dev/null +++ b/doc/Releasing.md @@ -0,0 +1,14 @@ +# stable release (should be done from master) + +``` +#adjust [version] +vi package.json + +lein release +git push --follow-tags + +# bump version - increase version and add -SNAPSHOT +vi package.json +git commit -am "version bump" +git push +``` \ No newline at end of file diff --git a/doc/tryItOut.png b/doc/tryItOut.png new file mode 100644 index 0000000..02396e1 Binary files /dev/null and b/doc/tryItOut.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..3245952 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "c4k-website", + "description": "Generate c4k yaml for a website deployment.", + "author": "meissa GmbH", + "version": "0.1.1-SNAPSHOT", + "homepage": "https://gitlab.com/domaindrivenarchitecture/c4k-website#readme", + "repository": "https://www.npmjs.com/package/c4k-website", + "license": "APACHE2", + "main": "c4k-website.js", + "bin": { + "c4k-website": "./c4k-website.js" + }, + "keywords": [ + "cljs", + "website", + "k8s", + "c4k", + "deployment", + "yaml", + "convention4kubernetes" + ], + "bugs": { + "url": "https://gitlab.com/domaindrivenarchitecture/c4k-website/issues" + }, + "dependencies": { + "js-base64": "^3.6.1", + "js-yaml": "^4.0.0" + }, + "devDependencies": { + "shadow-cljs": "^2.11.18", + "source-map-support": "^0.5.19" + } +} \ No newline at end of file diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..734fabc --- /dev/null +++ b/project.clj @@ -0,0 +1,46 @@ +(defproject org.domaindrivenarchitecture/c4k-website "0.1.1-SNAPSHOT" + :description "website c4k-installation package" + :url "https://domaindrivenarchitecture.org" + :license {:name "Apache License, Version 2.0" + :url "https://www.apache.org/licenses/LICENSE-2.0.html"} + :dependencies [[org.clojure/clojure "1.10.3"] + [org.clojure/tools.reader "1.3.6"] + [org.domaindrivenarchitecture/c4k-common-clj "3.0.1"] + [hickory "0.7.1"]] + :target-path "target/%s/" + :source-paths ["src/main/cljc" + "src/main/clj"] + :resource-paths ["src/main/resources"] + :repositories [["snapshots" :clojars] + ["releases" :clojars]] + :deploy-repositories [["snapshots" {:sign-releases false :url "https://clojars.org/repo"}] + ["releases" {:sign-releases false :url "https://clojars.org/repo"}]] + :profiles {:test {:test-paths ["src/test/cljc"] + :resource-paths ["src/test/resources"] + :dependencies [[dda/data-test "0.1.1"]]} + :dev {:plugins [[lein-shell "0.5.0"]]} + :uberjar {:aot :all + :main dda.c4k-website.uberjar + :uberjar-name "c4k-website-standalone.jar" + :dependencies [[org.clojure/tools.cli "1.0.206"] + [ch.qos.logback/logback-classic "1.3.0-alpha4" + :exclusions [com.sun.mail/javax.mail]] + [org.slf4j/jcl-over-slf4j "2.0.0-alpha1"]]}} + :release-tasks [["test"] + ["vcs" "assert-committed"] + ["change" "version" "leiningen.release/bump-version" "release"] + ["vcs" "commit"] + ["vcs" "tag" "v" "--no-sign"] + ["change" "version" "leiningen.release/bump-version"]] + :aliases {"native" ["shell" + "native-image" + "--report-unsupported-elements-at-runtime" + "--initialize-at-build-time" + "-jar" "target/uberjar/c4k-website-standalone.jar" + "-H:ResourceConfigurationFiles=graalvm-resource-config.json" + "-H:Log=registerResource" + "-H:Name=target/graalvm/${:name}"] + "inst" ["shell" + "sh" + "-c" + "lein uberjar && sudo install -m=755 target/uberjar/c4k-website-standalone.jar /usr/local/bin/c4k-website-standalone.jar"]}) diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..99ea0c9 --- /dev/null +++ b/public/index.html @@ -0,0 +1,17 @@ + + + + + + c4k-website + + + + + + +
+ + + + \ No newline at end of file diff --git a/src/main/clj/dda/c4k_gitea/uberjar.clj b/src/main/clj/dda/c4k_gitea/uberjar.clj new file mode 100644 index 0000000..eb2b794 --- /dev/null +++ b/src/main/clj/dda/c4k_gitea/uberjar.clj @@ -0,0 +1,9 @@ +(ns dda.c4k-website.uberjar + (:gen-class) + (:require + [dda.c4k-website.core :as core] + [dda.c4k-website.website :as website] + [dda.c4k-common.uberjar :as uberjar])) + +(defn -main [& cmd-args] + (uberjar/main-common "c4k-website" website/config? website/auth? website/config-defaults core/k8s-objects cmd-args)) diff --git a/src/main/cljc/dda/c4k_gitea/core.cljc b/src/main/cljc/dda/c4k_gitea/core.cljc new file mode 100644 index 0000000..974eddd --- /dev/null +++ b/src/main/cljc/dda/c4k_gitea/core.cljc @@ -0,0 +1,29 @@ +(ns dda.c4k-website.core + (:require + [dda.c4k-common.yaml :as yaml] + [dda.c4k-common.common :as cm] + [dda.c4k-website.website :as website] + [dda.c4k-common.postgres :as postgres])) + +(defn k8s-objects [config] + (let [storage-class (if (contains? config :postgres-data-volume-path) :manual :local-path)] + (cm/concat-vec + (map yaml/to-string + (filter #(not (nil? %)) + [(postgres/generate-config {:postgres-size :2gb :db-name "website"}) + (postgres/generate-secret config) + (when (contains? config :postgres-data-volume-path) + (postgres/generate-persistent-volume (select-keys config [:postgres-data-volume-path :pv-storage-size-gb]))) + (postgres/generate-pvc {:pv-storage-size-gb 5 + :pvc-storage-class-name storage-class}) + (postgres/generate-deployment {:postgres-image "postgres:14" + :postgres-size :2gb}) + (postgres/generate-service) + (website/generate-deployment) + (website/generate-service) + (website/generate-service-ssh) + (website/generate-data-volume config) + (website/generate-appini-env config) + (website/generate-secrets config) + (website/generate-ingress config) + (website/generate-certificate config)]))))) diff --git a/src/main/cljc/dda/c4k_gitea/gitea.cljc b/src/main/cljc/dda/c4k_gitea/gitea.cljc new file mode 100644 index 0000000..71dc246 --- /dev/null +++ b/src/main/cljc/dda/c4k_gitea/gitea.cljc @@ -0,0 +1,138 @@ +(ns dda.c4k-website.website + (:require + [clojure.spec.alpha :as s] + [clojure.string :as st] + #?(: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]) + [dda.c4k-common.yaml :as yaml] + [dda.c4k-common.common :as cm] + [dda.c4k-common.base64 :as b64] + [dda.c4k-common.predicate :as pred] + [dda.c4k-common.postgres :as postgres])) + +(defn domain-list? + [input] + (or + (st/blank? input) + (pred/string-of-separated-by? pred/fqdn-string? #"," input))) + +(s/def ::default-app-name string?) +(s/def ::fqdn pred/fqdn-string?) +(s/def ::mailer-from pred/bash-env-string?) +(s/def ::mailer-host-port pred/host-and-port-string?) +(s/def ::service-domain-whitelist domain-list?) +(s/def ::service-noreply-address string?) +(s/def ::mailer-user pred/bash-env-string?) +(s/def ::mailer-pw pred/bash-env-string?) +(s/def ::issuer pred/letsencrypt-issuer?) +(s/def ::volume-total-storage-size (partial pred/int-gt-n? 5)) + +(def config-defaults {:issuer "staging"}) + +(def config? (s/keys :req-un [::fqdn + ::mailer-from + ::mailer-host-port + ::service-noreply-address] + :opt-un [::issuer + ::default-app-name + ::service-domain-whitelist])) + +(def auth? (s/keys :req-un [::postgres/postgres-db-user ::postgres/postgres-db-password ::mailer-user ::mailer-pw])) + +(def vol? (s/keys :req-un [::volume-total-storage-size])) + +(defn data-storage-by-volume-size + [total] + total) + + +#?(:cljs + (defmethod yaml/load-resource :website [resource-name] + (case resource-name + "website/appini-env-configmap.yaml" (rc/inline "website/appini-env-configmap.yaml") + "website/deployment.yaml" (rc/inline "website/deployment.yaml") + "website/certificate.yaml" (rc/inline "website/certificate.yaml") + "website/ingress.yaml" (rc/inline "website/ingress.yaml") + "website/secrets.yaml" (rc/inline "website/secrets.yaml") + "website/service.yaml" (rc/inline "website/service.yaml") + "website/service-ssh.yaml" (rc/inline "website/service-ssh.yaml") + "website/datavolume.yaml" (rc/inline "website/datavolume.yaml") + (throw (js/Error. "Undefined Resource!"))))) + +#?(:cljs + (defmethod yaml/load-as-edn :website [resource-name] + (yaml/from-string (yaml/load-resource resource-name)))) + +(defn-spec generate-appini-env pred/map-or-seq? + [config config?] + (let [{:keys [default-app-name + fqdn + mailer-from + mailer-host-port + service-domain-whitelist + service-noreply-address] + :or {default-app-name "website instance" + service-domain-whitelist fqdn}} + config] + (-> + (yaml/load-as-edn "website/appini-env-configmap.yaml") + (cm/replace-all-matching-values-by-new-value "APPNAME" default-app-name) + (cm/replace-all-matching-values-by-new-value "FQDN" fqdn) + (cm/replace-all-matching-values-by-new-value "URL" (str "https://" fqdn)) + (cm/replace-all-matching-values-by-new-value "FROM" mailer-from) + (cm/replace-all-matching-values-by-new-value "HOSTANDPORT" mailer-host-port) + (cm/replace-all-matching-values-by-new-value "WHITELISTDOMAINS" service-domain-whitelist) + (cm/replace-all-matching-values-by-new-value "NOREPLY" service-noreply-address)))) + +(defn-spec generate-secrets pred/map-or-seq? + [auth auth?] + (let [{:keys [postgres-db-user + postgres-db-password + mailer-user + mailer-pw]} auth] + (-> + (yaml/load-as-edn "website/secrets.yaml") + (cm/replace-all-matching-values-by-new-value "DBUSER" (b64/encode postgres-db-user)) + (cm/replace-all-matching-values-by-new-value "DBPW" (b64/encode postgres-db-password)) + (cm/replace-all-matching-values-by-new-value "MAILERUSER" (b64/encode mailer-user)) + (cm/replace-all-matching-values-by-new-value "MAILERPW" (b64/encode mailer-pw))))) + +(defn-spec generate-ingress pred/map-or-seq? + [config config?] + (let [{:keys [fqdn issuer]} config] + (-> + (yaml/load-as-edn "website/ingress.yaml") + (cm/replace-all-matching-values-by-new-value "FQDN" fqdn)))) + +(defn-spec generate-certificate pred/map-or-seq? + [config config?] + (let [{:keys [fqdn issuer] + :or {issuer "staging"}} config + letsencrypt-issuer (name issuer)] + (-> + (yaml/load-as-edn "website/certificate.yaml") + (assoc-in [:spec :issuerRef :name] letsencrypt-issuer) + (cm/replace-all-matching-values-by-new-value "FQDN" fqdn)))) + +(defn-spec generate-data-volume pred/map-or-seq? + [config vol?] + (let [{:keys [volume-total-storage-size]} config + data-storage-size (data-storage-by-volume-size volume-total-storage-size)] + (-> + (yaml/load-as-edn "website/datavolume.yaml") + (cm/replace-all-matching-values-by-new-value "DATASTORAGESIZE" (str (str data-storage-size) "Gi"))))) + +(defn-spec generate-deployment pred/map-or-seq? + [] + (yaml/load-as-edn "website/deployment.yaml")) + +(defn-spec generate-service pred/map-or-seq? + [] + (yaml/load-as-edn "website/service.yaml")) + +(defn-spec generate-service-ssh pred/map-or-seq? + [] + (yaml/load-as-edn "website/service-ssh.yaml")) diff --git a/src/main/cljs/dda/c4k_gitea/browser.cljs b/src/main/cljs/dda/c4k_gitea/browser.cljs new file mode 100644 index 0000000..79ce286 --- /dev/null +++ b/src/main/cljs/dda/c4k_gitea/browser.cljs @@ -0,0 +1,125 @@ +(ns dda.c4k-website.browser + (:require + [clojure.string :as st] + [clojure.tools.reader.edn :as edn] + [dda.c4k-website.core :as core] + [dda.c4k-website.website :as website] + [dda.c4k-common.browser :as br] + [dda.c4k-common.postgres :as pgc] + [dda.c4k-common.common :as cm])) + +(defn generate-group + [name + content] + [{:type :element + :tag :div + :attrs {:class "rounded border border-3 m-3 p-2"} + :content [{:type :element + :tag :b + :attrs {:style "z-index: 1; position: relative; top: -1.3rem;"} + :content name} + {:type :element + :tag :fieldset + :content content}]}]) + +(defn generate-content [] + (cm/concat-vec + [(assoc + (br/generate-needs-validation) :content + (cm/concat-vec + (generate-group + "domain" + (cm/concat-vec + (br/generate-input-field "fqdn" "Your fqdn:" "repo.test.de") + (br/generate-input-field "mailer-from" "Your mailer email address:" "test@test.de") + (br/generate-input-field "mailer-host-port" "Your mailer host with port:" "test.de:123") + (br/generate-input-field "service-noreply-address" "Your noreply domain:" "test.de") + (br/generate-input-field "issuer" "(Optional) Your issuer prod/staging:" "") + (br/generate-input-field "app-name" "(Optional) Your app name:" "") + (br/generate-input-field "domain-whitelist" "(Optional) Domain whitelist for registration email-addresses:" ""))) + (generate-group + "provider" + (cm/concat-vec + (br/generate-input-field "volume-total-storage-size" "Your website volume-total-storage-size:" "20") + (br/generate-input-field "postgres-data-volume-path" "(Optional) Your postgres-data-volume-path if Persistent Volumes are not generated by an Operator:" ""))) + (generate-group + "credentials" + (br/generate-text-area + "auth" "Your auth.edn:" + "{:postgres-db-user \"website\" + :postgres-db-password \"website-db-password\" + :mailer-user \"test@test.de\" + :mailer-pw \"mail-test-password\"}" + "5")) + [(br/generate-br)] + (br/generate-button "generate-button" "Generate c4k yaml")))] + (br/generate-output "c4k-website-output" "Your c4k deployment.yaml:" "25"))) + +(defn generate-content-div + [] + {:type :element + :tag :div + :content + (generate-content)}) + +(defn config-from-document [] + (let [postgres-data-volume-path (br/get-content-from-element "postgres-data-volume-path" :optional true) + issuer (br/get-content-from-element "issuer" :optional true) + app-name (br/get-content-from-element "app-name" :optional true) + domain-whitelist (br/get-content-from-element "domain-whitelist" :optional true)] + (merge + {:fqdn (br/get-content-from-element "fqdn") + :mailer-from (br/get-content-from-element "mailer-from") + :mailer-host-port (br/get-content-from-element "mailer-host-port") + :service-noreply-address (br/get-content-from-element "service-noreply-address") + :volume-total-storage-size (br/get-content-from-element "volume-total-storage-size" :deserializer js/parseInt)} + (when (not (st/blank? postgres-data-volume-path)) + {:postgres-data-volume-path postgres-data-volume-path}) + (when (not (st/blank? issuer)) + {:issuer issuer}) + (when (not (st/blank? app-name)) + {:default-app-name app-name}) + (when (not (st/blank? domain-whitelist)) + {:service-domain-whitelist domain-whitelist}) + ))) + +(defn validate-all! [] + (br/validate! "fqdn" ::website/fqdn) + (br/validate! "mailer-from" ::website/mailer-from) + (br/validate! "mailer-host-port" ::website/mailer-host-port) + (br/validate! "service-noreply-address" ::website/service-noreply-address) + (br/validate! "issuer" ::website/issuer :optional true) + (br/validate! "app-name" ::website/default-app-name :optional true) + (br/validate! "domain-whitelist" ::website/service-domain-whitelist :optional true) + (br/validate! "postgres-data-volume-path" ::pgc/postgres-data-volume-path :optional true) + (br/validate! "volume-total-storage-size" ::website/volume-total-storage-size :deserializer js/parseInt) + (br/validate! "auth" website/auth? :deserializer edn/read-string) + (br/set-form-validated!)) + +(defn add-validate-listener [name] + (-> (br/get-element-by-id name) + (.addEventListener "blur" #(do (validate-all!))))) + + +(defn init [] + (br/append-hickory (generate-content-div)) + (-> js/document + (.getElementById "generate-button") + (.addEventListener "click" + #(do (validate-all!) + (-> (cm/generate-common + (config-from-document) + (br/get-content-from-element "auth" :deserializer edn/read-string) + website/config-defaults + core/k8s-objects) + (br/set-output!))))) + (add-validate-listener "fqdn") + (add-validate-listener "mailer-from") + (add-validate-listener "mailer-host-port") + (add-validate-listener "service-noreply-address") + (add-validate-listener "app-name") + (add-validate-listener "domain-whitelist") + (add-validate-listener "postgres-data-volume-path") + (add-validate-listener "volume-total-storage-size") + (add-validate-listener "issuer") + (add-validate-listener "auth")) \ No newline at end of file diff --git a/src/main/resources/gitea/certificate.yaml b/src/main/resources/gitea/certificate.yaml new file mode 100644 index 0000000..a9dacca --- /dev/null +++ b/src/main/resources/gitea/certificate.yaml @@ -0,0 +1,15 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: website-cert + namespace: default +spec: + secretName: website-cert + commonName: FQDN + duration: 2160h # 90d + renewBefore: 360h # 15d + dnsNames: + - FQDN + issuerRef: + name: staging + kind: ClusterIssuer \ No newline at end of file diff --git a/src/main/resources/gitea/datavolume.yaml b/src/main/resources/gitea/datavolume.yaml new file mode 100644 index 0000000..7be576e --- /dev/null +++ b/src/main/resources/gitea/datavolume.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: website-data-pvc + namespace: default + labels: + app: website +spec: + storageClassName: local-path + accessModes: + - ReadWriteOnce + resources: + requests: + storage: DATASTORAGESIZE + \ No newline at end of file diff --git a/src/main/resources/gitea/ingress.yaml b/src/main/resources/gitea/ingress.yaml new file mode 100644 index 0000000..f7aed60 --- /dev/null +++ b/src/main/resources/gitea/ingress.yaml @@ -0,0 +1,24 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-website + namespace: default + annotations: + ingress.kubernetes.io/ssl-redirect: "true" + traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd +spec: + tls: + - hosts: + - FQDN + secretName: website-cert + rules: + - host: FQDN + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: website-service + port: + number: 3000 diff --git a/src/main/resources/gitea/nginx-configmap.yaml b/src/main/resources/gitea/nginx-configmap.yaml new file mode 100644 index 0000000..49c58ab --- /dev/null +++ b/src/main/resources/gitea/nginx-configmap.yaml @@ -0,0 +1,56 @@ +# ToDo: +# content-pfad für nginx server definieren +# sinnvolle security policies konfigurieren + +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-env + namespace: default +data: + nginx.conv: | + user nginx; + worker_processes 3; + error_log /var/log/nginx/error.log; + events { + worker_connections 10240; + } + http { + log_format main + 'remote_addr:$remote_addr\t' + 'time_local:$time_local\t' + 'method:$request_method\t' + 'uri:$request_uri\t' + 'host:$host\t' + 'status:$status\t' + 'bytes_sent:$body_bytes_sent\t' + 'referer:$http_referer\t' + 'useragent:$http_user_agent\t' + 'forwardedfor:$http_x_forwarded_for\t' + 'request_time:$request_time'; + access_log /var/log/nginx/access.log main; + server { + listen 80; + server_name _; + location / { + root html; + index index.html index.htm; + } + } + include /etc/nginx/virtualhost/virtualhost.conf; + } + virtualhost.conf: | + upstream app { + server localhost:8080; + keepalive 1024; + } + server { + listen 80 default_server; + root /usr/local/app; + access_log /var/log/nginx/app.access_log main; + error_log /var/log/nginx/app.error_log; + location / { + proxy_pass http://app/; + proxy_http_version 1.1; + } + } diff --git a/src/main/resources/gitea/nginx-deployment.yaml b/src/main/resources/gitea/nginx-deployment.yaml new file mode 100644 index 0000000..29db5ce --- /dev/null +++ b/src/main/resources/gitea/nginx-deployment.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + namespace: default + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + name: nginx + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + protocol: TCP + envFrom: + - configMapRef: + name: nginx-env + volumeMounts: + - name: website-data-volume + mountPath: "/data" + volumes: + - name: website-data-volume + persistentVolumeClaim: + claimName: website-data-pvc + diff --git a/src/main/resources/gitea/website-server-service.yaml b/src/main/resources/gitea/website-server-service.yaml new file mode 100644 index 0000000..0425f79 --- /dev/null +++ b/src/main/resources/gitea/website-server-service.yaml @@ -0,0 +1,16 @@ +kind: Service +apiVersion: v1 +metadata: + name: website-server-service + namespace: default + annotations: + metallb.universe.tf/allow-shared-ip: "shared-ip-service-group" +spec: + type: LoadBalancer + selector: + app: website-server + ports: + - port: 80 + targetPort: 80 + protocol: TCP + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..8985f2b --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,50 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + INFO + + + + + logs/pallet.log + + logs/old/pallet.%d{yyyy-MM-dd}.log + 3 + + + %date %level [%thread] %logger{10} %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/cljc/dda/c4k_gitea/website_test.cljc b/src/test/cljc/dda/c4k_gitea/website_test.cljc new file mode 100644 index 0000000..c1485ad --- /dev/null +++ b/src/test/cljc/dda/c4k_gitea/website_test.cljc @@ -0,0 +1,73 @@ +(ns dda.c4k-website.website-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.test-helper :as th] + [dda.c4k-common.base64 :as b64] + [dda.c4k-website.website :as cut])) + +(st/instrument `cut/generate-appini-env) +(st/instrument `cut/generate-ingress) +(st/instrument `cut/generate-secrets) + +(deftest should-generate-appini-env + (is (= {:APP_NAME-c1 "", + :APP_NAME-c2 "test website", + :website__mailer__FROM-c1 "", + :website__mailer__FROM-c2 "test@test.com", + :website__mailer__HOST-c1 "m.t.de:123", + :website__mailer__HOST-c2 "mail.test.com:123", + :website__server__DOMAIN-c1 "test.de", + :website__server__DOMAIN-c2 "test.com", + :website__server__ROOT_URL-c1 "https://test.de", + :website__server__ROOT_URL-c2 "https://test.com", + :website__server__SSH_DOMAIN-c1 "test.de", + :website__server__SSH_DOMAIN-c2 "test.com", + :website__service__EMAIL_DOMAIN_WHITELIST-c1 "adb.de", + :website__service__EMAIL_DOMAIN_WHITELIST-c2 "test.com,test.net", + :website__service__NO_REPLY_ADDRESS-c1 "", + :website__service__NO_REPLY_ADDRESS-c2 "noreply@test.com"} + (th/map-diff (cut/generate-appini-env {:default-app-name "" + :fqdn "test.de" + :mailer-from "" + :mailer-host-port "m.t.de:123" + :service-domain-whitelist "adb.de" + :service-noreply-address "" + }) + (cut/generate-appini-env {:default-app-name "test website" + :fqdn "test.com" + :mailer-from "test@test.com" + :mailer-host-port "mail.test.com:123" + :service-domain-whitelist "test.com,test.net" + :service-noreply-address "noreply@test.com" + }))))) + +(deftest should-generate-certificate + (is (= {:name-c2 "prod", :name-c1 "staging"} + (th/map-diff (cut/generate-certificate {}) + (cut/generate-certificate {:issuer "prod"}))))) + +(deftest should-generate-secret + (is (= {:website__database__USER-c1 "", + :website__database__USER-c2 (b64/encode "pg-user"), + :website__database__PASSWD-c1 "", + :website__database__PASSWD-c2 (b64/encode "pg-pw"), + :website__mailer__USER-c1 "", + :website__mailer__USER-c2 (b64/encode "maileruser"), + :website__mailer__PASSWD-c1 "", + :website__mailer__PASSWD-c2 (b64/encode "mailerpw")} + (th/map-diff (cut/generate-secrets {:postgres-db-user "" + :postgres-db-password "" + :mailer-user "" + :mailer-pw ""}) + (cut/generate-secrets {:postgres-db-user "pg-user" + :postgres-db-password "pg-pw" + :mailer-user "maileruser" + :mailer-pw "mailerpw"}))))) + +(deftest should-generate-data-volume + (is (= {:storage-c1 "1Gi", + :storage-c2 "15Gi"} + (th/map-diff (cut/generate-data-volume {:volume-total-storage-size 1}) + (cut/generate-data-volume {:volume-total-storage-size 15}))))) \ No newline at end of file