chore(katenary): Use the new organization repository

This commit is contained in:
2025-07-13 12:15:12 +02:00
parent 74b73cafc1
commit ffde44ae57
18 changed files with 1080 additions and 0 deletions

6
.posthtmlrc Normal file
View File

@@ -0,0 +1,6 @@
{
"plugins": {
"posthtml-doctype": { "doctype": "HTML 5" },
"posthtml-include": {}
}
}

24
Makefile Normal file
View File

@@ -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

74
add-domain.py Normal file
View File

@@ -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 <NAMESPACE> 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))

14
compose.katenary.yaml Normal file
View File

@@ -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

17
compose.yaml Normal file
View File

@@ -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

BIN
src/bg-head.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
src/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

49
src/index.html Normal file
View File

@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Katenary: Effortless Helm Chart Conversion"
/>
<meta
name="keywords"
content="Katenary, Helm, Kubernetes, Compose, Docker, Podman, Helm chart"
/>
<meta name="author" content="Patrice Ferlet (aka metal3d)" />
<link rel="icon" href="icon.ico" />
<title>Katenary - Effortless Helm Chart Conversion</title>
<link rel="stylesheet" href="main.css" />
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/panda-syntax-dark.min.css"
/>
<script src="links.js" defer></script>
<script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script>
<script defer>
hljs.highlightAll();
</script>
</head>
<body>
<include src="src/partials/navbar.html"></include>
<include src="src/partials/header.html"></include>
<main>
<include src="src/partials/features.html"></include>
<include src="src/partials/how-it-works.html"></include>
<include src="src/partials/how-to-use.html"></include>
<include src="src/partials/tutorials.html"></include>
<include src="src/partials/get-started.html"></include>
</main>
<include src="src/partials/footer.html"></include>
</body>
</html>

60
src/links.js Normal file
View File

@@ -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);
});
});
});

535
src/main.css Normal file
View File

@@ -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;
}

View File

@@ -0,0 +1,41 @@
<section class="features" id="features">
<div class="container">
<h2>Why Katenary?</h2>
<p>
Simplify your deployment workflow by converting Compose files into
production-ready Helm Charts with ease.
</p>
<div class="feature-grid from-left">
<div class="feature-item">
<h3>Automated Conversion</h3>
<p>
Generate complete Helm Charts from your Compose files effortlessly.
</p>
</div>
<div class="feature-item">
<h3>Flexible Configuration</h3>
<p>Customize deployments with `values.yaml` and environment labels.</p>
</div>
<div class="feature-item">
<h3>Dependency Management</h3>
<p>
Ensure proper service startup sequences using `depends_on` support.
</p>
</div>
<div class="feature-item">
<h3>Open Source</h3>
<p>Free, opensource, under the <strong>MIT license!</strong></p>
</div>
</div>
<div class="video from-right">
<iframe
src="https://www.youtube.com/embed/RrX5jNxS9IA?si=i0kRVFXOwT3ZqZdB"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
</div>
</div>
</section>

53
src/partials/footer.html Normal file
View File

@@ -0,0 +1,53 @@
<footer class="footer">
<div class="container">
<section>
<div>
<h2>Special thanks</h2>
<p>Content creation:</p>
<ul>
<li>
<a href="https://blender.org">Blender</a> that helps a lot to create
videos and animations
</li>
<li>
<a href="https://highlightjs.org">Highlight.js</a> for the code
syntax highlighting
</li>
</ul>
<p>For graphical elements</p>
<ul>
<li>
<a href="https://inkscape.org">Inkscape</a> for the icons and images
</li>
<li>
<a href="https://gimp.org">The Gimp</a> for many others image
manipulation
</li>
</ul>
</div>
</section>
<section>
<div>
<h2>Links</h2>
<p>For this website:</p>
<ul>
<li>
<a href="https://github.com/Katenary/katenary">Katenary on GitHub</a>
to follow, star, and contribute
</li>
<li>
<a
href="https://matrix.to/#/!JuGCanxvQEESclXmfX:matrix.org?via=matrix.org"
>Discussion on Matrix.org</a
>
the official Matrix channel for Katenary
</li>
</ul>
</div>
</section>
</div>
<div class="container">
<p>&copy; 2024, 2025 Katenary. Open Source Project under MIT License.</p>
</div>
</footer>

View File

