Compare commits
361 Commits
0.2.0-alph
...
3.0.0-rc5
Author | SHA1 | Date | |
---|---|---|---|
327e0aa799 | |||
b7e948954b | |||
93c0658c19 | |||
bdf74f2deb | |||
2435bb9f8a | |||
5ee38dcf7f | |||
a754dc9275 | |||
732e080da3 | |||
22e1ebdce1 | |||
e4280f2c4e | |||
a99609d21d | |||
e55658d382 | |||
9dbe9e1114 | |||
eea198786a | |||
da9c0a7b58 | |||
d582b884bf | |||
aa3e4c64ee | |||
ce479629f6 | |||
89fd516b20 | |||
8bbb991afb | |||
4c5c9ba553 | |||
ef1511d4f7 | |||
a290219c81 | |||
c4e406f855 | |||
2beac5f4dd | |||
5f5cd0268a | |||
796cbb6d30 | |||
39ebf7eb78 | |||
a1c5161ff7 | |||
2307ad667e | |||
a26e832fed | |||
8582010d90 | |||
b7186eb8b4 | |||
0f3528818f | |||
9fadc77979 | |||
13d231a62c | |||
6cbaa06bec | |||
1552e9c877 | |||
3b2cd37ea4 | |||
4c566cbc1a | |||
69c528e5e0 | |||
42bf35593f | |||
4e739c5f08 | |||
f75623b4ff | |||
d82b5e4a98 | |||
42c738211c | |||
c54bc35c9f | |||
1f406dc558 | |||
0c0b50b0df | |||
6efee2fbae | |||
47e149e20b | |||
d163700147 | |||
fa3befaa88 | |||
e5b7c0d550 | |||
9e4663cc6e | |||
1a5c95d240 | |||
21845c5de1 | |||
4a91d458fd | |||
a52b20efab | |||
02c4c5168b | |||
7b875454cb | |||
2da5d9df08 | |||
a0eb3c0bb6 | |||
7e76543c48 | |||
09c2c86d59 | |||
e58948bb44 | |||
a00b03b2aa | |||
b47b956798 | |||
a409a1347e | |||
8999aabc21 | |||
14e8907437 | |||
740c400b9a | |||
7cd38bbd23 | |||
8c509b5bff | |||
28b22a0b30 | |||
748d0bf1ea | |||
130e6d4e24 | |||
a66fec07e1 | |||
a3d1e9342f | |||
72bc88661a | |||
e2b897eb9d | |||
9fcce059e5 | |||
36f6413917 | |||
8c729f3c57 | |||
063cc9d439 | |||
ac5317e600 | |||
bc0b65006d | |||
933f04bf5e | |||
7d46435ba2 | |||
d94bb8ac32 | |||
b143f743ef | |||
a8341a9b44 | |||
def5d097a4 | |||
ba0ae1bc60 | |||
d77029b597 | |||
b5f62d43af | |||
9220dc3278 | |||
36c72fb665 | |||
|
c90504f4f1 | ||
9ef961ae7c | |||
fe6663f9f4 | |||
1190b316bb | |||
7068dc229c | |||
|
61ce6fb25b | ||
116ef658d8 | |||
d7b354de8c | |||
|
d06c0574fb | ||
|
bcd7894e3b | ||
80aba05f66 | |||
41a4292939 | |||
131ea5d569 | |||
e36bbf41f4 | |||
c97b398914 | |||
d27ed76cf4 | |||
17b6ea51af | |||
0986f73f06 | |||
dcd282779f | |||
b93a3df98c | |||
4768330c1a | |||
23d0afd85f | |||
5c939383be | |||
72ddb8aa74 | |||
4d0e5b2e6a | |||
58d1e8e450 | |||
d0790f37a8 | |||
62b0576c2d | |||
e574a2e2a8 | |||
eb760d4299 | |||
3833037862 | |||
6dc92df4b5 | |||
d458cdbd73 | |||
628b35d471 | |||
84363be0e8 | |||
5284cdf5cc | |||
632ffc2b66 | |||
2ff705164e | |||
73ab867509 | |||
45e44dee14 | |||
689c2a4803 | |||
e023544c8a | |||
10b7a49bbf | |||
bb1354e228 | |||
957cc4bcf6 | |||
9f1f6c7e78 | |||
a80ddcc054 | |||
921eaff367 | |||
b63d8e4210 | |||
9766dac763 | |||
441be30720 | |||
e13653fba1 | |||
6b301f3171 | |||
9181c389d4 | |||
3b4dade699 | |||
4f0298c0a9 | |||
5f20585fb2 | |||
0714eac615 | |||
456e7f41f2 | |||
fc335247f8 | |||
ad16005091 | |||
a676372bbe | |||
7fadc45e9e | |||
41a0dc58a5 | |||
dc34d32c5c | |||
36984e3825 | |||
046410a5ec | |||
827b5bc830 | |||
8aee6d9983 | |||
7b890df1c5 | |||
e925f58e82 | |||
91fc0fd9f0 | |||
1a1d2b5ee8 | |||
95f3abfa74 | |||
3b51f41716 | |||
3f63375b60 | |||
8c97937b44 | |||
96f843630a | |||
48f6045cd3 | |||
49045a2ccd | |||
af8dabba85 | |||
cc1019b5a8 | |||
14877fbfa3 | |||
7b5e45131c | |||
b09316b416 | |||
315f5da970 | |||
9b392a1f64 | |||
9358076a36 | |||
427f2909d5 | |||
8ab1763902 | |||
e409be235e | |||
1ca71f264c | |||
5f464253c4 | |||
75869975f6 | |||
ac43256511 | |||
5b51ba6d98 | |||
dd63bb6343 | |||
817ebe0e53 | |||
6023ca4508 | |||
e0c829c2ce | |||
2d33367422 | |||
d72f371c59 | |||
db168c91c9 | |||
d31993953b | |||
63c6d5d0ef | |||
164a617869 | |||
865473b41b
|
|||
533e1422d0
|
|||
d2c8d08b7f
|
|||
4703aa7df5
|
|||
918f1b845b
|
|||
78dfb15cf5
|
|||
adc44a5e8b
|
|||
4367a01769
|
|||
d98268f45b
|
|||
ccfebd1a70
|
|||
e4f67dbd31
|
|||
d01a35e2d4
|
|||
0aa7023947
|
|||
451a1341bd
|
|||
da7d92bbfa | |||
15a2f25e51 | |||
f73d598bb4 | |||
98c7c6ddc1 | |||
39d63c11b1 | |||
8f4d69d6e2 | |||
531756d8ea | |||
e0c18ec2ad
|
|||
a3e7435544 | |||
c01cdf50c8
|
|||
2e3dd5032f
|
|||
8b01807568
|
|||
46c878b56e
|
|||
10b9342607 | |||
77de53c999
|
|||
6a7fedee7e
|
|||
c31299197f
|
|||
49c1fa5fb0
|
|||
d1186ee1e1
|
|||
50975ae94a
|
|||
7e8cb57979 | |||
12814f4732 | |||
dc41826691 | |||
6770f8176a | |||
cd946b2df6 | |||
734b0ed39d | |||
78d37c4405 | |||
fd3ba6d577 | |||
8ae9350d31 | |||
9621493343 | |||
f291d17aa3
|
|||
9826a54187
|
|||
d48fd2f911
|
|||
ec62a79d82
|
|||
58d19cce52
|
|||
3bb635a627 | |||
35f464a1cb | |||
85f1b2d43c | |||
77e8be4e63 | |||
57b274e345 | |||
3c743fb135 | |||
ab15614076
|
|||
c41fa22c59
|
|||
81ea0fbb6f
|
|||
821c038206
|
|||
19a37ace18
|
|||
d8bd66e66f
|
|||
c780e6c2a2
|
|||
3317459b0b | |||
564b939464 | |||
c7c18f01cd | |||
2f53638f82 | |||
5070101706 | |||
cc3c42b8fd | |||
45de7ab543
|
|||
984b50356a
|
|||
441b30a570
|
|||
2aad5d4b9d
|
|||
50169c8fbc
|
|||
fc67cb668d
|
|||
8e4b3be108
|
|||
3ae5ec99ff
|
|||
ef7fcb6133
|
|||
50c2f9d1dc
|
|||
eeb044bab0
|
|||
6ce52cc037
|
|||
9a3fc6a2b4
|
|||
4ded4d4e09
|
|||
dc47e73b4b | |||
76aa332dc2 | |||
5d4f72e984 | |||
5a358f0a6a | |||
ed9b4681ad | |||
d135f5bc0a
|
|||
f9cf3972d5
|
|||
6e33fa474a
|
|||
3a0cf1a7db
|
|||
475a025d9e
|
|||
c37bde487b | |||
|
982917c25b | ||
|
7feb7427f2 | ||
a2bf9c005b | |||
119b5d699c
|
|||
|
eca5154d75 | ||
16196aae34
|
|||
9726cc4a18
|
|||
7eb75015a0
|
|||
66dec780ae
|
|||
6cd361705d
|
|||
9c449eefab
|
|||
|
16c8e3dd20 | ||
b21c821086
|
|||
890d7b5017
|
|||
|
1266545919 | ||
874e12f35d
|
|||
b25b2fda2c
|
|||
e2a4d296f6
|
|||
7d5b4d9306
|
|||
a1d963c15b
|
|||
bb83384d62
|
|||
bc3c5dc932
|
|||
203449aa9e
|
|||
a3dbdfd8c6
|
|||
1b1397b45f
|
|||
1122c02e07
|
|||
01e045e4b7
|
|||
d28cbab0fe
|
|||
b1dc94e0e9
|
|||
f9fd6332d6 | |||
|
7203928d95 | ||
dc9b198208
|
|||
0f9a46f52d
|
|||
8cf3ff9f73
|
|||
114fab4870
|
|||
5fc8c06d8b
|
|||
51ca4e81d1
|
|||
a6f6b91ab9
|
|||
63afc3066b
|
|||
|
1e8fd44857 | ||
|
1403f9b198 | ||
|
22ef9211ec | ||
418a0a8029 | |||
165054ca53 | |||
a87391e726 | |||
f8dcd2026b | |||
f99f146af2 | |||
e72a8a2e9c | |||
0f73aa3125 | |||
7dc5d509f7 | |||
a9b75c48c4 | |||
6ea3a923cc | |||
6cd1af015b | |||
68a031d0be | |||
7ba68c2854 | |||
1dd8fef4b3 | |||
6a2417c361 | |||
ad316d1f49 | |||
ed22774a93 | |||
8dfca953dc | |||
7b774e84d8 | |||
d0576d4b81 | |||
c1fc388b26 | |||
86ca723aa8 |
@@ -4,3 +4,8 @@ root = true
|
||||
indent_style=tab
|
||||
indent_size=4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
|
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: develop
|
48
.github/workflows/go-test.yaml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Go-Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- develop
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
- 'releases/**'
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.24
|
||||
- name: Launch Test
|
||||
run: |
|
||||
go mod tidy
|
||||
go vet ./... && go test -coverprofile=coverprofile.out -json -v ./... > gotest.json
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tests-results
|
||||
path: |
|
||||
coverprofile.out
|
||||
gotest.json
|
||||
sonar:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: tests-results
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v5.2.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
36
.gitignore
vendored
@@ -1,8 +1,32 @@
|
||||
dist/*
|
||||
.cache/*
|
||||
chart/*
|
||||
docker-compose.yaml
|
||||
katenary
|
||||
*.env
|
||||
docker-compose*
|
||||
!examples/**/docker-compose*
|
||||
.credentials
|
||||
release.id
|
||||
cover*
|
||||
.sq
|
||||
.aider*
|
||||
.python_history
|
||||
.bash_history
|
||||
|
||||
.cache/
|
||||
.aider/
|
||||
.config/
|
||||
*/venv
|
||||
|
||||
# local binary
|
||||
./katenary
|
||||
|
||||
# will be treated later
|
||||
/examples/*
|
||||
|
||||
# nsis
|
||||
nsis/*.dll
|
||||
nsis/*.exe
|
||||
|
||||
doc/share
|
||||
__pycache__
|
||||
|
||||
.rpmmacros
|
||||
*.gpg
|
||||
|
||||
katenary
|
||||
|
0
.gitmodules
vendored
Normal file
26
.golangci.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
version: "2"
|
||||
run:
|
||||
issues-exit-code: 1
|
||||
linters:
|
||||
enabled:
|
||||
- unused
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- "(.+)_test.go"
|
||||
formatters:
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- "(.+)_test.go"
|
21
.markdownlint.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
# markdownlint configuration file
|
||||
default: true
|
||||
|
||||
MD013: # Line length
|
||||
line_length: 120
|
||||
|
||||
MD010: # Hard tabs
|
||||
code_blocks: false
|
||||
|
||||
# no inline HTML
|
||||
MD033: false
|
||||
|
||||
# heading as first line element...
|
||||
MD041: false
|
||||
|
||||
# list indentation
|
||||
MD007:
|
||||
indent: 4
|
||||
|
||||
# no problem using several code blocks styles
|
||||
MD046: false
|
20
.readthedocs.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.9"
|
||||
|
||||
mkdocs:
|
||||
configuration: doc/mkdocs.yml
|
||||
|
||||
# Optionally declare the Python requirements required to build your docs
|
||||
python:
|
||||
install:
|
||||
- requirements: doc/requirements.txt
|
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Patrice Ferlet
|
||||
Copyright (c) 2022-2025 The Katenary authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
517
Makefile
@@ -1,114 +1,501 @@
|
||||
# Strict mode
|
||||
SHELL := bash
|
||||
.SHELLFLAGS := -eu -o pipefail -c
|
||||
.ONESHELL:
|
||||
.DELETE_ON_ERROR:
|
||||
.PHONY: all binaries build check-dist-all check-dist-archlinux check-dist-debian check-dist-fedora check-dist-rocky check-dist-ubuntu check-sign clean-all clean-dist clean-go-cache clean-package-signer cover deb dist dist-full doc freebsd gpg-sign help install install-gomarkdoc katenary manpage packager-oci-image packages pacman prepare pull rpm rpm-sign sast serve-doc show-cover tar test uninstall upx warn-docker
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
# Get a version string from git
|
||||
CUR_SHA=$(shell git log -n1 --pretty='%h')
|
||||
CUR_BRANCH=$(shell git branch --show-current)
|
||||
VERSION=$(shell git describe --exact-match --tags $(CUR_SHA) 2>/dev/null || echo $(CUR_BRANCH)-$(CUR_SHA))
|
||||
CTN:=$(shell which podman 2>&1 1>/dev/null && echo "podman" || echo "docker")
|
||||
PREFIX=~/.local
|
||||
VERSION=$(shell git describe --exact-match --tags $(CUR_SHA) 2>/dev/null || echo $(CUR_BRANCH)-$(CUR_SHA))# use by golang flags
|
||||
|
||||
GO=container
|
||||
BLD_CMD=go build -o katenary -ldflags="-X 'main.Version=$(VERSION)'" .
|
||||
# Go build command and environment variables for target OS and architecture
|
||||
GOVERSION=1.24
|
||||
GO=container# container, local
|
||||
OUTPUT=katenary
|
||||
GOOS=linux
|
||||
GOARCH=amd64
|
||||
CGO_ENABLED=0
|
||||
PREFIX=~/.local
|
||||
|
||||
BUILD_IMAGE=docker.io/golang:1.17
|
||||
warn-docker:
|
||||
@echo -e "\033[1;31mWarning: Docker is not recommended, use Podman instead.\033[0m"
|
||||
sleep 5
|
||||
|
||||
.PHONY: help clean build
|
||||
# Get the container (Podman is preferred, but docker can be used too. It may failed with Docker.)
|
||||
# TODO: propose nerdctl
|
||||
CTN:=$(shell which podman 2>&1 1>/dev/null && echo "podman" || echo "docker")
|
||||
ifeq ($(CTN),podman)
|
||||
CTN_USERMAP=--userns=keep-id
|
||||
else
|
||||
$(MAKE) warn-docker
|
||||
CTN_USERMAP=--user=$(shell id -u):$(shell id -g) -e HOME=/tmp
|
||||
endif
|
||||
|
||||
|
||||
# Packaging OCI image, to build rpm, deb, pacman, tar packages
|
||||
# We changes the keep-id uid/gid for Podman, so that the user inside the container is the same as the user outside.
|
||||
# For Docker, as it doesn't support userns, we use common options, but it may fail...
|
||||
PKG_OCI_IMAGE=packaging:fedora
|
||||
ifeq ($(CTN),podman)
|
||||
# podman
|
||||
PKG_OCI_OPTS:=--rm -it \
|
||||
-v ./:/opt/katenary:z \
|
||||
--userns keep-id:uid=1001,gid=1001 \
|
||||
$(PKG_OCI_IMAGE)
|
||||
else
|
||||
# docker
|
||||
PKG_OCI_OPTS:=--rm -it \
|
||||
-v ./:/opt/katenary:z \
|
||||
-e HOME=/tmp \
|
||||
$(CTN_USERMAP) \
|
||||
$(PKG_OCI_IMAGE)
|
||||
endif
|
||||
GO_BUILD=go build -ldflags="-X 'katenary/generator.Version=$(VERSION)'" -o $(OUTPUT) ./cmd/katenary
|
||||
|
||||
|
||||
# UPX compression
|
||||
UPX_OPTS =
|
||||
UPX ?= upx $(UPX_OPTS)
|
||||
|
||||
BUILD_IMAGE=docker.io/golang:$(GOVERSION)
|
||||
|
||||
# List of source files
|
||||
SOURCES=$(shell find -name "*.go" -or -name "*.tpl" -type f | grep -v -P "^./example|^./vendor")
|
||||
# List of binaries to build and sign
|
||||
BINARIES=\
|
||||
dist/katenary-linux-amd64\
|
||||
dist/katenary-linux-arm64\
|
||||
dist/katenary-darwin-amd64\
|
||||
dist/katenary-freebsd-amd64\
|
||||
dist/katenary-freebsd-arm64\
|
||||
dist/katenary.exe\
|
||||
dist/katenary-windows-setup.exe
|
||||
|
||||
## GPG
|
||||
# List of signatures to build
|
||||
ASC_BINARIES=$(patsubst %,%.asc,$(BINARIES))
|
||||
# GPG signer
|
||||
SIGNER=metal3d@gmail.com
|
||||
|
||||
# Browser command to see coverage report after tests
|
||||
BROWSER=$(shell command -v epiphany || echo xdg-open)
|
||||
|
||||
all: build
|
||||
|
||||
.ONESHELL:
|
||||
help:
|
||||
@cat <<EOF
|
||||
@cat <<EOF | fold -s -w 80
|
||||
=== HELP ===
|
||||
To avoid you to install Go, the build is made by podman or docker.
|
||||
|
||||
You can use:
|
||||
Installinf (you can use local Go by setting GO=local)):
|
||||
# use podman or docker to build
|
||||
$$ make install
|
||||
# or use local Go
|
||||
$$ make install GO=local
|
||||
This will build and install katenary inside the PREFIX(/bin) value (default is $(PREFIX))
|
||||
|
||||
To change the PREFIX to somewhere where only root or sudo users can save the binary, it is recommended to build before install:
|
||||
|
||||
To change the PREFIX to somewhere where only root or sudo users can save the binary, it is recommended to build before install, one more time you can use local Go by setting GO=local:
|
||||
$$ make build
|
||||
$$ sudo make install PREFIX=/usr/local
|
||||
|
||||
Katenary is statically built (in Go), so there is no library to install.
|
||||
|
||||
To build for others OS:
|
||||
$$ make GOOS=linux GOARCH=amd64
|
||||
$$ make build GOOS=linux GOARCH=amd64
|
||||
This will build the binary for linux amd64.
|
||||
$$ make GOOS=linux GOARCH=arm
|
||||
|
||||
$$ make build GOOS=linux GOARCH=arm
|
||||
This will build the binary for linux arm.
|
||||
$$ make GOOS=windows GOARCH=amd64
|
||||
|
||||
$$ make build GOOS=windows GOARCH=amd64
|
||||
This will build the binary for windows amd64.
|
||||
$$ make GOOS=darwin GOARCH=amd64
|
||||
|
||||
$$ make build GOOS=darwin GOARCH=amd64
|
||||
This will build the binary for darwin amd64.
|
||||
|
||||
|
||||
Or you can build all versions:
|
||||
$$ make build-all
|
||||
|
||||
$$ make binaries
|
||||
EOF
|
||||
|
||||
|
||||
## BUILD
|
||||
|
||||
# Simply build the binary for the current OS and architecture
|
||||
build: pull katenary
|
||||
|
||||
build-all: pull dist dist/katenary-linux-amd64 dist/katenary-linux-arm64 dist/katenary.exe dist/katenary-darwin
|
||||
|
||||
pull:
|
||||
ifneq ($(GO),local)
|
||||
@echo -e "\033[1;32mPulling $(BUILD_IMAGE) docker image\033[0m"
|
||||
@$(CTN) pull $(BUILD_IMAGE)
|
||||
endif
|
||||
|
||||
dist:
|
||||
mkdir -p dist
|
||||
|
||||
dist/katenary-linux-amd64:
|
||||
@echo -e "\033[1;32mBuilding katenary for linux-amd64...\033[0m"
|
||||
$(MAKE) katenary GOOS=linux GOARCH=amd64
|
||||
strip katenary
|
||||
mv katenary dist/katenary-linux-amd64
|
||||
|
||||
|
||||
dist/katenary-linux-arm64:
|
||||
@echo -e "\033[1;32mBuilding katenary for linux-arm...\033[0m"
|
||||
$(MAKE) katenary GOOS=linux GOARCH=arm64
|
||||
strip katenary
|
||||
mv katenary dist/katenary-linux-arm64
|
||||
|
||||
dist/katenary.exe:
|
||||
@echo -e "\033[1;32mBuilding katenary for windows...\033[0m"
|
||||
$(MAKE) katenary GOOS=windows GOARCH=amd64
|
||||
strip katenary
|
||||
mv katenary dist/katenary-windows.exe
|
||||
|
||||
dist/katenary-darwin:
|
||||
@echo -e "\033[1;32mBuilding katenary for darwin...\033[0m"
|
||||
$(MAKE) katenary GOOS=darwin GOARCH=amd64
|
||||
strip katenary
|
||||
mv katenary dist/katenary-darwin
|
||||
|
||||
katenary: $(wildcard */*.go Makefile go.mod go.sum)
|
||||
@echo -e "\033[1;33mBuilding katenary $(VERSION)...\033[0m"
|
||||
katenary: $(SOURCES) go.mod go.sum
|
||||
ifeq ($(GO),local)
|
||||
@echo "=> Build in host using go"
|
||||
@echo
|
||||
@echo "=> Build on host using go"
|
||||
$(GO_BUILD)
|
||||
else
|
||||
@echo "=> Build in container using" $(CTN)
|
||||
@echo
|
||||
@$(CTN) run \
|
||||
-e CGO_ENABLED=$(CGO_ENABLED) \
|
||||
-e GOOS=$(GOOS) \
|
||||
-e GOARCH=$(GOARCH) \
|
||||
--rm -v $(PWD):/go/src/katenary:z \
|
||||
-w /go/src/katenary \
|
||||
-v go-cache:/go/pkg/mod:z \
|
||||
$(CTN_USERMAP) \
|
||||
$(BUILD_IMAGE) $(GO_BUILD)
|
||||
endif
|
||||
echo $(BLD_CMD)
|
||||
ifeq ($(GO),local)
|
||||
$(BLD_CMD)
|
||||
else ifeq ($(CTN),podman)
|
||||
@podman run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --userns keep-id -it docker.io/golang $(BLD_CMD)
|
||||
else
|
||||
@docker run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp -it docker.io/golang $(BLD_CMD)
|
||||
endif
|
||||
|
||||
|
||||
|
||||
# Make dist, build executables for all platforms, sign them, and compress them with upx if possible.
|
||||
# Also generate the windows installer.
|
||||
binaries: prepare $(BINARIES)
|
||||
dist: binaries upx packages
|
||||
dist-full: clean-dist dist gpg-sign check-sign rpm-sign check-dist-all
|
||||
|
||||
prepare: pull packager-oci-image
|
||||
mkdir -p dist
|
||||
|
||||
dist/katenary-linux-amd64: $(SOURCES) go.mod go.sum
|
||||
@echo
|
||||
@echo -e "\033[1;32mBuilding katenary $(VERSION) for linux-amd64...\033[0m"
|
||||
$(MAKE) katenary GOOS=linux GOARCH=amd64 OUTPUT=$@
|
||||
strip $@
|
||||
|
||||
dist/katenary-linux-arm64: $(SOURCES) go.mod go.sum
|
||||
@echo
|
||||
@echo -e "\033[1;32mBuilding katenary $(VERSION) for linux-arm...\033[0m"
|
||||
$(MAKE) katenary GOOS=linux GOARCH=arm64 OUTPUT=$@
|
||||
|
||||
dist/katenary.exe: $(SOURCES) go.mod go.sum
|
||||
@echo
|
||||
@echo -e "\033[1;32mBuilding katenary $(VERSION) for windows...\033[0m"
|
||||
$(MAKE) katenary GOOS=windows GOARCH=amd64 OUTPUT=$@
|
||||
|
||||
dist/katenary-darwin-amd64: $(SOURCES) go.mod go.sum
|
||||
@echo
|
||||
@echo -e "\033[1;32mBuilding katenary $(VERSION) for darwin...\033[0m"
|
||||
$(MAKE) katenary GOOS=darwin GOARCH=amd64 OUTPUT=$@
|
||||
|
||||
dist/katenary-freebsd-amd64: $(SOURCES) go.mod go.sum
|
||||
@echo
|
||||
@echo -e "\033[1;32mBuilding katenary $(VERSION) for freebsd...\033[0m"
|
||||
$(MAKE) katenary GOOS=freebsd GOARCH=amd64 OUTPUT=$@
|
||||
strip $@
|
||||
|
||||
dist/katenary-freebsd-arm64: $(SOURCES) go.mod go.sum
|
||||
@echo
|
||||
@echo -e "\033[1;32mBuilding katenary $(VERSION) for freebsd-arm64...\033[0m"
|
||||
$(MAKE) katenary GOOS=freebsd GOARCH=arm64 OUTPUT=$@
|
||||
|
||||
dist/katenary-windows-setup.exe: nsis/EnVar.dll dist/katenary.exe
|
||||
@$(CTN) run -w /opt/katenary $(PKG_OCI_OPTS) \
|
||||
makensis -DAPP_VERSION=$(VERSION) nsis/katenary.nsi
|
||||
mv nsis/katenary-windows-setup.exe dist/katenary-windows-setup.exe
|
||||
|
||||
# Download the EnVar plugin for NSIS, put it in the nsis directory, and clean up
|
||||
nsis/EnVar.dll:
|
||||
curl https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip -o nsis/EnVar_plugin.zip
|
||||
cd nsis
|
||||
unzip -o EnVar_plugin.zip Plugins/x86-unicode/EnVar.dll
|
||||
mv Plugins/x86-unicode/EnVar.dll EnVar.dll
|
||||
rm -rf EnVar_plugin.zip Plugins
|
||||
|
||||
# UPX compression
|
||||
upx: upx-linux upx-darwin
|
||||
|
||||
upx-linux: dist/katenary-linux-amd64 dist/katenary-linux-arm64
|
||||
$(UPX) $^
|
||||
|
||||
upx-darwin: dist/katenary-darwin-amd64
|
||||
$(UPX) --force-macos $^
|
||||
|
||||
## Linux / FreeBSD packages with fpm
|
||||
|
||||
DESCRIPTION := $(shell cat packaging/description | sed ':a;N;$$!ba;s/\n/\\n/g')
|
||||
|
||||
FPM_OPTS=--name katenary \
|
||||
--url https://katenary.org \
|
||||
--vendor "Katenary Project" \
|
||||
--maintainer "Patrice Ferlet <metal3d@gmail.com>" \
|
||||
--license "MIT" \
|
||||
--description="$$(printf "$(DESCRIPTION)" | fold -s)"
|
||||
|
||||
# base files (doc...)
|
||||
FPM_BASES=../LICENSE=/usr/local/share/doc/katenary/LICENSE \
|
||||
../README.md=/usr/local/share/doc/katenary/README.md
|
||||
|
||||
FPM_COMMON_FILES=$(FPM_BASES) ../doc/share/man/man1/katenary.1=/usr/local/share/man/man1/katenary.1
|
||||
|
||||
# ArchLinux has got inconsistent /usr/local/man directory
|
||||
FPM_COMMON_FILES_ARCHLINUX=$(FPM_BASES) ../doc/share/man/man1/katenary.1=/usr/local/man/man1/katenary.1 \
|
||||
|
||||
# Pacman refuses dashes in version, and should start with a number
|
||||
PACMAN_VERSION=$(shell echo $(VERSION) | sed 's/-/./g; s/^v//')
|
||||
|
||||
define RPM_MACROS
|
||||
%_signature gpg
|
||||
%_gpg_path /home/builder/.gnupg
|
||||
%_gpg_name $(SIGNER)
|
||||
%_gpgbin /usr/bin/gpg2
|
||||
%__gpg_sign_cmd %{__gpg} gpg --force-v3-sigs --batch --verbose --no-armor --no-secmem-warning -u "%{_gpg_name}" -sbo %{__signature_filename} --digest-algo sha256 %{__plaintext_filename}'
|
||||
endef
|
||||
|
||||
rpm: dist/katenary-linux-$(GOARCH)
|
||||
@echo "==> Building RPM packages for $(GOARCH)..."
|
||||
$(CTN) run -w /opt/katenary/dist $(PKG_OCI_OPTS) \
|
||||
fpm -s dir -t rpm -a $(GOARCH) -f $(FPM_OPTS) --version=$(VERSION) \
|
||||
$(FPM_COMMON_FILES) \
|
||||
./katenary-linux-$(GOARCH)=/usr/local/bin/katenary
|
||||
rpm-sign:
|
||||
[ -f .rpmmacros ] || echo "$(RPM_MACROS)" > .rpmmacros
|
||||
[ -f .secret.gpg ] || gpg --export-secret-keys -a $(SIGNER) > .secret.gpg
|
||||
$(CTN) run -w /opt/katenary/dist \
|
||||
-v ./.secret.gpg:/home/builder/signer.gpg \
|
||||
-v packager-gpg:/home/builder/.gnupg \
|
||||
$(PKG_OCI_OPTS) \
|
||||
gpg --import /home/builder/signer.gpg
|
||||
$(CTN) run -w /opt/katenary/dist \
|
||||
-v .rpmmacros:/home/builder/.rpmmacros:z \
|
||||
-v packager-gpg:/home/builder/.gnupg \
|
||||
$(PKG_OCI_OPTS) \
|
||||
bash -c 'for rpm in $$(find . -iname "*.rpm"); do echo signing: $$rpm; rpm --addsign $$rpm; done'
|
||||
|
||||
deb:
|
||||
@echo "==> Building DEB packages for $(GOARCH)..."
|
||||
$(CTN) run -w /opt/katenary/dist $(PKG_OCI_OPTS) \
|
||||
fpm -s dir -t deb -a $(GOARCH) -f $(FPM_OPTS) --version=$(VERSION) \
|
||||
$(FPM_COMMON_FILES) \
|
||||
./katenary-linux-$(GOARCH)=/usr/local/bin/katenary
|
||||
|
||||
pacman:
|
||||
@echo "==> Building Pacman packages for $(GOARCH)..."
|
||||
$(CTN) run -w /opt/katenary/dist $(PKG_OCI_OPTS) \
|
||||
fpm -s dir -t pacman -a $(GOARCH) -f $(FPM_OPTS) --version=$(PACMAN_VERSION) \
|
||||
$(FPM_COMMON_FILES_ARCHLINUX) \
|
||||
./katenary-linux-$(GOARCH)=/usr/local/bin/katenary
|
||||
|
||||
freebsd:
|
||||
@echo "==> Building FreeBSD packages for $(GOARCH)..."
|
||||
$(CTN) run -w /opt/katenary/dist $(PKG_OCI_OPTS) \
|
||||
fpm -s dir -t freebsd -a $(GOARCH) -f $(FPM_OPTS) --version=$(VERSION)\
|
||||
$(FPM_COMMON_FILES) \
|
||||
./katenary-freebsd-$(GOARCH)=/usr/local/bin/katenary
|
||||
mv dist/katenary-$(VERSION).txz dist/katenary-freebsd-$(VERSION).$(GOARCH).txz
|
||||
|
||||
tar:
|
||||
@echo "==> Building TAR packages for $(GOOS) $(GOARCH)..."
|
||||
$(CTN) run -w /opt/katenary/dist $(PKG_OCI_OPTS) \
|
||||
fpm -s dir -t tar -a $(GOARCH) -f $(FPM_OPTS) \
|
||||
$(FPM_COMMON_FILES) \
|
||||
./katenary-$(GOOS)-$(GOARCH)=/usr/local/bin/katenary
|
||||
mv dist/katenary.tar dist/katenary-$(GOOS)-$(VERSION).$(GOARCH).tar
|
||||
|
||||
packages: manpage packager-oci-image
|
||||
for arch in amd64 arm64; do \
|
||||
$(MAKE) rpm GOARCH=$$arch; \
|
||||
$(MAKE) deb GOARCH=$$arch; \
|
||||
$(MAKE) pacman GOARCH=$$arch; \
|
||||
$(MAKE) freebsd GOARCH=$$arch; \
|
||||
$(MAKE) tar GOARCH=$$arch GOOS=linux; \
|
||||
$(MAKE) tar GOARCH=$$arch GOOS=freebsd; \
|
||||
done
|
||||
|
||||
packager-oci-image:
|
||||
@$(CTN) build -t packaging:fedora ./packaging/oci 1>/dev/null
|
||||
|
||||
## GPG signing
|
||||
|
||||
gpg-sign:
|
||||
rm -f dist/*.asc
|
||||
$(MAKE) $(ASC_BINARIES)
|
||||
|
||||
check-sign:
|
||||
@echo "=> Checking signatures..."
|
||||
@for f in $(ASC_BINARIES); do \
|
||||
if gpg --verify $$f &>/dev/null; then \
|
||||
echo "Signature for $$f is valid"; \
|
||||
else \
|
||||
echo "Signature for $$f is invalid"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done
|
||||
@echo "=> checking in blank environment..."
|
||||
keyid=$(shell gpg -k --with-colons $(SIGNER)| grep '^pub' | cut -d: -f5);
|
||||
$(CTN) run --rm -it -e GPGKEY=$${keyid} -v ./dist:/opt/dist:z \
|
||||
packaging:fedora \
|
||||
bash -c '
|
||||
gpg --recv-key $$GPGKEY || exit 1;
|
||||
echo "Trusting $(SIGNER) key...";
|
||||
echo "trusted-key 483493B2DD0845DA8F21A26DF3702E3FAD8F76DC" >> ~/.gnupg/gpg.conf;
|
||||
gpg --update-trustdb;
|
||||
rm -f ~/.gnupg/gpg.conf;
|
||||
for f in /opt/dist/*.asc; do echo "==> $${f}"; gpg --verify $${f}; done;
|
||||
echo "=> Listing imported keys...";
|
||||
gpg -k
|
||||
'
|
||||
|
||||
dist/%.asc: dist/%
|
||||
gpg --armor --detach-sign --default-key $(SIGNER) $< &>/dev/null || exit 1
|
||||
|
||||
|
||||
check-dist-rocky:
|
||||
@echo "=> Checking Rocky Linux package..."
|
||||
p=$(wildcard dist/*x86_64.rpm);
|
||||
$(CTN) run --rm -it -v ./dist:/opt:z quay.io/rockylinux/rockylinux:latest bash -c "
|
||||
rpm -ivh /opt/$$(basename $$p);
|
||||
katenary version;
|
||||
"
|
||||
|
||||
check-dist-fedora:
|
||||
@echo "=> Checking Fedora package..."
|
||||
p=$(wildcard dist/*x86_64.rpm);
|
||||
$(CTN) run --rm -it -v ./dist:/opt:z quay.io/fedora/fedora:latest bash -c "
|
||||
rpm -ivh /opt/$$(basename $$p);
|
||||
katenary version;
|
||||
"
|
||||
|
||||
check-dist-archlinux:
|
||||
echo "=> Checking ArchLinux package..."
|
||||
p=$(wildcard dist/*x86_64.pkg.tar.zst);
|
||||
$(CTN) run --rm -it -v ./dist:/opt:z quay.io/archlinux/archlinux bash -c "
|
||||
pacman -U /opt/$$(basename $$p) --noconfirm;
|
||||
katenary version;
|
||||
"
|
||||
|
||||
check-dist-debian:
|
||||
@echo "=> Checking Debian package..."
|
||||
p=$(wildcard dist/*amd64.deb);
|
||||
$(CTN) run --rm -it -v ./dist:/opt:z debian:latest bash -c "
|
||||
dpkg -i /opt/$$(basename $$p);
|
||||
katenary version;
|
||||
"
|
||||
check-dist-ubuntu:
|
||||
@echo "=> Checking Ubuntu package..."
|
||||
p=$(wildcard dist/*amd64.deb);
|
||||
$(CTN) run --rm -it -v ./dist:/opt:z ubuntu:latest bash -c "
|
||||
dpkg -i /opt/$$(basename $$p);
|
||||
katenary version;
|
||||
"
|
||||
|
||||
check-dist-all:
|
||||
$(MAKE) check-dist-fedora
|
||||
$(MAKE) check-dist-rocky
|
||||
$(MAKE) check-dist-debian
|
||||
$(MAKE) check-dist-ubuntu
|
||||
$(MAKE) check-dist-archlinux
|
||||
|
||||
## installation and uninstallation
|
||||
|
||||
install: build
|
||||
cp katenary $(PREFIX)/bin/katenary
|
||||
install -Dm755 katenary $(PREFIX)/bin/katenary
|
||||
|
||||
uninstall:
|
||||
rm -f $(PREFIX)/bin/katenary
|
||||
|
||||
clean:
|
||||
serve-doc: doc
|
||||
@cd doc && \
|
||||
[ -d venv ] || python -m venv venv; \
|
||||
source venv/bin/activate && \
|
||||
echo "==> Installing requirements in the virtual env..."
|
||||
pip install -qq -r requirements.txt && \
|
||||
echo "==> Serving doc with mkdocs..." && \
|
||||
mkdocs serve
|
||||
|
||||
## Documentation generation
|
||||
|
||||
doc:
|
||||
@echo "=> Generating documentation..."
|
||||
# generate the labels doc and code doc
|
||||
$(MAKE) __label_doc
|
||||
|
||||
manpage:
|
||||
@echo "=> Generating manpage from documentation"
|
||||
@cd doc && \
|
||||
[ -d venv ] || python -m venv venv; \
|
||||
source venv/bin/activate && \
|
||||
echo "==> Installing requirements in the virtual env..." && \
|
||||
pip install -qq -r requirements.txt && \
|
||||
echo "==> Generating manpage..." && \
|
||||
MANPAGE=true mkdocs build && \
|
||||
rm -rf site &&
|
||||
echo "==> Manpage generated in doc/share/man/man1/katenary.1"
|
||||
|
||||
install-gomarkdoc:
|
||||
go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest
|
||||
|
||||
__label_doc:
|
||||
@command -v gomarkdoc || (echo "==> We need to install gomarkdoc..." && \
|
||||
$(MAKE) install-gomarkdoc)
|
||||
@echo "=> Generating labels doc..."
|
||||
# short label doc
|
||||
go run ./cmd/katenary help-labels -m | \
|
||||
sed -i '
|
||||
/START_LABEL_DOC/,/STOP_LABEL_DOC/{/<!--/!d};
|
||||
/START_LABEL_DOC/,/STOP_LABEL_DOC/r/dev/stdin
|
||||
' doc/docs/labels.md
|
||||
# detailed label doc
|
||||
go run ./cmd/katenary help-labels -am | sed 's/^##/###/' | \
|
||||
sed -i '
|
||||
/START_DETAILED_DOC/,/STOP_DETAILED_DOC/{/<!--/!d};
|
||||
/START_DETAILED_DOC/,/STOP_DETAILED_DOC/r/dev/stdin
|
||||
' doc/docs/labels.md
|
||||
|
||||
echo "=> Generating Code documentation..."
|
||||
PACKAGES=$$(for f in $$(find . -name "*.go" -type f); do dirname $$f; done | sort -u)
|
||||
for pack in $$PACKAGES; do
|
||||
echo "-> Generating doc for $$pack"
|
||||
gomarkdoc --repository.default-branch $(shell git branch --show-current) -o doc/docs/packages/$$pack.md $$pack
|
||||
sed -i '/^## Index/,/^##/ { /## Index/d; /^##/! d }' doc/docs/packages/$$pack.md
|
||||
done
|
||||
|
||||
|
||||
## TESTS, security analysis, and code quality
|
||||
|
||||
# Scan the source code.
|
||||
# - we don't need detection of text/template as it's not a web application, and
|
||||
# - we don't need sha1 detection as it is not used for cryptographic purposes.
|
||||
# Note: metrics are actually not sent to anyone - it's a thing that is removed from the code in the future.
|
||||
sast:
|
||||
opengrep \
|
||||
--config auto \
|
||||
--exclude-rule go.lang.security.audit.xss.import-text-template.import-text-template \
|
||||
--exclude-rule go.lang.security.audit.crypto.use_of_weak_crypto.use-of-sha1 \
|
||||
--metrics=on \
|
||||
.
|
||||
test:
|
||||
@echo -e "\033[1;33mTesting katenary $(VERSION)...\033[0m"
|
||||
go test -coverprofile=cover.out ./...
|
||||
$(MAKE) cover
|
||||
|
||||
cover:
|
||||
@go tool cover -func=cover.out | grep "total:"
|
||||
go tool cover -html=cover.out -o cover.html
|
||||
|
||||
show-cover:
|
||||
@[ -f cover.html ] || (echo "cover.html is not present, run make test before"; exit 1)
|
||||
if [ "$(BROWSER)" = "xdg-open" ]; then
|
||||
xdg-open cover.html
|
||||
else
|
||||
$(BROWSER) -i --new-window cover.html
|
||||
fi
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
clean-all: clean-dist clean-package-signer clean-go-cache
|
||||
|
||||
clean-dist:
|
||||
rm -rf dist
|
||||
rm -f katenary
|
||||
|
||||
|
||||
clean-package-signer:
|
||||
rm -f .secret.gpg .rpmmacros
|
||||
|
||||
clean-go-cache:
|
||||
$(CTN) volume rm -f go-cache
|
||||
|
||||
|
284
README.md
@@ -1,17 +1,59 @@
|
||||

|
||||
<div style="text-align:center; margin: auto 0 4em 0" align="center">
|
||||
<img src="./doc/docs/statics/logo-vertical.svg" alt="Katenary Logo" style="max-width: 90%" align="center"/>
|
||||
</div>
|
||||
|
||||
Katenary is a tool to help transforming `docker-compose` files to a working Helm Chart for Kubernetes.
|
||||
<div style="text-align:center; margin: auto 0 4em 0" align="center">
|
||||
|
||||
> **Important Note** Katenary is a tool to help building Helm Chart from a docker-compose file, but docker-compose doesn't propose as many features as what can do Kubernetes. So, we strongly recommend to use Katenary as a "bootstrap" tool and then to manually enhance the generated helm chart.
|
||||
[](https://katenary.readthedocs.io/latest/?badge=latest)
|
||||
[](https://goreportcard.com/report/github.com/Katenary/katenary)
|
||||
[](https://github.com/Katenary/katenary/releases)
|
||||
[](https://sonarcloud.io/summary/new_code?id=metal3d_katenary)
|
||||
[](https://sonarcloud.io/summary/new_code?id=metal3d_katenary)
|
||||
[](https://sonarcloud.io/summary/new_code?id=metal3d_katenary)
|
||||
|
||||
This project is partially made at [Smile](https://smile.eu)
|
||||
[](https://sonarcloud.io/summary/new_code?id=metal3d_katenary)
|
||||
|
||||

|
||||
</div>
|
||||
|
||||
<div style="text-align:center; margin: auto 0 4em 0" align="center">
|
||||
<h3>🚀 Unleash Productivity with Katenary! 🚀</h3>
|
||||
</div>
|
||||
|
||||
# Install
|
||||
Tired of manual conversions? Katenary harnesses the labels from your "`compose`" file to craft complete Helm Charts
|
||||
effortlessly, saving you time and energy.
|
||||
|
||||
You can download the binaries from the [Release](https://github.com/metal3d/katenary/releases) section. Copy the binary and rename it to `katenary`. Place the binary inside your `PATH`. You should now be able to call the `katenary` command.
|
||||
🛠️ Simple automated CLI: Katenary handles the grunt work, generating everything needed for seamless service binding
|
||||
and Helm Chart creation.
|
||||
|
||||
💡 Effortless Efficiency: You only need to add labels when it's necessary to precise things.
|
||||
Then call `katenary convert` and let the magic happen.
|
||||
|
||||
## What ?
|
||||
|
||||
Katenary is a tool to help to transform `compose` (`docker compose`, `podman compose`, `nerdctl compose`, ...) files
|
||||
to a working Helm Chart for Kubernetes.
|
||||
|
||||
Today, it's partially developed in collaboration with [Klee Group](https://www.kleegroup.com). Note that Katenary is
|
||||
and **will stay an open source and free (as freedom) project**. We are convinced that the best way to make it better is to
|
||||
share it with the community.
|
||||
|
||||
The main developer is [Patrice FERLET](https://github.com/metal3d).
|
||||
|
||||
## Install
|
||||
|
||||
You can download the binaries from the [Release](https://github.com/Katenary/katenary/releases) section. Copy the binary
|
||||
and rename it to `katenary`. Place the binary inside your `PATH`. You should now be able to call the `katenary` command.
|
||||
|
||||
You can of course get the binary with `go install -u github.com/Katenary/katenary/cmd/katenary/...` but the `main` branch
|
||||
is continuously updated. It's preferable to use releases.
|
||||
|
||||
You can use this commands on Linux:
|
||||
|
||||
```bash
|
||||
sh <(curl -sSL https://raw.githubusercontent.com/Katenary/katenary/master/install.sh)
|
||||
```
|
||||
|
||||
## Or, build yourself
|
||||
|
||||
If you've got `podman` or `docker`, you can build `katenary` by using:
|
||||
|
||||
@@ -20,22 +62,35 @@ make build
|
||||
```
|
||||
|
||||
You can then install it with:
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
It will use the default PREFIX (`~/.local/`) to install the binary in the `bin` subdirectory. You can force the PREFIX value at install time, but maybe you need to use "sudo":
|
||||
It will use the default PREFIX (`~/.local/`) to install the binary in the `bin` subdirectory. You can force the PREFIX
|
||||
value at install time, but maybe you need to use "sudo":
|
||||
|
||||
```bash
|
||||
sudo make install PREFIX=/usr/local
|
||||
```
|
||||
|
||||
If that goes wrong, you can use your local Go compiler:
|
||||
|
||||
# Tips
|
||||
```bash
|
||||
make build GO=local
|
||||
|
||||
We strongly recommand to add the "completion" call to you SHELL using the common bashrc, or whatever the profile file you use.
|
||||
# To force OS or architecture
|
||||
make build GO=local GOOS=linux GOARCH=arm64
|
||||
```
|
||||
|
||||
E.g. :
|
||||
Then place the `katenary` binary file inside your PATH.
|
||||
|
||||
## Tips
|
||||
|
||||
We strongly recommend adding the completion call to you SHELL using the common `bashrc`, or whatever the profile file
|
||||
you use.
|
||||
|
||||
E.g.,
|
||||
|
||||
```bash
|
||||
# bash in ~/.bashrc file
|
||||
@@ -44,75 +99,69 @@ source <(katenary completion bash)
|
||||
source <(katenary completion bash --no-description)
|
||||
|
||||
# zsh in ~/.zshrc
|
||||
source <(helm completion zsh)
|
||||
source <(katenary completion zsh)
|
||||
|
||||
# fish in ~/.config/fish/config.fish
|
||||
katenary completion fish | source
|
||||
|
||||
# experimental
|
||||
# powershell (as we don't provide any support on Windows yet, please avoid this...)
|
||||
```
|
||||
|
||||
# Usage
|
||||
## Usage
|
||||
|
||||
```
|
||||
Katenary aims to be a tool to convert docker-compose files to Helm Charts.
|
||||
It will create deployments, services, volumes, secrets, and ingress resources.
|
||||
But it will also create initContainers based on depend_on, healthcheck, and other features.
|
||||
It's not magical, sometimes you'll need to fix the generated charts.
|
||||
The general way to use it is to call one of these commands:
|
||||
```text
|
||||
|
||||
katenary convert
|
||||
katenary convert -f docker-compose.yml
|
||||
katenary convert -f docker-compose.yml -o ./charts
|
||||
Katenary is a tool to convert compose files to Helm Charts.
|
||||
|
||||
In case of, check the help of each command using:
|
||||
katenary <command> --help
|
||||
or
|
||||
"katenary help <command>"
|
||||
Each [command] and subcommand has got an "help" and "--help" flag to show more information.
|
||||
|
||||
Usage:
|
||||
katenary [command]
|
||||
|
||||
Examples:
|
||||
katenary convert -c docker-compose.yml -o ./charts
|
||||
|
||||
Available Commands:
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
convert Convert docker-compose to helm chart
|
||||
help Help about any command
|
||||
show-labels Show labels of a resource
|
||||
version Display version
|
||||
completion Generates completion scripts
|
||||
convert Converts a docker-compose file to a Helm Chart
|
||||
hash-composefiles Print the hash of the composefiles
|
||||
help Help about any command
|
||||
help-labels Print the labels help for all or a specific label
|
||||
schema Print the schema of the katenary file
|
||||
version Print the version number of Katenary
|
||||
|
||||
Flags:
|
||||
-h, --help help for katenary
|
||||
-h, --help help for katenary
|
||||
-v, --version version for katenary
|
||||
|
||||
Use "katenary [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
Katenary will try to find a `docker-compose.yaml` or `docker-compose.yml` file inside the current directory. It will check *the existence of the `chart` directory to create a new Helm Chart inside a named subdirectory. Katenary will ask you if you want to delete it before recreating.
|
||||
Katenary will try to find a `docker-compose.yaml` or `docker-compose.yml` file inside the current directory. It will
|
||||
check \*the existence of the `chart` directory to create a new Helm Chart inside a named subdirectory. Katenary will ask
|
||||
you if you want to delete it before recreating.
|
||||
|
||||
It creates a subdirectory inside `chart` that is named with the `appname` option (default is `MyApp`)
|
||||
|
||||
> To respect the ability to install the same application in the same namespace, Katenary will create "variable" names like `{{ .Release.Name }}-servicename`. So, you will need to use some labels inside your docker-compose file to help katenary to build a correct helm chart.
|
||||
> To respect the ability to install the same application in the same namespace, Katenary will create variable names
|
||||
> like `{{ .Release.Name }}-servicename`. So, you will need to use some labels inside your docker-compose file to help
|
||||
> Katenary to build a correct helm chart.
|
||||
|
||||
What can be interpreted by Katenary:
|
||||
|
||||
- Services with "image" section (cannot work with "build" section)
|
||||
- **Named Volumes** are transformed to persistent volume claims - note that local volume will break the transformation to Helm Chart because there is (for now) no way to make it working (see below for resolution)
|
||||
- if `ports` and/or `expose` section, katenary will create Services and bind the port to the corresponding container port
|
||||
- `depends_on` will add init containers to wait for the depending service (using the first port)
|
||||
- `env_file` list will create a configMap object per environemnt file (⚠ todo: the "to-service" label doesn't work with configMap for now)
|
||||
- some labels can help to bind values, for example:
|
||||
- `katenary.io/ingress: 80` will expose the port 80 in a ingress
|
||||
- `katenary.io/env-to-service: VARNAME` will convert the value to a variable `{{ .Release.Name }}-VARNAME` - it's usefull when you want to pass the name of a service as a variable (think about the service name for mysql to pass to a container that wants to connect to this)
|
||||
|
||||
Exemple of a possible `docker-compose.yaml` file:
|
||||
Example of a possible `docker-compose.yaml` file:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
webapp:
|
||||
image: php:7-apache
|
||||
environment:
|
||||
# note that "database" is a service name
|
||||
# note that "database" is a "compose" service name
|
||||
# so we need to adapt it with the map-env label
|
||||
DB_HOST: database
|
||||
# a pitty to repeat this values, isn't it?
|
||||
# so, let's change them with "values-from" label
|
||||
DB_USER: foo
|
||||
DB_PASSWORD: bar
|
||||
expose:
|
||||
- 80
|
||||
depends_on:
|
||||
@@ -120,39 +169,138 @@ services:
|
||||
# because it's the "exposed" port
|
||||
- database
|
||||
labels:
|
||||
# explain to katenary that "DB_HOST" value is variable (using release name)
|
||||
katenary.io/env-to-service: DB_HOST
|
||||
# expose the port 80 as an ingress
|
||||
katenary.io/ingress: 80
|
||||
katenary.v3/ingress: |-
|
||||
hostname: myapp.example.com
|
||||
port: 80
|
||||
# make adaptations, DB_HOST environment is actually the service name
|
||||
katenary.v3/map-env: |-
|
||||
DB_HOST: '{{ .Release.Name }}-database'
|
||||
# get the values from the "database" service
|
||||
# this will use the database secrets and environment,
|
||||
# see the "database" service to see the values
|
||||
katenary.v3/values-from: |-
|
||||
DB_USER: database.MARIADB_USER
|
||||
DB_PASSWORD: database.MARIADB_PASSWORD
|
||||
|
||||
database:
|
||||
image: mariadb:10
|
||||
env_file:
|
||||
# this will create a configMap
|
||||
# this valuse will be added in a configMap
|
||||
- my_env.env
|
||||
environment:
|
||||
MARIADB_USER: foo
|
||||
MARIADB_ROOT_PASSWORD: foobar
|
||||
MARIADB_PASSWORD: bar
|
||||
labels:
|
||||
# no need to declare this port in docker-compose
|
||||
# but katenary will need it
|
||||
katenary.io/ports: 3306
|
||||
katenary.v3/ports: |-
|
||||
- 3306
|
||||
# these variables are secrets
|
||||
katenary.v3/secrets: |-
|
||||
- MARIADB_ROOT_PASSWORD
|
||||
- MARIADB_PASSWORD
|
||||
```
|
||||
|
||||
# Labels
|
||||
## Labels
|
||||
|
||||
These labels could be found by `katenary show-labels`, and can be placed as "labels" inside your docker-compose file:
|
||||
These labels could be found by `katenary help-labels`, and can be placed as labels inside your docker-compose file:
|
||||
|
||||
```
|
||||
katenary.io/secret-envfiles : set the given file names as a secret instead of configmap
|
||||
katenary.io/ports : set the ports to expose as a service (coma separated)
|
||||
katenary.io/ingress : set the port to expose in an ingress (coma separated)
|
||||
katenary.io/env-to-service : specifies that the environment variable points on a service name (coma separated)
|
||||
katenary.io/configmap-volumes : specifies that the volumes points on a configmap (coma separated)
|
||||
katenary.io/same-pod : specifies that the pod should be deployed in the same pod than the given service name
|
||||
katenary.io/empty-dirs : specifies that the given volume names should be "emptyDir" instead of persistentVolumeClaim (coma separated)
|
||||
katenary.io/healthcheck : specifies that the container should be monitored by a healthcheck, **it overrides the docker-compose healthcheck**.
|
||||
You can use these form of label values:
|
||||
- "http://[not used address][:port][/path]" to specify an http healthcheck
|
||||
- "tcp://[not used address]:port" to specify a tcp healthcheck
|
||||
- other string is condidered as a "command" healthcheck
|
||||
```text
|
||||
To get more information about a label, use `katenary help-label <name_without_prefix>
|
||||
e.g. katenary help-label dependencies
|
||||
|
||||
katenary.v3/configmap-files: []string Inject files as Configmap.
|
||||
katenary.v3/cronjob: object Create a cronjob from the service.
|
||||
katenary.v3/dependencies: []object Add Helm dependencies to the service.
|
||||
katenary.v3/description: string Description of the service
|
||||
katenary.v3/env-from: []string Add environment variables from another service.
|
||||
katenary.v3/exchange-volumes: []object Add exchange volumes (empty directory on the node) to share data
|
||||
katenary.v3/health-check: object Health check to be added to the deployment.
|
||||
katenary.v3/ignore: bool Ignore the service
|
||||
katenary.v3/ingress: object Ingress rules to be added to the service.
|
||||
katenary.v3/main-app: bool Mark the service as the main app.
|
||||
katenary.v3/map-env: map[string]string Map env vars from the service to the deployment.
|
||||
katenary.v3/ports: []uint32 Ports to be added to the service.
|
||||
katenary.v3/same-pod: string Move the same-pod deployment to the target deployment.
|
||||
katenary.v3/secrets: []string Env vars to be set as secrets.
|
||||
katenary.v3/values: []string or map[string]string Environment variables to be added to the values.yaml
|
||||
katenary.v3/values-from: map[string]string Add values from another service.
|
||||
```
|
||||
|
||||
## Katenary.yaml file and schema validation
|
||||
|
||||
Instead of using labels inside the docker-compose file, you can use a `katenary.yaml` file to define the labels. This
|
||||
file is simpler to read and maintain, but you need to keep it up-to-date with the docker-compose file.
|
||||
|
||||
For example, instead of using this:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
katenary.v3/ingress: |-
|
||||
hostname: myapp.example.com
|
||||
port: 80
|
||||
```
|
||||
|
||||
You can remove the labels, and use a `katenary.yaml` file:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
ingress:
|
||||
hostname: myapp.example.com
|
||||
port: 80
|
||||
```
|
||||
|
||||
To validate the `katenary.yaml` file, you can use the JSON schema using the "master" raw content:
|
||||
|
||||
`https://raw.githubusercontent.com/Katenary/katenary/refs/heads/master/katenary.json`
|
||||
|
||||
It's easy to configure in [LazyVim](https://www.lazyvim.org/), using `nvim-lspconfig`,
|
||||
create a Lua file in your `plugins` directory, or apply the settings as the example below:
|
||||
|
||||
```lua
|
||||
-- yaml.lua
|
||||
|
||||
return {
|
||||
{
|
||||
"neovim/nvim-lspconfig",
|
||||
opts = {
|
||||
servers = {
|
||||
yamlls = {
|
||||
settings = {
|
||||
yaml = {
|
||||
schemas = {
|
||||
["https://raw.githubusercontent.com/Katenary/katenary/master/katenary.json"] = "katenary.yaml",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use this address to validate the `katenary.yaml` file in VSCode:
|
||||
|
||||
```json
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"https://raw.githubusercontent.com/Katenary/katenary/master/katenary.json": "katenary.yaml"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> You can, of course, replace the `master` with a specific tag or branch.
|
||||
|
||||
## What a name…
|
||||
|
||||
Katenary is the stylized name of the project that comes from the "catenary" word.
|
||||
|
||||
A catenary is a curve formed by a wire, rope, or chain hanging freely from two points that are not in the same vertical
|
||||
line. For example, the anchor chain between a boat and the anchor.
|
||||
|
||||
This curved link represents what we try to do, the project is a stretched link from docker-compose to helm chart.
|
||||
|
319
cmd/katenary/main.go
Normal file
@@ -0,0 +1,319 @@
|
||||
// Katenary CLI, main package.
|
||||
//
|
||||
// This package is not intended to be imported. It contains the
|
||||
// main function that build the command line with `cobra` package.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/generator"
|
||||
"katenary/generator/katenaryfile"
|
||||
"katenary/generator/labels"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const longHelp = `Katenary is a tool to convert compose files to Helm Charts.
|
||||
|
||||
Each [command] and subcommand has got an "help" and "--help" flag to show more information.
|
||||
`
|
||||
|
||||
func main() {
|
||||
rootCmd := buildRootCmd()
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func buildRootCmd() *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "katenary",
|
||||
Long: longHelp,
|
||||
Short: "Katenary is a tool to convert docker-compose files to Helm Charts",
|
||||
}
|
||||
rootCmd.Example = ` katenary convert -c docker-compose.yml -o ./charts`
|
||||
|
||||
rootCmd.Version = generator.GetVersion()
|
||||
rootCmd.CompletionOptions.DisableDescriptions = false
|
||||
rootCmd.CompletionOptions.DisableNoDescFlag = false
|
||||
|
||||
rootCmd.AddCommand(
|
||||
generateCompletionCommand(rootCmd.Name()),
|
||||
generateVersionCommand(),
|
||||
generateConvertCommand(),
|
||||
generateHashComposefilesCommand(),
|
||||
generateLabelHelpCommand(),
|
||||
generateSchemaCommand(),
|
||||
)
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
const completionHelp = `To load completions:
|
||||
|
||||
Bash:
|
||||
# Add this line in your ~/.bashrc or ~/.bash_profile file
|
||||
$ source <(%[1]s completion bash)
|
||||
|
||||
# Or, you can load completions for each users session. Execute once:
|
||||
# Linux:
|
||||
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
|
||||
# macOS:
|
||||
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
|
||||
|
||||
Zsh:
|
||||
# If shell completion is not already enabled in your environment,
|
||||
# you will need to enable it. You can execute the following once:
|
||||
|
||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
|
||||
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
|
||||
fish:
|
||||
$ %[1]s completion fish | source
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
|
||||
|
||||
PowerShell:
|
||||
PS> %[1]s completion powershell | Out-String | Invoke-Expression
|
||||
|
||||
# To load completions for every new session, run:
|
||||
PS> %[1]s completion powershell > %[1]s.ps1
|
||||
# and source this file from your PowerShell profile.
|
||||
`
|
||||
|
||||
func generateCompletionCommand(name string) *cobra.Command {
|
||||
bashV1 := false
|
||||
cmd := &cobra.Command{
|
||||
Use: "completion",
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
Short: "Generates completion scripts",
|
||||
Long: fmt.Sprintf(completionHelp, name),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return cmd.Help()
|
||||
}
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
// get the bash version
|
||||
if cmd.Flags().Changed("bash-v1") {
|
||||
return cmd.Root().GenBashCompletion(os.Stdout)
|
||||
}
|
||||
return cmd.Root().GenBashCompletionV2(os.Stdout, true)
|
||||
case "zsh":
|
||||
return cmd.Root().GenZshCompletion(os.Stdout)
|
||||
case "fish":
|
||||
return cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||
case "powershell":
|
||||
return cmd.Root().GenPowerShellCompletion(os.Stdout)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown completion type: %s", args[0])
|
||||
},
|
||||
}
|
||||
|
||||
// add a flag to force bash completion v1
|
||||
cmd.Flags().Bool("bash-v1", bashV1, "Force bash completion v1")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func generateConvertCommand() *cobra.Command {
|
||||
force := false
|
||||
outputDir := "./chart"
|
||||
dockerComposeFile := make([]string, 0)
|
||||
profiles := make([]string, 0)
|
||||
helmdepUpdate := false
|
||||
var appVersion *string
|
||||
givenAppVersion := ""
|
||||
chartVersion := "0.1.0"
|
||||
icon := ""
|
||||
envFiles := []string{}
|
||||
|
||||
convertCmd := &cobra.Command{
|
||||
Use: "convert",
|
||||
Short: "Converts a docker-compose file to a Helm Chart",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(strings.TrimSpace(givenAppVersion)) > 0 {
|
||||
appVersion = &givenAppVersion
|
||||
}
|
||||
return generator.Convert(generator.ConvertOptions{
|
||||
Force: force,
|
||||
OutputDir: outputDir,
|
||||
Profiles: profiles,
|
||||
HelmUpdate: helmdepUpdate,
|
||||
AppVersion: appVersion,
|
||||
ChartVersion: chartVersion,
|
||||
Icon: icon,
|
||||
EnvFiles: envFiles,
|
||||
}, dockerComposeFile...)
|
||||
},
|
||||
}
|
||||
|
||||
convertCmd.Flags().BoolVarP(
|
||||
&force,
|
||||
"force",
|
||||
"f",
|
||||
force,
|
||||
"Force the overwrite of the chart directory",
|
||||
)
|
||||
convertCmd.Flags().BoolVarP(
|
||||
&helmdepUpdate,
|
||||
"helm-update",
|
||||
"u",
|
||||
helmdepUpdate,
|
||||
"Update helm dependencies if helm is installed",
|
||||
)
|
||||
convertCmd.Flags().StringSliceVarP(
|
||||
&profiles,
|
||||
"profile",
|
||||
"p",
|
||||
profiles,
|
||||
"Specify the profiles to use",
|
||||
)
|
||||
convertCmd.Flags().StringVarP(
|
||||
&outputDir,
|
||||
"output-dir",
|
||||
"o",
|
||||
outputDir,
|
||||
"Specify the output directory",
|
||||
)
|
||||
convertCmd.Flags().StringSliceVarP(
|
||||
&dockerComposeFile,
|
||||
"compose-file",
|
||||
"c",
|
||||
cli.DefaultFileNames,
|
||||
"Specify an alternate compose files - can be specified multiple times or use coma to separate them.\n"+
|
||||
"Note that overides files are also used whatever the files you specify here.\nThe overides files are:\n"+
|
||||
strings.Join(cli.DefaultOverrideFileNames, ", \n")+
|
||||
"\n",
|
||||
)
|
||||
convertCmd.Flags().StringVarP(
|
||||
&givenAppVersion,
|
||||
"app-version",
|
||||
"a",
|
||||
"",
|
||||
"Specify the app version (in Chart.yaml)",
|
||||
)
|
||||
convertCmd.Flags().StringVarP(
|
||||
&chartVersion,
|
||||
"chart-version",
|
||||
"v",
|
||||
chartVersion,
|
||||
"Specify the chart version (in Chart.yaml)",
|
||||
)
|
||||
convertCmd.Flags().StringVarP(
|
||||
&icon,
|
||||
"icon",
|
||||
"i",
|
||||
"",
|
||||
"Specify the icon (in Chart.yaml), use a valid URL, Helm does not support local files at this time.",
|
||||
)
|
||||
convertCmd.Flags().StringSliceVarP(
|
||||
&envFiles,
|
||||
"env-file",
|
||||
"e",
|
||||
envFiles,
|
||||
"Specify the env file to use additonnaly to the .env file. Can be specified multiple times.",
|
||||
)
|
||||
|
||||
return convertCmd
|
||||
}
|
||||
|
||||
func generateVersionCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number of Katenary",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(generator.GetVersion())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func generateLabelHelpCommand() *cobra.Command {
|
||||
markdown := false
|
||||
all := false
|
||||
cmd := &cobra.Command{
|
||||
Use: "help-labels [label]",
|
||||
Short: "Print the labels help for all or a specific label",
|
||||
Long: `Print the labels help for all or a specific label
|
||||
If no label is specified, the help for all labels is printed.
|
||||
If a label is specified, the help for this label is printed.
|
||||
|
||||
The name of the label must be specified without the prefix ` + labels.Prefix() + `.
|
||||
|
||||
e.g.
|
||||
kanetary help-labels
|
||||
katenary help-labels ingress
|
||||
katenary help-labels map-env
|
||||
`,
|
||||
ValidArgs: labels.GetLabelNames(),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) > 0 {
|
||||
fmt.Println(labels.GetLabelHelpFor(args[0], markdown))
|
||||
return
|
||||
}
|
||||
if all {
|
||||
// show the help for all labels
|
||||
l := len(labels.GetLabelNames())
|
||||
for i, label := range labels.GetLabelNames() {
|
||||
fmt.Println(labels.GetLabelHelpFor(label, markdown))
|
||||
if !markdown && i < l-1 {
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
fmt.Println(labels.GetLabelHelp(markdown))
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&markdown, "markdown", "m", markdown, "Use the markdown format")
|
||||
cmd.Flags().BoolVarP(&all, "all", "a", all, "Print the full help for all labels")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func generateHashComposefilesCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "hash-composefiles [composefile]",
|
||||
Short: "Print the hash of the composefiles",
|
||||
Long: `Print the hash of the composefiles
|
||||
If no composefile is specified, the hash of all composefiles is printed.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) > 0 {
|
||||
if hash, err := utils.HashComposefiles(args); err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Println(hash)
|
||||
}
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func generateSchemaCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "schema",
|
||||
Short: "Print the schema of the katenary file",
|
||||
Long: "Generate a schama for katenary.yaml file that can be used to validate the file or to use with yaml LSP to complete and check your configuration.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(katenaryfile.GenerateSchema())
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
70
cmd/katenary/main_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildCommand(t *testing.T) {
|
||||
rootCmd := buildRootCmd()
|
||||
if rootCmd == nil {
|
||||
t.Errorf("Expected rootCmd to be defined")
|
||||
}
|
||||
if rootCmd.Use != "katenary" {
|
||||
t.Errorf("Expected rootCmd.Use to be katenary, got %s", rootCmd.Use)
|
||||
}
|
||||
numCommands := 6
|
||||
if len(rootCmd.Commands()) != numCommands {
|
||||
t.Errorf("Expected %d command, got %d", numCommands, len(rootCmd.Commands()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
cmd := buildRootCmd()
|
||||
if cmd == nil {
|
||||
t.Errorf("Expected cmd to be defined")
|
||||
}
|
||||
version := generateVersionCommand()
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
version.Run(cmd, nil)
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "(devel)") {
|
||||
t.Errorf("Expected output to contain '(devel)', got %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaCommand(t *testing.T) {
|
||||
cmd := buildRootCmd()
|
||||
if cmd == nil {
|
||||
t.Errorf("Expected cmd to be defined")
|
||||
}
|
||||
schema := generateSchemaCommand()
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
schema.Run(cmd, nil)
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// try to parse json
|
||||
schemaContent := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(output), &schemaContent); err != nil {
|
||||
t.Errorf("Expected valid json, got %s", output)
|
||||
}
|
||||
}
|
141
cmd/main.go
@@ -1,141 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"katenary/compose"
|
||||
"katenary/generator"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
composeFiles = []string{"docker-compose.yaml", "docker-compose.yml"}
|
||||
ComposeFile = ""
|
||||
AppName = "MyApp"
|
||||
ChartsDir = "chart"
|
||||
Version = "master"
|
||||
AppVersion = "0.0.1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
FindComposeFile()
|
||||
SetAppName()
|
||||
SetAppVersion()
|
||||
}
|
||||
|
||||
func FindComposeFile() bool {
|
||||
for _, file := range composeFiles {
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
ComposeFile = file
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetAppName sets the application name from the current directory name.
|
||||
func SetAppName() {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
AppName = filepath.Base(wd)
|
||||
|
||||
if AppName == "" {
|
||||
AppName = "MyApp"
|
||||
}
|
||||
}
|
||||
|
||||
// SetAppVersion set the AppVersion variable to the git version/tag
|
||||
func SetAppVersion() {
|
||||
AppVersion, _ = detectGitVersion()
|
||||
}
|
||||
|
||||
// Try to detect the git version/tag.
|
||||
func detectGitVersion() (string, error) {
|
||||
defaulVersion := "0.0.1"
|
||||
// Check if .git directory exists
|
||||
if s, err := os.Stat(".git"); err != nil {
|
||||
// .git should be a directory
|
||||
return defaulVersion, errors.New("no git repository found")
|
||||
} else if !s.IsDir() {
|
||||
// .git should be a directory
|
||||
return defaulVersion, errors.New(".git is not a directory")
|
||||
}
|
||||
|
||||
// check if "git" executable is callable
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
return defaulVersion, errors.New("git executable not found")
|
||||
}
|
||||
|
||||
// get the latest commit hash
|
||||
if out, err := exec.Command("git", "log", "-n1", "--pretty=format:%h").Output(); err == nil {
|
||||
latestCommit := strings.TrimSpace(string(out))
|
||||
// then get the current branch/tag
|
||||
out, err := exec.Command("git", "branch", "--show-current").Output()
|
||||
if err != nil {
|
||||
return defaulVersion, errors.New("git branch --show-current failed")
|
||||
} else {
|
||||
currentBranch := strings.TrimSpace(string(out))
|
||||
// finally, check if the current tag (if exists) correspond to the current commit
|
||||
// git describe --exact-match --tags <latestCommit>
|
||||
out, err := exec.Command("git", "describe", "--exact-match", "--tags", latestCommit).Output()
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
} else {
|
||||
return currentBranch + "-" + latestCommit, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaulVersion, errors.New("git log failed")
|
||||
}
|
||||
|
||||
func Convert(composeFile, appVersion, appName, chartDir string, force bool) {
|
||||
composeFound := FindComposeFile()
|
||||
_, err := os.Stat(ComposeFile)
|
||||
if !composeFound && err != nil {
|
||||
fmt.Println("No compose file found")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dirname := filepath.Join(chartDir, appName)
|
||||
if _, err := os.Stat(dirname); err == nil && !force {
|
||||
response := ""
|
||||
for response != "y" && response != "n" {
|
||||
response = "n"
|
||||
fmt.Printf(""+
|
||||
"The %s directory already exists, it will be \x1b[31;1mremoved\x1b[0m!\n"+
|
||||
"Do you really want to continue? [y/N]: ", dirname)
|
||||
fmt.Scanf("%s", &response)
|
||||
response = strings.ToLower(response)
|
||||
}
|
||||
if response == "n" {
|
||||
fmt.Println("Cancelled")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup and create the chart directory (until "templates")
|
||||
if err := os.RemoveAll(dirname); err != nil {
|
||||
fmt.Printf("Error removing %s: %s\n", dirname, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// create the templates directory
|
||||
templatesDir := filepath.Join(dirname, "templates")
|
||||
if err := os.MkdirAll(templatesDir, 0755); err != nil {
|
||||
fmt.Printf("Error creating %s: %s\n", templatesDir, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Parse the compose file now
|
||||
p := compose.NewParser(ComposeFile)
|
||||
p.Parse(appName)
|
||||
|
||||
// start generator
|
||||
generator.Generate(p, Version, appName, appVersion, ComposeFile, dirname)
|
||||
|
||||
}
|
@@ -1,96 +0,0 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/helm"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
ICON_EXCLAMATION = "❕"
|
||||
)
|
||||
|
||||
// Parser is a docker-compose parser.
|
||||
type Parser struct {
|
||||
Data *Compose
|
||||
}
|
||||
|
||||
var Appname = ""
|
||||
|
||||
// NewParser create a Parser and parse the file given in filename.
|
||||
func NewParser(filename string) *Parser {
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
c := NewCompose()
|
||||
dec := yaml.NewDecoder(f)
|
||||
dec.Decode(c)
|
||||
|
||||
p := &Parser{Data: c}
|
||||
|
||||
services := make(map[string][]string)
|
||||
// get the service list, to be sure that everything is ok
|
||||
|
||||
for name, s := range c.Services {
|
||||
if portlabel, ok := s.Labels[helm.LABEL_PORT]; ok {
|
||||
services := strings.Split(portlabel, ",")
|
||||
for _, serviceport := range services {
|
||||
portexists := false
|
||||
for _, found := range s.Ports {
|
||||
if found == serviceport {
|
||||
portexists = true
|
||||
}
|
||||
}
|
||||
if !portexists {
|
||||
s.Ports = append(s.Ports, serviceport)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s.Ports) > 0 {
|
||||
services[name] = s.Ports
|
||||
}
|
||||
}
|
||||
|
||||
// check if dependencies are resolved
|
||||
missing := []string{}
|
||||
for name, s := range c.Services {
|
||||
for _, dep := range s.DependsOn {
|
||||
if _, ok := services[dep]; !ok {
|
||||
missing = append(missing, fmt.Sprintf(
|
||||
"The service \"%s\" hasn't got "+
|
||||
"declared port for dependency from \"%s\" - please "+
|
||||
"append a %s label or a \"ports\" section in the docker-compose file",
|
||||
dep, name, helm.LABEL_PORT),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
log.Fatal(strings.Join(missing, "\n"))
|
||||
}
|
||||
|
||||
// check the build element
|
||||
for name, s := range c.Services {
|
||||
if s.RawBuild == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println(ICON_EXCLAMATION +
|
||||
" \x1b[33myou will need to build and push your image named \"" + s.Image + "\"" +
|
||||
" for the \"" + name + "\" service \x1b[0m")
|
||||
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(appname string) {
|
||||
Appname = appname
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
package compose
|
||||
|
||||
// Compose is a complete docker-compse representation.
|
||||
type Compose struct {
|
||||
Version string `yaml:"version"`
|
||||
Services map[string]*Service `yaml:"services"`
|
||||
Volumes map[string]interface{} `yaml:"volumes"`
|
||||
}
|
||||
|
||||
// NewCompose resturs a Compose object.
|
||||
func NewCompose() *Compose {
|
||||
c := &Compose{}
|
||||
c.Services = make(map[string]*Service)
|
||||
c.Volumes = make(map[string]interface{})
|
||||
return c
|
||||
}
|
||||
|
||||
// HealthCheck manage generic type to handle TCP, HTTP and TCP health check.
|
||||
type HealthCheck struct {
|
||||
Test []string `yaml:"test"`
|
||||
Interval string `yaml:"interval"`
|
||||
Timeout string `yaml:"timeout"`
|
||||
Retries int `yaml:"retries"`
|
||||
StartPeriod string `yaml:"start_period"`
|
||||
}
|
||||
|
||||
// Service represent a "service" in a docker-compose file.
|
||||
type Service struct {
|
||||
Image string `yaml:"image"`
|
||||
Ports []string `yaml:"ports"`
|
||||
Environment map[string]string `yaml:"environment"`
|
||||
Labels map[string]string `yaml:"labels"`
|
||||
DependsOn []string `yaml:"depends_on"`
|
||||
Volumes []string `yaml:"volumes"`
|
||||
Expose []int `yaml:"expose"`
|
||||
EnvFiles []string `yaml:"env_file"`
|
||||
RawBuild interface{} `yaml:"build"`
|
||||
HealthCheck *HealthCheck `yaml:"healthcheck"`
|
||||
}
|
6
doc/docs/.markdownlint.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
MD012: false
|
||||
MD013: false
|
||||
MD022: false
|
||||
MD033: false
|
||||
MD041: false
|
||||
MD046: false
|
105
doc/docs/coding.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# How Katenary works behind the scene
|
||||
|
||||
This section is for developers who want to take part in Katenary. Here we describe how it works and the expected
|
||||
principles.
|
||||
|
||||
## A few important points
|
||||
|
||||
Katenary is developed in Go. The version currently supported is 1.20. For reasons of readability, the `any` type is
|
||||
preferred to `interface{}`.
|
||||
|
||||
Since version v3, Katenary uses, in addition to `go-compose`, the `k8s` library to generate objects that are guaranteed
|
||||
to work before transformation. Katenary adds Helm syntax entries to add loops, transformations, and conditions.
|
||||
|
||||
We really try to follow best practices and code principles. But, Katenary needs a lot of workarounds and string
|
||||
manipulation during the process. There are, also, some drawbacks using standard k8s packages that make a lot of type
|
||||
checks when generating the objects. We need to finalize the values after object generation.
|
||||
|
||||
**This makes the coding a bit harder than simply converting from YAML to YAML.**
|
||||
|
||||
> If Katenary only generated YAML objects, the algorithms would be much simpler and would require less generation work.
|
||||
|
||||
## General principle
|
||||
|
||||
During conversion, the `generator` package is primarily responsible for creating "objects". The principle is to generate
|
||||
one `Deployment` per `compose` service. If the container coming from "compose" exposes ports (explicitly), then a
|
||||
service is created.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
|
||||
D[Deployment]:::outputs@{shape: curv-trap}
|
||||
C[Container List]@{shape: docs}
|
||||
ConfigMap:::outputs@{shape: curv-trap}
|
||||
Secrets:::outputs@{shape: curv-trap}
|
||||
H[Helm Chart.yaml file]:::outputs@{shape: curv-trap}
|
||||
Val[Values files]:::outputs@{shape: curv-trap}
|
||||
PVC:::outputs@{shape: curv-trap}
|
||||
S[Service]:::outputs@{shape: curv-trap}
|
||||
|
||||
A[Compose file]:::inputs --> B[Compose parser]
|
||||
B --> G[Generator]
|
||||
G --> P[Ports exposed to services] ---> S
|
||||
G ------> H
|
||||
G --> C --> D
|
||||
G ------> Val
|
||||
G ....-> M[Merge Continainers if same-pod]
|
||||
M ..-> C
|
||||
G --> E[Environment variables] ----> Secrets & ConfigMap
|
||||
G--> V[Bind volumes] -------> PVC
|
||||
V -----> CF[ Create ConfigMap\nfor static files as\nconfigmap-files] --> ConfigMap
|
||||
|
||||
Secrets & ConfigMap -- create envFrom --> D
|
||||
V -- bind volumes --> D
|
||||
|
||||
```
|
||||
|
||||
If the declaration of a container is to be integrated into another pod (via the `same-pod` label), this `Deployment` and
|
||||
its associated service are still created. They are deleted last, once the merge has been completed.
|
||||
|
||||
## Conversion in "`generator`" package
|
||||
|
||||
The `generator` package is where object struct are defined, and where you can find the `Generate()` function.
|
||||
|
||||
The generation fills `HelmChart` object using a loop:
|
||||
|
||||
```golang
|
||||
for _, service := range project.Services {
|
||||
dep := NewDeployment(service)
|
||||
y, _ := dep.Yaml()
|
||||
chart.Templates[dep.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**A lot** of string manipulations are made by each `Yaml()` methods. This is where you find the complex and impacting
|
||||
operations. The `Yaml` methods **don't return a valid YAML content**. This is a Helm Chart YAML content with template
|
||||
conditions, values and calls to helper templates.
|
||||
|
||||
> The `Yaml()` methods, in each object, need contribution, help, fixes, enhancements... They work, but there is a lot of
|
||||
> complexity. Please, create issues, pull-requests and conversation in the GitHub repository.
|
||||
|
||||
The final step, before sending all templates to chart, is to bind the containers inside the same pod where it's
|
||||
specified.
|
||||
|
||||
For each source container linked to the destination:
|
||||
|
||||
- we get the deployment of the source
|
||||
- we copy the container to the destination deployment
|
||||
- we get the associated service (if any)
|
||||
- we then copy the service port to the destination service
|
||||
- we finally remove the source service and deployment
|
||||
|
||||
> The `Configmap`, secrets, variables... are kept.
|
||||
|
||||
It finally computes the `helper` file.
|
||||
|
||||
## Conversion command
|
||||
|
||||
The `generator` works the same as described above. But the "convert" command makes some final steps:
|
||||
|
||||
- generate `values.yaml` and `Chart.yaml` files from the `HelmChart` object
|
||||
- add comments to the `values.yaml` files
|
||||
- add comments to the `Chart.yaml` files
|
18
doc/docs/dependencies.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Why those dependencies?
|
||||
|
||||
Katenary uses `compose-go` and several Kubernetes official packages.
|
||||
|
||||
- `github.com/compose-spec/compose-go`: to parse compose files. It ensures :
|
||||
- that the project respects the "compose" specification
|
||||
- that Katenary uses the "compose" struct exactly the same way `podman compose` or `docker copose` does
|
||||
- `github.com/spf13/cobra`: to parse command line arguments, sub-commands and flags. It also generates completion for
|
||||
bash, zsh, fish and PowerShell.
|
||||
- `github.com/thediveo/netdb`: to get the standard names of a service from its port number
|
||||
- `gopkg.in/yaml.v3`:
|
||||
- to generate `Chart.yaml` and `values.yaml` files (only)
|
||||
- to parse Katenary labels in the compose file
|
||||
- `k8s.io/api` and `k8s.io/apimachinery` to create Kubernetes objects
|
||||
- `sigs.k8s.io/yaml`: to generate Katenary YAML files in the format of Kubernetes objects
|
||||
|
||||
There are also some other packages used in the project, like `gopkg.in/yaml` to parse labels. I'm sorry to not list the
|
||||
entire dependencies. You can check the `go.mod` file to see all the dependencies.
|
112
doc/docs/faq.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## Why Katenary?
|
||||
|
||||
The main author[^1] of Katenary is a big fan of Podman, Docker and makes a huge use of Compose. He uses it a lot in his
|
||||
daily work. When he started to work with Kubernetes, he wanted to have the same experience as with Docker Compose.
|
||||
He wanted to have a tool that could convert his `docker-compose` files to Kubernetes manifests, but also to Helm charts.
|
||||
|
||||
Kompose was a good option. But the lacks of some options and configuration for the output Helm chart made him think
|
||||
about creating a new tool. He wanted to have a tool that could generate a complete Helm chart, with a lot of options
|
||||
and flexibility.
|
||||
|
||||
[^1]: I'm talking about myself :sunglasses: - Patrice FERLET, aka Metal3d, Tech Lead and DevOps Engineer at Klee Group.
|
||||
|
||||
## What's the difference between Katenary and Kompose?
|
||||
|
||||
[Kompose](https://kompose.io/) is a very nice tool, made by the Kubernetes community. It's a tool to convert
|
||||
`docker-compose` files to Kubernetes manifests. It's a very good tool, and it's more mature than Katenary.
|
||||
|
||||
Kompose is able to generate Helm charts, but [it could be not the case in future releases](https://github.com/kubernetes/kompose/issues/1716) for several reasons[^2].
|
||||
|
||||
[^2]:
|
||||
The author of Kompose explains that they have no bandwidth to maintain the Helm chart generation. It's a complex
|
||||
task, and we can confirm. Katenary takes a lot of time to be developed and maintained. This issue mentions Katenary as
|
||||
an alternative to Helm chart generation :smile:
|
||||
|
||||
The project is focused on Kubernetes manifests and proposes to use "Kustomize" to adapt the manifests. Helm seems to be
|
||||
not the priority.
|
||||
|
||||
Anyway, before this decision, the Helm chart generation was not what we expected. We wanted to have a more complete
|
||||
chart, with more options and more flexibility.
|
||||
|
||||
> That's why we decided to create Katenary.
|
||||
|
||||
Kompose didn't manage to generate a values file, complex volume binding, and many other things. It was also not able
|
||||
to manage dependencies between services.
|
||||
|
||||
> Be sure that we don't want to compete with Kompose. We just want to propose a different approach to the problem.
|
||||
|
||||
Kompose is an excellent tool, and we use it in some projects. It's a good choice if you want to convert
|
||||
your `docker-compose` files to Kubernetes manifests, but if you want to use Helm, Katenary is the tool you need.
|
||||
|
||||
## Why not using "one label" for all the configuration?
|
||||
|
||||
That was a discussion I had with my colleagues. The idea was to use a single label to store all the configuration.
|
||||
But, it's not a good idea.
|
||||
|
||||
Sometimes, you will have a long list of things to configure, like ports, ingress, dependencies, etc. It's better to have
|
||||
a clear and readable configuration. Segmented labels are easier to read and to maintain. It also avoids having too
|
||||
many indentation levels in the YAML file.
|
||||
|
||||
It is also more flexible. You can add or remove labels without changing the others.
|
||||
|
||||
## Why not using a configuration file?
|
||||
|
||||
The idea was to keep the configuration at a same place, and using the go-compose library to read the labels. It's
|
||||
easier to have a single file to manage.
|
||||
|
||||
By the way, Katenary auto accepts a `compose.katenary.yaml` file in the same directory. It's a way to separate the
|
||||
configuration from the compose file. It uses
|
||||
the [overrides' mechanism](https://docs.docker.com/compose/multiple-compose-files/merge/) like "compose" does.
|
||||
|
||||
## Why not developing with Rust?
|
||||
|
||||
Seriously...
|
||||
|
||||
OK, I will answer.
|
||||
|
||||
Rust is a good language. But, Podman, Docker, Kubernetes, Helm, and mostly all technologies around Kubernetes are
|
||||
written in Go. We have a large ecosystem in Go to manipulate, read, and write Kubernetes manifests as parsing
|
||||
Compose files.
|
||||
|
||||
> Go is better for this task.
|
||||
|
||||
There is no reason to use Rust for this project.
|
||||
|
||||
## Any chance to have a GUI?
|
||||
|
||||
Yes, it's a possibility. But, it's not a priority. We have a lot of things to do before. We need to stabilize the
|
||||
project, to have a good documentation, to have a good test coverage, and to have a good community.
|
||||
|
||||
But, in a not so far future, we could have a GUI. The choice of [Fyne.io](https://fyne.io) is already made, and we
|
||||
tested some concepts.
|
||||
|
||||
## I'm rich (or not), I want to help you. How can I do?
|
||||
|
||||
You can help us in many ways.
|
||||
|
||||
- The first things we really need, more than money, more than anything else, is to have feedback. If you use Katenary,
|
||||
if you have some issues, if you have some ideas, please open an issue on the [GitHub repository](https://github.com/Katenary/katenary).
|
||||
- The second thing is to help us to fix issues. If you're a Go developer, or if you want to fix the documentation,
|
||||
your help is greatly appreciated.
|
||||
- And then, of course, we need money, or sponsors.
|
||||
|
||||
### If you're a company
|
||||
|
||||
We will be happy to communicate your help by putting your logo on the website and in the documentation. You can sponsor
|
||||
us by giving us some money, or by giving us some time of your developers, or leaving us some time to work on the project.
|
||||
|
||||
### If you're an individual
|
||||
|
||||
All donators will be listed on the website and in the documentation. You can give us some money by using
|
||||
the [GitHub Sponsors]()
|
||||
|
||||
All main contributors[^3] will be listed on the website and in the documentation.
|
||||
|
||||
> If you want to be anonymous, please tell us.
|
||||
|
||||
[^3]:
|
||||
Main contributors are the people who have made a significant contribution to the project. It could be code,
|
||||
documentation, or any other help. There is no defined rules, at this time, to evaluate the contribution.
|
||||
It's a subjective decision.
|
162
doc/docs/index.md
Normal file
@@ -0,0 +1,162 @@
|
||||
<div class="md-center">
|
||||

|
||||
</div>
|
||||
|
||||
# Welcome to Katenary documentation
|
||||
|
||||
🚀 Unleash Productivity with Katenary! 🚀
|
||||
|
||||
Tired of manual conversions? Katenary harnesses the labels from your "compose" file to craft complete Helm Charts
|
||||
effortlessly, saving you time and energy.
|
||||
|
||||
🛠️ Simple automated CLI: Katenary handles the grunt work, generating everything needed for seamless service binding
|
||||
and Helm Chart creation.
|
||||
|
||||
💡 Effortless Efficiency: You only need to add labels when it's necessary to precise things. Then call `katenary convert`
|
||||
and let the magic happen.
|
||||
|
||||
<div style="margin: auto" class="zoomable">
|
||||

|
||||
</div>
|
||||
|
||||
# What is it?
|
||||
|
||||
Katenary is a tool made to help you to transform "compose" files (`compose.yaml`, `docker-compose.yml`,
|
||||
`podman-compose.yml`...) to complete and production ready [Helm Chart](https://helm.sh).
|
||||
|
||||
You'll be able to deploy your project in [:material-kubernetes: Kubernetes](https://kubernetes.io) in a few seconds
|
||||
(of course, more if you need to tweak with labels).
|
||||
|
||||
It uses your current file and optionally labels to configure the result.
|
||||
|
||||
It's an open source project, under MIT license, originally partially developed at [Smile](https://www.smile.eu).
|
||||
|
||||
Today, it's partially developed in collaboration with [Klee Group](https://www.kleegroup.com). Note that Katenary is
|
||||
and **will stay an open source and free (as freedom) project**. We are convinced that the best way to make it better is to
|
||||
share it with the community.
|
||||
|
||||
<div id="klee">
|
||||

|
||||
</div>
|
||||
|
||||
The main developer is [Patrice FERLET](https://github.com/metal3d).
|
||||
|
||||
The project source
|
||||
code is hosted on the [:fontawesome-brands-github: Katenary GitHub Repository](https://github.com/Katenary/katenary).
|
||||
|
||||
## Install Katenary
|
||||
|
||||
Katenary is developed using the :fontawesome-brands-golang:{ .gopher } [Go](https://go.dev) language.
|
||||
The binary is statically linked, so you can simply download it from the [release
|
||||
page](https://github.com/Katenary/katenary/releases) of the project in GitHub.
|
||||
|
||||
You need to select the right binary for your operating system and architecture, and copy the binary in a directory
|
||||
that is in your `PATH`.
|
||||
|
||||
If you are a Linux user, you can use the "one line installation command" which will download the binary in your
|
||||
`$HOME/.local/bin` directory if it exists.
|
||||
|
||||
```bash
|
||||
sh <(curl -sSL https://raw.githubusercontent.com/Katenary/katenary/master/install.sh)
|
||||
```
|
||||
|
||||
!!! Info "Upgrading is integrated to the `katenary` command"
|
||||
Katenary propose a `upgrade` sub-command to update the current binary to the latest stable release.
|
||||
|
||||
Of course, you need to install Katenary once :smile:
|
||||
|
||||
!!! Note "You prefer to compile it, no need to install Go"
|
||||
You can also build and install it yourself, the provided Makefile has got a `build` command that uses `podman` or
|
||||
`docker` to build the binary.
|
||||
|
||||
So, you don't need to install Go compiler :+1:.
|
||||
|
||||
But, note that the "master" branch is not the "stable" version. It's preferable to switch to a tag, or to use the
|
||||
releases.
|
||||
|
||||
To compile it, you can use the following commands:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Katenary/katenary.git
|
||||
cd katenary
|
||||
make build
|
||||
make install
|
||||
```
|
||||
|
||||
`make install` copies `./katenary` binary to your user binary path (`~/.local/bin`)
|
||||
|
||||
You can install it in other directory by changing the `PREFIX` variable. E.g.:
|
||||
|
||||
```bash
|
||||
make build
|
||||
sudo make install PREFIX=/usr/local
|
||||
```
|
||||
|
||||
Check if everything is OK using `katenary version` and / or `katenary help`
|
||||
|
||||
## Install completion
|
||||
|
||||
Katenary uses the very nice project named `cobra` to manage flags, argument and auto-completion.
|
||||
|
||||
You can activate it with:
|
||||
|
||||
```bash
|
||||
# replace "bash" by "zsh" if needed
|
||||
source <(katenary completion bash)
|
||||
```
|
||||
|
||||
Add this line in you `~/.profile`, `~/.bash_aliases` or `~/.bashrc` file to have completion at startup.
|
||||
|
||||
## What a name
|
||||
|
||||
A catenary is the curve that a hanging chain or cable assumes under its own weight when supported only at its ends.
|
||||
I, the maintainer, decided to name "Katenary" this project because it's like a chain that links a boat to a dock.
|
||||
Making the link between the "compose" world and the "Kubernetes" world is the main goal of this project.
|
||||
|
||||
Anyway, it's too late to change the name now :smile:
|
||||
|
||||
!!! Note "But I like this name!"
|
||||
|
||||
I spent time to find it :wink:
|
||||
|
||||
## Special thanks to
|
||||
|
||||
I really want to thank all the contributors, testers, and of course, the authors of the packages and tools that are used
|
||||
in this project. There is too many to list here. Katenary can work because of all these people. Open source is a great
|
||||
thing! :heart:
|
||||
|
||||
!!! Edit "Special thanks"
|
||||
|
||||
**Katenary is built with:** <br />
|
||||
|
||||
<a href="https://go.dev" target="_blank">:fontawesome-brands-golang:{ .go-logo }</a>
|
||||
|
||||
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.
|
||||
Because Docker, Podman, Kubernetes, and Helm are written in Go, Katenary is also written in Go and borrows packages
|
||||
from these projects to make it as efficient as possible.
|
||||
|
||||
Thanks to Kubernetes to provide [Kind](https://kind.sigs.k8s.io) that is used to test Katenary locally.
|
||||
|
||||
**Thanks to everyone who contributes to all these projects.**
|
||||
|
||||
Katenary can progress because of all these people. All contributions, as comments, issues, pull requests and
|
||||
feedbacks are welcome.
|
||||
|
||||
**Everything was also possible because of:** <br />
|
||||
|
||||
<ul>
|
||||
<li><a href="https://helm.sh" target="_blank"><img src="https://helm.sh/img/helm.svg" style="height: 1rem"/>
|
||||
Helm</a> that is the main toppic of Katenary, Kubernetes is easier to use with it.</li>
|
||||
<li><a href="https://cobra.dev/"><img src="https://cobra.dev/home/logo.png" style="height: 1rem"/> Cobra</a> that
|
||||
makes command, subcommand and completion possible for Katenary with ease.</li>
|
||||
<li>Podman, Docker, Kubernetes that are the main tools that Katenary is made for.</li>
|
||||
</ul>
|
||||
|
||||
**Documentation is built with:** <br />
|
||||
|
||||
<a href="https://www.mkdocs.org/" target="_blank">MkDocs</a> using <a
|
||||
href="https://squidfunk.github.io/mkdocs-material/" target="_blank">Material for MkDocs</a> theme template.
|
||||
|
||||
## License
|
||||
|
||||
Katenary is an open source project under the MIT license. You can use it, modify it, and distribute it as you want.
|
489
doc/docs/labels.md
Normal file
@@ -0,0 +1,489 @@
|
||||
# Labels documentation
|
||||
|
||||
Katenary proposes labels to set in `compose.yaml` files (or override files) to configure the Helm Chart generation.
|
||||
Because it is sometimes needed to have structured values, it is necessary to use the YAML syntax.
|
||||
While compose labels are string, we can use _here-doc_ syntax using `|` to use YAML multiline as value.
|
||||
|
||||
```yaml
|
||||
label-name: |-
|
||||
# this is actually a multiline string here
|
||||
key1: value1
|
||||
key2: value2
|
||||
```
|
||||
|
||||
Katenary will try to _Unmarshal_ these labels.
|
||||
|
||||
## Label list and types
|
||||
|
||||
<!-- START_LABEL_DOC : do not remove this tag !-->
|
||||
| Label name | Description | Type |
|
||||
| ------------------------------ | ---------------------------------------------------------------- | -------------------------------- |
|
||||
| `katenary.v3/configmap-files` | Inject files as Configmap. | `[]string` |
|
||||
| `katenary.v3/cronjob` | Create a cronjob from the service. | `object` |
|
||||
| `katenary.v3/dependencies` | Add Helm dependencies to the service. | `[]object` |
|
||||
| `katenary.v3/description` | Description of the service | `string` |
|
||||
| `katenary.v3/env-from` | Add environment variables from another service. | `[]string` |
|
||||
| `katenary.v3/exchange-volumes` | Add exchange volumes (empty directory on the node) to share data | `[]object` |
|
||||
| `katenary.v3/health-check` | Health check to be added to the deployment. | `object` |
|
||||
| `katenary.v3/ignore` | Ignore the service | `bool` |
|
||||
| `katenary.v3/ingress` | Ingress rules to be added to the service. | `object` |
|
||||
| `katenary.v3/main-app` | Mark the service as the main app. | `bool` |
|
||||
| `katenary.v3/map-env` | Map env vars from the service to the deployment. | `map[string]string` |
|
||||
| `katenary.v3/ports` | Ports to be added to the service. | `[]uint32` |
|
||||
| `katenary.v3/same-pod` | Move the same-pod deployment to the target deployment. | `string` |
|
||||
| `katenary.v3/secrets` | Env vars to be set as secrets. | `[]string` |
|
||||
| `katenary.v3/values` | Environment variables to be added to the values.yaml | `[]string or map[string]string` |
|
||||
| `katenary.v3/values-from` | Add values from another service. | `map[string]string` |
|
||||
|
||||
<!-- STOP_LABEL_DOC : do not remove this tag !-->
|
||||
|
||||
## Detailed description
|
||||
|
||||
<!-- START_DETAILED_DOC : do not remove this tag !-->
|
||||
### katenary.v3/configmap-files
|
||||
|
||||
Inject files as Configmap.
|
||||
|
||||
**Type**: `[]string`
|
||||
|
||||
It makes a file or directory to be converted to one or more ConfigMaps
|
||||
and mounted in the pod. The file or directory is relative to the
|
||||
service directory.
|
||||
|
||||
If it is a directory, all files inside it are added to the ConfigMap.
|
||||
|
||||
If the directory as subdirectories, so one configmap per subpath are created.
|
||||
|
||||
!!! Warning
|
||||
It is not intended to be used to store an entire project in configmaps.
|
||||
It is intended to be used to store configuration files that are not managed
|
||||
by the application, like nginx configuration files. Keep in mind that your
|
||||
project sources should be stored in an application image or in a storage.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
volumes
|
||||
- ./conf.d:/etc/nginx/conf.d
|
||||
labels:
|
||||
katenary.v3/configmap-files: |-
|
||||
- ./conf.d
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/cronjob
|
||||
|
||||
Create a cronjob from the service.
|
||||
|
||||
**Type**: `object`
|
||||
|
||||
This adds a cronjob to the chart.
|
||||
|
||||
The label value is a YAML object with the following attributes:
|
||||
- command: the command to be executed
|
||||
- schedule: the cron schedule (cron format or @every where "every" is a
|
||||
duration like 1h30m, daily, hourly...)
|
||||
- rbac: false (optionnal), if true, it will create a role, a rolebinding and
|
||||
a serviceaccount to make your cronjob able to connect the Kubernetes API
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
katenary.v3/cronjob: |-
|
||||
command: echo "hello world"
|
||||
schedule: "* */1 * * *" # or @hourly for example
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/dependencies
|
||||
|
||||
Add Helm dependencies to the service.
|
||||
|
||||
**Type**: `[]object`
|
||||
|
||||
Set the service to be, actually, a Helm dependency. This means that the
|
||||
service will not be exported as template. The dependencies are added to
|
||||
the Chart.yaml file and the values are added to the values.yaml file.
|
||||
|
||||
It's a list of objects with the following attributes:
|
||||
|
||||
- name: the name of the dependency
|
||||
- repository: the repository of the dependency
|
||||
- alias: the name of the dependency in values.yaml (optional)
|
||||
- values: the values to be set in values.yaml (optional)
|
||||
|
||||
!!! Info
|
||||
Katenary doesn't update the helm depenedencies by default.
|
||||
|
||||
Use `--helm-update` (or `-u`) flag to update the dependencies.
|
||||
|
||||
example: <code>katenary convert -u</code>
|
||||
|
||||
By setting an alias, it is possible to change the name of the dependency
|
||||
in values.yaml.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
katenary.v3/dependencies: |-
|
||||
- name: mariadb
|
||||
repository: oci://registry-1.docker.io/bitnamicharts
|
||||
|
||||
## optional, it changes the name of the section in values.yaml
|
||||
# alias: mydatabase
|
||||
|
||||
## optional, it adds the values to values.yaml
|
||||
values:
|
||||
auth:
|
||||
database: mydatabasename
|
||||
username: myuser
|
||||
password: the secret password
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/description
|
||||
|
||||
Description of the service
|
||||
|
||||
**Type**: `string`
|
||||
|
||||
This replaces the default comment in values.yaml file to the given description.
|
||||
It is useful to document the service and configuration.
|
||||
|
||||
The value can be set with a documentation in multiline format.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
katenary.v3/description: |-
|
||||
This is a description of the service.
|
||||
It can be multiline.
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/env-from
|
||||
|
||||
Add environment variables from another service.
|
||||
|
||||
**Type**: `[]string`
|
||||
|
||||
It adds environment variables from another service to the current service.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
service1:
|
||||
image: nginx:1.19
|
||||
environment:
|
||||
FOO: bar
|
||||
|
||||
service2:
|
||||
image: php:7.4-fpm
|
||||
labels:
|
||||
# get the congigMap from service1 where FOO is
|
||||
# defined inside this service too
|
||||
katenary.v3/env-from: |-
|
||||
- myservice1
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/exchange-volumes
|
||||
|
||||
Add exchange volumes (empty directory on the node) to share data
|
||||
|
||||
**Type**: `[]object`
|
||||
|
||||
This label allows sharing data between containres. The volume is created in
|
||||
the node and mounted in the pod. It is useful to share data between containers
|
||||
in a "same pod" logic. For example to let PHP-FPM and Nginx share the same direcotory.
|
||||
|
||||
This will create:
|
||||
|
||||
- an `emptyDir` volume in the deployment
|
||||
- a `voumeMount` in the pod for **each container**
|
||||
- a `initContainer` for each definition
|
||||
|
||||
Fields:
|
||||
- name: the name of the volume (manadatory)
|
||||
- mountPath: the path where the volume is mounted in the pod (optional, default is `/opt`)
|
||||
- init: a command to run to initialize the volume with data (optional)
|
||||
|
||||
!!! Warning
|
||||
This is highly experimental. This is mainly useful when using the "same-pod" label.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
nginx:
|
||||
# ...
|
||||
labels;
|
||||
katenary.v3/exchange-volumes: |-
|
||||
- name: php-fpm
|
||||
mountPath: /var/www/html
|
||||
php:
|
||||
# ...
|
||||
labels:
|
||||
katenary.v3/exchange-volumes: |-
|
||||
- name: php-fpm
|
||||
mountPath: /opt
|
||||
init: cp -ra /var/www/html/* /opt
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/health-check
|
||||
|
||||
Health check to be added to the deployment.
|
||||
|
||||
**Type**: `object`
|
||||
|
||||
Health check to be added to the deployment.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
katenary.v3/health-check: |-
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/ignore
|
||||
|
||||
Ignore the service
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
Ingoring a service to not be exported in helm chart.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
katenary.v3/ignore: "true"
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/ingress
|
||||
|
||||
Ingress rules to be added to the service.
|
||||
|
||||
**Type**: `object`
|
||||
|
||||
Declare an ingress rule for the service. The port should be exposed or
|
||||
declared with `katenary.v3/ports`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
katenary.v3/ingress: |-
|
||||
port: 80
|
||||
hostname: mywebsite.com (optional)
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/main-app
|
||||
|
||||
Mark the service as the main app.
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
This makes the service to be the main application. Its image tag is
|
||||
considered to be the Chart appVersion and to be the defaultvalue in Pod
|
||||
container image attribute.
|
||||
|
||||
!!! Warning
|
||||
This label cannot be repeated in others services. If this label is
|
||||
set in more than one service as true, Katenary will return an error.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
ghost:
|
||||
image: ghost:1.25.5
|
||||
labels:
|
||||
# The chart is now named ghost, and the appVersion is 1.25.5.
|
||||
# In Deployment, the image attribute is set to ghost:1.25.5 if
|
||||
# you don't change the "tag" attribute in values.yaml
|
||||
katenary.v3/main-app: true
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/map-env
|
||||
|
||||
Map env vars from the service to the deployment.
|
||||
|
||||
**Type**: `map[string]string`
|
||||
|
||||
Because you may need to change the variable for Kubernetes, this label
|
||||
forces the value to another. It is also particullary helpful to use a template
|
||||
value instead. For example, you could bind the value to a service name
|
||||
with Helm attributes:
|
||||
`{{ tpl .Release.Name . }}`.
|
||||
|
||||
If you use `__APP__` in the value, it will be replaced by the Chart name.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
env:
|
||||
DB_HOST: database
|
||||
RUNNING: docker
|
||||
OTHER: value
|
||||
labels:
|
||||
katenary.v3/map-env: |-
|
||||
RUNNING: kubernetes
|
||||
DB_HOST: '{{ include "__APP__.fullname" . }}-database'
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/ports
|
||||
|
||||
Ports to be added to the service.
|
||||
|
||||
**Type**: `[]uint32`
|
||||
|
||||
Only useful for services without exposed port. It is mandatory if the
|
||||
service is a dependency of another service.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
katenary.v3/ports: |-
|
||||
- 8080
|
||||
- 8081
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/same-pod
|
||||
|
||||
Move the same-pod deployment to the target deployment.
|
||||
|
||||
**Type**: `string`
|
||||
|
||||
This will make the service to be included in another service pod. Some services
|
||||
must work together in the same pod, like a sidecar or a proxy or nginx + php-fpm.
|
||||
|
||||
Note that volume and VolumeMount are copied from the source to the target
|
||||
deployment.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
web:
|
||||
image: nginx:1.19
|
||||
|
||||
php:
|
||||
image: php:7.4-fpm
|
||||
labels:
|
||||
katenary.v3/same-pod: web
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/secrets
|
||||
|
||||
Env vars to be set as secrets.
|
||||
|
||||
**Type**: `[]string`
|
||||
|
||||
This label allows setting the environment variables as secrets. The variable
|
||||
is removed from the environment and added to a secret object.
|
||||
|
||||
The variable can be set to the `katenary.v3/values` too,
|
||||
so the secret value can be configured in values.yaml
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
env:
|
||||
PASSWORD: a very secret password
|
||||
NOT_A_SECRET: a public value
|
||||
labels:
|
||||
katenary.v3/secrets: |-
|
||||
- PASSWORD
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/values
|
||||
|
||||
Environment variables to be added to the values.yaml
|
||||
|
||||
**Type**: `[]string or map[string]string`
|
||||
|
||||
By default, all environment variables in the "env" and environment
|
||||
files are added to configmaps with the static values set. This label
|
||||
allows adding environment variables to the values.yaml file.
|
||||
|
||||
Note that the value inside the configmap is `{{ tpl vaname . }}`, so
|
||||
you can set the value to a template that will be rendered with the
|
||||
values.yaml file.
|
||||
|
||||
The value can be set with a documentation. This may help to understand
|
||||
the purpose of the variable.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
env:
|
||||
FOO: bar
|
||||
DB_NAME: mydb
|
||||
TO_CONFIGURE: something that can be changed in values.yaml
|
||||
A_COMPLEX_VALUE: example
|
||||
labels:
|
||||
katenary.v3/values: |-
|
||||
# simple values, set as is in values.yaml
|
||||
- TO_CONFIGURE
|
||||
# complex values, set as a template in values.yaml with a documentation
|
||||
- A_COMPLEX_VALUE: |-
|
||||
This is the documentation for the variable to
|
||||
configure in values.yaml.
|
||||
It can be, of course, a multiline text.
|
||||
```
|
||||
|
||||
|
||||
### katenary.v3/values-from
|
||||
|
||||
Add values from another service.
|
||||
|
||||
**Type**: `map[string]string`
|
||||
|
||||
This label allows adding values from another service to the current service.
|
||||
It avoid duplicating values, environment or secrets that should be the same.
|
||||
|
||||
The key is the value to be added, and the value is the "key" to fetch in the
|
||||
form `service_name.environment_name`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
database:
|
||||
image: mariadb:10.5
|
||||
environment:
|
||||
MARIADB_USER: myuser
|
||||
MARIADB_PASSWORD: mypassword
|
||||
labels:
|
||||
# we can declare secrets
|
||||
katenary.v3/secrets: |-
|
||||
- MARIADB_PASSWORD
|
||||
php:
|
||||
image: php:7.4-fpm
|
||||
environment:
|
||||
# it's duplicated in docker / podman
|
||||
DB_USER: myuser
|
||||
DB_PASSWORD: mypassword
|
||||
labels:
|
||||
# removes the duplicated, use the configMap and secrets from "database"
|
||||
katenary.v3/values-from: |-
|
||||
DB_USER: database.MARIADB_USER
|
||||
DB_PASSWORD: database.MARIADB_PASSWORD
|
||||
```
|
||||
|
||||
|
||||
<!-- STOP_DETAILED_DOC : do not remove this tag !-->
|
12
doc/docs/packages/cmd/katenary.md
Normal file
@@ -0,0 +1,12 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# katenary
|
||||
|
||||
```go
|
||||
import "katenary/cmd/katenary"
|
||||
```
|
||||
|
||||
Katenary CLI, main package.
|
||||
|
||||
This package is not intended to be imported. It contains the main function that build the command line with \`cobra\` package.
|
||||
|
923
doc/docs/packages/generator.md
Normal file
@@ -0,0 +1,923 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# generator
|
||||
|
||||
```go
|
||||
import "katenary/generator"
|
||||
```
|
||||
|
||||
Package generator generates kubernetes objects from a "compose" file and transforms them into a helm chart.
|
||||
|
||||
The generator package is the core of katenary. It is responsible for generating kubernetes objects from a compose file and transforming them into a helm chart. Conversion manipulates Yaml representation of kubernetes object to add conditions, labels, annotations, etc. to the objects. It also create the values to be set to the values.yaml file.
|
||||
|
||||
The generate.Convert\(\) create an HelmChart object and call "Generate\(\)" method to convert from a compose file to a helm chart. It saves the helm chart in the given directory.
|
||||
|
||||
If you want to change or override the write behavior, you can use the HelmChart.Generate\(\) function and implement your own write function. This function returns the helm chart object containing all kubernetes objects and helm chart ingormation. It does not write the helm chart to the disk.
|
||||
|
||||
## Variables
|
||||
|
||||
<a name="Annotations"></a>
|
||||
|
||||
```go
|
||||
var (
|
||||
|
||||
// Standard annotationss
|
||||
Annotations = map[string]string{
|
||||
labels.LabelName("version"): Version,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
<a name="Version"></a>Version is the version of katenary. It is set at compile time.
|
||||
|
||||
```go
|
||||
var Version = "master" // changed at compile time
|
||||
```
|
||||
|
||||
<a name="Convert"></a>
|
||||
## func [Convert](<https://github.com/katenary/katenary/blob/develop/generator/converter.go#L99>)
|
||||
|
||||
```go
|
||||
func Convert(config ConvertOptions, dockerComposeFile ...string) error
|
||||
```
|
||||
|
||||
Convert a compose \(docker, podman...\) project to a helm chart. It calls Generate\(\) to generate the chart and then write it to the disk.
|
||||
|
||||
<a name="GetLabels"></a>
|
||||
## func [GetLabels](<https://github.com/katenary/katenary/blob/develop/generator/labels.go#L12>)
|
||||
|
||||
```go
|
||||
func GetLabels(serviceName, appName string) map[string]string
|
||||
```
|
||||
|
||||
GetLabels returns the labels for a service. It uses the appName to replace the \_\_replace\_\_ in the labels. This is used to generate the labels in the templates.
|
||||
|
||||
<a name="GetMatchLabels"></a>
|
||||
## func [GetMatchLabels](<https://github.com/katenary/katenary/blob/develop/generator/labels.go#L25>)
|
||||
|
||||
```go
|
||||
func GetMatchLabels(serviceName, appName string) map[string]string
|
||||
```
|
||||
|
||||
GetMatchLabels returns the matchLabels for a service. It uses the appName to replace the \_\_replace\_\_ in the labels. This is used to generate the matchLabels in the templates.
|
||||
|
||||
<a name="GetVersion"></a>
|
||||
## func [GetVersion](<https://github.com/katenary/katenary/blob/develop/generator/version.go#L14>)
|
||||
|
||||
```go
|
||||
func GetVersion() string
|
||||
```
|
||||
|
||||
GetVersion return the version of katneary. It's important to understand that the version is set at compile time for the github release. But, it the user get katneary using \`go install\`, the version should be different.
|
||||
|
||||
<a name="Helper"></a>
|
||||
## func [Helper](<https://github.com/katenary/katenary/blob/develop/generator/helper.go#L15>)
|
||||
|
||||
```go
|
||||
func Helper(name string) string
|
||||
```
|
||||
|
||||
Helper returns the \_helpers.tpl file for a chart.
|
||||
|
||||
<a name="NewCronJob"></a>
|
||||
## func [NewCronJob](<https://github.com/katenary/katenary/blob/develop/generator/cronJob.go#L28>)
|
||||
|
||||
```go
|
||||
func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) (*CronJob, *RBAC)
|
||||
```
|
||||
|
||||
NewCronJob creates a new CronJob from a compose service. The appName is the name of the application taken from the project name.
|
||||
|
||||
<a name="ToK8SYaml"></a>
|
||||
## func [ToK8SYaml](<https://github.com/katenary/katenary/blob/develop/generator/utils.go#L90>)
|
||||
|
||||
```go
|
||||
func ToK8SYaml(obj any) ([]byte, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="UnWrapTPL"></a>
|
||||
## func [UnWrapTPL](<https://github.com/katenary/katenary/blob/develop/generator/utils.go#L86>)
|
||||
|
||||
```go
|
||||
func UnWrapTPL(in []byte) []byte
|
||||
```
|
||||
|
||||
UnWrapTPL removes the line wrapping from a template.
|
||||
|
||||
<a name="ChartTemplate"></a>
|
||||
## type [ChartTemplate](<https://github.com/katenary/katenary/blob/develop/generator/chart.go#L21-L24>)
|
||||
|
||||
ChartTemplate is a template of a chart. It contains the content of the template and the name of the service. This is used internally to generate the templates.
|
||||
|
||||
```go
|
||||
type ChartTemplate struct {
|
||||
Servicename string
|
||||
Content []byte
|
||||
}
|
||||
```
|
||||
|
||||
<a name="ConfigMap"></a>
|
||||
## type [ConfigMap](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L37-L42>)
|
||||
|
||||
ConfigMap is a kubernetes ConfigMap. Implements the DataMap interface.
|
||||
|
||||
```go
|
||||
type ConfigMap struct {
|
||||
*corev1.ConfigMap
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewConfigMap"></a>
|
||||
### func [NewConfigMap](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L46>)
|
||||
|
||||
```go
|
||||
func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap
|
||||
```
|
||||
|
||||
NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name. The ConfigMap is filled by environment variables and labels "map\-env".
|
||||
|
||||
<a name="NewConfigMapFromDirectory"></a>
|
||||
### func [NewConfigMapFromDirectory](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L119>)
|
||||
|
||||
```go
|
||||
func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap
|
||||
```
|
||||
|
||||
NewConfigMapFromDirectory creates a new ConfigMap from a compose service. This path is the path to the file or directory. If the path is a directory, all files in the directory are added to the ConfigMap. Each subdirectory are ignored. Note that the Generate\(\) function will create the subdirectories ConfigMaps.
|
||||
|
||||
<a name="ConfigMap.AddBinaryData"></a>
|
||||
### func \(\*ConfigMap\) [AddBinaryData](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L157>)
|
||||
|
||||
```go
|
||||
func (c *ConfigMap) AddBinaryData(key string, value []byte)
|
||||
```
|
||||
|
||||
AddBinaryData adds binary data to the configmap. Append or overwrite the value if the key already exists.
|
||||
|
||||
<a name="ConfigMap.AddData"></a>
|
||||
### func \(\*ConfigMap\) [AddData](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L152>)
|
||||
|
||||
```go
|
||||
func (c *ConfigMap) AddData(key, value string)
|
||||
```
|
||||
|
||||
AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists.
|
||||
|
||||
<a name="ConfigMap.AppendDir"></a>
|
||||
### func \(\*ConfigMap\) [AppendDir](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L166>)
|
||||
|
||||
```go
|
||||
func (c *ConfigMap) AppendDir(path string) error
|
||||
```
|
||||
|
||||
AppendDir adds files from given path to the configmap. It is not recursive, to add all files in a directory, you need to call this function for each subdirectory.
|
||||
|
||||
<a name="ConfigMap.AppendFile"></a>
|
||||
### func \(\*ConfigMap\) [AppendFile](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L213>)
|
||||
|
||||
```go
|
||||
func (c *ConfigMap) AppendFile(path string) error
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="ConfigMap.Filename"></a>
|
||||
### func \(\*ConfigMap\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L237>)
|
||||
|
||||
```go
|
||||
func (c *ConfigMap) Filename() string
|
||||
```
|
||||
|
||||
Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path.
|
||||
|
||||
<a name="ConfigMap.SetData"></a>
|
||||
### func \(\*ConfigMap\) [SetData](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L247>)
|
||||
|
||||
```go
|
||||
func (c *ConfigMap) SetData(data map[string]string)
|
||||
```
|
||||
|
||||
SetData sets the data of the configmap. It replaces the entire data.
|
||||
|
||||
<a name="ConfigMap.Yaml"></a>
|
||||
### func \(\*ConfigMap\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L252>)
|
||||
|
||||
```go
|
||||
func (c *ConfigMap) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
Yaml returns the yaml representation of the configmap
|
||||
|
||||
<a name="ConfigMapMount"></a>
|
||||
## type [ConfigMapMount](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L28-L31>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type ConfigMapMount struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="ConvertOptions"></a>
|
||||
## type [ConvertOptions](<https://github.com/katenary/katenary/blob/develop/generator/chart.go#L27-L36>)
|
||||
|
||||
ConvertOptions are the options to convert a compose project to a helm chart.
|
||||
|
||||
```go
|
||||
type ConvertOptions struct {
|
||||
AppVersion *string
|
||||
OutputDir string
|
||||
ChartVersion string
|
||||
Icon string
|
||||
Profiles []string
|
||||
EnvFiles []string
|
||||
Force bool
|
||||
HelmUpdate bool
|
||||
}
|
||||
```
|
||||
|
||||
<a name="CronJob"></a>
|
||||
## type [CronJob](<https://github.com/katenary/katenary/blob/develop/generator/cronJob.go#L22-L25>)
|
||||
|
||||
CronJob is a kubernetes CronJob.
|
||||
|
||||
```go
|
||||
type CronJob struct {
|
||||
*batchv1.CronJob
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="CronJob.Filename"></a>
|
||||
### func \(\*CronJob\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/cronJob.go#L114>)
|
||||
|
||||
```go
|
||||
func (c *CronJob) Filename() string
|
||||
```
|
||||
|
||||
Filename returns the filename of the cronjob.
|
||||
|
||||
Implements the Yaml interface.
|
||||
|
||||
<a name="CronJob.Yaml"></a>
|
||||
### func \(\*CronJob\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/cronJob.go#L121>)
|
||||
|
||||
```go
|
||||
func (c *CronJob) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
Yaml returns the yaml representation of the cronjob.
|
||||
|
||||
Implements the Yaml interface.
|
||||
|
||||
<a name="CronJobValue"></a>
|
||||
## type [CronJobValue](<https://github.com/katenary/katenary/blob/develop/generator/values.go#L118-L123>)
|
||||
|
||||
CronJobValue is a cronjob configuration that will be saved in values.yaml.
|
||||
|
||||
```go
|
||||
type CronJobValue struct {
|
||||
Repository *RepositoryValue `yaml:"repository,omitempty"`
|
||||
Environment map[string]any `yaml:"environment,omitempty"`
|
||||
ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
|
||||
Schedule string `yaml:"schedule"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="DataMap"></a>
|
||||
## type [DataMap](<https://github.com/katenary/katenary/blob/develop/generator/types.go#L4-L7>)
|
||||
|
||||
DataMap is a kubernetes ConfigMap or Secret. It can be used to add data to the ConfigMap or Secret.
|
||||
|
||||
```go
|
||||
type DataMap interface {
|
||||
SetData(map[string]string)
|
||||
AddData(string, string)
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Deployment"></a>
|
||||
## type [Deployment](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L34-L44>)
|
||||
|
||||
Deployment is a kubernetes Deployment.
|
||||
|
||||
```go
|
||||
type Deployment struct {
|
||||
*appsv1.Deployment `yaml:",inline"`
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewDeployment"></a>
|
||||
### func [NewDeployment](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L48>)
|
||||
|
||||
```go
|
||||
func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment
|
||||
```
|
||||
|
||||
NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name. It also creates the Values map that will be used to create the values.yaml file.
|
||||
|
||||
<a name="Deployment.AddContainer"></a>
|
||||
### func \(\*Deployment\) [AddContainer](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L115>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) AddContainer(service types.ServiceConfig)
|
||||
```
|
||||
|
||||
AddContainer adds a container to the deployment.
|
||||
|
||||
<a name="Deployment.AddHealthCheck"></a>
|
||||
### func \(\*Deployment\) [AddHealthCheck](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L161>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Deployment.AddIngress"></a>
|
||||
### func \(\*Deployment\) [AddIngress](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L190>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress
|
||||
```
|
||||
|
||||
AddIngress adds an ingress to the deployment. It creates the ingress object.
|
||||
|
||||
<a name="Deployment.AddLegacyVolume"></a>
|
||||
### func \(\*Deployment\) [AddLegacyVolume](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L220>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) AddLegacyVolume(name, kind string)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Deployment.AddVolumes"></a>
|
||||
### func \(\*Deployment\) [AddVolumes](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L196>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string)
|
||||
```
|
||||
|
||||
AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment. If the volume is a bind volume it will warn the user that it is not supported yet.
|
||||
|
||||
<a name="Deployment.BindFrom"></a>
|
||||
### func \(\*Deployment\) [BindFrom](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L241>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Deployment.BindMapFilesToContainer"></a>
|
||||
### func \(\*Deployment\) [BindMapFilesToContainer](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L375>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) BindMapFilesToContainer(service types.ServiceConfig, secrets []string, appName string) (*corev1.Container, int)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Deployment.DependsOn"></a>
|
||||
### func \(\*Deployment\) [DependsOn](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L269>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) DependsOn(to *Deployment, servicename string) error
|
||||
```
|
||||
|
||||
DependsOn adds a initContainer to the deployment that will wait for the service to be up.
|
||||
|
||||
<a name="Deployment.Filename"></a>
|
||||
### func \(\*Deployment\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L300>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) Filename() string
|
||||
```
|
||||
|
||||
Filename returns the filename of the deployment.
|
||||
|
||||
<a name="Deployment.MountExchangeVolumes"></a>
|
||||
### func \(\*Deployment\) [MountExchangeVolumes](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L426>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) MountExchangeVolumes()
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Deployment.SetEnvFrom"></a>
|
||||
### func \(\*Deployment\) [SetEnvFrom](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L305>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, samePod ...bool)
|
||||
```
|
||||
|
||||
SetEnvFrom sets the environment variables to a configmap. The configmap is created.
|
||||
|
||||
<a name="Deployment.Yaml"></a>
|
||||
### func \(\*Deployment\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/deployment.go#L450>)
|
||||
|
||||
```go
|
||||
func (d *Deployment) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
Yaml returns the yaml representation of the deployment.
|
||||
|
||||
<a name="FileMapUsage"></a>
|
||||
## type [FileMapUsage](<https://github.com/katenary/katenary/blob/develop/generator/configMap.go#L21>)
|
||||
|
||||
FileMapUsage is the usage of the filemap.
|
||||
|
||||
```go
|
||||
type FileMapUsage uint8
|
||||
```
|
||||
|
||||
<a name="FileMapUsageConfigMap"></a>FileMapUsage constants.
|
||||
|
||||
```go
|
||||
const (
|
||||
FileMapUsageConfigMap FileMapUsage = iota // pure configmap for key:values.
|
||||
FileMapUsageFiles // files in a configmap.
|
||||
)
|
||||
```
|
||||
|
||||
<a name="HelmChart"></a>
|
||||
## type [HelmChart](<https://github.com/katenary/katenary/blob/develop/generator/chart.go#L40-L53>)
|
||||
|
||||
HelmChart is a Helm Chart representation. It contains all the templates, values, versions, helpers...
|
||||
|
||||
```go
|
||||
type HelmChart struct {
|
||||
Templates map[string]*ChartTemplate `yaml:"-"`
|
||||
Values map[string]any `yaml:"-"`
|
||||
VolumeMounts map[string]any `yaml:"-"`
|
||||
|
||||
Name string `yaml:"name"`
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
APIVersion string `yaml:"apiVersion"`
|
||||
Version string `yaml:"version"`
|
||||
AppVersion string `yaml:"appVersion"`
|
||||
Description string `yaml:"description"`
|
||||
Helper string `yaml:"-"`
|
||||
Dependencies []labelstructs.Dependency `yaml:"dependencies,omitempty"`
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Generate"></a>
|
||||
### func [Generate](<https://github.com/katenary/katenary/blob/develop/generator/generator.go#L31>)
|
||||
|
||||
```go
|
||||
func Generate(project *types.Project) (*HelmChart, error)
|
||||
```
|
||||
|
||||
Generate a chart from a compose project. This does not write files to disk, it only creates the HelmChart object.
|
||||
|
||||
The Generate function will create the HelmChart object this way:
|
||||
|
||||
- Detect the service port name or leave the port number if not found.
|
||||
- Create a deployment for each service that are not ingnore.
|
||||
- Create a service and ingresses for each service that has ports and/or declared ingresses.
|
||||
- Create a PVC or Configmap volumes for each volume.
|
||||
- Create init containers for each service which has dependencies to other services.
|
||||
- Create a chart dependencies.
|
||||
- Create a configmap and secrets from the environment variables.
|
||||
- Merge the same\-pod services.
|
||||
|
||||
<a name="NewChart"></a>
|
||||
### func [NewChart](<https://github.com/katenary/katenary/blob/develop/generator/chart.go#L56>)
|
||||
|
||||
```go
|
||||
func NewChart(name string) *HelmChart
|
||||
```
|
||||
|
||||
NewChart creates a new empty chart with the given name.
|
||||
|
||||
<a name="HelmChart.SaveTemplates"></a>
|
||||
### func \(\*HelmChart\) [SaveTemplates](<https://github.com/katenary/katenary/blob/develop/generator/chart.go#L71>)
|
||||
|
||||
```go
|
||||
func (chart *HelmChart) SaveTemplates(templateDir string)
|
||||
```
|
||||
|
||||
SaveTemplates the templates of the chart to the given directory.
|
||||
|
||||
<a name="Ingress"></a>
|
||||
## type [Ingress](<https://github.com/katenary/katenary/blob/develop/generator/ingress.go#L17-L21>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type Ingress struct {
|
||||
*networkv1.Ingress
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewIngress"></a>
|
||||
### func [NewIngress](<https://github.com/katenary/katenary/blob/develop/generator/ingress.go#L24>)
|
||||
|
||||
```go
|
||||
func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress
|
||||
```
|
||||
|
||||
NewIngress creates a new Ingress from a compose service.
|
||||
|
||||
<a name="Ingress.Filename"></a>
|
||||
### func \(\*Ingress\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/ingress.go#L128>)
|
||||
|
||||
```go
|
||||
func (ingress *Ingress) Filename() string
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Ingress.Yaml"></a>
|
||||
### func \(\*Ingress\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/ingress.go#L132>)
|
||||
|
||||
```go
|
||||
func (ingress *Ingress) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="IngressValue"></a>
|
||||
## type [IngressValue](<https://github.com/katenary/katenary/blob/develop/generator/values.go#L29-L36>)
|
||||
|
||||
IngressValue is a ingress configuration that will be saved in values.yaml.
|
||||
|
||||
```go
|
||||
type IngressValue struct {
|
||||
Annotations map[string]string `yaml:"annotations"`
|
||||
Host string `yaml:"host"`
|
||||
Path string `yaml:"path"`
|
||||
Class string `yaml:"class"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
TLS TLS `yaml:"tls"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="PersistenceValue"></a>
|
||||
## type [PersistenceValue](<https://github.com/katenary/katenary/blob/develop/generator/values.go#L16-L21>)
|
||||
|
||||
PersistenceValue is a persistence configuration that will be saved in values.yaml.
|
||||
|
||||
```go
|
||||
type PersistenceValue struct {
|
||||
StorageClass string `yaml:"storageClass"`
|
||||
Size string `yaml:"size"`
|
||||
AccessMode []string `yaml:"accessMode"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="RBAC"></a>
|
||||
## type [RBAC](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L20-L24>)
|
||||
|
||||
RBAC is a kubernetes RBAC containing a role, a rolebinding and an associated serviceaccount.
|
||||
|
||||
```go
|
||||
type RBAC struct {
|
||||
RoleBinding *RoleBinding
|
||||
Role *Role
|
||||
ServiceAccount *ServiceAccount
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewRBAC"></a>
|
||||
### func [NewRBAC](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L27>)
|
||||
|
||||
```go
|
||||
func NewRBAC(service types.ServiceConfig, appName string) *RBAC
|
||||
```
|
||||
|
||||
NewRBAC creates a new RBAC from a compose service. The appName is the name of the application taken from the project name.
|
||||
|
||||
<a name="RepositoryValue"></a>
|
||||
## type [RepositoryValue](<https://github.com/katenary/katenary/blob/develop/generator/values.go#L10-L13>)
|
||||
|
||||
RepositoryValue is a docker repository image and tag that will be saved in values.yaml.
|
||||
|
||||
```go
|
||||
type RepositoryValue struct {
|
||||
Image string `yaml:"image"`
|
||||
Tag string `yaml:"tag"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Role"></a>
|
||||
## type [Role](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L114-L117>)
|
||||
|
||||
Role is a kubernetes Role.
|
||||
|
||||
```go
|
||||
type Role struct {
|
||||
*rbacv1.Role
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Role.Filename"></a>
|
||||
### func \(\*Role\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L119>)
|
||||
|
||||
```go
|
||||
func (r *Role) Filename() string
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Role.Yaml"></a>
|
||||
### func \(\*Role\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L123>)
|
||||
|
||||
```go
|
||||
func (r *Role) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="RoleBinding"></a>
|
||||
## type [RoleBinding](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L100-L103>)
|
||||
|
||||
RoleBinding is a kubernetes RoleBinding.
|
||||
|
||||
```go
|
||||
type RoleBinding struct {
|
||||
*rbacv1.RoleBinding
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="RoleBinding.Filename"></a>
|
||||
### func \(\*RoleBinding\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L105>)
|
||||
|
||||
```go
|
||||
func (r *RoleBinding) Filename() string
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="RoleBinding.Yaml"></a>
|
||||
### func \(\*RoleBinding\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L109>)
|
||||
|
||||
```go
|
||||
func (r *RoleBinding) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Secret"></a>
|
||||
## type [Secret](<https://github.com/katenary/katenary/blob/develop/generator/secret.go#L22-L25>)
|
||||
|
||||
Secret is a kubernetes Secret.
|
||||
|
||||
Implements the DataMap interface.
|
||||
|
||||
```go
|
||||
type Secret struct {
|
||||
*corev1.Secret
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewSecret"></a>
|
||||
### func [NewSecret](<https://github.com/katenary/katenary/blob/develop/generator/secret.go#L28>)
|
||||
|
||||
```go
|
||||
func NewSecret(service types.ServiceConfig, appName string) *Secret
|
||||
```
|
||||
|
||||
NewSecret creates a new Secret from a compose service
|
||||
|
||||
<a name="Secret.AddData"></a>
|
||||
### func \(\*Secret\) [AddData](<https://github.com/katenary/katenary/blob/develop/generator/secret.go#L70>)
|
||||
|
||||
```go
|
||||
func (s *Secret) AddData(key, value string)
|
||||
```
|
||||
|
||||
AddData adds a key value pair to the secret.
|
||||
|
||||
<a name="Secret.Filename"></a>
|
||||
### func \(\*Secret\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/secret.go#L86>)
|
||||
|
||||
```go
|
||||
func (s *Secret) Filename() string
|
||||
```
|
||||
|
||||
Filename returns the filename of the secret.
|
||||
|
||||
<a name="Secret.SetData"></a>
|
||||
### func \(\*Secret\) [SetData](<https://github.com/katenary/katenary/blob/develop/generator/secret.go#L91>)
|
||||
|
||||
```go
|
||||
func (s *Secret) SetData(data map[string]string)
|
||||
```
|
||||
|
||||
SetData sets the data of the secret.
|
||||
|
||||
<a name="Secret.Yaml"></a>
|
||||
### func \(\*Secret\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/secret.go#L98>)
|
||||
|
||||
```go
|
||||
func (s *Secret) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
Yaml returns the yaml representation of the secret.
|
||||
|
||||
<a name="Service"></a>
|
||||
## type [Service](<https://github.com/katenary/katenary/blob/develop/generator/service.go#L18-L21>)
|
||||
|
||||
Service is a kubernetes Service.
|
||||
|
||||
```go
|
||||
type Service struct {
|
||||
*v1.Service `yaml:",inline"`
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewService"></a>
|
||||
### func [NewService](<https://github.com/katenary/katenary/blob/develop/generator/service.go#L24>)
|
||||
|
||||
```go
|
||||
func NewService(service types.ServiceConfig, appName string) *Service
|
||||
```
|
||||
|
||||
NewService creates a new Service from a compose service.
|
||||
|
||||
<a name="Service.AddPort"></a>
|
||||
### func \(\*Service\) [AddPort](<https://github.com/katenary/katenary/blob/develop/generator/service.go#L53>)
|
||||
|
||||
```go
|
||||
func (s *Service) AddPort(port types.ServicePortConfig, serviceName ...string)
|
||||
```
|
||||
|
||||
AddPort adds a port to the service.
|
||||
|
||||
<a name="Service.Filename"></a>
|
||||
### func \(\*Service\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/service.go#L78>)
|
||||
|
||||
```go
|
||||
func (s *Service) Filename() string
|
||||
```
|
||||
|
||||
Filename returns the filename of the service.
|
||||
|
||||
<a name="Service.Yaml"></a>
|
||||
### func \(\*Service\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/service.go#L83>)
|
||||
|
||||
```go
|
||||
func (s *Service) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
Yaml returns the yaml representation of the service.
|
||||
|
||||
<a name="ServiceAccount"></a>
|
||||
## type [ServiceAccount](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L132-L135>)
|
||||
|
||||
ServiceAccount is a kubernetes ServiceAccount.
|
||||
|
||||
```go
|
||||
type ServiceAccount struct {
|
||||
*corev1.ServiceAccount
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="ServiceAccount.Filename"></a>
|
||||
### func \(\*ServiceAccount\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L137>)
|
||||
|
||||
```go
|
||||
func (r *ServiceAccount) Filename() string
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="ServiceAccount.Yaml"></a>
|
||||
### func \(\*ServiceAccount\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/rbac.go#L141>)
|
||||
|
||||
```go
|
||||
func (r *ServiceAccount) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="TLS"></a>
|
||||
## type [TLS](<https://github.com/katenary/katenary/blob/develop/generator/values.go#L23-L26>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type TLS struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
SecretName string `yaml:"secretName"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Value"></a>
|
||||
## type [Value](<https://github.com/katenary/katenary/blob/develop/generator/values.go#L39-L50>)
|
||||
|
||||
Value will be saved in values.yaml. It contains configuration for all deployment and services.
|
||||
|
||||
```go
|
||||
type Value struct {
|
||||
Repository *RepositoryValue `yaml:"repository,omitempty"`
|
||||
Persistence map[string]*PersistenceValue `yaml:"persistence,omitempty"`
|
||||
Ingress *IngressValue `yaml:"ingress,omitempty"`
|
||||
Environment map[string]any `yaml:"environment,omitempty"`
|
||||
Replicas *uint32 `yaml:"replicas,omitempty"`
|
||||
CronJob *CronJobValue `yaml:"cronjob,omitempty"`
|
||||
NodeSelector map[string]string `yaml:"nodeSelector"`
|
||||
Resources map[string]any `yaml:"resources"`
|
||||
ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
|
||||
ServiceAccount string `yaml:"serviceAccount"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewValue"></a>
|
||||
### func [NewValue](<https://github.com/katenary/katenary/blob/develop/generator/values.go#L57>)
|
||||
|
||||
```go
|
||||
func NewValue(service types.ServiceConfig, main ...bool) *Value
|
||||
```
|
||||
|
||||
NewValue creates a new Value from a compose service. The value contains the necessary information to deploy the service \(image, tag, replicas, etc.\).
|
||||
|
||||
If \`main\` is true, the tag will be empty because it will be set in the helm chart appVersion.
|
||||
|
||||
<a name="Value.AddIngress"></a>
|
||||
### func \(\*Value\) [AddIngress](<https://github.com/katenary/katenary/blob/develop/generator/values.go#L90>)
|
||||
|
||||
```go
|
||||
func (v *Value) AddIngress(host, path string)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Value.AddPersistence"></a>
|
||||
### func \(\*Value\) [AddPersistence](<https://github.com/katenary/katenary/blob/develop/generator/values.go#L104>)
|
||||
|
||||
```go
|
||||
func (v *Value) AddPersistence(volumeName string)
|
||||
```
|
||||
|
||||
AddPersistence adds persistence configuration to the Value.
|
||||
|
||||
<a name="VolumeClaim"></a>
|
||||
## type [VolumeClaim](<https://github.com/katenary/katenary/blob/develop/generator/volume.go#L18-L23>)
|
||||
|
||||
VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim.
|
||||
|
||||
```go
|
||||
type VolumeClaim struct {
|
||||
*v1.PersistentVolumeClaim
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewVolumeClaim"></a>
|
||||
### func [NewVolumeClaim](<https://github.com/katenary/katenary/blob/develop/generator/volume.go#L26>)
|
||||
|
||||
```go
|
||||
func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *VolumeClaim
|
||||
```
|
||||
|
||||
NewVolumeClaim creates a new VolumeClaim from a compose service.
|
||||
|
||||
<a name="VolumeClaim.Filename"></a>
|
||||
### func \(\*VolumeClaim\) [Filename](<https://github.com/katenary/katenary/blob/develop/generator/volume.go#L62>)
|
||||
|
||||
```go
|
||||
func (v *VolumeClaim) Filename() string
|
||||
```
|
||||
|
||||
Filename returns the suggested filename for a VolumeClaim.
|
||||
|
||||
<a name="VolumeClaim.Yaml"></a>
|
||||
### func \(\*VolumeClaim\) [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/volume.go#L67>)
|
||||
|
||||
```go
|
||||
func (v *VolumeClaim) Yaml() ([]byte, error)
|
||||
```
|
||||
|
||||
Yaml marshals a VolumeClaim into yaml.
|
||||
|
||||
<a name="Yaml"></a>
|
||||
## type [Yaml](<https://github.com/katenary/katenary/blob/develop/generator/types.go#L10-L13>)
|
||||
|
||||
Yaml is a kubernetes object that can be converted to yaml.
|
||||
|
||||
```go
|
||||
type Yaml interface {
|
||||
Yaml() ([]byte, error)
|
||||
Filename() string
|
||||
}
|
||||
```
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
28
doc/docs/packages/generator/extrafiles.md
Normal file
@@ -0,0 +1,28 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# extrafiles
|
||||
|
||||
```go
|
||||
import "katenary/generator/extrafiles"
|
||||
```
|
||||
|
||||
Package extrafiles provides function to generate the Chart files that are not objects. Like README.md and notes.txt...
|
||||
|
||||
## func [NotesFile](<https://github.com/katenary/katenary/blob/develop/generator/extrafiles/notes.go#L13>)
|
||||
|
||||
```go
|
||||
func NotesFile(services []string) string
|
||||
```
|
||||
|
||||
NotesFile returns the content of the note.txt file.
|
||||
|
||||
<a name="ReadMeFile"></a>
|
||||
## func [ReadMeFile](<https://github.com/katenary/katenary/blob/develop/generator/extrafiles/readme.go#L46>)
|
||||
|
||||
```go
|
||||
func ReadMeFile(charname, description string, values map[string]any) string
|
||||
```
|
||||
|
||||
ReadMeFile returns the content of the README.md file.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
67
doc/docs/packages/generator/katenaryfile.md
Normal file
@@ -0,0 +1,67 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# katenaryfile
|
||||
|
||||
```go
|
||||
import "katenary/generator/katenaryfile"
|
||||
```
|
||||
|
||||
Package katenaryfile is a package for reading and writing katenary files.
|
||||
|
||||
A katenary file, named "katenary.yml" or "katenary.yaml", is a file where you can define the configuration of the conversion avoiding the use of labels in the compose file.
|
||||
|
||||
Formely, the file describe the same structure as in labels, and so that can be validated and completed by LSP. It also ease the use of katenary.
|
||||
|
||||
## func [GenerateSchema](<https://github.com/katenary/katenary/blob/develop/generator/katenaryfile/main.go#L137>)
|
||||
|
||||
```go
|
||||
func GenerateSchema() string
|
||||
```
|
||||
|
||||
GenerateSchema generates the schema for the katenary.yaml file.
|
||||
|
||||
<a name="OverrideWithConfig"></a>
|
||||
## func [OverrideWithConfig](<https://github.com/katenary/katenary/blob/develop/generator/katenaryfile/main.go#L49>)
|
||||
|
||||
```go
|
||||
func OverrideWithConfig(project *types.Project)
|
||||
```
|
||||
|
||||
OverrideWithConfig overrides the project with the katenary.yaml file. It will set the labels of the services with the values from the katenary.yaml file. It work in memory, so it will not modify the original project.
|
||||
|
||||
<a name="Service"></a>
|
||||
## type [Service](<https://github.com/katenary/katenary/blob/develop/generator/katenaryfile/main.go#L27-L44>)
|
||||
|
||||
Service is a struct that contains the service configuration for katenary
|
||||
|
||||
```go
|
||||
type Service struct {
|
||||
MainApp *bool `json:"main-app,omitempty" jsonschema:"title=Is this service the main application"`
|
||||
Values []StringOrMap `json:"values,omitempty" jsonschema:"description=Environment variables to be set in values.yaml with or without a description"`
|
||||
Secrets *labelstructs.Secrets `json:"secrets,omitempty" jsonschema:"title=Secrets,description=Environment variables to be set as secrets"`
|
||||
Ports *labelstructs.Ports `json:"ports,omitempty" jsonschema:"title=Ports,description=Ports to be exposed in services"`
|
||||
Ingress *labelstructs.Ingress `json:"ingress,omitempty" jsonschema:"title=Ingress,description=Ingress configuration"`
|
||||
HealthCheck *labelstructs.HealthCheck `json:"health-check,omitempty" jsonschema:"title=Health Check,description=Health check configuration that respects the kubernetes api"`
|
||||
SamePod *string `json:"same-pod,omitempty" jsonschema:"title=Same Pod,description=Service that should be in the same pod"`
|
||||
Description *string `json:"description,omitempty" jsonschema:"title=Description,description=Description of the service that will be injected in the values.yaml file"`
|
||||
Ignore *bool `json:"ignore,omitempty" jsonschema:"title=Ignore,description=Ignore the service in the conversion"`
|
||||
Dependencies []labelstructs.Dependency `json:"dependencies,omitempty" jsonschema:"title=Dependencies,description=Services that should be injected in the Chart.yaml file"`
|
||||
ConfigMapFile *labelstructs.ConfigMapFile `json:"configmap-files,omitempty" jsonschema:"title=ConfigMap Files,description=Files that should be injected as ConfigMap"`
|
||||
MapEnv *labelstructs.MapEnv `json:"map-env,omitempty" jsonschema:"title=Map Env,description=Map environment variables to another value"`
|
||||
CronJob *labelstructs.CronJob `json:"cron-job,omitempty" jsonschema:"title=Cron Job,description=Cron Job configuration"`
|
||||
EnvFrom *labelstructs.EnvFrom `json:"env-from,omitempty" jsonschema:"title=Env From,description=Inject environment variables from another service"`
|
||||
ExchangeVolumes []*labelstructs.ExchangeVolume `json:"exchange-volumes,omitempty" jsonschema:"title=Exchange Volumes,description=Exchange volumes between services"`
|
||||
ValuesFrom *labelstructs.ValueFrom `json:"values-from,omitempty" jsonschema:"title=Values From,description=Inject values from another service (secret or configmap environment variables)"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="StringOrMap"></a>
|
||||
## type [StringOrMap](<https://github.com/katenary/katenary/blob/develop/generator/katenaryfile/main.go#L24>)
|
||||
|
||||
StringOrMap is a struct that can be either a string or a map of strings. It's a helper struct to unmarshal the katenary.yaml file and produce the schema
|
||||
|
||||
```go
|
||||
type StringOrMap any
|
||||
```
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
110
doc/docs/packages/generator/labels.md
Normal file
@@ -0,0 +1,110 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# labels
|
||||
|
||||
```go
|
||||
import "katenary/generator/labels"
|
||||
```
|
||||
|
||||
Package labels provides functionality to parse and manipulate labels.
|
||||
|
||||
## Constants
|
||||
|
||||
<a name="KatenaryLabelPrefix"></a>
|
||||
|
||||
```go
|
||||
const KatenaryLabelPrefix = "katenary.v3"
|
||||
```
|
||||
|
||||
<a name="GetLabelHelp"></a>
|
||||
## func [GetLabelHelp](<https://github.com/katenary/katenary/blob/develop/generator/labels/katenaryLabels.go#L88>)
|
||||
|
||||
```go
|
||||
func GetLabelHelp(asMarkdown bool) string
|
||||
```
|
||||
|
||||
GetLabelHelp return the help for the labels.
|
||||
|
||||
<a name="GetLabelHelpFor"></a>
|
||||
## func [GetLabelHelpFor](<https://github.com/katenary/katenary/blob/develop/generator/labels/katenaryLabels.go#L97>)
|
||||
|
||||
```go
|
||||
func GetLabelHelpFor(labelname string, asMarkdown bool) string
|
||||
```
|
||||
|
||||
GetLabelHelpFor returns the help for a specific label.
|
||||
|
||||
<a name="GetLabelNames"></a>
|
||||
## func [GetLabelNames](<https://github.com/katenary/katenary/blob/develop/generator/labels/katenaryLabels.go#L72>)
|
||||
|
||||
```go
|
||||
func GetLabelNames() []string
|
||||
```
|
||||
|
||||
GetLabelNames returns a sorted list of all katenary label names.
|
||||
|
||||
<a name="Prefix"></a>
|
||||
## func [Prefix](<https://github.com/katenary/katenary/blob/develop/generator/labels/katenaryLabels.go#L235>)
|
||||
|
||||
```go
|
||||
func Prefix() string
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Help"></a>
|
||||
## type [Help](<https://github.com/katenary/katenary/blob/develop/generator/labels/katenaryLabels.go#L64-L69>)
|
||||
|
||||
Help is the documentation of a label.
|
||||
|
||||
```go
|
||||
type Help struct {
|
||||
Short string `yaml:"short"`
|
||||
Long string `yaml:"long"`
|
||||
Example string `yaml:"example"`
|
||||
Type string `yaml:"type"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Label"></a>
|
||||
## type [Label](<https://github.com/katenary/katenary/blob/develop/generator/labels/katenaryLabels.go#L57>)
|
||||
|
||||
Label is a katenary label to find in compose files.
|
||||
|
||||
```go
|
||||
type Label = string
|
||||
```
|
||||
|
||||
<a name="LabelMainApp"></a>Known labels.
|
||||
|
||||
```go
|
||||
const (
|
||||
LabelMainApp Label = KatenaryLabelPrefix + "/main-app"
|
||||
LabelValues Label = KatenaryLabelPrefix + "/values"
|
||||
LabelSecrets Label = KatenaryLabelPrefix + "/secrets"
|
||||
LabelPorts Label = KatenaryLabelPrefix + "/ports"
|
||||
LabelIngress Label = KatenaryLabelPrefix + "/ingress"
|
||||
LabelMapEnv Label = KatenaryLabelPrefix + "/map-env"
|
||||
LabelHealthCheck Label = KatenaryLabelPrefix + "/health-check"
|
||||
LabelSamePod Label = KatenaryLabelPrefix + "/same-pod"
|
||||
LabelDescription Label = KatenaryLabelPrefix + "/description"
|
||||
LabelIgnore Label = KatenaryLabelPrefix + "/ignore"
|
||||
LabelDependencies Label = KatenaryLabelPrefix + "/dependencies"
|
||||
LabelConfigMapFiles Label = KatenaryLabelPrefix + "/configmap-files"
|
||||
LabelCronJob Label = KatenaryLabelPrefix + "/cronjob"
|
||||
LabelEnvFrom Label = KatenaryLabelPrefix + "/env-from"
|
||||
LabelExchangeVolume Label = KatenaryLabelPrefix + "/exchange-volumes"
|
||||
LabelValueFrom Label = KatenaryLabelPrefix + "/values-from"
|
||||
)
|
||||
```
|
||||
|
||||
<a name="LabelName"></a>
|
||||
### func [LabelName](<https://github.com/katenary/katenary/blob/develop/generator/labels/katenaryLabels.go#L59>)
|
||||
|
||||
```go
|
||||
func LabelName(name string) Label
|
||||
```
|
||||
|
||||
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
246
doc/docs/packages/generator/labels/labelstructs.md
Normal file
@@ -0,0 +1,246 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# labelstructs
|
||||
|
||||
```go
|
||||
import "katenary/generator/labels/labelstructs"
|
||||
```
|
||||
|
||||
Package labelstructs is a package that contains the structs used to represent the labels in the yaml files.
|
||||
|
||||
## type [ConfigMapFile](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/configMap.go#L5>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type ConfigMapFile []string
|
||||
```
|
||||
|
||||
<a name="ConfigMapFileFrom"></a>
|
||||
### func [ConfigMapFileFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/configMap.go#L7>)
|
||||
|
||||
```go
|
||||
func ConfigMapFileFrom(data string) (ConfigMapFile, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="CronJob"></a>
|
||||
## type [CronJob](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/cronJob.go#L5-L10>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type CronJob struct {
|
||||
Image string `yaml:"image,omitempty" json:"image,omitempty"`
|
||||
Command string `yaml:"command" json:"command,omitempty"`
|
||||
Schedule string `yaml:"schedule" json:"schedule,omitempty"`
|
||||
Rbac bool `yaml:"rbac" json:"rbac,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="CronJobFrom"></a>
|
||||
### func [CronJobFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/cronJob.go#L12>)
|
||||
|
||||
```go
|
||||
func CronJobFrom(data string) (*CronJob, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Dependency"></a>
|
||||
## type [Dependency](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/dependencies.go#L6-L12>)
|
||||
|
||||
Dependency is a dependency of a chart to other charts.
|
||||
|
||||
```go
|
||||
type Dependency struct {
|
||||
Values map[string]any `yaml:"-" json:"values,omitempty"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Version string `yaml:"version" json:"version"`
|
||||
Repository string `yaml:"repository" json:"repository"`
|
||||
Alias string `yaml:"alias,omitempty" json:"alias,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="DependenciesFrom"></a>
|
||||
### func [DependenciesFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/dependencies.go#L15>)
|
||||
|
||||
```go
|
||||
func DependenciesFrom(data string) ([]Dependency, error)
|
||||
```
|
||||
|
||||
DependenciesFrom returns a slice of dependencies from the given string.
|
||||
|
||||
<a name="EnvFrom"></a>
|
||||
## type [EnvFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/envFrom.go#L5>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type EnvFrom []string
|
||||
```
|
||||
|
||||
<a name="EnvFromFrom"></a>
|
||||
### func [EnvFromFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/envFrom.go#L8>)
|
||||
|
||||
```go
|
||||
func EnvFromFrom(data string) (EnvFrom, error)
|
||||
```
|
||||
|
||||
EnvFromFrom returns a EnvFrom from the given string.
|
||||
|
||||
<a name="ExchangeVolume"></a>
|
||||
## type [ExchangeVolume](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/exchangeVolume.go#L5-L10>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type ExchangeVolume struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
MountPath string `yaml:"mountPath" json:"mountPath"`
|
||||
Type string `yaml:"type,omitempty" json:"type,omitempty"`
|
||||
Init string `yaml:"init,omitempty" json:"init,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewExchangeVolumes"></a>
|
||||
### func [NewExchangeVolumes](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/exchangeVolume.go#L12>)
|
||||
|
||||
```go
|
||||
func NewExchangeVolumes(data string) ([]*ExchangeVolume, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="HealthCheck"></a>
|
||||
## type [HealthCheck](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/probes.go#L11-L14>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type HealthCheck struct {
|
||||
LivenessProbe *corev1.Probe `yaml:"livenessProbe,omitempty" json:"livenessProbe,omitempty"`
|
||||
ReadinessProbe *corev1.Probe `yaml:"readinessProbe,omitempty" json:"readinessProbe,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="ProbeFrom"></a>
|
||||
### func [ProbeFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/probes.go#L16>)
|
||||
|
||||
```go
|
||||
func ProbeFrom(data string) (*HealthCheck, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Ingress"></a>
|
||||
## type [Ingress](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/ingress.go#L14-L22>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type Ingress struct {
|
||||
Port *int32 `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
Annotations map[string]string `yaml:"annotations,omitempty" jsonschema:"nullable" json:"annotations,omitempty"`
|
||||
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
|
||||
Path *string `yaml:"path,omitempty" json:"path,omitempty"`
|
||||
Class *string `yaml:"class,omitempty" json:"class,omitempty" jsonschema:"default:-"`
|
||||
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
||||
TLS *TLS `yaml:"tls,omitempty" json:"tls,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="IngressFrom"></a>
|
||||
### func [IngressFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/ingress.go#L25>)
|
||||
|
||||
```go
|
||||
func IngressFrom(data string) (*Ingress, error)
|
||||
```
|
||||
|
||||
IngressFrom creates a new Ingress from a compose service.
|
||||
|
||||
<a name="MapEnv"></a>
|
||||
## type [MapEnv](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/mapenv.go#L5>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type MapEnv map[string]string
|
||||
```
|
||||
|
||||
<a name="MapEnvFrom"></a>
|
||||
### func [MapEnvFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/mapenv.go#L8>)
|
||||
|
||||
```go
|
||||
func MapEnvFrom(data string) (MapEnv, error)
|
||||
```
|
||||
|
||||
MapEnvFrom returns a MapEnv from the given string.
|
||||
|
||||
<a name="Ports"></a>
|
||||
## type [Ports](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/ports.go#L5>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type Ports []uint32
|
||||
```
|
||||
|
||||
<a name="PortsFrom"></a>
|
||||
### func [PortsFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/ports.go#L8>)
|
||||
|
||||
```go
|
||||
func PortsFrom(data string) (Ports, error)
|
||||
```
|
||||
|
||||
PortsFrom returns a Ports from the given string.
|
||||
|
||||
<a name="Secrets"></a>
|
||||
## type [Secrets](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/secrets.go#L5>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type Secrets []string
|
||||
```
|
||||
|
||||
<a name="SecretsFrom"></a>
|
||||
### func [SecretsFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/secrets.go#L7>)
|
||||
|
||||
```go
|
||||
func SecretsFrom(data string) (Secrets, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="TLS"></a>
|
||||
## type [TLS](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/ingress.go#L10-L12>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type TLS struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="ValueFrom"></a>
|
||||
## type [ValueFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/valueFrom.go#L5>)
|
||||
|
||||
|
||||
|
||||
```go
|
||||
type ValueFrom map[string]string
|
||||
```
|
||||
|
||||
<a name="GetValueFrom"></a>
|
||||
### func [GetValueFrom](<https://github.com/katenary/katenary/blob/develop/generator/labels/labelstructs/valueFrom.go#L7>)
|
||||
|
||||
```go
|
||||
func GetValueFrom(data string) (*ValueFrom, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
19
doc/docs/packages/parser.md
Normal file
@@ -0,0 +1,19 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# parser
|
||||
|
||||
```go
|
||||
import "katenary/parser"
|
||||
```
|
||||
|
||||
Package parser is a wrapper around compose\-go to parse compose files.
|
||||
|
||||
## func [Parse](<https://github.com/katenary/katenary/blob/develop/parser/main.go#L29>)
|
||||
|
||||
```go
|
||||
func Parse(profiles []string, envFiles []string, dockerComposeFile ...string) (*types.Project, error)
|
||||
```
|
||||
|
||||
Parse compose files and return a project. The project is parsed with dotenv, osenv and profiles.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
221
doc/docs/packages/utils.md
Normal file
@@ -0,0 +1,221 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# utils
|
||||
|
||||
```go
|
||||
import "katenary/utils"
|
||||
```
|
||||
|
||||
Package utils provides some utility functions used in katenary. It defines some constants and functions used in the whole project.
|
||||
|
||||
## Constants
|
||||
|
||||
<a name="DirectoryPermission"></a>DirectoryPermission is the default values for permissions apply to created directories.
|
||||
|
||||
```go
|
||||
const DirectoryPermission = 0o755
|
||||
```
|
||||
|
||||
<a name="AsResourceName"></a>
|
||||
## func [AsResourceName](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L196>)
|
||||
|
||||
```go
|
||||
func AsResourceName(name string) string
|
||||
```
|
||||
|
||||
AsResourceName returns a resource name with underscores to respect the kubernetes naming convention. It's the opposite of FixedResourceName.
|
||||
|
||||
<a name="Confirm"></a>
|
||||
## func [Confirm](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L164>)
|
||||
|
||||
```go
|
||||
func Confirm(question string, icon ...Icon) bool
|
||||
```
|
||||
|
||||
Confirm asks a question and returns true if the answer is y.
|
||||
|
||||
<a name="CountStartingSpaces"></a>
|
||||
## func [CountStartingSpaces](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L41>)
|
||||
|
||||
```go
|
||||
func CountStartingSpaces(line string) int
|
||||
```
|
||||
|
||||
CountStartingSpaces counts the number of spaces at the beginning of a string.
|
||||
|
||||
<a name="EncodeBasicYaml"></a>
|
||||
## func [EncodeBasicYaml](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L178>)
|
||||
|
||||
```go
|
||||
func EncodeBasicYaml(data any) ([]byte, error)
|
||||
```
|
||||
|
||||
EncodeBasicYaml encodes a basic yaml from an interface.
|
||||
|
||||
<a name="FixedResourceName"></a>
|
||||
## func [FixedResourceName](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L190>)
|
||||
|
||||
```go
|
||||
func FixedResourceName(name string) string
|
||||
```
|
||||
|
||||
FixedResourceName returns a resource name without underscores to respect the kubernetes naming convention.
|
||||
|
||||
<a name="GetContainerByName"></a>
|
||||
## func [GetContainerByName](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L87>)
|
||||
|
||||
```go
|
||||
func GetContainerByName(name string, containers []corev1.Container) (*corev1.Container, int)
|
||||
```
|
||||
|
||||
GetContainerByName returns a container by name and its index in the array. It returns nil, \-1 if not found.
|
||||
|
||||
<a name="GetKind"></a>
|
||||
## func [GetKind](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L54>)
|
||||
|
||||
```go
|
||||
func GetKind(path string) (kind string)
|
||||
```
|
||||
|
||||
GetKind returns the kind of the resource from the file path.
|
||||
|
||||
<a name="GetServiceNameByPort"></a>
|
||||
## func [GetServiceNameByPort](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L77>)
|
||||
|
||||
```go
|
||||
func GetServiceNameByPort(port int) string
|
||||
```
|
||||
|
||||
GetServiceNameByPort returns the service name for a port. It the service name is not found, it returns an empty string.
|
||||
|
||||
<a name="GetValuesFromLabel"></a>
|
||||
## func [GetValuesFromLabel](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L129>)
|
||||
|
||||
```go
|
||||
func GetValuesFromLabel(service types.ServiceConfig, LabelValues string) map[string]*EnvConfig
|
||||
```
|
||||
|
||||
GetValuesFromLabel returns a map of values from a label.
|
||||
|
||||
<a name="HashComposefiles"></a>
|
||||
## func [HashComposefiles](<https://github.com/katenary/katenary/blob/develop/utils/hash.go#L12>)
|
||||
|
||||
```go
|
||||
func HashComposefiles(files []string) (string, error)
|
||||
```
|
||||
|
||||
HashComposefiles returns a hash of the compose files.
|
||||
|
||||
<a name="Int32Ptr"></a>
|
||||
## func [Int32Ptr](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L35>)
|
||||
|
||||
```go
|
||||
func Int32Ptr(i int32) *int32
|
||||
```
|
||||
|
||||
Int32Ptr returns a pointer to an int32.
|
||||
|
||||
<a name="PathToName"></a>
|
||||
## func [PathToName](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L106>)
|
||||
|
||||
```go
|
||||
func PathToName(path string) string
|
||||
```
|
||||
|
||||
PathToName converts a path to a kubernetes complient name.
|
||||
|
||||
<a name="StrPtr"></a>
|
||||
## func [StrPtr](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L38>)
|
||||
|
||||
```go
|
||||
func StrPtr(s string) *string
|
||||
```
|
||||
|
||||
StrPtr returns a pointer to a string.
|
||||
|
||||
<a name="TplName"></a>
|
||||
## func [TplName](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L22>)
|
||||
|
||||
```go
|
||||
func TplName(serviceName, appname string, suffix ...string) string
|
||||
```
|
||||
|
||||
TplName returns the name of the kubernetes resource as a template string. It is used in the templates and defined in \_helper.tpl file.
|
||||
|
||||
<a name="TplValue"></a>
|
||||
## func [TplValue](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L97>)
|
||||
|
||||
```go
|
||||
func TplValue(serviceName, variable string, pipes ...string) string
|
||||
```
|
||||
|
||||
TplValue returns a string that can be used in a template to access a value from the values file.
|
||||
|
||||
<a name="Warn"></a>
|
||||
## func [Warn](<https://github.com/katenary/katenary/blob/develop/utils/icons.go#L25>)
|
||||
|
||||
```go
|
||||
func Warn(msg ...any)
|
||||
```
|
||||
|
||||
Warn prints a warning message
|
||||
|
||||
<a name="WordWrap"></a>
|
||||
## func [WordWrap](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L159>)
|
||||
|
||||
```go
|
||||
func WordWrap(text string, lineWidth int) string
|
||||
```
|
||||
|
||||
WordWrap wraps a string to a given line width. Warning: it may break the string. You need to check the result.
|
||||
|
||||
<a name="Wrap"></a>
|
||||
## func [Wrap](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L71>)
|
||||
|
||||
```go
|
||||
func Wrap(src, above, below string) string
|
||||
```
|
||||
|
||||
Wrap wraps a string with a string above and below. It will respect the indentation of the src string.
|
||||
|
||||
<a name="EnvConfig"></a>
|
||||
## type [EnvConfig](<https://github.com/katenary/katenary/blob/develop/utils/utils.go#L123-L126>)
|
||||
|
||||
EnvConfig is a struct to hold the description of an environment variable.
|
||||
|
||||
```go
|
||||
type EnvConfig struct {
|
||||
Service types.ServiceConfig
|
||||
Description string
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Icon"></a>
|
||||
## type [Icon](<https://github.com/katenary/katenary/blob/develop/utils/icons.go#L6>)
|
||||
|
||||
Icon is a unicode icon
|
||||
|
||||
```go
|
||||
type Icon string
|
||||
```
|
||||
|
||||
<a name="IconSuccess"></a>Icons used in katenary.
|
||||
|
||||
```go
|
||||
const (
|
||||
IconSuccess Icon = "✅"
|
||||
IconFailure Icon = "❌"
|
||||
IconWarning Icon = "❕"
|
||||
IconNote Icon = "📝"
|
||||
IconWorld Icon = "🌐"
|
||||
IconPlug Icon = "🔌"
|
||||
IconPackage Icon = "📦"
|
||||
IconCabinet Icon = "🗄️"
|
||||
IconInfo Icon = "🔵"
|
||||
IconSecret Icon = "🔒"
|
||||
IconConfig Icon = "🔧"
|
||||
IconDependency Icon = "🔗"
|
||||
)
|
||||
```
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
48
doc/docs/statics/addons.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// Install the highlight.js in the documentation. Then
|
||||
// highlight all the source code.
|
||||
function hljsInstall() {
|
||||
const version = "11.9.0";
|
||||
const theme = "github-dark";
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = `//cdnjs.cloudflare.com/ajax/libs/highlight.js/${version}/highlight.min.js`;
|
||||
script.onload = () => {
|
||||
const style = document.createElement("link");
|
||||
style.rel = "stylesheet";
|
||||
style.href = `//cdnjs.cloudflare.com/ajax/libs/highlight.js/${version}/styles/${theme}.min.css`;
|
||||
document.head.appendChild(style);
|
||||
hljs.highlightAll();
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
// All images in an .zoomable div is zoomable, that
|
||||
// meanse that we can click to zoom and unzoom.
|
||||
// This needs specific CSS (see main.css).
|
||||
function makeImagesZoomable() {
|
||||
const zone = document.querySelectorAll(".zoomable");
|
||||
|
||||
zone.forEach((z, i) => {
|
||||
const im = z.querySelectorAll("img,svg");
|
||||
if (im.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.setAttribute("type", "checkbox");
|
||||
input.setAttribute("id", `image-zoom-${i}`);
|
||||
z.appendChild(input);
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.setAttribute("for", `image-zoom-${i}`);
|
||||
z.appendChild(label);
|
||||
|
||||
label.appendChild(im[0]);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
hljsInstall();
|
||||
makeImagesZoomable();
|
||||
});
|
121
doc/docs/statics/icon.svg
Normal file
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg211948"
|
||||
viewBox="0 0 95.440796 85.01416"
|
||||
height="85.01416"
|
||||
width="95.440796"
|
||||
version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata211954">
|
||||
<rdf:rdf>
|
||||
<cc:work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:work>
|
||||
</rdf:rdf>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs211952" />
|
||||
<linearGradient
|
||||
spreadMethod="pad"
|
||||
y2="0.30000001"
|
||||
x2="-0.1"
|
||||
y1="1.2"
|
||||
x1="0.30000001"
|
||||
id="3d_gradient2-logo-24885591-b378-4c55-b87b-b7d42ed10694">
|
||||
<stop
|
||||
id="stop211929"
|
||||
stop-opacity="1"
|
||||
stop-color="#ffffff"
|
||||
offset="0%" />
|
||||
<stop
|
||||
id="stop211931"
|
||||
stop-opacity="1"
|
||||
stop-color="#000000"
|
||||
offset="100%" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientTransform="rotate(-30)"
|
||||
spreadMethod="pad"
|
||||
y2="0.30000001"
|
||||
x2="-0.1"
|
||||
y1="1.2"
|
||||
x1="0.30000001"
|
||||
id="3d_gradient3-logo-24885591-b378-4c55-b87b-b7d42ed10694">
|
||||
<stop
|
||||
id="stop211934"
|
||||
stop-opacity="1"
|
||||
stop-color="#ffffff"
|
||||
offset="0%" />
|
||||
<stop
|
||||
id="stop211936"
|
||||
stop-opacity="1"
|
||||
stop-color="#cccccc"
|
||||
offset="50%" />
|
||||
<stop
|
||||
id="stop211938"
|
||||
stop-opacity="1"
|
||||
stop-color="#000000"
|
||||
offset="100%" />
|
||||
</linearGradient>
|
||||
<g
|
||||
id="logo-group"
|
||||
transform="translate(-394.01147,-211.65063)">
|
||||
<path
|
||||
d="m 773.83594,234.02344 c -0.4545,0.0375 -0.92821,0.1629 -1.40821,0.3789 -1.91999,0.864 -2.68851,2.68743 -1.72851,4.60743 l 21.4082,44.35351 0.0957,0.0957 -9.5996,21.02344 c -0.96,2.112 -0.28778,3.74342 1.82421,4.60742 0.576,0.288 1.15252,0.38477 1.72852,0.38477 1.248,0 2.2072,-0.76703 2.7832,-2.20703 l 31.10352,-68.35352 c 0.96,-2.112 0.28778,-3.64772 -1.82422,-4.51172 -2.112,-0.864 -3.64742,-0.28748 -4.60742,1.72852 l -17.85547,39.35937 -18.7207,-39.45507 c -0.648,-1.44 -1.83572,-2.12422 -3.19922,-2.01172 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path9" />
|
||||
<path
|
||||
d="m 692.07617,233.63477 c -15.26398,0 -26.68945,11.51921 -26.68945,26.7832 0,15.26398 11.13594,26.68945 25.91992,26.68945 8.44799,0 15.64875,-3.84038 19.96875,-9.98437 v 5.85547 c 0,2.11199 1.53614,3.64843 3.74414,3.64843 2.112,0 3.74414,-1.53644 3.74414,-3.64843 v -22.56055 c -0.096,-15.26399 -11.51951,-26.7832 -26.6875,-26.7832 z m 0,6.7207 c 11.03999,0 19.39063,8.63851 19.39063,20.0625 0,11.42399 -8.35064,19.96875 -19.39063,19.96875 -11.03999,0 -19.48828,-8.54476 -19.48828,-19.96875 0,-11.42399 8.44829,-20.0625 19.48828,-20.0625 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path8" />
|
||||
<path
|
||||
d="m 566.60352,233.63477 c -14.78399,0 -25.15235,11.13521 -25.15235,26.7832 0,15.64798 11.03981,26.68945 26.5918,26.68945 5.95199,0 13.24866,-2.59224 17.47265,-6.24023 1.536,-1.344 1.4406,-3.36079 -0.1914,-4.80078 -1.344,-1.056 -3.26508,-0.9606 -4.70508,0.1914 -2.784,2.4 -7.96818,4.22461 -12.57617,4.22461 -10.65599,0 -18.52799,-7.20007 -19.58399,-17.66406 h 38.78516 c 2.016,0 3.45508,-1.34338 3.45508,-3.35938 0,-15.07198 -9.59972,-25.82421 -24.0957,-25.82421 z m 0,6.62304 c 9.69599,0 16.22359,6.72003 17.18359,16.41602 h -35.13477 c 1.344,-9.69599 8.15919,-16.41602 17.95118,-16.41602 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path7" />
|
||||
<path
|
||||
d="m 756.875,233.53906 c -6.71999,0 -12.67202,3.16757 -16.41602,8.35156 v -4.1289 c 0,-2.304 -1.34248,-3.64649 -3.64648,-3.64649 -2.304,0 -3.64844,1.34249 -3.64844,3.64649 v 45.2168 c 0,2.30399 1.34444,3.64843 3.64844,3.64843 2.304,0 3.64648,-1.34444 3.64648,-3.64843 v -28.70313 c 0,-8.92799 7.87302,-14.68851 18.625,-13.72852 3.168,0.192 5.75965,0.76867 6.43164,-2.11132 0.768,-3.168 -2.68863,-4.89649 -8.64062,-4.89649 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path6" />
|
||||
<path
|
||||
d="m 628.04297,233.53906 c -7.10399,0 -13.34271,2.87999 -17.4707,7.58399 v -3.26368 c 0,-2.20799 -1.44044,-3.74414 -3.64844,-3.74414 -2.208,0 -3.74414,1.53615 -3.74414,3.74414 v 45.11915 c 0,2.20799 1.53614,3.64843 3.74414,3.64843 2.208,0 3.64844,-1.44044 3.64844,-3.64843 V 254.5625 c 0,-7.96799 7.19913,-14.01563 16.70312,-14.01563 9.79199,0 17.18359,5.66467 17.18359,17.47266 v 24.95899 c 0,2.11199 1.63215,3.64843 3.74414,3.64843 2.016,0 3.64844,-1.53644 3.64844,-3.64843 v -24.95899 c 0,-15.83998 -10.2726,-24.48047 -23.80859,-24.48047 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path5" />
|
||||
<path
|
||||
d="m 512.17187,217.41016 c -2.11199,0 -3.64843,1.53614 -3.64843,3.74414 v 14.88086 h -6.24024 c -2.01599,0 -3.35937,1.34367 -3.35937,3.26367 0,1.824 1.34338,3.16797 3.35937,3.16797 h 6.24024 v 25.63281 c 0,10.65599 7.39207,18.43134 17.66406,18.52734 h 2.01562 c 2.304,0 4.03321,-1.53644 4.03321,-3.64843 0,-2.208 -1.44104,-3.74415 -3.45703,-3.74415 h -2.5918 c -5.95199,0 -10.27148,-4.60677 -10.27148,-11.13476 V 242.4668 h 10.84765 c 2.016,0 3.35938,-1.34397 3.35938,-3.16797 0,-1.92 -1.34338,-3.26367 -3.35938,-3.26367 H 515.91602 V 221.1543 c 0,-2.208 -1.53615,-3.74414 -3.74415,-3.74414 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path4" />
|
||||
<image
|
||||
xlink:href=""
|
||||
id="container"
|
||||
x="272"
|
||||
y="144"
|
||||
width="480"
|
||||
height="480"
|
||||
style="display:none" />
|
||||
<image
|
||||
xlink:href=""
|
||||
id="icon_container"
|
||||
style="display:none"
|
||||
x="0"
|
||||
y="0"
|
||||
width="0"
|
||||
height="0" />
|
||||
<path
|
||||
style="fill:#388ec7;fill-opacity:1;stroke:none;stroke-width:1.41128"
|
||||
d="m 488.71354,251.36014 -33.31908,17.65778 -13.63054,6.42952 -13.63053,-6.42952 -33.31907,-17.65778 c 0,5.32502 -2.24747,16.12101 0.77828,20.72433 1.84892,2.81293 6.84753,4.46784 9.82323,6.09432 8.26868,4.51957 16.72019,8.751 24.98932,13.27082 3.1083,1.69895 7.62988,5.16897 11.35877,5.21476 3.70778,0.0455 8.2451,-3.55102 11.35878,-5.19752 8.33909,-4.40967 16.72005,-8.76818 24.98932,-13.28806 3.05286,-1.66866 8.68533,-3.49778 10.14461,-6.69253 2.3911,-5.23485 0.45691,-14.46175 0.45691,-20.12612 m -46.19236,16.00215 c 3.59731,-0.51544 7.48607,-3.47354 10.60152,-5.12097 7.58911,-4.0131 15.20174,-8.00941 22.71755,-12.13797 3.53683,-1.94283 9.05374,-3.7013 11.65911,-6.69847 3.28982,-3.78453 1.93068,-13.45759 -2.58668,-15.74886 -4.7363,-2.40229 -8.80251,1.12086 -12.85868,3.25251 l -21.20306,11.2665 c -2.56736,1.35182 -6.01463,4.01777 -9.08702,4.01777 -3.29073,0 -7.12524,-2.97742 -9.84427,-4.47101 -6.50984,-3.57597 -13.17851,-6.90091 -19.68854,-10.47698 -2.85668,-1.56921 -7.10587,-4.85415 -10.60153,-4.52437 -7.2756,0.6864 -9.69308,11.97712 -5.60111,16.68444 2.43875,2.80547 7.56529,4.49972 10.90188,6.26408 7.83167,4.14137 15.6667,8.28474 23.4748,12.46456 3.21433,1.72068 8.18431,5.79216 12.11603,5.22877 m 30.29007,-45.33762 v -1.39693 l -18.17404,-8.90973 -12.11603,4.65427 -12.87329,-4.72164 -18.93129,8.9771 v 1.39693 l 21.20305,11.17542 9.84427,4.34169 9.84428,-4.33415 z"
|
||||
id="path1" />
|
||||
<path
|
||||
d="m 334.95508,211.65039 c -2.016,0 -3.74414,1.63214 -3.74414,3.74414 v 67.48828 c 0,2.112 1.72814,3.74414 3.74414,3.74414 2.112,0 3.74414,-1.63214 3.74414,-3.74414 v -24.57617 l 8.54492,-8.64062 26.11133,35.32812 c 0.768,1.152 1.82397,1.63281 3.16797,1.63281 2.68799,0 4.5115,-3.36112 2.6875,-5.95312 l -26.5918,-36.28711 26.30469,-26.30469 c 2.304,-2.592 1.24701,-6.43164 -2.20899,-6.43164 -1.152,0 -2.01461,0.38375 -2.97461,1.34375 l -35.04101,35.04102 v -32.64063 c 0,-2.112 -1.63214,-3.74414 -3.74414,-3.74414 z"
|
||||
style="fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path3" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.7 KiB |
1
doc/docs/statics/klee.svg
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
doc/docs/statics/logo-bright.png
Normal file
After Width: | Height: | Size: 15 KiB |
118
doc/docs/statics/logo-bright.svg
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg211948"
|
||||
viewBox="0 0 544.44238 97.824005"
|
||||
height="97.824005"
|
||||
width="544.44238"
|
||||
version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata211954">
|
||||
<rdf:rdf>
|
||||
<cc:work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:work>
|
||||
</rdf:rdf>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs211952" />
|
||||
<linearGradient
|
||||
spreadMethod="pad"
|
||||
y2="0.30000001"
|
||||
x2="-0.1"
|
||||
y1="1.2"
|
||||
x1="0.30000001"
|
||||
id="3d_gradient2-logo-24885591-b378-4c55-b87b-b7d42ed10694">
|
||||
<stop
|
||||
id="stop211929"
|
||||
stop-opacity="1"
|
||||
stop-color="#ffffff"
|
||||
offset="0%" />
|
||||
<stop
|
||||
id="stop211931"
|
||||
stop-opacity="1"
|
||||
stop-color="#000000"
|
||||
offset="100%" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientTransform="rotate(-30)"
|
||||
spreadMethod="pad"
|
||||
y2="0.30000001"
|
||||
x2="-0.1"
|
||||
y1="1.2"
|
||||
x1="0.30000001"
|
||||
id="3d_gradient3-logo-24885591-b378-4c55-b87b-b7d42ed10694">
|
||||
<stop
|
||||
id="stop211934"
|
||||
stop-opacity="1"
|
||||
stop-color="#ffffff"
|
||||
offset="0%" />
|
||||
<stop
|
||||
id="stop211936"
|
||||
stop-opacity="1"
|
||||
stop-color="#cccccc"
|
||||
offset="50%" />
|
||||
<stop
|
||||
id="stop211938"
|
||||
stop-opacity="1"
|
||||
stop-color="#000000"
|
||||
offset="100%" />
|
||||
</linearGradient>
|
||||
<g
|
||||
id="logo-group"
|
||||
transform="translate(-185.54797,-175.3735)">
|
||||
<image
|
||||
xlink:href=""
|
||||
id="container"
|
||||
x="272"
|
||||
y="144"
|
||||
width="480"
|
||||
height="480"
|
||||
style="display:none" />
|
||||
<image
|
||||
xlink:href=""
|
||||
id="icon_container"
|
||||
style="display:none"
|
||||
x="0"
|
||||
y="0"
|
||||
width="0"
|
||||
height="0" />
|
||||
<g
|
||||
id="g18"
|
||||
transform="translate(-91.179677,-177.97015)">
|
||||
<path
|
||||
style="fill:#388ec7;fill-opacity:1;stroke:none;stroke-width:1.41128"
|
||||
d="m 650.16134,532.23713 -33.31908,17.65778 -13.63054,6.42952 -13.63053,-6.42952 -33.31907,-17.65778 c 0,5.32502 -2.24747,16.12101 0.77828,20.72433 1.84892,2.81293 6.84753,4.46784 9.82323,6.09432 8.26868,4.51957 16.72019,8.751 24.98932,13.27082 3.1083,1.69895 7.62988,5.16897 11.35877,5.21476 3.70778,0.0455 8.2451,-3.55102 11.35878,-5.19752 8.33909,-4.40967 16.72005,-8.76818 24.98932,-13.28806 3.05286,-1.66866 8.68533,-3.49778 10.14461,-6.69253 2.3911,-5.23485 0.45691,-14.46175 0.45691,-20.12612 m -46.19236,16.00215 c 3.59731,-0.51544 7.48607,-3.47354 10.60152,-5.12097 7.58911,-4.0131 15.20174,-8.00941 22.71755,-12.13797 3.53683,-1.94283 9.05374,-3.7013 11.65911,-6.69847 3.28982,-3.78453 1.93068,-13.45759 -2.58668,-15.74886 -4.7363,-2.40229 -8.80251,1.12086 -12.85868,3.25251 l -21.20306,11.2665 c -2.56736,1.35182 -6.01463,4.01777 -9.08702,4.01777 -3.29073,0 -7.12524,-2.97742 -9.84427,-4.47101 -6.50984,-3.57597 -13.17851,-6.90091 -19.68854,-10.47698 -2.85668,-1.56921 -7.10587,-4.85415 -10.60153,-4.52437 -7.2756,0.6864 -9.69308,11.97712 -5.60111,16.68444 2.43875,2.80547 7.56529,4.49972 10.90188,6.26408 7.83167,4.14137 15.6667,8.28474 23.4748,12.46456 3.21433,1.72068 8.18431,5.79216 12.11603,5.22877 m 30.29007,-45.33762 v -1.39693 l -18.17404,-8.90973 -12.11603,4.65427 -12.87329,-4.72164 -18.93129,8.9771 v 1.39693 l 21.20305,11.17542 9.84427,4.34169 9.84428,-4.33415 z"
|
||||
id="path17" />
|
||||
<path
|
||||
d="m 388.93329,654.58136 c 2.112,0 3.744,-1.632 3.744,-3.744 v -24.576 l 8.544,-8.64 26.112,35.328 c 0.768,1.152 1.824,1.632 3.168,1.632 2.688,0 4.512,-3.36 2.688,-5.952 l -26.592,-36.288 26.304,-26.304 c 2.304,-2.592 1.248,-6.432 -2.208,-6.432 -1.152,0 -2.016,0.384 -2.976,1.344 l -35.04,35.04 v -32.64 c 0,-2.112 -1.632,-3.744 -3.744,-3.744 -2.016,0 -3.744,1.632 -3.744,3.744 v 67.488 c 0,2.112 1.728,3.744 3.744,3.744 z m 75.55212,0.48 c 8.448,0 15.648,-3.84 19.968,-9.984 v 5.856 c 0,2.112 1.536,3.648 3.744,3.648 2.112,0 3.744,-1.536 3.744,-3.648 v -22.56 c -0.096,-15.264 -11.52,-26.784 -26.688,-26.784 -15.264,0 -26.688,11.52 -26.688,26.784 0,15.264 11.136,26.688 25.92,26.688 z m 0.768,-6.72 c -11.04,0 -19.488,-8.544 -19.488,-19.968 0,-11.424 8.448,-20.064 19.488,-20.064 11.04,0 19.392,8.64 19.392,20.064 0,11.424 -8.352,19.968 -19.392,19.968 z m 61.63201,6.24 h 2.016 c 2.304,0 4.032,-1.536 4.032,-3.648 0,-2.208 -1.44,-3.744 -3.456,-3.744 h -2.592 c -5.952,0 -10.272,-4.608 -10.272,-11.136 v -25.632 h 10.848 c 2.016,0 3.36,-1.344 3.36,-3.168 0,-1.92 -1.344,-3.264 -3.36,-3.264 h -10.848 v -14.88 c 0,-2.208 -1.536,-3.744 -3.744,-3.744 -2.112,0 -3.648,1.536 -3.648,3.744 v 14.88 h -6.24 c -2.016,0 -3.36,1.344 -3.36,3.264 0,1.824 1.344,3.168 3.36,3.168 h 6.24 v 25.632 c 0,10.656 7.392,18.432 17.664,18.528 z m 41.85604,0.48 c 5.952,0 13.248,-2.592 17.472,-6.24 1.536,-1.344 1.44,-3.36 -0.192,-4.8 -1.344,-1.056 -3.264,-0.96 -4.704,0.192 -2.784,2.4 -7.968,4.224 -12.576,4.224 -10.656,0 -18.528,-7.2 -19.584,-17.664 h 38.784 c 2.016,0 3.456,-1.344 3.456,-3.36 0,-15.072 -9.6,-25.824 -24.096,-25.824 -14.784,0 -25.152,11.136 -25.152,26.784 0,15.648 11.04,26.688 26.592,26.688 z m -1.44,-46.848 c 9.696,0 16.224,6.72 17.184,16.416 h -35.136 c 1.344,-9.696 8.16,-16.416 17.952,-16.416 z m 40.32003,46.368 c 2.208,0 3.648,-1.44 3.648,-3.648 v -28.416 c 0,-7.968 7.2,-14.016 16.704,-14.016 9.792,0 17.184,5.664 17.184,17.472 v 24.96 c 0,2.112 1.632,3.648 3.744,3.648 2.016,0 3.648,-1.536 3.648,-3.648 v -24.96 c 0,-15.84 -10.272,-24.48 -23.808,-24.48 -7.104,0 -13.344,2.88 -17.472,7.584 v -3.264 c 0,-2.208 -1.44,-3.744 -3.648,-3.744 -2.208,0 -3.744,1.536 -3.744,3.744 v 45.12 c 0,2.208 1.536,3.648 3.744,3.648 z m 84.384,0.48 c 8.448,0 15.648,-3.84 19.968,-9.984 v 5.856 c 0,2.112 1.536,3.648 3.744,3.648 2.112,0 3.744,-1.536 3.744,-3.648 v -22.56 c -0.096,-15.264 -11.52,-26.784 -26.688,-26.784 -15.264,0 -26.688,11.52 -26.688,26.784 0,15.264 11.136,26.688 25.92,26.688 z m 0.768,-6.72 c -11.04,0 -19.488,-8.544 -19.488,-19.968 0,-11.424 8.448,-20.064 19.488,-20.064 11.04,0 19.392,8.64 19.392,20.064 0,11.424 -8.352,19.968 -19.392,19.968 z m 44.73601,6.24 c 2.304,0 3.648,-1.344 3.648,-3.648 v -28.704 c 0,-8.928 7.872,-14.688 18.624,-13.728 3.168,0.192 5.76,0.768 6.432,-2.112 0.768,-3.168 -2.688,-4.896 -8.64,-4.896 -6.72,0 -12.672,3.168 -16.416,8.352 v -4.128 c 0,-2.304 -1.344,-3.648 -3.648,-3.648 -2.304,0 -3.648,1.344 -3.648,3.648 v 45.216 c 0,2.304 1.344,3.648 3.648,3.648 z m 49.34401,22.848 c 1.248,0 2.208,-0.768 2.784,-2.208 l 31.104,-68.352 c 0.96,-2.112 0.288,-3.648 -1.824,-4.512 -2.112,-0.864 -3.648,-0.288 -4.608,1.728 l -17.856,39.36 -18.72,-39.456 c -0.864,-1.92 -2.688,-2.496 -4.608,-1.632 -1.92,0.864 -2.688,2.688 -1.728,4.608 l 21.408,44.352 0.096,0.096 -9.6,21.024 c -0.96,2.112 -0.288,3.744 1.824,4.608 0.576,0.288 1.152,0.384 1.728,0.384 z"
|
||||
id="text18"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
aria-label="Katenary" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:#388ec7;fill-opacity:1;stroke:none;stroke-width:1.41128"
|
||||
d="m 558.98167,600.50526 -33.31908,17.65778 -13.63054,6.42952 -13.63053,-6.42952 -33.31907,-17.65778 c 0,5.32502 -2.24747,16.12101 0.77828,20.72433 1.84892,2.81293 6.84753,4.46784 9.82323,6.09432 8.26868,4.51957 16.72019,8.751 24.98932,13.27082 3.1083,1.69895 7.62988,5.16897 11.35877,5.21476 3.70778,0.0455 8.2451,-3.55102 11.35878,-5.19752 8.33909,-4.40967 16.72005,-8.76818 24.98932,-13.28806 3.05286,-1.66866 8.68533,-3.49778 10.14461,-6.69253 2.3911,-5.23485 0.45691,-14.46175 0.45691,-20.12612 m -46.19236,16.00215 c 3.59731,-0.51544 7.48607,-3.47354 10.60152,-5.12097 7.58911,-4.0131 15.20174,-8.00941 22.71755,-12.13797 3.53683,-1.94283 9.05374,-3.7013 11.65911,-6.69847 3.28982,-3.78453 1.93068,-13.45759 -2.58668,-15.74886 -4.7363,-2.40229 -8.80251,1.12086 -12.85868,3.25251 l -21.20306,11.2665 c -2.56736,1.35182 -6.01463,4.01777 -9.08702,4.01777 -3.29073,0 -7.12524,-2.97742 -9.84427,-4.47101 -6.50984,-3.57597 -13.17851,-6.90091 -19.68854,-10.47698 -2.85668,-1.56921 -7.10587,-4.85415 -10.60153,-4.52437 -7.2756,0.6864 -9.69308,11.97712 -5.60111,16.68444 2.43875,2.80547 7.56529,4.49972 10.90188,6.26408 7.83167,4.14137 15.6667,8.28474 23.4748,12.46456 3.21433,1.72068 8.18431,5.79216 12.11603,5.22877 m 30.29007,-45.33762 v -1.39693 l -18.17404,-8.90973 -12.11603,4.65427 -12.87329,-4.72164 -18.93129,8.9771 v 1.39693 l 21.20305,11.17542 9.84427,4.34169 9.84428,-4.33415 z"
|
||||
id="path18" />
|
||||
<g
|
||||
id="g19">
|
||||
<path
|
||||
style="fill:#388ec7;fill-opacity:1;stroke:none;stroke-width:1.41128"
|
||||
d="m 280.25003,221.48792 -33.31908,17.65778 -13.63054,6.42952 -13.63053,-6.42952 -33.31907,-17.65778 c 0,5.32502 -2.24747,16.12101 0.77828,20.72433 1.84892,2.81293 6.84753,4.46784 9.82323,6.09432 8.26868,4.51957 16.72019,8.751 24.98932,13.27082 3.1083,1.69895 7.62988,5.16897 11.35877,5.21476 3.70778,0.0455 8.2451,-3.55102 11.35878,-5.19752 8.33909,-4.40967 16.72005,-8.76818 24.98932,-13.28806 3.05286,-1.66866 8.68533,-3.49778 10.14461,-6.69253 2.3911,-5.23485 0.45691,-14.46175 0.45691,-20.12612 m -46.19236,16.00215 c 3.59731,-0.51544 7.48607,-3.47354 10.60152,-5.12097 7.58911,-4.0131 15.20174,-8.00941 22.71755,-12.13797 3.53683,-1.94283 9.05374,-3.7013 11.65911,-6.69847 3.28982,-3.78453 1.93068,-13.45759 -2.58668,-15.74886 -4.7363,-2.40229 -8.80251,1.12086 -12.85868,3.25251 l -21.20306,11.2665 c -2.56736,1.35182 -6.01463,4.01777 -9.08702,4.01777 -3.29073,0 -7.12524,-2.97742 -9.84427,-4.47101 -6.50984,-3.57597 -13.17851,-6.90091 -19.68854,-10.47698 -2.85668,-1.56921 -7.10587,-4.85415 -10.60153,-4.52437 -7.2756,0.6864 -9.69308,11.97712 -5.60111,16.68444 2.43875,2.80547 7.56529,4.49972 10.90188,6.26408 7.83167,4.14137 15.6667,8.28474 23.4748,12.46456 3.21433,1.72068 8.18431,5.79216 12.11603,5.22877 m 30.29007,-45.33762 v -1.39693 l -18.17404,-8.90973 -12.11603,4.65427 -12.87329,-4.72164 -18.93129,8.9771 v 1.39693 l 21.20305,11.17542 9.84427,4.34169 9.84428,-4.33415 z"
|
||||
id="path19" />
|
||||
<path
|
||||
d="m 297.75361,250.3495 c 2.112,0 3.744,-1.632 3.744,-3.744 v -24.576 l 8.544,-8.64 26.112,35.328 c 0.768,1.152 1.824,1.632 3.168,1.632 2.688,0 4.512,-3.36 2.688,-5.952 l -26.592,-36.288 26.304,-26.304 c 2.304,-2.592 1.248,-6.432 -2.208,-6.432 -1.152,0 -2.016,0.384 -2.976,1.344 l -35.04,35.04 v -32.64 c 0,-2.112 -1.632,-3.744 -3.744,-3.744 -2.016,0 -3.744,1.632 -3.744,3.744 v 67.488 c 0,2.112 1.728,3.744 3.744,3.744 z m 75.55211,0.48 c 8.448,0 15.648,-3.84 19.968,-9.984 v 5.856 c 0,2.112 1.536,3.648 3.744,3.648 2.112,0 3.744,-1.536 3.744,-3.648 v -22.56 c -0.096,-15.264 -11.52,-26.784 -26.688,-26.784 -15.264,0 -26.688,11.52 -26.688,26.784 0,15.264 11.136,26.688 25.92,26.688 z m 0.768,-6.72 c -11.04,0 -19.488,-8.544 -19.488,-19.968 0,-11.424 8.448,-20.064 19.488,-20.064 11.04,0 19.392,8.64 19.392,20.064 0,11.424 -8.352,19.968 -19.392,19.968 z m 61.63201,6.24 h 2.016 c 2.304,0 4.032,-1.536 4.032,-3.648 0,-2.208 -1.44,-3.744 -3.456,-3.744 h -2.592 c -5.952,0 -10.272,-4.608 -10.272,-11.136 v -25.632 h 10.848 c 2.016,0 3.36,-1.344 3.36,-3.168 0,-1.92 -1.344,-3.264 -3.36,-3.264 h -10.848 v -14.88 c 0,-2.208 -1.536,-3.744 -3.744,-3.744 -2.112,0 -3.648,1.536 -3.648,3.744 v 14.88 h -6.24 c -2.016,0 -3.36,1.344 -3.36,3.264 0,1.824 1.344,3.168 3.36,3.168 h 6.24 v 25.632 c 0,10.656 7.392,18.432 17.664,18.528 z m 41.85604,0.48 c 5.952,0 13.248,-2.592 17.472,-6.24 1.536,-1.344 1.44,-3.36 -0.192,-4.8 -1.344,-1.056 -3.264,-0.96 -4.704,0.192 -2.784,2.4 -7.968,4.224 -12.576,4.224 -10.656,0 -18.528,-7.2 -19.584,-17.664 h 38.784 c 2.016,0 3.456,-1.344 3.456,-3.36 0,-15.072 -9.6,-25.824 -24.096,-25.824 -14.784,0 -25.152,11.136 -25.152,26.784 0,15.648 11.04,26.688 26.592,26.688 z m -1.44,-46.848 c 9.696,0 16.224,6.72 17.184,16.416 h -35.136 c 1.344,-9.696 8.16,-16.416 17.952,-16.416 z m 40.32003,46.368 c 2.208,0 3.648,-1.44 3.648,-3.648 v -28.416 c 0,-7.968 7.2,-14.016 16.704,-14.016 9.792,0 17.184,5.664 17.184,17.472 v 24.96 c 0,2.112 1.632,3.648 3.744,3.648 2.016,0 3.648,-1.536 3.648,-3.648 v -24.96 c 0,-15.84 -10.272,-24.48 -23.808,-24.48 -7.104,0 -13.344,2.88 -17.472,7.584 v -3.264 c 0,-2.208 -1.44,-3.744 -3.648,-3.744 -2.208,0 -3.744,1.536 -3.744,3.744 v 45.12 c 0,2.208 1.536,3.648 3.744,3.648 z m 84.384,0.48 c 8.448,0 15.648,-3.84 19.968,-9.984 v 5.856 c 0,2.112 1.536,3.648 3.744,3.648 2.112,0 3.744,-1.536 3.744,-3.648 v -22.56 c -0.096,-15.264 -11.52,-26.784 -26.688,-26.784 -15.264,0 -26.688,11.52 -26.688,26.784 0,15.264 11.136,26.688 25.92,26.688 z m 0.768,-6.72 c -11.04,0 -19.488,-8.544 -19.488,-19.968 0,-11.424 8.448,-20.064 19.488,-20.064 11.04,0 19.392,8.64 19.392,20.064 0,11.424 -8.352,19.968 -19.392,19.968 z m 44.73601,6.24 c 2.304,0 3.648,-1.344 3.648,-3.648 v -28.704 c 0,-8.928 7.872,-14.688 18.624,-13.728 3.168,0.192 5.76,0.768 6.432,-2.112 0.768,-3.168 -2.688,-4.896 -8.64,-4.896 -6.72,0 -12.672,3.168 -16.416,8.352 v -4.128 c 0,-2.304 -1.344,-3.648 -3.648,-3.648 -2.304,0 -3.648,1.344 -3.648,3.648 v 45.216 c 0,2.304 1.344,3.648 3.648,3.648 z m 49.34402,22.848 c 1.248,0 2.208,-0.768 2.784,-2.208 l 31.104,-68.352 c 0.96,-2.112 0.288,-3.648 -1.824,-4.512 -2.112,-0.864 -3.648,-0.288 -4.608,1.728 l -17.856,39.36 -18.72,-39.456 c -0.864,-1.92 -2.688,-2.496 -4.608,-1.632 -1.92,0.864 -2.688,2.688 -1.728,4.608 l 21.408,44.352 0.096,0.096 -9.6,21.024 c -0.96,2.112 -0.288,3.744 1.824,4.608 0.576,0.288 1.152,0.384 1.728,0.384 z"
|
||||
id="text19"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
aria-label="Katenary" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 14 KiB |
121
doc/docs/statics/logo-dark.svg
Normal file
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg211948"
|
||||
viewBox="0 0 489.26056 97.824219"
|
||||
height="97.824219"
|
||||
width="489.26056"
|
||||
version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata211954">
|
||||
<rdf:rdf>
|
||||
<cc:work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:work>
|
||||
</rdf:rdf>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs211952" />
|
||||
<linearGradient
|
||||
spreadMethod="pad"
|
||||
y2="0.30000001"
|
||||
x2="-0.1"
|
||||
y1="1.2"
|
||||
x1="0.30000001"
|
||||
id="3d_gradient2-logo-24885591-b378-4c55-b87b-b7d42ed10694">
|
||||
<stop
|
||||
id="stop211929"
|
||||
stop-opacity="1"
|
||||
stop-color="#ffffff"
|
||||
offset="0%" />
|
||||
<stop
|
||||
id="stop211931"
|
||||
stop-opacity="1"
|
||||
stop-color="#000000"
|
||||
offset="100%" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientTransform="rotate(-30)"
|
||||
spreadMethod="pad"
|
||||
y2="0.30000001"
|
||||
x2="-0.1"
|
||||
y1="1.2"
|
||||
x1="0.30000001"
|
||||
id="3d_gradient3-logo-24885591-b378-4c55-b87b-b7d42ed10694">
|
||||
<stop
|
||||
id="stop211934"
|
||||
stop-opacity="1"
|
||||
stop-color="#ffffff"
|
||||
offset="0%" />
|
||||
<stop
|
||||
id="stop211936"
|
||||
stop-opacity="1"
|
||||
stop-color="#cccccc"
|
||||
offset="50%" />
|
||||
<stop
|
||||
id="stop211938"
|
||||
stop-opacity="1"
|
||||
stop-color="#000000"
|
||||
offset="100%" />
|
||||
</linearGradient>
|
||||
<g
|
||||
id="logo-group"
|
||||
transform="translate(-331.21094,-211.65039)">
|
||||
<path
|
||||
d="m 773.83594,234.02344 c -0.4545,0.0375 -0.92821,0.1629 -1.40821,0.3789 -1.91999,0.864 -2.68851,2.68743 -1.72851,4.60743 l 21.4082,44.35351 0.0957,0.0957 -9.5996,21.02344 c -0.96,2.112 -0.28778,3.74342 1.82421,4.60742 0.576,0.288 1.15252,0.38477 1.72852,0.38477 1.248,0 2.2072,-0.76703 2.7832,-2.20703 l 31.10352,-68.35352 c 0.96,-2.112 0.28778,-3.64772 -1.82422,-4.51172 -2.112,-0.864 -3.64742,-0.28748 -4.60742,1.72852 l -17.85547,39.35937 -18.7207,-39.45507 c -0.648,-1.44 -1.83572,-2.12422 -3.19922,-2.01172 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path9" />
|
||||
<path
|
||||
d="m 692.07617,233.63477 c -15.26398,0 -26.68945,11.51921 -26.68945,26.7832 0,15.26398 11.13594,26.68945 25.91992,26.68945 8.44799,0 15.64875,-3.84038 19.96875,-9.98437 v 5.85547 c 0,2.11199 1.53614,3.64843 3.74414,3.64843 2.112,0 3.74414,-1.53644 3.74414,-3.64843 v -22.56055 c -0.096,-15.26399 -11.51951,-26.7832 -26.6875,-26.7832 z m 0,6.7207 c 11.03999,0 19.39063,8.63851 19.39063,20.0625 0,11.42399 -8.35064,19.96875 -19.39063,19.96875 -11.03999,0 -19.48828,-8.54476 -19.48828,-19.96875 0,-11.42399 8.44829,-20.0625 19.48828,-20.0625 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path8" />
|
||||
<path
|
||||
d="m 566.60352,233.63477 c -14.78399,0 -25.15235,11.13521 -25.15235,26.7832 0,15.64798 11.03981,26.68945 26.5918,26.68945 5.95199,0 13.24866,-2.59224 17.47265,-6.24023 1.536,-1.344 1.4406,-3.36079 -0.1914,-4.80078 -1.344,-1.056 -3.26508,-0.9606 -4.70508,0.1914 -2.784,2.4 -7.96818,4.22461 -12.57617,4.22461 -10.65599,0 -18.52799,-7.20007 -19.58399,-17.66406 h 38.78516 c 2.016,0 3.45508,-1.34338 3.45508,-3.35938 0,-15.07198 -9.59972,-25.82421 -24.0957,-25.82421 z m 0,6.62304 c 9.69599,0 16.22359,6.72003 17.18359,16.41602 h -35.13477 c 1.344,-9.69599 8.15919,-16.41602 17.95118,-16.41602 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path7" />
|
||||
<path
|
||||
d="m 756.875,233.53906 c -6.71999,0 -12.67202,3.16757 -16.41602,8.35156 v -4.1289 c 0,-2.304 -1.34248,-3.64649 -3.64648,-3.64649 -2.304,0 -3.64844,1.34249 -3.64844,3.64649 v 45.2168 c 0,2.30399 1.34444,3.64843 3.64844,3.64843 2.304,0 3.64648,-1.34444 3.64648,-3.64843 v -28.70313 c 0,-8.92799 7.87302,-14.68851 18.625,-13.72852 3.168,0.192 5.75965,0.76867 6.43164,-2.11132 0.768,-3.168 -2.68863,-4.89649 -8.64062,-4.89649 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path6" />
|
||||
<path
|
||||
d="m 628.04297,233.53906 c -7.10399,0 -13.34271,2.87999 -17.4707,7.58399 v -3.26368 c 0,-2.20799 -1.44044,-3.74414 -3.64844,-3.74414 -2.208,0 -3.74414,1.53615 -3.74414,3.74414 v 45.11915 c 0,2.20799 1.53614,3.64843 3.74414,3.64843 2.208,0 3.64844,-1.44044 3.64844,-3.64843 V 254.5625 c 0,-7.96799 7.19913,-14.01563 16.70312,-14.01563 9.79199,0 17.18359,5.66467 17.18359,17.47266 v 24.95899 c 0,2.11199 1.63215,3.64843 3.74414,3.64843 2.016,0 3.64844,-1.53644 3.64844,-3.64843 v -24.95899 c 0,-15.83998 -10.2726,-24.48047 -23.80859,-24.48047 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path5" />
|
||||
<path
|
||||
d="m 512.17187,217.41016 c -2.11199,0 -3.64843,1.53614 -3.64843,3.74414 v 14.88086 h -6.24024 c -2.01599,0 -3.35937,1.34367 -3.35937,3.26367 0,1.824 1.34338,3.16797 3.35937,3.16797 h 6.24024 v 25.63281 c 0,10.65599 7.39207,18.43134 17.66406,18.52734 h 2.01562 c 2.304,0 4.03321,-1.53644 4.03321,-3.64843 0,-2.208 -1.44104,-3.74415 -3.45703,-3.74415 h -2.5918 c -5.95199,0 -10.27148,-4.60677 -10.27148,-11.13476 V 242.4668 h 10.84765 c 2.016,0 3.35938,-1.34397 3.35938,-3.16797 0,-1.92 -1.34338,-3.26367 -3.35938,-3.26367 H 515.91602 V 221.1543 c 0,-2.208 -1.53615,-3.74414 -3.74415,-3.74414 z"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path4" />
|
||||
<image
|
||||
xlink:href=""
|
||||
id="container"
|
||||
x="272"
|
||||
y="144"
|
||||
width="480"
|
||||
height="480"
|
||||
style="display:none" />
|
||||
<image
|
||||
xlink:href=""
|
||||
id="icon_container"
|
||||
style="display:none"
|
||||
x="0"
|
||||
y="0"
|
||||
width="0"
|
||||
height="0" />
|
||||
<path
|
||||
style="fill:#388ec7;fill-opacity:1;stroke:none;stroke-width:1.41128"
|
||||
d="m 488.71354,251.36014 -33.31908,17.65778 -13.63054,6.42952 -13.63053,-6.42952 -33.31907,-17.65778 c 0,5.32502 -2.24747,16.12101 0.77828,20.72433 1.84892,2.81293 6.84753,4.46784 9.82323,6.09432 8.26868,4.51957 16.72019,8.751 24.98932,13.27082 3.1083,1.69895 7.62988,5.16897 11.35877,5.21476 3.70778,0.0455 8.2451,-3.55102 11.35878,-5.19752 8.33909,-4.40967 16.72005,-8.76818 24.98932,-13.28806 3.05286,-1.66866 8.68533,-3.49778 10.14461,-6.69253 2.3911,-5.23485 0.45691,-14.46175 0.45691,-20.12612 m -46.19236,16.00215 c 3.59731,-0.51544 7.48607,-3.47354 10.60152,-5.12097 7.58911,-4.0131 15.20174,-8.00941 22.71755,-12.13797 3.53683,-1.94283 9.05374,-3.7013 11.65911,-6.69847 3.28982,-3.78453 1.93068,-13.45759 -2.58668,-15.74886 -4.7363,-2.40229 -8.80251,1.12086 -12.85868,3.25251 l -21.20306,11.2665 c -2.56736,1.35182 -6.01463,4.01777 -9.08702,4.01777 -3.29073,0 -7.12524,-2.97742 -9.84427,-4.47101 -6.50984,-3.57597 -13.17851,-6.90091 -19.68854,-10.47698 -2.85668,-1.56921 -7.10587,-4.85415 -10.60153,-4.52437 -7.2756,0.6864 -9.69308,11.97712 -5.60111,16.68444 2.43875,2.80547 7.56529,4.49972 10.90188,6.26408 7.83167,4.14137 15.6667,8.28474 23.4748,12.46456 3.21433,1.72068 8.18431,5.79216 12.11603,5.22877 m 30.29007,-45.33762 v -1.39693 l -18.17404,-8.90973 -12.11603,4.65427 -12.87329,-4.72164 -18.93129,8.9771 v 1.39693 l 21.20305,11.17542 9.84427,4.34169 9.84428,-4.33415 z"
|
||||
id="path1" />
|
||||
<path
|
||||
d="m 334.95508,211.65039 c -2.016,0 -3.74414,1.63214 -3.74414,3.74414 v 67.48828 c 0,2.112 1.72814,3.74414 3.74414,3.74414 2.112,0 3.74414,-1.63214 3.74414,-3.74414 v -24.57617 l 8.54492,-8.64062 26.11133,35.32812 c 0.768,1.152 1.82397,1.63281 3.16797,1.63281 2.68799,0 4.5115,-3.36112 2.6875,-5.95312 l -26.5918,-36.28711 26.30469,-26.30469 c 2.304,-2.592 1.24701,-6.43164 -2.20899,-6.43164 -1.152,0 -2.01461,0.38375 -2.97461,1.34375 l -35.04101,35.04102 v -32.64063 c 0,-2.112 -1.63214,-3.74414 -3.74414,-3.74414 z"
|
||||
style="fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
id="path3" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.7 KiB |
BIN
doc/docs/statics/logo-vertical.png
Normal file
After Width: | Height: | Size: 14 KiB |
118
doc/docs/statics/logo-vertical.svg
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg211948"
|
||||
viewBox="0 0 435.98074 184.9017"
|
||||
height="184.9017"
|
||||
width="435.98074"
|
||||
version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata211954">
|
||||
<rdf:rdf>
|
||||
<cc:work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:work>
|
||||
</rdf:rdf>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs211952" />
|
||||
<linearGradient
|
||||
spreadMethod="pad"
|
||||
y2="0.30000001"
|
||||
x2="-0.1"
|
||||
y1="1.2"
|
||||
x1="0.30000001"
|
||||
id="3d_gradient2-logo-24885591-b378-4c55-b87b-b7d42ed10694">
|
||||
<stop
|
||||
id="stop211929"
|
||||
stop-opacity="1"
|
||||
stop-color="#ffffff"
|
||||
offset="0%" />
|
||||
<stop
|
||||
id="stop211931"
|
||||
stop-opacity="1"
|
||||
stop-color="#000000"
|
||||
offset="100%" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientTransform="rotate(-30)"
|
||||
spreadMethod="pad"
|
||||
y2="0.30000001"
|
||||
x2="-0.1"
|
||||
y1="1.2"
|
||||
x1="0.30000001"
|
||||
id="3d_gradient3-logo-24885591-b378-4c55-b87b-b7d42ed10694">
|
||||
<stop
|
||||
id="stop211934"
|
||||
stop-opacity="1"
|
||||
stop-color="#ffffff"
|
||||
offset="0%" />
|
||||
<stop
|
||||
id="stop211936"
|
||||
stop-opacity="1"
|
||||
stop-color="#cccccc"
|
||||
offset="50%" />
|
||||
<stop
|
||||
id="stop211938"
|
||||
stop-opacity="1"
|
||||
stop-color="#000000"
|
||||
offset="100%" />
|
||||
</linearGradient>
|
||||
<g
|
||||
id="logo-group"
|
||||
transform="translate(-294.00961,-314.5575)">
|
||||
<image
|
||||
xlink:href=""
|
||||
id="container"
|
||||
x="272"
|
||||
y="144"
|
||||
width="480"
|
||||
height="480"
|
||||
style="display:none" />
|
||||
<image
|
||||
xlink:href=""
|
||||
id="icon_container"
|
||||
style="display:none"
|
||||
x="0"
|
||||
y="0"
|
||||
width="0"
|
||||
height="0" />
|
||||
<g
|
||||
id="g18"
|
||||
transform="translate(-91.179677,-177.97015)">
|
||||
<path
|
||||
style="fill:#388ec7;fill-opacity:1;stroke:none;stroke-width:1.41128"
|
||||
d="m 650.16134,532.23713 -33.31908,17.65778 -13.63054,6.42952 -13.63053,-6.42952 -33.31907,-17.65778 c 0,5.32502 -2.24747,16.12101 0.77828,20.72433 1.84892,2.81293 6.84753,4.46784 9.82323,6.09432 8.26868,4.51957 16.72019,8.751 24.98932,13.27082 3.1083,1.69895 7.62988,5.16897 11.35877,5.21476 3.70778,0.0455 8.2451,-3.55102 11.35878,-5.19752 8.33909,-4.40967 16.72005,-8.76818 24.98932,-13.28806 3.05286,-1.66866 8.68533,-3.49778 10.14461,-6.69253 2.3911,-5.23485 0.45691,-14.46175 0.45691,-20.12612 m -46.19236,16.00215 c 3.59731,-0.51544 7.48607,-3.47354 10.60152,-5.12097 7.58911,-4.0131 15.20174,-8.00941 22.71755,-12.13797 3.53683,-1.94283 9.05374,-3.7013 11.65911,-6.69847 3.28982,-3.78453 1.93068,-13.45759 -2.58668,-15.74886 -4.7363,-2.40229 -8.80251,1.12086 -12.85868,3.25251 l -21.20306,11.2665 c -2.56736,1.35182 -6.01463,4.01777 -9.08702,4.01777 -3.29073,0 -7.12524,-2.97742 -9.84427,-4.47101 -6.50984,-3.57597 -13.17851,-6.90091 -19.68854,-10.47698 -2.85668,-1.56921 -7.10587,-4.85415 -10.60153,-4.52437 -7.2756,0.6864 -9.69308,11.97712 -5.60111,16.68444 2.43875,2.80547 7.56529,4.49972 10.90188,6.26408 7.83167,4.14137 15.6667,8.28474 23.4748,12.46456 3.21433,1.72068 8.18431,5.79216 12.11603,5.22877 m 30.29007,-45.33762 v -1.39693 l -18.17404,-8.90973 -12.11603,4.65427 -12.87329,-4.72164 -18.93129,8.9771 v 1.39693 l 21.20305,11.17542 9.84427,4.34169 9.84428,-4.33415 z"
|
||||
id="path17" />
|
||||
<path
|
||||
d="m 388.93329,654.58136 c 2.112,0 3.744,-1.632 3.744,-3.744 v -24.576 l 8.544,-8.64 26.112,35.328 c 0.768,1.152 1.824,1.632 3.168,1.632 2.688,0 4.512,-3.36 2.688,-5.952 l -26.592,-36.288 26.304,-26.304 c 2.304,-2.592 1.248,-6.432 -2.208,-6.432 -1.152,0 -2.016,0.384 -2.976,1.344 l -35.04,35.04 v -32.64 c 0,-2.112 -1.632,-3.744 -3.744,-3.744 -2.016,0 -3.744,1.632 -3.744,3.744 v 67.488 c 0,2.112 1.728,3.744 3.744,3.744 z m 75.55212,0.48 c 8.448,0 15.648,-3.84 19.968,-9.984 v 5.856 c 0,2.112 1.536,3.648 3.744,3.648 2.112,0 3.744,-1.536 3.744,-3.648 v -22.56 c -0.096,-15.264 -11.52,-26.784 -26.688,-26.784 -15.264,0 -26.688,11.52 -26.688,26.784 0,15.264 11.136,26.688 25.92,26.688 z m 0.768,-6.72 c -11.04,0 -19.488,-8.544 -19.488,-19.968 0,-11.424 8.448,-20.064 19.488,-20.064 11.04,0 19.392,8.64 19.392,20.064 0,11.424 -8.352,19.968 -19.392,19.968 z m 61.63201,6.24 h 2.016 c 2.304,0 4.032,-1.536 4.032,-3.648 0,-2.208 -1.44,-3.744 -3.456,-3.744 h -2.592 c -5.952,0 -10.272,-4.608 -10.272,-11.136 v -25.632 h 10.848 c 2.016,0 3.36,-1.344 3.36,-3.168 0,-1.92 -1.344,-3.264 -3.36,-3.264 h -10.848 v -14.88 c 0,-2.208 -1.536,-3.744 -3.744,-3.744 -2.112,0 -3.648,1.536 -3.648,3.744 v 14.88 h -6.24 c -2.016,0 -3.36,1.344 -3.36,3.264 0,1.824 1.344,3.168 3.36,3.168 h 6.24 v 25.632 c 0,10.656 7.392,18.432 17.664,18.528 z m 41.85604,0.48 c 5.952,0 13.248,-2.592 17.472,-6.24 1.536,-1.344 1.44,-3.36 -0.192,-4.8 -1.344,-1.056 -3.264,-0.96 -4.704,0.192 -2.784,2.4 -7.968,4.224 -12.576,4.224 -10.656,0 -18.528,-7.2 -19.584,-17.664 h 38.784 c 2.016,0 3.456,-1.344 3.456,-3.36 0,-15.072 -9.6,-25.824 -24.096,-25.824 -14.784,0 -25.152,11.136 -25.152,26.784 0,15.648 11.04,26.688 26.592,26.688 z m -1.44,-46.848 c 9.696,0 16.224,6.72 17.184,16.416 h -35.136 c 1.344,-9.696 8.16,-16.416 17.952,-16.416 z m 40.32003,46.368 c 2.208,0 3.648,-1.44 3.648,-3.648 v -28.416 c 0,-7.968 7.2,-14.016 16.704,-14.016 9.792,0 17.184,5.664 17.184,17.472 v 24.96 c 0,2.112 1.632,3.648 3.744,3.648 2.016,0 3.648,-1.536 3.648,-3.648 v -24.96 c 0,-15.84 -10.272,-24.48 -23.808,-24.48 -7.104,0 -13.344,2.88 -17.472,7.584 v -3.264 c 0,-2.208 -1.44,-3.744 -3.648,-3.744 -2.208,0 -3.744,1.536 -3.744,3.744 v 45.12 c 0,2.208 1.536,3.648 3.744,3.648 z m 84.384,0.48 c 8.448,0 15.648,-3.84 19.968,-9.984 v 5.856 c 0,2.112 1.536,3.648 3.744,3.648 2.112,0 3.744,-1.536 3.744,-3.648 v -22.56 c -0.096,-15.264 -11.52,-26.784 -26.688,-26.784 -15.264,0 -26.688,11.52 -26.688,26.784 0,15.264 11.136,26.688 25.92,26.688 z m 0.768,-6.72 c -11.04,0 -19.488,-8.544 -19.488,-19.968 0,-11.424 8.448,-20.064 19.488,-20.064 11.04,0 19.392,8.64 19.392,20.064 0,11.424 -8.352,19.968 -19.392,19.968 z m 44.73601,6.24 c 2.304,0 3.648,-1.344 3.648,-3.648 v -28.704 c 0,-8.928 7.872,-14.688 18.624,-13.728 3.168,0.192 5.76,0.768 6.432,-2.112 0.768,-3.168 -2.688,-4.896 -8.64,-4.896 -6.72,0 -12.672,3.168 -16.416,8.352 v -4.128 c 0,-2.304 -1.344,-3.648 -3.648,-3.648 -2.304,0 -3.648,1.344 -3.648,3.648 v 45.216 c 0,2.304 1.344,3.648 3.648,3.648 z m 49.34401,22.848 c 1.248,0 2.208,-0.768 2.784,-2.208 l 31.104,-68.352 c 0.96,-2.112 0.288,-3.648 -1.824,-4.512 -2.112,-0.864 -3.648,-0.288 -4.608,1.728 l -17.856,39.36 -18.72,-39.456 c -0.864,-1.92 -2.688,-2.496 -4.608,-1.632 -1.92,0.864 -2.688,2.688 -1.728,4.608 l 21.408,44.352 0.096,0.096 -9.6,21.024 c -0.96,2.112 -0.288,3.744 1.824,4.608 0.576,0.288 1.152,0.384 1.728,0.384 z"
|
||||
id="text18"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
aria-label="Katenary" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:#388ec7;fill-opacity:1;stroke:none;stroke-width:1.41128"
|
||||
d="m 558.98167,600.50526 -33.31908,17.65778 -13.63054,6.42952 -13.63053,-6.42952 -33.31907,-17.65778 c 0,5.32502 -2.24747,16.12101 0.77828,20.72433 1.84892,2.81293 6.84753,4.46784 9.82323,6.09432 8.26868,4.51957 16.72019,8.751 24.98932,13.27082 3.1083,1.69895 7.62988,5.16897 11.35877,5.21476 3.70778,0.0455 8.2451,-3.55102 11.35878,-5.19752 8.33909,-4.40967 16.72005,-8.76818 24.98932,-13.28806 3.05286,-1.66866 8.68533,-3.49778 10.14461,-6.69253 2.3911,-5.23485 0.45691,-14.46175 0.45691,-20.12612 m -46.19236,16.00215 c 3.59731,-0.51544 7.48607,-3.47354 10.60152,-5.12097 7.58911,-4.0131 15.20174,-8.00941 22.71755,-12.13797 3.53683,-1.94283 9.05374,-3.7013 11.65911,-6.69847 3.28982,-3.78453 1.93068,-13.45759 -2.58668,-15.74886 -4.7363,-2.40229 -8.80251,1.12086 -12.85868,3.25251 l -21.20306,11.2665 c -2.56736,1.35182 -6.01463,4.01777 -9.08702,4.01777 -3.29073,0 -7.12524,-2.97742 -9.84427,-4.47101 -6.50984,-3.57597 -13.17851,-6.90091 -19.68854,-10.47698 -2.85668,-1.56921 -7.10587,-4.85415 -10.60153,-4.52437 -7.2756,0.6864 -9.69308,11.97712 -5.60111,16.68444 2.43875,2.80547 7.56529,4.49972 10.90188,6.26408 7.83167,4.14137 15.6667,8.28474 23.4748,12.46456 3.21433,1.72068 8.18431,5.79216 12.11603,5.22877 m 30.29007,-45.33762 v -1.39693 l -18.17404,-8.90973 -12.11603,4.65427 -12.87329,-4.72164 -18.93129,8.9771 v 1.39693 l 21.20305,11.17542 9.84427,4.34169 9.84428,-4.33415 z"
|
||||
id="path18" />
|
||||
<g
|
||||
id="g19">
|
||||
<path
|
||||
style="fill:#388ec7;fill-opacity:1;stroke:none;stroke-width:1.41128"
|
||||
d="m 280.25003,221.48792 -33.31908,17.65778 -13.63054,6.42952 -13.63053,-6.42952 -33.31907,-17.65778 c 0,5.32502 -2.24747,16.12101 0.77828,20.72433 1.84892,2.81293 6.84753,4.46784 9.82323,6.09432 8.26868,4.51957 16.72019,8.751 24.98932,13.27082 3.1083,1.69895 7.62988,5.16897 11.35877,5.21476 3.70778,0.0455 8.2451,-3.55102 11.35878,-5.19752 8.33909,-4.40967 16.72005,-8.76818 24.98932,-13.28806 3.05286,-1.66866 8.68533,-3.49778 10.14461,-6.69253 2.3911,-5.23485 0.45691,-14.46175 0.45691,-20.12612 m -46.19236,16.00215 c 3.59731,-0.51544 7.48607,-3.47354 10.60152,-5.12097 7.58911,-4.0131 15.20174,-8.00941 22.71755,-12.13797 3.53683,-1.94283 9.05374,-3.7013 11.65911,-6.69847 3.28982,-3.78453 1.93068,-13.45759 -2.58668,-15.74886 -4.7363,-2.40229 -8.80251,1.12086 -12.85868,3.25251 l -21.20306,11.2665 c -2.56736,1.35182 -6.01463,4.01777 -9.08702,4.01777 -3.29073,0 -7.12524,-2.97742 -9.84427,-4.47101 -6.50984,-3.57597 -13.17851,-6.90091 -19.68854,-10.47698 -2.85668,-1.56921 -7.10587,-4.85415 -10.60153,-4.52437 -7.2756,0.6864 -9.69308,11.97712 -5.60111,16.68444 2.43875,2.80547 7.56529,4.49972 10.90188,6.26408 7.83167,4.14137 15.6667,8.28474 23.4748,12.46456 3.21433,1.72068 8.18431,5.79216 12.11603,5.22877 m 30.29007,-45.33762 v -1.39693 l -18.17404,-8.90973 -12.11603,4.65427 -12.87329,-4.72164 -18.93129,8.9771 v 1.39693 l 21.20305,11.17542 9.84427,4.34169 9.84428,-4.33415 z"
|
||||
id="path19" />
|
||||
<path
|
||||
d="m 297.75361,250.3495 c 2.112,0 3.744,-1.632 3.744,-3.744 v -24.576 l 8.544,-8.64 26.112,35.328 c 0.768,1.152 1.824,1.632 3.168,1.632 2.688,0 4.512,-3.36 2.688,-5.952 l -26.592,-36.288 26.304,-26.304 c 2.304,-2.592 1.248,-6.432 -2.208,-6.432 -1.152,0 -2.016,0.384 -2.976,1.344 l -35.04,35.04 v -32.64 c 0,-2.112 -1.632,-3.744 -3.744,-3.744 -2.016,0 -3.744,1.632 -3.744,3.744 v 67.488 c 0,2.112 1.728,3.744 3.744,3.744 z m 75.55211,0.48 c 8.448,0 15.648,-3.84 19.968,-9.984 v 5.856 c 0,2.112 1.536,3.648 3.744,3.648 2.112,0 3.744,-1.536 3.744,-3.648 v -22.56 c -0.096,-15.264 -11.52,-26.784 -26.688,-26.784 -15.264,0 -26.688,11.52 -26.688,26.784 0,15.264 11.136,26.688 25.92,26.688 z m 0.768,-6.72 c -11.04,0 -19.488,-8.544 -19.488,-19.968 0,-11.424 8.448,-20.064 19.488,-20.064 11.04,0 19.392,8.64 19.392,20.064 0,11.424 -8.352,19.968 -19.392,19.968 z m 61.63201,6.24 h 2.016 c 2.304,0 4.032,-1.536 4.032,-3.648 0,-2.208 -1.44,-3.744 -3.456,-3.744 h -2.592 c -5.952,0 -10.272,-4.608 -10.272,-11.136 v -25.632 h 10.848 c 2.016,0 3.36,-1.344 3.36,-3.168 0,-1.92 -1.344,-3.264 -3.36,-3.264 h -10.848 v -14.88 c 0,-2.208 -1.536,-3.744 -3.744,-3.744 -2.112,0 -3.648,1.536 -3.648,3.744 v 14.88 h -6.24 c -2.016,0 -3.36,1.344 -3.36,3.264 0,1.824 1.344,3.168 3.36,3.168 h 6.24 v 25.632 c 0,10.656 7.392,18.432 17.664,18.528 z m 41.85604,0.48 c 5.952,0 13.248,-2.592 17.472,-6.24 1.536,-1.344 1.44,-3.36 -0.192,-4.8 -1.344,-1.056 -3.264,-0.96 -4.704,0.192 -2.784,2.4 -7.968,4.224 -12.576,4.224 -10.656,0 -18.528,-7.2 -19.584,-17.664 h 38.784 c 2.016,0 3.456,-1.344 3.456,-3.36 0,-15.072 -9.6,-25.824 -24.096,-25.824 -14.784,0 -25.152,11.136 -25.152,26.784 0,15.648 11.04,26.688 26.592,26.688 z m -1.44,-46.848 c 9.696,0 16.224,6.72 17.184,16.416 h -35.136 c 1.344,-9.696 8.16,-16.416 17.952,-16.416 z m 40.32003,46.368 c 2.208,0 3.648,-1.44 3.648,-3.648 v -28.416 c 0,-7.968 7.2,-14.016 16.704,-14.016 9.792,0 17.184,5.664 17.184,17.472 v 24.96 c 0,2.112 1.632,3.648 3.744,3.648 2.016,0 3.648,-1.536 3.648,-3.648 v -24.96 c 0,-15.84 -10.272,-24.48 -23.808,-24.48 -7.104,0 -13.344,2.88 -17.472,7.584 v -3.264 c 0,-2.208 -1.44,-3.744 -3.648,-3.744 -2.208,0 -3.744,1.536 -3.744,3.744 v 45.12 c 0,2.208 1.536,3.648 3.744,3.648 z m 84.384,0.48 c 8.448,0 15.648,-3.84 19.968,-9.984 v 5.856 c 0,2.112 1.536,3.648 3.744,3.648 2.112,0 3.744,-1.536 3.744,-3.648 v -22.56 c -0.096,-15.264 -11.52,-26.784 -26.688,-26.784 -15.264,0 -26.688,11.52 -26.688,26.784 0,15.264 11.136,26.688 25.92,26.688 z m 0.768,-6.72 c -11.04,0 -19.488,-8.544 -19.488,-19.968 0,-11.424 8.448,-20.064 19.488,-20.064 11.04,0 19.392,8.64 19.392,20.064 0,11.424 -8.352,19.968 -19.392,19.968 z m 44.73601,6.24 c 2.304,0 3.648,-1.344 3.648,-3.648 v -28.704 c 0,-8.928 7.872,-14.688 18.624,-13.728 3.168,0.192 5.76,0.768 6.432,-2.112 0.768,-3.168 -2.688,-4.896 -8.64,-4.896 -6.72,0 -12.672,3.168 -16.416,8.352 v -4.128 c 0,-2.304 -1.344,-3.648 -3.648,-3.648 -2.304,0 -3.648,1.344 -3.648,3.648 v 45.216 c 0,2.304 1.344,3.648 3.648,3.648 z m 49.34402,22.848 c 1.248,0 2.208,-0.768 2.784,-2.208 l 31.104,-68.352 c 0.96,-2.112 0.288,-3.648 -1.824,-4.512 -2.112,-0.864 -3.648,-0.288 -4.608,1.728 l -17.856,39.36 -18.72,-39.456 c -0.864,-1.92 -2.688,-2.496 -4.608,-1.632 -1.92,0.864 -2.688,2.688 -1.728,4.608 l 21.408,44.352 0.096,0.096 -9.6,21.024 c -0.96,2.112 -0.288,3.744 1.824,4.608 0.576,0.288 1.152,0.384 1.728,0.384 z"
|
||||
id="text19"
|
||||
style="font-size:96px;line-height:0;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;fill:#ff7f2a;stroke-width:51.0236;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0"
|
||||
aria-label="Katenary" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 14 KiB |
145
doc/docs/statics/logo.svg
Normal file
After Width: | Height: | Size: 21 KiB |
117
doc/docs/statics/main.css
Normal file
@@ -0,0 +1,117 @@
|
||||
:root {
|
||||
--code-bg-color: var(--lt-color-gray-800);
|
||||
--code-fg-color: var(--lt-color-gray-300);
|
||||
--md-primary-fg-color: var(--lt-color-gray-900);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="default"] {
|
||||
--md-primary-fg-color: var(--md-code-fg-color);
|
||||
}
|
||||
|
||||
div.smile-logo {
|
||||
display: flex;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
div.smile-logo img {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
button.md-clipboard::after {
|
||||
transition: all 0.5s ease;
|
||||
color: var(--lt-color-gray-500);
|
||||
}
|
||||
button.md-clipboard:hover::after {
|
||||
color: var(--md-primary-bg-color);
|
||||
}
|
||||
|
||||
article a,
|
||||
article a:visited {
|
||||
color: var(--md-code-hl-number-color) !important;
|
||||
}
|
||||
|
||||
.md-center {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.md-center img {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.md-nav__item .md-nav__link--active {
|
||||
color: var(--md-code-hl-string-color) !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.go-logo {
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
/* HLJS */
|
||||
pre code.hljs {
|
||||
background-color: var(--code-bg-color);
|
||||
color: var(--code-fg-color);
|
||||
}
|
||||
|
||||
table tbody code {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
font-size: 1em !important;
|
||||
background-color: transparent !important;
|
||||
color: var(--md-code-hl-special-color) !important;
|
||||
}
|
||||
|
||||
h3[id*="katenaryio"] {
|
||||
color: var(--md-code-hl-special-color);
|
||||
}
|
||||
|
||||
#logo {
|
||||
background-image: url("logo-vertical.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
height: 8em;
|
||||
width: 100%;
|
||||
margin: 0 auto 2rem auto;
|
||||
}
|
||||
|
||||
/*Zoomable images*/
|
||||
.zoomable svg {
|
||||
background-color: var(--md-default-bg-color);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .zoomable svg {
|
||||
background-color: var(--md-default-bg-color);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .zoomable svg .colorize {
|
||||
fill: var(--md-typeset-color) !important;
|
||||
}
|
||||
|
||||
.zoomable input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (min-width: 1399px) {
|
||||
.zoomable label > * {
|
||||
cursor: zoom-in;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.zoomable input[type="checkbox"]:checked ~ label > * {
|
||||
transform: scale(2);
|
||||
cursor: zoom-out;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#klee {
|
||||
filter: drop-shadow(0 0 16px var(--md-default-fg-color));
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
#klee svg {
|
||||
zoom: 2;
|
||||
}
|
BIN
doc/docs/statics/workflow.png
Normal file
After Width: | Height: | Size: 124 KiB |
538
doc/docs/statics/workflow.svg
Normal file
After Width: | Height: | Size: 310 KiB |
262
doc/docs/usage.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# Basic Usage
|
||||
|
||||
Basically, you can use `katenary` to transpose a docker-compose file (or any compose file compatible with
|
||||
`podman-compose` and `docker-compose`) to a configurable Helm Chart. This resulting helm chart can be installed with
|
||||
`helm` command to your Kubernetes cluster.
|
||||
|
||||
For very basic compose files, without any specific configuration, Katenary will create a working helm chart using the
|
||||
simple command line:
|
||||
|
||||
```bash
|
||||
katenary convert
|
||||
```
|
||||
|
||||
This will create a `chart` directory with the helm chart inside.
|
||||
|
||||
But, in general, you will need to add a few configurations to help Katenary to transpose the compose file to a working
|
||||
helm chart.
|
||||
|
||||
There are two ways to configure Katenary:
|
||||
|
||||
- Using the compose files, adding labels to the services
|
||||
- Using a specific file named `katenary.yaml`
|
||||
|
||||
The Katenary file `katenary.yaml` has benefits over the labels in the compose file:
|
||||
|
||||
- you can validate the configuration with a schema, and use completion in your editor
|
||||
- you separate the configuration and leave the compose file "intact"
|
||||
- the syntax is a bit simpler, instead of using `katenary.v3/xxx: |-` you can use `xxx: ...`
|
||||
|
||||
But: **this implies that you have to maintain two files if the compose file changes.**
|
||||
|
||||
For example. With "labels", you should do:
|
||||
|
||||
```yaml
|
||||
# in compose file
|
||||
services:
|
||||
webapp:
|
||||
image: php:7-apache
|
||||
ports:
|
||||
- 8080:80
|
||||
environment:
|
||||
DB_HOST: database
|
||||
labels:
|
||||
katenary.v3/ingress: |-
|
||||
hostname: myapp.example.com
|
||||
port: 8080
|
||||
katenary.v3/map-env: |-
|
||||
DB_HOST: "{{ .Release.Name }}-database"
|
||||
```
|
||||
|
||||
Using a Katenary file, you can do:
|
||||
|
||||
```yaml
|
||||
# in compose file, no need to add labels
|
||||
services:
|
||||
webapp:
|
||||
image: php:7-apache
|
||||
ports:
|
||||
- 8080:80
|
||||
environment:
|
||||
DB_HOST: database
|
||||
|
||||
# in katenary.yaml
|
||||
webapp:
|
||||
ingress:
|
||||
hostname: myapp.example.com
|
||||
port: 8080
|
||||
|
||||
map-env:
|
||||
DB_HOST: "{{ .Release.Name }}-database"
|
||||
```
|
||||
|
||||
!!! Warning "YAML in multiline label"
|
||||
|
||||
Compose only accept text label. So, to put a complete YAML content in the target label,
|
||||
you need to use a pipe char (`|` or `|-`) and to **indent** your content.
|
||||
|
||||
For example :
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
# your labels
|
||||
foo: bar
|
||||
# katenary labels with multiline
|
||||
katenary.v3/ingress: |-
|
||||
hostname: my.website.tld
|
||||
port: 80
|
||||
katenary.v3/ports: |-
|
||||
- 1234
|
||||
```
|
||||
|
||||
Katenary transforms compose services this way:
|
||||
|
||||
- Takes the service and create a "Deployment" file
|
||||
- if a port is declared, Katenary creates a service (`ClusterIP`)
|
||||
- if a port is exposed, Katenary creates a service (`NodePort`)
|
||||
- environment variables will be stored inside a `ConfigMap`
|
||||
- image, tags, and ingresses configuration are also stored in `values.yaml` file
|
||||
- if named volumes are declared, Katenary create `PersistentVolumeClaims` - not enabled in values file
|
||||
- `depends_on` needs that the pointed service declared a port. If not, you can use labels to inform Katenary
|
||||
|
||||
For any other specific configuration, like binding local files as `ConfigMap`, bind variables, add values with
|
||||
documentation, etc. You'll need to use labels.
|
||||
|
||||
Katenary can also configure containers grouping in pods, declare dependencies, ignore some services, force variables as
|
||||
secrets, mount files as `configMap`, and many others things. To adapt the helm chart generation, you will need to use
|
||||
some specific labels.
|
||||
|
||||
For more complete label usage, see [the labels page](labels.md).
|
||||
|
||||
!!! Info "Overriding file"
|
||||
|
||||
It could be sometimes more convinient to separate the
|
||||
configuration related to Katenary inside a secondary file.
|
||||
|
||||
Instead of adding labels inside the `compose.yaml` file,
|
||||
you can create a file named `compose.katenary.yaml` and
|
||||
declare your labels inside. Katenary will detect it by
|
||||
default.
|
||||
|
||||
**No need to precise the file in the command line.**
|
||||
|
||||
## Make conversion
|
||||
|
||||
After having installed `katenary`, the standard usage is to call:
|
||||
|
||||
katenary convert
|
||||
|
||||
It will search standard compose files in the current directory and try to create a helm chart in "chart" directory.
|
||||
|
||||
!!! Info
|
||||
Katenary uses the compose-go library which respects the Docker and Docker-Compose specification. Keep in mind that
|
||||
it will find files exactly the same way as `docker-compose` and `podman-compose` do it.
|
||||
|
||||
Of course, you can provide others files than the default with (cumulative) `-c` options:
|
||||
|
||||
katenary convert -c file1.yaml -c file2.yaml
|
||||
|
||||
## Some common labels to use
|
||||
|
||||
Katenary proposes a lot of labels to configure the helm chart generation, but some are very important.
|
||||
|
||||
!!! Info
|
||||
For more complete label usage, see [the labels page](labels.md).
|
||||
|
||||
### Work with Depends On?
|
||||
|
||||
Kubernetes does not provide service or pod starting detection from others pods. But Katenary will create `initContainer`
|
||||
to make you able to wait for a service to respond. But you'll probably need to adapt a bit the compose file.
|
||||
|
||||
See this compose file:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
webapp:
|
||||
image: php:8-apache
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
database:
|
||||
image: mariadb
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: foobar
|
||||
```
|
||||
|
||||
In this case, `webapp` needs to know the `database` port because the `depends_on` points on it and Kubernetes has not
|
||||
(yet) solution to check the database startup. Katenary wants to create a `initContainer` to hit on the related service.
|
||||
So, instead of exposing the port in the compose definition, let's declare this to Katenary with labels:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
webapp:
|
||||
image: php:8-apache
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
database:
|
||||
image: mariadb
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: foobar
|
||||
labels:
|
||||
katenary.v3/ports: |-
|
||||
- 3306
|
||||
```
|
||||
|
||||
### Declare ingresses
|
||||
|
||||
It's very common to have an Ingress resource on web application to deploy on Kubernetes. It allows exposing the
|
||||
service to the outside of the cluster (you need to install an ingress controller).
|
||||
|
||||
Katenary can create this resource for you. You just need to declare the hostname and the port to bind.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
webapp:
|
||||
image: ...
|
||||
ports: 8080:5050
|
||||
labels:
|
||||
katenary.v3/ingress: |-
|
||||
# the target port is 5050 wich is the "service" port
|
||||
port: 5050
|
||||
hostname: myapp.example.com
|
||||
```
|
||||
|
||||
Note that the port to bind is the one used by the container, not the used locally. This is because Katenary create a
|
||||
service to bind the container itself.
|
||||
|
||||
### Map environment to helm values
|
||||
|
||||
A lot of framework needs to receive service host or IP in an environment variable to configure the connection. For
|
||||
example, to connect a PHP application to a database.
|
||||
|
||||
With a compose file, there is no problem as Docker/Podman allows resolving the name by container name:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
webapp:
|
||||
image: php:7-apache
|
||||
environment:
|
||||
DB_HOST: database
|
||||
|
||||
database:
|
||||
image: mariadb
|
||||
```
|
||||
|
||||
Katenary prefixes the services with `{{ .Release.Name }}` (to make it possible to install the application several times
|
||||
in a namespace), so you need to "remap" the environment variable to the right one.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
webapp:
|
||||
image: php:7-apache
|
||||
environment:
|
||||
DB_HOST: database
|
||||
labels:
|
||||
katenary.v3/mapenv: |-
|
||||
DB_HOST: "{{ .Release.Name }}-database"
|
||||
|
||||
database:
|
||||
image: mariadb
|
||||
```
|
||||
|
||||
This label can be used to map others environment for any others reason. E.g. to change an informational environment
|
||||
variable.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
webapp:
|
||||
#...
|
||||
environment:
|
||||
RUNNING: docker
|
||||
labels:
|
||||
katenary.v3/mapenv: |-
|
||||
RUNNING: kubernetes
|
||||
```
|
||||
|
||||
In the above example, `RUNNING` will be set to `kubernetes` when you'll deploy the application with helm, and it's
|
||||
`docker` for "Podman" and "Docker" executions.
|
48
doc/fix.py
Normal file
@@ -0,0 +1,48 @@
|
||||
""" Fix the markdown files to replace code blocs by lists when the code blocs are lists."""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from typing import Tuple
|
||||
|
||||
# get markdown bloc code
|
||||
re_code = re.compile(r"```(.*?)```", re.DOTALL)
|
||||
|
||||
|
||||
def fix(text: str) -> Tuple[str, bool]:
|
||||
"""Fix the markdown text to replace code blocs by lists when the code blocs are lists."""
|
||||
# in the text, get the code blocs
|
||||
code_blocs = re_code.findall(text)
|
||||
# for each code bloc, if lines begin by a "-", this is a list. So,
|
||||
# make it a mkdocs list and remove the block code
|
||||
fixed = False
|
||||
for code in code_blocs:
|
||||
lines = code.split("\n")
|
||||
lines = [line.strip() for line in lines if line.strip()]
|
||||
if all(line.startswith("-") for line in lines):
|
||||
fixed = True
|
||||
# make a mkdocs list
|
||||
lines = [f"- {line[1:]}" for line in lines]
|
||||
# replace the code bloc by the list
|
||||
text = text.replace(f"```{code}```", "\n".join(lines))
|
||||
return text, fixed
|
||||
|
||||
|
||||
def main(filename: str):
|
||||
"""Fix and rewrite the markdown file."""
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
content, fixed = fix(text)
|
||||
|
||||
if not fixed:
|
||||
return
|
||||
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python fix.py <file>")
|
||||
sys.exit(1)
|
||||
|
||||
main(sys.argv[1])
|
74
doc/mkdocs.yml
Normal file
@@ -0,0 +1,74 @@
|
||||
site_name: Katenary documentation
|
||||
docs_dir: ./docs
|
||||
plugins:
|
||||
- search
|
||||
- inline-svg
|
||||
- manpage:
|
||||
enabled: !ENV [MANPAGE, false]
|
||||
preprocess: preprocess.py
|
||||
pages:
|
||||
- title: Katenary
|
||||
header: Katenary helm chart generator
|
||||
output: share/man/man1/katenary.1
|
||||
inputs:
|
||||
- usage.md
|
||||
- labels.md
|
||||
theme:
|
||||
name: material
|
||||
custom_dir: overrides
|
||||
logo: statics/logo-bright.svg
|
||||
favicon: statics/icon.svg
|
||||
palette:
|
||||
- scheme: slate
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
|
||||
- scheme: default
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- footnotes
|
||||
- attr_list
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
use_pygments: false
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
extra_css:
|
||||
- statics/main.css
|
||||
extra_javascript:
|
||||
- statics/addons.js
|
||||
copyright: Copyright © 2021 - 2024 - Katenary authors
|
||||
extra:
|
||||
generator: false
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/katenary/katenary
|
||||
nav:
|
||||
- "Home": index.md
|
||||
- usage.md
|
||||
- labels.md
|
||||
- Behind the scene:
|
||||
- coding.md
|
||||
- dependencies.md
|
||||
- FAQ: faq.md
|
||||
- Go Packages:
|
||||
- packages/cmd/katenary.md
|
||||
- packages/parser.md
|
||||
- packages/utils.md
|
||||
- Generator:
|
||||
- Index: packages/generator.md
|
||||
- ExtraFiles: packages/generator/extrafiles.md
|
||||
- labels:
|
||||
- packages/generator/labels.md
|
||||
- Labelstructs: packages/generator/labels/labelstructs.md
|
||||
- KatenaryFile: packages/generator/katenaryfile.md
|
56
doc/overrides/partials/footer.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{#- This file was automatically generated - do not edit -#}
|
||||
<footer class="md-footer">
|
||||
{% if page.previous_page or page.next_page %} {% if page.meta and
|
||||
page.meta.hide %} {% set hidden = "hidden" if "footer" in page.meta.hide %} {%
|
||||
endif %}
|
||||
<nav
|
||||
class="md-footer__inner md-grid"
|
||||
aria-label="{{ lang.t('footer.title') }}"
|
||||
{{
|
||||
hidden
|
||||
}}
|
||||
>
|
||||
{% if page.previous_page %} {% set direction = lang.t("footer.previous") %}
|
||||
<a
|
||||
href="{{ page.previous_page.url | url }}"
|
||||
class="md-footer__link md-footer__link--prev"
|
||||
aria-label="{{ direction }}: {{ page.previous_page.title | e }}"
|
||||
rel="prev"
|
||||
>
|
||||
<div class="md-footer__button md-icon">
|
||||
{% include ".icons/material/arrow-left.svg" %}
|
||||
</div>
|
||||
<div class="md-footer__title">
|
||||
<div class="md-ellipsis">
|
||||
<span class="md-footer__direction"> {{ direction }} </span>
|
||||
{{ page.previous_page.title }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %} {% if page.next_page %} {% set direction = lang.t("footer.next")
|
||||
%}
|
||||
<a
|
||||
href="{{ page.next_page.url | url }}"
|
||||
class="md-footer__link md-footer__link--next"
|
||||
aria-label="{{ direction }}: {{ page.next_page.title | e }}"
|
||||
rel="next"
|
||||
>
|
||||
<div class="md-footer__title">
|
||||
<div class="md-ellipsis">
|
||||
<span class="md-footer__direction"> {{ direction }} </span>
|
||||
{{ page.next_page.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-footer__button md-icon">
|
||||
{% include ".icons/material/arrow-right.svg" %}
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
<div class="md-footer-meta md-typeset">
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
{% include "partials/copyright.html" %} {% if config.extra.social %} {%
|
||||
include "partials/social.html" %} {% endif %}
|
||||
</div>
|
||||
</footer>
|
22
doc/preprocess.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Called by mkdocs to preprocess files and build manpages"""
|
||||
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
|
||||
|
||||
def to_remove(tag: Tag) -> bool:
|
||||
"""Removes images, SVGs, links containing images or SVGs, and permalinks from the BeautifulSoup object."""
|
||||
if tag.name in {"img", "svg"}:
|
||||
return True
|
||||
# remove links containing images or SVGs
|
||||
if tag.name == "a" and tag.img and to_remove(tag.img):
|
||||
return True
|
||||
# remove permalinks
|
||||
if tag.name == "a" and "headerlink" in tag.get("class", ()):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def preprocess(soup: BeautifulSoup, output: str) -> None:
|
||||
"""Preprocess the BeautifulSoup object to remove unwanted elements."""
|
||||
for element in soup.find_all(to_remove):
|
||||
element.decompose()
|
9
doc/requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
mkdocs==1.*
|
||||
Jinja2==3.*
|
||||
MarkupSafe==3.*
|
||||
pymdown-extensions==10.*
|
||||
mkdocs-material==9.*
|
||||
mkdocs-material-extensions==1.*
|
||||
mkdocs-plugin-inline-svg-mod
|
||||
beautifulsoup4==4.*
|
||||
mkdocs-manpage[preprocess]
|
@@ -1,10 +0,0 @@
|
||||
# Basic example
|
||||
|
||||
This is a basic example of what can do Katenary with standard docker-compose file.
|
||||
|
||||
In this example:
|
||||
|
||||
- `depends_on` yield a `initContainer` in the webapp ddeployment to wait for database
|
||||
- so we need to declare the listened port inside `database` container as we don't use it with docker-compose- also, we needed to declare that `DB_HOST` is actually a service name
|
||||
|
||||
Take a look on [chart/basic](chart/basic) directory to see what `katenary convert` command has generated.
|
@@ -1,8 +0,0 @@
|
||||
# Create on 2022-02-17T10:27:30+01:00
|
||||
# Katenary command line: katenary convert
|
||||
apiVersion: v2
|
||||
appVersion: 0.0.1
|
||||
description: A helm chart for basic
|
||||
name: basic
|
||||
type: application
|
||||
version: 0.1.0
|
@@ -1,8 +0,0 @@
|
||||
|
||||
Congratulations,
|
||||
|
||||
Your application is now deployed. This may take a while to be up and responding.
|
||||
|
||||
{{ if .Values.webapp.ingress.enabled -}}
|
||||
- webapp is accessible on : http://{{ .Values.webapp.ingress.host }}
|
||||
{{- end }}
|
@@ -1,39 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-database'
|
||||
labels:
|
||||
katenary.io/component: database
|
||||
katenary.io/project: basic
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||
katenary.io/version: master-3619cc4
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
katenary.io/component: database
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
katenary.io/component: database
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
spec:
|
||||
containers:
|
||||
- name: database
|
||||
image: '{{ .Values.database.image }}'
|
||||
ports:
|
||||
- name: database
|
||||
containerPort: 3306
|
||||
env:
|
||||
- name: MARIADB_PASSWORD
|
||||
value: foo
|
||||
- name: MARIADB_DATABASE
|
||||
value: myapp
|
||||
- name: MARIADB_ROOT_PASSWORD
|
||||
value: foobar
|
||||
- name: MARIADB_USER
|
||||
value: foo
|
||||
|
@@ -1,19 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-database'
|
||||
labels:
|
||||
katenary.io/component: database
|
||||
katenary.io/project: basic
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||
katenary.io/version: master-3619cc4
|
||||
spec:
|
||||
selector:
|
||||
katenary.io/component: database
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 3306
|
||||
targetPort: 3306
|
@@ -1,48 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-webapp'
|
||||
labels:
|
||||
katenary.io/component: webapp
|
||||
katenary.io/project: basic
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||
katenary.io/version: master-3619cc4
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
katenary.io/component: webapp
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
katenary.io/component: webapp
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
spec:
|
||||
initContainers:
|
||||
- name: check-database
|
||||
image: busybox
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |-
|
||||
OK=0
|
||||
echo "Checking database port"
|
||||
while [ $OK != 1 ]; do
|
||||
echo -n "."
|
||||
nc -z {{ .Release.Name }}-database 3306 2>&1 >/dev/null && OK=1 || sleep 1
|
||||
done
|
||||
echo
|
||||
echo "Done"
|
||||
containers:
|
||||
- name: webapp
|
||||
image: '{{ .Values.webapp.image }}'
|
||||
ports:
|
||||
- name: webapp
|
||||
containerPort: 80
|
||||
env:
|
||||
- name: DB_HOST
|
||||
value: '{{ .Release.Name }}-database'
|
||||
|
@@ -1,34 +0,0 @@
|
||||
{{- if .Values.webapp.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-webapp'
|
||||
labels:
|
||||
katenary.io/component: webapp
|
||||
katenary.io/project: basic
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||
katenary.io/version: master-3619cc4
|
||||
spec:
|
||||
{{- if and .Values.webapp.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: '{{ .Values.webapp.ingress.class }}'
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: '{{ .Values.webapp.ingress.host }}'
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: '{{ .Release.Name }}-webapp'
|
||||
port:
|
||||
number: 80
|
||||
{{- else }}
|
||||
serviceName: '{{ .Release.Name }}-webapp'
|
||||
servicePort: 80
|
||||
{{- end }}
|
||||
|
||||
{{- end -}}
|
@@ -1,19 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-webapp'
|
||||
labels:
|
||||
katenary.io/component: webapp
|
||||
katenary.io/project: basic
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
|
||||
katenary.io/version: master-3619cc4
|
||||
spec:
|
||||
selector:
|
||||
katenary.io/component: webapp
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
@@ -1,8 +0,0 @@
|
||||
database:
|
||||
image: mariadb:10
|
||||
webapp:
|
||||
image: php:7-apache
|
||||
ingress:
|
||||
class: nginx
|
||||
enabled: false
|
||||
host: webapp.basic.tld
|
@@ -1,30 +0,0 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
webapp:
|
||||
image: php:7-apache
|
||||
|
||||
environment:
|
||||
DB_HOST: database
|
||||
ports:
|
||||
- "8080:80"
|
||||
labels:
|
||||
# expose an ingress
|
||||
katenary.io/ingress: 80
|
||||
# DB_HOST is actually a service name
|
||||
katenary.io/env-to-service: DB_HOST
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
database:
|
||||
image: mariadb:10
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: foobar
|
||||
MARIADB_USER: foo
|
||||
MARIADB_PASSWORD: foo
|
||||
MARIADB_DATABASE: myapp
|
||||
labels:
|
||||
# because we don't provide "ports" or "expose", alert katenary
|
||||
# to use the mysql port for service declaration
|
||||
katenary.io/ports: 3306
|
@@ -1,13 +0,0 @@
|
||||
# Make it possible to bind several containers in one pod
|
||||
|
||||
In this example, we need to make nginx and php-fpm to run inside the same "pod". The reason is that we configured FPM to listen an unix socket instead of the 9000 port.
|
||||
|
||||
Because NGinx will need to connect to the unix socket wich is a file, both containers should share the same node and work together.
|
||||
|
||||
So, in the docker-compose file, we need to declare:
|
||||
- `katenary.io/empty-dirs: socket` where `socket` is the "volume name", this will avoid the creation of a PVC
|
||||
- `katenary.io/same-pod: http` in `php` container to declare that this will be added in the `containers` section of the `http` deployment
|
||||
|
||||
You can note that we also use `configmap-volumes` to declare our configuration as `configMap`.
|
||||
|
||||
Take a look on [chart/same-pod](chart/same-pod) directory to see the result of the `katenary convert` command.
|
@@ -1,8 +0,0 @@
|
||||
# Create on 2022-02-17T11:36:02+01:00
|
||||
# Katenary command line: katenary convert --force
|
||||
apiVersion: v2
|
||||
appVersion: 0.0.1
|
||||
description: A helm chart for same-pod
|
||||
name: same-pod
|
||||
type: application
|
||||
version: 0.1.0
|
@@ -1,8 +0,0 @@
|
||||
|
||||
Congratulations,
|
||||
|
||||
Your application is now deployed. This may take a while to be up and responding.
|
||||
|
||||
{{ if .Values.http.ingress.enabled -}}
|
||||
- http is accessible on : http://{{ .Values.http.ingress.host }}
|
||||
{{- end }}
|
@@ -1,23 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-config-nginx-http'
|
||||
labels:
|
||||
katenary.io/component: ""
|
||||
katenary.io/project: same-pod
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||
katenary.io/version: master-bf44d44
|
||||
data:
|
||||
default.conf: |
|
||||
upstream _php {
|
||||
server unix:/sock/fpm.sock;
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
location ~ ^/index\.php(/|$) {
|
||||
fastcgi_pass _php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-config-php-php'
|
||||
labels:
|
||||
katenary.io/component: ""
|
||||
katenary.io/project: same-pod
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||
katenary.io/version: master-bf44d44
|
||||
data:
|
||||
www.conf: |
|
||||
[www]
|
||||
user = www-data
|
||||
group = www-data
|
||||
|
||||
listen = /sock/fpm.sock
|
||||
|
||||
pm = dynamic
|
||||
pm.max_children = 5
|
||||
pm.start_servers = 2
|
||||
pm.min_spare_servers = 1
|
||||
pm.max_spare_servers = 3
|
||||
|
||||
access.log = /proc/self/fd/2
|
||||
log_limit = 8192
|
||||
clear_env = no
|
||||
catch_workers_output = yes
|
||||
decorate_workers_output = no
|
@@ -1,52 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-http'
|
||||
labels:
|
||||
katenary.io/component: http
|
||||
katenary.io/project: same-pod
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||
katenary.io/version: master-bf44d44
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
katenary.io/component: http
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
katenary.io/component: http
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
spec:
|
||||
containers:
|
||||
- name: http
|
||||
image: '{{ .Values.http.image }}'
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
volumeMounts:
|
||||
- mountPath: /sock
|
||||
name: sock
|
||||
- mountPath: /etc/nginx/conf.d
|
||||
name: config-nginx
|
||||
- name: php
|
||||
image: '{{ .Values.php.image }}'
|
||||
volumeMounts:
|
||||
- mountPath: /sock
|
||||
name: sock
|
||||
- mountPath: /usr/local/etc/php-fpm.d/www.conf
|
||||
name: config-php
|
||||
subPath: www.conf
|
||||
volumes:
|
||||
- emptyDir: {}
|
||||
name: sock
|
||||
- configMap:
|
||||
name: '{{ .Release.Name }}-config-nginx-http'
|
||||
name: config-nginx
|
||||
- configMap:
|
||||
name: '{{ .Release.Name }}-config-php-php'
|
||||
name: config-php
|
||||
|
@@ -1,34 +0,0 @@
|
||||
{{- if .Values.http.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-http'
|
||||
labels:
|
||||
katenary.io/component: http
|
||||
katenary.io/project: same-pod
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||
katenary.io/version: master-bf44d44
|
||||
spec:
|
||||
{{- if and .Values.http.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: '{{ .Values.http.ingress.class }}'
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: '{{ .Values.http.ingress.host }}'
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: '{{ .Release.Name }}-http'
|
||||
port:
|
||||
number: 80
|
||||
{{- else }}
|
||||
serviceName: '{{ .Release.Name }}-http'
|
||||
servicePort: 80
|
||||
{{- end }}
|
||||
|
||||
{{- end -}}
|
@@ -1,19 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-http'
|
||||
labels:
|
||||
katenary.io/component: http
|
||||
katenary.io/project: same-pod
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
annotations:
|
||||
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
|
||||
katenary.io/version: master-bf44d44
|
||||
spec:
|
||||
selector:
|
||||
katenary.io/component: http
|
||||
katenary.io/release: '{{ .Release.Name }}'
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
@@ -1,8 +0,0 @@
|
||||
http:
|
||||
image: nginx:alpine
|
||||
ingress:
|
||||
class: nginx
|
||||
enabled: false
|
||||
host: http.same-pod.tld
|
||||
php:
|
||||
image: php:fpm
|
@@ -1,10 +0,0 @@
|
||||
upstream _php {
|
||||
server unix:/sock/fpm.sock;
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
location ~ ^/index\.php(/|$) {
|
||||
fastcgi_pass _php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
[www]
|
||||
user = www-data
|
||||
group = www-data
|
||||
|
||||
listen = /sock/fpm.sock
|
||||
|
||||
pm = dynamic
|
||||
pm.max_children = 5
|
||||
pm.start_servers = 2
|
||||
pm.min_spare_servers = 1
|
||||
pm.max_spare_servers = 3
|
||||
|
||||
access.log = /proc/self/fd/2
|
||||
log_limit = 8192
|
||||
clear_env = no
|
||||
catch_workers_output = yes
|
||||
decorate_workers_output = no
|
@@ -1,38 +0,0 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
http:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- "sock:/sock"
|
||||
- "./config/nginx:/etc/nginx/conf.d:z"
|
||||
|
||||
labels:
|
||||
# the "sock" volume will need to be shared to the same pod, so let's
|
||||
# declare that this is not a PVC
|
||||
katenary.io/empty-dirs: sock
|
||||
|
||||
# use ./config/nginx as a configMap
|
||||
katenary.io/configmap-volumes: ./config/nginx
|
||||
|
||||
# declare an ingress
|
||||
katenary.io/ingress: 80
|
||||
|
||||
php:
|
||||
image: php:fpm
|
||||
volumes:
|
||||
- "sock:/sock"
|
||||
- "./config/php/www.conf:/usr/local/etc/php-fpm.d/www.conf:z"
|
||||
labels:
|
||||
# fpm will need to use a unix socket shared
|
||||
# with nginx (http service above), so we want here
|
||||
# make a single pod containing nginx and php
|
||||
katenary.io/same-pod: http
|
||||
# use the ./config/php files as a configMap
|
||||
katenary.io/configmap-volumes: ./config/php/www.conf
|
||||
|
||||
volumes:
|
||||
sock:
|
426
generator/chart.go
Normal file
@@ -0,0 +1,426 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
"katenary/generator/labels/labelstructs"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// ChartTemplate is a template of a chart. It contains the content of the template and the name of the service.
|
||||
// This is used internally to generate the templates.
|
||||
type ChartTemplate struct {
|
||||
Servicename string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
// ConvertOptions are the options to convert a compose project to a helm chart.
|
||||
type ConvertOptions struct {
|
||||
AppVersion *string
|
||||
OutputDir string
|
||||
ChartVersion string
|
||||
Icon string
|
||||
Profiles []string
|
||||
EnvFiles []string
|
||||
Force bool
|
||||
HelmUpdate bool
|
||||
}
|
||||
|
||||
// HelmChart is a Helm Chart representation. It contains all the
|
||||
// templates, values, versions, helpers...
|
||||
type HelmChart struct {
|
||||
Templates map[string]*ChartTemplate `yaml:"-"`
|
||||
Values map[string]any `yaml:"-"`
|
||||
VolumeMounts map[string]any `yaml:"-"`
|
||||
composeHash *string `yaml:"-"`
|
||||
Name string `yaml:"name"`
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
APIVersion string `yaml:"apiVersion"`
|
||||
Version string `yaml:"version"`
|
||||
AppVersion string `yaml:"appVersion"`
|
||||
Description string `yaml:"description"`
|
||||
Helper string `yaml:"-"`
|
||||
Dependencies []labelstructs.Dependency `yaml:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
// NewChart creates a new empty chart with the given name.
|
||||
func NewChart(name string) *HelmChart {
|
||||
return &HelmChart{
|
||||
Name: name,
|
||||
Templates: make(map[string]*ChartTemplate, 0),
|
||||
Description: "A Helm chart for " + name,
|
||||
APIVersion: "v2",
|
||||
Version: "",
|
||||
AppVersion: "", // set to 0.1.0 by default if no "main-app" label is found
|
||||
Values: map[string]any{
|
||||
"pullSecrets": []string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SaveTemplates the templates of the chart to the given directory.
|
||||
func (chart *HelmChart) SaveTemplates(templateDir string) {
|
||||
for name, template := range chart.Templates {
|
||||
t := template.Content
|
||||
t = removeNewlinesInsideBrackets(t)
|
||||
t = removeUnwantedLines(t)
|
||||
// t = addModeline(t)
|
||||
|
||||
kind := utils.GetKind(name)
|
||||
var icon utils.Icon
|
||||
switch kind {
|
||||
case "deployment":
|
||||
icon = utils.IconPackage
|
||||
case "service":
|
||||
icon = utils.IconPlug
|
||||
case "ingress":
|
||||
icon = utils.IconWorld
|
||||
case "volumeclaim":
|
||||
icon = utils.IconCabinet
|
||||
case "configmap":
|
||||
icon = utils.IconConfig
|
||||
case "secret":
|
||||
icon = utils.IconSecret
|
||||
default:
|
||||
icon = utils.IconInfo
|
||||
}
|
||||
|
||||
servicename := template.Servicename
|
||||
if err := os.MkdirAll(filepath.Join(templateDir, servicename), utils.DirectoryPermission); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(icon, "Creating", kind, servicename)
|
||||
// if the name is a path, create the directory
|
||||
if strings.Contains(name, string(filepath.Separator)) {
|
||||
name = filepath.Join(templateDir, name)
|
||||
err := os.MkdirAll(filepath.Dir(name), utils.DirectoryPermission)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
// remove the serivce name from the template name
|
||||
name = strings.Replace(name, servicename+".", "", 1)
|
||||
name = filepath.Join(templateDir, servicename, name)
|
||||
}
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.Write(t); err != nil {
|
||||
log.Fatal("error writing template file:", err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// generateConfigMapsAndSecrets creates the configmaps and secrets from the environment variables.
|
||||
func (chart *HelmChart) generateConfigMapsAndSecrets(project *types.Project) error {
|
||||
appName := chart.Name
|
||||
for _, s := range project.Services {
|
||||
if len(s.Environment) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
originalEnv := types.MappingWithEquals{}
|
||||
secretsVar := types.MappingWithEquals{}
|
||||
|
||||
// copy env to originalEnv
|
||||
maps.Copy(originalEnv, s.Environment)
|
||||
|
||||
if v, ok := s.Labels[labels.LabelSecrets]; ok {
|
||||
list, err := labelstructs.SecretsFrom(v)
|
||||
if err != nil {
|
||||
log.Fatal("error unmarshaling secrets label:", err)
|
||||
}
|
||||
for _, secret := range list {
|
||||
if secret == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := s.Environment[secret]; !ok {
|
||||
fmt.Printf("%s secret %s not found in environment", utils.IconWarning, secret)
|
||||
continue
|
||||
}
|
||||
secretsVar[secret] = s.Environment[secret]
|
||||
}
|
||||
}
|
||||
|
||||
if len(secretsVar) > 0 {
|
||||
s.Environment = secretsVar
|
||||
sec := NewSecret(s, appName)
|
||||
y, _ := sec.Yaml()
|
||||
name := sec.service.Name
|
||||
chart.Templates[name+".secret.yaml"] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: s.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// remove secrets from env
|
||||
s.Environment = originalEnv // back to original
|
||||
for k := range secretsVar {
|
||||
delete(s.Environment, k)
|
||||
}
|
||||
if len(s.Environment) > 0 {
|
||||
cm := NewConfigMap(s, appName, false)
|
||||
y, _ := cm.Yaml()
|
||||
name := cm.service.Name
|
||||
chart.Templates[name+".configmap.yaml"] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: s.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (chart *HelmChart) generateDeployment(service types.ServiceConfig, deployments map[string]*Deployment, services map[string]*Service, podToMerge map[string]*types.ServiceConfig, appName string) error {
|
||||
// check the "ports" label from container and add it to the service
|
||||
if err := fixPorts(&service); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// isgnored service
|
||||
if isIgnored(service) {
|
||||
fmt.Printf("%s Ignoring service %s\n", utils.IconInfo, service.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// helm dependency
|
||||
if isHelmDependency, err := chart.setDependencies(service); err != nil {
|
||||
return err
|
||||
} else if isHelmDependency {
|
||||
return nil
|
||||
}
|
||||
|
||||
// create all deployments
|
||||
d := NewDeployment(service, chart)
|
||||
deployments[service.Name] = d
|
||||
|
||||
// generate the cronjob if needed
|
||||
chart.setCronJob(service, appName)
|
||||
|
||||
if exchange, ok := service.Labels[labels.LabelExchangeVolume]; ok {
|
||||
// we need to add a volume and a mount point
|
||||
ex, err := labelstructs.NewExchangeVolumes(exchange)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, exchangeVolume := range ex {
|
||||
d.AddLegacyVolume("exchange-"+exchangeVolume.Name, exchangeVolume.Type)
|
||||
d.exchangesVolumes[service.Name] = exchangeVolume
|
||||
}
|
||||
}
|
||||
|
||||
// get the same-pod label if exists, add it to the list.
|
||||
// We later will copy some parts to the target deployment and remove this one.
|
||||
if samePod, ok := service.Labels[labels.LabelSamePod]; ok && samePod != "" {
|
||||
podToMerge[samePod] = &service
|
||||
}
|
||||
|
||||
// create the needed service for the container port
|
||||
if len(service.Ports) > 0 {
|
||||
s := NewService(service, appName)
|
||||
services[service.Name] = s
|
||||
}
|
||||
|
||||
// create all ingresses
|
||||
if ingress := d.AddIngress(service, appName); ingress != nil {
|
||||
y, _ := ingress.Yaml()
|
||||
chart.Templates[ingress.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setChartVersion sets the chart version from the service image tag.
|
||||
func (chart *HelmChart) setChartVersion(service types.ServiceConfig) {
|
||||
if chart.Version == "" {
|
||||
image := service.Image
|
||||
parts := strings.Split(image, ":")
|
||||
if len(parts) > 1 {
|
||||
chart.AppVersion = parts[1]
|
||||
} else {
|
||||
chart.AppVersion = "0.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setCronJob creates a cronjob from the service labels.
|
||||
func (chart *HelmChart) setCronJob(service types.ServiceConfig, appName string) *CronJob {
|
||||
if _, ok := service.Labels[labels.LabelCronJob]; !ok {
|
||||
return nil
|
||||
}
|
||||
cronjob, rbac := NewCronJob(service, chart, appName)
|
||||
y, _ := cronjob.Yaml()
|
||||
chart.Templates[cronjob.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
|
||||
if rbac != nil {
|
||||
y, _ := rbac.RoleBinding.Yaml()
|
||||
chart.Templates[rbac.RoleBinding.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
y, _ = rbac.Role.Yaml()
|
||||
chart.Templates[rbac.Role.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
y, _ = rbac.ServiceAccount.Yaml()
|
||||
chart.Templates[rbac.ServiceAccount.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return cronjob
|
||||
}
|
||||
|
||||
// setDependencies sets the dependencies from the service labels.
|
||||
func (chart *HelmChart) setDependencies(service types.ServiceConfig) (bool, error) {
|
||||
// helm dependency
|
||||
if v, ok := service.Labels[labels.LabelDependencies]; ok {
|
||||
d, err := labelstructs.DependenciesFrom(v)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, dep := range d {
|
||||
fmt.Printf("%s Adding dependency to %s\n", utils.IconDependency, dep.Name)
|
||||
chart.Dependencies = append(chart.Dependencies, dep)
|
||||
name := dep.Name
|
||||
if dep.Alias != "" {
|
||||
name = dep.Alias
|
||||
}
|
||||
// add the dependency env vars to the values.yaml
|
||||
chart.Values[name] = dep.Values
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// setSharedConf sets the shared configmap to the service.
|
||||
func (chart *HelmChart) setSharedConf(service types.ServiceConfig, deployments map[string]*Deployment) {
|
||||
// if the service has the "shared-conf" label, we need to add the configmap
|
||||
// to the chart and add the env vars to the service
|
||||
if _, ok := service.Labels[labels.LabelEnvFrom]; !ok {
|
||||
return
|
||||
}
|
||||
fromservices, err := labelstructs.EnvFromFrom(service.Labels[labels.LabelEnvFrom])
|
||||
if err != nil {
|
||||
log.Fatal("error unmarshaling env-from label:", err)
|
||||
}
|
||||
// find the configmap in the chart templates
|
||||
for _, fromservice := range fromservices {
|
||||
if _, ok := chart.Templates[fromservice+".configmap.yaml"]; !ok {
|
||||
log.Printf("configmap %s not found in chart templates", fromservice)
|
||||
continue
|
||||
}
|
||||
// find the corresponding target deployment
|
||||
target := findDeployment(service.Name, deployments)
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
// add the configmap to the service
|
||||
addConfigMapToService(service.Name, fromservice, chart.Name, target)
|
||||
}
|
||||
}
|
||||
|
||||
// setEnvironmentValuesFrom sets the environment values from another service.
|
||||
func (chart *HelmChart) setEnvironmentValuesFrom(service types.ServiceConfig, deployments map[string]*Deployment) {
|
||||
if _, ok := service.Labels[labels.LabelValueFrom]; !ok {
|
||||
return
|
||||
}
|
||||
mapping, err := labelstructs.GetValueFrom(service.Labels[labels.LabelValueFrom])
|
||||
if err != nil {
|
||||
log.Fatal("error unmarshaling values-from label:", err)
|
||||
}
|
||||
|
||||
findDeployment := func(name string) *Deployment {
|
||||
for _, dep := range deployments {
|
||||
if dep.service.Name == name {
|
||||
return dep
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// each mapping key is the environment, and the value is serivename.variable name
|
||||
for env, from := range *mapping {
|
||||
// find the deployment that has the variable
|
||||
depName := strings.Split(from, ".")
|
||||
dep := findDeployment(depName[0])
|
||||
target := findDeployment(service.Name)
|
||||
if dep == nil || target == nil {
|
||||
log.Fatalf("deployment %s or %s not found", depName[0], service.Name)
|
||||
}
|
||||
container, index := utils.GetContainerByName(target.service.ContainerName, target.Spec.Template.Spec.Containers)
|
||||
if container == nil {
|
||||
log.Fatalf("Container %s not found", target.GetName())
|
||||
}
|
||||
reourceName := fmt.Sprintf(`{{ include "%s.fullname" . }}-%s`, chart.Name, depName[0])
|
||||
// add environment with from
|
||||
|
||||
// is it a secret?
|
||||
isSecret := false
|
||||
secrets, err := labelstructs.SecretsFrom(dep.service.Labels[labels.LabelSecrets])
|
||||
if err == nil {
|
||||
if slices.Contains(secrets, depName[1]) {
|
||||
isSecret = true
|
||||
}
|
||||
}
|
||||
|
||||
if !isSecret {
|
||||
container.Env = append(container.Env, corev1.EnvVar{
|
||||
Name: env,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: reourceName,
|
||||
},
|
||||
Key: depName[1],
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
container.Env = append(container.Env, corev1.EnvVar{
|
||||
Name: env,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: reourceName,
|
||||
},
|
||||
Key: depName[1],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
// the environment is bound, so we shouldn't add it to the values.yaml or in any other place
|
||||
delete(service.Environment, env)
|
||||
// also, remove the values
|
||||
target.boundEnvVar = append(target.boundEnvVar, env)
|
||||
// and save the container
|
||||
target.Spec.Template.Spec.Containers[index] = *container
|
||||
|
||||
}
|
||||
}
|
157
generator/chart_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestValuesFrom(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
aa:
|
||||
image: nginx:latest
|
||||
environment:
|
||||
AA_USER: foo
|
||||
bb:
|
||||
image: nginx:latest
|
||||
labels:
|
||||
%[1]s/values-from: |-
|
||||
BB_USER: aa.USER
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/aa/configmap.yaml")
|
||||
configMap := v1.ConfigMap{}
|
||||
if err := yaml.Unmarshal([]byte(output), &configMap); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
data := configMap.Data
|
||||
if v, ok := data["AA_USER"]; !ok || v != "foo" {
|
||||
t.Errorf("Expected AA_USER to be foo, got %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValuesFromCopy(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
aa:
|
||||
image: nginx:latest
|
||||
environment:
|
||||
AA_USER: foo
|
||||
bb:
|
||||
image: nginx:latest
|
||||
labels:
|
||||
%[1]s/values-from: |-
|
||||
BB_USER: aa.AA_USER
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/bb/deployment.yaml")
|
||||
dep := appsv1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dep); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
containers := dep.Spec.Template.Spec.Containers
|
||||
environment := containers[0].Env[0]
|
||||
|
||||
envFrom := environment.ValueFrom.ConfigMapKeyRef
|
||||
if envFrom.Key != "AA_USER" {
|
||||
t.Errorf("Expected AA_USER, got %s", envFrom.Key)
|
||||
}
|
||||
if !strings.Contains(envFrom.Name, "aa") {
|
||||
t.Errorf("Expected aa, got %s", envFrom.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValuesFromSecret(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
aa:
|
||||
image: nginx:latest
|
||||
environment:
|
||||
AA_USER: foo
|
||||
labels:
|
||||
%[1]s/secrets: |-
|
||||
- AA_USER
|
||||
bb:
|
||||
image: nginx:latest
|
||||
labels:
|
||||
%[1]s/values-from: |-
|
||||
BB_USER: aa.AA_USER
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/bb/deployment.yaml")
|
||||
dep := appsv1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dep); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
containers := dep.Spec.Template.Spec.Containers
|
||||
environment := containers[0].Env[0]
|
||||
|
||||
envFrom := environment.ValueFrom.SecretKeyRef
|
||||
if envFrom.Key != "AA_USER" {
|
||||
t.Errorf("Expected AA_USER, got %s", envFrom.Key)
|
||||
}
|
||||
if !strings.Contains(envFrom.Name, "aa") {
|
||||
t.Errorf("Expected aa, got %s", envFrom.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvFrom(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
environment:
|
||||
Foo: bar
|
||||
BAZ: qux
|
||||
db:
|
||||
image: postgres
|
||||
labels:
|
||||
%[1]s/env-from: |-
|
||||
- web
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/db/deployment.yaml")
|
||||
dep := appsv1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dep); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
envFrom := dep.Spec.Template.Spec.Containers[0].EnvFrom
|
||||
if len(envFrom) != 1 {
|
||||
t.Fatalf("Expected 1 envFrom, got %d", len(envFrom))
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
package generator
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestColor(t *testing.T) {
|
||||
Red("Red text")
|
||||
Grey("Grey text")
|
||||
}
|
254
generator/configMap.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
"katenary/generator/labels/labelstructs"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// FileMapUsage is the usage of the filemap.
|
||||
type FileMapUsage uint8
|
||||
|
||||
// FileMapUsage constants.
|
||||
const (
|
||||
FileMapUsageConfigMap FileMapUsage = iota // pure configmap for key:values.
|
||||
FileMapUsageFiles // files in a configmap.
|
||||
)
|
||||
|
||||
// only used to check interface implementation
|
||||
var (
|
||||
_ DataMap = (*ConfigMap)(nil)
|
||||
_ Yaml = (*ConfigMap)(nil)
|
||||
)
|
||||
|
||||
// ConfigMap is a kubernetes ConfigMap.
|
||||
// Implements the DataMap interface.
|
||||
type ConfigMap struct {
|
||||
*corev1.ConfigMap
|
||||
service *types.ServiceConfig
|
||||
path string
|
||||
usage FileMapUsage
|
||||
}
|
||||
|
||||
// NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name.
|
||||
// The ConfigMap is filled by environment variables and labels "map-env".
|
||||
func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap {
|
||||
done := map[string]bool{}
|
||||
drop := map[string]bool{}
|
||||
labelValues := []string{}
|
||||
|
||||
cm := &ConfigMap{
|
||||
service: &service,
|
||||
ConfigMap: &corev1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Data: make(map[string]string),
|
||||
},
|
||||
}
|
||||
|
||||
// get the secrets from the labels
|
||||
secrets, err := labelstructs.SecretsFrom(service.Labels[labels.LabelSecrets])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// drop the secrets from the environment
|
||||
for _, secret := range secrets {
|
||||
drop[secret] = true
|
||||
}
|
||||
// get the label values from the labels
|
||||
varDescriptons := utils.GetValuesFromLabel(service, labels.LabelValues)
|
||||
for value := range varDescriptons {
|
||||
labelValues = append(labelValues, value)
|
||||
}
|
||||
|
||||
// change the environment variables to the values defined in the values.yaml
|
||||
for _, value := range labelValues {
|
||||
if _, ok := service.Environment[value]; !ok {
|
||||
done[value] = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !forFile {
|
||||
// do not bind env variables to the configmap
|
||||
// remove the variables that are already defined in the environment
|
||||
if l, ok := service.Labels[labels.LabelMapEnv]; ok {
|
||||
envmap, err := labelstructs.MapEnvFrom(l)
|
||||
if err != nil {
|
||||
log.Fatal("Error parsing map-env", err)
|
||||
}
|
||||
for key, value := range envmap {
|
||||
cm.AddData(key, strings.ReplaceAll(value, "__APP__", appName))
|
||||
done[key] = true
|
||||
}
|
||||
}
|
||||
for key, env := range service.Environment {
|
||||
_, isDropped := drop[key]
|
||||
_, isDone := done[key]
|
||||
if isDropped || isDone {
|
||||
continue
|
||||
}
|
||||
cm.AddData(key, *env)
|
||||
}
|
||||
}
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// NewConfigMapFromDirectory creates a new ConfigMap from a compose service. This path is the path to the
|
||||
// file or directory. If the path is a directory, all files in the directory are added to the ConfigMap.
|
||||
// Each subdirectory are ignored. Note that the Generate() function will create the subdirectories ConfigMaps.
|
||||
func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap {
|
||||
normalized := path
|
||||
normalized = strings.TrimLeft(normalized, ".")
|
||||
normalized = strings.TrimLeft(normalized, "/")
|
||||
normalized = regexp.MustCompile(`[^a-zA-Z0-9-]+`).ReplaceAllString(normalized, "-")
|
||||
|
||||
cm := &ConfigMap{
|
||||
path: path,
|
||||
service: &service,
|
||||
usage: FileMapUsageFiles,
|
||||
ConfigMap: &corev1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName) + "-" + normalized,
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Data: make(map[string]string),
|
||||
},
|
||||
}
|
||||
// cumulate the path to the WorkingDir
|
||||
path = filepath.Join(service.WorkingDir, path)
|
||||
path = filepath.Clean(path)
|
||||
if err := cm.AppendDir(path); err != nil {
|
||||
log.Fatal("Error adding files to configmap:", err)
|
||||
}
|
||||
return cm
|
||||
}
|
||||
|
||||
// AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists.
|
||||
func (c *ConfigMap) AddData(key, value string) {
|
||||
c.Data[key] = value
|
||||
}
|
||||
|
||||
// AddBinaryData adds binary data to the configmap. Append or overwrite the value if the key already exists.
|
||||
func (c *ConfigMap) AddBinaryData(key string, value []byte) {
|
||||
if c.BinaryData == nil {
|
||||
c.BinaryData = make(map[string][]byte)
|
||||
}
|
||||
c.BinaryData[key] = value
|
||||
}
|
||||
|
||||
// AppendDir adds files from given path to the configmap. It is not recursive, to add all files in a directory,
|
||||
// you need to call this function for each subdirectory.
|
||||
func (c *ConfigMap) AppendDir(path string) error {
|
||||
// read all files in the path and add them to the configmap
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("path %s does not exist, %w", path, err)
|
||||
}
|
||||
// recursively read all files in the path and add them to the configmap
|
||||
if stat.IsDir() {
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
utils.Warn("Subdirectories are ignored for the moment, skipping", filepath.Join(path, file.Name()))
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(path, file.Name())
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// remove the path from the file
|
||||
filename := filepath.Base(path)
|
||||
if utf8.Valid(content) {
|
||||
c.AddData(filename, string(content))
|
||||
} else {
|
||||
c.AddBinaryData(filename, content)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
// add the file to the configmap
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := filepath.Base(path)
|
||||
if utf8.Valid(content) {
|
||||
c.AddData(filename, string(content))
|
||||
} else {
|
||||
c.AddBinaryData(filename, content)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConfigMap) AppendFile(path string) error {
|
||||
// read all files in the path and add them to the configmap
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("path %s doesn not exists, %w", path, err)
|
||||
}
|
||||
// recursively read all files in the path and add them to the configmap
|
||||
if !stat.IsDir() {
|
||||
// add the file to the configmap
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if utf8.Valid(content) {
|
||||
c.AddData(filepath.Base(path), string(content))
|
||||
} else {
|
||||
c.AddBinaryData(filepath.Base(path), content)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path.
|
||||
func (c *ConfigMap) Filename() string {
|
||||
switch c.usage {
|
||||
case FileMapUsageFiles:
|
||||
return filepath.Join(c.service.Name, "statics", c.path, "configmap.yaml")
|
||||
default:
|
||||
return c.service.Name + ".configmap.yaml"
|
||||
}
|
||||
}
|
||||
|
||||
// SetData sets the data of the configmap. It replaces the entire data.
|
||||
func (c *ConfigMap) SetData(data map[string]string) {
|
||||
c.Data = data
|
||||
}
|
||||
|
||||
// Yaml returns the yaml representation of the configmap
|
||||
func (c *ConfigMap) Yaml() ([]byte, error) {
|
||||
return ToK8SYaml(c)
|
||||
}
|
92
generator/configMap_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestEnvInConfigMap(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
environment:
|
||||
- FOO=bar
|
||||
- BAR=baz
|
||||
`
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/web/configmap.yaml")
|
||||
configMap := v1.ConfigMap{}
|
||||
if err := yaml.Unmarshal([]byte(output), &configMap); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
data := configMap.Data
|
||||
if len(data) != 2 {
|
||||
t.Errorf("Expected 2 data, got %d", len(data))
|
||||
}
|
||||
if data["FOO"] != "bar" {
|
||||
t.Errorf("Expected FOO to be bar, got %s", data["FOO"])
|
||||
}
|
||||
if data["BAR"] != "baz" {
|
||||
t.Errorf("Expected BAR to be baz, got %s", data["BAR"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapEnv(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
environment:
|
||||
FOO: bar
|
||||
labels:
|
||||
%[1]s/map-env: |-
|
||||
FOO: 'baz'
|
||||
`
|
||||
|
||||
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/web/configmap.yaml")
|
||||
configMap := v1.ConfigMap{}
|
||||
if err := yaml.Unmarshal([]byte(output), &configMap); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
data := configMap.Data
|
||||
if v, ok := data["FOO"]; !ok || v != "baz" {
|
||||
t.Errorf("Expected FOO to be baz, got %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendBadFile(t *testing.T) {
|
||||
cm := NewConfigMap(types.ServiceConfig{}, "app", true)
|
||||
err := cm.AppendFile("foo")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendBadDir(t *testing.T) {
|
||||
cm := NewConfigMap(types.ServiceConfig{}, "app", true)
|
||||
err := cm.AppendDir("foo")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
}
|
||||
}
|
699
generator/converter.go
Normal file
@@ -0,0 +1,699 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"katenary/generator/extrafiles"
|
||||
"katenary/generator/katenaryfile"
|
||||
"katenary/generator/labels"
|
||||
"katenary/generator/labels/labelstructs"
|
||||
"katenary/parser"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
)
|
||||
|
||||
const ingressClassHelp = `# Default value for ingress.class annotation
|
||||
# class: "-"
|
||||
# If the value is "-", controller will not set ingressClassName
|
||||
# If the value is "", Ingress will be set to an empty string, so
|
||||
# controller will use the default value for ingressClass
|
||||
# If the value is specified, controller will set the named class e.g. "nginx"
|
||||
`
|
||||
|
||||
const storageClassHelp = `# Storage class to use for PVCs
|
||||
# storageClass: "-" means use default
|
||||
# storageClass: "" means do not specify
|
||||
# storageClass: "foo" means use that storageClass
|
||||
`
|
||||
|
||||
const headerHelp = `# This file is autogenerated by katenary
|
||||
#
|
||||
# DO NOT EDIT IT BY HAND UNLESS YOU KNOW WHAT YOU ARE DOING
|
||||
#
|
||||
# If you want to change the content of this file, you should edit the
|
||||
# compose file and run katenary again.
|
||||
# If you need to override some values, you can do it in a override file
|
||||
# and use the -f flag to specify it when running the helm command.
|
||||
|
||||
|
||||
`
|
||||
|
||||
const imagePullSecretHelp = `
|
||||
# imagePullSecrets allows you to specify a name of an image pull secret.
|
||||
# You must provide a list of object with the name field set to the name of the
|
||||
# e.g.
|
||||
# pullSecrets:
|
||||
# - name: regcred
|
||||
# You are, for now, responsible for creating the secret.
|
||||
`
|
||||
|
||||
const imagePullPolicyHelp = `# imagePullPolicy allows you to specify a policy to cache or always pull an image.
|
||||
# You must provide a string value with one of the following values:
|
||||
# - Always -> will always pull the image
|
||||
# - Never -> will never pull the image, the image should be present on the node
|
||||
# - IfNotPresent -> will pull the image only if it is not present on the node
|
||||
`
|
||||
|
||||
const resourceHelp = `# Resources allows you to specify the resource requests and limits for a service.
|
||||
# Resources are used to specify the amount of CPU and memory that
|
||||
# a container needs.
|
||||
#
|
||||
# e.g.
|
||||
# resources:
|
||||
# requests:
|
||||
# memory: "64Mi"
|
||||
# cpu: "250m"
|
||||
# limits:
|
||||
# memory: "128Mi"
|
||||
# cpu: "500m"
|
||||
`
|
||||
|
||||
const mainTagAppDoc = `This is the version of the main application.
|
||||
Leave it to blank to use the Chart "AppVersion" value.`
|
||||
|
||||
var unwantedLines = []string{
|
||||
"creationTimestamp:",
|
||||
"status:",
|
||||
}
|
||||
|
||||
var ingressTLSHelp = `# Ingress TLS configuration
|
||||
# If enabled, a secret containing the certificate and the key should be
|
||||
# created by the ingress controller. If the name if emtpy, so the secret
|
||||
# name is generated. You can specify the secret name to use your own secret.
|
||||
`
|
||||
|
||||
// keyRegExp checks if the line starts by a #
|
||||
var keyRegExp = regexp.MustCompile(`^\s*[^#]+:.*`)
|
||||
|
||||
// Convert a compose (docker, podman...) project to a helm chart.
|
||||
// It calls Generate() to generate the chart and then write it to the disk.
|
||||
func Convert(config ConvertOptions, dockerComposeFile ...string) error {
|
||||
var (
|
||||
templateDir = filepath.Join(config.OutputDir, "templates")
|
||||
helpersPath = filepath.Join(config.OutputDir, "templates", "_helpers.tpl")
|
||||
chartPath = filepath.Join(config.OutputDir, "Chart.yaml")
|
||||
valuesPath = filepath.Join(config.OutputDir, "values.yaml")
|
||||
readmePath = filepath.Join(config.OutputDir, "README.md")
|
||||
notesPath = filepath.Join(templateDir, "NOTES.txt")
|
||||
)
|
||||
|
||||
// the current working directory is the directory
|
||||
currentDir, _ := os.Getwd()
|
||||
// go to the root of the project
|
||||
if err := os.Chdir(filepath.Dir(dockerComposeFile[0])); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(currentDir); err != nil { // after the generation, go back to the original directory
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// repove the directory part of the docker-compose files
|
||||
for i, f := range dockerComposeFile {
|
||||
dockerComposeFile[i] = filepath.Base(f)
|
||||
}
|
||||
|
||||
// parse the compose files
|
||||
project, err := parser.Parse(config.Profiles, config.EnvFiles, dockerComposeFile...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// check older version of labels
|
||||
if err := checkOldLabels(project); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: use katenary.yaml file here to set the labels
|
||||
katenaryfile.OverrideWithConfig(project)
|
||||
|
||||
if !config.Force {
|
||||
// check if the chart directory exists
|
||||
// if yes, prevent the user from overwriting it and ask for confirmation
|
||||
if _, err := os.Stat(config.OutputDir); err == nil {
|
||||
overwrite := utils.Confirm(
|
||||
"The chart directory "+config.OutputDir+" already exists, do you want to overwrite it?",
|
||||
utils.IconWarning,
|
||||
)
|
||||
if !overwrite {
|
||||
fmt.Println("Aborting")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
fmt.Println() // clean line
|
||||
}
|
||||
|
||||
// Build the objects !
|
||||
chart, err := Generate(project)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// if the app version is set from the command line, use it
|
||||
if config.AppVersion != nil {
|
||||
chart.AppVersion = *config.AppVersion
|
||||
}
|
||||
chart.Version = config.ChartVersion
|
||||
|
||||
// remove the chart directory if it exists
|
||||
os.RemoveAll(config.OutputDir)
|
||||
|
||||
// create the chart directory
|
||||
if err := os.MkdirAll(templateDir, utils.DirectoryPermission); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add icon from the command line
|
||||
if config.Icon != "" {
|
||||
chart.Icon = config.Icon
|
||||
}
|
||||
|
||||
// write the templates to the disk
|
||||
chart.SaveTemplates(templateDir)
|
||||
|
||||
// write the Chart.yaml file
|
||||
buildCharYamlFile(chart, project, chartPath)
|
||||
|
||||
// build and write the values.yaml file
|
||||
buildValues(chart, project, valuesPath)
|
||||
|
||||
// write the _helpers.tpl to the disk
|
||||
writeContent(helpersPath, []byte(chart.Helper))
|
||||
|
||||
// write the readme to the disk
|
||||
readme := extrafiles.ReadMeFile(chart.Name, chart.Description, chart.Values)
|
||||
writeContent(readmePath, []byte(readme))
|
||||
|
||||
// get the list of services to write in the notes
|
||||
buildNotesFile(project, notesPath)
|
||||
|
||||
// call helm update if needed
|
||||
callHelmUpdate(config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func addChartDoc(values []byte, project *types.Project) []byte {
|
||||
chartDoc := fmt.Sprintf(`# This is the main values.yaml file for the %s chart.
|
||||
# More information can be found in the chart's README.md file.
|
||||
#
|
||||
`, project.Name)
|
||||
|
||||
lines := strings.Split(string(values), "\n")
|
||||
for i, line := range lines {
|
||||
if regexp.MustCompile(`(?m)^name:`).MatchString(line) {
|
||||
doc := "\n# Name of the chart (required), basically the name of the project.\n"
|
||||
lines[i] = doc + line
|
||||
} else if regexp.MustCompile(`(?m)^version:`).MatchString(line) {
|
||||
doc := "\n# Version of the chart (required)\n"
|
||||
lines[i] = doc + line
|
||||
} else if strings.Contains(line, "appVersion:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
doc := fmt.Sprintf(
|
||||
"\n%s# Version of the application (required).\n%s# This should be the main application version.\n",
|
||||
strings.Repeat(" ", spaces),
|
||||
strings.Repeat(" ", spaces),
|
||||
)
|
||||
lines[i] = doc + line
|
||||
} else if strings.Contains(line, "dependencies:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
doc := fmt.Sprintf("\n"+
|
||||
"%s# Dependencies are external charts that this chart will depend on.\n"+
|
||||
"%s# More information can be found in the chart's README.md file.\n",
|
||||
strings.Repeat(" ", spaces),
|
||||
strings.Repeat(" ", spaces),
|
||||
)
|
||||
lines[i] = doc + line
|
||||
}
|
||||
}
|
||||
return []byte(chartDoc + strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func addCommentsToValues(values []byte) []byte {
|
||||
lines := strings.Split(string(values), "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "ingress:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
spacesString := strings.Repeat(" ", spaces)
|
||||
// indent ingressClassHelper comment
|
||||
ingressClassHelp := strings.ReplaceAll(ingressClassHelp, "\n", "\n"+spacesString)
|
||||
ingressClassHelp = strings.TrimRight(ingressClassHelp, " ")
|
||||
ingressClassHelp = spacesString + ingressClassHelp
|
||||
lines[i] = ingressClassHelp + line
|
||||
}
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func addDependencyDescription(values []byte, dependencies []labelstructs.Dependency) []byte {
|
||||
for _, d := range dependencies {
|
||||
name := d.Name
|
||||
if d.Alias != "" {
|
||||
name = d.Alias
|
||||
}
|
||||
|
||||
values = regexp.MustCompile(
|
||||
`(?m)^`+name+`:$`,
|
||||
).ReplaceAll(
|
||||
values,
|
||||
[]byte("\n# "+d.Name+" helm dependency configuration\n"+name+":"),
|
||||
)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// addDescriptions adds the description from the label to the values.yaml file on top
|
||||
// of the service definition.
|
||||
func addDescriptions(values []byte, project types.Project) []byte {
|
||||
for _, service := range project.Services {
|
||||
if description, ok := service.Labels[labels.LabelDescription]; ok {
|
||||
// set it as comment
|
||||
description = "\n# " + strings.ReplaceAll(description, "\n", "\n# ")
|
||||
|
||||
values = regexp.MustCompile(
|
||||
`(?m)^`+service.Name+`:$`,
|
||||
).ReplaceAll(values, []byte(description+"\n"+service.Name+":"))
|
||||
} else {
|
||||
// set it as comment
|
||||
description = "\n# " + service.Name + " configuration"
|
||||
|
||||
values = regexp.MustCompile(
|
||||
`(?m)^`+service.Name+`:$`,
|
||||
).ReplaceAll(
|
||||
values,
|
||||
[]byte(description+"\n"+service.Name+":"),
|
||||
)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func addDocToVariable(service types.ServiceConfig, lines []string) []string {
|
||||
currentService := ""
|
||||
variables := utils.GetValuesFromLabel(service, labels.LabelValues)
|
||||
for i, line := range lines {
|
||||
// if the line is a service, it is a name followed by a colon
|
||||
if regexp.MustCompile(`(?m)^` + service.Name + `:`).MatchString(line) {
|
||||
currentService = service.Name
|
||||
}
|
||||
// for each variable in the service, add the description
|
||||
for varname, variable := range variables {
|
||||
if variable == nil {
|
||||
continue
|
||||
}
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
if regexp.MustCompile(`(?m)\s*`+varname+`:`).MatchString(line) && currentService == service.Name {
|
||||
|
||||
// add # to the beginning of the Description
|
||||
doc := strings.ReplaceAll("\n"+variable.Description, "\n", "\n"+strings.Repeat(" ", spaces)+"# ")
|
||||
doc = strings.TrimRight(doc, " ")
|
||||
doc += "\n" + line
|
||||
|
||||
lines[i] = doc
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func addImagePullPolicyHelp(values []byte) []byte {
|
||||
// add imagePullPolicy help
|
||||
lines := strings.Split(string(values), "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "imagePullPolicy:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
spacesString := strings.Repeat(" ", spaces)
|
||||
// indent imagePullPolicyHelp comment
|
||||
imagePullPolicyHelp := strings.ReplaceAll(imagePullPolicyHelp, "\n", "\n"+spacesString)
|
||||
imagePullPolicyHelp = strings.TrimRight(imagePullPolicyHelp, " ")
|
||||
imagePullPolicyHelp = spacesString + imagePullPolicyHelp
|
||||
lines[i] = imagePullPolicyHelp + line
|
||||
}
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func addImagePullSecretsHelp(values []byte) []byte {
|
||||
// add imagePullSecrets help
|
||||
lines := strings.Split(string(values), "\n")
|
||||
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "pullSecrets:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
spacesString := strings.Repeat(" ", spaces)
|
||||
// indent imagePullSecretHelp comment
|
||||
imagePullSecretHelp := strings.ReplaceAll(imagePullSecretHelp, "\n", "\n"+spacesString)
|
||||
imagePullSecretHelp = strings.TrimRight(imagePullSecretHelp, " ")
|
||||
imagePullSecretHelp = spacesString + imagePullSecretHelp
|
||||
lines[i] = imagePullSecretHelp + line
|
||||
}
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func addMainAppDoc(lines []string, service types.ServiceConfig) []string {
|
||||
inService := false
|
||||
inRegistry := false
|
||||
for i, line := range lines {
|
||||
if regexp.MustCompile(`^` + service.Name + `:`).MatchString(line) {
|
||||
inService = true
|
||||
}
|
||||
if inService && regexp.MustCompile(`^\s*repository:.*`).MatchString(line) {
|
||||
inRegistry = true
|
||||
}
|
||||
if inService && inRegistry {
|
||||
if regexp.MustCompile(`^\s*tag: .*`).MatchString(line) {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
doc := strings.ReplaceAll(mainTagAppDoc, "\n", "\n"+strings.Repeat(" ", spaces)+"# ")
|
||||
doc = strings.Repeat(" ", spaces) + "# " + doc
|
||||
|
||||
lines[i] = doc + "\n" + line + "\n"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func addMainTagAppDoc(values []byte, project *types.Project) []byte {
|
||||
lines := strings.Split(string(values), "\n")
|
||||
|
||||
for _, service := range project.Services {
|
||||
// read the label LabelMainApp
|
||||
if v, ok := service.Labels[labels.LabelMainApp]; !ok {
|
||||
continue
|
||||
} else if v == "false" || v == "no" || v == "0" {
|
||||
continue
|
||||
} else {
|
||||
fmt.Printf("%s Adding main tag app doc %s\n", utils.IconConfig, service.Name)
|
||||
}
|
||||
|
||||
lines = addMainAppDoc(lines, service)
|
||||
}
|
||||
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func addResourceHelp(values []byte) []byte {
|
||||
lines := strings.Split(string(values), "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "resources:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
spacesString := strings.Repeat(" ", spaces)
|
||||
// indent resourceHelp comment
|
||||
resourceHelp := strings.ReplaceAll(resourceHelp, "\n", "\n"+spacesString)
|
||||
resourceHelp = strings.TrimRight(resourceHelp, " ")
|
||||
resourceHelp = spacesString + resourceHelp
|
||||
lines[i] = resourceHelp + line
|
||||
}
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
// addStorageClassHelp adds a comment to the values.yaml file to explain how to
|
||||
// use the storageClass option.
|
||||
func addStorageClassHelp(values []byte) []byte {
|
||||
lines := strings.Split(string(values), "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "storageClass:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
spacesString := strings.Repeat(" ", spaces)
|
||||
// indent ingressClassHelper comment
|
||||
storageClassHelp := strings.ReplaceAll(storageClassHelp, "\n", "\n"+spacesString)
|
||||
storageClassHelp = strings.TrimRight(storageClassHelp, " ")
|
||||
storageClassHelp = spacesString + storageClassHelp
|
||||
lines[i] = storageClassHelp + line
|
||||
}
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func addVariablesDoc(values []byte, project *types.Project) []byte {
|
||||
lines := strings.Split(string(values), "\n")
|
||||
|
||||
for _, service := range project.Services {
|
||||
lines = addDocToVariable(service, lines)
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
// addYAMLSelectorPath adds a selector path to the yaml file for each key
|
||||
// as comment. E.g. foo.ingress.host
|
||||
func addYAMLSelectorPath(values []byte) []byte {
|
||||
lines := strings.Split(string(values), "\n")
|
||||
currentKey := ""
|
||||
currentLevel := 0
|
||||
toReturn := []string{}
|
||||
for _, line := range lines {
|
||||
// if the line is a not a key, continue
|
||||
if !keyRegExp.MatchString(line) {
|
||||
toReturn = append(toReturn, line)
|
||||
continue
|
||||
}
|
||||
// get the key
|
||||
key := strings.TrimSpace(strings.Split(line, ":")[0])
|
||||
|
||||
// get the spaces
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
|
||||
if spaces/2 > currentLevel {
|
||||
currentLevel++
|
||||
} else if spaces/2 < currentLevel {
|
||||
currentLevel--
|
||||
}
|
||||
currentKey = strings.Join(strings.Split(currentKey, ".")[:spaces/2], ".")
|
||||
|
||||
if currentLevel == 0 {
|
||||
currentKey = key
|
||||
toReturn = append(toReturn, line)
|
||||
continue
|
||||
}
|
||||
// if the key is not empty, add the selector path
|
||||
if currentKey != "" {
|
||||
currentKey += "."
|
||||
}
|
||||
currentKey += key
|
||||
// add the selector path as comment
|
||||
toReturn = append(
|
||||
toReturn,
|
||||
strings.Repeat(" ", spaces)+"# key: "+currentKey+"\n"+line,
|
||||
)
|
||||
}
|
||||
return []byte(strings.Join(toReturn, "\n"))
|
||||
}
|
||||
|
||||
// addTLSHelp adds a comment to the values.yaml file to explain how to
|
||||
// use the tls option.
|
||||
func addTLSHelp(values []byte) []byte {
|
||||
lines := strings.Split(string(values), "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "tls:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
spacesString := strings.Repeat(" ", spaces)
|
||||
// indent ingressClassHelper comment
|
||||
ingressTLSHelp := strings.ReplaceAll(ingressTLSHelp, "\n", "\n"+spacesString)
|
||||
ingressTLSHelp = strings.TrimRight(ingressTLSHelp, " ")
|
||||
ingressTLSHelp = spacesString + ingressTLSHelp
|
||||
lines[i] = ingressTLSHelp + line
|
||||
}
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func buildCharYamlFile(chart *HelmChart, project *types.Project, chartPath string) {
|
||||
// calculate the sha1 hash of the services
|
||||
yamlChart, err := utils.EncodeBasicYaml(chart)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// concat chart adding a comment with hash of services on top
|
||||
yamlChart = append(fmt.Appendf(nil, "# compose hash (sha1): %s\n", *chart.composeHash), yamlChart...)
|
||||
// add the list of compose files
|
||||
files := []string{}
|
||||
for _, file := range project.ComposeFiles {
|
||||
base := filepath.Base(file)
|
||||
files = append(files, base)
|
||||
}
|
||||
yamlChart = append(fmt.Appendf(nil, "# compose files: %s\n", strings.Join(files, ", ")), yamlChart...)
|
||||
// add generated date
|
||||
yamlChart = append(fmt.Appendf(nil, "# generated at: %s\n", time.Now().Format(time.RFC3339)), yamlChart...)
|
||||
|
||||
// document Chart.yaml file
|
||||
yamlChart = addChartDoc(yamlChart, project)
|
||||
|
||||
writeContent(chartPath, yamlChart)
|
||||
}
|
||||
|
||||
func buildNotesFile(project *types.Project, notesPath string) {
|
||||
// get the list of services to write in the notes
|
||||
services := make([]string, 0)
|
||||
for _, service := range project.Services {
|
||||
services = append(services, service.Name)
|
||||
}
|
||||
// write the notes to the disk
|
||||
notes := extrafiles.NotesFile(services)
|
||||
writeContent(notesPath, []byte(notes))
|
||||
}
|
||||
|
||||
func buildValues(chart *HelmChart, project *types.Project, valuesPath string) {
|
||||
values, err := utils.EncodeBasicYaml(&chart.Values)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
values = addDescriptions(values, *project)
|
||||
values = addDependencyDescription(values, chart.Dependencies)
|
||||
values = addCommentsToValues(values)
|
||||
values = addStorageClassHelp(values)
|
||||
values = addImagePullSecretsHelp(values)
|
||||
values = addImagePullPolicyHelp(values)
|
||||
values = addVariablesDoc(values, project)
|
||||
values = addMainTagAppDoc(values, project)
|
||||
values = addResourceHelp(values)
|
||||
values = addTLSHelp(values)
|
||||
values = addYAMLSelectorPath(values)
|
||||
values = append([]byte(headerHelp), values...)
|
||||
|
||||
// add vim modeline
|
||||
values = append(values, []byte("\n# vim: ft=yaml\n")...)
|
||||
|
||||
// write the values to the disk
|
||||
writeContent(valuesPath, values)
|
||||
}
|
||||
|
||||
func callHelmUpdate(config ConvertOptions) {
|
||||
executeAndHandleError := func(fn func(ConvertOptions) error, config ConvertOptions, message string) {
|
||||
if err := fn(config); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(utils.IconSuccess, message)
|
||||
}
|
||||
if config.HelmUpdate {
|
||||
executeAndHandleError(helmUpdate, config, "Helm dependencies updated")
|
||||
executeAndHandleError(helmLint, config, "Helm chart linted")
|
||||
fmt.Println(utils.IconSuccess, "Helm chart created successfully")
|
||||
}
|
||||
}
|
||||
|
||||
func removeNewlinesInsideBrackets(values []byte) []byte {
|
||||
re, err := regexp.Compile(`(?s)\{\{(.*?)\}\}`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return re.ReplaceAllFunc(values, func(b []byte) []byte {
|
||||
// get the first match
|
||||
matches := re.FindSubmatch(b)
|
||||
replacement := bytes.ReplaceAll(matches[1], []byte("\n"), []byte(" "))
|
||||
// remove repeated spaces
|
||||
replacement = regexp.MustCompile(`\s+`).ReplaceAll(replacement, []byte(" "))
|
||||
// remove newlines inside brackets
|
||||
return bytes.ReplaceAll(b, matches[1], replacement)
|
||||
})
|
||||
}
|
||||
|
||||
func removeUnwantedLines(values []byte) []byte {
|
||||
lines := strings.Split(string(values), "\n")
|
||||
output := []string{}
|
||||
for _, line := range lines {
|
||||
next := false
|
||||
for _, unwanted := range unwantedLines {
|
||||
if strings.Contains(line, unwanted) {
|
||||
next = true
|
||||
}
|
||||
}
|
||||
if !next {
|
||||
output = append(output, line)
|
||||
}
|
||||
}
|
||||
return []byte(strings.Join(output, "\n"))
|
||||
}
|
||||
|
||||
func writeContent(path string, content []byte) {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() {
|
||||
if _, err := f.Write(content); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// helmLint runs "helm lint" on the output directory.
|
||||
func helmLint(config ConvertOptions) error {
|
||||
fmt.Println(utils.IconInfo, "Linting...")
|
||||
helm, err := exec.LookPath("helm")
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := exec.Command(helm, "lint", config.OutputDir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// helmUpdate runs "helm dependency update" on the output directory.
|
||||
func helmUpdate(config ConvertOptions) error {
|
||||
// lookup for "helm" binary
|
||||
fmt.Println(utils.IconInfo, "Updating helm dependencies...")
|
||||
helm, err := exec.LookPath("helm")
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// run "helm dependency update"
|
||||
cmd := exec.Command(helm, "dependency", "update", config.OutputDir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// check if the project makes use of older labels (kanetary.[^v3])
|
||||
func checkOldLabels(project *types.Project) error {
|
||||
badServices := make([]string, 0)
|
||||
for _, service := range project.Services {
|
||||
for label := range service.Labels {
|
||||
if strings.Contains(label, "katenary.") && !strings.Contains(label, labels.KatenaryLabelPrefix) {
|
||||
badServices = append(badServices, fmt.Sprintf("- %s: %s", service.Name, label))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(badServices) > 0 {
|
||||
message := fmt.Sprintf(` Old labels detected in project "%s".
|
||||
|
||||
The current version of katenary uses labels with the prefix "%s" which are not compatible with previous versions.
|
||||
Your project is not compatible with this version.
|
||||
|
||||
Please upgrade your labels to follow the current version
|
||||
|
||||
Services to upgrade:
|
||||
%s`,
|
||||
project.Name,
|
||||
labels.KatenaryLabelPrefix[0:len(labels.KatenaryLabelPrefix)-1],
|
||||
strings.Join(badServices, "\n"),
|
||||
)
|
||||
|
||||
return errors.New(utils.WordWrap(message, 80))
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
123
generator/cronJob.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/generator/labels"
|
||||
"katenary/generator/labels/labelstructs"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// only used to check interface implementation
|
||||
var (
|
||||
_ Yaml = (*CronJob)(nil)
|
||||
)
|
||||
|
||||
// CronJob is a kubernetes CronJob.
|
||||
type CronJob struct {
|
||||
*batchv1.CronJob
|
||||
service *types.ServiceConfig
|
||||
}
|
||||
|
||||
// NewCronJob creates a new CronJob from a compose service. The appName is the name of the application taken from the project name.
|
||||
func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) (*CronJob, *RBAC) {
|
||||
labels, ok := service.Labels[labels.LabelCronJob]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
mapping, err := labelstructs.CronJobFrom(labels)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing cronjob labels: %s", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if _, ok := chart.Values[service.Name]; !ok {
|
||||
chart.Values[service.Name] = NewValue(service, false)
|
||||
}
|
||||
if chart.Values[service.Name].(*Value).CronJob == nil {
|
||||
chart.Values[service.Name].(*Value).CronJob = &CronJobValue{}
|
||||
}
|
||||
chart.Values[service.Name].(*Value).CronJob.Schedule = mapping.Schedule
|
||||
chart.Values[service.Name].(*Value).CronJob.ImagePullPolicy = "IfNotPresent"
|
||||
chart.Values[service.Name].(*Value).CronJob.Environment = map[string]any{}
|
||||
|
||||
image, tag := mapping.Image, ""
|
||||
if image == "" { // if image is not set, use the image from the service
|
||||
image = service.Image
|
||||
}
|
||||
|
||||
if strings.Contains(image, ":") {
|
||||
image = strings.Split(service.Image, ":")[0]
|
||||
tag = strings.Split(service.Image, ":")[1]
|
||||
}
|
||||
|
||||
chart.Values[service.Name].(*Value).CronJob.Repository = &RepositoryValue{
|
||||
Image: image,
|
||||
Tag: tag,
|
||||
}
|
||||
|
||||
cronjob := &CronJob{
|
||||
CronJob: &batchv1.CronJob{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "CronJob",
|
||||
APIVersion: "batch/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "{{ .Values." + service.Name + ".cronjob.schedule }}",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "cronjob",
|
||||
Image: "{{ .Values." + service.Name + ".cronjob.repository.image }}:{{ default .Values." + service.Name + ".cronjob.repository.tag \"latest\" }}",
|
||||
Command: []string{
|
||||
"sh",
|
||||
"-c",
|
||||
mapping.Command,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
service: &service,
|
||||
}
|
||||
|
||||
var rbac *RBAC
|
||||
if mapping.Rbac {
|
||||
rbac = NewRBAC(service, appName)
|
||||
// add the service account to the cronjob
|
||||
cronjob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName = utils.TplName(service.Name, appName)
|
||||
}
|
||||
|
||||
return cronjob, rbac
|
||||
}
|
||||
|
||||
// Filename returns the filename of the cronjob.
|
||||
//
|
||||
// Implements the Yaml interface.
|
||||
func (c *CronJob) Filename() string {
|
||||
return c.service.Name + ".cronjob.yaml"
|
||||
}
|
||||
|
||||
// Yaml returns the yaml representation of the cronjob.
|
||||
//
|
||||
// Implements the Yaml interface.
|
||||
func (c *CronJob) Yaml() ([]byte, error) {
|
||||
return ToK8SYaml(c)
|
||||
}
|
115
generator/cronJob_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestBasicCronJob(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
cron:
|
||||
image: fedora
|
||||
labels:
|
||||
katenary.v3/cronjob: |
|
||||
image: alpine
|
||||
command: echo hello
|
||||
schedule: "*/1 * * * *"
|
||||
rbac: false
|
||||
`
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/cron/cronjob.yaml")
|
||||
cronJob := batchv1.CronJob{}
|
||||
if err := yaml.Unmarshal([]byte(output), &cronJob); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
if cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Image != "alpine:latest" {
|
||||
t.Errorf("Expected image to be alpine, got %s", cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Image)
|
||||
}
|
||||
combinedCommand := strings.Join(cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Command, " ")
|
||||
if combinedCommand != "sh -c echo hello" {
|
||||
t.Errorf("Expected command to be sh -c echo hello, got %s", combinedCommand)
|
||||
}
|
||||
if cronJob.Spec.Schedule != "*/1 * * * *" {
|
||||
t.Errorf("Expected schedule to be */1 * * * *, got %s", cronJob.Spec.Schedule)
|
||||
}
|
||||
|
||||
// ensure that there are a deployment for the fedora Container
|
||||
var err error
|
||||
output, err = helmTemplate(ConvertOptions{
|
||||
OutputDir: "./chart",
|
||||
}, "-s", "templates/cron/deployment.yaml")
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
deployment := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &deployment); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
if deployment.Spec.Template.Spec.Containers[0].Image != "fedora:latest" {
|
||||
t.Errorf("Expected image to be fedora, got %s", deployment.Spec.Template.Spec.Containers[0].Image)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronJobbWithRBAC(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
cron:
|
||||
image: fedora
|
||||
labels:
|
||||
katenary.v3/cronjob: |
|
||||
image: alpine
|
||||
command: echo hello
|
||||
schedule: "*/1 * * * *"
|
||||
rbac: true
|
||||
`
|
||||
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/cron/cronjob.yaml")
|
||||
cronJob := batchv1.CronJob{}
|
||||
if err := yaml.Unmarshal([]byte(output), &cronJob); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
if cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName == "" {
|
||||
t.Errorf("Expected ServiceAccountName to be set")
|
||||
}
|
||||
|
||||
// find the service account file
|
||||
output, err := helmTemplate(ConvertOptions{
|
||||
OutputDir: "./chart",
|
||||
}, "-s", "templates/cron/serviceaccount.yaml")
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
serviceAccount := corev1.ServiceAccount{}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(output), &serviceAccount); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
if serviceAccount.Name == "" {
|
||||
t.Errorf("Expected ServiceAccountName to be set")
|
||||
}
|
||||
|
||||
// ensure that the serviceAccount is equal to the cronJob
|
||||
if serviceAccount.Name != cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName {
|
||||
t.Errorf("Expected ServiceAccountName to be %s, got %s", cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName, serviceAccount.Name)
|
||||
}
|
||||
}
|
732
generator/deployment.go
Normal file
@@ -0,0 +1,732 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
"katenary/generator/labels/labelstructs"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var _ Yaml = (*Deployment)(nil)
|
||||
|
||||
type mountPathConfig struct {
|
||||
mountPath string
|
||||
subPath string
|
||||
}
|
||||
|
||||
type ConfigMapMount struct {
|
||||
configMap *ConfigMap
|
||||
mountPath []mountPathConfig
|
||||
}
|
||||
|
||||
// Deployment is a kubernetes Deployment.
|
||||
type Deployment struct {
|
||||
*appsv1.Deployment `yaml:",inline"`
|
||||
chart *HelmChart `yaml:"-"`
|
||||
configMaps map[string]*ConfigMapMount `yaml:"-"`
|
||||
volumeMap map[string]string `yaml:"-"` // keep map of fixed named to original volume name
|
||||
service *types.ServiceConfig `yaml:"-"`
|
||||
defaultTag string `yaml:"-"`
|
||||
isMainApp bool `yaml:"-"`
|
||||
exchangesVolumes map[string]*labelstructs.ExchangeVolume `yaml:"-"`
|
||||
boundEnvVar []string `yaml:"-"` // environement to remove
|
||||
}
|
||||
|
||||
// NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name.
|
||||
// It also creates the Values map that will be used to create the values.yaml file.
|
||||
func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment {
|
||||
isMainApp := false
|
||||
if mainLabel, ok := service.Labels[labels.LabelMainApp]; ok {
|
||||
main := strings.ToLower(mainLabel)
|
||||
isMainApp = main == "true" || main == "yes" || main == "1"
|
||||
}
|
||||
|
||||
defaultTag := `default "latest"`
|
||||
if isMainApp {
|
||||
defaultTag = `default .Chart.AppVersion`
|
||||
}
|
||||
|
||||
chart.Values[service.Name] = NewValue(service, isMainApp)
|
||||
appName := chart.Name
|
||||
|
||||
dep := &Deployment{
|
||||
isMainApp: isMainApp,
|
||||
defaultTag: defaultTag,
|
||||
service: &service,
|
||||
chart: chart,
|
||||
Deployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: utils.Int32Ptr(1),
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: GetMatchLabels(service.Name, appName),
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: GetMatchLabels(service.Name, appName),
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
NodeSelector: map[string]string{
|
||||
labels.LabelName("node-selector"): "replace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
configMaps: make(map[string]*ConfigMapMount),
|
||||
volumeMap: make(map[string]string),
|
||||
exchangesVolumes: map[string]*labelstructs.ExchangeVolume{},
|
||||
boundEnvVar: []string{},
|
||||
}
|
||||
|
||||
// add containers
|
||||
dep.AddContainer(service)
|
||||
|
||||
// add volumes
|
||||
dep.AddVolumes(service, appName)
|
||||
|
||||
if service.Environment != nil {
|
||||
dep.SetEnvFrom(service, appName)
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
|
||||
// AddContainer adds a container to the deployment.
|
||||
func (d *Deployment) AddContainer(service types.ServiceConfig) {
|
||||
ports := []corev1.ContainerPort{}
|
||||
|
||||
for _, port := range service.Ports {
|
||||
name := utils.GetServiceNameByPort(int(port.Target))
|
||||
if name == "" {
|
||||
utils.Warn("Port name not found for port ", port.Target, " in service ", service.Name, ". Using port number instead")
|
||||
name = fmt.Sprintf("port-%d", port.Target)
|
||||
}
|
||||
ports = append(ports, corev1.ContainerPort{
|
||||
ContainerPort: int32(port.Target),
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
container := corev1.Container{
|
||||
Image: utils.TplValue(service.Name, "repository.image") + ":" +
|
||||
utils.TplValue(service.Name, "repository.tag", d.defaultTag),
|
||||
Ports: ports,
|
||||
Name: service.ContainerName,
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{},
|
||||
},
|
||||
Command: service.Command,
|
||||
}
|
||||
if _, ok := d.chart.Values[service.Name]; !ok {
|
||||
d.chart.Values[service.Name] = NewValue(service, d.isMainApp)
|
||||
}
|
||||
d.chart.Values[service.Name].(*Value).ImagePullPolicy = string(corev1.PullIfNotPresent)
|
||||
|
||||
// add an imagePullSecret, it actually does not work because the secret is not
|
||||
// created but it add the reference in the YAML file. We'll change it in Yaml()
|
||||
// method.
|
||||
d.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{
|
||||
Name: `{{ .Values.pullSecrets | toYaml | indent __indent__ }}`,
|
||||
}}
|
||||
|
||||
// add ServiceAccount to the deployment
|
||||
d.Spec.Template.Spec.ServiceAccountName = `{{ .Values.` + service.Name + `.serviceAccount | quote }}`
|
||||
|
||||
d.AddHealthCheck(service, &container)
|
||||
|
||||
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, container)
|
||||
}
|
||||
|
||||
func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container) {
|
||||
// get the label for healthcheck
|
||||
if v, ok := service.Labels[labels.LabelHealthCheck]; ok {
|
||||
probes, err := labelstructs.ProbeFrom(v)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
container.LivenessProbe = probes.LivenessProbe
|
||||
container.ReadinessProbe = probes.ReadinessProbe
|
||||
return
|
||||
}
|
||||
|
||||
if service.HealthCheck != nil {
|
||||
period := 30.0
|
||||
if service.HealthCheck.Interval != nil {
|
||||
period = time.Duration(*service.HealthCheck.Interval).Seconds()
|
||||
}
|
||||
container.LivenessProbe = &corev1.Probe{
|
||||
ProbeHandler: corev1.ProbeHandler{
|
||||
Exec: &corev1.ExecAction{
|
||||
Command: service.HealthCheck.Test[1:],
|
||||
},
|
||||
},
|
||||
PeriodSeconds: int32(period),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddIngress adds an ingress to the deployment. It creates the ingress object.
|
||||
func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress {
|
||||
return NewIngress(service, d.chart)
|
||||
}
|
||||
|
||||
// AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment.
|
||||
// If the volume is a bind volume it will warn the user that it is not supported yet.
|
||||
func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) {
|
||||
tobind := map[string]bool{}
|
||||
if v, ok := service.Labels[labels.LabelConfigMapFiles]; ok {
|
||||
binds, err := labelstructs.ConfigMapFileFrom(v)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, bind := range binds {
|
||||
tobind[bind] = true
|
||||
}
|
||||
}
|
||||
|
||||
isSamePod := false
|
||||
if v, ok := service.Labels[labels.LabelSamePod]; !ok {
|
||||
isSamePod = false
|
||||
} else {
|
||||
isSamePod = v != ""
|
||||
}
|
||||
|
||||
for _, volume := range service.Volumes {
|
||||
d.bindVolumes(volume, isSamePod, tobind, service, appName)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Deployment) AddLegacyVolume(name, kind string) {
|
||||
// ensure the volume is not present
|
||||
for _, v := range d.Spec.Template.Spec.Volumes {
|
||||
if v.Name == name {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// init
|
||||
if d.Spec.Template.Spec.Volumes == nil {
|
||||
d.Spec.Template.Spec.Volumes = []corev1.Volume{}
|
||||
}
|
||||
|
||||
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
Name: name,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) {
|
||||
// find the volume in the binded deployment
|
||||
for _, bindedVolume := range binded.Spec.Template.Spec.Volumes {
|
||||
skip := false
|
||||
for _, targetVol := range d.Spec.Template.Spec.Volumes {
|
||||
if targetVol.Name == bindedVolume.Name {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !skip {
|
||||
// add the volume to the current deployment
|
||||
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, bindedVolume)
|
||||
// get the container
|
||||
}
|
||||
// add volume mount to the container
|
||||
targetContainer, ti := utils.GetContainerByName(service.ContainerName, d.Spec.Template.Spec.Containers)
|
||||
sourceContainer, _ := utils.GetContainerByName(service.ContainerName, binded.Spec.Template.Spec.Containers)
|
||||
for _, bindedMount := range sourceContainer.VolumeMounts {
|
||||
if bindedMount.Name == bindedVolume.Name {
|
||||
targetContainer.VolumeMounts = append(targetContainer.VolumeMounts, bindedMount)
|
||||
}
|
||||
}
|
||||
d.Spec.Template.Spec.Containers[ti] = *targetContainer
|
||||
}
|
||||
}
|
||||
|
||||
// DependsOn adds a initContainer to the deployment that will wait for the service to be up.
|
||||
func (d *Deployment) DependsOn(to *Deployment, servicename string) error {
|
||||
// Add a initContainer with busybox:latest using netcat to check if the service is up
|
||||
// it will wait until the service responds to all ports
|
||||
for _, container := range to.Spec.Template.Spec.Containers {
|
||||
commands := []string{}
|
||||
if len(container.Ports) == 0 {
|
||||
utils.Warn("No ports found for service ",
|
||||
servicename,
|
||||
". You should declare a port in the service or use "+
|
||||
labels.LabelPorts+
|
||||
" label.",
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, port := range container.Ports {
|
||||
command := fmt.Sprintf("until nc -z %s %d; do\n sleep 1;\ndone", to.Name, port.ContainerPort)
|
||||
commands = append(commands, command)
|
||||
}
|
||||
|
||||
command := []string{"/bin/sh", "-c", strings.Join(commands, "\n")}
|
||||
d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, corev1.Container{
|
||||
Name: "wait-for-" + to.service.Name,
|
||||
Image: "busybox:latest",
|
||||
Command: command,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filename returns the filename of the deployment.
|
||||
func (d *Deployment) Filename() string {
|
||||
return d.service.Name + ".deployment.yaml"
|
||||
}
|
||||
|
||||
// SetEnvFrom sets the environment variables to a configmap. The configmap is created.
|
||||
func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, samePod ...bool) {
|
||||
if len(service.Environment) == 0 {
|
||||
return
|
||||
}
|
||||
inSamePod := len(samePod) > 0 && samePod[0]
|
||||
|
||||
drop := []string{}
|
||||
secrets := []string{}
|
||||
|
||||
defer func() {
|
||||
c, index := d.BindMapFilesToContainer(service, secrets, appName)
|
||||
if c == nil || index == -1 {
|
||||
log.Println("Container not found for service ", service.Name)
|
||||
return
|
||||
}
|
||||
d.Spec.Template.Spec.Containers[index] = *c
|
||||
}()
|
||||
|
||||
// secrets from label
|
||||
labelSecrets, err := labelstructs.SecretsFrom(service.Labels[labels.LabelSecrets])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// values from label
|
||||
varDescriptons := utils.GetValuesFromLabel(service, labels.LabelValues)
|
||||
labelValues := []string{}
|
||||
for v := range varDescriptons {
|
||||
labelValues = append(labelValues, v)
|
||||
}
|
||||
|
||||
for _, secret := range labelSecrets {
|
||||
// get the secret name
|
||||
_, ok := service.Environment[secret]
|
||||
if !ok {
|
||||
drop = append(drop, secret)
|
||||
utils.Warn("Secret " + secret + " not found in service " + service.Name + " - skpped")
|
||||
continue
|
||||
}
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
|
||||
if inSamePod {
|
||||
return
|
||||
}
|
||||
|
||||
// for each values from label "values", add it to Values map and change the envFrom
|
||||
// value to {{ .Values.<service>.<value> }}
|
||||
for _, value := range labelValues {
|
||||
// get the environment variable name
|
||||
val, ok := service.Environment[value]
|
||||
if !ok {
|
||||
drop = append(drop, value)
|
||||
utils.Warn("Environment variable " + value + " not found in service " + service.Name + " - skpped")
|
||||
continue
|
||||
}
|
||||
if d.chart.Values[service.Name].(*Value).Environment == nil {
|
||||
d.chart.Values[service.Name].(*Value).Environment = make(map[string]any)
|
||||
}
|
||||
d.chart.Values[service.Name].(*Value).Environment[value] = *val
|
||||
// set the environment variable to bind to the values.yaml file
|
||||
v := utils.TplValue(service.Name, "environment."+value)
|
||||
service.Environment[value] = &v
|
||||
}
|
||||
|
||||
for _, value := range drop {
|
||||
delete(service.Environment, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Deployment) BindMapFilesToContainer(service types.ServiceConfig, secrets []string, appName string) (*corev1.Container, int) {
|
||||
fromSources := []corev1.EnvFromSource{}
|
||||
|
||||
envSize := len(service.Environment)
|
||||
|
||||
for _, secret := range secrets {
|
||||
for k := range service.Environment {
|
||||
if k == secret {
|
||||
envSize--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if envSize > 0 {
|
||||
if service.Name == "db" {
|
||||
log.Println("Service ", service.Name, " has environment variables")
|
||||
log.Println(service.Environment)
|
||||
}
|
||||
fromSources = append(fromSources, corev1.EnvFromSource{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(secrets) > 0 {
|
||||
fromSources = append(fromSources, corev1.EnvFromSource{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
container, index := utils.GetContainerByName(service.ContainerName, d.Spec.Template.Spec.Containers)
|
||||
if container == nil {
|
||||
utils.Warn("Container not found for service " + service.Name)
|
||||
return nil, -1
|
||||
}
|
||||
|
||||
container.EnvFrom = append(container.EnvFrom, fromSources...)
|
||||
|
||||
if container.Env == nil {
|
||||
container.Env = []corev1.EnvVar{}
|
||||
}
|
||||
return container, index
|
||||
}
|
||||
|
||||
func (d *Deployment) MountExchangeVolumes() {
|
||||
for name, ex := range d.exchangesVolumes {
|
||||
for i, c := range d.Spec.Template.Spec.Containers {
|
||||
c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{
|
||||
Name: "exchange-" + ex.Name,
|
||||
MountPath: ex.MountPath,
|
||||
})
|
||||
if len(ex.Init) > 0 && name == c.Name {
|
||||
d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, corev1.Container{
|
||||
Command: []string{"/bin/sh", "-c", ex.Init},
|
||||
Image: c.Image,
|
||||
Name: "exhange-init-" + name,
|
||||
VolumeMounts: []corev1.VolumeMount{{
|
||||
Name: "exchange-" + ex.Name,
|
||||
MountPath: ex.MountPath,
|
||||
}},
|
||||
})
|
||||
}
|
||||
d.Spec.Template.Spec.Containers[i] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Yaml returns the yaml representation of the deployment.
|
||||
func (d *Deployment) Yaml() ([]byte, error) {
|
||||
var y []byte
|
||||
var err error
|
||||
serviceName := d.service.Name
|
||||
|
||||
if y, err = ToK8SYaml(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// for each volume mount, add a condition "if values has persistence"
|
||||
changing := false
|
||||
content := strings.Split(string(y), "\n")
|
||||
spaces := ""
|
||||
volumeName := ""
|
||||
|
||||
nameDirective := "name: "
|
||||
|
||||
// this loop add condition for each volume mount
|
||||
for line, volume := range content {
|
||||
// find the volume name
|
||||
for i := line; i < len(content); i++ {
|
||||
if strings.Contains(content[i], nameDirective) {
|
||||
volumeName = strings.TrimSpace(strings.Replace(content[i], "name: ", "", 1))
|
||||
break
|
||||
}
|
||||
}
|
||||
if volumeName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := d.configMaps[volumeName]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(volume, "mountPath: ") {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(volume))
|
||||
varName, ok := d.volumeMap[volumeName]
|
||||
if !ok {
|
||||
// this case happens when the volume is a "bind" volume comming from a "same-pod" service.
|
||||
continue
|
||||
}
|
||||
varName = strings.ReplaceAll(varName, "-", "_")
|
||||
content[line] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + varName + `.enabled }}` + "\n" + volume
|
||||
changing = true
|
||||
}
|
||||
if strings.Contains(volume, nameDirective) && changing {
|
||||
content[line] = volume + "\n" + spaces + "{{- end }}"
|
||||
changing = false
|
||||
}
|
||||
}
|
||||
|
||||
changing = false
|
||||
inVolumes := false
|
||||
volumeName = ""
|
||||
// this loop changes imagePullPolicy to {{ .Values.<service>.imagePullPolicy }}
|
||||
// and the volume definition adding the condition "if values has persistence"
|
||||
for i, line := range content {
|
||||
|
||||
if strings.Contains(line, "imagePullPolicy:") {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
|
||||
content[i] = spaces + "imagePullPolicy: {{ .Values." + serviceName + ".imagePullPolicy }}"
|
||||
}
|
||||
|
||||
// find the volume name
|
||||
for i := i; i < len(content); i++ {
|
||||
if strings.Contains(content[i], "- name: ") {
|
||||
volumeName = strings.TrimSpace(strings.Replace(content[i], "- name: ", "", 1))
|
||||
break
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "volumes:") {
|
||||
inVolumes = true
|
||||
}
|
||||
|
||||
if volumeName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := d.configMaps[volumeName]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, "- name: ") && inVolumes {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
|
||||
varName := d.volumeMap[volumeName]
|
||||
varName = strings.ReplaceAll(varName, "-", "_")
|
||||
content[i] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + varName + `.enabled }}` + "\n" + line
|
||||
changing = true
|
||||
}
|
||||
if strings.Contains(line, "claimName: ") && changing {
|
||||
content[i] = line + "\n" + spaces + "{{- end }}"
|
||||
changing = false
|
||||
}
|
||||
}
|
||||
|
||||
// for impagePullSecrets, replace the name with the value from values.yaml
|
||||
for i, line := range content {
|
||||
if strings.Contains(line, "imagePullSecrets:") {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
|
||||
line = spaces + "{{- if .Values.pullSecrets }}"
|
||||
line += "\n" + spaces + "imagePullSecrets:\n"
|
||||
line += spaces + "{{- .Values.pullSecrets | toYaml | nindent __indent__ }}"
|
||||
line += "\n" + spaces + "{{- end }}"
|
||||
content[i] = line
|
||||
}
|
||||
}
|
||||
|
||||
// Find the replicas line and replace it with the value from values.yaml
|
||||
for i, line := range content {
|
||||
// manage nodeSelector
|
||||
if strings.Contains(line, "nodeSelector:") {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
|
||||
pre := spaces + `{{- if .Values.` + serviceName + `.nodeSelector }}`
|
||||
post := spaces + "{{- end }}"
|
||||
ns := spaces + "nodeSelector:\n"
|
||||
ns += spaces + ` {{- .Values.` + serviceName + `.nodeSelector | toYaml | nindent __indent__ }}`
|
||||
line = pre + "\n" + ns + "\n" + post
|
||||
}
|
||||
// manage replicas
|
||||
if strings.Contains(line, "replicas:") {
|
||||
line = regexp.MustCompile("replicas: .*$").ReplaceAllString(line, "replicas: {{ .Values."+serviceName+".replicas }}")
|
||||
}
|
||||
|
||||
// manage serviceAccount, add condition to use the serviceAccount from values.yaml
|
||||
if strings.Contains(line, "serviceAccountName:") {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
|
||||
pre := spaces + `{{- if ne .Values.` + serviceName + `.serviceAccount "" }}`
|
||||
post := spaces + "{{- end }}"
|
||||
line = strings.ReplaceAll(line, "'", "")
|
||||
line = pre + "\n" + line + "\n" + post
|
||||
}
|
||||
|
||||
if strings.Contains(line, "resources: {}") {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
|
||||
pre := spaces + `{{- if .Values.` + serviceName + `.resources }}`
|
||||
post := spaces + "{{- end }}"
|
||||
|
||||
line = strings.ReplaceAll(line, "resources: {}", "resources:")
|
||||
line += "\n" + spaces + " {{ .Values." + serviceName + ".resources | toYaml | nindent __indent__ }}"
|
||||
line = pre + "\n" + line + "\n" + post
|
||||
}
|
||||
|
||||
content[i] = line
|
||||
}
|
||||
|
||||
// find the katenary.v3/node-selector line, and remove it
|
||||
for i, line := range content {
|
||||
if strings.Contains(line, labels.LabelName("node-selector")) {
|
||||
content = append(content[:i], content[i+1:]...)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, "- name: '{{ .Values.pullSecrets ") {
|
||||
content = append(content[:i], content[i+1:]...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(strings.Join(content, "\n")), nil
|
||||
}
|
||||
|
||||
func (d *Deployment) appendDirectoryToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) {
|
||||
pathnme := utils.PathToName(volume.Source)
|
||||
if _, ok := d.configMaps[pathnme]; !ok {
|
||||
d.configMaps[pathnme] = &ConfigMapMount{
|
||||
mountPath: []mountPathConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make it recursive to add all files in the directory and subdirectories
|
||||
_, err := os.ReadDir(volume.Source)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cm := NewConfigMapFromDirectory(service, appName, volume.Source)
|
||||
d.configMaps[pathnme] = &ConfigMapMount{
|
||||
configMap: cm,
|
||||
mountPath: append(d.configMaps[pathnme].mountPath, mountPathConfig{
|
||||
mountPath: volume.Target,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Deployment) appendFileToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) {
|
||||
// In case of a file, add it to the configmap and use "subPath" to mount it
|
||||
// Note that the volumes and volume mounts are not added to the deployment yet, they will be added later
|
||||
// in generate.go
|
||||
dirname := filepath.Dir(volume.Source)
|
||||
pathname := utils.PathToName(dirname)
|
||||
var cm *ConfigMap
|
||||
if v, ok := d.configMaps[pathname]; !ok {
|
||||
cm = NewConfigMap(*d.service, appName, true)
|
||||
cm.usage = FileMapUsageFiles
|
||||
cm.path = dirname
|
||||
cm.Name = utils.TplName(service.Name, appName) + "-" + pathname
|
||||
d.configMaps[pathname] = &ConfigMapMount{
|
||||
configMap: cm,
|
||||
mountPath: []mountPathConfig{{
|
||||
mountPath: volume.Target,
|
||||
subPath: filepath.Base(volume.Source),
|
||||
}},
|
||||
}
|
||||
} else {
|
||||
cm = v.configMap
|
||||
mp := d.configMaps[pathname].mountPath
|
||||
mp = append(mp, mountPathConfig{
|
||||
mountPath: volume.Target,
|
||||
subPath: filepath.Base(volume.Source),
|
||||
})
|
||||
d.configMaps[pathname].mountPath = mp
|
||||
|
||||
}
|
||||
if err := cm.AppendFile(volume.Source); err != nil {
|
||||
log.Fatal("Error adding file to configmap:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Deployment) bindVolumes(volume types.ServiceVolumeConfig, isSamePod bool, tobind map[string]bool, service types.ServiceConfig, appName string) {
|
||||
container, index := utils.GetContainerByName(service.ContainerName, d.Spec.Template.Spec.Containers)
|
||||
|
||||
defer func(d *Deployment, container *corev1.Container, index int) {
|
||||
d.Spec.Template.Spec.Containers[index] = *container
|
||||
}(d, container, index)
|
||||
|
||||
if _, found := tobind[volume.Source]; !isSamePod && volume.Type == "bind" && !found {
|
||||
utils.Warn(
|
||||
"Bind volumes are not supported yet, " +
|
||||
"excepting for those declared as " +
|
||||
labels.LabelConfigMapFiles +
|
||||
", skipping volume " + volume.Source +
|
||||
" from service " + service.Name,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if container == nil {
|
||||
utils.Warn("Container not found for volume", volume.Source)
|
||||
return
|
||||
}
|
||||
|
||||
// ensure that the volume is not already present in the container
|
||||
for _, vm := range container.VolumeMounts {
|
||||
if vm.Name == volume.Source {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch volume.Type {
|
||||
case "volume":
|
||||
// Add volume to container
|
||||
fixedName := utils.FixedResourceName(volume.Source)
|
||||
d.volumeMap[fixedName] = volume.Source
|
||||
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
|
||||
Name: fixedName,
|
||||
MountPath: volume.Target,
|
||||
})
|
||||
// Add volume to values.yaml only if it the service is not in the same pod that another service.
|
||||
// If it is in the same pod, the volume will be added to the other service later
|
||||
if _, ok := service.Labels[labels.LabelSamePod]; !ok {
|
||||
d.chart.Values[service.Name].(*Value).AddPersistence(volume.Source)
|
||||
}
|
||||
// Add volume to deployment
|
||||
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
Name: fixedName,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
|
||||
ClaimName: utils.TplName(service.Name, appName, volume.Source),
|
||||
},
|
||||
},
|
||||
})
|
||||
case "bind":
|
||||
// Add volume to container
|
||||
stat, err := os.Stat(volume.Source)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
d.appendDirectoryToConfigMap(service, appName, volume)
|
||||
} else {
|
||||
d.appendFileToConfigMap(service, appName, volume)
|
||||
}
|
||||
}
|
||||
}
|
533
generator/deployment_test.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
yaml3 "gopkg.in/yaml.v3"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const webTemplateOutput = `templates/web/deployment.yaml`
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
`
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", webTemplateOutput)
|
||||
|
||||
// dt := DeploymentTest{}
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
if *dt.Spec.Replicas != 1 {
|
||||
t.Errorf("Expected replicas to be 1, got %d", dt.Spec.Replicas)
|
||||
t.Errorf("Output: %s", output)
|
||||
}
|
||||
|
||||
if dt.Spec.Template.Spec.Containers[0].Image != "nginx:1.29" {
|
||||
t.Errorf("Expected image to be nginx:1.29, got %s", dt.Spec.Template.Spec.Containers[0].Image)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateOneDeploymentWithSamePod(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
|
||||
fpm:
|
||||
image: php:fpm
|
||||
ports:
|
||||
- 9000:9000
|
||||
labels:
|
||||
katenary.v3/same-pod: web
|
||||
`
|
||||
|
||||
outDir := "./chart"
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", webTemplateOutput)
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
if len(dt.Spec.Template.Spec.Containers) != 2 {
|
||||
t.Errorf("Expected 2 containers, got %d", len(dt.Spec.Template.Spec.Containers))
|
||||
}
|
||||
// endsure that the fpm service is not created
|
||||
|
||||
var err error
|
||||
_, err = helmTemplate(ConvertOptions{
|
||||
OutputDir: outDir,
|
||||
}, "-s", "templates/fpm/deployment.yaml")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
}
|
||||
|
||||
// ensure that the web service is created and has got 2 ports
|
||||
output, err = helmTemplate(ConvertOptions{
|
||||
OutputDir: outDir,
|
||||
}, "-s", "templates/web/service.yaml")
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
service := corev1.Service{}
|
||||
if err := yaml.Unmarshal([]byte(output), &service); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
if len(service.Spec.Ports) != 2 {
|
||||
t.Errorf("Expected 2 ports, got %d", len(service.Spec.Ports))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDependsOn(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
database:
|
||||
image: mariadb:10.5
|
||||
ports:
|
||||
- 3306:3306
|
||||
`
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", webTemplateOutput)
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
if len(dt.Spec.Template.Spec.Containers) != 1 {
|
||||
t.Errorf("Expected 1 container, got %d", len(dt.Spec.Template.Spec.Containers))
|
||||
}
|
||||
// find an init container
|
||||
if len(dt.Spec.Template.Spec.InitContainers) != 1 {
|
||||
t.Errorf("Expected 1 init container, got %d", len(dt.Spec.Template.Spec.InitContainers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmDependencies(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.5
|
||||
ports:
|
||||
- 3306:3306
|
||||
labels:
|
||||
%s/dependencies: |
|
||||
- name: mariadb
|
||||
repository: oci://registry-1.docker.io/bitnamicharts
|
||||
version: 18.x.X
|
||||
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", webTemplateOutput)
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
// ensure that there is no mariasb deployment
|
||||
_, err := helmTemplate(ConvertOptions{
|
||||
OutputDir: "./chart",
|
||||
}, "-s", "templates/mariadb/deployment.yaml")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
}
|
||||
|
||||
// check that Chart.yaml has the dependency
|
||||
chart := HelmChart{}
|
||||
chartFile := "./chart/Chart.yaml"
|
||||
if _, err := os.Stat(chartFile); os.IsNotExist(err) {
|
||||
t.Errorf("Chart.yaml does not exist")
|
||||
}
|
||||
chartContent, err := os.ReadFile(chartFile)
|
||||
if err != nil {
|
||||
t.Errorf("Error reading Chart.yaml: %s", err)
|
||||
}
|
||||
if err := yaml.Unmarshal(chartContent, &chart); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
if len(chart.Dependencies) != 1 {
|
||||
t.Errorf("Expected 1 dependency, got %d", len(chart.Dependencies))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLivenessProbesFromHealthCheck(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
`
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", webTemplateOutput)
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
if dt.Spec.Template.Spec.Containers[0].LivenessProbe == nil {
|
||||
t.Errorf("Expected liveness probe to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbesFromLabels(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
labels:
|
||||
%s/health-check: |
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 80
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 80
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", webTemplateOutput)
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
if dt.Spec.Template.Spec.Containers[0].LivenessProbe == nil {
|
||||
t.Errorf("Expected liveness probe to be set")
|
||||
}
|
||||
if dt.Spec.Template.Spec.Containers[0].ReadinessProbe == nil {
|
||||
t.Errorf("Expected readiness probe to be set")
|
||||
}
|
||||
t.Logf("LivenessProbe: %+v", dt.Spec.Template.Spec.Containers[0].LivenessProbe)
|
||||
|
||||
// ensure that the liveness probe is set to /healthz
|
||||
if dt.Spec.Template.Spec.Containers[0].LivenessProbe.HTTPGet.Path != "/healthz" {
|
||||
t.Errorf("Expected liveness probe path to be /healthz, got %s", dt.Spec.Template.Spec.Containers[0].LivenessProbe.HTTPGet.Path)
|
||||
}
|
||||
|
||||
// ensure that the readiness probe is set to /ready
|
||||
if dt.Spec.Template.Spec.Containers[0].ReadinessProbe.HTTPGet.Path != "/ready" {
|
||||
t.Errorf("Expected readiness probe path to be /ready, got %s", dt.Spec.Template.Spec.Containers[0].ReadinessProbe.HTTPGet.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetValues(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
environment:
|
||||
FOO: bar
|
||||
BAZ: qux
|
||||
labels:
|
||||
%s/values: |
|
||||
- FOO
|
||||
`
|
||||
|
||||
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", webTemplateOutput)
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
// readh the values.yaml, we must have FOO in web environment but not BAZ
|
||||
valuesFile := "./chart/values.yaml"
|
||||
if _, err := os.Stat(valuesFile); os.IsNotExist(err) {
|
||||
t.Errorf("values.yaml does not exist")
|
||||
}
|
||||
valuesContent, err := os.ReadFile(valuesFile)
|
||||
if err != nil {
|
||||
t.Errorf("Error reading values.yaml: %s", err)
|
||||
}
|
||||
mapping := struct {
|
||||
Web struct {
|
||||
Environment map[string]string `yaml:"environment"`
|
||||
} `yaml:"web"`
|
||||
}{}
|
||||
if err := yaml3.Unmarshal(valuesContent, &mapping); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
if v, ok := mapping.Web.Environment["FOO"]; !ok {
|
||||
t.Errorf("Expected FOO in web environment")
|
||||
if v != "bar" {
|
||||
t.Errorf("Expected FOO to be bar, got %s", v)
|
||||
}
|
||||
}
|
||||
if v, ok := mapping.Web.Environment["BAZ"]; ok {
|
||||
t.Errorf("Expected BAZ not in web environment")
|
||||
if v != "qux" {
|
||||
t.Errorf("Expected BAZ to be qux, got %s", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithUnderscoreInContainerName(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web-app:
|
||||
image: nginx:1.29
|
||||
container_name: web_app_container
|
||||
environment:
|
||||
FOO: BAR
|
||||
labels:
|
||||
%s/values: |
|
||||
- FOO
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/web_app/deployment.yaml")
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
// find container.name
|
||||
containerName := dt.Spec.Template.Spec.Containers[0].Name
|
||||
if strings.Contains(containerName, "_") {
|
||||
t.Errorf("Expected container name to not contain underscores, got %s", containerName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithDashes(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web-app:
|
||||
image: nginx:1.29
|
||||
environment:
|
||||
FOO: BAR
|
||||
labels:
|
||||
%s/values: |
|
||||
- FOO
|
||||
`
|
||||
|
||||
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/web_app/deployment.yaml")
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
valuesFile := "./chart/values.yaml"
|
||||
if _, err := os.Stat(valuesFile); os.IsNotExist(err) {
|
||||
t.Errorf("values.yaml does not exist")
|
||||
}
|
||||
valuesContent, err := os.ReadFile(valuesFile)
|
||||
if err != nil {
|
||||
t.Errorf("Error reading values.yaml: %s", err)
|
||||
}
|
||||
mapping := struct {
|
||||
Web struct {
|
||||
Environment map[string]string `yaml:"environment"`
|
||||
} `yaml:"web_app"`
|
||||
}{}
|
||||
if err := yaml3.Unmarshal(valuesContent, &mapping); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
// we must have FOO in web_app environment (not web-app)
|
||||
// this validates that the service name is converted to a valid k8s name
|
||||
if v, ok := mapping.Web.Environment["FOO"]; !ok {
|
||||
t.Errorf("Expected FOO in web_app environment")
|
||||
if v != "BAR" {
|
||||
t.Errorf("Expected FOO to be BAR, got %s", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashesWithValueFrom(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web-app:
|
||||
image: nginx:1.29
|
||||
environment:
|
||||
FOO: BAR
|
||||
labels:
|
||||
%[1]s/values: |
|
||||
- FOO
|
||||
web2:
|
||||
image: nginx:1.29
|
||||
labels:
|
||||
%[1]s/values-from: |
|
||||
BAR: web-app.FOO
|
||||
`
|
||||
|
||||
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/web2/deployment.yaml")
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
valuesFile := "./chart/values.yaml"
|
||||
if _, err := os.Stat(valuesFile); os.IsNotExist(err) {
|
||||
t.Errorf("values.yaml does not exist")
|
||||
}
|
||||
valuesContent, err := os.ReadFile(valuesFile)
|
||||
if err != nil {
|
||||
t.Errorf("Error reading values.yaml: %s", err)
|
||||
}
|
||||
mapping := struct {
|
||||
Web struct {
|
||||
Environment map[string]string `yaml:"environment"`
|
||||
} `yaml:"web_app"`
|
||||
}{}
|
||||
if err := yaml3.Unmarshal(valuesContent, &mapping); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
|
||||
// we must have FOO in web_app environment (not web-app)
|
||||
// this validates that the service name is converted to a valid k8s name
|
||||
if v, ok := mapping.Web.Environment["FOO"]; !ok {
|
||||
t.Errorf("Expected FOO in web_app environment")
|
||||
if v != "BAR" {
|
||||
t.Errorf("Expected FOO to be BAR, got %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that the deployment has the value from the other service
|
||||
barenv := dt.Spec.Template.Spec.Containers[0].Env[0]
|
||||
if barenv.Value != "" {
|
||||
t.Errorf("Expected value to be empty")
|
||||
}
|
||||
if barenv.ValueFrom == nil {
|
||||
t.Errorf("Expected valueFrom to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckCommand(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web-app:
|
||||
image: nginx:1.29
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |-
|
||||
echo "Hello, World!"
|
||||
echo "Done"
|
||||
`
|
||||
|
||||
// composeFile = fmt.Sprintf(composeFile, labels.Prefix())
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/web_app/deployment.yaml")
|
||||
dt := v1.Deployment{}
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
// find the command in the container
|
||||
command := dt.Spec.Template.Spec.Containers[0].Command
|
||||
if len(command) != 3 {
|
||||
t.Errorf("Expected command to have 3 elements, got %d", len(command))
|
||||
}
|
||||
if command[0] != "sh" || command[1] != "-c" {
|
||||
t.Errorf("Expected command to be 'sh -c', got %s", strings.Join(command, " "))
|
||||
}
|
||||
}
|
17
generator/doc.go
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
Package generator generates kubernetes objects from a "compose" file and transforms them into a helm chart.
|
||||
|
||||
The generator package is the core of katenary. It is responsible for generating kubernetes objects from a compose file
|
||||
and transforming them into a helm chart.
|
||||
Conversion manipulates Yaml representation of kubernetes object to add conditions, labels, annotations, etc. to the
|
||||
objects. It also create the values to be set to the values.yaml file.
|
||||
|
||||
The generate.Convert() create an HelmChart object and call "Generate()" method to convert from a compose file to a helm
|
||||
chart. It saves the helm chart in the given directory.
|
||||
|
||||
If you want to change or override the write behavior, you can use the HelmChart.Generate() function and implement your
|
||||
own write function. This function returns
|
||||
the helm chart object containing all kubernetes objects and helm chart ingormation. It does not write the helm chart to
|
||||
the disk.
|
||||
*/
|
||||
package generator
|
2
generator/extrafiles/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
/* Package extrafiles provides function to generate the Chart files that are not objects. Like README.md and notes.txt... */
|
||||
package extrafiles
|
31
generator/extrafiles/notes.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package extrafiles
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed notes.tpl
|
||||
var notesTemplate string
|
||||
|
||||
// NotesFile returns the content of the note.txt file.
|
||||
func NotesFile(services []string) string {
|
||||
// build a list of ingress URLs if there are any
|
||||
ingresses := make([]string, len(services))
|
||||
for i, service := range services {
|
||||
condition := fmt.Sprintf(`{{- if and .Values.%[1]s.ingress .Values.%[1]s.ingress.enabled }}`, service)
|
||||
line := fmt.Sprintf(`{{- $count = add1 $count -}}{{- $listOfURL = printf "%%s\n- http://%%s" $listOfURL (tpl .Values.%s.ingress.host .) -}}`, service)
|
||||
ingresses[i] = fmt.Sprintf("%s\n%s\n{{- end }}", condition, line)
|
||||
}
|
||||
|
||||
// inject the list of ingress URLs into the notes template
|
||||
notes := strings.Split(notesTemplate, "\n")
|
||||
for i, line := range notes {
|
||||
if strings.Contains(line, "ingress_list") {
|
||||
notes[i] = strings.Join(ingresses, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(notes, "\n")
|
||||
}
|
39
generator/extrafiles/notes.tpl
Normal file
@@ -0,0 +1,39 @@
|
||||
Thanks to have installed {{ .Chart.Name }} {{ .Chart.Version }} as {{ .Release.Name }} ({{.Chart.AppVersion }}).
|
||||
|
||||
# Get release information
|
||||
|
||||
To learn more about the release, try:
|
||||
|
||||
$ helm -n {{ .Release.Namespace }} status {{ .Release.Name }}
|
||||
$ helm -n {{ .Release.Namespace }} get values {{ .Release.Name }}
|
||||
$ helm -n {{ .Release.Namespace }} get all {{ .Release.Name }}
|
||||
|
||||
# To delete the release
|
||||
|
||||
Use helm uninstall command to delete the release.
|
||||
|
||||
$ helm -n {{ .Release.Namespace }} uninstall {{ .Release.Name }}
|
||||
|
||||
Note that some resources may still be in use after a release is deleted. For exemple, PersistentVolumeClaims are not deleted by default for some storage classes or if some annotations are set.
|
||||
|
||||
# More information
|
||||
|
||||
You can see this notes again by running:
|
||||
|
||||
$ helm -n {{ .Release.Namespace }} get notes {{ .Release.Name }}
|
||||
|
||||
{{- $count := 0 -}}
|
||||
{{- $listOfURL := "" -}}
|
||||
{{* DO NOT REMOVE, replaced by notes.go: ingress_list *}}
|
||||
{{- if gt $count 0 }}
|
||||
|
||||
# List of activated ingresses URL:
|
||||
{{ $listOfURL }}
|
||||
|
||||
You can get these urls with kubectl:
|
||||
|
||||
kubeclt get ingress -n {{ .Release.Namespace }}
|
||||
|
||||
{{- end }}
|
||||
|
||||
Thanks for using Helm!
|
40
generator/extrafiles/notes_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package extrafiles
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// override the embedded template for testing
|
||||
var testTemplate = `
|
||||
Some header
|
||||
{{ ingress_list }}
|
||||
Some footer
|
||||
`
|
||||
|
||||
func init() {
|
||||
notesTemplate = testTemplate
|
||||
}
|
||||
|
||||
func TestNotesFile_NoServices(t *testing.T) {
|
||||
result := NotesFile([]string{})
|
||||
if !strings.Contains(result, "Some header") || !strings.Contains(result, "Some footer") {
|
||||
t.Errorf("Expected template header/footer in output, got: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotesFile_WithServices(t *testing.T) {
|
||||
services := []string{"svc1", "svc2"}
|
||||
result := NotesFile(services)
|
||||
|
||||
for _, svc := range services {
|
||||
cond := "{{- if and .Values." + svc + ".ingress .Values." + svc + ".ingress.enabled }}"
|
||||
line := "{{- $count = add1 $count -}}{{- $listOfURL = printf \"%s\\n- http://%s\" $listOfURL (tpl .Values." + svc + ".ingress.host .) -}}"
|
||||
if !strings.Contains(result, cond) {
|
||||
t.Errorf("Expected condition for service %s in output", svc)
|
||||
}
|
||||
if !strings.Contains(result, line) {
|
||||
t.Errorf("Expected line for service %s in output", svc)
|
||||
}
|
||||
}
|
||||
}
|
100
generator/extrafiles/readme.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package extrafiles
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed readme.tpl
|
||||
var readmeTemplate string
|
||||
|
||||
type chart struct {
|
||||
Name string
|
||||
Description string
|
||||
Values []string
|
||||
}
|
||||
|
||||
func parseValues(prefix string, values map[string]any, result map[string]string) {
|
||||
for key, value := range values {
|
||||
path := key
|
||||
if prefix != "" {
|
||||
path = prefix + "." + key
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
for i, u := range v {
|
||||
parseValues(fmt.Sprintf("%s[%d]", path, i), map[string]any{"value": u}, result)
|
||||
}
|
||||
case map[string]any:
|
||||
parseValues(path, v, result)
|
||||
default:
|
||||
strValue := fmt.Sprintf("`%v`", value)
|
||||
result["`"+path+"`"] = strValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReadMeFile returns the content of the README.md file.
|
||||
func ReadMeFile(charname, description string, values map[string]any) string {
|
||||
// values is a yaml structure with keys and structured values...
|
||||
// we want to make list of dot separated keys and their values
|
||||
|
||||
vv := map[string]any{}
|
||||
out, _ := yaml.Marshal(values)
|
||||
if err := yaml.Unmarshal(out, &vv); err != nil {
|
||||
log.Printf("Error parsing values: %s", err)
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
parseValues("", vv, result)
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"repeat": func(s string, count int) string {
|
||||
return strings.Repeat(s, count)
|
||||
},
|
||||
}
|
||||
tpl, err := template.New("readme").Funcs(funcMap).Parse(readmeTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
valuesLines := []string{}
|
||||
maxParamLen := 0
|
||||
maxDefaultLen := 0
|
||||
for key, value := range result {
|
||||
if len(key) > maxParamLen {
|
||||
maxParamLen = len(key)
|
||||
}
|
||||
if len(value) > maxDefaultLen {
|
||||
maxDefaultLen = len(value)
|
||||
}
|
||||
}
|
||||
for key, value := range result {
|
||||
valuesLines = append(valuesLines, fmt.Sprintf("| %-*s | %-*s |", maxParamLen, key, maxDefaultLen, value))
|
||||
}
|
||||
sort.Strings(valuesLines)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
err = tpl.Execute(buf, map[string]any{
|
||||
"DescrptionPadding": maxParamLen,
|
||||
"DefaultPadding": maxDefaultLen,
|
||||
"Chart": chart{
|
||||
Name: charname,
|
||||
Description: description,
|
||||
Values: valuesLines,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
32
generator/extrafiles/readme.tpl
Normal file
@@ -0,0 +1,32 @@
|
||||
# {{ .Chart.Name }}
|
||||
|
||||
{{ .Chart.Description }}
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
To install the chart with the release name `my-release`:
|
||||
|
||||
```bash
|
||||
# Standard Helm install
|
||||
$ helm install my-release {{ .Chart.Name }}
|
||||
|
||||
# To use a custom namespace and force the creation of the namespace
|
||||
$ helm install my-release --namespace my-namespace --create-namespace {{ .Chart.Name }}
|
||||
|
||||
# To use a custom values file
|
||||
$ helm install my-release -f my-values.yaml {{ .Chart.Name }}
|
||||
```
|
||||
|
||||
See the [Helm documentation](https://helm.sh/docs/intro/using_helm/) for more information on installing and managing the chart.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following table lists the configurable parameters of the {{ .Chart.Name }} chart and their default values.
|
||||
|
||||
| {{ printf "%-*s" .DescrptionPadding "Parameter" }} | {{ printf "%-*s" .DefaultPadding "Default" }} |
|
||||
| {{ repeat "-" .DescrptionPadding }} | {{ repeat "-" .DefaultPadding }} |
|
||||
{{- range .Chart.Values }}
|
||||
{{ . }}
|
||||
{{- end }}
|
||||
|
||||
|
33
generator/extrafiles/readme_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package extrafiles
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadMeFile_Basic(t *testing.T) {
|
||||
values := map[string]any{
|
||||
"replicas": 2,
|
||||
"image": map[string]any{
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
},
|
||||
}
|
||||
|
||||
result := ReadMeFile("testchart", "A test chart", values)
|
||||
t.Logf("Generated README content:\n%s", result)
|
||||
paramerRegExp := regexp.MustCompile(`\|\s+` + "`" + `(.*?)` + "`" + `\s+\|\s+` + "`" + `(.*?)` + "`" + `\s+\|`)
|
||||
matches := paramerRegExp.FindAllStringSubmatch(result, -1)
|
||||
if len(matches) != 3 {
|
||||
t.Errorf("Expected 5 lines in the table for headers and parameters, got %d", len(matches))
|
||||
}
|
||||
if matches[0][1] != "image.repository" || matches[0][2] != "nginx" {
|
||||
t.Errorf("Expected third line to be image.repository, got %s", matches[1])
|
||||
}
|
||||
if matches[1][1] != "image.tag" || matches[1][2] != "latest" {
|
||||
t.Errorf("Expected fourth line to be image.tag, got %s", matches[2])
|
||||
}
|
||||
if matches[2][1] != "replicas" || matches[2][2] != "2" {
|
||||
t.Errorf("Expected second line to be replicas, got %s", matches[0])
|
||||
}
|
||||
}
|
432
generator/generator.go
Normal file
@@ -0,0 +1,432 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
"katenary/generator/labels/labelstructs"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Generate a chart from a compose project.
|
||||
// This does not write files to disk, it only creates the HelmChart object.
|
||||
//
|
||||
// The Generate function will create the HelmChart object this way:
|
||||
//
|
||||
// - Detect the service port name or leave the port number if not found.
|
||||
// - Create a deployment for each service that are not ingnore.
|
||||
// - Create a service and ingresses for each service that has ports and/or declared ingresses.
|
||||
// - Create a PVC or Configmap volumes for each volume.
|
||||
// - Create init containers for each service which has dependencies to other services.
|
||||
// - Create a chart dependencies.
|
||||
// - Create a configmap and secrets from the environment variables.
|
||||
// - Merge the same-pod services.
|
||||
func Generate(project *types.Project) (*HelmChart, error) {
|
||||
var (
|
||||
appName = project.Name
|
||||
deployments = make(map[string]*Deployment, len(project.Services))
|
||||
services = make(map[string]*Service)
|
||||
podToMerge = make(map[string]*types.ServiceConfig)
|
||||
)
|
||||
chart := NewChart(appName)
|
||||
|
||||
// Add the compose files hash to the chart annotations
|
||||
hash, err := utils.HashComposefiles(project.ComposeFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Annotations[labels.LabelName("compose-hash")] = hash
|
||||
chart.composeHash = &hash
|
||||
|
||||
// drop all services with the "ignore" label
|
||||
dropIngoredServices(project)
|
||||
|
||||
fixContainerNames(project)
|
||||
|
||||
// rename all services name to remove dashes
|
||||
if err := fixResourceNames(project); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// find the "main-app" label, and set chart.AppVersion to the tag if exists
|
||||
mainCount := 0
|
||||
for _, service := range project.Services {
|
||||
if serviceIsMain(service) {
|
||||
mainCount++
|
||||
if mainCount > 1 {
|
||||
return nil, fmt.Errorf("found more than one main app")
|
||||
}
|
||||
chart.setChartVersion(service)
|
||||
}
|
||||
}
|
||||
if mainCount == 0 {
|
||||
chart.AppVersion = "0.1.0"
|
||||
}
|
||||
|
||||
// first pass, create all deployments whatewer they are.
|
||||
for _, service := range project.Services {
|
||||
err := chart.generateDeployment(service, deployments, services, podToMerge, appName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// now we have all deployments, we can create PVC if needed (it's separated from
|
||||
// the above loop because we need all deployments to not duplicate PVC for "same-pod" services)
|
||||
// bind static volumes
|
||||
for _, service := range project.Services {
|
||||
addStaticVolumes(deployments, service)
|
||||
}
|
||||
for _, service := range project.Services {
|
||||
err := buildVolumes(service, chart, deployments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// if we have built exchange volumes, we need to moint them in each deployment
|
||||
for _, d := range deployments {
|
||||
d.MountExchangeVolumes()
|
||||
}
|
||||
|
||||
// drop all "same-pod" deployments because the containers and volumes are already
|
||||
// in the target deployment
|
||||
for _, service := range podToMerge {
|
||||
if samepod, ok := service.Labels[labels.LabelSamePod]; ok && samepod != "" {
|
||||
// move this deployment volumes to the target deployment
|
||||
if target, ok := deployments[samepod]; ok {
|
||||
target.AddContainer(*service)
|
||||
target.BindFrom(*service, deployments[service.Name])
|
||||
target.SetEnvFrom(*service, appName, true)
|
||||
// copy all init containers
|
||||
initContainers := deployments[service.Name].Spec.Template.Spec.InitContainers
|
||||
target.Spec.Template.Spec.InitContainers = append(target.Spec.Template.Spec.InitContainers, initContainers...)
|
||||
delete(deployments, service.Name)
|
||||
} else {
|
||||
log.Printf("service %[1]s is declared as %[2]s, but %[2]s is not defined", service.Name, labels.LabelSamePod)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create init containers for all DependsOn
|
||||
for _, s := range project.Services {
|
||||
for _, d := range s.GetDependencies() {
|
||||
if dep, ok := deployments[d]; ok {
|
||||
err := deployments[s.Name].DependsOn(dep, d)
|
||||
if err != nil {
|
||||
log.Printf("error creating init container for service %[1]s: %[2]s", s.Name, err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("service %[1]s depends on %[2]s, but %[2]s is not defined", s.Name, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
// it's now time to get "value-from", before makeing the secrets and configmaps!
|
||||
for _, s := range project.Services {
|
||||
chart.setEnvironmentValuesFrom(s, deployments)
|
||||
}
|
||||
|
||||
// generate configmaps with environment variables
|
||||
if err := chart.generateConfigMapsAndSecrets(project); err != nil {
|
||||
log.Fatalf("error generating configmaps and secrets: %s", err)
|
||||
}
|
||||
|
||||
// if the env-from label is set, we need to add the env vars from the configmap
|
||||
// to the environment of the service
|
||||
for _, s := range project.Services {
|
||||
chart.setSharedConf(s, deployments)
|
||||
}
|
||||
|
||||
// remove all "boundEnv" from the values
|
||||
for _, d := range deployments {
|
||||
if len(d.boundEnvVar) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range d.boundEnvVar {
|
||||
delete(chart.Values[d.service.Name].(*Value).Environment, e)
|
||||
}
|
||||
}
|
||||
|
||||
// generate yaml files
|
||||
for _, d := range deployments {
|
||||
y, err := d.Yaml()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chart.Templates[d.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: d.service.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// generate all services
|
||||
for _, s := range services {
|
||||
// add the service ports to the target service if it's a "same-pod" service
|
||||
if samePod, ok := podToMerge[s.service.Name]; ok {
|
||||
// get the target service
|
||||
target := services[samePod.Name]
|
||||
// merge the services
|
||||
s.Spec.Ports = append(s.Spec.Ports, target.Spec.Ports...)
|
||||
}
|
||||
y, _ := s.Yaml()
|
||||
chart.Templates[s.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: s.service.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// drop all "same-pod" services
|
||||
for _, s := range podToMerge {
|
||||
// get the target service
|
||||
target := services[s.Name]
|
||||
if target != nil {
|
||||
delete(chart.Templates, target.Filename())
|
||||
}
|
||||
}
|
||||
|
||||
// compute all needed resplacements in YAML templates
|
||||
for n, v := range chart.Templates {
|
||||
v.Content = removeReplaceString(v.Content)
|
||||
v.Content = computeNIndent(v.Content)
|
||||
chart.Templates[n].Content = v.Content
|
||||
}
|
||||
|
||||
// generate helper
|
||||
chart.Helper = Helper(appName)
|
||||
|
||||
return chart, nil
|
||||
}
|
||||
|
||||
// dropIngoredServices removes all services with the "ignore" label set to true (or yes).
|
||||
func dropIngoredServices(project *types.Project) {
|
||||
for i, service := range project.Services {
|
||||
if isIgnored(service) {
|
||||
project.Services = append(project.Services[:i], project.Services[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fixResourceNames renames all services and related resources to remove dashes.
|
||||
func fixResourceNames(project *types.Project) error {
|
||||
// rename all services name to remove dashes
|
||||
for i, service := range project.Services {
|
||||
if service.Name != utils.AsResourceName(service.Name) {
|
||||
fixed := utils.AsResourceName(service.Name)
|
||||
for j, s := range project.Services {
|
||||
// for the same-pod services, we need to keep the original name
|
||||
if samepod, ok := s.Labels[labels.LabelSamePod]; ok && samepod == service.Name {
|
||||
s.Labels[labels.LabelSamePod] = fixed
|
||||
project.Services[j] = s
|
||||
}
|
||||
// also, the value-from label should be updated
|
||||
if valuefrom, ok := s.Labels[labels.LabelValueFrom]; ok {
|
||||
vf, err := labelstructs.GetValueFrom(valuefrom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for varname, bind := range *vf {
|
||||
log.Printf("service %s, varname %s, bind %s", service.Name, varname, bind)
|
||||
bind := strings.ReplaceAll(bind, service.Name, fixed)
|
||||
(*vf)[varname] = bind
|
||||
}
|
||||
output, err := yaml.Marshal(vf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Labels[labels.LabelValueFrom] = string(output)
|
||||
}
|
||||
}
|
||||
service.Name = fixed
|
||||
project.Services[i] = service
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// serviceIsMain returns true if the service is the main app.
|
||||
func serviceIsMain(service types.ServiceConfig) bool {
|
||||
if main, ok := service.Labels[labels.LabelMainApp]; ok {
|
||||
return main == "true" || main == "yes" || main == "1"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceConfig) {
|
||||
// add the bound configMaps files to the deployment containers
|
||||
var d *Deployment
|
||||
var ok bool
|
||||
if d, ok = deployments[service.Name]; !ok {
|
||||
log.Printf("service %s not found in deployments", service.Name)
|
||||
return
|
||||
}
|
||||
|
||||
container, index := utils.GetContainerByName(service.ContainerName, d.Spec.Template.Spec.Containers)
|
||||
if container == nil { // may append for the same-pod services
|
||||
return
|
||||
}
|
||||
for volumeName, config := range d.configMaps {
|
||||
var y []byte
|
||||
var err error
|
||||
if y, err = config.configMap.Yaml(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// add the configmap to the chart
|
||||
d.chart.Templates[config.configMap.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: d.service.Name,
|
||||
}
|
||||
// add the moint path to the container
|
||||
for _, m := range config.mountPath {
|
||||
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
|
||||
Name: utils.PathToName(volumeName),
|
||||
MountPath: m.mountPath,
|
||||
SubPath: m.subPath,
|
||||
})
|
||||
}
|
||||
|
||||
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
Name: utils.PathToName(volumeName),
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: config.configMap.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
d.Spec.Template.Spec.Containers[index] = *container
|
||||
}
|
||||
|
||||
// computeNIndentm replace all __indent__ labels with the number of spaces before the label.
|
||||
func computeNIndent(b []byte) []byte {
|
||||
lines := bytes.Split(b, []byte("\n"))
|
||||
for i, line := range lines {
|
||||
if !bytes.Contains(line, []byte("__indent__")) {
|
||||
continue
|
||||
}
|
||||
startSpaces := ""
|
||||
spaces := regexp.MustCompile(`^\s+`).FindAllString(string(line), -1)
|
||||
if len(spaces) > 0 {
|
||||
startSpaces = spaces[0]
|
||||
}
|
||||
line = []byte(startSpaces + strings.TrimLeft(string(line), " "))
|
||||
line = bytes.ReplaceAll(line, []byte("__indent__"), fmt.Appendf(nil, "%d", len(startSpaces)))
|
||||
lines[i] = line
|
||||
}
|
||||
return bytes.Join(lines, []byte("\n"))
|
||||
}
|
||||
|
||||
// removeReplaceString replace all __replace_ labels with the value of the
|
||||
// capture group and remove all new lines and repeated spaces.
|
||||
//
|
||||
// we created:
|
||||
//
|
||||
// __replace_bar: '{{ include "foo.labels" .
|
||||
// }}'
|
||||
//
|
||||
// note the new line and spaces...
|
||||
//
|
||||
// we now want to replace it with {{ include "foo.labels" . }}, without the label name.
|
||||
func removeReplaceString(b []byte) []byte {
|
||||
// replace all matches with the value of the capture group
|
||||
// and remove all new lines and repeated spaces
|
||||
b = replaceLabelRegexp.ReplaceAllFunc(b, func(b []byte) []byte {
|
||||
inc := replaceLabelRegexp.FindSubmatch(b)[1]
|
||||
inc = bytes.ReplaceAll(inc, []byte("\n"), []byte(""))
|
||||
inc = bytes.ReplaceAll(inc, []byte("\r"), []byte(""))
|
||||
inc = regexp.MustCompile(`\s+`).ReplaceAll(inc, []byte(" "))
|
||||
return inc
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// buildVolumes creates the volumes for the service.
|
||||
func buildVolumes(service types.ServiceConfig, chart *HelmChart, deployments map[string]*Deployment) error {
|
||||
appName := chart.Name
|
||||
for _, v := range service.Volumes {
|
||||
// Do not add volumes if the pod is injected in a deployments
|
||||
// via "same-pod" and the volume in destination deployment exists
|
||||
if samePodVolume(service, v, deployments) {
|
||||
continue
|
||||
}
|
||||
switch v.Type {
|
||||
case "volume":
|
||||
v.Source = utils.AsResourceName(v.Source)
|
||||
pvc := NewVolumeClaim(service, v.Source, appName)
|
||||
|
||||
// if the service is integrated in another deployment, we need to add the volume
|
||||
// to the target deployment
|
||||
if override, ok := service.Labels[labels.LabelSamePod]; ok {
|
||||
pvc.nameOverride = override
|
||||
pvc.Spec.StorageClassName = utils.StrPtr(`{{ .Values.` + override + `.persistence.` + v.Source + `.storageClass }}`)
|
||||
chart.Values[override].(*Value).AddPersistence(v.Source)
|
||||
}
|
||||
y, _ := pvc.Yaml()
|
||||
chart.Templates[pvc.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// samePodVolume returns true if the volume is already in the target deployment.
|
||||
func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, deployments map[string]*Deployment) bool {
|
||||
// if the service has volumes, and it has "same-pod" label
|
||||
// - get the target deployment
|
||||
// - check if it has the same volume
|
||||
// if not, return false
|
||||
|
||||
if v.Source == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(service.Volumes) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
targetDeployment := ""
|
||||
if targetName, ok := service.Labels[labels.LabelSamePod]; !ok {
|
||||
return false
|
||||
} else {
|
||||
targetDeployment = targetName
|
||||
}
|
||||
|
||||
// get the target deployment
|
||||
target := findDeployment(targetDeployment, deployments)
|
||||
if target == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if it has the same volume
|
||||
for _, tv := range target.Spec.Template.Spec.Volumes {
|
||||
if tv.Name == v.Source {
|
||||
log.Printf("found same pod volume %s in deployment %s and %s", tv.Name, service.Name, targetDeployment)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fixContainerNames(project *types.Project) {
|
||||
// fix container names to be unique
|
||||
for i, service := range project.Services {
|
||||
if service.ContainerName == "" {
|
||||
service.ContainerName = utils.FixedResourceName(service.Name)
|
||||
} else {
|
||||
service.ContainerName = utils.FixedResourceName(service.ContainerName)
|
||||
}
|
||||
project.Services[i] = service
|
||||
}
|
||||
}
|
19
generator/globals.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/generator/labels"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// find all labels starting by __replace_ and ending with ":"
|
||||
// and get the value between the quotes
|
||||
// ?s => multiline
|
||||
// (?P<inc>.+?) => named capture group to "inc" variable (so we could use $inc in the replace)
|
||||
replaceLabelRegexp = regexp.MustCompile(`(?s)__replace_.+?: '(?P<inc>.+?)'`)
|
||||
|
||||
// Standard annotationss
|
||||
Annotations = map[string]string{
|
||||
labels.LabelName("version"): Version,
|
||||
}
|
||||
)
|
36
generator/helmHelper.tpl
Normal file
@@ -0,0 +1,36 @@
|
||||
{{- define "__APP__.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "__APP__.name" -}}
|
||||
{{- if .Values.nameOverride -}}
|
||||
{{- .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "__APP__.labels" -}}
|
||||
{{ include "__APP__.selectorLabels" .}}
|
||||
{{ if .Chart.Version -}}
|
||||
{{ printf "__PREFIX__/chart-version: '%s'" .Chart.Version }}
|
||||
{{- end }}
|
||||
{{ if .Chart.AppVersion -}}
|
||||
{{ printf "__PREFIX__/app-version: '%s'" .Chart.AppVersion }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "__APP__.selectorLabels" -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{ printf "__PREFIX__/name: %s" $name }}
|
||||
{{ printf "__PREFIX__/instance: %s" .Release.Name }}
|
||||
{{- end -}}
|
20
generator/helper.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"katenary/generator/labels"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// helmHelper is a template for the _helpers.tpl file in the chart templates directory.
|
||||
//
|
||||
//go:embed helmHelper.tpl
|
||||
var helmHelper string
|
||||
|
||||
// Helper returns the _helpers.tpl file for a chart.
|
||||
func Helper(name string) string {
|
||||
helmHelper := strings.ReplaceAll(helmHelper, "__APP__", name)
|
||||
helmHelper = strings.ReplaceAll(helmHelper, "__PREFIX__", labels.KatenaryLabelPrefix)
|
||||
helmHelper = strings.ReplaceAll(helmHelper, "__VERSION__", "0.1.0")
|
||||
return helmHelper
|
||||
}
|
203
generator/ingress.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/generator/labels"
|
||||
"katenary/generator/labels/labelstructs"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
networkv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var _ Yaml = (*Ingress)(nil)
|
||||
|
||||
type Ingress struct {
|
||||
*networkv1.Ingress
|
||||
service *types.ServiceConfig `yaml:"-"`
|
||||
appName string `yaml:"-"`
|
||||
}
|
||||
|
||||
// NewIngress creates a new Ingress from a compose service.
|
||||
func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress {
|
||||
appName := Chart.Name
|
||||
|
||||
if service.Labels == nil {
|
||||
service.Labels = make(map[string]string)
|
||||
}
|
||||
var label string
|
||||
var ok bool
|
||||
if label, ok = service.Labels[labels.LabelIngress]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
mapping, err := labelstructs.IngressFrom(label)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse ingress label: %s\n", err)
|
||||
}
|
||||
if mapping.Hostname == "" {
|
||||
mapping.Hostname = service.Name + ".tld"
|
||||
}
|
||||
|
||||
// create the ingress
|
||||
pathType := networkv1.PathTypeImplementationSpecific
|
||||
|
||||
// fix the service name, and create the full name from variable name
|
||||
// which is injected in the YAML() method
|
||||
serviceName := strings.ReplaceAll(service.Name, "_", "-")
|
||||
fullName := `{{ $fullname }}-` + serviceName
|
||||
|
||||
// Add the ingress host to the values.yaml
|
||||
if Chart.Values[service.Name] == nil {
|
||||
Chart.Values[service.Name] = &Value{}
|
||||
}
|
||||
|
||||
Chart.Values[service.Name].(*Value).Ingress = &IngressValue{
|
||||
Enabled: mapping.Enabled,
|
||||
Path: *mapping.Path,
|
||||
Host: mapping.Hostname,
|
||||
Class: *mapping.Class,
|
||||
Annotations: mapping.Annotations,
|
||||
TLS: TLS{Enabled: mapping.TLS.Enabled},
|
||||
}
|
||||
|
||||
// ingressClassName := `{{ .Values.` + service.Name + `.ingress.class }}`
|
||||
ingressClassName := utils.TplValue(service.Name, "ingress.class")
|
||||
|
||||
servicePortName := utils.GetServiceNameByPort(int(*mapping.Port))
|
||||
ingressService := &networkv1.IngressServiceBackend{
|
||||
Name: fullName,
|
||||
Port: networkv1.ServiceBackendPort{},
|
||||
}
|
||||
if servicePortName != "" {
|
||||
ingressService.Port.Name = servicePortName
|
||||
} else {
|
||||
ingressService.Port.Number = *mapping.Port
|
||||
}
|
||||
|
||||
ing := &Ingress{
|
||||
service: &service,
|
||||
appName: appName,
|
||||
Ingress: &networkv1.Ingress{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Ingress",
|
||||
APIVersion: "networking.k8s.io/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fullName,
|
||||
Labels: GetLabels(serviceName, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Spec: networkv1.IngressSpec{
|
||||
IngressClassName: &ingressClassName,
|
||||
Rules: []networkv1.IngressRule{
|
||||
{
|
||||
Host: utils.TplValue(serviceName, "ingress.host"),
|
||||
IngressRuleValue: networkv1.IngressRuleValue{
|
||||
HTTP: &networkv1.HTTPIngressRuleValue{
|
||||
Paths: []networkv1.HTTPIngressPath{
|
||||
{
|
||||
Path: utils.TplValue(serviceName, "ingress.path"),
|
||||
PathType: &pathType,
|
||||
Backend: networkv1.IngressBackend{
|
||||
Service: ingressService,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: []networkv1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{
|
||||
`{{ tpl .Values.` + serviceName + `.ingress.host . }}`,
|
||||
},
|
||||
SecretName: `{{ .Values.` + serviceName + `.ingress.tls.secretName | default $tlsname }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return ing
|
||||
}
|
||||
|
||||
func (ingress *Ingress) Filename() string {
|
||||
return ingress.service.Name + ".ingress.yaml"
|
||||
}
|
||||
|
||||
func (ingress *Ingress) Yaml() ([]byte, error) {
|
||||
var ret []byte
|
||||
var err error
|
||||
if ret, err = ToK8SYaml(ingress); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceName := ingress.service.Name
|
||||
|
||||
ret = UnWrapTPL(ret)
|
||||
|
||||
lines := strings.Split(string(ret), "\n")
|
||||
|
||||
// first pass, wrap the tls part with `{{- if .Values.serviceName.ingress.tlsEnabled -}}`
|
||||
// and `{{- end -}}`
|
||||
|
||||
from, to, spaces := -1, -1, -1
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "tls:") {
|
||||
from = i
|
||||
spaces = utils.CountStartingSpaces(line)
|
||||
continue
|
||||
}
|
||||
if from > -1 {
|
||||
if utils.CountStartingSpaces(line) >= spaces {
|
||||
to = i
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if from > -1 && to > -1 {
|
||||
lines[from] = strings.Repeat(" ", spaces) +
|
||||
`{{- if .Values.` + serviceName + `.ingress.tls.enabled }}` +
|
||||
"\n" +
|
||||
lines[from]
|
||||
lines[to] = strings.Repeat(" ", spaces) + `{{ end -}}`
|
||||
}
|
||||
|
||||
out := []string{
|
||||
`{{- if .Values.` + serviceName + `.ingress.enabled -}}`,
|
||||
`{{- $fullname := include "` + ingress.appName + `.fullname" . -}}`,
|
||||
`{{- $tlsname := printf "%s-%s-tls" $fullname "` + ingress.service.Name + `" -}}`,
|
||||
}
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "loadBalancer: ") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, "labels:") {
|
||||
// add annotations above labels from values.yaml
|
||||
content := `` +
|
||||
` {{- if .Values.` + serviceName + `.ingress.annotations -}}` + "\n" +
|
||||
` {{- toYaml .Values.` + serviceName + `.ingress.annotations | nindent 4 }}` + "\n" +
|
||||
` {{- end }}` + "\n" +
|
||||
line
|
||||
|
||||
out = append(out, content)
|
||||
} else if strings.Contains(line, "ingressClassName: ") {
|
||||
content := utils.Wrap(
|
||||
line,
|
||||
`{{- if ne .Values.`+serviceName+`.ingress.class "-" }}`,
|
||||
`{{- end }}`,
|
||||
)
|
||||
out = append(out, content)
|
||||
} else {
|
||||
out = append(out, line)
|
||||
}
|
||||
}
|
||||
out = append(out, `{{- end -}}`)
|
||||
ret = []byte(strings.Join(out, "\n"))
|
||||
return ret, nil
|
||||
}
|
128
generator/ingress_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/networking/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestSimpleIngress(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
labels:
|
||||
%s/ingress: |-
|
||||
hostname: my.test.tld
|
||||
port: 80
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(
|
||||
t,
|
||||
"-s", "templates/web/ingress.yaml",
|
||||
"--set", "web.ingress.enabled=true",
|
||||
)
|
||||
ingress := v1.Ingress{}
|
||||
if err := yaml.Unmarshal([]byte(output), &ingress); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
if len(ingress.Spec.Rules) != 1 {
|
||||
t.Errorf("Expected 1 rule, got %d", len(ingress.Spec.Rules))
|
||||
}
|
||||
if ingress.Spec.Rules[0].Host != "my.test.tld" {
|
||||
t.Errorf("Expected host to be my.test.tld, got %s", ingress.Spec.Rules[0].Host)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLS(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
labels:
|
||||
%s/ingress: |-
|
||||
hostname: my.test.tld
|
||||
port: 80
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(
|
||||
t,
|
||||
"-s", "templates/web/ingress.yaml",
|
||||
"--set", "web.ingress.enabled=true",
|
||||
)
|
||||
ingress := v1.Ingress{}
|
||||
if err := yaml.Unmarshal([]byte(output), &ingress); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
// find the tls section
|
||||
tls := ingress.Spec.TLS
|
||||
if len(tls) != 1 {
|
||||
t.Errorf("Expected 1 tls section, got %d", len(tls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSName(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
labels:
|
||||
%s/ingress: |-
|
||||
hostname: my.test.tld
|
||||
port: 80
|
||||
`
|
||||
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(
|
||||
t,
|
||||
"-s",
|
||||
"templates/web/ingress.yaml",
|
||||
"--set", "web.ingress.enabled=true",
|
||||
"--set", "web.ingress.tls.secretName=mysecret",
|
||||
)
|
||||
ingress := v1.Ingress{}
|
||||
if err := yaml.Unmarshal([]byte(output), &ingress); err != nil {
|
||||
t.Errorf(unmarshalError, err)
|
||||
}
|
||||
// find the tls section
|
||||
tls := ingress.Spec.TLS
|
||||
if len(tls) != 1 {
|
||||
t.Errorf("Expected 1 tls section, got %d", len(tls))
|
||||
}
|
||||
if tls[0].SecretName != "mysecret" {
|
||||
t.Errorf("Expected secretName to be mysecret, got %s", tls[0].SecretName)
|
||||
}
|
||||
}
|
10
generator/katenaryfile/doc.go
Normal file
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
Package katenaryfile is a package for reading and writing katenary files.
|
||||
|
||||
A katenary file, named "katenary.yml" or "katenary.yaml", is a file where you can define the
|
||||
configuration of the conversion avoiding the use of labels in the compose file.
|
||||
|
||||
Formely, the file describe the same structure as in labels, and so that can be validated and
|
||||
completed by LSP. It also ease the use of katenary.
|
||||
*/
|
||||
package katenaryfile
|
165
generator/katenaryfile/main.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package katenaryfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
"katenary/generator/labels/labelstructs"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/invopop/jsonschema"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var allowedKatenaryYamlFileNames = []string{"katenary.yaml", "katenary.yml"}
|
||||
|
||||
// StringOrMap is a struct that can be either a string or a map of strings.
|
||||
// It's a helper struct to unmarshal the katenary.yaml file and produce the schema
|
||||
type StringOrMap any
|
||||
|
||||
// Service is a struct that contains the service configuration for katenary
|
||||
type Service struct {
|
||||
MainApp *bool `json:"main-app,omitempty" jsonschema:"title=Is this service the main application"`
|
||||
Values []StringOrMap `json:"values,omitempty" jsonschema:"description=Environment variables to be set in values.yaml with or without a description"`
|
||||
Secrets *labelstructs.Secrets `json:"secrets,omitempty" jsonschema:"title=Secrets,description=Environment variables to be set as secrets"`
|
||||
Ports *labelstructs.Ports `json:"ports,omitempty" jsonschema:"title=Ports,description=Ports to be exposed in services"`
|
||||
Ingress *labelstructs.Ingress `json:"ingress,omitempty" jsonschema:"title=Ingress,description=Ingress configuration"`
|
||||
HealthCheck *labelstructs.HealthCheck `json:"health-check,omitempty" jsonschema:"title=Health Check,description=Health check configuration that respects the kubernetes api"`
|
||||
SamePod *string `json:"same-pod,omitempty" jsonschema:"title=Same Pod,description=Service that should be in the same pod"`
|
||||
Description *string `json:"description,omitempty" jsonschema:"title=Description,description=Description of the service that will be injected in the values.yaml file"`
|
||||
Ignore *bool `json:"ignore,omitempty" jsonschema:"title=Ignore,description=Ignore the service in the conversion"`
|
||||
Dependencies []labelstructs.Dependency `json:"dependencies,omitempty" jsonschema:"title=Dependencies,description=Services that should be injected in the Chart.yaml file"`
|
||||
ConfigMapFile *labelstructs.ConfigMapFile `json:"configmap-files,omitempty" jsonschema:"title=ConfigMap Files,description=Files that should be injected as ConfigMap"`
|
||||
MapEnv *labelstructs.MapEnv `json:"map-env,omitempty" jsonschema:"title=Map Env,description=Map environment variables to another value"`
|
||||
CronJob *labelstructs.CronJob `json:"cron-job,omitempty" jsonschema:"title=Cron Job,description=Cron Job configuration"`
|
||||
EnvFrom *labelstructs.EnvFrom `json:"env-from,omitempty" jsonschema:"title=Env From,description=Inject environment variables from another service"`
|
||||
ExchangeVolumes []*labelstructs.ExchangeVolume `json:"exchange-volumes,omitempty" jsonschema:"title=Exchange Volumes,description=Exchange volumes between services"`
|
||||
ValuesFrom *labelstructs.ValueFrom `json:"values-from,omitempty" jsonschema:"title=Values From,description=Inject values from another service (secret or configmap environment variables)"`
|
||||
}
|
||||
|
||||
// OverrideWithConfig overrides the project with the katenary.yaml file. It
|
||||
// will set the labels of the services with the values from the katenary.yaml file.
|
||||
// It work in memory, so it will not modify the original project.
|
||||
func OverrideWithConfig(project *types.Project) {
|
||||
var yamlFile string
|
||||
var err error
|
||||
for _, yamlFile = range allowedKatenaryYamlFileNames {
|
||||
_, err = os.Stat(yamlFile)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// no katenary file found
|
||||
return
|
||||
}
|
||||
fmt.Println(utils.IconInfo, "Using katenary file", yamlFile)
|
||||
|
||||
services := make(map[string]Service)
|
||||
fp, err := os.Open(yamlFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := yaml.NewDecoder(fp).Decode(&services); err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
for i, p := range project.Services {
|
||||
name := p.Name
|
||||
if project.Services[i].Labels == nil {
|
||||
project.Services[i].Labels = make(map[string]string)
|
||||
}
|
||||
mustGetLabelContent := func(o any, s *types.ServiceConfig, labelName string) {
|
||||
err := getLabelContent(o, s, labelName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if s, ok := services[name]; ok {
|
||||
mustGetLabelContent(s.MainApp, &project.Services[i], labels.LabelMainApp)
|
||||
mustGetLabelContent(s.Values, &project.Services[i], labels.LabelValues)
|
||||
mustGetLabelContent(s.Secrets, &project.Services[i], labels.LabelSecrets)
|
||||
mustGetLabelContent(s.Ports, &project.Services[i], labels.LabelPorts)
|
||||
mustGetLabelContent(s.Ingress, &project.Services[i], labels.LabelIngress)
|
||||
mustGetLabelContent(s.HealthCheck, &project.Services[i], labels.LabelHealthCheck)
|
||||
mustGetLabelContent(s.SamePod, &project.Services[i], labels.LabelSamePod)
|
||||
mustGetLabelContent(s.Description, &project.Services[i], labels.LabelDescription)
|
||||
mustGetLabelContent(s.Ignore, &project.Services[i], labels.LabelIgnore)
|
||||
mustGetLabelContent(s.Dependencies, &project.Services[i], labels.LabelDependencies)
|
||||
mustGetLabelContent(s.ConfigMapFile, &project.Services[i], labels.LabelConfigMapFiles)
|
||||
mustGetLabelContent(s.MapEnv, &project.Services[i], labels.LabelMapEnv)
|
||||
mustGetLabelContent(s.CronJob, &project.Services[i], labels.LabelCronJob)
|
||||
mustGetLabelContent(s.EnvFrom, &project.Services[i], labels.LabelEnvFrom)
|
||||
mustGetLabelContent(s.ExchangeVolumes, &project.Services[i], labels.LabelExchangeVolume)
|
||||
mustGetLabelContent(s.ValuesFrom, &project.Services[i], labels.LabelValueFrom)
|
||||
}
|
||||
}
|
||||
fmt.Println(utils.IconInfo, "Katenary file loaded successfully, the services are now configured.")
|
||||
}
|
||||
|
||||
func getLabelContent(o any, service *types.ServiceConfig, labelName string) error {
|
||||
if reflect.ValueOf(o).IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := yaml.Marshal(o)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
val := strings.TrimSpace(string(c))
|
||||
if labelName == labels.LabelIngress {
|
||||
// special case, values must be set from some defaults
|
||||
ing, err := labelstructs.IngressFrom(val)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
c, err := yaml.Marshal(ing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val = strings.TrimSpace(string(c))
|
||||
}
|
||||
|
||||
service.Labels[labelName] = val
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateSchema generates the schema for the katenary.yaml file.
|
||||
func GenerateSchema() string {
|
||||
s := jsonschema.Reflect(map[string]Service{})
|
||||
|
||||
// redefine the IntOrString type from k8s
|
||||
s.Definitions["IntOrString"] = &jsonschema.Schema{
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Type: "integer"},
|
||||
{Type: "string"},
|
||||
},
|
||||
}
|
||||
|
||||
// same for the StringOrMap type, that can be either a string or a map of string:string
|
||||
s.Definitions["StringOrMap"] = &jsonschema.Schema{
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Type: "string"},
|
||||
{Type: "object", AdditionalProperties: &jsonschema.Schema{Type: "string"}},
|
||||
},
|
||||
}
|
||||
|
||||
c, _ := s.MarshalJSON()
|
||||
// indent the json
|
||||
var out bytes.Buffer
|
||||
err := json.Indent(&out, c, "", " ")
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
125
generator/katenaryfile/main_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package katenaryfile
|
||||
|
||||
import (
|
||||
"katenary/generator/labels"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
)
|
||||
|
||||
func TestBuildSchema(t *testing.T) {
|
||||
sh := GenerateSchema()
|
||||
if len(sh) == 0 {
|
||||
t.Errorf("Expected schema to be defined")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverrideProjectWithKatenaryFile(t *testing.T) {
|
||||
composeContent := `
|
||||
services:
|
||||
webapp:
|
||||
image: nginx:latest
|
||||
`
|
||||
|
||||
katenaryfileContent := `
|
||||
webapp:
|
||||
ports:
|
||||
- 80
|
||||
`
|
||||
|
||||
// create /tmp/katenary-test-override directory, save the compose.yaml file
|
||||
tmpDir, err := os.MkdirTemp("", "katenary-test-override")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %s", err.Error())
|
||||
}
|
||||
composeFile := filepath.Join(tmpDir, "compose.yaml")
|
||||
katenaryFile := filepath.Join(tmpDir, "katenary.yaml")
|
||||
|
||||
os.MkdirAll(tmpDir, 0755)
|
||||
if err := os.WriteFile(composeFile, []byte(composeContent), 0644); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
if err := os.WriteFile(katenaryFile, []byte(katenaryfileContent), 0644); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
c, _ := os.ReadFile(composeFile)
|
||||
log.Println(string(c))
|
||||
|
||||
// chand dir to this directory
|
||||
os.Chdir(tmpDir)
|
||||
options, _ := cli.NewProjectOptions(nil,
|
||||
cli.WithWorkingDirectory(tmpDir),
|
||||
cli.WithDefaultConfigPath,
|
||||
)
|
||||
project, err := cli.ProjectFromOptions(options)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create project from options: %s", err.Error())
|
||||
}
|
||||
|
||||
OverrideWithConfig(project)
|
||||
w := project.Services[0].Labels
|
||||
if v, ok := w[labels.LabelPorts]; !ok {
|
||||
t.Fatal("Expected ports to be defined", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverrideProjectWithIngress(t *testing.T) {
|
||||
composeContent := `
|
||||
services:
|
||||
webapp:
|
||||
image: nginx:latest
|
||||
`
|
||||
|
||||
katenaryfileContent := `
|
||||
webapp:
|
||||
ports:
|
||||
- 80
|
||||
ingress:
|
||||
port: 80
|
||||
`
|
||||
|
||||
// create /tmp/katenary-test-override directory, save the compose.yaml file
|
||||
tmpDir, err := os.MkdirTemp("", "katenary-test-override")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %s", err.Error())
|
||||
}
|
||||
composeFile := filepath.Join(tmpDir, "compose.yaml")
|
||||
katenaryFile := filepath.Join(tmpDir, "katenary.yaml")
|
||||
|
||||
os.MkdirAll(tmpDir, 0755)
|
||||
if err := os.WriteFile(composeFile, []byte(composeContent), 0644); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
if err := os.WriteFile(katenaryFile, []byte(katenaryfileContent), 0644); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
c, _ := os.ReadFile(composeFile)
|
||||
log.Println(string(c))
|
||||
|
||||
// chand dir to this directory
|
||||
os.Chdir(tmpDir)
|
||||
options, _ := cli.NewProjectOptions(nil,
|
||||
cli.WithWorkingDirectory(tmpDir),
|
||||
cli.WithDefaultConfigPath,
|
||||
)
|
||||
project, err := cli.ProjectFromOptions(options)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create project from options: %s", err.Error())
|
||||
}
|
||||
|
||||
OverrideWithConfig(project)
|
||||
w := project.Services[0].Labels
|
||||
if v, ok := w[labels.LabelPorts]; !ok {
|
||||
t.Fatal("Expected ports to be defined", v)
|
||||
}
|
||||
if v, ok := w[labels.LabelIngress]; !ok {
|
||||
t.Fatal("Expected ingress to be defined", v)
|
||||
}
|
||||
}
|
34
generator/labels.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/generator/labels"
|
||||
)
|
||||
|
||||
var componentLabel = labels.LabelName("component")
|
||||
|
||||
// GetLabels returns the labels for a service. It uses the appName to replace the __replace__ in the labels.
|
||||
// This is used to generate the labels in the templates.
|
||||
func GetLabels(serviceName, appName string) map[string]string {
|
||||
labels := map[string]string{
|
||||
componentLabel: serviceName,
|
||||
}
|
||||
|
||||
key := `{{- include "%s.labels" . | nindent __indent__ }}`
|
||||
labels[`__replace_`+serviceName] = fmt.Sprintf(key, appName)
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
// GetMatchLabels returns the matchLabels for a service. It uses the appName to replace the __replace__ in the labels.
|
||||
// This is used to generate the matchLabels in the templates.
|
||||
func GetMatchLabels(serviceName, appName string) map[string]string {
|
||||
labels := map[string]string{
|
||||
componentLabel: serviceName,
|
||||
}
|
||||
|
||||
key := `{{- include "%s.selectorLabels" . | nindent __indent__ }}`
|
||||
labels[`__replace_`+serviceName] = fmt.Sprintf(key, appName)
|
||||
|
||||
return labels
|
||||
}
|
2
generator/labels/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package labels provides functionality to parse and manipulate labels.
|
||||
package labels
|
14
generator/labels/help-template.md.tpl
Normal file
@@ -0,0 +1,14 @@
|
||||
## {{ .KatenaryPrefix }}/{{ .Name }}
|
||||
|
||||
{{ .Help.Short }}
|
||||
|
||||
**Type**: `{{ .Help.Type }}`
|
||||
|
||||
{{ .Help.Long }}
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
{{ .Help.Example }}
|
||||
```
|
||||
|