diff --git a/.posthtmlrc b/.posthtmlrc new file mode 100644 index 0000000..6537f5d --- /dev/null +++ b/.posthtmlrc @@ -0,0 +1,6 @@ +{ + "plugins": { + "posthtml-doctype": { "doctype": "HTML 5" }, + "posthtml-include": {} + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5aa686a --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +RELEASE=website +NS=katenary + +build: dist chart + +chart: dist + katenary convert -f -p deploy + +dist: $(wildcard src/* compose.yaml) + rm -rf dist + podman run --rm -it -u $(id -u):$(id -g) -v $(PWD):/app -w /app node:alpine sh -c "yarn install && yarn parcel build --no-source-maps" + +deploy: + helm -n $(NS) upgrade --install $(RELEASE) ./chart/ -f override.yaml --create-namespace + sleep 1 + $(MAKE) add-redirect + kubectl -n $(NS) rollout restart deployment $(RELEASE)-server + +add-redirect: + kubectl -n $(NS) apply -f <(./venv/bin/python add-domain.py) + +serve: + rm -rf dist + podman compose up diff --git a/add-domain.py b/add-domain.py new file mode 100644 index 0000000..b86d376 --- /dev/null +++ b/add-domain.py @@ -0,0 +1,74 @@ +"""Script to add a domain without tht "www" and force redirect to www with TLS + +To apply the changes to the cluster, you can use the following command: + + kubectl -n apply -f <(python add-domain.py) + + +""" + +import os +import subprocess + +import yaml + + +def get_ingress(ns: str, name: str) -> dict: + """Get the ingress object from the cluster""" + process = subprocess.Popen( + ["kubectl", "get", "ingress", "-n", ns, name, "-o", "yaml"], + stdout=subprocess.PIPE, + ) + stdout, _ = process.communicate() + return yaml.safe_load(stdout) + + +def tranform(name: str, ns: str, domain: str) -> dict: + """Return the transformed ingress object""" + + # get the ingress content + ingress = get_ingress(ns, name) + + # remove all nginx annotations + ingress["metadata"]["annotations"] = { + k: v for k, v in ingress["metadata"]["annotations"].items() if "nginx" not in k + } + + # change the name of the ingress + ingress["metadata"]["name"] = f"{name}-redirect" + + # add nginx.ingress.kubernetes.io/permanent-redirect annotation + ingress["metadata"]["annotations"].update( + {"nginx.ingress.kubernetes.io/permanent-redirect": f"https://www.{domain}"} + ) + + # change hostname + ingress["spec"]["tls"][0]["hosts"] = [domain] + ingress["spec"]["rules"][0]["host"] = domain + + # change the secret name + ingress["spec"]["tls"][0]["secretName"] = f"{name}-redirect" + + # cleanup the metadata + ingress.pop("status") + to_remove = [ + "creationTimestamp", + "generation", + "resourceVersion", + "selfLink", + "uid", + ] + for meta in to_remove: + ingress["metadata"].pop(meta) if meta in ingress["metadata"] else None + + # print the new yaml content + return ingress + + +if __name__ == "__main__": + name = os.getenv("NAME", "website-server") # name of the ingress + ns = os.getenv("NAMESPACE", "katenary") # namespace + domain = os.getenv("DOMAIN", "katenary.org") # domain name without www + transformed = tranform(name, ns, domain) + + print(yaml.dump(transformed)) diff --git a/compose.katenary.yaml b/compose.katenary.yaml new file mode 100644 index 0000000..81631b9 --- /dev/null +++ b/compose.katenary.yaml @@ -0,0 +1,14 @@ +services: + server: + image: docker.io/nginx + ports: + - 8080:80 + volumes: + - ./dist:/usr/share/nginx/html + labels: + katenary.v3/ingress: |- + hostname: katenary.org + port: 80 + + katenary.v3/configmap-files: |- + - ./dist diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..a6b2f0d --- /dev/null +++ b/compose.yaml @@ -0,0 +1,17 @@ +services: + dev: + image: docker.io/node:alpine + volumes: + - ./:/app:z + working_dir: /app + user: ${UID}:${GROUPS} + command: + - sh + - -c + - |- + yarn install + yarn parcel serve --dist-dir /tmp/dist + ports: + - 1234:1234 + labels: + katenary.v3/ignore: true diff --git a/src/bg-head.webp b/src/bg-head.webp new file mode 100644 index 0000000..fc0a0b3 Binary files /dev/null and b/src/bg-head.webp differ diff --git a/src/icon.ico b/src/icon.ico new file mode 100644 index 0000000..0a4b332 Binary files /dev/null and b/src/icon.ico differ diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..bee68e8 --- /dev/null +++ b/src/index.html @@ -0,0 +1,49 @@ + + + + + + + + + + Katenary - Effortless Helm Chart Conversion + + + + + + + + + + + + + + +
+ + + + + +
+ + + + diff --git a/src/links.js b/src/links.js new file mode 100644 index 0000000..97c5660 --- /dev/null +++ b/src/links.js @@ -0,0 +1,60 @@ +document.addEventListener("DOMContentLoaded", () => { + const sections = document.querySelectorAll("main > section, body > header"); + const links = document.querySelectorAll("nav a:not([target=_blank])"); + const linksArray = Array.from(links); + + let scrollingOnClick = false; // Deactivate the observer if the scroll is on a link click + + // Intersection Observer + const observer = new IntersectionObserver( + (entries) => { + if (scrollingOnClick) return; // ignore if the scroll is manual + + entries.forEach((entry) => { + if (entry.isIntersecting) { + const id = entry.target.id; + const link = linksArray.find((link) => link.href.includes(`#${id}`)); + + linksArray.forEach((link) => link.classList.remove("active")); + if (link) link.classList.add("active"); + + // update URL + history.replaceState(null, null, `#${id}`); + } + }); + }, + { rootMargin: "-50% 0px -50% 0px" }, + ); + + sections.forEach((section) => observer.observe(section)); + + // Gestion du clic sur un lien + linksArray.forEach((link) => { + link.addEventListener("click", (e) => { + e.preventDefault(); // avoid default navigation + + const targetId = link.getAttribute("href").slice(1); + const targetSection = document.getElementById(targetId); + + // deactivate observer temporarily + scrollingOnClick = true; + + // scroll to the target + targetSection.scrollIntoView({ behavior: "smooth" }); + + // update link state + linksArray.forEach((link) => link.classList.remove("active")); + link.classList.add("active"); + + // update URL + history.replaceState(null, null, `#${targetId}`); + + // on scroll end, reactivate the observer + const onScrollEnd = () => { + scrollingOnClick = false; + window.removeEventListener("scroll", onScrollEnd); // Nettoyage + }; + window.addEventListener("scroll", onScrollEnd); + }); + }); +}); diff --git a/src/main.css b/src/main.css new file mode 100644 index 0000000..11f98ec --- /dev/null +++ b/src/main.css @@ -0,0 +1,535 @@ +/* styles.css */ +:root { + --primary-color: #2a1827; + --secondary-color: #6c757d; + --dark-color: #343a40; + --light-color: #f4f4f4; + --white: #fff; + --danger-color: #dc3545; + --covered: 50%; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: Arial, sans-serif; + line-height: 1.6; + margin: 0; + padding: 0; + color: #333; + overflow-x: hidden; +} + +img { + max-width: 100%; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.header { + background: var(--dark-color); + background-image: url(./bg-head.webp); + background-size: cover; + background-repeat: no-repeat; + background-position: bottom; + background-attachment: fixed; + background-blend-mode: multiply; + color: #fff; + text-align: center; + padding: 50px 20px; +} + +.header .container { + display: flex; + flex-direction: column; + justify-content: space-evenly; + display: flex; + height: 100%; +} + +@media (min-width: 768px) { + .header { + display: flex; + align-items: center; + height: 100vh; + padding: 0; + padding-bottom: 115px; + } + + main>section { + flex: 1; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + } +} + +.header h1 { + font-size: 3rem; + margin: 0; + background-image: url("https://github.com/Katenary/katenary/raw/refs/heads/develop/doc/docs/statics/logo-vertical.svg"); + color: transparent; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + height: 265px; +} + +.header p { + font-size: 1.2rem; + margin: 20px 0; +} + +.btn-primary { + display: inline-block; + background: #fff; + color: var(--primary-color); + padding: 10px 20px; + border-radius: 5px; + text-decoration: none; + font-weight: bold; + margin-top: 20px; +} + +.btn-primary:hover { + background: #0056b3; + color: #fff; +} + +.features { + background: var(--light-color); + padding: 50px 20px; + text-align: center; +} + +.features h2 { + font-size: 2.5rem; + margin-bottom: 20px; +} + +.feature-grid { + /* + display: grida; + gap: 20px; + margin-top: 20px; + grid-auto-rows: 200px; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + */ + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; +} + +@keyframes slidingLeft { + from { + transform: translateX(-100vw); + filter: opacity(0) blur(10px); + } + + to { + transform: translateX(0); + filter: opacity(1) blur(0); + } +} + +@keyframes slidingRight { + from { + transform: translateX(100vw); + filter: opacity(0) blur(10px); + } + + to { + transform: translateX(0); + filter: opacity(1) blur(0); + } +} + +.feature-item { + background: var(--white); + border: 1px solid #ddd; + flex: 1 1 200px; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +@media (min-width: 768px) { + :root { + --animation-slide: entry 0% contain 0%; + } + + .from-left { + view-timeline: --reveal-blockl; + view-timeline-axis: block; + animation: linear slidingLeft forwards; + animation-range: var(--animation-slide); + animation-timeline: --reveal-blockl; + } + + .from-right { + view-timeline: --reveal-blockr; + view-timeline-axis: block; + animation: linear slidingRight forwards; + animation-range: var(--animation-slide); + animation-timeline: --reveal-blockr; + } +} + +.how-it-works { + padding: 50px 20px; + text-align: center; +} + +.how-to-use { + padding: 50px 20px; + text-align: center; +} + +.tutorials { + padding: 50px 20px; + text-align: center; +} + +h2 { + font-size: 2.5rem; + margin-bottom: 20px; + text-align: center; +} + +.how-it-works ol { + list-style: decimal inside; + text-align: left; + max-width: 800px; + margin: 0 auto; + padding: 0; +} + +.image-placeholder { + margin: 30px auto; + height: 200px; + width: 100%; + max-width: 800px; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.2rem; + border-radius: 8px; +} + +.image-placeholder img { + margin: 10px; + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; +} + +.get-started { + background: var(--primary-color); + color: var(--white); + padding: 50px 20px; + text-align: center; +} + +.get-started pre { + background: var(--white); + color: #333; + padding: 10px; + margin: 20px 0; + border-radius: 5px; +} + +div.video { + padding-top: 25px; +} + +pre { + text-align: left; +} + +@media (min-width: 768px) { + .example { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + } + + iframe { + width: 100%; + height: 500px; + } +} + +iframe { + max-width: 100%; +} + +section { + width: 100%; +} + +section:nth-child(even) .example { + text-align: right; + flex-direction: row-reverse !important; +} + +section:nth-child(odd) .example { + text-align: left; + flex-direction: row; +} + +section:nth-child(even) .example h3 { + text-align: right; +} + +.example>div { + flex: 1 1 200px; + align-content: center; +} + +code.hljs { + border-radius: 5px; + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.5); +} + +p code { + background: var(--dark-color); + color: var(--light-color); + padding: 1px 5px; + border-radius: 5px; + display: inline-block; +} + +section.alternate:nth-child(odd) { + background: var(--light-color); + color: var(--dark-color); +} + +section.alternate:nth-child(even) { + background: var(--dark-color); + color: var(--light-color); +} + +#menu-toggle { + position: fixed; + z-index: 11; + margin: 0; +} + +@media (min-width: 768px) { + #menu-toggle { + display: none; + } + + .navbar { + background: var(--white); + color: var(--dark-color); + display: flex; + justify-content: space-evenly; + align-items: center; + position: fixed; + top: 0px; + z-index: 10; + width: 100%; + } + + .navbar .container { + padding: 0; + } + + nav ul { + display: flex; + flex-wrap: wrap; + list-style: none; + } + + nav ul li { + margin: 0 10px; + } + + nav ul li a { + color: var(--dark-color); + text-decoration: none; + display: block; + padding: 10px; + } + + nav ul li a:hover { + color: var(--light-color); + background: var(--dark-color); + } + + nav ul li a.active { + color: var(--light-color) !important; + background: var(--dark-color) !important; + } +} + +@media (max-width: 768px) { + .navbar { + display: none !important; + } + + .navbar:hover, + #menu-toggle:focus~.navbar { + display: block !important; + } + + .navbar { + opacity: 0.8; + position: fixed; + top: 10px; + left: 0px; + color: var(--white); + font-size: 1rem; + cursor: pointer; + background: var(--dark-color); + color: var(--white); + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 20px; + border: 1px solid var(--dark-color); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + border-radius: 5px; + } + + .navbar:has(a:active) { + display: none; + } + + nav ul { + display: block; + list-style: none; + padding: 0; + margin: 0; + } + + nav ul li { + margin: 0; + } + + nav ul li a { + color: var(--white); + text-decoration: none; + display: block; + padding: 10px; + } +} + +footer { + display: flex; + flex-wrap: wrap; + background: #333; + color: #fff; + text-align: center; + padding: 20px 0; + font-size: 0.9rem; + flex-wrap: wrap; +} + +footer section { + padding: 20px; + text-align: center; + flex: 1 1 250px; +} + +footer section p { + font-weight: bold; +} + +footer ul { + list-style: none; + padding: 0; +} + +footer ul li { + margin: 5px 0; +} + +footer a { + color: var(--white); +} + +footer .container { + display: flex; + flex: 1 1 auto; + justify-content: space-between; + align-items: start; + width: 100%; + flex-wrap: wrap; +} + +footer .container>p { + flex: 1 1 auto; + text-align: center; +} + +footer section { + flex: 1 1 auto; + align-self: normal; + position: relative; +} + +@media (min-width: 768px) { + footer section:after { + content: " "; + display: block; + background: #fff; + position: absolute; + right: 0; + top: 5%; + height: 90%; + width: 1px; + filter: opacity(0.5) blur(1px); + } + + footer section:last-child:after { + display: none; + } +} + +.large-icon { + font-size: 3rem; +} + +@keyframes bouncing { + 0% { + transform: translateY(0); + } + + 50% { + transform: translateY(-20px); + } + + 100% { + transform: translateY(0); + } +} + +.down { + position: relative; + bottom: 0; +} + +a.down { + background: var(--white); + color: var(--dark-color); + width: 120px; + height: 120px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + align-self: center; + animation: bouncing 2s infinite; +} diff --git a/src/partials/features.html b/src/partials/features.html new file mode 100644 index 0000000..a6d8330 --- /dev/null +++ b/src/partials/features.html @@ -0,0 +1,41 @@ +
+
+

Why Katenary?

+

+ Simplify your deployment workflow by converting Compose files into + production-ready Helm Charts with ease. +

+
+
+

Automated Conversion

+

+ Generate complete Helm Charts from your Compose files effortlessly. +

+
+
+

Flexible Configuration

+

Customize deployments with `values.yaml` and environment labels.

+
+
+

Dependency Management

+

+ Ensure proper service startup sequences using `depends_on` support. +

+
+
+

Open Source

+

Free, opensource, under the MIT license!

+
+
+
+ +
+
+
diff --git a/src/partials/footer.html b/src/partials/footer.html new file mode 100644 index 0000000..7119f96 --- /dev/null +++ b/src/partials/footer.html @@ -0,0 +1,53 @@ + diff --git a/src/partials/get-started.html b/src/partials/get-started.html new file mode 100644 index 0000000..4017c56 --- /dev/null +++ b/src/partials/get-started.html @@ -0,0 +1,12 @@ +
+
+

Get Started

+

Download Katenary’s binary and start using it today.

+
sh <(curl -sSL https://raw.githubusercontent.com/Katenary/katenary/master/install.sh)
+

+ Go to GitHub +

+
+
diff --git a/src/partials/header.html b/src/partials/header.html new file mode 100644 index 0000000..8ae7203 --- /dev/null +++ b/src/partials/header.html @@ -0,0 +1,12 @@ +
+
+

Katenary

+

Effortless Helm Chart Conversion for Kubernetes Deployments

+ +
+
diff --git a/src/partials/how-it-works.html b/src/partials/how-it-works.html new file mode 100644 index 0000000..702ab6e --- /dev/null +++ b/src/partials/how-it-works.html @@ -0,0 +1,45 @@ +
+
+

How It Works

+

+ Katenary simply read your compose.yaml file (or + docker-compose.yaml) and use + official libraries to read it and generate Kubernetes + resources as YAML. +

+

+ Then, it adds templating conditions, values file, define a + Chart.yaml file, adapt dependencies if needed, and many + others things. +

+

+ Using configuration files to be mounted? No problem, + Katenary will create ConfigMaps if you declared that thes + directories or files are statics. +
+ (Do not do this for sources of your project, use it for simple + configuration files) +

+

+ The result is a complete "Helm Chart" that can be installed, configured, + packaged and shared. +

+
+ Katenary Workflow +
+

+ Almost everything can be overriden as Ingresses, Dependencies, values, + environment variables, secrets... +

+
    +
  1. Add optional labels to your Compose files.
  2. +
  3. Run katenary convert from the command line.
  4. +
  5. Deploy the generated Helm Chart in Kubernetes.
  6. +
+
+
diff --git a/src/partials/how-to-use.html b/src/partials/how-to-use.html new file mode 100644 index 0000000..b4400a8 --- /dev/null +++ b/src/partials/how-to-use.html @@ -0,0 +1,92 @@ +
+
+

How to use?

+

+ Install the binary, and use katenar convert command line + inside your project directory +

+

+ You can adapt your compose YAML file with labels, or add a + compose.katenary.yaml file to override your project. +

+

+ You may also use a specific katenary.yaml file that accepts + the directives without using labels. +

+
+
+
+

Only add labels! (if needed!)

+

+ You can adapt, configure, or change the conversion behaviour addind + labels. +

+

+ It + doesn't change the docker compose or + podman compose + behaviour. It is only used while using + katenary compose command line +

+

+ There are + plenty of labels + to help you to customize and adapt the resulting Helm Chart. +

+
+
+

+# your "docker-compose.yml", or "compose.yaml"
+services:
+web:
+  image: docker.io/nginx:latest
+  ports:
+    - "80:80"
+  labels:
+    # generate an ingress resource in the Helm Chart
+    katenary.io/ingress: |-
+      hostname: example.com
+      port: 80
+          
+
+
+
+ +
+
+
+

Ease the deployment

+

+ Kubernetes somtimes lacks of automation. Katenary helps you to add + what is needed, like having a + depends_on feature. +

+
+
+

+# your "docker-compose.yml", or "compose.yaml"
+services:
+  db:
+    image: docker.io/postgres:latest
+    # ...
+    labels:
+      katenary.v3/ports: |-
+        - 5432
+      
+  web:
+    image: php:fpm
+    # ...
+    depends_on:
+      - db
+            
+
+
+
+
+
diff --git a/src/partials/navbar.html b/src/partials/navbar.html new file mode 100644 index 0000000..7956c7b --- /dev/null +++ b/src/partials/navbar.html @@ -0,0 +1,24 @@ + + diff --git a/src/partials/tutorials.html b/src/partials/tutorials.html new file mode 100644 index 0000000..bd013ac --- /dev/null +++ b/src/partials/tutorials.html @@ -0,0 +1,22 @@ +
+
+

Watch the Tutorials

+

+ A playlist + is progressivelly filled to help the Katenary adoption. Take a look and + learn how it is simple. +

+ +
+