Compare commits

..

No commits in common. "main" and "1.1.4" have entirely different histories.
main ... 1.1.4

34 changed files with 1387 additions and 1165 deletions

2
.gitignore vendored
View file

@ -1,5 +1,3 @@
.eastwood
.clj-kondo/ .clj-kondo/
.lsp/ .lsp/

View file

@ -6,7 +6,7 @@ stages:
- image - image
.img: &img .img: &img
image: "domaindrivenarchitecture/ddadevops-dind:4.11.4" image: "domaindrivenarchitecture/ddadevops-dind:4.7.2"
services: services:
- docker:dind - docker:dind
before_script: before_script:
@ -16,7 +16,7 @@ stages:
- export IMAGE_TAG=$CI_COMMIT_TAG - export IMAGE_TAG=$CI_COMMIT_TAG
.cljs-job: &cljs .cljs-job: &cljs
image: "domaindrivenarchitecture/ddadevops-clj-cljs:4.11.4" image: "domaindrivenarchitecture/ddadevops-clj-cljs:4.7.2"
cache: cache:
key: ${CI_COMMIT_REF_SLUG} key: ${CI_COMMIT_REF_SLUG}
paths: paths:
@ -29,7 +29,7 @@ stages:
- npm install - npm install
.clj-job: &clj .clj-job: &clj
image: "domaindrivenarchitecture/ddadevops-clj:4.11.4" image: "domaindrivenarchitecture/ddadevops-clj-cljs:4.7.2"
cache: cache:
key: ${CI_COMMIT_REF_SLUG} key: ${CI_COMMIT_REF_SLUG}
paths: paths:
@ -93,15 +93,6 @@ package-uberjar:
paths: paths:
- target/uberjar - target/uberjar
package-native:
<<: *clj
stage: package
script:
- pyb package_native
artifacts:
paths:
- target/graalvm
release-to-clojars: release-to-clojars:
<<: *clj <<: *clj
<<: *tag_only <<: *tag_only

109
README.md
View file