@@ -0,0 +1,12 @@
<section class="get-started" id="get-started">
<div class="container">
<h2>Get Started</h2>
<p>Download Katenarys binary and start using it today.</p>
<pre><code>sh &lt;(curl -sSL https://raw.githubusercontent.com/Katenary/katenary/master/install.sh)</code></pre>
<p>
<a href="https://github.com/Katenary/katenary" class="btn-primary"
>Go to GitHub</a
>
</p>
</div>
</section>

12
src/partials/header.html Normal file
View File

@@ -0,0 +1,12 @@
<header class="header">
<div class="container">
<h1>Katenary</h1>
<p>Effortless Helm Chart Conversion for Kubernetes Deployments</p>
<a
href="#features"
class="down-arrow large-icon down"
title="Scroll down to learn more"
><i class="fa fa-arrow-down"></i
></a>
</div>
</header>

View File

@@ -0,0 +1,45 @@
<section class="how-it-works" id="how-it-works">
<div class="container">
<h2>How It Works</h2>
<p>
Katenary simply read your <code>compose.yaml</code> file (or
<code>docker-compose.yaml</code>) and use
<strong>official libraries</strong> to read it and generate Kubernetes
resources as YAML.
</p>
<p>
Then, it adds templating conditions, values file, define a
<code>Chart.yaml</code> file, adapt dependencies if needed, and many
others things.
</p>
<p>
<strong>Using configuration files to be mounted?</strong> No problem,
Katenary will create <code>ConfigMaps</code> if you declared that thes
directories or files are statics.
<br />
<small
>(Do not do this for sources of your project, use it for simple
configuration files)</small
>
</p>
<p>
The result is a complete "Helm Chart" that can be installed, configured,
packaged and shared.
</p>
<div class="image-placeholder">
<img
src="https://github.com/Katenary/katenary/raw/refs/heads/develop/doc/docs/statics/workflow.svg"
alt="Katenary Workflow"
/>
</div>
<p>
Almost everything can be overriden as Ingresses, Dependencies, values,
environment variables, secrets...
</p>
<ol>
<li>Add optional labels to your Compose files.</li>
<li>Run <code>katenary convert</code> from the command line.</li>
<li>Deploy the generated Helm Chart in Kubernetes.</li>
</ol>
</div>
</section>

View File

@@ -0,0 +1,92 @@
<section class="how-to-use" id="how-to-use">
<div class="container">
<h2>How to use?</h2>
<p>
Install the binary, and use <code>katenar convert</code> command line
inside your project directory
</p>
<p>
You can adapt your compose YAML file with labels, or add a
<code>compose.katenary.yaml</code> file to override your project.
</p>
<p>
You may also use a specific <code>katenary.yaml</code> file that accepts
the directives without using labels.
</p>
<section class="alternate">
<div class="container example">
<div class="from-left">
<h3>Only add labels! (if needed!)</h3>
<p>
You can adapt, configure, or change the conversion behaviour addind
labels.
</p>
<p>
It
<strong
>doesn't change the <code>docker compose</code> or
<code>podman compose</code></strong
>
behaviour. It is only used while using
<code>katenary compose</code> command line
</p>
<p>
There are
<a
href="https://katenary.readthedocs.io/en/latest/labels/"
target="_blank"
>plenty of labels</a
>
to help you to customize and adapt the resulting Helm Chart.
</p>
</div>
<div>
<pre class="from-right"><code class="language-yaml">
# 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
</code></pre>
</div>
</div>
</section>
<section class="alternate">
<div class="container example">
<div class="from-right">
<h3>Ease the deployment</h3>
<p>
Kubernetes somtimes lacks of automation. Katenary helps you to add
what is needed, like having a
<code>depends_on</code> feature.
</p>
</div>
<div>
<pre class="from-left"><code class="language-yaml">
# 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
</code></pre>
</div>
</div>
</section>
</div>
</section>

24
src/partials/navbar.html Normal file
View File

@@ -0,0 +1,24 @@
<button class="btn-primary" id="menu-toggle">
<i class="fas fa-bars"></i>
</button>
<div class="navbar">
<div class="container">
<nav>
<ul>
<li>
<a href="#"><i class="fa fa-house"></i> Katenary</a>
</li>
<li><a href="#features">Why Katenary?</a></li>
<li><a href="#how-it-works">How It Works</a></li>
<li><a href="#how-to-use">How to use</a></li>
<li><a href="#tutorials">Tutorials</a></li>
<li><a href="#get-started">Get Started</a></li>
<li>
<a href="https://github.com/Katenary/katenary" target="_blank"
><i class="fab fa-github"></i> Katenary on GitHub</a
>
</li>
</ul>
</nav>
</div>
</div>

View File

@@ -0,0 +1,22 @@
<section class="tutorials" id="tutorials">
<div class="container">
<h2>Watch the Tutorials</h2>
<p>
<a
href="https://www.youtube.com/watch?v=kvVN8gPxqOA&list=PLrq-nCZV_rv6GRBFlA7WxUz2h3DM5teCi"
target="_blank"
>A playlist</a
>
is progressivelly filled to help the Katenary adoption. Take a look and
learn how it is simple.
</p>
<iframe
src="https://www.youtube.com/embed/kvVN8gPxqOA?si=tsPoMPh4PbKFutTx"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
</div>
</section>