Compare commits

..

No commits in common. 'main' and 'v6.0.2' have entirely different histories.
main ... v6.0.2

1
.gitignore vendored

@ -27,4 +27,3 @@ my-auth.edn
.clj-kondo/
.lsp/
.eastwood

@ -5,7 +5,7 @@ stages:
- upload
.cljs-job: &cljs
image: "domaindrivenarchitecture/ddadevops-clj-cljs:4.11.4"
image: domaindrivenarchitecture/shadow-cljs
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
@ -13,49 +13,46 @@ stages:
- .shadow-cljs/
- .m2
before_script:
- export RELEASE_ARTIFACT_TOKEN=$MEISSA_REPO_BUERO_RW
- echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- npm install
.clj-job: &clj
image: "domaindrivenarchitecture/ddadevops-clj:4.11.4"
image: domaindrivenarchitecture/lein
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .m2
before_script:
- export RELEASE_ARTIFACT_TOKEN=$MEISSA_REPO_BUERO_RW
- mkdir -p /root/.lein
- echo "{:auth {:repository-auth {#\"clojars\" {:username \"${CLOJARS_USER}\" :password \"${CLOJARS_TOKEN_DOMAINDRIVENARCHITECTURE}\" }}}}" > ~/.lein/profiles.clj
.tag_only: &tag_only
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+$/'
test-clj:
<<: *clj
stage: build_and_test
script:
- pyb test_clj
- lein test
test-cljs:
<<: *cljs
stage: build_and_test
script:
- pyb test_cljs
- shadow-cljs compile test
- node target/node-tests.js
upload-clj-release:
<<: *clj
<<: *tag_only
stage: upload
rules:
- if: '$CI_COMMIT_TAG != null'
script:
- pyb upload_clj
- lein deploy
upload-cljs-release:
<<: *clj
<<: *tag_only
stage: upload
rules:
- if: '$CI_COMMIT_TAG != null'
script:
- pyb upload_cljs
- mv project.clj project-clj.clj && mv project-cljs.clj project.clj
- lein deploy