@ -2,66 +2,27 @@
[![Clojars Project](https://img.shields.io/clojars/v/org.domaindrivenarchitecture/c4k-website.svg)](https://clojars.org/org.domaindrivenarchitecture/c4k-website) [![pipeline status](https://gitlab.com/domaindrivenarchitecture/c4k-website/badges/master/pipeline.svg)](https://gitlab.com/domaindrivenarchitecture/c4k-website/-/commits/main) [![Clojars Project](https://img.shields.io/clojars/v/org.domaindrivenarchitecture/c4k-website.svg)](https://clojars.org/org.domaindrivenarchitecture/c4k-website) [![pipeline status](https://gitlab.com/domaindrivenarchitecture/c4k-website/badges/master/pipeline.svg)](https://gitlab.com/domaindrivenarchitecture/c4k-website/-/commits/main)
[<img src="https://domaindrivenarchitecture.org/img/delta-chat.svg" width=20 alt="DeltaChat"> chat over e-mail](mailto:buero@meissa-gmbh.de?subject=community-chat) | [<img src="https://meissa.de/images/parts/contact/mastodon36_hue9b2464f10b18e134322af482b9c915e_5501_filter_14705073121015236177.png" width=20 alt="M"> meissa@social.meissa-gmbh.de](https://social.meissa-gmbh.de/@meissa) | [Blog](https://domaindrivenarchitecture.org) | [Website](https://meissa.de) [<img src="https://domaindrivenarchitecture.org/img/delta-chat.svg" width=20 alt="DeltaChat"> chat over e-mail](mailto:buero@meissa-gmbh.de?subject=community-chat) | [<img src="https://meissa-gmbh.de/img/community/Mastodon_Logotype.svg" width=20 alt="team@social.meissa-gmbh.de"> team@social.meissa-gmbh.de](https://social.meissa-gmbh.de/@team) | [Website & Blog](https://domaindrivenarchitecture.org)
## Purpose ## Purpose
**GitOps for static generated Websites.** Delivering cryogen generated static sites with the push of a few buttons.
c4k-website generates k8s-manifest for a webserver serving static html. c4k-website generates configuration files for multiple nginx webservers, and
corresponding cryogen static site generator build containers. This automatically downloads a `<branch>.zip` from a specified gitea API url. You need an authorization token to access the specified gitea user account. The build container is based on clojure:lein.
c4k-website is an example how to create k8s manifests for OneShotDeployments with https://repo.prod.meissa.de/meissa/c4k-common. Following the example in valid-config.edn and valid-auth.edn you can add as many websites as you like (provided you have the DNS Routes set up). One set of configmaps, deployment, services etc will be created for each element in the :websites and :auth list.
## Features Configs generated by c4k-website consists of the following parts:
- [convention 4 kubernetes: c4k-website](#convention-4-kubernetes-c4k-website) * nginx deployment + configmap + service + ingress + certificate definitions
- [Purpose](#purpose) * nginx deployment has init container, building the website at startup time
- [Features](#features) * cron job for building and build secret for storing auth data
- [Serving multiple websites](#serving-multiple-websites) * respective volume claims
- [https \& Letsencrypt Certificates](#https--letsencrypt-certificates)
- [Ratelimiting](#ratelimiting)
- [Monitoring](#monitoring)
- [GitOps your Websites](#gitops-your-websites)
- [Support Hugo and Cryogene](#support-hugo-and-cryogene)
- [Try out](#try-out)
- [Usage](#usage)
- [Development \& mirrors](#development--mirrors)
- [License](#license)
### Serving multiple websites ## Status
You can serve many websites, each of can listen to a list of domain names. E.g. Stable - we use this setup on production.
```yaml
# Config example
websites:
- unique-name: "test.io"
fqdns: ["test.de", "test.org", "www.test.de", "www.test.org"]
- unique-name: "example.io"
fqdns: ["example.org", "www.example.com"]
```
### https & Letsencrypt Certificates
The domain names listed will get their certificates from letsencrypt out of the box.
### Ratelimiting
There is a Ratelimit included for each website. This makes it harder to DOS one or all websites served.
### Monitoring
Monitoring on GrafanaCloud (or any other grafana) is included out of the box.
### GitOps your Websites
If your repository is on a frogejo or gitea repo, we included GitOps. We ask every ten minutes the repo api for new pushed content.
If there is new content we generate the new static html & serve it.
### Support Hugo and Cryogene
For static html generation we support cryogen & hugo.
## Try out ## Try out
@ -73,18 +34,48 @@ Your input will stay in your browser. No server interaction is required.
You will also be able to try out on cli: You will also be able to try out on cli:
``` ```
c4k-website src/test/resources/valid-config.yaml src/test/resources/valid-auth.yaml target/graalvm/c4k-website src/test/resources/website-test/valid-config.yaml src/test/resources/website-test/valid-auth.yaml | kubeval -
target/graalvm/c4k-website src/test/resources/website-test/valid-config.yaml src/test/resources/website-test/valid-auth.yaml | kubectl apply -f -
``` ```
## Usage ## Usage
Prerequisites: You need:
* DNS routes pointing to your k8s cluster.
* frogejo or gitea instance for your website repo. In doubt use https://codeberg.org
* A git repository with your hugo / cryogene website
* the project provides a `generate.sh` (maybe your generatoin needs some preprocessing)
* `generate.sh` deliver its static generated html to `target/html`
* DNS routes matching the fqdns in the lists
* cryogen as a static site generator
* a cryogen project ready to build
* a gitea account which holds the buildable project
* an authorization token for that account
* and a kubernetes cluster provisioned by [provs]
Add a unique identifier for each website to config.edn and auth.edn (e.g. domain-name).
Add the list of fqdns, your gitea host, name of the website repo and the respective branch name (usually "main") to config.edn.
Add auth token and gitea user with access to the website repo to auth.edn.
Let c4k-website generate your .yaml file.
Apply this file on your cluster with `kubectl apply -f yourApp.yaml`.
Done.
### Script Execution
Optionally you can specify a trusted script in your config.edn.
1. `sha256sum` "/path/to/your/script/file"
2. copy the output of this command
3. add the :sha256-output "hash-of-file file.name" :key value pair to the respective collection in :websites.
Also, see the example in valid-config.edn. The script file needs to exist in the `<branch>.zip` and path specification to the script file should be relative to the root of the unzipped folder. Scripts can be of any type - as long as an according shebang exists in the first line.
```bash
sha256sum your-script-file # use output of this call
sha256sum scripts/your-script-file # or this
sha256sum scripts/foo/bar/your-script-file # or this
```
### resource requests and limits
You may want to adjust the resource requests and limits of the build and init containers to your specific scenario.
## Development & mirrors ## Development & mirrors
@ -99,7 +90,7 @@ For more details about our repository model see: https://repo.prod.meissa.de/mei
## License ## License
Copyright © 2022, 2023, 2024 meissa GmbH Copyright © 2022 meissa GmbH
Licensed under the [Apache License, Version 2.0](LICENSE) (the "License") Licensed under the [Apache License, Version 2.0](LICENSE) (the "License")
Pls. find licenses of our subcomponents [here](doc/SUBCOMPONENT_LICENSE) Pls. find licenses of our subcomponents [here](doc/SUBCOMPONENT_LICENSE)

View file

@ -28,9 +28,8 @@ def initialize(project):
"release_organisation": "meissa", "release_organisation": "meissa",
"release_repository_name": name, "release_repository_name": name,
"release_artifacts": [ "release_artifacts": [
"target/graalvm/" + name, "target/uberjar/c4k-website-standalone.jar",
"target/uberjar/" + name + "-standalone.jar", "target/frontend-build/c4k-website.js",
"target/frontend-build/" + name + ".js"
], ],
} }
@ -56,7 +55,7 @@ def test_schema(project):
"java -jar target/uberjar/c4k-website-standalone.jar " "java -jar target/uberjar/c4k-website-standalone.jar "
+ "src/test/resources/website-test/valid-config.yaml " + "src/test/resources/website-test/valid-config.yaml "
+ "src/test/resources/website-test/valid-auth.yaml | " + "src/test/resources/website-test/valid-auth.yaml | "
+ """kubeconform --kubernetes-version 1.23.0 --strict --skip "Certificate,Middleware" -""", + "kubeconform --kubernetes-version 1.23.0 --strict --skip Certificate -",
shell=True, shell=True,
check=True, check=True,
) )
@ -106,60 +105,6 @@ def package_uberjar(project):
check=True, check=True,
) )
@task
def package_native(project):
run(
"mkdir -p target/graalvm",
shell=True,
check=True,
)
run(
"native-image " +
"--native-image-info " +
"--report-unsupported-elements-at-runtime " +
"--no-server " +
"--no-fallback " +
"--features=clj_easy.graal_build_time.InitClojureClasses " +
"-jar target/uberjar/" + project.name + "-standalone.jar " +
"-march=compatibility " +
"-H:IncludeResources=.*.yaml " +
"-H:IncludeResources=logback.xml " +
"-H:Log=registerResource:verbose " +
"-H:Name=target/graalvm/" + project.name + "",
shell=True,
check=True,
)
run(
"sha256sum target/graalvm/" + project.name + " > target/graalvm/" + project.name + ".sha256",
shell=True,
check=True,
)
run(
"sha512sum target/graalvm/" + project.name + " > target/graalvm/" + project.name + ".sha512",
shell=True,
check=True,
)
@task
def inst(project):
run(
"lein uberjar",
shell=True,
check=True,
)
package_native(project)
run(
"sudo install -m=755 target/uberjar/" + project.name + "-standalone.jar /usr/local/bin/" + project.name + "-standalone.jar",
shell=True,
check=True,
)
run(
"sudo install -m=755 target/graalvm/" + project.name + " /usr/local/bin/" + project.name + "",
shell=True,
check=True,
)
@task @task
def upload_clj(project): def upload_clj(project):

77
doc/Releasing.md Normal file
View file

@ -0,0 +1,77 @@
# Release process
## ... for testing (snapshots)
Make sure your clojars.org credentials are correctly set in your ~/.lein/profiles.clj file.
``` bash
git add .
git commit
```
``` bash
lein deploy # or lein deploy clojars
```
## ... for stable release patch version
Make sure tags are protected in gitlab:
Repository Settings -> Protected Tags -> set \*.\*.\* as tag and save.
Make sure all your changes are committed:
``` bash
git checkout main # for old projects replace main with master
git add .
git commit
```
Open package.json, find "version" keyword and remove "-SNAPSHOT" from version number.
``` bash
git add .
# REPLACE x.x.x with the correct version
git commit -m "Release vx.x.x"
lein release
git push --follow-tags
```
Open package.json again, increase version increment by one and add "-SNAPSHOT".
``` bash
git commit -am "[Skip-CI] version bump"
git push
```
## ... for stable release minor version
Make sure tags are protected in gitlab:
Repository Settings -> Protected Tags -> set \*.\*.\* as tag and save.
``` bash
git checkout main # for old projects replace main with master
git add .
git commit
```
In package.json, find "version" keyword and remove "-SNAPSHOT" from version number.
Increment minor version by one, set patch version to zero.
Open project.clj, find ":version" keyword, increment minor version by one, set patch version to zero.
Leave "-SNAPSHOT" be.
``` bash
git add .
# REPLACE x.x.x with the correct version
git commit -m "Release vx.x.x"
lein release
git push --follow-tags
```
Open package.json again, increase version increment by one and add "-SNAPSHOT".
``` bash
git commit -am "[Skip-CI] version bump"
git push
```
Done.

View file

@ -1,81 +1,26 @@
# GitOps for Websites
```mermaid
sequenceDiagram
Actor a as Website Author
participant j as Job triggerd by Cron
participant f as Forgejo Instance
participant g as Your Git Repo for Website
a ->> g: commit & push some new content
j ->> f: check repo hash for new commits
activate j
f ->> g: get lates commit hash
f -->> j:
j ->> f: download repo
j ->> j: generate.sh
j ->> j: cp /target/html to website
deactivate j
```
# Runtime view
For the example configuration
```yaml
issuer: "staging"
websiteconfigs:
- unique-name: "test.io"
fqdns: ["test.de", "test.org", "www.test.de", "www.test.org"]
forgejo-host: "codeberg.org"
repo-name: "repo"
branchname: "main"
- unique-name: "example.io"
fqdns: ["example.org", "www.example.com"]
forgejo-host: "fineForgejoHost.net"
repo-name: "repo"
branchname: "main"
mon-cfg:
grafana-cloud-url: "url-for-your-prom-remote-write-endpoint"
cluster-name: "website"
cluster-stage: "test"
```
the website runtime looks like:
```mermaid ```mermaid
C4Context C4Context
title c4k-webserver title c4k-webserver
Boundary(k8s, "cluster") { Boundary(website, "website") {
Boundary(test_io, "namespace test-io"){ System(website_ing1, "ingress f. host meissa-gmbh.de")
System(website_ingt, "ingress f. test.de") System(website_ing2, "ingress f. host meissa.de")
Boundary(test_de_srv_t, "webserver") { Boundary(website_pod, "website pod"){
System(wst, "webserver") Boundary(aaa, "website container") {
SystemDb(file_htmlt, "static html") System(ws, "webserver")
Rel(wst, file_htmlt, "file ro") SystemDb(file_html, "static html")
} Rel(ws, file_html, "file ro")
Boundary(aab, "cron generate website") {
System(git_clonet, "git clone/pull & generate.sh & copy to static html")
SystemDb(file_gitt, "git repo for test.io")
Rel(git_clonet, file_gitt, "file rw")
Rel(file_gitt, file_htmlt, "file rw")
}
} }
Rel(website_ingt, wst, "http") Boundary(aab, "cron build website") {
Boundary(example_io, "namespace example-io"){ System(git_clone, "git clone/pull & lein ring server & copy to static html")
System(website_inge, "ingress f. example.org") SystemDb(file_git, "git repo")
Boundary(test_de_srv_e, "webserver") { Rel(git_clone, file_git, "file rw")
System(wse, "webserver") Rel(git_clone, file_html, "file rw")
SystemDb(file_htmle, "static html") }
Rel(wse, file_htmle, "file ro")
}
Boundary(aeb, "cron generate website") {
System(git_clonee, "git clone/pull & generate.sh & copy to static html")
SystemDb(file_gite, "git repo for example.io")
Rel(git_clonee, file_gite, "file rw")
Rel(file_gite, file_htmle, "file rw")
}
}
Rel(website_inge, wse, "http")
} }
Rel(website_ing1, ws, "http")
Rel(website_ing2, ws, "http")
}
``` ```
[![](https://mermaid.ink/img/pako:eNqNU8tugzAQ_JWVD1UqJaka5cSxSX-guSIhgxewamxkL01RxL_XQFExSdr6gtee2ccMvrDMCGQRO-wPRhN-UqzBL5KkELL9--aMqUP7gXa8eDGNFty2q_5cEq4hZt_bmD3CZUT169Q6wmrCJVIXzz3Yfy06B_kWSuMIKpTO8U1RpeVW9Dl-y7C7n2FJXjaa1EbMmgUfevys34DEOZ-DM68Nl9qLEM647NRNpFGxeT8h9JiucqkwKalSPccRJ5lBH95kvaEasgekPgBrrgjd3aHSnpZZoyFtpBJw27nFVIWkJFNGD177AIbgqW6UggdQKDVY7wmMQ_ujzNQtkIG_hgql8JmnAhZrc1eGWTtz3ijG-d-0UMRrXjfb_9zMpB0sCX7u0f6SqA6yLYC7BTAs4guwNavQVlwK_y4HW2JGJVbepshvBea8URSzWHce2tSCE74KScayKOfK4Zrxhsyp1RmLyDY4gY6SF5ZX42H3BTITMPU)](https://mermaid.live/edit#pako:eNqNU8tugzAQ_JWVD1UqJaka5cSxSX-guSIhgxewamxkL01RxL_XQFExSdr6gtee2ccMvrDMCGQRO-wPRhN-UqzBL5KkELL9--aMqUP7gXa8eDGNFty2q_5cEq4hZt_bmD3CZUT169Q6wmrCJVIXzz3Yfy06B_kWSuMIKpTO8U1RpeVW9Dl-y7C7n2FJXjaa1EbMmgUfevys34DEOZ-DM68Nl9qLEM647NRNpFGxeT8h9JiucqkwKalSPccRJ5lBH95kvaEasgekPgBrrgjd3aHSnpZZoyFtpBJw27nFVIWkJFNGD177AIbgqW6UggdQKDVY7wmMQ_ujzNQtkIG_hgql8JmnAhZrc1eGWTtz3ijG-d-0UMRrXjfb_9zMpB0sCX7u0f6SqA6yLYC7BTAs4guwNavQVlwK_y4HW2JGJVbepshvBea8URSzWHce2tSCE74KScayKOfK4Zrxhsyp1RmLyDY4gY6SF5ZX42H3BTITMPU)

View file

@ -0,0 +1,632 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.22-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0" xml:space="preserve"/>
<node id="n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="154.25" width="221.25" x="674.8125" y="-212.076171875"/>
<y:Fill color="#C0C0C0" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" textColor="#000000" verticalTextPosition="bottom" visible="true" width="36.292236328125" x="4.0" xml:space="preserve" y="4.0">Client</y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n1">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="514.0" width="1041.25" x="-97.5625" y="-35.638671875"/>
<y:Fill color="#3366FF" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="28.515846252441406" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" textColor="#000000" verticalTextPosition="bottom" visible="true" width="131.1511993408203" x="4.0" xml:space="preserve" y="4.0">Hetzner-Server</y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="518.625" y="266.69427546920065">
<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.022751508694942912" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="426.5" width="691.75" x="-57.5" y="10.111328125"/>
<y:Fill color="#33CCCC" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" textColor="#000000" verticalTextPosition="bottom" visible="true" width="39.61585998535156" x="4.0" xml:space="preserve" y="4.0">Node</y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n3">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="85.0" width="93.75" x="290.0" y="60.548828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.34767150878906" x="3.7011642456054688" xml:space="preserve" y="30.966041564941406">Webserver 1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="octagon"/>
</y:ShapeNode>
</data>
</node>
<node id="n4">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="98.75" width="78.75" x="128.9375" y="53.673828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="61.10578918457031" x="8.822105407714844" xml:space="preserve" y="37.841041564941406">Ingress1<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="fatarrow2"/>
</y:ShapeNode>
</data>
</node>
<node id="n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="98.75" width="78.75" x="128.9375" y="182.423828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="61.10578918457031" x="8.822105407714844" xml:space="preserve" y="37.841041564941406">Ingress2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="fatarrow2"/>
</y:ShapeNode>
</data>
</node>
<node id="n6">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="85.0" width="93.75" x="290.0" y="189.298828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.34767150878906" x="3.7011642456054688" xml:space="preserve" y="30.966041564941406">Webserver 2<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="octagon"/>
</y:ShapeNode>
</data>
</node>
<node id="n7">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="112.5" width="123.75" x="-47.5" y="175.548828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.3358154296875" x="35.20709228515625" xml:space="preserve" y="44.716041564941406">Metallb<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="octagon"/>
</y:ShapeNode>
</data>
</node>
<node id="n8">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="153.75" width="148.75" x="-1048.8125" y="-1.1875"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="28.515846252441406" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.38749694824219" x="32.181251525878906" xml:space="preserve" y="62.6170768737793">pybuilder<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n9">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="160.0" width="148.75" x="-680.0625" y="266.25"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="28.515846252441406" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="51.15971374511719" x="48.795143127441406" xml:space="preserve" y="65.7420768737793">provs<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n10">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="160.0" width="148.75" x="-1048.8125" y="266.25"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="28.515846252441406" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="32.547821044921875" x="58.10108947753906" xml:space="preserve" y="65.7420768737793">c4k<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n11">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="153.75" width="148.75" x="-680.0625" y="-1.1875"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="28.515846252441406" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="86.96148681640625" x="30.894256591796875" xml:space="preserve" y="62.6170768737793">terraform<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n12">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="75.0" width="75.0" x="-643.1875" y="-173.138671875"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="25.792043685913086" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.8880615234375" x="18.55596923828125" xml:space="preserve" y="24.603978157043457">AWS<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n13">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="75.0" width="75.0" x="-331.90625" y="38.1875"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="16" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="25.792043685913086" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="63.69612121582031" x="5.651939392089844" xml:space="preserve" y="24.603978157043457">Hetzner<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n14">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="75.0" width="141.25" x="714.8125" y="-172.451171875"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="61.432403564453125" x="39.90879821777344" xml:space="preserve" y="27.327942848205566">local build<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n15">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="63.75" width="118.75" x="456.5" y="71.173828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="47.52583312988281" x="35.612083435058594" xml:space="preserve" y="20.341041564941406">Mount<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="111.3167724609375" x="4.0" xml:space="preserve" y="39.40588569641113">Holds website data
<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="0.5" nodeRatioX="-0.5" nodeRatioY="0.5" offsetX="4.0" offsetY="-4.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n16">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="63.75" width="118.75" x="456.5" y="199.923828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="47.52583312988281" x="35.612083435058594" xml:space="preserve" y="20.341041564941406">Mount<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="111.3167724609375" x="4.0" xml:space="preserve" y="39.40588569641113">Holds website data
<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="0.5" nodeRatioX="-0.5" nodeRatioY="0.5" offsetX="4.0" offsetY="-4.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n17">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="98.75" width="78.75" x="128.9375" y="311.173828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="15.255950927734375" x="31.747024536132812" xml:space="preserve" y="37.841041564941406">...<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="fatarrow2"/>
</y:ShapeNode>
</data>
</node>
<node id="n18">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="85.0" width="93.75" x="290.0" y="318.048828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="15.255950927734375" x="39.24702453613281" xml:space="preserve" y="30.966041564941406">...<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="octagon"/>
</y:ShapeNode>
</data>
</node>
<node id="n19">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="63.75" width="118.75" x="456.5" y="328.673828125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="15.255950927734375" x="51.74702453613281" xml:space="preserve" y="20.341041564941406">...<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="20.344114303588867" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="111.3167724609375" x="4.0" xml:space="preserve" y="39.40588569641113">Holds website data
<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="0.5" nodeRatioX="-0.5" nodeRatioY="0.5" offsetX="4.0" offsetY="-4.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n20">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="74.375" width="255.5" x="657.6875" y="65.861328125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="250.9450225830078" x="2.2774887084960938" xml:space="preserve" y="25.653541564941406">Folder in /var/www/fqdn-folder-name<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n21">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="74.375" width="255.5" x="657.6875" y="194.611328125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="250.9450225830078" x="2.2774887084960938" xml:space="preserve" y="25.653541564941406">Folder in /var/www/fqdn-folder-name<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n22">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="74.375" width="255.5" x="657.6875" y="323.361328125"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="23.067916870117188" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="250.9450225830078" x="2.2774887084960938" xml:space="preserve" y="25.653541564941406">Folder in /var/www/fqdn-folder-name<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<edge id="e0" source="n7" target="n7">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="14.375" y="231.798828125"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="0.0" ratio="1.0" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e1" source="n7" target="n4">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e2" source="n7" target="n5">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e3" source="n4" target="n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e4" source="n8" target="n11">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.41203308105469" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.26309204101562" x="67.36845397949219" xml:space="preserve" y="10.293983459472656">out_conf.edn
out_auth.edn<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e5" source="n11" target="n9">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.41203308105469" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="40.27003479003906" x="8.364982604980469" xml:space="preserve" y="31.418983459472656">IP
FQDN<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e6" source="n11" target="n12">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="-625.0275" y="-58.35633333333334"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.706016540527344" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.841064453125" x="-76.92666845703047" xml:space="preserve" y="-69.5675916035971">DNS Entry<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e7" source="n11" target="n13">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="-441.69499999999994" y="98.875"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.41203308105469" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="68.63603210449219" x="65.34923394775524" xml:space="preserve" y="18.702248697916332">ssh keys
server size<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e8" source="n13" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="57.11804962158203" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="108.4290771484375" x="11.446461425783014" xml:space="preserve" y="19.35144684855112">spin up server
of requested size
with ssh keys<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e9" source="n9" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e10" source="n9" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e11" source="n9" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.41203308105469" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="95.46807861328125" x="124.88346069335978" xml:space="preserve" y="-5.053327657063903">k3s and
c4k application<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e12" source="n9" target="n10">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="-787.3125" y="291.125"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.41203308105469" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.26309204101562" x="-102.75654602050781" xml:space="preserve" y="-76.6949691772461">out_conf.edn
out_auth.edn<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e13" source="n10" target="n9">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="-793.8125" y="404.125"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.706016540527344" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="132.40110778808594" x="18.766946105956777" xml:space="preserve" y="44.647786814371784">application yaml files<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e14" source="n4" target="n3">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e15" source="n5" target="n6">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e16" source="n6" target="n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e17" source="n17" target="n18">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e18" source="n15" target="n3">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e19" source="n20" target="n15">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e20" source="n16" target="n6">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e21" source="n19" target="n18">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e22" source="n4" target="n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e23" source="n2" target="n1">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e24" source="n7" target="n17">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e25" source="n6" target="n2">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e26" source="n22" target="n19">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e27" source="n21" target="n16">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e28" source="n14" target="n20">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="36.688228607177734" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.39651489257812" x="4.9553675537108575" xml:space="preserve" y="75.47775874328613">copy to
via scp/rsync<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e29" source="n13" target="n11">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="-438.44499999999994" y="62.251666666666665"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="39.41203308105469" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="30.31201171875" x="-116.9447558593746" xml:space="preserve" y="-49.27187929280607">IPv4
IPv6<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e30" source="n12" target="n11">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="-588.3494999999999" y="-62.498333333333335"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="21.706016540527344" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="54.99903869628906" x="13.953326354981414" xml:space="preserve" y="25.047580271402865">ssh keys<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
doc/meissa-website-arch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -11,161 +11,50 @@
### Modularisierung von Templates ### Modularisierung von Templates
Als Website-Pfleger möchte ich Templates versioniert verwenden können. Damit kann ich entscheiden, wann ich Template Weiterentwicklungen folgen möchte. Als Website-Pfleger möchte ich Templates versioniert verwenden können. Damit kann ich entscheiden, wann ich Template Weiterentwicklungen folgen möchte.
Bei hugo:
wird typischerweise mit git submodule umgesetzt
=> Micha meint, dass dann der Entwicklungsroundtrip um 1 bis 2 Schritte größer wird.
(git pull im submodul ordner & keine sprechende versionsnumme).
Addendum: Es gibt hugo modules, die Versionsverwaltung und Modularisierung einfach machen: https://www.nickgracilla.com/posts/master-hugo-modules-managing-themes-as-modules/
bei cryogen:
modularisierung über jar-file => im kombinierten Entwicklungszyklus wird mit snapshot ein Schritt weniger gebraucht.
## 002 ## 002
### Websitestruktur ist einfach zu lernen ### Websitestruktur ist einfach zu lernen
Die fertige Websitestruktur soll möglichst unkompliziert und somit einfach für den Website-Pfleger zu lernen sein. Die fertige Websitestruktur soll möglichst unkompliziert und somit einfach für den Website-Pfleger zu lernen sein.
These: Damit jemand die Website-Struktur lernen kann, muss klar sein, wie das Theme funktionieert und wir wir unsere Inhalte strukturieren.
Theme: Setzt sich aus der Menge der Layouts, CSS und JS zusammen
Layout: Besteht aus einem oder mehreren Partials und repräsentiert einen Seitentyp im Theme
Partials: sind HTML und Template Elemente, die in Layouts enthalten sein können.
Asset:
Hugo: Erlaubt Trennung von Inhalt und Theme, relativ viel Freiheit bei Folder-Structure (Erik sucht evtl. noch Details raus)
Cryogen: Hat Inhalte und Theme gemischt, Folder sind teilweise fest im Code verdrahtet
Die Doku muss auf Theme-Ebene gemacht werden.
## 003 ## 003
### Responsive Design ### Responsive Design
Als Website-Konsument möchte ich die Website auf Smartphon, Tablet oder auf dem 4K-Sreen konsumieren können. Als Website-Konsument möchte ich die Website auf Smartphon, Tablet oder auf dem 4K-Sreen konsumieren können.
Große Auswahl an Themes bei Hugo, Auswahl bei reinen Bootstrap-themes (noch) unklar.
Zeit in Recherche von Themes investieren (2h) mit Fokus auf Aussehen, interne Struktur und Erweiterbarkeit.
## 004 ## 004
### Inhalte ohne Layout ### Inhalte ohne Layout
Der Website-Pfleger soll Inhalte möglichst einfach verändern können, ohne dass er sich um das Layout kümmern muss (evtl Markdown oder Asciidoc ?). Der Website-Pfleger soll Inhalte möglichst einfach verändern können, ohne dass er sich um das Layout kümmern muss (evtl Markdown oder Asciidoc ?).
Michael und Ansgar wollen Seiten mit Markdown machen können.
Erik findet Markdown auf den LandingPages nicht wichtig und in manchen Fällen kontraproduktiv.
## 005 ## 005
### Website muss keine Inhalte von extern downloaden (müssen) - z.B. Fonts, css etc ### Website muss keine Inhalte von extern downloaden (müssen) - z.B. Fonts, css etc
Als Website-Konsument möchte ich nur der besuchten Seite meine Daten zugestehen - und nicht der gesamten tracking-Welt. Als Website-Konsument möchte ich nur der besuchten Seite meine Daten zugestehen - und nicht der gesamten tracking-Welt.
Geht bei beiden.
## 007 ## 007
### Rückwärtskompatibilität ### Rückwärtskompatibilität
Als Website-Betreiber möchte ich, dass alte Websites (bspw Informatikbüro Jerger) mit c4k-website weiterhin funktionieren, ohne bei ihnen Anpassungen machen zu müssen (wir brauchen eine Wartungsperspektive für 3 weitere bestehende Websites). Als Website-Betreiber möchte ich, dass alte Websites (bspw Informatikbüro Jerger) mit c4k-website weiterhin funktionieren, ohne bei ihnen Anpassungen machen zu müssen. (cluster muss weiterlaufen bei Änderungen)
Todos sind:
* lein als build-aufruf muss einheitlicher werden
* c4k-website müsste evtl. mit einem weitern flavor "hugo" umgehen können
* Migrationspfad für bestehende Websites (Anleitung für Navigation / bisherige Content-Struktur)
* POC: der die wesentlichen Features für dda.io abbildet (Navigation, Migration für Seiten&Bilder, Blog, partials)
## 008 ## 008
### Website soll statisch sein ### Website soll statisch sein
Als Website-Betreiber möchte ich eine statische Website ausliefern, damit der Website Betrieb nicht so komplex wird. Der Website betreiber muss sich zudem bei statischen Websites nicht so sehr um die Sicherheit von Credentials, Assets und ähnliches kümmern. Als Website-Betreiber möchte ich eine statische Website ausliefern, damit der Website Betrieb nicht so komplex wird.
Geht bei beiden.
## 009 ## 009
### Einfache und Zugängliche Technologie ### Einfache und Zugängliche Technologie
a) Die Technologie, die der Theme-Entwickler zum Bau der WS verwendet (mit Technik drumherum), soll gut dokumentiert und zugänglich sein. Damit ist es für den Website-Entwickler einfach, Änderungen im technischen Kontext der Website umzusetzen. Die Technologie, die der Website-Entwickler zum Bau der WS verwendet, soll gut dokumentiert und zugänglich sein. Damit ist es für den Website-Entwickler einfach, Änderungen im technischen Kontext der Website umzusetzen.
Beim Generator stehen da so Fragen an wie:
* Lookup-Order in der Dir-Struktur
* Modul-Frage
* Content Organisation (Page-Bundles bei hugo)
* Debug-Möglichkeiten
Community (Hugo) <-> Wissen in der Firma (Micha hat das Zeugs in Cryogen eingebaut)
(Ausführliche) Dokumentation
b) Die Technologie, die der Content-Entwickler verwendet, soll gut dokumentiert und zugänglich sein. Damit ist es für den Website-Entwickler einfach, Änderungen im Inhalt umzusetzen.
Beim Theme stehen da so Fragen an wie:
* Wie stelle ich navigation her
* Link in texts
* Welche Layouts existieren
* Bilder
* Meta-Infos
* Muss ich Graph Head bedienen?
## 010 ## 010
### Spass bei Entwicklung und Pflege ### Spass bei Entwicklung und Pflege
Website-Entwickler und Website-Pfleger sollten Spaß haben bei Website redesignen und Pflege (techn. und inhaltliche Pflege) Website-Entwickler und Website-Pfleger sollten Spaß haben bei Website redesignen und Pflege (techn. und inhaltliche Pflege)
Spassfaktoren:
* Startgeschwindigkeit des Tools
* Hugo startet schnell, ohne JDK
* Cryogen braucht länger, kann evtl mit GraalVM beschleunigt werden
* Buildzeit
* Hugo kommt and WYSIWYG feeling heran, buildzeit zur Vorschau bei <1s
* Cryogen builds brauchen ca +-10s
* Entwicklungsflow
* Instantane Anzeige von Änderungen auf der Site bei Hugo
* Editor-Freiheit
* Markdown hat schon im Editor ein PreView
* Markdown erlaubt kleine Freiheiten, ohne immer gleich am Theme ändern zu müssen.
* Bild-Nachbearbeitungspipeline
* Mehr Automatisierung bei Bild-Metainfos, Sizing, Dimensionierung
* Spassbremse: Hugo erinnert mich an Helm - und das hat Brechreitzfaktor für Micha
* Hugo wäre Investierung in die Zukunft
* Hugo-Wissen kann auf dem Lebenslauf evtl. nützlich sein.
## 011 ## 011
### Wohlfühlen mit schönem und funktionierendem Design ### Wohlfühlen mit schönem und funktionierendem Design
Der Website-Konsument soll sich bei dem Besuch der WS wohlfühlen und schnell erfassen können worum es geht. Der Website-Konsument soll sich bei dem Besuch der WS wohlfühlen und schnell erfassen können worum es geht.
Geht bei beiden.
## 012 ## 012
### OpenSource ### OpenSource
Der Entwickler und Betreiber möchte Tools und Layouts mit OS-Lizenz, da das unkomplizierter ist. Der Entwickler und Betreiber möchte Tools und Layouts mit OS-Lizenz, da das unkomplizierter ist.
Geht bei beiden.
## 013 ## 013
### Leichtgewichtiger Buildprozess ### Leichtgewichtiger Buildprozess
Die Website-Betreiber sollen keine hohen Kosten beim Betreiben der Website haben. Die Website-Betreiber sollen keine hohen Kosten beim Betreiben der Website haben.
Geht bei beiden.
Cryogen brauch evtl. GraalVM, aber tendenziell sind beide tools gleichwertig in Gewichtigkeit des Bau-Prozesses.
## 015 ## 015
### Template-Eigenschaften sind überschreibbar ### Template-Eigenschaften sind überschreibbar
Als Website-Entwickler möchte ich Template Eigenschaften auf Ebene von Website oder Seite überschreiben können, damit ich kleine Änderungen schnell umsetzen kann. Als Website-Entwickler möchte ich Template Eigenschaften auf Ebene von Website oder Seite überschreiben können, damit ich kleine Änderungen schnell umsetzen kann.
Lokales überschreiben von Themes funktioniert:
Hugo:
* Entweder über go modules
* Oder lokale theme ordner
Cryogen:
* Ebenfalls über lokale theme ordner
## 016
### Unser theme / layout soll privat bleiben können
Als meissa Mitglied wollen wir verhindern, dass jemand einfach unsere Website clonen kann.
Funktioniert bei beiden: Via git repo.
## 017
### URL-Redirects
Als Website-Pfleger möchte ich Redirects definieren können um eingägnge und stabile alternativ URLs zu einer Seite definieren zu können.
Kann cryogen nicht.
Unklar bei Hugo - Aliases?
Resourcen: https://gohugo.io/content-management/urls/
## 018
### Domain-Logik ist in Programiersprache beschreibbar.
Als Theme-Entwickler möchte ich möglichst wenig Logik in Templating haben müssen.

View file

@ -7,7 +7,7 @@ import logging
name = 'c4k-website' name = 'c4k-website'
MODULE = 'build' MODULE = 'build'
PROJECT_ROOT_PATH = '../..' PROJECT_ROOT_PATH = '../..'
version = "2.0.4-dev" version = "1.1.4"
@init @init

View file

@ -3,12 +3,9 @@ FROM clojure:lein
# Prepare Entrypoint Script # Prepare Entrypoint Script
ADD resources /tmp ADD resources /tmp
ENV HUGO_VERSION="0.125.5"
ENV GO_VERSION="1.19.13"
ENV BUILDDIR="/etc/website" ENV BUILDDIR="/etc/website"
ENV SOURCEDIR="/etc/websitesource" ENV SOURCEDIR="/etc/websitesource"
ENV WEBSITEROOT="/var/www/html/website/" ENV WEBSITEROOT="/var/www/html/website/"
ENV HASHFILEDIR="/var/hashfile.d" ENV HASHFILEDIR="/var/hashfile.d"
ENV PATH="${PATH}:/usr/local/go/bin"
RUN /tmp/install.sh RUN /tmp/install.sh

View file

@ -20,13 +20,17 @@ if [[ $currentHash == $newHash ]]
echo "Nothing to do" echo "Nothing to do"
else else
echo $currentHash > $HASHFILEDIR/$hashfilename echo $currentHash > $HASHFILEDIR/$hashfilename
echo "Generate .netrc file"
generate-netrc-file
echo "Downloading website data" echo "Downloading website data"
get-website-data $filename get-website-data $filename
unzip-website-data $filename unzip-website-data $filename
echo "Executing Custom Scripts, if applicable"
execute-scripts-when-existing
echo "Building website" echo "Building website"
build-website build-website
echo "Moving files" echo "Moving files"
move-website-files-to-target move-website-files-to-target
fi fi

View file

@ -1,9 +1,5 @@
#!/bin/bash #!/bin/bash
function generate-netrc-file() {
echo "machine $GITHOST password $AUTHTOKEN" > ~/.netrc
}
function get-website-data() { function get-website-data() {
curl -H "Authorization: token $AUTHTOKEN" -o $SOURCEDIR/$1 $GITREPOURL curl -H "Authorization: token $AUTHTOKEN" -o $SOURCEDIR/$1 $GITREPOURL
} }
@ -20,50 +16,30 @@ function unzip-website-data() {
unzip $SOURCEDIR/$1 -d $BUILDDIR unzip $SOURCEDIR/$1 -d $BUILDDIR
} }
function execute-scripts-when-existing() {
websitedir=$(ls $BUILDDIR)
if [[ -f $BUILDDIR/$websitedir/$SCRIPTFILE ]]
then
checksum="$(sha256sum $BUILDDIR/$websitedir/$SCRIPTFILE | grep -oE "^[a-z0-9]+")"
if [[ "$SHA256SUM" == "$checksum" ]]
then
chmod +x $BUILDDIR/$websitedir/$SCRIPTFILE
(cd $BUILDDIR; dir=$(ls); cd $dir; ./$SCRIPTFILE) #make sure paths defined in scriptfile are relative to $dir
else
printf "Provided SHA256 Sum does not match calculated sum. Exiting."
printf "Calculated SHA256: $checksum"
printf "Given SHA256: $SHA256SUM"
exit 1
fi
else
printf "No script file provided."
fi
}
function build-website() { function build-website() {
(cd $BUILDDIR; dir=$(ls); cd $dir; bash generate.sh;) (cd $BUILDDIR; dir=$(ls); cd $dir; lein run;)
} }
function move-website-files-to-target() { function move-website-files-to-target() {
(cd $BUILDDIR; dir=$(ls); cd $dir; rsync -ru --exclude-from "/etc/exclude.pattern" --delete target/html/* $WEBSITEROOT;) (cd $BUILDDIR; dir=$(ls); cd $dir; rsync -ru --exclude-from "/etc/exclude.pattern" --delete resources/public/* $WEBSITEROOT;)
} }
function install-hugo-from-deb() {
curl -L "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb" -o hugo_extended_${HUGO_VERSION}_linux-amd64.deb
curl -L "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_checksums.txt" -o checksums.txt
EXPECTED_CHECKSUM="$(sha256sum hugo_extended_${HUGO_VERSION}_linux-amd64.deb)"
ACTUAL_CHECKSUM="$(grep hugo_extended_${HUGO_VERSION}_linux-amd64.deb checksums.txt)"
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]
then
>&2 echo 'ERROR: Invalid installer checksum'
rm hugo.deb
exit 1
fi
echo "Installing hugo"
echo
dpkg -i hugo_extended_${HUGO_VERSION}_linux-amd64.deb
echo "Clean up"
rm hugo_extended_${HUGO_VERSION}_linux-amd64.deb
rm checksums.txt
}
function install-go-from-tar() {
curl -L "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o go_linux-amd64.tar.gz
EXPECTED_CHECKSUM="4643d4c29c55f53fa0349367d7f1bb5ca554ea6ef528c146825b0f8464e2e668 go_linux-amd64.tar.gz"
ACTUAL_CHECKSUM="$(sha256sum go_linux-amd64.tar.gz)"
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]
then
>&2 echo 'ERROR: Invalid installer checksum'
rm go_linux-amd64.tar.gz
exit 1
fi
echo "Installing go"
echo
tar -C /usr/local -xzf go_linux-amd64.tar.gz
echo "Clean up"
rm go_linux-amd64.tar.gz
}

View file

@ -1,28 +1,14 @@
#!/bin/bash #!/bin/bash
set -exo pipefail apt update > /dev/null;
function main() apt install -y unzip rsync jq imagemagick
{
{
upgradeSystem
apt-get install -qqy unzip rsync jq imagemagick curl git
install-hugo-from-deb
install-go-from-tar
install -d /etc/lein/ mkdir /etc/lein/
install -m 0700 /tmp/entrypoint.sh /
install -m 0700 /tmp/functions.sh /usr/local/bin/
install -m 0700 /tmp/exclude.pattern /etc/
install -m 0700 /tmp/project.clj /etc/lein/
cd /etc/lein install -m 0700 /tmp/entrypoint.sh /
lein deps install -m 0700 /tmp/functions.sh /usr/local/bin/
install -m 0700 /tmp/exclude.pattern /etc/
cleanupDocker install -m 0700 /tmp/project.clj /etc/lein/
} > /dev/null cd /etc/lein;
} lein deps;
source /tmp/install_functions_debian.sh
source /tmp/functions.sh
DEBIAN_FRONTEND=noninteractive DEBCONF_NOWARNINGS=yes main

View file

@ -0,0 +1,12 @@
FROM c4k-website-build
RUN apt update
RUN apt -yqq --no-install-recommends --yes install curl default-jre-headless
# TODO: path does not exist
RUN curl -L -o /tmp/serverspec.jar \
https://github.com/DomainDrivenArchitecture/dda-serverspec-crate/releases/download/2.0.0/dda-serverspec-standalone.jar
COPY serverspec.edn /tmp/serverspec.edn
RUN java -jar /tmp/serverspec.jar /tmp/serverspec.edn -v

View file

@ -2,7 +2,7 @@
"name": "c4k-website", "name": "c4k-website",
"description": "Generate c4k yaml for a website deployment.", "description": "Generate c4k yaml for a website deployment.",
"author": "meissa GmbH", "author": "meissa GmbH",
"version": "2.0.4-SNAPSHOT", "version": "1.1.4",
"homepage": "https://gitlab.com/domaindrivenarchitecture/c4k-website#readme", "homepage": "https://gitlab.com/domaindrivenarchitecture/c4k-website#readme",
"repository": "https://www.npmjs.com/package/c4k-website", "repository": "https://www.npmjs.com/package/c4k-website",
"license": "APACHE2", "license": "APACHE2",
@ -27,7 +27,7 @@
"js-yaml": "^4.0.0" "js-yaml": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"shadow-cljs": "^2.27.4", "shadow-cljs": "^2.11.18",
"source-map-support": "^0.5.21" "source-map-support": "^0.5.19"
} }
} }

View file

@ -1,11 +1,11 @@
(defproject org.domaindrivenarchitecture/c4k-website "2.0.4-SNAPSHOT" (defproject org.domaindrivenarchitecture/c4k-website "1.1.4"
:description "website c4k-installation package" :description "website c4k-installation package"
:url "https://domaindrivenarchitecture.org" :url "https://domaindrivenarchitecture.org"
:license {:name "Apache License, Version 2.0" :license {:name "Apache License, Version 2.0"
:url "https://www.apache.org/licenses/LICENSE-2.0.html"} :url "https://www.apache.org/licenses/LICENSE-2.0.html"}
:dependencies [[org.clojure/clojure "1.11.3"] :dependencies [[org.clojure/clojure "1.11.1"]
[org.clojure/tools.reader "1.4.2"] [org.clojure/tools.reader "1.3.6"]
[org.domaindrivenarchitecture/c4k-common-clj "6.2.3"] [org.domaindrivenarchitecture/c4k-common-clj "6.0.3"]
[hickory "0.7.1" :exclusions [viebel/codox-klipse-theme]]] [hickory "0.7.1" :exclusions [viebel/codox-klipse-theme]]]
:target-path "target/%s/" :target-path "target/%s/"
:source-paths ["src/main/cljc" :source-paths ["src/main/cljc"
@ -22,15 +22,25 @@
:uberjar {:aot :all :uberjar {:aot :all
:main dda.c4k-website.uberjar :main dda.c4k-website.uberjar
:uberjar-name "c4k-website-standalone.jar" :uberjar-name "c4k-website-standalone.jar"
:dependencies [[org.clojure/tools.cli "1.1.230"] :dependencies [[org.clojure/tools.cli "1.0.219"]
[ch.qos.logback/logback-classic "1.5.6" [ch.qos.logback/logback-classic "1.4.11"
:exclusions [com.sun.mail/javax.mail]] :exclusions [com.sun.mail/javax.mail]]
[org.slf4j/jcl-over-slf4j "2.0.13"] [org.slf4j/jcl-over-slf4j "2.0.9"]]}}
[com.github.clj-easy/graal-build-time "1.0.5"]]}}
:release-tasks [["test"] :release-tasks [["test"]
["vcs" "assert-committed"] ["vcs" "assert-committed"]
["change" "version" "leiningen.release/bump-version" "release"] ["change" "version" "leiningen.release/bump-version" "release"]
["vcs" "commit"] ["vcs" "commit"]
["vcs" "tag" "v" "--no-sign"] ["vcs" "tag" "v" "--no-sign"]
["change" "version" "leiningen.release/bump-version"]] ["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"]})

View file

@ -4,7 +4,7 @@
"src/test/cljc" "src/test/cljc"
"src/test/cljs" "src/test/cljs"
"src/test/resources"] "src/test/resources"]
:dependencies [[org.domaindrivenarchitecture/c4k-common-cljs "6.2.3"] :dependencies [[org.domaindrivenarchitecture/c4k-common-cljs "6.0.1"]
[hickory "0.7.1"]] [hickory "0.7.1"]]
:builds {:frontend {:target :browser :builds {:frontend {:target :browser
:modules {:main {:init-fn dda.c4k-website.browser/init}} :modules {:main {:init-fn dda.c4k-website.browser/init}}

View file

@ -7,152 +7,80 @@
[dda.c4k-common.common :as cm] [dda.c4k-common.common :as cm]
[dda.c4k-common.predicate :as cp] [dda.c4k-common.predicate :as cp]
[dda.c4k-common.monitoring :as mon] [dda.c4k-common.monitoring :as mon]
[dda.c4k-common.namespace :as ns] [dda.c4k-website.website :as website]))
[dda.c4k-common.ingress :as ing]
[dda.c4k-website.website :as web])) (def config-defaults {:issuer "staging"
:volume-size "3"})
(s/def ::mon-cfg ::mon/mon-cfg) (s/def ::mon-cfg ::mon/mon-cfg)
(s/def ::mon-auth ::mon/mon-auth) (s/def ::mon-auth ::mon/mon-auth)
(s/def ::unique-name ::web/unique-name)
(s/def ::issuer ::web/issuer)
(s/def ::volume-size ::web/volume-size)
(s/def ::average-rate ::ing/average-rate)
(s/def ::burst-rate ::ing/burst-rate)
(s/def ::authtoken ::web/authtoken) (def config? (s/keys :req-un [::website/websites]
(s/def ::fqdns ::web/fqdns) :opt-un [::website/issuer
(s/def ::forgejo-host ::web/forgejo-host) ::website/volume-size
(s/def ::repo-owner ::web/repo-owner) ::mon-cfg]))
(s/def ::repo-name ::web/repo-name)
(s/def ::branchname ::web/branchname)
(s/def ::build-cpu-request ::web/build-cpu-request)
(s/def ::build-memory-request ::web/build-memory-request)
(s/def ::build-cpu-limit ::web/build-cpu-limit)
(s/def ::build-memory-limit ::web/build-memory-limit)
(s/def ::redirects ::web/redirects)
(def websiteconfig? (s/keys :req-un [::unique-name (def auth? (s/keys :req-un [::website/auth]
::fqdns
::forgejo-host
::repo-owner
::repo-name
::branchname]
:opt-un [::issuer
::volume-size
::build-cpu-request
::build-cpu-limit
::build-memory-request
::build-memory-limit
::redirects]))
(def websiteauth? web/websiteauth?)
(def websiteauths? (s/keys :req-un [::websiteauths]))
(s/def ::websiteconfigs (s/coll-of websiteconfig?))
(s/def ::websiteauths (s/coll-of websiteauth?))
(def config? (s/keys :req-un [::websiteconfigs]
:opt-un [::issuer
::volume-size
::mon-cfg
::average-rate
::burst-rate]))
(def auth? (s/keys :req-un [::websiteauths]
:opt-un [::mon-auth])) :opt-un [::mon-auth]))
(def config-defaults {:issuer "staging"}) (defn-spec sort-config cp/map-or-seq?
(def website-config-defaults {:build-cpu-request "500m"
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:redirects []
:average-rate 20
:burst-rate 40})
(defn-spec sort-config map?
[unsorted-config config?] [unsorted-config config?]
(let [sorted-websiteconfigs (into [] (sort-by :unique-name (unsorted-config :websiteconfigs)))] (let [sorted-websites (into [] (sort-by :unique-name (unsorted-config :websites)))]
(-> unsorted-config (-> unsorted-config
(assoc-in [:websiteconfigs] sorted-websiteconfigs)))) (assoc-in [:websites] sorted-websites))))
(defn-spec sort-auth map? (defn-spec sort-auth cp/map-or-seq?
[unsorted-auth auth?] [unsorted-auth auth?]
(let [sorted-auth (into [] (sort-by :unique-name (unsorted-auth :websiteauths)))] (let [sorted-auth (into [] (sort-by :unique-name (unsorted-auth :auth)))]
(-> unsorted-auth (-> unsorted-auth
(assoc-in [:websiteauths] sorted-auth)))) (assoc-in [:auth] sorted-auth))))
(defn-spec flatten-and-reduce-config map? (defn-spec flatten-and-reduce-config cp/map-or-seq?
[config config?] [config config?]
(let (let
[first-entry (first (:websiteconfigs config))] [first-entry (first (:websites config))]
(conj first-entry (conj first-entry
(when (contains? config :issuer) (when (contains? config :issuer)
{:issuer (config :issuer)}) {:issuer (config :issuer)})
(when (contains? config :volume-size) (when (contains? config :volume-size)
{:volume-size (config :volume-size)}) {:volume-size (config :volume-size)}))))
(when (contains? config :average-rate)
{:average-rate (config :average-rate)})
(when (contains? config :burst-rate)
{:burst-rate (config :burst-rate)}))))
(defn-spec flatten-and-reduce-auth map? (defn-spec flatten-and-reduce-auth cp/map-or-seq?
[auth auth?] [auth auth?]
(-> auth :websiteauths first)) (-> auth :auth first))
(defn-spec generate-ingress seq? (defn generate-configs [config auth]
[config websiteconfig?] (loop [config (sort-config config)
(let [name (web/replace-dots-by-minus (:unique-name config)) auth (sort-auth auth)
final-config (merge website-config-defaults
{:service-name name
:service-port 80
:namespace name}
config)]
(ing/generate-simple-ingress final-config)))
(defn-spec generate seq?
[config config?
auth auth?]
(loop [sorted-config (sort-config config)
sorted-auth (sort-auth auth)
result []] result []]
(if (and (empty? (sorted-config :websiteconfigs)) (empty? (sorted-auth :websiteauths))) (if (and (empty? (config :websites)) (empty? (auth :auth)))
result result
(recur (-> (recur (->
sorted-config config
(assoc-in [:websiteconfigs] (rest (sorted-config :websiteconfigs)))) (assoc-in [:websites] (rest (config :websites))))
(-> (->
sorted-auth auth
(assoc-in [:websiteauths] (rest (sorted-auth :websiteauths)))) (assoc-in [:auth] (rest (auth :auth))))
(let [curr-flat-websiteconfig (conj result
(merge (website/generate-nginx-deployment (flatten-and-reduce-config config))
website-config-defaults (website/generate-nginx-configmap (flatten-and-reduce-config config))
(flatten-and-reduce-config sorted-config)) (website/generate-nginx-service (flatten-and-reduce-config config))
name (web/replace-dots-by-minus (:unique-name curr-flat-websiteconfig))] (website/generate-website-content-volume (flatten-and-reduce-config config))
(cm/concat-vec (website/generate-hashfile-volume (flatten-and-reduce-config config))
result (website/generate-website-ingress (flatten-and-reduce-config config))
(ns/generate (merge {:namespace name} curr-flat-websiteconfig)) (website/generate-website-certificate (flatten-and-reduce-config config))
[(web/generate-nginx-deployment curr-flat-websiteconfig) (website/generate-website-build-cron (flatten-and-reduce-config config))
(web/generate-nginx-configmap curr-flat-websiteconfig) (website/generate-website-build-secret (flatten-and-reduce-config config) (flatten-and-reduce-auth auth)))))))
(web/generate-nginx-service curr-flat-websiteconfig)
(web/generate-content-pvc curr-flat-websiteconfig)
(web/generate-hash-state-pvc curr-flat-websiteconfig)
(web/generate-build-cron curr-flat-websiteconfig)
(web/generate-build-configmap curr-flat-websiteconfig)
(web/generate-build-secret (flatten-and-reduce-auth sorted-auth))]
(generate-ingress curr-flat-websiteconfig)))))))
(defn-spec k8s-objects cp/map-or-seq? (defn-spec k8s-objects cp/map-or-seq?
[config config? [config config?
auth auth?] auth auth?]
(cm/concat-vec (cm/concat-vec
(map yaml/to-string (map yaml/to-string
(filter (filter
#(not (nil? %)) #(not (nil? %))
(cm/concat-vec (cm/concat-vec
(generate config auth) (generate-configs config auth)
(when (:contains? config :mon-cfg) (when (:contains? config :mon-cfg)
(mon/generate (:mon-cfg config) (:mon-auth auth)))))))) (mon/generate (:mon-cfg config) (:mon-auth auth))))))))

View file

@ -1,73 +1,109 @@
(ns dda.c4k-website.website (ns dda.c4k-website.website
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.string :as str] #?(:cljs [shadow.resource :as rc])
#?(:clj [orchestra.core :refer [defn-spec]] #?(:clj [orchestra.core :refer [defn-spec]]
:cljs [orchestra.core :refer-macros [defn-spec]]) :cljs [orchestra.core :refer-macros [defn-spec]])
#?(:cljs [dda.c4k-common.macros :refer-macros [inline-resources]]) #?(:clj [clojure.edn :as edn]
:cljs [cljs.reader :as edn])
[dda.c4k-common.yaml :as yaml] [dda.c4k-common.yaml :as yaml]
[dda.c4k-common.common :as cm] [dda.c4k-common.common :as cm]
[dda.c4k-common.base64 :as b64] [dda.c4k-common.base64 :as b64]
[dda.c4k-common.predicate :as pred])) [dda.c4k-common.predicate :as pred]
[dda.c4k-common.ingress :as ing]
[clojure.string :as str]))
(defn fqdn-list? (defn fqdn-list?
[input] [input]
(every? true? (map pred/fqdn-string? input))) (every? true? (map pred/fqdn-string? input)))
(s/def ::unique-name string?) (s/def ::unique-name string?)
(s/def ::sha256sum-output string?)
(s/def ::issuer pred/letsencrypt-issuer?) (s/def ::issuer pred/letsencrypt-issuer?)
(s/def ::volume-size pred/integer-string?) (s/def ::volume-size pred/integer-string?)
(s/def ::authtoken pred/bash-env-string?) (s/def ::authtoken pred/bash-env-string?)
(s/def ::fqdns (s/coll-of pred/fqdn-string?)) (s/def ::fqdns (s/coll-of pred/fqdn-string?))
(s/def ::forgejo-host pred/fqdn-string?) (s/def ::gitea-host pred/fqdn-string?)
(s/def ::repo-name string?) (s/def ::gitea-repo string?)
(s/def ::branchname string?) (s/def ::branchname string?)
(s/def ::repo-owner string?) (s/def ::username string?)
(s/def ::build-cpu-request string?) (s/def ::build-cpu-request string?)
(s/def ::build-memory-request string?) (s/def ::build-memory-request string?)
(s/def ::build-cpu-limit string?) (s/def ::build-cpu-limit string?)
(s/def ::build-memory-limit string?) (s/def ::build-memory-limit string?)
(s/def ::redirect (s/tuple string? string?))
(s/def ::redirects (s/coll-of ::redirect))
(def websiteconfig? (s/keys :req-un [::unique-name (def websiteconfig? (s/keys :req-un [::unique-name
::fqdns ::fqdns
::forgejo-host ::gitea-host
::repo-owner ::gitea-repo
::repo-name ::branchname]
::branchname :opt-un [::issuer
::volume-size
::sha256sum-output
::build-cpu-request ::build-cpu-request
::build-cpu-limit ::build-cpu-limit
::build-memory-request ::build-memory-request
::build-memory-limit ::build-memory-limit]))
::issuer
::volume-size
::redirects]))
(def websiteauth? (s/keys :req-un [::unique-name ::authtoken])) (def websiteauth? (s/keys :req-un [::unique-name ::username ::authtoken]))
(s/def ::websites (s/coll-of websiteconfig?))
(s/def ::auth (s/coll-of websiteauth?))
(def websites? (s/keys :req-un [::websites]))
(def auth? (s/keys :req-un [::auth]))
(defn-spec get-hash-from-sha256sum-output string?
[sha256sum-output string?]
(if (nil? sha256sum-output)
nil
(first (str/split sha256sum-output #"\ +"))))
(defn-spec get-file-name-from-sha256sum-output string?
[sha256sum-output string?]
(if (nil? sha256sum-output)
nil
(second (str/split (str/trim sha256sum-output) #"\ +"))))
(defn-spec replace-dots-by-minus string? (defn-spec replace-dots-by-minus string?
[fqdn pred/fqdn-string?] [fqdn pred/fqdn-string?]
(str/replace fqdn #"\." "-")) (str/replace fqdn #"\." "-"))
(defn-spec generate-app-name string?
[unique-name pred/fqdn-string?]
(str (replace-dots-by-minus unique-name) "-website"))
(defn-spec generate-service-name string?
[unique-name pred/fqdn-string?]
(str (replace-dots-by-minus unique-name) "-service"))
(defn-spec generate-cert-name string?
[unique-name pred/fqdn-string?]
(str (replace-dots-by-minus unique-name) "-cert"))
(defn-spec generate-ingress-name string?
[unique-name pred/fqdn-string?]
(str (replace-dots-by-minus unique-name) "-ingress"))
; https://your.gitea.host/api/v1/repos/<owner>/<repo>/archive/<branch>.zip ; https://your.gitea.host/api/v1/repos/<owner>/<repo>/archive/<branch>.zip
(defn-spec generate-gitrepourl string? (defn-spec generate-gitrepourl string?
[host pred/fqdn-string? [host pred/fqdn-string?
owner string?
repo string? repo string?
user string?
branch string?] branch string?]
(str "https://" host "/api/v1/repos/" owner "/" repo "/archive/" branch ".zip")) (str "https://" host "/api/v1/repos/" user "/" repo "/archive/" branch ".zip"))
; https://your.gitea.host/api/v1/repos/<owner>/<repo>/git/commits/HEAD ; https://your.gitea.host/api/v1/repos/<owner>/<repo>/git/commits/HEAD
(defn-spec generate-gitcommiturl string? (defn-spec generate-gitcommiturl string?
[host pred/fqdn-string? [host pred/fqdn-string?
owner string? repo string?
repo string?] user string?]
(str "https://" host "/api/v1/repos/" owner "/" repo "/git/" "commits/" "HEAD")) (str "https://" host "/api/v1/repos/" user "/" repo "/git/" "commits/" "HEAD"))
(defn-spec replace-all-matching-substrings-beginning-with pred/map-or-seq?
(defn-spec replace-all-matching-prefixes map? [col pred/map-or-seq?
[col map?
value-to-partly-match string? value-to-partly-match string?
value-to-inplace string?] value-to-inplace string?]
(clojure.walk/postwalk #(if (and (= (type value-to-partly-match) (type %)) (clojure.walk/postwalk #(if (and (= (type value-to-partly-match) (type %))
@ -75,128 +111,114 @@
(str/replace % value-to-partly-match value-to-inplace) %) (str/replace % value-to-partly-match value-to-inplace) %)
col)) col))
(defn-spec replace-common-data pred/map-or-seq?
(defn-spec generate-redirects string? [resource-file string?
[config websiteconfig? config websiteconfig?]
indent (s/or :pos pos-int? :zero zero?)] (let [{:keys [unique-name]} config]
(let [{:keys [redirects]} config]
(str/join
(str "\n" (str/join (take indent (repeat " "))))
(map
#(str "rewrite ^" (first %1) "\\$ " (second %1) " permanent;")
redirects))))
(defn-spec generate-nginx-configmap map?
[config websiteconfig?]
(let [{:keys [fqdns unique-name]} config
name (replace-dots-by-minus unique-name)]
(-> (->
(yaml/load-as-edn "website/nginx-configmap.yaml") (yaml/load-as-edn resource-file)
(replace-all-matching-prefixes "NAME" name) (assoc-in [:metadata :labels :app.kubernetes.part-of] (generate-app-name unique-name))
(#(assoc-in % [:data :website.conf] (replace-all-matching-substrings-beginning-with "NAME" (replace-dots-by-minus unique-name)))))
(str/replace
(-> % :data :website.conf)
#"FQDN"
(str (str/join " " fqdns) ";"))))
(#(assoc-in % [:data :website.conf]
(str/replace
(-> % :data :website.conf)
#"REDIRECTS"
(generate-redirects config 2)))))))
(defn-spec generate-build-configmap pred/map-or-seq? (defn-spec replace-build-data pred/map-or-seq?
[config websiteconfig?] [resource-file string?
(let [{:keys [unique-name config websiteconfig?]
forgejo-host (let [{:keys [sha256sum-output build-cpu-request build-cpu-limit build-memory-request build-memory-limit]
repo-owner :or {build-cpu-request "500m" build-cpu-limit "1700m" build-memory-request "256Mi" build-memory-limit "512Mi"}} config]
repo-name
branchname]} config
name (replace-dots-by-minus unique-name)]
(-> (->
(yaml/load-as-edn "website/build-configmap.yaml") (replace-common-data resource-file config)
(replace-all-matching-prefixes "NAME" name) (cm/replace-all-matching-values-by-new-value "CHECK_SUM" (get-hash-from-sha256sum-output sha256sum-output))
(cm/replace-all-matching-values-by-new-value "GITHOST" forgejo-host) (cm/replace-all-matching-values-by-new-value "SCRIPT_FILE" (get-file-name-from-sha256sum-output sha256sum-output))
(cm/replace-all-matching-values-by-new-value "REPOURL" (generate-gitrepourl
forgejo-host
repo-owner
repo-name
branchname))
(cm/replace-all-matching-values-by-new-value "COMMITURL" (generate-gitcommiturl
forgejo-host
repo-owner
repo-name)))))
(defn-spec generate-build-secret pred/map-or-seq?
[auth websiteauth?]
(let [{:keys [unique-name
authtoken]} auth
name (replace-dots-by-minus unique-name)]
(->
(yaml/load-as-edn "website/build-secret.yaml")
(replace-all-matching-prefixes "NAME" name)
(cm/replace-all-matching-values-by-new-value "TOKEN" (b64/encode authtoken)))))
(defn-spec generate-content-pvc map?
[config websiteconfig?]
(let [{:keys [unique-name volume-size]} config
name (replace-dots-by-minus unique-name)]
(->
(yaml/load-as-edn "website/content-pvc.yaml")
(replace-all-matching-prefixes "NAME" name)
(cm/replace-all-matching-values-by-new-value "WEBSITESTORAGESIZE" (str volume-size "Gi")))))
; TODO: Non-Secret-Parts should be config map
(defn-spec generate-hash-state-pvc map?
[config websiteconfig?]
(let [{:keys [unique-name]} config
name (replace-dots-by-minus unique-name)]
(->
(yaml/load-as-edn "website/hash-state-pvc.yaml")
(replace-all-matching-prefixes "NAME" name))))
(defn-spec generate-nginx-deployment map?
[config websiteconfig?]
(let [{:keys [unique-name build-cpu-request build-cpu-limit
build-memory-request build-memory-limit]} config
name (replace-dots-by-minus unique-name)]
(->
(yaml/load-as-edn "website/nginx-deployment.yaml")
(assoc-in [:metadata :namespace] name)
(replace-all-matching-prefixes "NAME" name)
(cm/replace-all-matching-values-by-new-value "BUILD_CPU_REQUEST" build-cpu-request) (cm/replace-all-matching-values-by-new-value "BUILD_CPU_REQUEST" build-cpu-request)
(cm/replace-all-matching-values-by-new-value "BUILD_CPU_LIMIT" build-cpu-limit) (cm/replace-all-matching-values-by-new-value "BUILD_CPU_LIMIT" build-cpu-limit)
(cm/replace-all-matching-values-by-new-value "BUILD_MEMORY_REQUEST" build-memory-request) (cm/replace-all-matching-values-by-new-value "BUILD_MEMORY_REQUEST" build-memory-request)
(cm/replace-all-matching-values-by-new-value "BUILD_MEMORY_LIMIT" build-memory-limit)))) (cm/replace-all-matching-values-by-new-value "BUILD_MEMORY_LIMIT" build-memory-limit))))
(defn-spec generate-build-cron map?
[config websiteconfig?]
(let [{:keys [unique-name build-cpu-request build-cpu-limit build-memory-request
build-memory-limit]} config
name (replace-dots-by-minus unique-name)]
(->
(yaml/load-as-edn "website/build-cron.yaml")
(replace-all-matching-prefixes "NAME" name)
(cm/replace-all-matching-values-by-new-value "BUILD_CPU_REQUEST" build-cpu-request)
(cm/replace-all-matching-values-by-new-value "BUILD_CPU_LIMIT" build-cpu-limit)
(cm/replace-all-matching-values-by-new-value "BUILD_MEMORY_REQUEST" build-memory-request)
(cm/replace-all-matching-values-by-new-value "BUILD_MEMORY_LIMIT" build-memory-limit))))
(defn-spec generate-nginx-service map?
[config websiteconfig?]
(let [{:keys [unique-name]} config
name (replace-dots-by-minus unique-name)]
(->
(yaml/load-as-edn "website/nginx-service.yaml")
(assoc-in [:metadata :namespace] name)
(replace-all-matching-prefixes "NAME" name))))
#?(:cljs #?(:cljs
(defmethod yaml/load-resource :website [resource-name] (defmethod yaml/load-resource :website [resource-name]
(get (inline-resources "website") resource-name))) (case resource-name
"website/nginx-configmap.yaml" (rc/inline "website/nginx-configmap.yaml")
"website/nginx-deployment.yaml" (rc/inline "website/nginx-deployment.yaml")
"website/nginx-service.yaml" (rc/inline "website/nginx-service.yaml")
"website/website-build-cron.yaml" (rc/inline "website/website-build-cron.yaml")
"website/website-build-secret.yaml" (rc/inline "website/website-build-secret.yaml")
"website/website-content-volume.yaml" (rc/inline "website/website-content-volume.yaml")
"website/hashfile-volume.yaml" (rc/inline "website/hashfile-volume.yaml")
(throw (js/Error. "Undefined Resource!")))))
(defn-spec generate-nginx-deployment pred/map-or-seq?
[config websiteconfig?]
(replace-build-data "website/nginx-deployment.yaml" config))
(defn-spec generate-nginx-configmap pred/map-or-seq?
[config websiteconfig?]
(let [{:keys [fqdns]} config]
(->
(replace-common-data "website/nginx-configmap.yaml" config)
(#(assoc-in %
[:data :website.conf]
(str/replace
(-> % :data :website.conf) #"FQDN" (str (str/join " " fqdns) ";")))))))
(defn-spec generate-nginx-service pred/map-or-seq?
[config websiteconfig?]
(replace-common-data "website/nginx-service.yaml" config))
(defn-spec generate-website-content-volume pred/map-or-seq?
[config websiteconfig?]
(let [{:keys [volume-size]
:or {volume-size "3"}} config]
(->
(replace-common-data "website/website-content-volume.yaml" config)
(cm/replace-all-matching-values-by-new-value "WEBSITESTORAGESIZE" (str volume-size "Gi")))))
(defn-spec generate-hashfile-volume pred/map-or-seq?
[config websiteconfig?]
(replace-common-data "website/hashfile-volume.yaml" config))
(defn-spec generate-website-ingress pred/map-or-seq?
[config websiteconfig?]
(let [{:keys [unique-name fqdns]} config]
(ing/generate-ingress {:fqdns fqdns
:app-name (generate-app-name unique-name)
:ingress-name (generate-ingress-name unique-name)
:service-name (generate-service-name unique-name)
:service-port 80})))
(defn-spec generate-website-certificate pred/map-or-seq?
[config websiteconfig?]
(let [{:keys [unique-name issuer fqdns]
:or {issuer "staging"}} config]
(ing/generate-certificate {:fqdns fqdns
:app-name (generate-app-name unique-name)
:cert-name (generate-cert-name unique-name)
:issuer issuer})))
(defn-spec generate-website-build-cron pred/map-or-seq?
[config websiteconfig?]
(replace-build-data "website/website-build-cron.yaml" config))
(defn-spec generate-website-build-secret pred/map-or-seq?
[config websiteconfig?
auth websiteauth?]
(let [{:keys [gitea-host
gitea-repo
branchname]} config
{:keys [authtoken
username]} auth]
(->
(replace-common-data "website/website-build-secret.yaml" config)
(cm/replace-all-matching-values-by-new-value "TOKEN" (b64/encode authtoken))
(cm/replace-all-matching-values-by-new-value "REPOURL" (b64/encode
(generate-gitrepourl
gitea-host
gitea-repo
username
branchname)))
(cm/replace-all-matching-values-by-new-value "COMMITURL" (b64/encode
(generate-gitcommiturl
gitea-host
gitea-repo
username))))))

View file

@ -24,20 +24,18 @@
(br/generate-group (br/generate-group
"website-data" "website-data"
(br/generate-text-area (br/generate-text-area
"websiteconfigs" "Contains fqdns, repo infos, an optional sha256sum-output for script execution for each website:" "websites" "Contains fqdns, repo infos, an optional sha256sum-output for script execution for each website:"
"{ :websiteconfigs "{ :websites
[{:unique-name \"test.io\", [{:unique-name \"test.io\",
:fqdns [\"test.de\" \"www.test.de\"], :fqdns [\"test.de\" \"www.test.de\"],
:forgejo-host \"githost.de\", :gitea-host \"githost.de\",
:repo-owner \"someuser\", :gitea-repo \"repo\",
:repo-name \"repo\",
:branchname \"main\", :branchname \"main\",
:sha256sum-output \"123456789ab123cd345de script-file-name.sh\"} :sha256sum-output \"123456789ab123cd345de script-file-name.sh\"}
{:unique-name \"example.io \", {:unique-name \"example.io \",
:fqdns [\"example.org\" \"www.example.org\"], :fqdns [\"example.org\" \"www.example.org\"],
:forgejo-host \"githost.org\", :gitea-host \"githost.org\",
:repo-owner \"someuser\", :gitea-repo \"repo\",
:repo-name \"repo\",
:branchname \"main\", :branchname \"main\",
:build-cpu-request \"1500m\", :build-cpu-request \"1500m\",
:build-cpu-limit \"3000m\", :build-cpu-limit \"3000m\",
@ -51,10 +49,12 @@
"{:mon-auth "{:mon-auth
{:grafana-cloud-user \"your-user-id\" {:grafana-cloud-user \"your-user-id\"
:grafana-cloud-password \"your-cloud-password\"} :grafana-cloud-password \"your-cloud-password\"}
:websiteauths :auth
[{:unique-name \"test.io\", [{:unique-name \"test.io\",
:username \"someuser\",
:authtoken \"abedjgbasdodj\"} :authtoken \"abedjgbasdodj\"}
{:unique-name \"example.io\", {:unique-name \"example.io\",
:username \"someuser\",
:authtoken \"abedjgbasdodj\"}]}" :authtoken \"abedjgbasdodj\"}]}"
"7")) "7"))
[(br/generate-br)] [(br/generate-br)]
@ -70,12 +70,12 @@
(defn config-from-document [] (defn config-from-document []
(let [issuer (br/get-content-from-element "issuer" :optional true) (let [issuer (br/get-content-from-element "issuer" :optional true)
websiteconfigs (br/get-content-from-element "websiteconfigs" :deserializer edn/read-string) websites (br/get-content-from-element "websites" :deserializer edn/read-string)
mon-cluster-name (br/get-content-from-element "mon-cluster-name" :optional true) mon-cluster-name (br/get-content-from-element "mon-cluster-name" :optional true)
mon-cluster-stage (br/get-content-from-element "mon-cluster-stage" :optional true) mon-cluster-stage (br/get-content-from-element "mon-cluster-stage" :optional true)
mon-cloud-url (br/get-content-from-element "mon-cloud-url" :optional true)] mon-cloud-url (br/get-content-from-element "mon-cloud-url" :optional true)]
(merge (merge
{:websiteconfigs websiteconfigs} {:websites websites}
(when (not (st/blank? issuer)) (when (not (st/blank? issuer))
{:issuer issuer}) {:issuer issuer})
(when (some? mon-cluster-name) (when (some? mon-cluster-name)
@ -84,12 +84,12 @@
:grafana-cloud-url mon-cloud-url}})))) :grafana-cloud-url mon-cloud-url}}))))
(defn validate-all! [] (defn validate-all! []
(br/validate! "websiteconfigs" core/websiteconfigs? :deserializer edn/read-string) (br/validate! "websites" website/websites? :deserializer edn/read-string)
(br/validate! "issuer" ::core/issuer :optional true) (br/validate! "issuer" ::website/issuer :optional true)
(br/validate! "mon-cluster-name" ::mon/cluster-name :optional true) (br/validate! "mon-cluster-name" ::mon/cluster-name :optional true)
(br/validate! "mon-cluster-stage" ::mon/cluster-stage :optional true) (br/validate! "mon-cluster-stage" ::mon/cluster-stage :optional true)
(br/validate! "mon-cloud-url" ::mon/grafana-cloud-url :optional true) (br/validate! "mon-cloud-url" ::mon/grafana-cloud-url :optional true)
(br/validate! "auth" core/websiteauths? :deserializer edn/read-string) (br/validate! "auth" website/auth? :deserializer edn/read-string)
(br/set-form-validated!)) (br/set-form-validated!))
(defn add-validate-listener [name] (defn add-validate-listener [name]
@ -108,7 +108,7 @@
core/config-defaults core/config-defaults
core/k8s-objects) core/k8s-objects)
(br/set-output!))))) (br/set-output!)))))
(add-validate-listener "websiteconfigs") (add-validate-listener "websites")
(add-validate-listener "issuer") (add-validate-listener "issuer")
(add-validate-listener "mon-cluster-name") (add-validate-listener "mon-cluster-name")
(add-validate-listener "mon-cluster-stage") (add-validate-listener "mon-cluster-stage")

View file

@ -1,11 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: build-configmap
namespace: NAME
labels:
app.kubernetes.part-of: NAME-website
data:
GITHOST: GITHOST
GITREPOURL: REPOURL
GITCOMMITURL: COMMITURL

View file

@ -1,9 +1,10 @@
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: hash-state-volume name: NAME-hashfile-volume
namespace: NAME namespace: default
labels: labels:
app: NAME-nginx
app.kubernetes.part-of: NAME-website app.kubernetes.part-of: NAME-website
spec: spec:
storageClassName: local-path storageClassName: local-path
@ -12,3 +13,4 @@ spec:
resources: resources:
requests: requests:
storage: 16Mi storage: 16Mi

View file

@ -1,15 +1,15 @@
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: etc-nginx name: NAME-configmap
namespace: NAME namespace: default
labels: labels:
app.kubernetes.part-of: NAME-website app.kubernetes.part-of: NAME-website
data: data:
nginx.conf: | nginx.conf: |
user nginx; user nginx;
worker_processes 3; worker_processes 3;
error_log /var/log/nginx/error.log info; error_log /var/log/nginx/error.log;
pid /var/log/nginx/nginx.pid; pid /var/log/nginx/nginx.pid;
worker_rlimit_nofile 8192; worker_rlimit_nofile 8192;
events { events {
@ -93,6 +93,5 @@ data:
location / { location / {
try_files $uri $uri/ /index.html =404; try_files $uri $uri/ /index.html =404;
} }
# redirects
REDIRECTS
} }

View file

@ -1,76 +1,76 @@
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: nginx name: NAME-deployment
namespace: NAME
labels: labels:
app.kubernetes.part-of: NAME-website app.kubernetes.part-of: NAME-website
spec: spec:
replicas: 1 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: nginx app: NAME-nginx
template: template:
metadata: metadata:
namespace: NAME
labels: labels:
app: nginx app: NAME-nginx
app.kubernetes.part-of: NAME-website
spec: spec:
containers: containers:
- name: nginx - name: NAME-nginx
image: nginx:latest image: nginx:latest
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
ports: ports:
- containerPort: 80 - containerPort: 80
command: [ "/bin/bash", "-c", "nginx -g 'daemon off;'" ] volumeMounts:
volumeMounts: - mountPath: /etc/nginx
- mountPath: /etc/nginx readOnly: true
readOnly: true name: nginx-config-volume
name: etc-nginx - mountPath: /var/log/nginx
- mountPath: /tmp name: log
name: tmp - mountPath: /var/www/html/website
- mountPath: /var/www/html/website name: content-volume
name: content-volume readOnly: true
readOnly: true
initContainers: initContainers:
- image: domaindrivenarchitecture/c4k-website-build - image: domaindrivenarchitecture/c4k-website-build
name: init-build-container name: NAME-init-build-container
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
resources: resources:
requests: requests:
cpu: BUILD_CPU_REQUEST cpu: BUILD_CPU_REQUEST
memory: BUILD_MEMORY_REQUEST memory: BUILD_MEMORY_REQUEST
limits: limits:
cpu: BUILD_CPU_LIMIT cpu: BUILD_CPU_LIMIT
memory: BUILD_MEMORY_LIMIT memory: BUILD_MEMORY_LIMIT
command: ["/entrypoint.sh"] command: ["/entrypoint.sh"]
envFrom: envFrom:
- configMapRef: - secretRef:
name: build-configmap name: NAME-secret
- secretRef: env:
name: build-secret - name: SHA256SUM
volumeMounts: value: CHECK_SUM
- name: content-volume - name: SCRIPTFILE
mountPath: /var/www/html/website value: SCRIPT_FILE
- name: hash-state-volume volumeMounts:
mountPath: /var/hashfile.d - name: content-volume
mountPath: /var/www/html/website
- name: hashfile-volume
mountPath: /var/hashfile.d
volumes: volumes:
- name: etc-nginx - name: nginx-config-volume
configMap: configMap:
name: etc-nginx name: NAME-configmap
items: items:
- key: nginx.conf - key: nginx.conf
path: nginx.conf path: nginx.conf
- key: website.conf - key: website.conf
path: conf.d/website.conf path: conf.d/website.conf
- key: mime.types - key: mime.types
path: mime.types path: mime.types
- name: tmp - name: log
emptyDir: {} emptyDir: {}
- name: content-volume - name: content-volume
persistentVolumeClaim: persistentVolumeClaim:
claimName: content-volume claimName: NAME-content-volume
- name: hash-state-volume - name: hashfile-volume
persistentVolumeClaim: persistentVolumeClaim:
claimName: hash-state-volume claimName: NAME-hashfile-volume

View file

@ -1,14 +1,15 @@
kind: Service kind: Service
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: NAME name: NAME-service
namespace: default
labels: labels:
app: NAME app: NAME-nginx
app.kubernetes.part-of: NAME-website app.kubernetes.part-of: NAME-website
spec: namespace: default
spec:
selector: selector:
app: nginx app: NAME-nginx
ports: ports:
- name: nginx-http - name: nginx-http
port: 80 port: 80

View file

@ -1,8 +1,7 @@
apiVersion: batch/v1 apiVersion: batch/v1
kind: CronJob kind: CronJob
metadata: metadata:
name: build-cron name: NAME-build-cron
namespace: NAME
labels: labels:
app.kubernetes.part-of: NAME-website app.kubernetes.part-of: NAME-website
spec: spec:
@ -11,17 +10,11 @@ spec:
failedJobsHistoryLimit: 1 failedJobsHistoryLimit: 1
jobTemplate: jobTemplate:
spec: spec:
activeDeadlineSeconds: 415
template: template:
metadata:
namespace: NAME
labels:
app: build-cron
app.kubernetes.part-of: NAME-website
spec: spec:
containers: containers:
- image: domaindrivenarchitecture/c4k-website-build - image: domaindrivenarchitecture/c4k-website-build
name: build-cron-container name: NAME-build-app
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
resources: resources:
requests: requests:
@ -32,21 +25,24 @@ spec:
memory: BUILD_MEMORY_LIMIT memory: BUILD_MEMORY_LIMIT
command: ["/entrypoint.sh"] command: ["/entrypoint.sh"]
envFrom: envFrom:
- configMapRef:
name: build-configmap
- secretRef: - secretRef:
name: build-secret name: NAME-secret
env:
- name: SHA256SUM
value: CHECK_SUM
- name: SCRIPTFILE
value: SCRIPT_FILE
volumeMounts: volumeMounts:
- name: content-volume - name: content-volume
mountPath: /var/www/html/website mountPath: /var/www/html/website
- name: hash-state-volume - name: hashfile-volume
mountPath: /var/hashfile.d mountPath: /var/hashfile.d
volumes: volumes:
- name: content-volume - name: content-volume
persistentVolumeClaim: persistentVolumeClaim:
claimName: content-volume claimName: NAME-content-volume
- name: hash-state-volume - name: hashfile-volume
persistentVolumeClaim: persistentVolumeClaim:
claimName: hash-state-volume claimName: NAME-hashfile-volume
restartPolicy: OnFailure restartPolicy: OnFailure

View file

@ -1,9 +1,10 @@
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
name: build-secret name: NAME-secret
namespace: NAME
labels: labels:
app.kubernetes.part-of: NAME-website app.kubernetes.part-of: NAME-website
data: data:
AUTHTOKEN: TOKEN AUTHTOKEN: TOKEN
GITREPOURL: REPOURL
GITCOMMITURL: COMMITURL

View file

@ -1,9 +1,10 @@
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: content-volume name: NAME-content-volume
namespace: NAME namespace: default
labels: labels:
app: NAME-nginx
app.kubernetes.part-of: NAME-website app.kubernetes.part-of: NAME-website
spec: spec:
storageClassName: local-path storageClassName: local-path
@ -12,3 +13,4 @@ spec:
resources: resources:
requests: requests:
storage: WEBSITESTORAGESIZE storage: WEBSITESTORAGESIZE

View file

@ -4,15 +4,9 @@
#?(:clj [clojure.test :refer [deftest is are testing run-tests]] #?(:clj [clojure.test :refer [deftest is are testing run-tests]]
:cljs [cljs.test :refer-macros [deftest is are testing run-tests]]) :cljs [cljs.test :refer-macros [deftest is are testing run-tests]])
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
[dda.c4k-common.yaml :as yaml] [dda.c4k-common.yaml :as yaml]
[dda.c4k-website.core :as cut])) [dda.c4k-website.core :as cut]
[clojure.spec.alpha :as s]))
(st/instrument `cut/sort-config)
(st/instrument `cut/flattened-and-reduced-config)
(st/instrument `cut/flatten-and-reduce-auth)
(st/instrument `cut/generate-ingress)
(st/instrument `cut/generate)
#?(:cljs #?(:cljs
(defmethod yaml/load-resource :website-test [resource-name] (defmethod yaml/load-resource :website-test [resource-name]
@ -26,93 +20,89 @@
(is (s/valid? cut/auth? (yaml/load-as-edn "website-test/valid-auth.yaml")))) (is (s/valid? cut/auth? (yaml/load-as-edn "website-test/valid-auth.yaml"))))
(def websites1 (def websites1
{:websiteconfigs {:websites
[{:unique-name "example.io" [{:unique-name "example.io"
:fqdns ["example.org", "www.example.com"] :fqdns ["example.org", "www.example.com"]
:forgejo-host "finegitehost.net" :gitea-host "finegitehost.net"
:repo-owner "someuser" :gitea-repo "repo"
:repo-name "repo"
:branchname "main"} :branchname "main"}
{:unique-name "test.io" {:unique-name "test.io"
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"] :fqdns ["test.de" "test.org" "www.test.de" "www.test.org"]
:forgejo-host "gitlab.de" :gitea-host "gitlab.de"
:repo-owner "someuser" :gitea-repo "repo"
:repo-name "repo"
:branchname "main"}]}) :branchname "main"}]})
(def websites2 (def websites2
{:websiteconfigs {:websites
[{:unique-name "test.io" [{:unique-name "test.io"
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"] :fqdns ["test.de" "test.org" "www.test.de" "www.test.org"]
:forgejo-host "gitlab.de" :gitea-host "gitlab.de"
:repo-owner "someuser" :gitea-repo "repo"
:repo-name "repo"
:branchname "main"} :branchname "main"}
{:unique-name "example.io" {:unique-name "example.io"
:fqdns ["example.org", "www.example.com"] :fqdns ["example.org", "www.example.com"]
:forgejo-host "finegitehost.net" :gitea-host "finegitehost.net"
:repo-owner "someuser" :gitea-repo "repo"
:repo-name "repo"
:branchname "main"}]}) :branchname "main"}]})
(def auth1 (def auth1
{:websiteauths {:auth
[{:unique-name "example.io" [{:unique-name "example.io"
:username "someuser"
:authtoken "abedjgbasdodj"} :authtoken "abedjgbasdodj"}
{:unique-name "test.io" {:unique-name "test.io"
:username "someuser"
:authtoken "abedjgbasdodj"}]}) :authtoken "abedjgbasdodj"}]})
(def auth2 (def auth2
{:websiteauths {:auth
[{:unique-name "test.io" [{:unique-name "test.io"
:username "someuser"
:authtoken "abedjgbasdodj"} :authtoken "abedjgbasdodj"}
{:unique-name "example.io" {:unique-name "example.io"
:username "someuser"
:authtoken "abedjgbasdodj"}]}) :authtoken "abedjgbasdodj"}]})
(def flattened-and-reduced-config (def flattened-and-reduced-config
{:unique-name "example.io", {:unique-name "example.io",
:fqdns ["example.org" "www.example.com"], :fqdns ["example.org" "www.example.com"],
:forgejo-host "finegitehost.net", :gitea-host "finegitehost.net",
:repo-owner "someuser", :gitea-repo "repo",
:repo-name "repo",
:branchname "main"}) :branchname "main"})
(def flattened-and-reduced-auth (def flattened-and-reduced-auth
{:unique-name "example.io", {:unique-name "example.io",
:username "someuser",
:authtoken "abedjgbasdodj"}) :authtoken "abedjgbasdodj"})
(deftest sorts-config (deftest sorts-config
(is (= {:issuer "staging", (is (= {:issuer "staging",
:websiteconfigs :websites
[{:unique-name "example.io", [{:unique-name "example.io",
:fqdns ["example.org" "www.example.com"], :fqdns ["example.org" "www.example.com"],
:forgejo-host "finegitehost.net", :gitea-host "finegitehost.net",
:repo-owner "someuser", :gitea-repo "repo",
:repo-name "repo",
:branchname "main"}, :branchname "main"},
{:unique-name "test.io", {:unique-name "test.io",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"], :fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:forgejo-host "gitlab.de", :gitea-host "gitlab.de",
:repo-owner "someuser", :gitea-repo "repo",
:repo-name "repo",
:branchname "main", :branchname "main",
:sha256sum-output "123456789ab123cd345de script-file-name.sh"}], :sha256sum-output "123456789ab123cd345de script-file-name.sh"}],
:mon-cfg {:grafana-cloud-url "url-for-your-prom-remote-write-endpoint", :cluster-name "jitsi", :cluster-stage "test"}} :mon-cfg {:grafana-cloud-url "url-for-your-prom-remote-write-endpoint", :cluster-name "jitsi", :cluster-stage "test"}}
(cut/sort-config (cut/sort-config
{:issuer "staging", {:issuer "staging",
:websiteconfigs :websites
[{:unique-name "test.io", [{:unique-name "test.io",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"], :fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:forgejo-host "gitlab.de", :gitea-host "gitlab.de",
:repo-owner "someuser", :gitea-repo "repo",
:repo-name "repo",
:branchname "main", :branchname "main",
:sha256sum-output "123456789ab123cd345de script-file-name.sh"} :sha256sum-output "123456789ab123cd345de script-file-name.sh"}
{:unique-name "example.io", {:unique-name "example.io",
:fqdns ["example.org" "www.example.com"], :fqdns ["example.org" "www.example.com"],
:forgejo-host "finegitehost.net", :gitea-host "finegitehost.net",
:repo-owner "someuser", :gitea-repo "repo",
:repo-name "repo",
:branchname "main"}], :branchname "main"}],
:mon-cfg {:grafana-cloud-url "url-for-your-prom-remote-write-endpoint", :cluster-name "jitsi", :cluster-stage "test"}})))) :mon-cfg {:grafana-cloud-url "url-for-your-prom-remote-write-endpoint", :cluster-name "jitsi", :cluster-stage "test"}}))))
@ -129,68 +119,3 @@
(cut/flatten-and-reduce-auth (cut/sort-auth auth1)))) (cut/flatten-and-reduce-auth (cut/sort-auth auth1))))
(is (= flattened-and-reduced-auth (is (= flattened-and-reduced-auth
(cut/flatten-and-reduce-auth (cut/sort-auth auth2))))) (cut/flatten-and-reduce-auth (cut/sort-auth auth2)))))
(deftest test-generate
(is (= 24
(count (cut/generate
(yaml/load-as-edn "website-test/valid-config.yaml")
(yaml/load-as-edn "website-test/valid-auth.yaml"))))))
(deftest should-generate-ingress
(is (= [{:host "test.de",
:http
{:paths
[{:pathType "Prefix",
:path "/",
:backend {:service {:name "test-io", :port {:number 80}}}}]}}
{:host "test.org",
:http
{:paths
[{:pathType "Prefix",
:path "/",
:backend {:service {:name "test-io", :port {:number 80}}}}]}}
{:host "www.test.de",
:http
{:paths
[{:pathType "Prefix",
:path "/",
:backend {:service {:name "test-io", :port {:number 80}}}}]}}
{:host "www.test.org",
:http
{:paths
[{:pathType "Prefix",
:path "/",
:backend {:service {:name "test-io", :port {:number 80}}}}]}}]
(get-in
(cut/generate-ingress {:unique-name "test.io",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:forgejo-host "gitlab.de",
:repo-owner "someuser",
:repo-name "repo",
:sha256sum-output "123456789ab123cd345de script-file-name.sh",
:issuer "staging",
:branchname "main",
:build-cpu-request "500m"
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:redirects []})
[2 :spec :rules])))
(is (= "test-io"
(get-in
(cut/generate-ingress {:unique-name "test.io",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:forgejo-host "gitlab.de",
:repo-owner "someuser",
:repo-name "repo",
:sha256sum-output "123456789ab123cd345de script-file-name.sh",
:issuer "staging",
:branchname "main",
:build-cpu-request "500m"
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:redirects []})
[2 :metadata :namespace]))))

View file

@ -1,316 +1,223 @@
(ns dda.c4k-website.website-test (ns dda.c4k-website.website-test
(:require (:require
[clojure.string :as str]
#?(:clj [clojure.test :refer [deftest is are testing run-tests]] #?(:clj [clojure.test :refer [deftest is are testing run-tests]]
:cljs [cljs.test :refer-macros [deftest is are testing run-tests]]) :cljs [cljs.test :refer-macros [deftest is are testing run-tests]])
[clojure.spec.test.alpha :as st] [clojure.spec.test.alpha :as st]
[dda.c4k-website.website :as cut])) [dda.c4k-common.test-helper :as th]
[dda.c4k-common.base64 :as b64]
[dda.c4k-website.website :as cut]
[clojure.spec.alpha :as s]))
(st/instrument `cut/replace-dots-by-minus)
(st/instrument `cut/generate-gitrepourl)
(st/instrument `cut/generate-gitcommiturl)
(st/instrument `cut/replace-all-matching-prefixes)
(st/instrument `cut/generate-redirects)
(st/instrument `cut/generate-nginx-configmap) (st/instrument `cut/generate-nginx-configmap)
(st/instrument `cut/generate-build-secret) (st/instrument `cut/generate-nginx-deployment)
(st/instrument `cut/generate-content-pvc)
(st/instrument `cut/generate-hash-state-pvc)
(st/instrument `cut/generate-build-cron)
(st/instrument `cut/generate-nginx-service) (st/instrument `cut/generate-nginx-service)
(st/instrument `cut/generate-website-content-volume)
(st/instrument `cut/generate-hashfile-volume)
(st/instrument `cut/generate-website-ingress)
(st/instrument `cut/generate-website-certificate)
(st/instrument `cut/generate-website-build-cron)
(st/instrument `cut/generate-website-build-secret)
(deftest should-generate-gitrepourl
(is (= "https://mygit.de/api/v1/repos/someuser/repo/archive/main.zip"
(cut/generate-gitrepourl "mygit.de" "someuser" "repo" "main"))))
(deftest should-generate-gitcommiturl
(is (= "https://mygit.de/api/v1/repos/someuser/repo/git/commits/HEAD"
(cut/generate-gitcommiturl "mygit.de" "someuser" "repo"))))
(deftest should-generate-redirects
(is (= "rewrite ^/products.html\\$ /offer.html permanent;\n rewrite ^/one-more\\$ /redirect permanent;"
(cut/generate-redirects {:issuer "staging"
:build-cpu-request "500m"
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:unique-name "test.io",
:redirects [["/products.html", "/offer.html"]
["/one-more", "/redirect"]]
:forgejo-host "gitea.evilorg",
:repo-owner "someuser",
:repo-name "none",
:branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}
2)))
(is (= ""
(cut/generate-redirects {:issuer "staging"
:build-cpu-request "500m"
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:unique-name "test.io",
:redirects []
:forgejo-host "gitea.evilorg",
:repo-owner "someuser",
:repo-name "none",
:branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}
0))))
(deftest should-generate-resource-requests
(is (= {:requests {:cpu "1500m", :memory "512Mi"}, :limits {:cpu "3000m", :memory "1024Mi"}}
(-> (cut/generate-nginx-deployment {:forgejo-host "gitlab.de",
:repo-owner "someuser",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:repo-name "repo",
:issuer "staging",
:branchname "main",
:unique-name "test.io",
:redirects [],
:build-cpu-request "1500m"
:build-cpu-limit "3000m"
:build-memory-request "512Mi"
:build-memory-limit "1024Mi"
:volume-size 3})
:spec :template :spec :initContainers first :resources)))
(is (= "test-io"
(-> (cut/generate-nginx-deployment {:forgejo-host "gitlab.de",
:repo-owner "someuser",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:repo-name "repo",
:issuer "staging",
:branchname "main",
:unique-name "test.io",
:redirects [],
:build-cpu-request "1500m"
:build-cpu-limit "3000m"
:build-memory-request "512Mi"
:build-memory-limit "1024Mi"
:volume-size 3})
:metadata :namespace))))
(deftest should-generate-nginx-configmap-website (deftest should-generate-nginx-configmap-website
(is (str/includes? (is (= "server {\n listen 80 default_server;\n listen [::]:80 default_server;\n server_name test.de www.test.de test-it.de www.test-it.de;\n add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload'; \n add_header X-Frame-Options \"SAMEORIGIN\";\n add_header X-Content-Type-Options nosniff;\n add_header Referrer-Policy \"strict-origin\";\n # add_header Permissions-Policy \"permissions here\";\n root /var/www/html/website/;\n index index.html;\n location / {\n try_files $uri $uri/ /index.html =404;\n }\n}\n"
(:website.conf (:data (cut/generate-nginx-configmap {:issuer "staging" (:website.conf (:data (cut/generate-nginx-configmap {:unique-name "test.io",
:build-cpu-request "500m" :gitea-host "gitea.evilorg",
:build-cpu-limit "1700m" :gitea-repo "none",
:build-memory-request "256Mi" :branchname "mablain",
:build-memory-limit "512Mi" :fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]})))))
:volume-size "3" (is (= "types {\n text/html html htm shtml;\n text/css css;\n text/xml xml rss;\n image/gif gif;\n image/jpeg jpeg jpg;\n application/x-javascript js;\n text/plain txt;\n text/x-component htc;\n text/mathml mml;\n image/svg+xml svg svgz;\n image/png png;\n image/x-icon ico;\n image/x-jng jng;\n image/vnd.wap.wbmp wbmp;\n application/java-archive jar war ear;\n application/mac-binhex40 hqx;\n application/pdf pdf;\n application/x-cocoa cco;\n application/x-java-archive-diff jardiff;\n application/x-java-jnlp-file jnlp;\n application/x-makeself run;\n application/x-perl pl pm;\n application/x-pilot prc pdb;\n application/x-rar-compressed rar;\n application/x-redhat-package-manager rpm;\n application/x-sea sea;\n application/x-shockwave-flash swf;\n application/x-stuffit sit;\n application/x-tcl tcl tk;\n application/x-x509-ca-cert der pem crt;\n application/x-xpinstall xpi;\n application/zip zip;\n application/octet-stream deb;\n application/octet-stream bin exe dll;\n application/octet-stream dmg;\n application/octet-stream eot;\n application/octet-stream iso img;\n application/octet-stream msi msp msm;\n audio/mpeg mp3;\n audio/x-realaudio ra;\n video/mpeg mpeg mpg;\n video/quicktime mov;\n video/x-flv flv;\n video/x-msvideo avi;\n video/x-ms-wmv wmv;\n video/x-ms-asf asx asf;\n video/x-mng mng;\n}\n"
:unique-name "test.io", (:mime.types (:data (cut/generate-nginx-configmap {:unique-name "test.io",
:redirects [["/products.html", "/offer.html"] :gitea-host "gitea.evilorg",
["/one-more", "/redirect"]] :gitea-repo "none",
:forgejo-host "gitea.evilorg",
:repo-owner "someuser",
:repo-name "none",
:branchname "mablain", :branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}))) :fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]})))))
" /offer.html permanent;\n")) (is (= "user nginx;\nworker_processes 3;\nerror_log /var/log/nginx/error.log;\npid /var/log/nginx/nginx.pid;\nworker_rlimit_nofile 8192;\nevents {\n worker_connections 4096;\n}\nhttp {\n include /etc/nginx/mime.types;\n default_type application/octet-stream;\n log_format main '$remote_addr - $remote_user [$time_local] $status'\n '\"$request\" $body_bytes_sent \"$http_referer\"'\n '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n access_log /var/log/nginx/access.log main;\n sendfile on;\n tcp_nopush on;\n keepalive_timeout 65;\n server_names_hash_bucket_size 128;\n include /etc/nginx/conf.d/website.conf;\n}\n"
(is (str/includes? (:nginx.conf (:data (cut/generate-nginx-configmap {:unique-name "test.io",
(:website.conf (:data (cut/generate-nginx-configmap {:issuer "staging" :gitea-host "gitea.evilorg",
:build-cpu-request "500m" :gitea-repo "none",
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:unique-name "test.io",
:redirects [["/products.html", "/offer.html"]
["/one-more", "/redirect"]]
:forgejo-host "gitea.evilorg",
:repo-owner "someuser",
:repo-name "none",
:branchname "mablain", :branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}))) :fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]})))))
" /redirect permanent;\n"))
(is (str/includes?
(:website.conf (:data (cut/generate-nginx-configmap {:issuer "staging"
:build-cpu-request "500m"
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:unique-name "test.io",
:redirects [],
:forgejo-host "gitea.evilorg",
:repo-owner "someuser",
:repo-name "none",
:branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]})))
"server_name test.de www.test.de test-it.de www.test-it.de;"))
(is (= {:apiVersion "v1", (is (= {:apiVersion "v1",
:kind "ConfigMap", :kind "ConfigMap",
:metadata {:labels {:app.kubernetes.part-of "test-io-website"}, :metadata {:name "test-io-configmap",
:namespace "test-io", :labels {:app.kubernetes.part-of "test-io-website"},
:name "etc-nginx"}} :namespace "default"}}
(dissoc (cut/generate-nginx-configmap {:issuer "staging" (dissoc (cut/generate-nginx-configmap {:unique-name "test.io",
:build-cpu-request "500m" :gitea-host "gitea.evilorg",
:build-cpu-limit "1700m" :gitea-repo "none",
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:unique-name "test.io",
:redirects [],
:forgejo-host "gitea.evilorg",
:repo-owner "someuser",
:repo-name "none",
:branchname "mablain", :branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}) :fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}) :data))))
:data))))
(deftest should-generate-nginx-deployment
(is (= {:apiVersion "apps/v1",
:kind "Deployment",
:metadata {:name "test-io-deployment", :labels {:app.kubernetes.part-of "test-io-website"}},
:spec
{:replicas 1,
:selector {:matchLabels {:app "test-io-nginx"}},
:template
{:metadata {:labels {:app "test-io-nginx"}},
:spec
{:containers
[{:name "test-io-nginx",
:image "nginx:latest",
:imagePullPolicy "IfNotPresent",
:ports [{:containerPort 80}],
:volumeMounts
[{:mountPath "/etc/nginx", :readOnly true, :name "nginx-config-volume"}
{:mountPath "/var/log/nginx", :name "log"}
{:mountPath "/var/www/html/website", :name "content-volume", :readOnly true}]}],
:initContainers
[{:image "domaindrivenarchitecture/c4k-website-build",
:name "test-io-init-build-container",
:imagePullPolicy "IfNotPresent",
:resources {:requests {:cpu "500m", :memory "256Mi"}, :limits {:cpu "1700m", :memory "512Mi"}},
:command ["/entrypoint.sh"],
:envFrom [{:secretRef {:name "test-io-secret"}}],
:env [{:name "SHA256SUM", :value "123456789ab123cd345de"} {:name "SCRIPTFILE", :value "script-file-name.sh"}],
:volumeMounts [{:name "content-volume", :mountPath "/var/www/html/website"}
{:name "hashfile-volume", :mountPath "/var/hashfile.d"}]}],
:volumes
[{:name "nginx-config-volume",
:configMap
{:name "test-io-configmap",
:items
[{:key "nginx.conf", :path "nginx.conf"}
{:key "website.conf", :path "conf.d/website.conf"}
{:key "mime.types", :path "mime.types"}]}}
{:name "log", :emptyDir {}}
{:name "content-volume", :persistentVolumeClaim {:claimName "test-io-content-volume"}}
{:name "hashfile-volume", :persistentVolumeClaim {:claimName "test-io-hashfile-volume"}}]}}}}
(cut/generate-nginx-deployment {:gitea-host "gitlab.de",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:gitea-repo "repo",
:sha256sum-output "123456789ab123cd345de script-file-name.sh",
:issuer "staging",
:branchname "main",
:unique-name "test.io"}))))
(deftest should-generate-resource-requests
(is (= {:requests {:cpu "500m", :memory "256Mi"}, :limits {:cpu "1700m", :memory "512Mi"}}
(-> (cut/generate-nginx-deployment {:gitea-host "gitlab.de",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:gitea-repo "repo",
:sha256sum-output "123456789ab123cd345de script-file-name.sh",
:issuer "staging",
:branchname "main",
:unique-name "test.io"})
:spec :template :spec :initContainers first :resources )))
(is (= {:requests {:cpu "1500m", :memory "512Mi"}, :limits {:cpu "3000m", :memory "1024Mi"}}
(-> (cut/generate-nginx-deployment {:gitea-host "gitlab.de",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:gitea-repo "repo",
:sha256sum-output "123456789ab123cd345de script-file-name.sh",
:issuer "staging",
:branchname "main",
:unique-name "test.io"
:build-cpu-request "1500m"
:build-cpu-limit "3000m"
:build-memory-request "512Mi"
:build-memory-limit "1024Mi"})
:spec :template :spec :initContainers first :resources))))
(deftest should-generate-nginx-service (deftest should-generate-nginx-service
(is (= {:kind "Service", (is (= {:name-c1 "test-io-service",
:apiVersion "v1", :name-c2 "test-org-service",
:metadata :app-c1 "test-io-nginx",
{:name "test-io", :app-c2 "test-org-nginx",
:namespace "test-io", :app.kubernetes.part-of-c1 "test-io-website",
:labels {:app "test-io", :app.kubernetes.part-of "test-io-website"}}, :app.kubernetes.part-of-c2 "test-org-website"}
:spec (th/map-diff (cut/generate-nginx-service {:unique-name "test.io",
{:selector {:app "nginx"}, :ports [{:name "nginx-http", :port 80}]}} :gitea-host "gitea.evilorg",
(cut/generate-nginx-service {:issuer "staging" :gitea-repo "none",
:build-cpu-request "500m" :branchname "mablain",
:build-cpu-limit "1700m" :fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]})
:build-memory-request "256Mi" (cut/generate-nginx-service {:unique-name "test.org",
:build-memory-limit "512Mi" :gitea-host "gitea.evilorg",
:volume-size "3" :gitea-repo "none",
:unique-name "test.io", :branchname "mablain",
:redirects [], :fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]})))))
:forgejo-host "gitea.evilorg",
:repo-owner "someuser",
:repo-name "none",
:branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}))))
(deftest should-generate-website-build-cron
(deftest should-generate-build-cron
(is (= {:apiVersion "batch/v1", (is (= {:apiVersion "batch/v1",
:kind "CronJob", :kind "CronJob",
:metadata {:name "build-cron", :metadata {:name "test-io-build-cron", :labels {:app.kubernetes.part-of "test-io-website"}},
:namespace "test-io",
:labels {:app.kubernetes.part-of "test-io-website"}},
:spec :spec
{:schedule "0/7 * * * *", {:schedule "0/7 * * * *",
:successfulJobsHistoryLimit 1, :successfulJobsHistoryLimit 1,
:failedJobsHistoryLimit 1, :failedJobsHistoryLimit 1,
:jobTemplate :jobTemplate
{:spec {:spec
{:activeDeadlineSeconds 415, {:template
:template {:spec
{:metadata
{:namespace "test-io",
:labels
{:app "build-cron", :app.kubernetes.part-of "test-io-website"}}
:spec
{:containers {:containers
[{:image "domaindrivenarchitecture/c4k-website-build", [{:image "domaindrivenarchitecture/c4k-website-build",
:name "build-cron-container", :name "test-io-build-app",
:imagePullPolicy "IfNotPresent", :imagePullPolicy "IfNotPresent",
:resources {:requests {:cpu "500m", :memory "256Mi"}, :limits {:cpu "1700m", :memory "512Mi"}}, :resources {:requests {:cpu "500m", :memory "256Mi"}, :limits {:cpu "1700m", :memory "512Mi"}},
:command ["/entrypoint.sh"], :command ["/entrypoint.sh"],
:envFrom [{:configMapRef {:name "build-configmap"}} :envFrom [{:secretRef {:name "test-io-secret"}}],
{:secretRef {:name "build-secret"}}], :env [{:name "SHA256SUM", :value "123456789ab123cd345de"} {:name "SCRIPTFILE", :value "script-file-name.sh"}],
:volumeMounts [{:name "content-volume", :mountPath "/var/www/html/website"} :volumeMounts [{:name "content-volume", :mountPath "/var/www/html/website"}
{:name "hash-state-volume", :mountPath "/var/hashfile.d"}]}], {:name "hashfile-volume", :mountPath "/var/hashfile.d"}]}],
:volumes [{:name "content-volume", :persistentVolumeClaim {:claimName "content-volume"}} :volumes [{:name "content-volume", :persistentVolumeClaim {:claimName "test-io-content-volume"}}
{:name "hash-state-volume", :persistentVolumeClaim {:claimName "hash-state-volume"}}], {:name "hashfile-volume", :persistentVolumeClaim {:claimName "test-io-hashfile-volume"}}],
:restartPolicy "OnFailure"}}}}}} :restartPolicy "OnFailure"}}}}}}
(cut/generate-build-cron {:issuer "staging" (cut/generate-website-build-cron {:gitea-host "gitlab.de",
:build-cpu-request "500m" :fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:build-cpu-limit "1700m" :gitea-repo "repo",
:build-memory-request "256Mi" :sha256sum-output "123456789ab123cd345de script-file-name.sh",
:build-memory-limit "512Mi" :issuer "staging",
:volume-size "3" :branchname "main",
:forgejo-host "gitlab.de", :unique-name "test.io"}))))
:repo-owner "someuser",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:repo-name "repo",
:branchname "main",
:unique-name "test.io",
:redirects []}))))
(deftest should-generate-build-configmap (deftest should-generate-website-build-secret
(is (= {:apiVersion "v1",
:kind "ConfigMap",
:metadata {:name "build-configmap",
:namespace "test-io",
:labels {:app.kubernetes.part-of "test-io-website"}},
:data
{:GITHOST "mygit.de"
:GITREPOURL "https://mygit.de/api/v1/repos/someuser/repo/archive/main.zip"
:GITCOMMITURL "https://mygit.de/api/v1/repos/someuser/repo/git/commits/HEAD"}}
(cut/generate-build-configmap {:issuer "staging"
:build-cpu-request "500m"
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:forgejo-host "mygit.de",
:repo-owner "someuser",
:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:repo-name "repo",
:branchname "main",
:unique-name "test.io",
:redirects []}))))
(deftest should-generate-build-secret
(is (= {:apiVersion "v1", (is (= {:apiVersion "v1",
:kind "Secret", :kind "Secret",
:metadata {:name "build-secret", :metadata {:name "test-io-secret", :labels {:app.kubernetes.part-of "test-io-website"}},
:namespace "test-io",
:labels {:app.kubernetes.part-of "test-io-website"}},
:data :data
{:AUTHTOKEN "YWJlZGpnYmFzZG9kag=="}} {:AUTHTOKEN "YWJlZGpnYmFzZG9kag==",
(cut/generate-build-secret {:unique-name "test.io", :GITREPOURL "aHR0cHM6Ly9naXRsYWIuZGUvYXBpL3YxL3JlcG9zL3NvbWV1c2VyL3JlcG8vYXJjaGl2ZS9tYWluLnppcA==",
:authtoken "abedjgbasdodj"})))) :GITCOMMITURL "aHR0cHM6Ly9naXRsYWIuZGUvYXBpL3YxL3JlcG9zL3NvbWV1c2VyL3JlcG8vZ2l0L2NvbW1pdHMvSEVBRA=="}}
(cut/generate-website-build-secret {:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"],
:gitea-repo "repo",
:sha256sum-output "123456789ab123cd345de script-file-name.sh",
:issuer "staging",
:branchname "main",
:unique-name "test.io",
:gitea-host "gitlab.de"}
{:unique-name "test.io",
:authtoken "abedjgbasdodj",
:username "someuser"}))))
(deftest should-generate-content-pvc (deftest should-generate-website-content-volume
(is (= {:name-c1 "test-io-content-volume",
:name-c2 "test-org-content-volume",
:app-c1 "test-io-nginx",
:app-c2 "test-org-nginx",
:app.kubernetes.part-of-c1 "test-io-website",
:app.kubernetes.part-of-c2 "test-org-website"}
(th/map-diff (cut/generate-website-content-volume {:unique-name "test.io",
:gitea-host "gitea.evilorg",
:gitea-repo "none",
:branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]})
(cut/generate-website-content-volume {:unique-name "test.org",
:gitea-host "gitea.evilorg",
:gitea-repo "none",
:branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]})))))
(deftest should-generate-hashfile-volume
(is (= {:apiVersion "v1", (is (= {:apiVersion "v1",
:kind "PersistentVolumeClaim", :kind "PersistentVolumeClaim",
:metadata :metadata
{:name "content-volume", {:name "test-io-hashfile-volume",
:namespace "test-io", :namespace "default",
:labels {:app.kubernetes.part-of "test-io-website"}}, :labels {:app "test-io-nginx", :app.kubernetes.part-of "test-io-website"}},
:spec :spec {:storageClassName "local-path", :accessModes ["ReadWriteOnce"], :resources {:requests {:storage "16Mi"}}}}
{:storageClassName "local-path", (cut/generate-hashfile-volume {:unique-name "test.io",
:accessModes ["ReadWriteOnce"], :gitea-host "gitea.evilorg",
:resources {:requests {:storage "3Gi"}}}} :gitea-repo "none",
(cut/generate-content-pvc {:issuer "staging" :branchname "mablain",
:build-cpu-request "500m" :fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}))))
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:unique-name "test.io",
:redirects [],
:forgejo-host "gitea.evilorg",
:repo-owner "someuser",
:repo-name "none",
:branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}))))
(deftest should-generate-hash-state-pvc
(is (= {:apiVersion "v1",
:kind "PersistentVolumeClaim",
:metadata
{:name "hash-state-volume",
:namespace "test-io",
:labels {:app.kubernetes.part-of "test-io-website"}},
:spec {:storageClassName "local-path",
:accessModes ["ReadWriteOnce"],
:resources {:requests {:storage "16Mi"}}}}
(cut/generate-hash-state-pvc {:issuer "staging"
:build-cpu-request "500m"
:build-cpu-limit "1700m"
:build-memory-request "256Mi"
:build-memory-limit "512Mi"
:volume-size "3"
:unique-name "test.io",
:redirects [],
:forgejo-host "gitea.evilorg",
:repo-owner "someuser",
:repo-name "none",
:branchname "mablain",
:fqdns ["test.de" "www.test.de" "test-it.de" "www.test-it.de"]}))))

View file

@ -1,7 +1,9 @@
websiteauths: auth:
- unique-name: "test.io" - unique-name: "test.io"
username: "someuser"
authtoken: "abedjgbasdodj" authtoken: "abedjgbasdodj"
- unique-name: "example.io" - unique-name: "example.io"
username: "someuser"
authtoken: "abedjgbasdodj" authtoken: "abedjgbasdodj"
mon-auth: mon-auth:
grafana-cloud-user: "user" grafana-cloud-user: "user"

View file

@ -1,22 +1,17 @@
issuer: "staging" issuer: "staging"
websiteconfigs: websites:
- unique-name: "test.io" - unique-name: "test.io"
fqdns: ["test.de", "test.org", "www.test.de", "www.test.org"] fqdns: ["test.de", "test.org", "www.test.de", "www.test.org"]
forgejo-host: "codeberg.org" gitea-host: "gitlab.de"
repo-owner: "someuser" gitea-repo: "repo"
repo-name: "repo"
branchname: "main" branchname: "main"
redirects: sha256sum-output: "123456789ab123cd345de script-file-name.sh"
- ["/products.html", "/offer.html"]
- ["/one-more", "/redirect"]
- unique-name: "example.io" - unique-name: "example.io"
fqdns: ["example.org", "www.example.com"] fqdns: ["example.org", "www.example.com"]
forgejo-host: "fineForgejoHost.net" gitea-host: "finegitehost.net"
repo-owner: "someotheruser" gitea-repo: "repo"
repo-name: "repo"
branchname: "main" branchname: "main"
mon-cfg: mon-cfg:
grafana-cloud-url: "url-for-your-prom-remote-write-endpoint" grafana-cloud-url: "url-for-your-prom-remote-write-endpoint"
cluster-name: "website" cluster-name: "jitsi"
cluster-stage: "test" cluster-stage: "test"
average-rate: 50