@ -3,410 +3,84 @@
[<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)
## Rationale
There are many comparable solutions for creating c4k deployments like `helm` or `kustomize`.
`kustomize` is great to manage your k8s manifests by splitting huge files into handy parts.
`helm` is great because of its large community.
Why do we need another one? Why do you continue the reading here?
We combine the simplicity of `kustomize` with the ability to do real programming like software developers would do.
Following the principle
"Use programming language for programming"
we clearly enjoy writing Kubernetes manifests with Clojure. In comparison to helms templating, things such as business logic, conventions, input validation, versions, dependencies and reuse are much easier and much more reliable to implement with c4k.
By the way, c4k means "convention for kubernetes".
### Features
c4k-common supports the following use cases:
- [convention 4 kubernetes: c4k-common](#convention-4-kubernetes-c4k-common)
- [Rationale](#rationale)
- [Features](#features)
- [Target CLI and Web Frontend](#target-cli-and-web-frontend)
- [Separate Configuration From Credentials](#separate-configuration-from-credentials)
- [Input as EDN or Yaml](#input-as-edn-or-yaml)
- [Inline k8s Resources for Versioning \& Dependencies](#inline-k8s-resources-for-versioning--dependencies)
- [Work on Structured Data Instead of Flat Templating](#work-on-structured-data-instead-of-flat-templating)
- [Validate Your Inputs](#validate-your-inputs)
- [Namespaces](#namespaces)
- [Ingress](#ingress)
- [Postgres Database](#postgres-database)
- [Monitoring With Grafana Cloud](#monitoring-with-grafana-cloud)
- [Refactoring \& Module Overview](#refactoring--module-overview)
- [Development \& Mirrors](#development--mirrors)
- [License](#license)
#### Target CLI and Web Frontend
To create your own c4k module set up your cli analogous to the following:
```clojure
(defn -main [& cmd-args]
(uberjar/main-common
"c4k-forgejo" ;; name of your app
core/config? ;; schema for config validation
core/auth? ;; schema for credential validation
core/config-defaults ;; want to set default values?
core/k8s-objects ;; the function generate the k8s manifest
cmd-args ;; command line arguments given
))
```
The full example can be found here: https://repo.prod.meissa.de/meissa/c4k-forgejo/src/branch/main/src/main/clj/dda/c4k_forgejo/uberjar.clj
With c4k instead of using cli to generate manifests, you can also generate your manifests via web-application (using page local js without server interaction)
```html
<html>
<head>
<link href="bootstrap.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="c4k-content"></div>
<script src="js/main.js"></script>
</body>
</html>
```
[![Try it out](doc/tryItOut.png "Try out yourself")](https://domaindrivenarchitecture.org/pages/dda-provision/c4k-forgejo/)
See: https://repo.prod.meissa.de/meissa/c4k-forgejo/src/branch/main/public/index.html
## Purpose
and: https://repo.prod.meissa.de/meissa/c4k-forgejo/src/branch/main/src/main/cljs/dda/c4k_forgejo/browser.cljs
c4k-common provides the foundation for all our c4k modules.
#### Separate Configuration From Credentials
It is now possible to generate a working prometheus monitoring file in yaml format.
Your config.edn and your auth.edn should at least contain the following fields:
We think it is a good idea to have credentials separated from configuration. All our functions, cli and frontend are following this principle. Thus, for executing separated config and authentication infos have to be provided.
The following command line yields the resulting k8s manifests in `k8s-manifest.yaml`:
```bash
java -jar c4k-common.jar config.edn auth.edn > k8s-manifest.yaml
```
Using the tool `jarwrapper` the command line can even be shortened to:
```bash
c4k-common config.edn auth.edn > k8s-manifest.yaml
```
#### Input as EDN or Yaml
c4k-common supports yaml and edn as format for all of its resources (input and output).
Hence, the following command line will also work:
```bash
c4k-common config.yaml auth.yaml > k8s-manifest.yaml
```
#### Inline k8s Resources for Versioning & Dependencies
We inline all resources used in our libraries & applications. You can generate k8s manifests everywhere without additional external dependencies.
In case of
* java: Resources are included in the jar-file out of the box (see https://repo.prod.meissa.de/meissa/c4k-forgejo/src/branch/main/project.clj#L13).
* js: With a slim macro call we inline resources to the resulting js file (see https://repo.prod.meissa.de/meissa/c4k-forgejo/src/branch/main/src/main/cljc/dda/c4k_forgejo/forgejo.cljc#L72-L74)
* native: On native builds we also inline resources (see https://repo.prod.meissa.de/meissa/c4k-forgejo/src/branch/main/build.py#L126)
#### Work on Structured Data Instead of Flat Templating
To keep things simple, we also do templating. But we convert given k8s resources to structured data.
This allows us to have more control and do unit tests:
k8s-resource:
```yaml
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: ratelimit
spec:
rateLimit:
average: AVG
burst: BRS
```
Replace values:
config.edn - minimal example
```clojure
(defn-spec generate-rate-limit-middleware pred/map-or-seq?
[config rate-limit-config?]
(let [{:keys [max-rate max-concurrent-requests]} config]
(->
(yaml/load-as-edn "forgejo/middleware-ratelimit.yaml")
(cm/replace-key-value :average max-rate)
(cm/replace-key-value :burst max-concurrent-requests))))
```
{:k3s-cluster-name "your-cluster-name"
:k3s-cluster-stage :prod
:grafana-cloud-url "your-url"}
```
Have a unit-test:
auth.edn - minimal example
```clojure
(deftest should-generate-middleware-ratelimit
(is (= {:apiVersion "traefik.containo.us/v1alpha1",
:kind "Middleware",
:metadata {:name "ratelimit"},
:spec {:rateLimit {:average 10, :burst 5}}}
(cut/generate-rate-limit-middleware {:max-rate 10, :max-concurrent-requests 5}))))
```
{:grafana-cloud-user "user"
:grafana-cloud-password "password"}
```
#### Validate Your Inputs
Have you recognized the `defn-spec` macro above? We use allover validation, e.g.
```clojure
(def rate-limit-config? (s/keys :req-un [::max-rate
::max-concurrent-requests]))
call (with jarwrapper installed):
(defn-spec generate-rate-limit-middleware pred/map-or-seq?
[config rate-limit-config?]
...)
```
#### Namespaces
We support namespaces for ingress & postgres (monitoring lives in it's own namespace `monitoring`).
```clojure
(dda.c4k-common.namespace/generate {:namespace "myapp"})
```
yields:
```clojure
[{:apiVersion "v1"
:kind "Namespace"
:metadata {:name "myapp"}}]
```
which renders to:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: myapp
```
#### Ingress
In most cases we use `generate-ingress-and-cert` which generates an ingress in combination with letsencrypt cert for a named service.
```clojure
(dda.c4k-common.ingress/generate-ingress-and-cert
{:fqdns ["test.jit.si"]
:service-name "web"
:service-port 80})
```
yields:
```clojure
[{:apiVersion "cert-manager.io/v1",
:kind "Certificate",
...
:spec
{:secretName "web",
:commonName "test.jit.si",
:duration "2160h",
:renewBefore "720h",
:dnsNames ["test.jit.si"],
:issuerRef {:name "staging", :kind "ClusterIssuer"}}}
{:apiVersion "networking.k8s.io/v1",
:kind "Ingress",
...
:spec
{:tls [{:hosts ["test.jit.si"], :secretName "web"}],
:rules
[{:host "test.jit.si",
:http {:paths [{:path "/",
:pathType "Prefix",
:backend
{:service {:name "web",
:port {:number 80}}}}]}}]}}]
```
which renders to:
```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
...
spec:
secretName: web
commonName: test.jit.si
duration: 2160h
renewBefore: 720h
dnsNames:
- test.jit.si
issuerRef:
name: staging
kind: ClusterIssuer
---
apiVersion: networking.k8s.io/v1
kind: Ingress
...
spec:
tls:
- hosts:
- test.jit.si
secretName: web
rules:
- host: test.jit.si
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: web
port:
number: 80
```
#### Postgres Database
If your application needs a database, we often use postgres:
```clojure
(cut/generate-deployment {:postgres-image "postgres:16"})
```
yields:
```clojure
{:apiVersion "apps/v1",
:kind "Deployment",
...
:spec
{:selector {:matchLabels {:app "postgresql"}},
:strategy {:type "Recreate"},
:template
{:metadata {:labels {:app "postgresql"}},
:spec
{:containers
[{:image "postgres:16",
:name "postgresql",
:env
[{:name "POSTGRES_USER", :valueFrom {:secretKeyRef {:name "postgres-secret", :key "postgres-user"}}}
{:name "POSTGRES_PASSWORD", :valueFrom {:secretKeyRef {:name "postgres-secret", :key "postgres-password"}}}
{:name "POSTGRES_DB", :valueFrom {:configMapKeyRef {:name "postgres-config", :key "postgres-db"}}}],
:ports [{:containerPort 5432, :name "postgresql"}],
:volumeMounts
[...],
:volumes
[...]}}}}
```
which renders to:
```yaml
apiVersion: apps/v1
kind: Deployment
...
spec:
selector:
matchLabels:
app: postgresql
strategy:
type: Recreate
template:
metadata:
labels:
app: postgresql
spec:
containers:
- image: postgres:16
name: postgresql
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-password
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: postgres-config
key: postgres-db
ports:
- containerPort: 5432
name: postgresql
volumeMounts:
...
volumes:
...
```bash
c4k-common-standalone.jar config.edn auth.edn > monitoring.yaml
```
We optimized our db installation to run between 2Gb anf 16Gb Ram usage.
#### Monitoring With Grafana Cloud
With minimal config of
## Rationale
```clojure
(def conf
{:k3s-cluster-name "your-cluster-name"
:k3s-cluster-stage :prod
:grafana-cloud-url "your-url"})
There are many comparable solutions for creating c4k deployments like helm or kustomize. Why do we need another one?
* We like the simplicity of kustomize. Yaml in, yaml out, the ability to lint the result and the option to split large yaml files into objects. But a simple overwriting per environment may not be enough ...
* We like helm packages. A package encapsulates the setup for an application. On the one hand, but on the other hand we don't like the idea of having to program and debug in a template language. We can program much better in real programming languages.
(def auth
{:grafana-cloud-user "user"
:grafana-cloud-password "password"})
Our convention 4 kubernetes c4k-* tools combine the advantages of both approaches:
* Packages for one application
* Programming in clojure
* yaml / edn as input and output, no more magic
* good validation, integration as api, cli or in the browser
(monitoring/generate conf auth)
```
## Usage
You can connect your application to grafana cloud.
c4k-common provides the basic functionality for our c4k-modules.
## Refactoring & Module Overview
| Module | Version | [common load-as-edn][edn1] | [groups for webview][bgrp1] | [use common ingress][ing1] | [use common monitoring][mon1] | [validate examples][val1] | [ci with pyb][cipyb] | [inline-macro to load resources][macro] | [native build][native] | namespaces |
| ------------- |---------|:--------------------------:|:---------------------------:|:--------------------------:|:-----------------------------:|:-------------------------:|:--------------------:|:---------------------------------------:|:----------------------:|:----------:|
| c4k-keycloak | 0.2 | x | x | x | x | x | | | | |
| c4k-taiga | 0.1 | x | x | x | x | x | x | x | x | |
| c4k-nextcloud | 10.2 | x | x | x | x | x | x | x | x | x |
| c4k-jitsi | 2.1 | x | x | x | x | x | x | x | x | |
| c4k-forgejo | 3.0 | x | x | x | x | x | x | x | x | |
| c4k-shynet | 1.0 | | | | | | | | | |
| c4k-website | 2.0 | x | x | x | x | x | x | x | x | |
<!---
1. version
2. configs as EDN and YAML
3. renamed test-helper
4. common load-as-edn
5. standardized uberjar
6. groups for webview
7. use common ingress
-->
| Module | Version | [renamed test-helper][th1] | [common load-as-edn][edn1] | [standardized uberjar][ujar1] | [groups for webview][bgrp1] | [use common ingress][ing1] | [use common monitoring][mon1] | [validate examples][val1] | [repo model][repo1] |
|---------------|---------|:--------------------------:|:--------------------------:|:-----------------------------:|:---------------------------:|:--------------------------:|:-----------------------------:|:-------------------------:|:-------------------:|
| c4k-keycloak | 0.2 | - | x | x | x | x | x | x | x |
| c4k-jira | 1.1 | - | | | | | | | x |
| c4k-nextcloud | 4.0 | - | x | x | x | x | x | x | x |
| c4k-jitsi | 1.3 | - | x | x | x | x | x | x | x |
| c4k-forgejo | 2.0 | x | x | x | x | x | x | x | x |
| c4k-shynet | 1.0 | - | | x | | | | | x |
| c4k-website | 1.1 | x | x | x | x | x | x | x | |
[th1]: https://gitlab.com/domaindrivenarchitecture/c4k-gitea/-/merge_requests/1
[edn1]: https://gitlab.com/domaindrivenarchitecture/c4k-website/-/merge_requests/1
[ing1]: https://repo.prod.meissa.de/meissa/c4k-jitsi/commit/214aa41c28662fbf7a49998e17404e7ac9216430
[ujar1]: https://repo.prod.meissa.de/meissa/c4k-jitsi/commit/b852a74dc561c3ab619e4f4d0748ab51e75edc13
[bgrp1]: https://repo.prod.meissa.de/meissa/c4k-jitsi/commit/7ea442adaef727d5b48b242fd0baaaf51902d06e
[mon1]: https://repo.prod.meissa.de/meissa/c4k-jitsi/commit/19e580188ea56ea26ff3a0bfb08ca428b881ad9a
[val1]: https://repo.prod.meissa.de/meissa/c4k-jitsi/commit/5f08a108072569473463fb8f19150a12e564e54f
[repo1]: https://repo.prod.meissa.de/meissa/c4k-forgejo/commit/e9ee6136f3347d5fccefa6b5b4a02d30c4dc42e1
[cipyb]: https://gitlab.com/domaindrivenarchitecture/c4k-jitsi/-/merge_requests/1
[macro]: https://repo.prod.meissa.de/meissa/c4k-jitsi/commit/61d05ceedb6dcbc6bb96b96fe6f03598e2878195
[native]: https://repo.prod.meissa.de/meissa/c4k-forgejo/pulls/4/files
## Development & Mirrors
Development happens at: https://repo.prod.meissa.de/meissa/c4k-common
Mirrors are:
* https://gitlab.com/domaindrivenarchitecture/c4k-common (issues and PR)
* https://github.com/DomainDrivenArchitecture/c4k-common
For more details about our repository model see: https://repo.prod.meissa.de/meissa/federate-your-repos
[repo1]: https://repo.prod.meissa.de/meissa/c4k-forgejo/commit/e9ee6136f3347d5fccefa6b5b4a02d30c4dc42e1
## License
Copyright © 2022, 2023, 2024 meissa GmbH
Copyright © 2022 meissa GmbH
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)

@ -1,124 +0,0 @@
from os import environ
from subprocess import run
from pybuilder.core import init, task
from ddadevops import *
default_task = "dev"
name = "c4k-common"
MODULE = "not-used"
PROJECT_ROOT_PATH = "."
@init
def initialize(project):
input = {
"name": name,
"module": MODULE,
"stage": "notused",
"project_root_path": PROJECT_ROOT_PATH,
"build_types": [],
"mixin_types": ["RELEASE"],
"release_primary_build_file": "project.clj",
"release_secondary_build_files": [
"project-cljs.clj",
],
"release_main_branch": "main",
}
build = ReleaseMixin(project, input)
build.initialize_build_dir()
@task
def test_clj(project):
run("lein test", shell=True, check=True)
@task
def test_cljs(project):
run("shadow-cljs compile test", shell=True, check=True)
run("node target/node-tests.js", shell=True, check=True)
@task
def upload_clj(project):
run("lein deploy", shell=True, check=True)
@task
def upload_cljs(project):
run(
"mv project.clj project-clj.clj && mv project-cljs.clj project.clj",
shell=True,
check=True,
)
run("lein deploy", shell=True, check=True)
run(
"mv project.clj project-cljs.clj && mv project-clj.clj project.clj",
shell=True,
check=True,
)
@task
def lint(project):
# TODO: Do proper configuration
"""run(
"lein eastwood",
shell=True,
check=True,
)"""
run(
"lein ancient check",
shell=True,
check=True,
)
@task
def patch(project):
linttest(project, "PATCH")
release(project)
@task
def minor(project):
linttest(project, "MINOR")
release(project)
@task
def major(project):
linttest(project, "MAJOR")
release(project)
@task
def dev(project):
linttest(project, "NONE")
@task
def prepare(project):
build = get_devops_build(project)
build.prepare_release()
@task
def tag(project):
build = get_devops_build(project)
build.tag_bump_and_push_release()
def release(project):
prepare(project)
tag(project)
def linttest(project, release_type):
build = get_devops_build(project)
build.update_release_type(release_type)
test_clj(project)
test_cljs(project)
lint(project)

@ -49,20 +49,3 @@ C4Context
Rel(app-backup, app-db-storage, "*dbc")
```
# Layout of a component on example of namespace
```mermaid
classDiagram
class namespace {
config? // the external representation
default-config // static defaults
generate(config, auth) seq
}
class namespace-internal {
config? // the internal representation
generate-namespace(config) map
}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

@ -1,4 +1,4 @@
(defproject org.domaindrivenarchitecture/c4k-common-cljs "6.3.2-SNAPSHOT"
(defproject org.domaindrivenarchitecture/c4k-common-cljs "6.0.2"
:description "Contains predicates and tools for c4k"
:url "https://domaindrivenarchitecture.org"
:license {:name "Apache License, Version 2.0"

@ -1,14 +1,14 @@
(defproject org.domaindrivenarchitecture/c4k-common-clj "6.3.2-SNAPSHOT"
(defproject org.domaindrivenarchitecture/c4k-common-clj "6.0.2"
:description "Contains predicates and tools for c4k"
:url "https://domaindrivenarchitecture.org"
:license {:name "Apache License, Version 2.0"
:url "https://www.apache.org/licenses/LICENSE-2.0.html"}
:dependencies [[org.clojure/clojure "1.11.3"]
[org.clojure/tools.reader "1.4.2"]
:dependencies [[org.clojure/clojure "1.11.1" :scope "provided"]
[org.clojure/tools.reader "1.3.6"]
[aero "1.1.6"]
[orchestra "2021.01.01-1"]
[expound "0.9.0"]
[clj-commons/clj-yaml "1.0.27"]]
[clj-commons/clj-yaml "1.0.26"]]
:target-path "target/%s/"
:source-paths ["src/main/cljc"
"src/main/clj"]
@ -25,13 +25,17 @@
:uberjar {:aot :all
:main dda.c4k-common.uberjar
:uberjar-name "c4k-common-standalone.jar"
:dependencies [[org.clojure/tools.cli "1.1.230"]
[ch.qos.logback/logback-classic "1.5.6"
:dependencies [[org.clojure/tools.cli "1.0.214"]
[ch.qos.logback/logback-classic "1.4.6"
:exclusions [com.sun.mail/javax.mail]]
[org.slf4j/jcl-over-slf4j "2.0.13"]]}}
[org.slf4j/jcl-over-slf4j "2.0.7"]]}}
:release-tasks [["test"]
["vcs" "assert-committed"]
["change" "version" "leiningen.release/bump-version" "release"]
["vcs" "commit"]
["vcs" "tag" "v" "--no-sign"]
["change" "version" "leiningen.release/bump-version"]])
["change" "version" "leiningen.release/bump-version"]]
:aliases {"inst" ["shell"
"sh"
"-c"
"lein uberjar && sudo install -m=755 target/uberjar/c4k-common-standalone.jar /usr/local/bin/c4k-common-standalone.jar"]})

@ -8,10 +8,10 @@
[input string?]
(.encodeToString
(Base64/getEncoder)
(.getBytes ^String input "UTF-8")))
(.getBytes input "UTF-8")))
(defn-spec decode string?
[input string?]
(String.
(.decode (Base64/getDecoder) ^String input)
(.decode (Base64/getDecoder) input)
"UTF-8"))

@ -7,6 +7,21 @@
:cljs [orchestra.core :refer-macros [defn-spec]])
[dda.c4k-common.predicate :as cp]))
;; deprecated functions were moved to dda.c4k-common.predicate
(defn ^{:deprecated "0.1"} bash-env-string?
[input]
(and (string? input)
(not (re-matches #".*['\"\$]+.*" input))))
(defn ^{:deprecated "0.1"} fqdn-string?
[input]
(and (string? input)
(some? (re-matches #"(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)" input))))
(defn ^{:deprecated "0.1"} letsencrypt-issuer?
[input]
(contains? #{:prod :staging} input))
(defn-spec replace-named-value cp/map-or-seq?
[coll cp/map-or-seq?
name string?
@ -27,23 +42,15 @@
%)
coll))
(defn-spec replace-all-matching cp/map-or-seq?
[coll cp/map-or-seq?
match-value string?
replace-value cp/str-or-number?]
(clojure.walk/postwalk #(if (and (= (type match-value) (type %))
(= match-value %))
replace-value
%)
coll))
(defn-spec ^{:deprecated "6.2.4"} replace-all-matching-values-by-new-value cp/map-or-seq?
"Use replace-all-matching instead"
(defn-spec replace-all-matching-values-by-new-value cp/map-or-seq?
[coll cp/map-or-seq?
value-to-match string?
value-to-replace cp/str-or-number?]
(replace-all-matching coll value-to-match value-to-replace))
(clojure.walk/postwalk #(if (and (= (type value-to-match) (type %))
(= value-to-match %))
value-to-replace
%)
coll))
(defn-spec concat-vec vector?
[& vs (s/* cp/string-sequence?)]

@ -1,80 +1,84 @@
(ns dda.c4k-common.ingress
(:require
[clojure.spec.alpha :as s]
#?(:cljs [shadow.resource :as rc])
#?(:clj [orchestra.core :refer [defn-spec]]
:cljs [orchestra.core :refer-macros [defn-spec]])
[dda.c4k-common.namespace :as ns]
[dda.c4k-common.ingress.ingress-internal :as int]))
#?(:clj [clojure.edn :as edn]
:cljs [cljs.reader :as edn])
[dda.c4k-common.yaml :as yaml]
[dda.c4k-common.common :as cm]
[dda.c4k-common.predicate :as pred]))
(s/def ::issuer ::int/issuer)
(s/def ::service-name ::int/service-name)
(s/def ::app-name ::int/app-name)
(s/def ::ingress-name ::int/ingress-name)
(s/def ::cert-name ::int/cert-name)
(s/def ::service-port ::int/service-port)
(s/def ::fqdns ::int/fqdns)
(s/def ::average-rate ::int/average-rate)
(s/def ::burst-rate ::int/burst-rate)
(s/def ::issuer pred/letsencrypt-issuer?)
(s/def ::service-name string?)
(s/def ::app-name string?)
(s/def ::ingress-name string?)
(s/def ::cert-name string?)
(s/def ::service-port pos-int?)
(s/def ::fqdns (s/coll-of pred/fqdn-string?))
(def simple-ingress? (s/keys :req-un [::fqdns ::service-name ::service-port]
:opt-un [::issuer ::average-rate ::burst-rate ::ns/namespace]))
:opt-un [::issuer]))
(def ingress? (s/keys :req-un [::fqdns ::app-name ::ingress-name ::service-name ::service-port]
:opt-un [::issuer ::cert-name ::rate-limit-name ::ns/namespace]))
:opt-un [::issuer ::cert-name]))
(def certificate? (s/keys :req-un [::fqdns ::app-name ::cert-name]
:opt-un [::issuer ::ns/namespace]))
:opt-un [::issuer]))
(def rate-limit-config? (s/keys :req-un [::rate-limit-name
::average-rate
::burst-rate]))
(def ingress-defaults {:issuer "staging"})
(def default-config
(merge ns/default-config
{:issuer "staging"
:average-rate 10
:burst-rate 20}))
#?(:cljs
(defmethod yaml/load-resource :ingress [resource-name]
(case resource-name
"ingress/host-rule.yaml" (rc/inline "ingress/host-rule.yaml")
"ingress/certificate.yaml" (rc/inline "ingress/certificate.yaml")
"ingress/ingress.yaml" (rc/inline "ingress/ingress.yaml")
(throw (js/Error. "Undefined Resource!")))))
(defn-spec generate-host-rule pred/map-or-seq?
[service-name ::service-name
service-port ::service-port
fqdn pred/fqdn-string?]
(->
(yaml/load-as-edn "ingress/host-rule.yaml")
(cm/replace-all-matching-values-by-new-value "FQDN" fqdn)
(cm/replace-all-matching-values-by-new-value "SERVICE_PORT" service-port)
(cm/replace-all-matching-values-by-new-value "SERVICE_NAME" service-name)))
(defn-spec generate-certificate map?
[config certificate?]
(let [final-config (merge default-config
config)]
(int/generate-certificate final-config)))
(defn-spec generate-ingress map?
(defn-spec generate-ingress pred/map-or-seq?
[config ingress?]
(let [final-config (merge default-config
config)]
(int/generate-ingress final-config)))
(defn-spec generate-ingress-and-cert seq?
[config simple-ingress?]
(let [{:keys [service-name]} config
final-config (merge {:app-name service-name
:ingress-name service-name
:cert-name service-name}
default-config
config)]
[(int/generate-certificate final-config)
(int/generate-ingress final-config)]))
(let [{:keys [ingress-name cert-name service-name service-port fqdns app-name]} config]
(->
(yaml/load-as-edn "ingress/ingress.yaml")
(assoc-in [:metadata :name] ingress-name)
(assoc-in [:metadata :labels :app.kubernetes.part-of] app-name)
(assoc-in [:spec :tls 0 :secretName] cert-name)
(assoc-in [:spec :tls 0 :hosts] fqdns)
(assoc-in [:spec :rules] (mapv (partial generate-host-rule service-name service-port) fqdns)))))
(defn-spec generate-certificate pred/map-or-seq?
[config certificate?]
(let [{:keys [cert-name issuer fqdns app-name]
:or {issuer "staging"}} config
letsencrypt-issuer (name issuer)]
(->
(yaml/load-as-edn "ingress/certificate.yaml")
(assoc-in [:metadata :name] cert-name)
(assoc-in [:metadata :labels :app.kubernetes.part-of] app-name)
(assoc-in [:spec :secretName] cert-name)
(assoc-in [:spec :commonName] (first fqdns))
(assoc-in [:spec :dnsNames] fqdns)
(assoc-in [:spec :issuerRef :name] letsencrypt-issuer))))
(defn-spec generate-simple-ingress seq?
[config simple-ingress?]
(let [{:keys [service-name]} config
final-config (merge {:app-name service-name
:ingress-name service-name
:cert-name service-name
:rate-limit-name service-name}
default-config
config)
{:keys [average-rate burst-rate]} final-config]
[(int/generate-certificate final-config)
(int/generate-rate-limit-middleware {:rate-limit-name service-name
:namespace (:namespace final-config)
:average-rate average-rate
:burst-rate burst-rate})
(int/generate-ingress final-config)]))
(defn-spec generate-ingress-and-cert any?
[simple-ingress-config simple-ingress?]
(let [{:keys [service-name]} simple-ingress-config
config (merge {:app-name service-name
:ingress-name service-name
:cert-name service-name}
ingress-defaults
simple-ingress-config)]
[(generate-certificate config)
(generate-ingress config)]))

@ -1,108 +0,0 @@
(ns dda.c4k-common.ingress.ingress-internal
(:require
[clojure.spec.alpha :as s]
#?(:cljs [shadow.resource :as rc])
#?(:clj [orchestra.core :refer [defn-spec]]
:cljs [orchestra.core :refer-macros [defn-spec]])
[dda.c4k-common.yaml :as yaml]
[dda.c4k-common.common :as cm]
[dda.c4k-common.namespace :as ns]
[dda.c4k-common.predicate :as pred]))
#?(:cljs
(defmethod yaml/load-resource :ingress [resource-name]
(case resource-name
"ingress/certificate.yaml" (rc/inline "ingress/certificate.yaml")
"ingress/host-rule.yaml" (rc/inline "ingress/host-rule.yaml")
"ingress/ingress.yaml" (rc/inline "ingress/ingress.yaml")
"ingress/middleware-ratelimit.yaml" (rc/inline "ingress/middleware-ratelimit.yaml")
(throw (js/Error. (str "Undefined Resource: " resource-name))))))
(s/def ::issuer pred/letsencrypt-issuer?)
(s/def ::service-name string?)
(s/def ::app-name string?)
(s/def ::ingress-name string?)
(s/def ::cert-name string?)
(s/def ::service-port pos-int?)
(s/def ::fqdns (s/coll-of pred/fqdn-string?))
(s/def ::average-rate pos-int?)
(s/def ::burst-rate pos-int?)
(def ingress? (s/keys :req-un [::ingress-name ::app-name
::ns/namespace
::service-name ::service-port
::issuer ::cert-name
::fqdns]
:opt-un [::rate-limit-name]))
(def certificate? (s/keys :req-un [::fqdns ::app-name ::cert-name ::issuer ::ns/namespace]))
(def rate-limit-config? (s/keys :req-un [::rate-limit-name
::ns/namespace
::average-rate
::burst-rate]))
(defn-spec generate-host-rule map?
[service-name ::service-name
service-port ::service-port
fqdn pred/fqdn-string?]
(->
(yaml/load-as-edn "ingress/host-rule.yaml")
(cm/replace-all-matching "FQDN" fqdn)
(cm/replace-all-matching "SERVICE_PORT" service-port)
(cm/replace-all-matching "SERVICE_NAME" service-name)))
(defn-spec generate-certificate map?
[config certificate?]
(let [{:keys [cert-name issuer fqdns app-name namespace]} config
letsencrypt-issuer (name issuer)]
(->
(yaml/load-as-edn "ingress/certificate.yaml")
(assoc-in [:metadata :name] cert-name)
(assoc-in [:metadata :namespace] namespace)
(assoc-in [:metadata :labels :app.kubernetes.part-of] app-name)
(assoc-in [:spec :secretName] cert-name)
(assoc-in [:spec :commonName] (first fqdns))
(assoc-in [:spec :dnsNames] fqdns)
(assoc-in [:spec :issuerRef :name] letsencrypt-issuer))))
(defn-spec generate-rate-limit-middleware map?
[config rate-limit-config?]
(let [{:keys [rate-limit-name average-rate burst-rate namespace]} config]
(->
(yaml/load-as-edn "ingress/middleware-ratelimit.yaml")
(assoc-in [:metadata :name] (str rate-limit-name "-ratelimit"))
(assoc-in [:metadata :namespace] namespace)
(assoc-in [:spec :rateLimit :average] average-rate)
(assoc-in [:spec :rateLimit :burst] burst-rate))))
(defn-spec generate-ingress map?
[config ingress?]
(let [{:keys [ingress-name cert-name service-name service-port
fqdns app-name rate-limit-name namespace]} config]
(->
(yaml/load-as-edn "ingress/ingress.yaml")
(assoc-in [:metadata :name] ingress-name)
(assoc-in [:metadata :namespace] namespace)
(assoc-in [:metadata :labels :app.kubernetes.part-of] app-name)
(assoc-in [:metadata :annotations]
{:traefik.ingress.kubernetes.io/router.entrypoints
"web, websecure"
:traefik.ingress.kubernetes.io/router.middlewares
(if rate-limit-name
(str "default-redirect-https@kubernetescrd, "
namespace "-" rate-limit-name "-ratelimit@kubernetescrd")
"default-redirect-https@kubernetescrd")
:metallb.universe.tf/address-pool "public"})
(assoc-in [:spec :tls 0 :secretName] cert-name)
(assoc-in [:spec :tls 0 :hosts] fqdns)
(assoc-in [:spec :rules]
(mapv (partial generate-host-rule service-name service-port) fqdns)))))

@ -1,34 +0,0 @@
(ns dda.c4k-common.macros
(:require [clojure.java.io :as io]
[clojure.string :as str])
(:import java.util.jar.JarFile))
(defn inline-resource-file [resource-url relative-resource-folder-path]
(let [files (.listFiles (io/file resource-url))
file-contents (map slurp files)
file-names (map #(str relative-resource-folder-path "/" (.getName %)) files)]
(zipmap file-names file-contents)))
(defn inline-resource-jar [resource-url]
(let [resource-url-string (.toString resource-url)
; Remove jar:file:
start-absolute (str/replace-first resource-url-string "jar:file:" "")
; Split path into jar base and search folder
jar-split (str/split start-absolute #"!/")
absolute-jar-path (first jar-split)
relative-file-path (second jar-split)
jar (JarFile. absolute-jar-path)
files (->> (enumeration-seq (.entries jar))
(filter #(str/starts-with? % relative-file-path))
(filter #(not (.isDirectory %))))
file-names (map #(.getName %) files)
file-contents (map #(slurp (.getInputStream jar %)) files)]
(zipmap file-names file-contents)))
(defmacro inline-resources [resource-path]
(let [resource-url (io/resource resource-path)
resource-protocol (.getProtocol resource-url)]
(case resource-protocol
"file" (inline-resource-file resource-url resource-path)
"jar" (inline-resource-jar resource-url))))

@ -1,43 +1,128 @@
(ns dda.c4k-common.monitoring
(:require
[clojure.spec.alpha :as s]
#?(:cljs [shadow.resource :as rc])
#?(:clj [orchestra.core :refer [defn-spec]]
:cljs [orchestra.core :refer-macros [defn-spec]])
[dda.c4k-common.yaml :as yaml]
[dda.c4k-common.monitoring.monitoring-internal :as int]))
[dda.c4k-common.predicate :as cp]
[dda.c4k-common.common :as cm]
[clojure.string :as str]))
(s/def ::grafana-cloud-user ::int/grafana-cloud-user)
(s/def ::grafana-cloud-password ::int/grafana-cloud-password)
(s/def ::grafana-cloud-url ::int/grafana-cloud-url)
(s/def ::cluster-name ::int/cluster-name)
(s/def ::cluster-stage ::int/cluster-stage)
(s/def ::grafana-cloud-user cp/bash-env-string?)
(s/def ::grafana-cloud-password cp/bash-env-string?)
(s/def ::grafana-cloud-url string?)
(s/def ::cluster-name string?)
(s/def ::cluster-stage cp/stage?)
(s/def ::pvc-storage-class-name cp/pvc-storage-class-name?)
(s/def ::node-regex string?)
(s/def ::traefik-regex string?)
(s/def ::kube-state-regex string?)
(s/def ::mon-cfg (s/keys :req-un [::grafana-cloud-url
::cluster-name
::cluster-stage]))
(s/def ::mon-auth (s/keys :req-un [::grafana-cloud-user
::grafana-cloud-password]))
(s/def ::storage (s/keys :opt-un [::pvc-storage-class-name]))
(s/def ::filter-regex (s/keys :req-un [::node-regex
::traefik-regex
::kube-state-regex]))
(def metric-regex {:node-regex
(str "node_cpu_sec.+|node_load[0-9]+|node_memory_Buf.*|node_memory_Mem.*|"
"node_memory_Cached.*|node_disk_[r,w,i].*|node_filesystem_[s,a].*|"
"node_network_receive_bytes_total|node_network_transmit_bytes_total")
:traefik-regex (str "traefik_entrypoint_.*_total|"
"traefik_entrypoint_.*_seconds_count|"
"traefik_router_.*_total|"
"traefik_router_.*_seconds_count|"
"traefik_service_.*_total|"
"traefik_service_.*_seconds_count|"
"traefik_tls_certs_not_after")
:kube-state-regex (str "kube_pod_container_status_restarts_total|"
"kube_pod_status_reason|kube_node_status_capacity|kube_node_status_allocatable|"
"kube_cronjob_status_active|kube_job_status_failed")})
(def filter-regex-string int/filter-regex-string)
(def filter-regex-string
(str/join "|" (vals metric-regex)))
#?(:cljs
(defmethod yaml/load-resource :monitoring [resource-name]
(case resource-name
"monitoring/namespace.yaml" (rc/inline "monitoring/namespace.yaml")
(defn-spec generate seq?
"monitoring/kube-state-metrics/cluster-role-binding.yaml" (rc/inline "monitoring/kube-state-metrics/cluster-role-binding.yaml")
"monitoring/kube-state-metrics/cluster-role.yaml" (rc/inline "monitoring/kube-state-metrics/cluster-role.yaml")
"monitoring/kube-state-metrics/deployment.yaml" (rc/inline "monitoring/kube-state-metrics/deployment.yaml")
"monitoring/kube-state-metrics/service-account.yaml" (rc/inline "monitoring/kube-state-metrics/service-account.yaml")
"monitoring/kube-state-metrics/service.yaml" (rc/inline "monitoring/kube-state-metrics/service.yaml")
"monitoring/node-exporter/cluster-role-binding.yaml" (rc/inline "monitoring/node-exporter/cluster-role-binding.yaml")
"monitoring/node-exporter/cluster-role.yaml" (rc/inline "monitoring/node-exporter/cluster-role.yaml")
"monitoring/node-exporter/daemon-set.yaml" (rc/inline "monitoring/node-exporter/daemon-set.yaml")
"monitoring/node-exporter/service-account.yaml" (rc/inline "monitoring/node-exporter/service-account.yaml")
"monitoring/node-exporter/service.yaml" (rc/inline "monitoring/node-exporter/service.yaml")
"monitoring/prometheus/cluster-role-binding.yaml" (rc/inline "monitoring/prometheus/cluster-role-binding.yaml")
"monitoring/prometheus/cluster-role.yaml" (rc/inline "monitoring/prometheus/cluster-role.yaml")
"monitoring/prometheus/config.yaml" (rc/inline "monitoring/prometheus/config.yaml")
"monitoring/prometheus/deployment.yaml" (rc/inline "monitoring/prometheus/deployment.yaml")
"monitoring/prometheus/prometheus.yaml" (rc/inline "monitoring/prometheus/prometheus.yaml")
"monitoring/prometheus/service-account.yaml" (rc/inline "monitoring/prometheus/service-account.yaml")
"monitoring/prometheus/service.yaml" (rc/inline "monitoring/prometheus/service.yaml")
(throw (js/Error. "Undefined Resource!")))))
(defn-spec generate-stateful-set cp/map-or-seq?
[config ::storage]
(let [{:keys [pvc-storage-class-name]
:or {pvc-storage-class-name :manual}} config]
(->
(yaml/load-as-edn "monitoring/stateful-set.yaml")
(assoc-in [:spec :volumeClaimTemplates 0 :spec :storageClassName] (name pvc-storage-class-name)))))
(defn-spec generate-prometheus-config cp/map-or-seq?
[config ::mon-cfg
auth ::mon-auth]
(let [{:keys [grafana-cloud-url cluster-name cluster-stage]} config
{:keys [grafana-cloud-user grafana-cloud-password]} auth]
(->
(yaml/load-as-edn "monitoring/prometheus/prometheus.yaml")
(assoc-in [:global :external_labels :cluster]
cluster-name)
(assoc-in [:global :external_labels :stage]
cluster-stage)
(assoc-in [:remote_write 0 :url]
grafana-cloud-url)
(assoc-in [:remote_write 0 :basic_auth :username]
grafana-cloud-user)
(assoc-in [:remote_write 0 :basic_auth :password]
grafana-cloud-password)
(cm/replace-all-matching-values-by-new-value "FILTER_REGEX" filter-regex-string))))
(defn-spec generate-config cp/map-or-seq?
[config ::mon-cfg
auth ::mon-auth]
(->
(yaml/load-as-edn "monitoring/prometheus/config.yaml")
(assoc-in [:stringData :prometheus.yaml]
(yaml/to-string
(generate-prometheus-config config auth)))))
(defn-spec generate cp/map-or-seq?
[config ::mon-cfg
auth ::mon-auth]
[(yaml/load-as-edn "monitoring/namespace.yaml")
(yaml/load-as-edn "monitoring/prometheus-cluster-role.yaml")
(yaml/load-as-edn "monitoring/prometheus-cluster-role-binding.yaml")
(yaml/load-as-edn "monitoring/prometheus-service.yaml")
(yaml/load-as-edn "monitoring/prometheus-service-account.yaml")
(int/generate-config config auth)
(yaml/load-as-edn "monitoring/prometheus-deployment.yaml")
(yaml/load-as-edn "monitoring/node-exporter-service-account.yaml")
(yaml/load-as-edn "monitoring/node-exporter-cluster-role.yaml")
(yaml/load-as-edn "monitoring/node-exporter-cluster-role-binding.yaml")
(yaml/load-as-edn "monitoring/node-exporter-daemon-set.yaml")
(yaml/load-as-edn "monitoring/node-exporter-service.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics-cluster-role-binding.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics-cluster-role.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics-deployment.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics-service-account.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics-service.yaml")])
(yaml/load-as-edn "monitoring/prometheus/cluster-role.yaml")
(yaml/load-as-edn "monitoring/prometheus/cluster-role-binding.yaml")
(yaml/load-as-edn "monitoring/prometheus/service.yaml")
(yaml/load-as-edn "monitoring/prometheus/service-account.yaml")
(generate-config config auth)
(yaml/load-as-edn "monitoring/prometheus/deployment.yaml")
(yaml/load-as-edn "monitoring/node-exporter/service-account.yaml")
(yaml/load-as-edn "monitoring/node-exporter/cluster-role.yaml")
(yaml/load-as-edn "monitoring/node-exporter/cluster-role-binding.yaml")
(yaml/load-as-edn "monitoring/node-exporter/daemon-set.yaml")
(yaml/load-as-edn "monitoring/node-exporter/service.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics/cluster-role-binding.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics/cluster-role.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics/deployment.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics/service-account.yaml")
(yaml/load-as-edn "monitoring/kube-state-metrics/service.yaml")])

@ -1,92 +0,0 @@
(ns dda.c4k-common.monitoring.monitoring-internal
(:require
[clojure.spec.alpha :as s]
#?(:cljs [shadow.resource :as rc])
#?(:clj [orchestra.core :refer [defn-spec]]
:cljs [orchestra.core :refer-macros [defn-spec]])
[dda.c4k-common.yaml :as yaml]
[dda.c4k-common.predicate :as cp]
[dda.c4k-common.common :as cm]
[clojure.string :as str]))
#?(:cljs
(defmethod yaml/load-resource :monitoring [resource-name]
(case resource-name
"monitoring/kube-state-metrics-cluster-role-binding.yaml" (rc/inline "monitoring/kube-state-metrics-cluster-role-binding.yaml")
"monitoring/kube-state-metrics-cluster-role.yaml" (rc/inline "monitoring/kube-state-metrics-cluster-role.yaml")
"monitoring/kube-state-metrics-deployment.yaml" (rc/inline "monitoring/kube-state-metrics-deployment.yaml")
"monitoring/kube-state-metrics-service-account.yaml" (rc/inline "monitoring/kube-state-metrics-service-account.yaml")
"monitoring/kube-state-metrics-service.yaml" (rc/inline "monitoring/kube-state-metrics-service.yaml")
"monitoring/namespace.yaml" (rc/inline "monitoring/namespace.yaml")
"monitoring/node-exporter-cluster-role-binding.yaml" (rc/inline "monitoring/node-exporter-cluster-role-binding.yaml")
"monitoring/node-exporter-cluster-role.yaml" (rc/inline "monitoring/node-exporter-cluster-role.yaml")
"monitoring/node-exporter-daemon-set.yaml" (rc/inline "monitoring/node-exporter-daemon-set.yaml")
"monitoring/node-exporter-service-account.yaml" (rc/inline "monitoring/node-exporter-service-account.yaml")
"monitoring/node-exporter-service.yaml" (rc/inline "monitoring/node-exporter-service.yaml")
"monitoring/prometheus-cluster-role-binding.yaml" (rc/inline "monitoring/prometheus-cluster-role-binding.yaml")
"monitoring/prometheus-cluster-role.yaml" (rc/inline "monitoring/prometheus-cluster-role.yaml")
"monitoring/prometheus-config.yaml" (rc/inline "monitoring/prometheus-config.yaml")
"monitoring/prometheus-deployment.yaml" (rc/inline "monitoring/prometheus-deployment.yaml")
"monitoring/prometheus-prometheus.yaml" (rc/inline "monitoring/prometheus-prometheus.yaml")
"monitoring/prometheus-service-account.yaml" (rc/inline "monitoring/prometheus-service-account.yaml")
"monitoring/prometheus-service.yaml" (rc/inline "monitoring/prometheus-service.yaml")
(throw (js/Error. (str "Undefined Resource: " resource-name))))))
(s/def ::grafana-cloud-user cp/bash-env-string?)
(s/def ::grafana-cloud-password cp/bash-env-string?)
(s/def ::grafana-cloud-url string?)
(s/def ::cluster-name string?)
(s/def ::cluster-stage cp/stage?)
(s/def ::mon-cfg (s/keys :req-un [::grafana-cloud-url
::cluster-name
::cluster-stage]))
(s/def ::mon-auth (s/keys :req-un [::grafana-cloud-user
::grafana-cloud-password]))
(def metric-regex {:node-regex
(str "node_cpu_sec.+|node_load[0-9]+|node_memory_Buf.*|node_memory_Mem.*|"
"node_memory_Cached.*|node_disk_[r,w,i].*|node_filesystem_[s,a].*|"
"node_network_receive_bytes_total|node_network_transmit_bytes_total")
:traefik-regex (str "traefik_entrypoint_.*_total|"
"traefik_entrypoint_.*_seconds_count|"
"traefik_router_.*_total|"
"traefik_router_.*_seconds_count|"
"traefik_service_.*_total|"
"traefik_service_.*_seconds_count|"
"traefik_tls_certs_not_after")
:kube-state-regex (str "kube_pod_container_status_restarts_total|"
"kube_pod_status_reason|kube_node_status_capacity|kube_node_status_allocatable|"
"kube_cronjob_status_active|kube_job_status_failed")})
(def filter-regex-string
(str/join "|" (vals metric-regex)))
(defn-spec generate-prometheus-config map?
[config ::mon-cfg
auth ::mon-auth]
(let [{:keys [grafana-cloud-url cluster-name cluster-stage]} config
{:keys [grafana-cloud-user grafana-cloud-password]} auth]
(->
(yaml/load-as-edn "monitoring/prometheus-prometheus.yaml")
(assoc-in [:global :external_labels :cluster]
cluster-name)
(assoc-in [:global :external_labels :stage]
cluster-stage)
(assoc-in [:remote_write 0 :url]
grafana-cloud-url)
(assoc-in [:remote_write 0 :basic_auth :username]
grafana-cloud-user)
(assoc-in [:remote_write 0 :basic_auth :password]
grafana-cloud-password)
(cm/replace-all-matching "FILTER_REGEX" filter-regex-string))))
(defn-spec generate-config map?
[config ::mon-cfg
auth ::mon-auth]
(->
(yaml/load-as-edn "monitoring/prometheus-config.yaml")
(assoc-in [:stringData :prometheus.yaml]
(yaml/to-string
(generate-prometheus-config config auth)))))

@ -1,19 +0,0 @@
(ns dda.c4k-common.namespace
(:require
#?(:clj [orchestra.core :refer [defn-spec]]
:cljs [orchestra.core :refer-macros [defn-spec]])
[clojure.spec.alpha :as s]
[dda.c4k-common.namespace.namespace-internal :as int]))
(s/def ::namespace ::int/namespace)
(def config? (s/keys :req-un []
:opt-un [::namespace]))
(def default-config {:namespace "default"})
(defn-spec generate seq?
[config config?]
(let [final-config (merge default-config
config)]
[(int/generate-namespace final-config)]))

@ -1,23 +0,0 @@
(ns dda.c4k-common.namespace.namespace-internal
(:require
[clojure.spec.alpha :as s]
#?(:clj [orchestra.core :refer [defn-spec]]
:cljs [orchestra.core :refer-macros [defn-spec]])
[dda.c4k-common.yaml :as yaml]
#?(:cljs [dda.c4k-common.macros :refer-macros [inline-resources]])))
#?(:cljs
(defmethod yaml/load-resource :namespace [resource-name]
(get (inline-resources "namespace") resource-name)))
(s/def ::namespace string?)
(def config? (s/keys :req-un [::namespace]
:opt-un []))
(defn-spec generate-namespace map?
[config config?]
(let [{:keys [namespace]} config]
(->
(yaml/load-as-edn "namespace/namespace.yaml")
(assoc-in [:metadata :name] namespace))))

@ -1,94 +1,99 @@
(ns dda.c4k-common.postgres
(:require
[clojure.spec.alpha :as s]
#?(:cljs [shadow.resource :as rc])
#?(:clj [orchestra.core :refer [defn-spec]]
:cljs [orchestra.core :refer-macros [defn-spec]])
[dda.c4k-common.namespace :as ns]
[dda.c4k-common.postgres.postgres-internal :as int]))
(def postgres-size? int/postgres-size?)
(def postgres-image? int/postgres-image?)
(s/def ::postgres-db-user ::int/postgres-db-user)
(s/def ::postgres-db-password ::int/postgres-db-password)
(s/def ::postgres-data-volume-path ::int/postgres-data-volume-path)
(s/def ::postgres-size ::int/postgres-size)
(s/def ::db-name ::int/db-name)
(s/def ::pvc-storage-class-name ::int/pvc-storage-class-name)
(s/def ::pv-storage-size-gb ::int/pv-storage-size-gb)
[dda.c4k-common.yaml :as yaml]
[dda.c4k-common.base64 :as b64]
[dda.c4k-common.predicate :as cp]
[dda.c4k-common.common :as cm]))
(defn postgres-size?
[input]
(contains? #{:2gb :4gb :8gb :16gb} input))
(defn postgres-image?
[input]
(contains? #{"postgres:13" "postgres:14"} input))
(s/def ::postgres-db-user cp/bash-env-string?)
(s/def ::postgres-db-password cp/bash-env-string?)
(s/def ::postgres-data-volume-path string?)
(s/def ::postgres-size postgres-size?)
(s/def ::db-name cp/bash-env-string?)
(s/def ::pvc-storage-class-name cp/pvc-storage-class-name?)
(s/def ::pv-storage-size-gb pos?)
(def pg-config?
(s/keys :opt-un [::postgres-size ::db-name ::postgres-data-volume-path
::pvc-storage-class-name ::pv-storage-size-gb ::ns/namespace]))
::pvc-storage-class-name ::pv-storage-size-gb]))
(def pg-auth?
(s/keys :opt-un [::postgres-db-user ::postgres-db-password]))
(def postgres-function (s/keys :opt-un [::deserializer ::optional]))
(def default-config (merge ns/default-config
{:postgres-image "postgres:13"
:postgres-size :2gb
:db-name "postgres"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 10
:pvc-storage-class-name "manual"}))
(defn-spec generate-config map?
#?(:cljs
(defmethod yaml/load-resource :postgres [resource-name]
(case resource-name
"postgres/config-2gb.yaml" (rc/inline "postgres/config-2gb.yaml")
"postgres/config-4gb.yaml" (rc/inline "postgres/config-4gb.yaml")
"postgres/config-8gb.yaml" (rc/inline "postgres/config-8gb.yaml")
"postgres/config-16gb.yaml" (rc/inline "postgres/config-16gb.yaml")
"postgres/deployment.yaml" (rc/inline "postgres/deployment.yaml")
"postgres/persistent-volume.yaml" (rc/inline "postgres/persistent-volume.yaml")
"postgres/pvc.yaml" (rc/inline "postgres/pvc.yaml")
"postgres/secret.yaml" (rc/inline "postgres/secret.yaml")
"postgres/service.yaml" (rc/inline "postgres/service.yaml")
(throw (js/Error. "Undefined Resource!")))))
(defn-spec generate-config cp/map-or-seq?
[& config (s/? pg-config?)]
(let [final-config (merge default-config
(first config))]
(int/generate-config final-config)))
(defn-spec generate-deployment map?
(let [{:keys [postgres-size db-name]
:or {postgres-size :2gb
db-name "postgres"}} (first config)]
(->
(yaml/from-string (yaml/load-resource
(str "postgres/config-" (name postgres-size) ".yaml")))
(assoc-in [:data :postgres-db] db-name))))
; TODO: why do we need a sequence of configs?
(defn-spec generate-deployment cp/map-or-seq?
[& config (s/? pg-config?)]
(let [final-config (merge default-config
(first config))]
(int/generate-deployment final-config)))
(let [{:keys [postgres-image]
:or {postgres-image "postgres:13"}} (first config)]
(->
(yaml/from-string (yaml/load-resource "postgres/deployment.yaml"))
(assoc-in [:spec :template :spec :containers 0 :image] postgres-image))))
(defn-spec generate-persistent-volume map?
(defn-spec generate-persistent-volume cp/map-or-seq?
[config pg-config?]
(let [final-config (merge default-config
config)]
(int/generate-persistent-volume final-config)))
(defn-spec generate-pvc map?
[config pg-config?]
(let [final-config (merge default-config
config)]
(int/generate-pvc final-config)))
(defn-spec generate-secret map?
([auth pg-auth?]
(let [final-config default-config]
(int/generate-secret final-config auth)))
([config pg-config?
auth pg-auth?]
(let [final-config (merge default-config
config)]
(int/generate-secret final-config auth))))
(defn-spec generate-service map?
(let [{:keys [postgres-data-volume-path pv-storage-size-gb]
:or {postgres-data-volume-path "/var/postgres"
pv-storage-size-gb 10}} config]
(->
(yaml/from-string (yaml/load-resource "postgres/persistent-volume.yaml"))
(assoc-in [:spec :hostPath :path] postgres-data-volume-path)
(assoc-in [:spec :capacity :storage] (str pv-storage-size-gb "Gi")))))
(defn-spec generate-pvc cp/map-or-seq?
[config pg-config?]
(let [final-config (merge default-config
config)]
(int/generate-service final-config)))
(defn-spec generate seq?
[config pg-config?
auth pg-auth?]
(let [final-config (merge default-config
config)]
[(int/generate-secret final-config auth)
(int/generate-persistent-volume final-config)
(int/generate-config final-config)
(int/generate-pvc final-config)
(int/generate-deployment final-config)
(int/generate-service final-config)]))
(let [{:keys [pv-storage-size-gb pvc-storage-class-name]
:or {pv-storage-size-gb 10
pvc-storage-class-name "manual"}} config]
(->
(yaml/from-string (yaml/load-resource "postgres/pvc.yaml"))
(assoc-in [:spec :resources :requests :storage] (str pv-storage-size-gb "Gi"))
(assoc-in [:spec :storageClassName] (name pvc-storage-class-name)))))
(defn-spec generate-secret cp/map-or-seq?
[my-auth any?]
(let [{:keys [postgres-db-user postgres-db-password]} my-auth]
(->
(yaml/from-string (yaml/load-resource "postgres/secret.yaml"))
(cm/replace-key-value :postgres-user (b64/encode postgres-db-user))
(cm/replace-key-value :postgres-password (b64/encode postgres-db-password)))))
(defn-spec generate-service cp/map-or-seq?
[]
(yaml/from-string (yaml/load-resource "postgres/service.yaml")))

@ -1,111 +0,0 @@
(ns dda.c4k-common.postgres.postgres-internal
(:require
[clojure.spec.alpha :as s]
#?(:cljs [shadow.resource :as rc])
#?(:clj [orchestra.core :refer [defn-spec]]
:cljs [orchestra.core :refer-macros [defn-spec]])
[dda.c4k-common.yaml :as yaml]
[dda.c4k-common.base64 :as b64]
[dda.c4k-common.predicate :as cp]
[dda.c4k-common.common :as cm]
[dda.c4k-common.namespace :as ns]))
#?(:cljs
(defmethod yaml/load-resource :postgres [resource-name]
(case resource-name
"postgres/config-2gb.yaml" (rc/inline "postgres/config-2gb.yaml")
"postgres/config-4gb.yaml" (rc/inline "postgres/config-4gb.yaml")
"postgres/config-8gb.yaml" (rc/inline "postgres/config-8gb.yaml")
"postgres/config-16gb.yaml" (rc/inline "postgres/config-16gb.yaml")
"postgres/deployment.yaml" (rc/inline "postgres/deployment.yaml")
"postgres/persistent-volume.yaml" (rc/inline "postgres/persistent-volume.yaml")
"postgres/pvc.yaml" (rc/inline "postgres/pvc.yaml")
"postgres/secret.yaml" (rc/inline "postgres/secret.yaml")
"postgres/service.yaml" (rc/inline "postgres/service.yaml")
(throw (js/Error. (str "Undefined Resource: " resource-name))))))
(defn postgres-size?
[input]
(contains? #{:2gb :4gb :8gb :16gb} input))
(defn postgres-image?
[input]
(contains? #{"postgres:13" "postgres:14" "postgres:15" "postgres:16"} input))
(s/def ::postgres-db-user cp/bash-env-string?)
(s/def ::postgres-db-password cp/bash-env-string?)
(s/def ::postgres-data-volume-path string?)
(s/def ::postgres-size postgres-size?)
(s/def ::db-name cp/bash-env-string?)
(s/def ::pvc-storage-class-name cp/pvc-storage-class-name?)
(s/def ::pv-storage-size-gb pos?)
(def pg-config?
(s/keys :req-un [::postgres-size ::db-name ::postgres-data-volume-path
::pvc-storage-class-name ::pv-storage-size-gb ::ns/namespace]))
(def pg-auth?
(s/keys :req-un [::postgres-db-user ::postgres-db-password]))
(def postgres-function (s/keys :opt-un [::deserializer ::optional]))
(defn-spec generate-config map?
[config pg-config?]
(let [{:keys [postgres-size db-name namespace]} config]
(->
(yaml/from-string (yaml/load-resource
(str "postgres/config-" (name postgres-size) ".yaml")))
(assoc-in [:metadata :namespace] namespace)
(assoc-in [:data :postgres-db] db-name))))
(defn-spec generate-deployment map?
[config pg-config?]
(let [{:keys [postgres-image namespace]} config]
(->
(yaml/from-string (yaml/load-resource "postgres/deployment.yaml"))
(assoc-in [:metadata :namespace] namespace)
(assoc-in [:spec :template :spec :containers 0 :image] postgres-image))))
(defn-spec generate-persistent-volume map?
[config pg-config?]
(let [{:keys [postgres-data-volume-path pv-storage-size-gb namespace]} config]
(->
(yaml/from-string (yaml/load-resource "postgres/persistent-volume.yaml"))
(assoc-in [:metadata :namespace] namespace)
(assoc-in [:spec :hostPath :path] postgres-data-volume-path)
(assoc-in [:spec :capacity :storage] (str pv-storage-size-gb "Gi")))))
(defn-spec generate-pvc map?
[config pg-config?]
(let [{:keys [pv-storage-size-gb pvc-storage-class-name namespace]} config]
(->
(yaml/from-string (yaml/load-resource "postgres/pvc.yaml"))
(assoc-in [:metadata :namespace] namespace)
(assoc-in [:spec :resources :requests :storage] (str pv-storage-size-gb "Gi"))
(assoc-in [:spec :storageClassName] (name pvc-storage-class-name)))))
(defn-spec generate-secret map?
[config pg-config?
auth pg-auth?]
(let [{:keys [namespace]} config
{:keys [postgres-db-user postgres-db-password]} auth]
(->
(yaml/from-string (yaml/load-resource "postgres/secret.yaml"))
(assoc-in [:metadata :namespace] namespace)
(cm/replace-key-value :postgres-user (b64/encode postgres-db-user))
(cm/replace-key-value :postgres-password (b64/encode postgres-db-password)))))
(defn-spec generate-service map?
[config pg-config?]
(let [{:keys [namespace]} config]
(->
(yaml/from-string (yaml/load-resource "postgres/service.yaml"))
(assoc-in [:metadata :namespace] namespace))))

@ -33,7 +33,7 @@
(defn pvc-storage-class-name?
[input]
(contains? #{"manual" "local-path" "hcloud-volumes" "hcloud-volumes-encrypted"} input))
(contains? #{"manual" "local-path"} input))
(defn port-number?
[input]

@ -21,27 +21,20 @@
(s/def ::deserializer fn?)
(s/def ::optional boolean?)
(def dom-function-parameter (s/keys :opt-un [::deserializer ::optional]))
(defn-spec get-content-value-from-element js-object?
[name string?]
(-> (get-element-by-id name)
(.-value)))
(defn-spec deserialize-content js-object?
[content string?
deserializer ::deserializer
optional ::optional]
(cond
(and optional (st/blank? content))
nil
:else
(apply deserializer [content])))
(defn-spec get-content-from-element js-object?
[name string?
& {:keys [deserializer optional]
:or {deserializer identity optional false}} dom-function-parameter]
(-> (get-content-value-from-element name)
(deserialize-content deserializer optional)))
:or {deserializer nil optional false}} dom-function-parameter]
(let [content (-> (get-element-by-id name)
(.-value))]
(cond
(and optional (some? deserializer))
(when-not (st/blank? content)
(apply deserializer [content]))
(and (false? optional) (some? deserializer))
(apply deserializer [content])
:else
content)))
(defn-spec set-validation-result! js-object?
[name string?
@ -57,7 +50,7 @@
[name string?
spec js-object?
& {:keys [deserializer optional]
:or {deserializer identity optional false}} dom-function-parameter]
:or {deserializer nil optional false}} dom-function-parameter]
(let [content (get-content-from-element name :optional optional :deserializer deserializer)]
(if (or (and optional (st/blank? content))
(s/valid? spec content))

@ -1,2 +0,0 @@
(ns dda.c4k-common.macros
(:require-macros [dda.c4k-common.macros]))

@ -3,6 +3,7 @@
["js-yaml" :as yaml]
[clojure.string :as st]
[orchestra.core :refer-macros [defn-spec]]
[shadow.resource :as rc]
[dda.c4k-common.predicate :as cp]))
(defn string-or-keyword? [input]

@ -1,7 +1,10 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: c4k-common-https-ingress
namespace: default
labels:
app.kubernetes.part-of: c4k-common-app
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web, websecure
traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd

@ -1,9 +0,0 @@
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: ratelimit
namespace: default
spec:
rateLimit:
average: AVG
burst: BRS

@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: default

@ -1,21 +0,0 @@
(ns dda.c4k-common.macros-test
(:require
[clojure.test :refer [deftest is are testing run-tests]]
[dda.c4k-common.macros :as cut :refer [inline-resources]]))
(deftest should-count-inline-resources
(is (= 3 (count (inline-resources "dda/c4k_common/inline_resources_test")))))
(deftest should-inline-resources
(let [resource-path (fn [name] (str "dda/c4k_common/inline_resources_test/" name))
inlined-resources (inline-resources "dda/c4k_common/inline_resources_test")]
(is (= "1" (get inlined-resources (resource-path "inline_resource_1.yaml"))))
(is (= "2" (get inlined-resources (resource-path "inline_resource_2.yaml"))))
(is (= "3" (get inlined-resources (resource-path "inline_resource_3.yaml"))))))
(deftest should-inline-jar-resources
(let [jar-url (java.net.URL. "jar:file:./src/test/resources/dda/c4k_common/inline_jar_test/test.jar!/inline_resources_test/")
inlined-resources (cut/inline-resource-jar jar-url)]
(is (= "1" (get inlined-resources "inline_resources_test/inline_resource_1.yaml")))
(is (= "2" (get inlined-resources "inline_resources_test/inline_resource_2.yaml")))
(is (= "3" (get inlined-resources "inline_resources_test/inline_resource_3.yaml")))))

@ -1,8 +1,8 @@
(ns dda.c4k-common.monitoring.monitoring-regex-test
(ns dda.c4k-common.monitoring-regex-test
(:require
[clojure.test :refer [deftest is are testing run-tests]]
[data-test :refer :all]
[dda.c4k-common.monitoring.monitoring-internal :as cut]))
[dda.c4k-common.monitoring :as cut]))
(defn filter-by-regex
[regex-str collection]

@ -1,135 +0,0 @@
(ns dda.c4k-common.ingress.ingress-internal-test
(:require
#?(:clj [clojure.test :refer [deftest is are testing run-tests]]
:cljs [cljs.test :refer-macros [deftest is are testing run-tests]])
[clojure.spec.test.alpha :as st]
[dda.c4k-common.ingress.ingress-internal :as cut]))
(st/instrument `cut/generate-host-rule)
(st/instrument `cut/generate-certificate)
(st/instrument `cut/generate-rate-limit-middleware)
(st/instrument `cut/generate-ingress)
(deftest should-generate-rule
(is (= {:host "test.com",
:http
{:paths
[{:pathType "Prefix",
:path "/",
:backend
{:service {:name "myservice", :port {:number 3000}}}}]}}
(cut/generate-host-rule "myservice" 3000 "test.com"))))
(deftest should-generate-certificate
(is (= {:apiVersion "cert-manager.io/v1",
:kind "Certificate",
:metadata {:name "test-io-cert",
:namespace "default",
:labels {:app.kubernetes.part-of "c4k-common-app"}},
:spec
{:secretName "test-io-cert",
:commonName "test.de",
:duration "2160h",
:renewBefore "720h",
:dnsNames ["test.de" "test.org" "www.test.de" "www.test.org"],
:issuerRef {:name "prod", :kind "ClusterIssuer"}}}
(cut/generate-certificate {:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"]
:app-name "c4k-common-app"
:cert-name "test-io-cert"
:issuer "prod"
:namespace "default"})))
(is (= {:apiVersion "cert-manager.io/v1",
:kind "Certificate",
:metadata {:name "test-io-cert",
:namespace "myapp",
:labels {:app.kubernetes.part-of "c4k-common-app"}},
:spec
{:secretName "test-io-cert",
:commonName "test.de",
:duration "2160h",
:renewBefore "720h",
:dnsNames ["test.de" "test.org" "www.test.de" "www.test.org"],
:issuerRef {:name "prod", :kind "ClusterIssuer"}}}
(cut/generate-certificate {:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"]
:app-name "c4k-common-app"
:cert-name "test-io-cert"
:issuer "prod"
:namespace "myapp"}))))
(deftest should-generate-middleware-ratelimit
(is (= {:apiVersion "traefik.containo.us/v1alpha1",
:kind "Middleware",
:metadata {:name "normal-ratelimit"
:namespace "myapp",},
:spec {:rateLimit {:average 10, :burst 5}}}
(cut/generate-rate-limit-middleware {:rate-limit-name "normal"
:namespace "myapp"
:average-rate 10, :burst-rate 5}))))
(deftest should-generate-ingress
(is (= {:apiVersion "networking.k8s.io/v1",
:kind "Ingress",
:metadata
{:namespace "myapp",
:name "test-io-https-ingress",
:labels {:app.kubernetes.part-of "c4k-common-app"},
:annotations {:traefik.ingress.kubernetes.io/router.entrypoints
"web, websecure"
:traefik.ingress.kubernetes.io/router.middlewares
"default-redirect-https@kubernetescrd"
:metallb.universe.tf/address-pool "public"}}}
(dissoc (cut/generate-ingress
{:ingress-name "test-io-https-ingress"
:app-name "c4k-common-app"
:namespace "myapp"
:service-name "test-io-service" :service-port 80
:issuer "prod" :cert-name "noname"
:fqdns ["test.de" "www.test.de" "test-it.de"
"www.test-it.de"]}) :spec)))
(is (= {:name "test-io-https-ingress",
:namespace "default",
:labels {:app.kubernetes.part-of "c4k-common-app"},
:annotations {:traefik.ingress.kubernetes.io/router.entrypoints
"web, websecure"
:traefik.ingress.kubernetes.io/router.middlewares
"default-redirect-https@kubernetescrd, default-normal-ratelimit@kubernetescrd",
:metallb.universe.tf/address-pool "public"}}
(:metadata (cut/generate-ingress
{
:ingress-name "test-io-https-ingress"
:app-name "c4k-common-app"
:namespace "default"
:service-name "test-io-service" :service-port 80
:rate-limit-name "normal"
:issuer "prod" :cert-name "noname"
:fqdns ["test.de"]}))))
(is (= {:tls
[{:hosts
["test.de" "www.test.de" "test-it.de" "www.test-it.de"],
:secretName "test-io-cert"}]
:rules
[{:host "test.de",
:http
{:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}
{:host "www.test.de",
:http
{:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}
{:host "test-it.de",
:http
{:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}
{:host "www.test-it.de",
:http
{:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}]}
(:spec (cut/generate-ingress {
:ingress-name "test-io-https-ingress"
:app-name "c4k-common-app"
:namespace "default"
:service-name "test-io-service" :service-port 80
:issuer "prod" :cert-name "test-io-cert"
:fqdns ["test.de" "www.test.de"
"test-it.de"
"www.test-it.de"]})))))

@ -5,10 +5,67 @@
[clojure.spec.test.alpha :as st]
[dda.c4k-common.ingress :as cut]))
(st/instrument `cut/generate-host-rule)
(st/instrument `cut/generate-ingress)
(st/instrument `cut/generate-certificate)
(st/instrument `cut/generate-ingress-and-cert)
(st/instrument `cut/generate-simple-ingress)
(deftest should-generate-rule
(is (= {:host "test.com",
:http
{:paths
[{:pathType "Prefix",
:path "/",
:backend
{:service {:name "myservice", :port {:number 3000}}}}]}}
(cut/generate-host-rule "myservice" 3000 "test.com"))))
(deftest should-generate-ingress
(is (= {:apiVersion "networking.k8s.io/v1",
:kind "Ingress",
:metadata
{:name "test-io-https-ingress",
:namespace "default",
:labels {:app.kubernetes.part-of "c4k-common-app"},
:annotations {:traefik.ingress.kubernetes.io/router.entrypoints
"web, websecure"
:traefik.ingress.kubernetes.io/router.middlewares
"default-redirect-https@kubernetescrd"
:metallb.universe.tf/address-pool "public"}}}
(dissoc (cut/generate-ingress
{:issuer "prod"
:service-name "test-io-service"
:app-name "c4k-common-app"
:service-port 80
:ingress-name "test-io-https-ingress"
:fqdns ["test.de" "www.test.de" "test-it.de"
"www.test-it.de"]}) :spec)))
(is (= {:tls
[{:hosts
["test.de" "www.test.de" "test-it.de" "www.test-it.de"],
:secretName "test-io-cert"}]
:rules
[{:host "test.de",
:http
{:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}
{:host "www.test.de",
:http
{:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}
{:host "test-it.de",
:http
{:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}
{:host "www.test-it.de",
:http
{:paths [{:pathType "Prefix", :path "/", :backend {:service {:name "test-io-service", :port {:number 80}}}}]}}]}
(:spec (cut/generate-ingress {:issuer "prod"
:app-name "c4k-common-app"
:service-name "test-io-service"
:service-port 80
:ingress-name "test-io-https-ingress"
:cert-name "test-io-cert"
:fqdns ["test.de" "www.test.de"
"test-it.de"
"www.test-it.de"]})))))
(deftest should-generate-certificate
(is (= {:apiVersion "cert-manager.io/v1",
@ -21,29 +78,12 @@
:commonName "test.de",
:duration "2160h",
:renewBefore "720h",
:dnsNames ["test.de"],
:issuerRef {:name "staging", :kind "ClusterIssuer"}}}
(cut/generate-certificate {:fqdns ["test.de"]
:dnsNames ["test.de" "test.org" "www.test.de" "www.test.org"],
:issuerRef {:name "prod", :kind "ClusterIssuer"}}}
(cut/generate-certificate {:fqdns ["test.de" "test.org" "www.test.de" "www.test.org"]
:app-name "c4k-common-app"
:cert-name "test-io-cert"}))))
(deftest should-generate-ingress
(is (= {:name "test-io-https-ingress",
:namespace "default",
:labels {:app.kubernetes.part-of "c4k-common-app"},
:annotations {:traefik.ingress.kubernetes.io/router.entrypoints
"web, websecure"
:traefik.ingress.kubernetes.io/router.middlewares
"default-redirect-https@kubernetescrd",
:metallb.universe.tf/address-pool "public"}}
(:metadata (cut/generate-ingress
{:ingress-name "test-io-https-ingress"
:app-name "c4k-common-app"
:service-name "test-io-service" :service-port 80
:cert-name "myCert"
:fqdns ["test.de"]})))))
:cert-name "test-io-cert"
:issuer "prod"}))))
(deftest should-generate-ingress-and-cert
(is (= [{:apiVersion "cert-manager.io/v1",
@ -81,47 +121,4 @@
:port {:number 80}}}}]}}]}}]
(cut/generate-ingress-and-cert {:fqdns ["test.jit.si"]
:service-name "web"
:service-port 80}))))
(deftest should-generate-simple-ingress
(is (= [{:apiVersion "cert-manager.io/v1",
:kind "Certificate",
:metadata
{:name "web",
:labels {:app.kubernetes.part-of "web"},
:namespace "default"},
:spec
{:secretName "web",
:commonName "test.jit.si",
:duration "2160h",
:renewBefore "720h",
:dnsNames ["test.jit.si"],
:issuerRef {:name "staging", :kind "ClusterIssuer"}}}
{:apiVersion "traefik.containo.us/v1alpha1",
:kind "Middleware",
:metadata {:name "web-ratelimit"
:namespace "default"},
:spec {:rateLimit {:average 10, :burst 20}}}
{:apiVersion "networking.k8s.io/v1",
:kind "Ingress",
:metadata
{:name "web",
:namespace "default",
:labels {:app.kubernetes.part-of "web"},
:annotations
{:traefik.ingress.kubernetes.io/router.entrypoints "web, websecure",
:traefik.ingress.kubernetes.io/router.middlewares
"default-redirect-https@kubernetescrd, default-web-ratelimit@kubernetescrd",
:metallb.universe.tf/address-pool "public"}},
:spec
{:tls [{:hosts ["test.jit.si"], :secretName "web"}],
:rules
[{:host "test.jit.si",
:http {:paths [{:path "/",
:pathType "Prefix",
:backend
{:service {:name "web",
:port {:number 80}}}}]}}]}}]
(cut/generate-simple-ingress {:fqdns ["test.jit.si"]
:service-name "web"
:service-port 80}))))
:service-port 80}))))

@ -1,59 +0,0 @@
(ns dda.c4k-common.monitoring.monitoring-internal-test
(:require
#?(:clj [clojure.test :refer [deftest is are testing run-tests]]
:cljs [cljs.test :refer-macros [deftest is are testing run-tests]])
[clojure.string :as str]
[clojure.spec.test.alpha :as st]
[dda.c4k-common.monitoring.monitoring-internal :as cut]))
(st/instrument `cut/generate-stateful-set)
(st/instrument `cut/generate-agent-config)
(st/instrument `cut/generate-config)
(def conf {:cluster-name "clustername"
:cluster-stage "test"
:grafana-cloud-url "https://some.url/with/path"})
(def auth {:grafana-cloud-user "user"
:grafana-cloud-password "password"
:hetzner-cloud-ro-token "ro-token"})
(def invalid-conf {:cluster-name "clustername"
:cluster-stage "test"
:grafana-clud-url "https://some.url/with/path"})
(def invalid-auth {:grafana-cloud-user "user"
:grafana-clod-password "password"
:hetzner-cloud-ro-token "ro-token"})
(deftest should-not-generate-config
(is (thrown?
#?(:clj Exception :cljs js/Error)
(cut/generate-config invalid-conf auth))))
(deftest should-not-generate-auth
(is (thrown?
#?(:clj Exception :cljs js/Error)
(cut/generate-config conf invalid-auth))))
(deftest should-generate-prometheus-remote-write-auth
(is (= {:username "user",
:password "password"}
(get-in
(cut/generate-prometheus-config conf auth)
[:remote_write 0 :basic_auth]))))
(deftest should-generate-prometheus-external-labels
(is (= {:cluster "clustername",
:stage "test"}
(get-in
(cut/generate-prometheus-config conf auth)
[:global :external_labels]))))
(deftest should-generate-config
(is (str/starts-with?
(get-in
(cut/generate-config conf auth)
[:stringData :prometheus.yaml])
"global:\n scrape_interval:")))

@ -2,10 +2,16 @@
(:require
#?(:clj [clojure.test :refer [deftest is are testing run-tests]]
:cljs [cljs.test :refer-macros [deftest is are testing run-tests]])
[clojure.string :as s]
[clojure.spec.test.alpha :as st]
[dda.c4k-common.monitoring :as cut]))
[dda.c4k-common.monitoring :as cut]
[dda.c4k-common.yaml :as yaml]
[clojure.string :as str]))
(st/instrument `cut/generate)
(st/instrument `cut/generate-stateful-set)
(st/instrument `cut/generate-agent-config)
(st/instrument `cut/generate-config)
(def conf {:cluster-name "clustername"
:cluster-stage "test"
@ -15,7 +21,45 @@
:grafana-cloud-password "password"
:hetzner-cloud-ro-token "ro-token"})
(def invalid-conf {:cluster-name "clustername"
:cluster-stage "test"
:grafana-clud-url "https://some.url/with/path"})
(def invalid-auth {:grafana-cloud-user "user"
:grafana-clod-password "password"
:hetzner-cloud-ro-token "ro-token"})
(deftest should-not-generate-config
(is (thrown?
#?(:clj Exception :cljs js/Error)
(cut/generate-config invalid-conf auth))))
(deftest should-not-generate-auth
(is (thrown?
#?(:clj Exception :cljs js/Error)
(cut/generate-config conf invalid-auth))))
(deftest should-generate
(is (= 17
(count (cut/generate conf auth)))))
(deftest should-generate-prometheus-remote-write-auth
(is (= {:username "user",
:password "password"}
(get-in
(cut/generate-prometheus-config conf auth)
[:remote_write 0 :basic_auth]))))
(deftest should-generate-prometheus-external-labels
(is (= {:cluster "clustername",
:stage "test"}
(get-in
(cut/generate-prometheus-config conf auth)
[:global :external_labels]))))
(deftest should-generate-config
(is (s/starts-with?
(get-in
(cut/generate-config conf auth)
[:stringData :prometheus.yaml])
"global:\n scrape_interval:")))

@ -1,18 +0,0 @@
(ns dda.c4k-common.namespace-test
(:require
#?(:clj [clojure.test :refer [deftest is are testing run-tests]]
:cljs [cljs.test :refer-macros [deftest is are testing run-tests]])
[clojure.spec.test.alpha :as st]
[dda.c4k-common.namespace :as cut]))
(st/instrument `cut/generate)
(deftest should-generate-simple-ingress
(is (= [{:apiVersion "v1"
:kind "Namespace"
:metadata {:name "default"}}]
(cut/generate {})))
(is (= [{:apiVersion "v1"
:kind "Namespace"
:metadata {:name "myapp"}}]
(cut/generate {:namespace "myapp"}))))

@ -1,180 +0,0 @@
(ns dda.c4k-common.postgres.postgres-internal-test
(:require
#?(:clj [clojure.test :refer [deftest is are testing run-tests]]
:cljs [cljs.test :refer-macros [deftest is are testing run-tests]])
[clojure.spec.test.alpha :as st]
[dda.c4k-common.postgres.postgres-internal :as cut]))
(st/instrument `cut/generate-config)
(st/instrument `cut/generate-deployment)
(st/instrument `cut/generate-persistent-volume)
(st/instrument `cut/generate-pvc)
(st/instrument `cut/generate-secret)
(st/instrument `cut/generate-service)
(deftest should-generate-config
(is (= {:name "postgres-config",
:namespace "default"
:labels {:app "postgres"}}
(:metadata (cut/generate-config {:postgres-image "postgres:13"
:postgres-size :2gb
:db-name "postgres"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 10
:pvc-storage-class-name "manual"
:namespace "default"}))))
(is (= {:name "postgres-config",
:namespace "myapp"
:labels {:app "postgres"}}
(:metadata (cut/generate-config {:postgres-image "postgres:13"
:postgres-size :2gb
:db-name "postgres"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 10
:pvc-storage-class-name "manual"
:namespace "myapp"}))))
(is (= {:postgres-db "postgres"
:postgresql.conf
"max_connections = 100\nwork_mem = 4MB\nshared_buffers = 512MB\n"}
(:data (cut/generate-config {:postgres-image "postgres:13"
:postgres-size :2gb
:db-name "postgres"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 10
:pvc-storage-class-name "manual"
:namespace "default"}))))
(is (= {:postgres-db "postgres"
:postgresql.conf
"max_connections = 700\nwork_mem = 3MB\nshared_buffers = 2048MB\n"}
(:data (cut/generate-config {:postgres-image "postgres:13"
:postgres-size :8gb
:db-name "postgres"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 10
:pvc-storage-class-name "manual"
:namespace "default"}))))
(is (= {:postgres-db "test"
:postgresql.conf
"max_connections = 100\nwork_mem = 4MB\nshared_buffers = 512MB\n"}
(:data (cut/generate-config {:postgres-image "postgres:13"
:postgres-size :2gb
:db-name "test"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 10
:pvc-storage-class-name "manual"
:namespace "default"}))))
)
(deftest should-generate-deployment
(is (= [{:image "postgres:14"
:name "postgresql"
:env
[{:name "POSTGRES_USER"
:valueFrom
{:secretKeyRef
{:name "postgres-secret", :key "postgres-user"}}}
{:name "POSTGRES_PASSWORD"
:valueFrom
{:secretKeyRef
{:name "postgres-secret", :key "postgres-password"}}}
{:name "POSTGRES_DB"
:valueFrom
{:configMapKeyRef
{:name "postgres-config", :key "postgres-db"}}}]
:ports [{:containerPort 5432, :name "postgresql"}]
:volumeMounts
[{:name "postgres-config-volume"
:mountPath "/etc/postgresql/postgresql.conf"
:subPath "postgresql.conf"
:readOnly true}
{:name "postgre-data-volume"
:mountPath "/var/lib/postgresql/data"}]}]
(get-in (cut/generate-deployment {:postgres-image "postgres:14"
:postgres-size :2gb
:db-name "test"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 10
:pvc-storage-class-name "manual"
:namespace "default"})
[:spec :template :spec :containers])))
(is (= {:name "postgresql",
:namespace "myapp"}
(:metadata (cut/generate-deployment {:postgres-image "postgres:14"
:postgres-size :2gb
:db-name "test"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 10
:pvc-storage-class-name "manual"
:namespace "myapp"})))))
(deftest should-generate-persistent-volume
(is (= {:kind "PersistentVolume"
:apiVersion "v1"
:metadata
{:name "postgres-pv-volume",
:namespace "default"
:labels {:type "local"}}
:spec
{:storageClassName "manual"
:accessModes ["ReadWriteOnce"]
:capacity {:storage "20Gi"}
:hostPath {:path "xx"}}}
(cut/generate-persistent-volume {:postgres-image "postgres:14"
:postgres-size :2gb
:db-name "test"
:pvc-storage-class-name "manual"
:postgres-data-volume-path "xx"
:pv-storage-size-gb 20
:namespace "default"}))))
(deftest should-generate-persistent-volume-claim
(is (= {:apiVersion "v1"
:kind "PersistentVolumeClaim"
:metadata
{:name "postgres-claim",
:namespace "default"
:labels {:app "postgres"}}
:spec
{:storageClassName "local-path"
:accessModes ["ReadWriteOnce"]
:resources {:requests {:storage "20Gi"}}}}
(cut/generate-pvc {:postgres-image "postgres:13"
:postgres-size :2gb
:db-name "postgres"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 20
:pvc-storage-class-name "local-path"
:namespace "default"}))))
(deftest should-generate-secret
(is (= {:apiVersion "v1"
:kind "Secret"
:metadata {:name "postgres-secret" :namespace "default"}
:type "Opaque"
:data
{:postgres-user "eHgtdXM=", :postgres-password "eHgtcHc="}}
(cut/generate-secret {:postgres-image "postgres:13"
:postgres-size :2gb
:db-name "postgres"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 20
:pvc-storage-class-name "local-path"
:namespace "default"}
{:postgres-db-user "xx-us" :postgres-db-password "xx-pw"}))))
(deftest should-generate-service
(is (= {:name "postgresql-service" :namespace "default"}
(:metadata (cut/generate-service
{:postgres-image "postgres:13"
:postgres-size :2gb
:db-name "postgres"
:postgres-data-volume-path "/var/postgres"
:pv-storage-size-gb 20
:pvc-storage-class-name "local-path"
:namespace "default"})))))

@ -3,55 +3,132 @@
#?(:clj [clojure.test :refer [deftest is are testing run-tests]]
:cljs [cljs.test :refer-macros [deftest is are testing run-tests]])
[clojure.spec.test.alpha :as st]
[dda.c4k-common.test-helper :as ct]
[dda.c4k-common.postgres :as cut]))
(st/instrument `cut/generate-config)
(st/instrument `cut/generate-deployment)
(st/instrument `cut/generate-persistent-volume)
(st/instrument `cut/generate-pvc)
(st/instrument `cut/generate-secret)
(st/instrument `cut/generate)
(st/instrument `cut/generate-service)
(deftest should-generate-config
(is (= {:postgres-db "postgres"
:postgresql.conf
"max_connections = 100\nwork_mem = 4MB\nshared_buffers = 512MB\n"}
(:data (cut/generate-config)))))
(:data (cut/generate-config))))
(is (= {:postgres-db "postgres"
:postgresql.conf
"max_connections = 700\nwork_mem = 3MB\nshared_buffers = 2048MB\n"}
(:data (cut/generate-config {:postgres-size :8gb}))))
(is (= {:postgres-db "test"
:postgresql.conf
"max_connections = 100\nwork_mem = 4MB\nshared_buffers = 512MB\n"}
(:data (cut/generate-config {:db-name "test"}))))
)
(deftest should-generate-config-diff
(is (= {:postgres-db-c1 "postgres",
:postgres-db-c2 "test",
:postgresql.conf-c1 "max_connections = 100\nwork_mem = 4MB\nshared_buffers = 512MB\n",
:postgresql.conf-c2 "max_connections = 700\nwork_mem = 3MB\nshared_buffers = 2048MB\n"}
(ct/map-diff (cut/generate-config) (cut/generate-config {:db-name "test" :postgres-size :8gb})))))
(deftest should-generate-persistent-volume
(is (= {:kind "PersistentVolume"
:apiVersion "v1"
:metadata
{:name "postgres-pv-volume", :namespace "default" :labels {:type "local"}}
{:name "postgres-pv-volume", :labels {:type "local"}}
:spec
{:storageClassName "manual"
:accessModes ["ReadWriteOnce"]
:capacity {:storage "10Gi"}
:hostPath {:path "xx"}}}
(cut/generate-persistent-volume {:postgres-data-volume-path "xx"}))))
(cut/generate-persistent-volume {:postgres-data-volume-path "xx"})))
(is (= {:kind "PersistentVolume"
:apiVersion "v1"
:metadata
{:name "postgres-pv-volume", :labels {:type "local"}}
:spec
{:storageClassName "manual"
:accessModes ["ReadWriteOnce"]
:capacity {:storage "20Gi"}
:hostPath {:path "xx"}}}
(cut/generate-persistent-volume {:postgres-data-volume-path "xx"
:pv-storage-size-gb 20}))))
(deftest should-generate-persistent-volume-diff
(is (= {:storage-c1 "10Gi", :storage-c2 "20Gi",
:path-c1 "/var/postgres", :path-c2 "xx"}
(ct/map-diff (cut/generate-persistent-volume {})
(cut/generate-persistent-volume {:postgres-data-volume-path "xx"
:pv-storage-size-gb 20})))))
(deftest should-generate-persistent-volume-claim
(is (= {:apiVersion "v1"
:kind "PersistentVolumeClaim"
:metadata
{:name "postgres-claim", :namespace "default" :labels {:app "postgres"}}
{:name "postgres-claim", :labels {:app "postgres"}}
:spec
{:storageClassName "manual"
:accessModes ["ReadWriteOnce"]
:resources {:requests {:storage "10Gi"}}}}
(cut/generate-pvc {}))))
(cut/generate-pvc {})))
(is (= {:apiVersion "v1"
:kind "PersistentVolumeClaim"
:metadata
{:name "postgres-claim", :labels {:app "postgres"}}
:spec
{:storageClassName "local-path"
:accessModes ["ReadWriteOnce"]
:resources {:requests {:storage "20Gi"}}}}
(cut/generate-pvc {:pv-storage-size-gb 20
:pvc-storage-class-name "local-path"}))))
(deftest should-generate-persistent-volume-claim-diff
(is (= {:storageClassName-c1 "manual", :storageClassName-c2 "local-path",
:storage-c1 "10Gi", :storage-c2 "20Gi"}
(ct/map-diff (cut/generate-pvc {})
(cut/generate-pvc {:pv-storage-size-gb 20
:pvc-storage-class-name "local-path"})))))
(deftest should-generate-secret
(is (= {:apiVersion "v1",
:kind "Secret",
:metadata {:name "postgres-secret", :namespace "default"},
:type "Opaque",
:data {:postgres-user "eHgtdXM=", :postgres-password "eHgtcHc="}}
(is (= {:apiVersion "v1"
:kind "Secret"
:metadata {:name "postgres-secret"}
:type "Opaque"
:data
{:postgres-user "eHgtdXM=", :postgres-password "eHgtcHc="}}
(cut/generate-secret {:postgres-db-user "xx-us" :postgres-db-password "xx-pw"}))))
(deftest should-generate-deployment
(is (= [{:image "postgres:14"
:name "postgresql"
:env
[{:name "POSTGRES_USER"
:valueFrom
{:secretKeyRef
{:name "postgres-secret", :key "postgres-user"}}}
{:name "POSTGRES_PASSWORD"
:valueFrom
{:secretKeyRef
{:name "postgres-secret", :key "postgres-password"}}}
{:name "POSTGRES_DB"
:valueFrom
{:configMapKeyRef
{:name "postgres-config", :key "postgres-db"}}}]
:ports [{:containerPort 5432, :name "postgresql"}]
:volumeMounts
[{:name "postgres-config-volume"
:mountPath "/etc/postgresql/postgresql.conf"
:subPath "postgresql.conf"
:readOnly true}
{:name "postgre-data-volume"
:mountPath "/var/lib/postgresql/data"}]}]
(get-in (cut/generate-deployment {:postgres-image "postgres:14"})
[:spec :template :spec :containers]))))
(deftest should-generate
(is (= 6
(count (cut/generate {}
{:postgres-db-user "user"
:postgres-db-password "password"})))))
(deftest should-generate-deployment-diff
(is (= {:image-c1 "postgres:13", :image-c2 "postgres:14"}
(ct/map-diff (cut/generate-deployment) (cut/generate-deployment {:postgres-image "postgres:14"})))))

@ -1,34 +0,0 @@
(ns dda.c4k-common.browser-test
(:require
[cljs.test :refer-macros [deftest is are testing run-tests]]
[clojure.spec.test.alpha :as st]
[dda.c4k-common.browser :as cut]))
(st/instrument `cut/print-debug)
(st/instrument `cut/get-element-by-id)
(st/instrument `cut/get-content-from-element)
(st/instrument `cut/deserialize-content)
(st/instrument `cut/get-content-value-from-element)
(st/instrument `cut/set-validation-result!)
(st/instrument `cut/validate!)
(st/instrument `cut/set-output!)
(st/instrument `cut/set-form-validated!)
(st/instrument `cut/create-js-obj-from-html)
(st/instrument `cut/append-to-c4k-content)
(st/instrument `cut/append-hickory)
(st/instrument `cut/generate-feedback-tag)
(st/instrument `cut/generate-label)
(st/instrument `cut/generate-br)
(st/instrument `cut/generate-input-field)
(st/instrument `cut/generate-text-area)
(st/instrument `cut/generate-button)
(st/instrument `cut/generate-output)
(st/instrument `cut/generate-needs-validation)
(st/instrument `cut/generate-group)
(deftest should-deserialize-content
(is (= (cut/deserialize-content " " identity true) nil))
(is (= (cut/deserialize-content "test" keyword false) :test))
(is (= (cut/deserialize-content "test" identity false) "test"))
(is (= (cut/deserialize-content "test" identity true) "test")))

@ -1,14 +0,0 @@
(ns dda.c4k-common.macros-test
(:require
[cljs.test :refer-macros [deftest is]]
[dda.c4k-common.macros :refer-macros [inline-resources]]))
(deftest should-count-inline-resources
(is (= 4 (count (inline-resources "ingress")))))
(deftest should-inline-resources
(let [resource-path (fn [name] (str "dda/c4k_common/inline_resources_test/" name))
inlined-resources (inline-resources "dda/c4k_common/inline_resources_test")]
(is (= "1" (get inlined-resources (resource-path "inline_resource_1.yaml"))))
(is (= "2" (get inlined-resources (resource-path "inline_resource_2.yaml"))))
(is (= "3" (get inlined-resources (resource-path "inline_resource_3.yaml"))))))
Loading…
Cancel
Save