453 Commits

Author SHA1 Message Date
6c2e8fa9b2 chore(typo): remove invisible characters
All checks were successful
/ build-packages (amd64, darwin) (push) Successful in 1m4s
/ build-packages (amd64, freebsd) (push) Successful in 1m52s
/ build-packages (amd64, linux) (push) Successful in 2m15s
/ build-packages (amd64, windows) (push) Successful in 1m24s
/ build-packages (arm64, freebsd) (push) Successful in 1m48s
/ build-packages (arm64, linux) (push) Successful in 2m13s
Build and push OCI image / build (push) Successful in 1m57s
Go-Tests / tests (push) Successful in 1m31s
Go-Tests / sonar (push) Successful in 42s
2025-08-22 14:06:00 +02:00
46f41ea01d Enhance versionning on build packages (#165)
Some checks failed
Go-Tests / sonar (push) Has been cancelled
Go-Tests / tests (push) Has been cancelled
It was too challenging to only use "tags" without a prefix to trigger builds. So, the new rule is to prefix releases tags by "releases/"

Reviewed-on: #165
2025-08-22 14:03:36 +02:00
d30690a45a packages(actios): extract version from releases tags
All checks were successful
Go-Tests / tests (push) Successful in 2m2s
Go-Tests / sonar (push) Successful in 43s
2025-08-22 10:09:10 +02:00
00409472b7 chore(packages): Make better packages
All checks were successful
Go-Tests / tests (push) Successful in 1m30s
Go-Tests / sonar (push) Successful in 52s
- we need man page in docs to build on CI/CD platform
- new workflows to build packages and push them to the repository
2025-08-21 17:55:28 +02:00
8174597c55 More visible message
All checks were successful
Go-Tests / tests (push) Successful in 2m0s
Go-Tests / sonar (push) Successful in 52s
2025-08-20 14:35:03 +02:00
ea11563ee0 feat(repo): needed to checkout
All checks were successful
Go-Tests / tests (push) Successful in 1m51s
Go-Tests / sonar (push) Successful in 51s
2025-08-20 12:24:58 +02:00
2de50f5158 feat(doc): Change repo to katenary.io
All checks were successful
Go-Tests / tests (push) Successful in 1m59s
Go-Tests / sonar (push) Successful in 52s
2025-08-20 12:16:14 +02:00
e3951b144e feat(repo): Modify github on push
- Close all PR
- Add mirror information in README.md
2025-08-20 12:14:54 +02:00
94792121a2 Merge pull request 'fix(generator): use args and command' (#164) from fixes/bug-163 into master
All checks were successful
Go-Tests / tests (push) Successful in 1m50s
Go-Tests / sonar (push) Successful in 50s
Reviewed-on: #164
2025-08-20 07:42:02 +00:00
b7a036a3dd fix(generator): use args and command
All checks were successful
Go-Tests / tests (pull_request) Successful in 1m57s
Go-Tests / sonar (pull_request) Successful in 47s
Common mistake...

- Docker and Podman uses "command" as argument to "entrypoint"
- Kubernetes uses "command" as entrypoint and "args" as argument

So, we needed to use Command and Args in all related objects.

Fixes #163
2025-08-20 09:37:36 +02:00
d1f2957512 Merge pull request 'feat-move-to-gitea' (#162) from feat-move-to-gitea into master
All checks were successful
Go-Tests / tests (push) Successful in 1m44s
Go-Tests / sonar (push) Successful in 48s
Reviewed-on: #162
2025-08-19 22:06:28 +00:00
95609a3092 feat(repo): use the katenary.io URL
All checks were successful
Go-Tests / tests (pull_request) Successful in 2m0s
Go-Tests / sonar (pull_request) Successful in 57s
2025-08-19 23:58:51 +02:00
8e54d611eb feat(doc): Remove other github references 2025-08-19 23:58:26 +02:00
ef1ff2737f feat(repo): Change references to katenary repository 2025-08-19 23:36:48 +02:00
d34f97fa5d feat(repo): Go to repo.katenary.io 2025-08-19 23:23:17 +02:00
c85e61f5d0 Merge branch 'master' of repo.katenary.io:Katenary/katenary
All checks were successful
Go-Tests / tests (push) Successful in 1m49s
Go-Tests / sonar (push) Successful in 49s
2025-08-19 23:20:12 +02:00
f1c31b0eeb feat(doc): move some things in README
As we moved project to repo.katenary.io, badges were not OK
2025-08-19 22:31:50 +02:00
677b0d7b3c Merge pull request 'feat(actions): Change action to Gitea' (#161) from test-workflow into master
All checks were successful
Go-Tests / tests (push) Successful in 1m59s
Go-Tests / sonar (push) Successful in 1m7s
Reviewed-on: #161
2025-08-19 19:59:17 +00:00
f539be8f52 Remove proxy
All checks were successful
Go-Tests / tests (pull_request) Successful in 2m29s
Go-Tests / sonar (pull_request) Successful in 51s
2025-08-19 21:54:01 +02:00
4336490a72 feat(actions): Change action to Gitea
- Build and packages on Gitea action
- Use go 1.25
- Update dependencies
2025-08-19 21:52:33 +02:00
9f75f874d1 Features/buildah build (#159)
* feat(oci): Build using pure Buildah
2025-08-13 10:20:48 +02:00
90eda75219 feat(oci): Use Buildah
And set version the right way...
2025-08-04 16:18:35 +02:00
b13e922966 feat(package) Version were not correctly set
As the code moved in "internal" package, we need to set the version to
the new location
2025-08-04 16:13:35 +02:00
0d36a48f93 fix(securty): Fix security alert
Code scanning alert: we need to specify permissions on this action.
2025-08-03 23:29:39 +02:00
60016b8c3e Merge pull request #157 from Katenary/features/oci-image
Features/oci image
2025-08-03 23:06:06 +02:00
07263520b6 feat(oci): remove testing on push branches 2025-08-03 23:01:54 +02:00
0f3812b666 feat(oci): Add OCI image generation
Not fully tested but it should produce an image in ghcr.io
2025-08-03 22:54:19 +02:00
50a9d0ddde feat(md): fixes typo and rules 2025-08-03 22:53:53 +02:00
f5d3005c3b Merge pull request #156 from Katenary/feat-move-to-internal
feat(refacto): move everything in internal package
2025-08-03 16:03:17 +02:00
76b3b83d07 feat(workflow): change when to make CI
We will abandon develop branch for a while. It's better to work with
feature branches.
2025-08-03 15:59:00 +02:00
14ca5bf0ea feat(refacto): move everything in internal package
This allows to install katenary with `go install` and to clean up the
project folder.
2025-08-03 15:54:58 +02:00
d1768e5742 Merge pull request #155 from Katenary/develop
Develop
2025-08-03 14:20:17 +02:00
0fcf0f60e8 chore(modules): Update versions 2025-08-03 14:15:55 +02:00
e9ad85a0ac chore(make): Split and enhancements
- split Makefile in several sub files
- use a separated container for tests
-
2025-08-03 14:15:35 +02:00
b4b122fe7f Do not ignore cmd/katenary directory 2025-08-03 14:14:29 +02:00
6aaeda7a3c chore(modernize): use iterator 2025-08-03 14:13:57 +02:00
0b3f7b2b5c chore(code): remove dead code 2025-08-03 14:13:42 +02:00
e9e7c5f7b5 chore(test): fix concurrency problem 2025-08-03 14:12:49 +02:00
0df53ed5ae Merge pull request #154 from Katenary/develop
fix(configmap): File from the root of project ends by a dash
2025-08-03 13:16:21 +02:00
136478aff7 fix(configmap): File from the root of project ends by a dash
fixes #150

If we mount a file from the root of project, the names ends by a dash.
That makes helm failing to install the package.
2025-08-03 10:20:11 +02:00
5b812b30f7 Merge pull request #146 from Katenary/develop
fix(katenaryfile): Schema is broken without json annotation
2025-07-15 21:25:39 +02:00
f2b5c16e71 fix(katenaryfile): Schema is broken without json annotation
We need to add json annotation. The yaml annoation is not enought when
generating the schema.
2025-07-15 21:24:17 +02:00
df1f29c0ff Merge pull request #145 from Katenary/develop
fix(katenaryfile): Fix parsing katenary file
2025-07-15 21:18:45 +02:00
afbd6fc1ac fix(katenaryfile): Fix parsing katenary file
It seems that "json" annotation fails to get dashed names (like
configmap-files). That means that all labels in katernay.yaml file
couldn't be parsed.

Using `yaml` annotaiton fixes the problem. Fixes #144.
2025-07-15 21:13:22 +02:00
327e0aa799 Merge pull request #142 from Katenary/develop
chore(make): manpage requirements are now in requirements
2025-07-15 13:58:48 +02:00
b7e948954b chore(make): manpage requirements are now in requirements 2025-07-15 13:57:35 +02:00
93c0658c19 Merge pull request #141 from Katenary/develop
Develop
2025-07-15 13:53:20 +02:00
bdf74f2deb chore(cleanup): removed unecessary file 2025-07-15 13:51:21 +02:00
2435bb9f8a chore(doc): Rebuild label list and description 2025-07-15 13:50:07 +02:00
5ee38dcf7f Merge pull request #140 from Katenary/develop
Develop
2025-07-15 13:45:38 +02:00
a754dc9275 fix(make): no need to auto retrieve key now 2025-07-15 13:44:25 +02:00
732e080da3 Merge branch 'master' into develop 2025-07-15 13:42:15 +02:00
22e1ebdce1 chore(make): huge refactor, add more tests
- more test on gpg signature
- change build target in the help message
- refactor upx compression
-
2025-07-15 13:41:49 +02:00
e4280f2c4e chore(build): Fixes cache, add better cleanup, add PHONY 2025-07-15 10:27:24 +02:00
a99609d21d Merge pull request #139 from Katenary/develop
fix(doc): Fix documentation linkg
2025-07-13 15:41:01 +02:00
e55658d382 fix(doc): Fix documentation linkg
No translation, so remove "en" from the url.
2025-07-13 15:40:30 +02:00
9dbe9e1114 Merge pull request #138 from Katenary/develop
Develop
2025-07-13 15:37:31 +02:00
eea198786a fix(doc): Readthedocs needs manpage plugin
A pitty, I wanted to leave it separated
2025-07-13 15:29:16 +02:00
da9c0a7b58 chore(doc): regenerate and fixes a typo 2025-07-13 15:16:39 +02:00
d582b884bf chore(rules): Avoid mardownlint to complain 2025-07-13 15:16:20 +02:00
aa3e4c64ee chore(ignore): ignoring binary in the current directory 2025-07-13 15:08:32 +02:00
ce479629f6 fix(service): Fixes naming service ports
- ports should be named "port-XXX" with XXX to be the port number if the
port name cannot be discovered
- it follows what we do in Deployment objects
- this should fix #132
2025-07-13 15:02:26 +02:00
89fd516b20 Merge pull request #137 from Katenary/develop
Develop
2025-07-13 00:53:45 +02:00
8bbb991afb chore(doc): Move to new repository 2025-07-13 00:43:21 +02:00
4c5c9ba553 chore(doc): Move to new repository 2025-07-13 00:39:25 +02:00
ef1511d4f7 Merge pull request #134 from Katenary/develop
Develop
2025-07-13 00:21:15 +02:00
a290219c81 chore(licence): I prefer to mention the project 2025-07-13 00:19:02 +02:00
c4e406f855 chore(moving): Moved repository to organization 2025-07-13 00:17:48 +02:00
2beac5f4dd Merge pull request #135 from Katenary/issue-133
fix(deployment): Missed the command from compose file
2025-07-13 00:10:22 +02:00
5f5cd0268a fix(deployment): Missed the command from compose file
Fixes #133
2025-07-13 00:07:07 +02:00
796cbb6d30 chore(package): Rewrite package targets
- rewrite to make packages building more consistent
- add rpm signature with GPG
- auto test packages in several distributions to ensure the packages are
OK

Even if loops to build stuffs are sometimes easier to automate, it's
simpler to fixup if some rules (targets) are explicitly defined.
2025-07-12 09:12:12 +02:00
39ebf7eb78 chore(package): change description, add a final dot 2025-07-12 09:08:41 +02:00
a1c5161ff7 chore(package): Be able to sign RPM 2025-07-12 09:08:28 +02:00
2307ad667e chore(dev): changed how to test version
- force release version check to use "v" (or not) as first character
- one line condtion
2025-07-11 09:17:33 +02:00
a26e832fed fix(doc): recompiled documentation 2025-07-11 09:16:43 +02:00
8582010d90 fix(versionning): Fix version
Version should be x.y.z for releases. It makes things homogenous for
debian version (that needs a number as first char) and it is
semantically better.

At this time, the Makefile is OK. Maybe we don't need `PKG_VERSION`
later.
2025-07-10 15:15:25 +02:00
b7186eb8b4 chore(makefile): Refacto
Refactor the makefile:

- use the "package" container image to build windows installer
- cleanup and reorder targets
- enhance the way we named packages (add version and architecture to the
name)
2025-07-10 08:13:49 +02:00
0f3528818f chore(package): Add NSIS to the package image
This container image can make Windows installer too.
2025-07-10 08:11:39 +02:00
9fadc77979 chore(package): Add application in registry
Allow the application to appear in "Install/Uninstall" Windows
parameters.
2025-07-10 08:10:11 +02:00
13d231a62c feat(package): Add RPM, deb, pacman and tar packages and manpage
Build package using fpm. As I don't want ruby/gem in my computer, the
build system creates an OCI image with Podman (at this time), and
generate dist pacakges.

To enhance the packages, a manpage is now generated from the
documentation.
2025-07-09 14:11:46 +02:00
6cbaa06bec chore(license): Change date and copyright
I keep MIT license, just changed date and pseudo
2025-07-07 23:50:03 +02:00
1552e9c877 Merge pull request #131 from metal3d/develop
Develop
2025-07-07 23:02:13 +02:00
3b2cd37ea4 chore(test): add tests 2025-07-07 22:48:13 +02:00
4c566cbc1a fix(doc): Fix function doc 2025-07-07 22:47:59 +02:00
69c528e5e0 chore(doc): Enhance documentation, fix typo 2025-07-07 22:10:27 +02:00
42bf35593f chore(makefile): rewrite correctly some rules
- cleanup
- sign and check gpg signature
- fixup NSIS installer method
- also sign windows installer
2025-07-07 21:58:15 +02:00
4e739c5f08 fix(doc): Fixing duplicated package 2025-07-07 21:57:14 +02:00
f75623b4ff chore(nsis): sort language
Sort all languages, excepting the first one wich is the default language
2025-07-07 21:31:58 +02:00
d82b5e4a98 feat(nsis): Add more languages, files and license page
- Include maximum of language for the Windows installer
- Add licence page to inform the user that Katenary is free (as freedom)
2025-07-07 18:18:20 +02:00
42c738211c fix(build): Fixing build, do not use alpine
Alpine image seems to break the Windows binary (striped by error) and
makes katenary unusable.
2025-07-07 17:59:16 +02:00
c54bc35c9f feat(nsis): Create installer for Windows
Use nsis to create an installer for Windows user. It installs the binary
for the user (for now) and set the Path to access katenary binary from
cmd or Powershell.
2025-07-07 17:58:21 +02:00
1f406dc558 Merge pull request #130 from metal3d/develop
fix(build): Windows is a shame
2025-07-07 13:50:31 +02:00
0c0b50b0df fix(build): Windows is a shame
- Windows defender says that UPX compressed files are virus
(https://github.com/upx/upx/issues/437) so I stop using this for
Windows. Sorry, you will download a huge binary
- Windows, one more time, stop making the binary working when it's
stripped. Then, one more time, I cannot reduce the binary size

I'm thinking very seriously about no longer offering support for Windows
and letting people compile Katenary on their own.
2025-07-07 13:47:27 +02:00
6efee2fbae Merge pull request #129 from metal3d/develop
Many fixes on code quality and CI
2025-07-06 15:37:08 +02:00
47e149e20b chore(doc): Regenerated doc 2025-07-06 15:36:43 +02:00
d163700147 feat(tests): Add tests for label structures 2025-07-06 15:31:21 +02:00
fa3befaa88 fix(tests): Fixing unbound error 2025-07-06 15:31:05 +02:00
e5b7c0d550 fix(doc): Respect Go recommandation in docstring 2025-07-06 15:30:31 +02:00
9e4663cc6e chore(convention): Respect Go convention, package name should be lowercase 2025-07-06 14:34:16 +02:00
1a5c95d240 fix(ci): typo in permission block 2025-07-06 14:26:20 +02:00
21845c5de1 fix(ci): typo in permission block 2025-07-06 14:24:12 +02:00
4a91d458fd fix(ci): try to add permission block 2025-07-06 14:21:56 +02:00
a52b20efab Merge pull request #128 from metal3d/feat-change-actions
Feat change actions
2025-07-06 14:16:38 +02:00
02c4c5168b feat(ci): Changes CI
- use SonarQube action
- remove the helm installation as it is already present
- changes types and branches activation
2025-07-06 14:13:23 +02:00
7b875454cb feat(chore): use range over int
More readable range
2025-07-06 11:53:56 +02:00
2da5d9df08 feat(chore): modernize code 2025-07-06 11:53:24 +02:00
a0eb3c0bb6 fix(doc): Fix doc string 2025-07-06 11:52:48 +02:00
7e76543c48 fix(doc): Fix the docstring following Go recommandation 2025-07-06 11:41:19 +02:00
09c2c86d59 fix(permission): globalize and fixes
Permission alert frop OpenGrep is wrong, as the directory must use 0755.
To make things working and to ease futur changes, I set the default
permission in a constant.
2025-07-06 11:39:19 +02:00
e58948bb44 feat(code): use better check on empty string 2025-07-06 10:53:18 +02:00
a00b03b2aa feat(chore): Add SAST with opengrep 2025-07-06 10:52:56 +02:00
b47b956798 fix(install): Enhance checks and code
- Opengrep complains about non quoted strings
- Bad indentation fixed
2025-07-06 10:52:35 +02:00
a409a1347e feat(chore): rename variable 2025-07-06 10:51:25 +02:00
8999aabc21 fix(security): Change access rights on generated directories
Ppengrep complains, it is right.
2025-07-06 10:51:09 +02:00
14e8907437 feat(doc): Add package doc for labels 2025-07-06 10:49:54 +02:00
740c400b9a Merge pull request #127 from metal3d/develop
feat(doc): regenerate the package docs
2025-07-04 15:05:34 +02:00
7cd38bbd23 feat(doc): regenerate the package docs 2025-07-04 14:59:49 +02:00
8c509b5bff Merge pull request #126 from metal3d/develop
Merge branch 'master' into develop
2025-07-04 14:53:00 +02:00
28b22a0b30 Merge branch 'master' into develop 2025-07-04 14:49:35 +02:00
748d0bf1ea Merge pull request #120 from metal3d/develop 2025-06-27 00:27:49 +02:00
130e6d4e24 chore(dependencies): Update dependencies 2025-06-26 23:57:44 +02:00
a66fec07e1 chore(optim): Optimizing some piece of code
Simply use modern methods
2025-06-26 23:57:19 +02:00
a3d1e9342f fix(doc): Follow Go recommendations 2025-06-26 23:56:06 +02:00
72bc88661a fix(convension): Fix APIVersion
Sonarlint complains about "ApiVersion"
2025-06-26 23:37:20 +02:00
e2b897eb9d fix(generation): Fix container name
Container names were built using the service name. We didn't checked the
name and leave underscores inside.

This commit does:
- fix all service and rename containers (`container_name`)
- use `ContainerName` everywhere we need to get the container by name

See #106
2025-06-26 23:37:20 +02:00
9fcce059e5 fix(doc): Fixing method name in comment 2025-06-26 23:18:11 +02:00
36f6413917 feat(build): UPX compression
UPX can compress binaries from 22Mo to 7Mo on Linux / Windows and 14Mo
on MacOS. Let's use it to reduce binary size.

I don't know why it doesn't work on FreeBSD.
2025-06-15 16:02:45 +02:00
8c729f3c57 fix(build): remove "-it" options
Building in parallel make a warn message as TTY is not OK
2025-06-15 16:01:16 +02:00
063cc9d439 Merge pull request #119 from metal3d/develop
Develop
2025-06-15 16:14:10 +03:00
ac5317e600 feat(doc): regenerate docs 2025-06-15 14:48:35 +02:00
bc0b65006d feat(doc): Add doc target
This only regenerates docs for packages.
2025-06-15 14:48:24 +02:00
933f04bf5e feat(version): change Go version 2025-06-15 14:43:58 +02:00
7d46435ba2 Merge pull request #118 from metal3d/develop
Develop
2025-06-04 15:22:09 +02:00
d94bb8ac32 feat(doc): regenerate documentation 2025-06-04 15:18:11 +02:00
b143f743ef feat(chore): Add tests and use "any" instead of "inteface" 2025-06-04 15:17:26 +02:00
a8341a9b44 feat(tests): .Close() can be "unchecked" 2025-06-04 14:41:53 +02:00
def5d097a4 feat(tests): Fixing linter problems
Using golangci-lint
2025-06-04 14:29:13 +02:00
ba0ae1bc60 Merge pull request #117 from metal3d/develop
Develop
2025-06-04 09:51:48 +02:00
d77029b597 feat(version): update dependencies
New versions for:
- Cobra
- K8S API
- indirect subpackages
2025-06-04 09:48:52 +02:00
b5f62d43af fix(test): Fix non constant string format
Go 1.24 made several changes about formatting messages:
https://tip.golang.org/doc/go1.24#vet

These changes make tests (and vet) craching.

The fix is to use a string format and give error message as argument.
2025-06-04 09:46:13 +02:00
9220dc3278 fix(test): Fix bad test
We were failing if the test is OK...
2025-06-04 09:24:33 +02:00
36c72fb665 Merge pull request #114 from GSergeevich/readme_fix
Fix example of docker-compose.yml
2025-04-02 15:01:06 +02:00
Герман Плотников
c90504f4f1 Fix example of docker-compose.yml 2025-03-26 18:57:47 +03:00
9ef961ae7c issue(107): Really drop ignored services
It's, at this time, not needed to keep the ignored services inside the
project. Maybe later someone will ask to keep env variables, or
something like this... But at this time, it's a source of bug like #107.
2025-01-19 23:38:17 +01:00
fe6663f9f4 issue(106): Fix service names with dashes
See #106, I need to add a test on "same-pod" label.
2025-01-19 23:24:09 +01:00
1190b316bb Merge pull request #108 from metal3d/develop
Bump versions
2025-01-17 22:19:42 +01:00
7068dc229c Merge pull request #104 from metal3d/dependabot/go_modules/develop/k8s.io/api-0.32.1
chore(deps): bump k8s.io/api from 0.32.0 to 0.32.1
2025-01-17 22:12:36 +01:00
dependabot[bot]
61ce6fb25b chore(deps): bump k8s.io/api from 0.32.0 to 0.32.1
Bumps [k8s.io/api](https://github.com/kubernetes/api) from 0.32.0 to 0.32.1.
- [Commits](https://github.com/kubernetes/api/compare/v0.32.0...v0.32.1)

---
updated-dependencies:
- dependency-name: k8s.io/api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-17 21:10:36 +00:00
116ef658d8 Merge pull request #103 from metal3d/dependabot/go_modules/develop/github.com/invopop/jsonschema-0.13.0
chore(deps): bump github.com/invopop/jsonschema from 0.12.0 to 0.13.0
2025-01-17 22:09:53 +01:00
d7b354de8c Merge pull request #105 from metal3d/dependabot/go_modules/develop/k8s.io/apimachinery-0.32.1
chore(deps): bump k8s.io/apimachinery from 0.32.0 to 0.32.1
2025-01-17 22:09:28 +01:00
dependabot[bot]
d06c0574fb chore(deps): bump k8s.io/apimachinery from 0.32.0 to 0.32.1
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.32.0 to 0.32.1.
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.32.0...v0.32.1)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-16 22:31:32 +00:00
dependabot[bot]
bcd7894e3b chore(deps): bump github.com/invopop/jsonschema from 0.12.0 to 0.13.0
Bumps [github.com/invopop/jsonschema](https://github.com/invopop/jsonschema) from 0.12.0 to 0.13.0.
- [Commits](https://github.com/invopop/jsonschema/compare/v0.12.0...v0.13.0)

---
updated-dependencies:
- dependency-name: github.com/invopop/jsonschema
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-31 22:51:12 +00:00
80aba05f66 Merge pull request #101 from metal3d/develop
Enhance tests
2024-12-17 18:25:34 +01:00
41a4292939 chore(test): enhance error handling and avoid repetitions 2024-12-17 18:22:45 +01:00
131ea5d569 mod(version): update k8s.io api modules 2024-12-17 18:22:20 +01:00
e36bbf41f4 Update go-test.yaml
Use mod tidy as dependabot may change the go.mod file
2024-12-17 18:09:40 +01:00
c97b398914 Merge pull request #96 from metal3d/develop
Fixing docs
2024-12-05 09:47:29 +01:00
d27ed76cf4 doc(readme): better comments 2024-12-05 07:24:23 +01:00
17b6ea51af fix(doc): Bad variable name in secrets 2024-12-05 07:23:58 +01:00
0986f73f06 Merge pull request #95 from metal3d/develop
Add SecretName in TLS + adapt github actions
2024-12-05 07:16:33 +01:00
dcd282779f test(gh): Ordering steps...
I'm stupid... I need to upload artifacts after the creation.
2024-12-05 07:08:16 +01:00
b93a3df98c test(gh): Checkout project 2024-12-05 07:04:47 +01:00
4768330c1a test(gh): Fix job name 2024-12-05 06:55:50 +01:00
23d0afd85f test(gh): Use 2 steps
I prefer to split tests and sonar upload
2024-12-05 06:54:38 +01:00
5c939383be chore(tls): Add secretName to the values
The seret name for TLS wasn't editable, it may be useful to change it
when we generate TLS certificates for specific installation.
2024-12-05 06:43:43 +01:00
72ddb8aa74 Merge pull request #94 from metal3d/develop
chore(presentation): Update README
2024-12-03 15:14:54 +01:00
4d0e5b2e6a chore(presentation): Update README 2024-12-03 15:13:28 +01:00
58d1e8e450 Merge pull request #92 from metal3d/develop
doc(refresh): Refresh doc after changing functions
2024-12-03 14:47:52 +01:00
d0790f37a8 doc(refresh): Refresh doc after changing functions 2024-12-03 14:45:27 +01:00
62b0576c2d Merge pull request #91 from metal3d/develop
Fix binary data, Add tests, Error management
2024-12-03 14:44:19 +01:00
e574a2e2a8 chore(errors): Better error management
We must remove all "Fatal" calls and use errors instead, to be returned
and managed globally.
This is the first step, but it is, at this time, a real problem. Tests
are complicated without this.
2024-12-03 14:37:13 +01:00
eb760d4299 test(subdir): Add globally mount binary files 2024-12-03 14:03:36 +01:00
3833037862 doc(refresh): after changing names and adding functions 2024-12-03 13:52:22 +01:00
6dc92df4b5 chore(icons): Better icons for warning and info 2024-12-03 13:51:39 +01:00
d458cdbd73 chore(configmap): Manage binary data in configMap
We should be now able to detect and manage binary files to be injected
in configMaps
2024-12-03 13:50:58 +01:00
628b35d471 doc(readme): fix the schema location 2024-12-01 08:48:33 +01:00
84363be0e8 Fix Readme after merging master into develop 2024-11-27 09:27:28 +01:00
5284cdf5cc Merge pull request #90 from metal3d/develop
Remove update functions and fix dependabot target branch
2024-11-27 09:23:44 +01:00
632ffc2b66 chore(module): update dependencies
Updated k8s.io/api, x/exp, x/net, and structured-merge-diff
2024-11-27 09:21:33 +01:00
2ff705164e chore(module): Cleanup 2024-11-27 09:17:45 +01:00
73ab867509 chore(update): remove the update methods
The update functions were not linked and absolutely not stable anyway. I
will find a better way to propose a check-update method later.
2024-11-27 09:16:49 +01:00
45e44dee14 wip(update): the update function is not clean 2024-11-27 00:46:15 +01:00
689c2a4803 Use develop branch please 2024-11-27 00:26:25 +01:00
e023544c8a Merge pull request #88 from metal3d/develop
Enhance doc, description, examples
2024-11-27 00:00:58 +01:00
10b7a49bbf doc(enhancement): Katenary is complete, and we can add values-from example
- Katenary is no longer a "bootstraper", it **should** generate a
complete Helm chart
- Add a `values-from` example, because this label is very useful
2024-11-26 23:43:57 +01:00
bb1354e228 doc(refacto): Rewrite label types, and format table
- I prefer using Go types, more explicit in my humble opinion
- The markdown table wasn't well-formed
2024-11-26 23:42:39 +01:00
957cc4bcf6 Merge pull request #87 from metal3d/develop
Add schema to the root, fix test coverage
2024-11-26 17:55:46 +01:00
9f1f6c7e78 test(version): cover the version function 2024-11-26 17:53:43 +01:00
a80ddcc054 test(main): More tests in main command package 2024-11-26 17:45:57 +01:00
921eaff367 chore(output): Version should be printed in stdout
It seems that "println" failed to write on stdout
2024-11-26 17:45:42 +01:00
b63d8e4210 doc(readme): Explain the use of the yaml/json schema
And set heading level to 2
2024-11-26 17:10:54 +01:00
9766dac763 yaml(schema): Propose a schema to use with LSP
This schema works, at least, with yaml-lsp (yamlls) in NeoVim.
2024-11-26 17:02:25 +01:00
441be30720 chore(version): Get the version following how katenary is installed 2024-11-26 16:47:37 +01:00
e13653fba1 doc(readme): fix the command line help output 2024-11-26 16:47:05 +01:00
6b301f3171 Merge pull request #86 from metal3d/develop
Add "values-from" and more tests
2024-11-26 16:29:14 +01:00
9181c389d4 doc(refresh): Refresh documentation 2024-11-26 16:11:46 +01:00
3b4dade699 chore(label): new label "values-from"
This labels allow to use some environment variables from another service
and use the configMap / secret instead of the original value. This is
useful to avoid duplication of values for several variables.
2024-11-26 16:11:12 +01:00
4f0298c0a9 test(utils): Add some tests 2024-11-26 16:09:12 +01:00
5f20585fb2 Merge pull request #85 from metal3d/develop
test(codecov): remove codecov
2024-11-26 09:10:48 +01:00
0714eac615 test(codecov): remove codecov
Codecov is very nice but we already use SonarQube to evaluate the code
coverage.
2024-11-26 09:08:49 +01:00
456e7f41f2 Merge pull request #82 from metal3d/develop
Some fixes on "same-pod" and volumes + add some tests
2024-11-25 23:17:56 +01:00
fc335247f8 chore(tests): exlude tests for sonarqube 2024-11-25 23:11:14 +01:00
ad16005091 test(schema): Add test on ingress 2024-11-25 12:10:41 +01:00
a676372bbe chore(clean): remove unused functions 2024-11-25 12:02:19 +01:00
7fadc45e9e chore(typo): bad markdow, bad label name 2024-11-25 12:02:04 +01:00
41a0dc58a5 chore(typo): fix some typos 2024-11-25 12:00:04 +01:00
dc34d32c5c test(secrets): add tests 2024-11-25 11:54:38 +01:00
36984e3825 chore(clean): remove unused functions 2024-11-25 11:54:18 +01:00
046410a5ec chore(refacto): use utils package 2024-11-25 11:54:00 +01:00
827b5bc830 test(values): add map-env and exchange volumes tests 2024-11-22 16:23:00 +01:00
8aee6d9983 fix(nil): The service can not exist 2024-11-22 16:11:55 +01:00
7b890df1c5 fix(schema): Use ingress default values
Ingress has some default values, like path and classname. We need to
ensure that values are taken or nil, and to apply them if they are not
set explicitally. Port is a sepcial case.
2024-11-22 15:55:59 +01:00
e925f58e82 doc(add): Add more documentation about katenary file 2024-11-22 15:12:44 +01:00
91fc0fd9f0 fix(doc): missed a new line 2024-11-22 14:58:43 +01:00
1a1d2b5ee8 fix(configmap): do not write env var in file CM
File CM are configmap to store "static" data (file content), do not set
environment variables inside.
2024-11-22 14:55:42 +01:00
95f3abfa74 feat(volume): add "exchange volumes"
This volumes are "emptyDir" and can have init command. For example, in a
"same-pod", it allow the user to copy data from image to a directory
that is mounted on others pods.
2024-11-22 14:54:36 +01:00
3b51f41716 chore(names): Fix resource name
Use an utility function to fix some names
2024-11-21 11:12:38 +01:00
3f63375b60 refacto(labels): use external files
Files are more readable as external. Use "go:embed" to inject them.
2024-11-21 11:08:55 +01:00
8c97937b44 test(schema): Add tests 2024-11-21 11:08:09 +01:00
96f843630a chore(misc): make the code more readable here 2024-11-21 11:04:19 +01:00
48f6045cd3 fix(same-pod): environnment and volume mapping
We must ensure that the volume is owned by the container.
The environmment configMap wasn't bound.
2024-11-21 11:03:10 +01:00
49045a2ccd Merge pull request #81 from metal3d/develop
Cleanup, re-factorization and allow the use of a katenary.yaml file.
Fixing a wrapping string problem.
Move the label management in a package.
2024-11-19 13:44:04 +01:00
af8dabba85 fix(secrets): Wrapping values is unecessary
It seems that go-compose now escape the string
2024-11-18 17:42:21 +01:00
cc1019b5a8 chore(refacto): fix secret and use katenary schema
- add possibility to use a katenary.yaml file to setup values
- fix secret generation
2024-11-18 17:12:12 +01:00
14877fbfa3 Remove example for now 2024-11-10 00:49:38 +01:00
7b5e45131c doc(fix): fixes the main-app documentation
There were a problem, sentence was truncated
2024-11-09 14:19:56 +01:00
b09316b416 refactor(yaml): globalize fixups
The ToK8SYaml() function makes the job, it's now simpler to manage fixes
2024-11-09 14:18:27 +01:00
315f5da970 Merge pull request #79 from metal3d/develop
Activable tls
2024-11-09 13:57:35 +01:00
9b392a1f64 Fix problem on getting tls mapping
The types were not compatible to get TLS activation
2024-11-08 16:55:18 +01:00
9358076a36 chore(ingress): Allow tls activation 2024-11-08 15:51:36 +01:00
427f2909d5 Merge pull request #78 from metal3d/develop
Fixes template injection and variable names on volumes
2024-11-08 15:03:20 +01:00
8ab1763902 version(actions) Update actions and Go version
- checkout is now v4
- setup-go is now v5
- go version to use 1.23
2024-11-08 13:35:52 +01:00
e409be235e version(go) go 1.23 is now mandatory 2024-11-08 13:30:47 +01:00
1ca71f264c version(mod) Update others dependencies 2024-11-08 13:29:06 +01:00
5f464253c4 version(k8s/api) Update kubernetes api package 2024-11-08 13:26:14 +01:00
75869975f6 version(netdb) Update version 2024-11-08 13:24:04 +01:00
ac43256511 version(cobra) Update cobra version 2024-11-08 13:20:38 +01:00
5b51ba6d98 doc(fix): Regenerate the doc 2024-11-08 13:13:27 +01:00
dd63bb6343 chore(fixes): Unwrap yaml before converting and fix volume name variable
2 fixes:

- the first problem to resolve is that some volume names can have "-" in
the name. We now replace them by "_"
- the second problem is that k8s.io library truncates the lines and so
we cannot split the files by lines. We now "unwrap" the result.

TODO: globalize the `yaml.Marshal()` code to our own specific function
2024-11-08 13:11:14 +01:00
817ebe0e53 Remove logs
Made for debug, we must remove them.

Todo, add a logger system for debug.
2024-11-08 13:08:20 +01:00
6023ca4508 Merge pull request #77 from metal3d/develop
Merge Develop to prepare V3
2024-10-29 17:45:05 +01:00
e0c829c2ce Merge branch 'master' into develop 2024-10-29 17:42:17 +01:00
2d33367422 chore(optim): manage space in struct
Warning given by sonarlint, saves a few bytes.
2024-10-24 17:24:36 +02:00
d72f371c59 fix(generation): fix the volume var/path name
Underscores are forbidden by Kubernetes (should be a valid URL string),
we replace "_" by "-" in names, and we leave the values file using the
original name. So a volume named "foo_bar" in compose file, is
registered as "foo_bar" in the values file, but "foo-bar" is used as
volume name in deployments, volume claims, ...
2024-10-24 17:23:26 +02:00
db168c91c9 fix(generation): use tpl in note for hostnames
The value in hostname in values file can be a templated string. So, we
should execute the content.
2024-10-24 17:21:04 +02:00
d31993953b fix(doc): livenessProbe and readinessProbe must be set
In the health-check label, we now need to specify the kind of check to
do - in the expected form from Kubernetes specification.
2024-10-23 16:32:50 +02:00
63c6d5d0ef chore(format): moved import
Formatter changed the import order
2024-10-23 16:20:29 +02:00
164a617869 fix(values): Remove tplString in environment directive
We badly set a "tpl string" in values.yaml file for "values" labels set
in compose.yaml file.
2024-10-23 16:17:01 +02:00
865473b41b Enhance documentation
- upgraded mkdocs and dependencise (+ add mermaid)
- linted markdown
- add more details
2024-10-18 09:36:56 +02:00
533e1422d0 Sonar complience
Use valid names and factorize some constants checks
2024-10-18 09:36:55 +02:00
d2c8d08b7f Validate markdown
Use markdownlint / marksman in your editor please
2024-10-18 09:36:55 +02:00
4703aa7df5 MkDocs needs 4 spaces for lists 2024-10-18 09:36:55 +02:00
918f1b845b Fix problems and adding functionnalities
Many fixes and enhancements:

- Add icon option
- Add env file managment
- Ordering compose parsing options
- Fix path with underscores
- Fix image and tag discovery
- Better documentation for labels
2024-10-18 09:36:54 +02:00
78dfb15cf5 Add the command package 2024-05-07 13:19:04 +02:00
adc44a5e8b Refactorization and ordering 2024-05-07 13:18:00 +02:00
4367a01769 Big refactorization
- reduce complexity
- use better tools to format the code
- add more tests
- and too many things to list here

We are rewriting for V3, so these commits are sometimes big and not
fully detailed. Of course, further work will be more documented.
2024-05-06 21:11:36 +02:00
d98268f45b Add more tests on probes and dependencies 2024-04-25 00:20:04 +02:00
ccfebd1a70 We need helm linting at this time
Because the linting makes the dependency update. We will need to split
linting and dep update later.
2024-04-25 00:18:57 +02:00
e4f67dbd31 Fix the parsing of probes 2024-04-25 00:18:04 +02:00
d01a35e2d4 Use real types to parse labels
We were using `yaml.Unmarshal` on basic types or inline structs. This
was not efficient and not clear to defined what we expect in labels.
We now use types to unmarshal the labels.

Only the `values` label is, at this time, parsed by GetValuesFromLabel
because this `utils` function is clearly a special case.
2024-04-24 23:06:45 +02:00
0aa7023947 Avoid repetition 2024-04-24 21:53:24 +02:00
451a1341bd Do not check coverage on test file dude 2024-04-24 21:52:59 +02:00
da7d92bbfa Add more tests, refactor to fix problems
Signed-off-by: Patrice Ferlet <metal3d@gmail.com>
2024-04-24 20:55:27 +02:00
15a2f25e51 Exclude doc from the analysis 2024-04-24 14:23:31 +02:00
f73d598bb4 Standardization
- changed variables that was uppercased, that's not OK for linters
- cleanup some documentation
- remove the "/" in label prefix, a function is now used to get the
  complete label (`labelName()`)
- some cleanup in tpl files, and so on...
2024-04-24 14:03:41 +02:00
98c7c6ddc1 Use latest Go compiler 2024-04-24 13:57:06 +02:00
39d63c11b1 Remove coverage files 2024-04-24 13:51:46 +02:00
8f4d69d6e2 Add sonar profile 2024-04-24 13:51:25 +02:00
531756d8ea Use sonarcloud 2024-04-24 13:48:01 +02:00
e0c18ec2ad Avoid repetitions 2024-04-23 15:45:31 +02:00
a3e7435544 Add test for static volumes
And moved a test from deployment_test that was not the right place to be
created.
2024-04-23 15:38:50 +02:00
c01cdf50c8 Only on PR and on push to master 2024-04-23 14:59:20 +02:00
2e3dd5032f Add codecov 2024-04-23 14:54:42 +02:00
8b01807568 Add workflows 2024-04-23 14:43:57 +02:00
46c878b56e Add coverage files 2024-04-23 14:42:55 +02:00
10b9342607 Update README.md
Alert on v3 version
2024-04-23 14:37:19 +02:00
77de53c999 Create more tests 2024-04-23 14:26:23 +02:00
6a7fedee7e We shouldn't quote encoded values
Quoting before encoding in base64 adds the quotes in the encoded data.
That's a bad behavior.
2024-04-23 14:24:06 +02:00
c31299197f Remove the bats tests
That was interesting but finally not so useful. We will make better
tests in Go. The hard part will be to make them working in CI/CD.
2024-04-23 10:13:38 +02:00
49c1fa5fb0 Explain what happens 2024-04-23 10:10:58 +02:00
d1186ee1e1 Refresh doc 2024-04-23 08:07:20 +02:00
50975ae94a Fix static volume binding
It is possible there are many things like this to fix. I made too much
complexity on searching services in deployment while the map key is
enough to get the righ deployment for a compose service.

Need to check the "same-pod" possibilities later.
2024-04-23 08:05:00 +02:00
7e8cb57979 Fixes 2024-04-22 15:43:02 +02:00
12814f4732 Cleanup 2024-04-22 15:36:49 +02:00
dc41826691 Back to normal 2024-04-22 15:32:39 +02:00
6770f8176a Make pure svg calls 2024-04-22 15:29:32 +02:00
cd946b2df6 Try another thing 2024-04-22 15:20:29 +02:00
734b0ed39d Try to embed logos 2024-04-22 15:17:28 +02:00
78d37c4405 Ease installation 2024-04-22 13:55:53 +02:00
fd3ba6d577 Fix doc after changes in source files 2024-04-22 13:31:30 +02:00
8ae9350d31 Add YAML keys in the comments
I eases developpers and admins to know the key to override when they
create an override file or to use `--set` argument for Helm.
2024-04-22 13:28:22 +02:00
9621493343 Add resources in containers and values 2024-04-22 13:27:44 +02:00
f291d17aa3 Fixup documentation after changing packages 2024-04-21 16:37:20 +02:00
9826a54187 Fix notes.txt problems
We were using a bad method to read the ingress values. It's not ensured
by using the service names + checking the "ingress" key.
2024-04-21 16:35:32 +02:00
d48fd2f911 make a better override + add more values (serviceAccount, nodeSelector...) 2024-04-21 16:34:21 +02:00
ec62a79d82 Better override list and documentation 2024-04-19 22:26:45 +02:00
58d19cce52 Fix the chart app version 2024-04-19 22:12:09 +02:00
3bb635a627 Add FAQ page 2024-04-19 12:11:43 +02:00
35f464a1cb Add footnotes and search 2024-04-19 12:11:18 +02:00
85f1b2d43c Fixes the ingress doc 2024-04-19 11:28:27 +02:00
77e8be4e63 Container can be null
In case of deployment with "same-pod" label, the container can be not
found in the deployment.
2024-04-19 11:27:48 +02:00
57b274e345 Fixing documentation 2024-04-19 11:17:54 +02:00
3c743fb135 Fixup hard problems on bound volumes
Recreated the method to bind local content to configMaps with subPath.
That simplify a few how we can bound files and not only directory
content.
2024-04-19 11:13:24 +02:00
ab15614076 Use "helm" filtype first for vim modeline + tests
"helm" can be managed by vim/neovim plugins, so it's a good idea to add
it as default, then use "gotmpl.yaml".

Add basic tests...
2024-04-11 09:37:10 +02:00
c41fa22c59 Update k8s.io/api + fix changed function 2024-04-11 09:34:58 +02:00
81ea0fbb6f fix fonts in svg, one more time... 2024-04-10 22:55:58 +02:00
821c038206 Fix fonts 2024-04-10 22:53:32 +02:00
19a37ace18 Better styles, logo, effects...
- Make a SVG with classes to invert the color of strokes
- Set a better logo + one vertical
2024-04-10 22:25:07 +02:00
d8bd66e66f Change license date, enhance and fix documentation 2024-04-10 14:19:07 +02:00
c780e6c2a2 Change doc, icon and logo 2024-04-10 13:53:58 +02:00
3317459b0b Code cleaning 2024-04-10 04:54:38 +02:00
564b939464 Remove useless composition call 2024-04-10 04:54:16 +02:00
c7c18f01cd Fixup documentation
- better gomarkdown generation that now fixed the escaped strings, no
  need to use pandoc anymore
- added workflow image
- upgraded versions of mkdocs requirements
2024-04-10 04:51:45 +02:00
2f53638f82 Add documentation 2024-04-10 04:49:36 +02:00
5070101706 Add workflow image and zoom on click 2024-04-10 04:48:51 +02:00
cc3c42b8fd Add venv to ignore list 2024-04-10 04:47:52 +02:00
45de7ab543 Fix samepod generation
The container was not merged to the target deployment. It necessary to
make one more loop to apply the container + remove the source
deployment.
2024-04-08 23:15:05 +02:00
984b50356a Do not pass by composed struct
It useless to use the composed struct field as the current "object" is
composed by this one. Get Name field directly. (Thanks staticcheck)
2024-04-05 07:58:37 +02:00
441b30a570 Code cleaning
Using gofumpt. Add documentation.
Some fixes on type checking and const icon type declaration.
2024-04-05 07:56:27 +02:00
2aad5d4b9d Upgrading mod package 2024-04-04 09:53:33 +02:00
50169c8fbc Changing logo, fix doc, catchy text... 2024-04-04 09:50:17 +02:00
fc67cb668d Fix line width to 120 columns 2024-04-03 23:30:24 +02:00
8e4b3be108 Add nvimrc file
Editorconfig seems to not allow the line wrapping. I'm using neovim with
`set exrc`, so this local file is used to fix some behaviors.

If a contributor can give solutions for a global configuration that
works everywhere...
2024-04-03 23:28:04 +02:00
3ae5ec99ff Typo, format of markdown
I prefer to limit 120 columns. A .nvimrc will be proposed to avoid
having to wide markdown lines.
2024-04-03 23:26:54 +02:00
ef7fcb6133 Some users want to use "host" instead of "hostname"
We accept both, nothing more.
2024-04-03 22:44:41 +02:00
50c2f9d1dc Use new labels in documentation, and fix content 2024-04-03 22:38:23 +02:00
eeb044bab0 Add markdown editor configuration 2024-04-03 22:37:56 +02:00
6ce52cc037 Reindentation and change labels 2024-04-03 22:37:22 +02:00
9a3fc6a2b4 Fix "depends_on" check
If "depends_on" is set, we need to ensure that the target service has
got declared ports. It's necessary, at this time, to ensure the target
is started (with an initContainer)

As soon as Kubernetes proposes a better check, we will be able to fix
this requirement.
2024-04-03 22:22:48 +02:00
4ded4d4e09 Set the compose file list using overrides
This is a hack to force the compose package to read all files provided
with the -c flag.
2024-04-03 22:08:01 +02:00
dc47e73b4b Temorary fixing overrides
WIP: this breaks the -c flag, but it's a start

We can now use compose.katenary.yaml file as a default override.
2024-04-03 21:35:04 +02:00
76aa332dc2 Fix dependencies parsing
The code was fetching a simple object while it should have been fetching an array of objects.
2024-04-03 21:34:15 +02:00
5d4f72e984 Fix unued variable, useless functions... 2024-04-03 21:33:26 +02:00
5a358f0a6a Add labels docs
This file was ignored by error in .gitignore
2024-04-03 16:16:28 +02:00
ed9b4681ad Unignore some yaml files
katenaryLabelsDoc.yaml is a file that is used to generate the documentation, it is not a file that should be ignored.
2024-04-03 16:15:24 +02:00
d135f5bc0a Remove the Smile sponsorship
One more time, thanks a lot for the adventure
2024-04-03 14:24:43 +02:00
f9cf3972d5 Removed Smile sponsorship
Thanks for the given time.
2024-04-03 14:18:16 +02:00
6e33fa474a Get the newest version of go-compose
- better integration of compose file
- needed to impose to not resolve path in "parser" package to get them
  from the working directory. This should be improved later
2024-04-03 14:15:50 +02:00
3a0cf1a7db Set test(s) to PHONY 2024-04-03 14:15:28 +02:00
475a025d9e Go to Katenary V3
This is the next-gen of Katenary
2023-12-06 15:24:02 +01:00
c37bde487b Add note for the next release 2023-06-13 09:30:06 +02:00
samzong.lu
982917c25b Update README.md
fix error command  of  completion with zsh
2023-02-07 09:40:58 +01:00
adrian-salas
7feb7427f2 ISSUE-30 - Fix syntax on tcp liveness probe
Closes #30
2023-02-07 09:40:22 +01:00
a2bf9c005b Fix bracket
fixes #50 - the brackets are valid in JSON/YAML
2023-02-07 09:39:12 +01:00
119b5d699c Use compose-go 1.2.8 2022-07-08 11:40:16 +02:00
dependabot[bot]
eca5154d75 Bump gopkg.in/yaml.v3 from 3.0.0 to 3.0.1
Bumps [gopkg.in/yaml.v3](https://github.com/go-yaml/yaml) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: gopkg.in/yaml.v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-08 11:38:35 +02:00
16196aae34 Set smile logo to base_url 2022-07-08 10:37:17 +02:00
9726cc4a18 Fix and comment empty-dirs + volume-from 2022-07-08 10:26:01 +02:00
7eb75015a0 Fix first dot in name 2022-07-08 10:00:54 +02:00
66dec780ae Fix secret/conf environment from file 2022-07-08 09:56:55 +02:00
6cd361705d Try to fix the problem with Smile logo 2022-07-08 08:58:37 +02:00
9c449eefab Fix secret that disapeared from Values
- must fix #24
- optimisation on memory locks
- add `AddEnvironment` function to help management
2022-06-22 10:55:11 +02:00
dependabot[bot]
16c8e3dd20 Bump github.com/spf13/cobra from 1.4.0 to 1.5.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.4.0...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-22 09:55:35 +02:00
b21c821086 Split source file 2022-06-22 09:54:41 +02:00
890d7b5017 Fixup broken merge 2022-06-22 09:54:41 +02:00
dependabot[bot]
1266545919 Bump github.com/compose-spec/compose-go from 1.2.5 to 1.2.7 (#19)
Bumps [github.com/compose-spec/compose-go](https://github.com/compose-spec/compose-go) from 1.2.5 to 1.2.7.
- [Release notes](https://github.com/compose-spec/compose-go/releases)
- [Commits](https://github.com/compose-spec/compose-go/compare/v1.2.5...v1.2.7)

---
updated-dependencies:
- dependency-name: github.com/compose-spec/compose-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-22 09:53:13 +02:00
874e12f35d Fix links 2022-06-14 10:26:24 +02:00
b25b2fda2c Fix colors 2022-06-14 10:21:55 +02:00
e2a4d296f6 Fix Smile logo image 2022-06-14 10:14:08 +02:00
7d5b4d9306 Fixup styles 2022-06-14 10:08:22 +02:00
a1d963c15b Fix dependencies 2022-06-14 09:28:14 +02:00
bb83384d62 Better doc for ReadTheDocs 2022-06-14 09:25:43 +02:00
bc3c5dc932 Change nav top color 2022-06-13 14:00:10 +02:00
203449aa9e Fix logo path 2022-06-13 13:55:43 +02:00
a3dbdfd8c6 Add site_dirs 2022-06-13 13:50:57 +02:00
1b1397b45f Fix paths 2022-06-13 13:47:58 +02:00
1122c02e07 Fix MarkupSafe 2022-06-13 13:40:45 +02:00
01e045e4b7 Try to fix mkdocs again... 2022-06-13 13:37:09 +02:00
d28cbab0fe Fix mkdocs 2022-06-13 13:29:57 +02:00
b1dc94e0e9 Add doc (WIP) 2022-06-13 13:18:31 +02:00
f9fd6332d6 Feat cronjob (#23)
Make possible to declare cronTabs inside docker-compose file.

⇒ Also, add multiple compose file injection with `-c` arguments 

⇒ Also, fixes “ignore depends on” for same pod 

⇒ Also fixes
 
* fix [Be able to specify compose.yml files and its override #21](https://github.com/metal3d/katenary/issues/21)
* fix [Be able to ignore ports to expose in a katenary.io/ports list #16](https://github.com/metal3d/katenary/issues/16)

And more fixes… (later, we will use branches in a better way, that was a hard, long fix process)
2022-06-10 16:15:18 +02:00
Thomas BERNARD
7203928d95 Fix a typo (#22) 2022-06-09 18:35:04 +02:00
dc9b198208 Quote path label 2022-06-01 16:22:45 +02:00
0f9a46f52d Fix env with points to underscore 2022-06-01 16:13:10 +02:00
8cf3ff9f73 Add local test directory 2022-05-24 11:17:46 +02:00
114fab4870 Fix the problem with environment as secret
We needed to filter the environment coming from a env file, but declared
as secet in `secret-vars` label

fix #17
2022-05-23 12:11:23 +02:00
5fc8c06d8b Layout again 2022-05-23 11:24:06 +02:00
51ca4e81d1 Layout... 2022-05-23 11:23:27 +02:00
a6f6b91ab9 Layout 2022-05-23 11:20:52 +02:00
63afc3066b Fix problems with local volumes to configmap
- make it possible to mount only one file as configmap
- remove "dots" from path to convert it to volume name
- see #11 that can be resolved
2022-05-23 11:13:42 +02:00
adrian-salas
1e8fd44857 ISSUE-12 - Trim space on port label (#14)
Co-authored-by: Adrian SALAS <adrian.salas@smile.fr>

fix #12
2022-05-22 22:53:05 +02:00
adrian-salas
1403f9b198 ISSUE-13 - Ignore comment prefix in envfiles (#15)
Co-authored-by: Adrian SALAS <adrian.salas@smile.fr>

fix #13
2022-05-22 22:52:07 +02:00
dependabot[bot]
22ef9211ec Bump github.com/compose-spec/compose-go from 1.2.4 to 1.2.5 (#10)
Bumps [github.com/compose-spec/compose-go](https://github.com/compose-spec/compose-go) from 1.2.4 to 1.2.5.
- [Release notes](https://github.com/compose-spec/compose-go/releases)
- [Commits](https://github.com/compose-spec/compose-go/compare/v1.2.4...v1.2.5)

---
updated-dependencies:
- dependency-name: github.com/compose-spec/compose-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-18 14:20:03 +02:00
418a0a8029 Use compose-go + improvements (#9)
Use compose-go https://github.com/compose-spec/compose-go  to make Katenary parsing compose file the official way.
Add labels:
- `volume-from` (with `same-pod`) to avoid volume repetition
- `ignore` to ignore a service
- `mapenv` (replaces the `env-to-service`) to map environment to helm variable (as a template string)
- `secret-vars` declares variables as secret values

More:
- Now, environment (as secret vars) are set in values.yaml
- Ingress has got annotations in values.yaml
- Probes (liveness probe) are improved
- fixed code to optimize
- many others fixes about path, bad volume check, refactorisation, tests...
2022-05-08 09:55:25 +02:00
165054ca53 For single env_file, it's a string...
see #8
2022-04-01 17:49:10 +02:00
a87391e726 Fix healthcheck
see #8
2022-04-01 17:39:41 +02:00
f8dcd2026b Fix test command to make it work as string / slice
see #8
2022-04-01 16:58:13 +02:00
f99f146af2 Force build with "build-all" by empty dist/* 2022-04-01 10:47:58 +02:00
e72a8a2e9c Fix envfile detection
+ The envfiles were not added!

see #8

TODO: what are the others properties to fix this way?
2022-04-01 10:43:08 +02:00
0f73aa3125 Manage ugly format for command
Some people uses pure string as command, we need to cast to []string
slice.

see #8
2022-04-01 10:12:14 +02:00
7dc5d509f7 Fix the problem with "ugly" environment syntax
We can now manage "- A=B" format as "A: B"

Some others properties than environment may have this problem (e.g.,
command) so we will fix this later.

fix #4
2022-04-01 09:22:00 +02:00
a9b75c48c4 Add test for bad volumes 2022-04-01 08:22:06 +02:00
6ea3a923cc Avoid "not mapped" volumes
fix #5
2022-04-01 08:18:45 +02:00
6cd1af015b Manage empty image name
And avoid removing charts if there are problems before generating the
helm chart

fix #6
2022-04-01 08:15:39 +02:00
68a031d0be Remove bad log 2022-04-01 08:15:01 +02:00
7ba68c2854 If there is no image name, fail!
fix #6
2022-04-01 08:04:37 +02:00
1dd8fef4b3 Fix compose file argument
fix #7
2022-04-01 07:59:53 +02:00
6a2417c361 Create dependabot.yml 2022-03-31 14:30:42 +02:00
ad316d1f49 Removed the forcing of install type 2022-03-31 14:25:40 +02:00
ed22774a93 Use pure shell to read release.id 2022-03-31 14:17:15 +02:00
8dfca953dc Fix push-release 2022-03-31 14:14:54 +02:00
7b774e84d8 Develop (#3)
* Update command added
* Ensure that the upgraded version is really greater
* Update command should not update prerelease
* Minor presentation changes
* Fix command generation in containers from docker-compose file
- Refactored service creation
* Place the full command to the "cmd" package
* Update cobra to v1.4.0
* Updated build and release creation
* Created an install script
* Add more doc
* Add some tests...
* Add a test directive in Makefile
2022-03-31 14:12:20 +02:00
d0576d4b81 Add name explanation 2022-02-17 15:12:15 +01:00
c1fc388b26 Fix Smile link 2022-02-17 15:07:25 +01:00
86ca723aa8 Fixup logos 2022-02-17 15:04:13 +01:00
35ecb0d4d9 Add temporary logo 2022-02-17 12:22:52 +01:00
89adc17857 Add doc for "same-pod" example 2022-02-17 11:54:09 +01:00
16fddbc6aa Fixed ignore list 2022-02-17 11:53:15 +01:00
8543bc5232 Added some generated examples 2022-02-17 11:48:33 +01:00
3d45401649 Fixup doc 2022-02-17 11:38:48 +01:00
95c24be14a Make it possible to mount one file from configMap 2022-02-17 11:38:23 +01:00
bf44d442e5 Fix "/" in configMap names
Fix #2
2022-02-17 11:04:04 +01:00
5d574015ce Fix the missed volume mount for empty dirs
Fix #1
2022-02-17 10:43:07 +01:00
3619cc4b20 Update README.md
Add Releases information to install katenary
2022-02-17 10:06:50 +01:00
5a49c4f869 Forgotten logo file... 2022-02-17 09:58:44 +01:00
88fb12a3bf Add Smile :) 2022-02-17 09:58:09 +01:00
d387d9aec0 Add MIT License 2022-02-17 09:42:32 +01:00
1343f99e39 Better build command + Doc 2022-02-16 18:32:51 +01:00
90d04346d5 Add completion doc 2022-02-16 17:57:14 +01:00
950a77aade Fix documentation 2022-02-16 17:48:57 +01:00
6ef4f7ac42 Fix template 2022-02-16 17:44:25 +01:00
513039e3c9 Add ability to link containers in one pod 2022-02-16 17:40:11 +01:00
a60ab484d2 And one more time, fix flag strings 2022-02-16 11:11:30 +01:00
d9fcf5a1b9 Fix doc 2022-02-16 11:09:08 +01:00
0668b718ea Doc 2022-02-16 11:02:56 +01:00
0d1a6f8c82 Fix some indentation behavio + add indent flag 2022-02-16 10:56:21 +01:00
a4834a0661 Now manage kubernetes version in ingress
The backend and ingressClassName are now under condition
2022-02-16 10:37:46 +01:00
722c7424d0 Fix version flag 2022-02-14 17:15:11 +01:00
b602aa5e39 Fix the use of version flag 2022-02-14 17:02:29 +01:00
d965e1d19b Use "cobra" to propose better command/flags 2022-02-14 17:00:32 +01:00
5a4d9e396d Add healtcheck + some fixes
- better docs
- add healtcheck based on docker-compoe commands or labels
- fix some problems on secret names
- better dependency check
2022-02-14 14:37:09 +01:00
8164603b47 Add a clean directive 2022-02-14 14:36:43 +01:00
9aec646ab2 Fix the compose detection 2022-02-02 10:30:32 +01:00
8d4ea90a9a Fix name generation with "." and "/" 2022-02-02 10:15:14 +01:00
4320519a2a Fix the volume name with minus sign 2022-02-02 10:09:42 +01:00
b9e91d56aa Allow different docker-compose extensions 2022-02-02 10:02:35 +01:00
93a06b52fb Give a default value for GO var 2022-01-26 09:44:23 +01:00
cb88f2879d Give better information at build time 2022-01-26 09:43:01 +01:00
1e79e954c5 Fix the source change detection 2022-01-26 09:21:36 +01:00
69982e4514 Add information about the compiled version 2022-01-26 09:18:32 +01:00
691c1a3b78 We must lock inside the goroutines 2021-12-17 12:08:50 +01:00
332f7a8787 Fix some locks problem 2021-12-17 12:05:38 +01:00
6273e5531a Fix doc and syntax 2021-12-17 11:48:32 +01:00
e0382a8b83 Fix directory check for .git 2021-12-17 11:46:35 +01:00
3385b61272 Check if .git is a directory 2021-12-17 11:40:56 +01:00
a0e02af06e The version was overwritten to empty string 2021-12-17 11:04:43 +01:00
7adac3662e Autodetect git version/branch/hash for appversion 2021-12-17 10:59:57 +01:00
ca9ab8a13b Set all help message to lower case 2021-12-17 10:35:21 +01:00
b16897b875 Fix the section following the config type. 2021-12-17 10:29:08 +01:00
8ccdb2854b Remove annotation for ingres.class
This yield an error on new Kubernetes
TODO: check k8s version like does "helm create"?
2021-12-17 10:28:16 +01:00
df60c2c866 Refactorisation to writers in generator package 2021-12-05 10:13:11 +01:00
fe2a655796 Enhancements, see details
- Added a message to explain to the user that it needs to build and push
  images
- The Release Name string is now a constant ins source code, later we will be able to
  change it to (for example) a template call
- SHA256 injected label (from docker-compose file content) is a bit long, SHA1 is shorter
- We now add compose command line and generation date in the Values file
- Code cleanup, refactorisation and enhancements
- More documentation in the source code
2021-12-05 09:05:48 +01:00
714ccf771d Mount with SELinux label to build 2021-12-05 07:40:11 +01:00
156 changed files with 15763 additions and 1377 deletions

View File

@@ -4,3 +4,8 @@ root = true
indent_style=tab
indent_size=4
[*.md]
trim_trailing_whitespace = false
indent_style = space
indent_size = 4

View File

@@ -0,0 +1,160 @@
on:
push:
tags:
- "releases/*"
env:
PACKAGE_API: https://repo.katenary.io/api/packages/katenary
CGO_ENABLED: 0
BIN_TARGET: /usr/bin/katenary
FPM_BASE: ../LICENSE=/usr/share/doc/katenary/LICENSE ../README.md=/usr/share/doc/katenary/README.md
FPM_COMMON_FILES: ../doc/share/man/man1/katenary.1=/usr/share/man/man1/katenary.1
FPM_COMMON_FILES_ARCHLINUX: ../doc/share/man/man1/katenary.1=/usr/man/man1/katenary.1
VENDOR: "Katenary Project"
MAINTANER: "Patrice Ferlet <metal3d@gmail.com>"
LICENSE: MIT
DESCRIPTION: "Effortless conversion from compose files (docker, podman) to Helm Charts"
URL: "https://katenary.io"
VERSION: ${{ gitea.ref_name }}
jobs:
build-packages:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
goarch: [amd64, arm64]
goos: [linux, freebsd, windows, darwin]
exclude:
- goos: windows
goarch: arm64
- goos: darwin
goarch: arm64
steps:
- name: Extract version
run: |
VERSION="${VERSION#releases/}"
echo "Exporting variable VERSION=$VERSION"
echo "VERSION=$VERSION" >> $GITEA_OUTPUT
echo "VERSION=$VERSION" >> $GITEA_ENV
- name: Install Unix dependencies
if: matrix.goos == 'linux' || matrix.goos == 'freebsd'
run: |-
sudo apt-get update && sudo apt-get install -y rpm zstd libarchive-tools binutils ruby curl
sudo gem install fpm
- name: Install Windows dependencies
if: matrix.goos == 'windows'
run: |-
sudo apt-get update && sudo apt-get install -y nsis curl
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.25
- name: Compile binary
if: matrix.goos == 'linux' || matrix.goos == 'freebsd' || matrix.goos == 'darwin'
run: |-
echo "Building binary version $VERSION"
mkdir -p dist
GOARCH=${{ matrix.goarch }} GOOS=${{ matrix.goos }}\
go build -ldflags="-X 'repo.katenary.io/katenary/katenary/internal/generator.Version=$VERSION'" \
-trimpath -o dist/katenary-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/katenary
- name: Package (Linux)
if: matrix.goos == 'linux'
run: |-
mkdir -p dist
cd dist
# rpm and deb
for p in rpm deb; do
fpm -s dir -t $p \
--name katenary \
--url "$URL" \
--vendor "$VENDOR" \
--maintainer "$MAINTANER" \
--license "$LICENSE" \
--description "$DESCRIPTION" \
-a ${{ matrix.goarch }} -f $FPM_OPTS --version=$VERSION \
$FPM_BASE $FPM_COMMON_FILES \
./katenary-${{ matrix.goos }}-${{ matrix.goarch }}=$BIN_TARGET
done
# arch linux
fpm -s dir -t pacman \
--name katenary \
--url "$URL" \
--vendor "$VENDOR" \
--maintainer "$MAINTANER" \
--license "$LICENSE" \
--description "$DESCRIPTION" \
-a ${{ matrix.goarch }} -f $FPM_OPTS --version=$VERSION \
$FPM_BASE $FPM_COMMON_FILES_ARCHLINUX \
./katenary-${{ matrix.goos }}-${{ matrix.goarch }}=$BIN_TARGET
- name: Package (FreeBSD)
if: matrix.goos == 'freebsd'
run: |-
# freebsd
mkdir -p dist
cd dist
fpm -s dir -t freebsd \
--name katenary \
--url "$URL" \
--vendor "$VENDOR" \
--maintainer "$MAINTANER" \
--license "$LICENSE" \
--description "$DESCRIPTION" \
-a ${{ matrix.goarch }} -f $FPM_OPTS --version=$VERSION \
$FPM_BASE $FPM_COMMON_FILES \
./katenary-${{ matrix.goos }}-${{ matrix.goarch }}=$BIN_TARGET
- name: Package setup.exe for windows
if: matrix.goos == 'windows'
run: |-
mkdir -p dist
GOARCH=${{ matrix.goarch }} GOOS=${{ matrix.goos }}\
go build -ldflags="-X 'repo.katenary.io/katenary/katenary/internal/generator.Version=$VERSION'" \
-trimpath -o dist/katenary.exe ./cmd/katenary
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
cd ..
makensis -DAPP_VERSION=$VERSION nsis/katenary.nsi
mv nsis/katenary-windows-setup.exe dist/katenary-windows-setup.exe
- name: Upload to packages repositories
run: |-
for file in katenary-{freebsd,linux}-{amd64,arm64} katenary-darwin-amd64 katenary.exe katenary-windows-setup.exe; do
[[ -f dist/$file ]] || continue
echo "Push $file"
curl -sSLf --user ${{ gitea.actor }}:${{ secrets.ACCESS_TOKEN }} \
--upload-file dist/$file \
${PACKAGE_API}/generic/katenary/${VERSION}/$(basename $file)
done
for file in $(find dist -name "*.rpm"); do
echo "Push $file"
curl -sSLf --user ${{ gitea.actor }}:${{ secrets.ACCESS_TOKEN }} \
--upload-file $file \
${PACKAGE_API}/rpm/upload
done
for file in $(find dist -name "*.deb"); do
echo "Push $file"
curl -sSLf --user ${{ gitea.actor }}:${{ secrets.ACCESS_TOKEN }} \
--upload-file $file \
${PACKAGE_API}/debian/pool/any/main/upload
done
for file in $(find dist -name "*.zst"); do
echo "Push $file"
curl -sSLf --user ${{ gitea.actor }}:${{ secrets.ACCESS_TOKEN }} \
--upload-file $file \
${PACKAGE_API}/arch/core
done

View File

@@ -0,0 +1,55 @@
name: Build and push OCI image
on:
push:
tags:
- "releases/*"
env:
REGISTRY: repo.katenary.io
IMAGE_NAME: ${{ gitea.repository }}
STORAGE_DRIVER: vfs
VERSION: ${{ gitea.ref_name }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Extract version
run: |
VERSION="${VERSION#releases/}"
echo "Exporting variable VERSION=$VERSION"
echo "VERSION=$VERSION" >> $GITEA_OUTPUT
echo "VERSION=$VERSION" >> $GITEA_ENV
- name: Install Buildah
run: |-
env
if ! command -v buildah 2>/dev/null; then
echo "Install Buildah"
sudo apt-get update
sudo apt-get install -y buildah
else
echo "Buildah already installed"
fi
- name: Login to repository
run: |
buildah login $REGISTRY -u ${{ gitea.actor }} -p ${{ secrets.ACCESS_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v4
- name: Build and tag
run: |-
echo "Building image $REGISTRY/${IMAGE_NAME,,}:$VERSION / latest"
buildah build --isolation=chroot --build-arg VERSION=$VERSION -t katenary -f ./oci/katenary/Containerfile .
buildah tag katenary $REGISTRY/${IMAGE_NAME,,}:$VERSION
buildah tag katenary $REGISTRY/${IMAGE_NAME,,}:latest
- name: Push image
run: |-
buildah push $REGISTRY/${IMAGE_NAME,,}:$VERSION
buildah push $REGISTRY/${IMAGE_NAME,,}:latest

View File

@@ -0,0 +1,68 @@
name: Go-Tests
on:
pull_request:
types:
- opened
- edited
push:
branches:
- "master"
- "main"
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
jobs:
tests:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.25
- name: Install helm
run: |
command -v helm || curl -sSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
- name: Tidy
run: |
go mod tidy
- name: Launch Test
run: |
go vet ./...
go test -coverprofile=coverprofile.out -json -v ./... > gotest.json
# - uses: actions/upload-artifact@v4
- name: Upload artifact
uses: christopherhx/gitea-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:
- name: Checkout
uses: actions/checkout@v4
#- uses: actions/download-artifact@v4
- name: Download artifact
uses: christopherhx/gitea-download-artifact@v4
with:
name: tests-results
- name: Sonar
run: |
export SONAR_SCANNER_VERSION=7.2.0.5079
export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux-x64
curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux-x64.zip
unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/
export PATH=$SONAR_SCANNER_HOME/bin:$PATH
sonar-scanner \
-Dsonar.organization=katenary \
-Dsonar.projectKey=katenary_katenary

37
.github/workflows/rewrite-readme.yaml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Set README on GitHub
# Trigger this workflow every 12 hours.
on:
push:
branches:
- "**"
jobs:
rewrite_readme:
name: Append header on README
runs-on: ubuntu-latest
if: contains(github.server_url, 'github.com')
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Write header
run: |
cp README.md __OLD.md
cat 1> README.md <<-EOF
> **⚠️ Project has moved !⚠️**
>
> **Project moved to https://repo.katenary.io/katenary/katenary**
>
> Please, update your bookmaks. Discussion about the move is here : https://github.com/orgs/Katenary/discussions/162
EOF
cat __OLD.md >> README.md
- name: Commit
uses: EndBug/add-and-commit@v9
with:
add: README.md
message: "Add mirror information"
push: true

28
.github/workflows/stale.yaml vendored Normal file
View File

@@ -0,0 +1,28 @@
# Thanks to blender authors to provide this.
# Adaptation made for Katenary
name: Close GitHub Pull Requests
# Trigger this workflow every 12 hours.
on:
schedule:
- cron: "* */12 * * *"
jobs:
close_prs:
name: Close Pull Requests
runs-on: ubuntu-latest
if: contains(github.server_url, 'github.com')
permissions:
pull-requests: write
steps:
- uses: actions/stale@v9
with:
# Number of days before a pull request is marked as stale.
days-before-pr-stale: 0
# Number of days before a pull request is closed.
days-before-pr-close: 0
# Message posted when closing a pull request.
stale-pr-message: |
This pull request has been automatically closed because this repository is a read-only mirror.
Katenary developement happens on [repo.katenary.io](https://repo.katenary.io/Katenary/katenary).

35
.gitignore vendored
View File

@@ -1,5 +1,32 @@
.cache/*
chart/*
docker-compose.yaml
katenary
dist/*
*.env
.credentials
release.id
cover*
.sq
.aider*
.python_history
.bash_history
.cache/
.aider/
.config/
*/venv
# will be treated later
/examples/*
# nsis
nsis/*.dll
nsis/*.exe
__pycache__
.rpmmacros
*.gpg
# local binaries
katenary
!cmd/katenary
!oci/katenary

0
.gitmodules vendored Normal file
View File

26
.golangci.yml Normal file
View 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
View 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
View 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

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

114
Makefile
View File

@@ -1,45 +1,121 @@
# 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")
VERSION=$(shell git describe --exact-match --tags $(CUR_SHA) 2>/dev/null || echo $(CUR_BRANCH)-$(CUR_SHA))# use by golang flags
# Go build command and environment variables for target OS and architecture
GOVERSION=1.25
GO=container# container, local
OUTPUT=katenary
GOOS=linux
GOARCH=amd64
CGO_ENABLED=0
PREFIX=~/.local
.ONESHELL:
# UPX compression
UPX_OPTS =
UPX ?= upx $(UPX_OPTS)
# 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)
include makefiles/build.mk
include makefiles/containers.mk
include makefiles/doc.mk
include makefiles/gpg.mk
include makefiles/packager.mk
include makefiles/test.mk
all: build
# if docker is used instead of podman, we warn the user
warn-docker:
@echo -e "\033[1;31mWarning: Docker is not recommended, use Podman instead.\033[0m"
sleep 5
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.
Katenary is statically built (in Go), so there is no library to install.
To build for others OS:
$$ make build GOOS=linux GOARCH=amd64
This will build the binary for linux amd64.
$$ make build GOOS=linux GOARCH=arm
This will build the binary for linux arm.
$$ make build GOOS=windows GOARCH=amd64
This will build the binary for windows amd64.
$$ make build GOOS=darwin GOARCH=amd64
This will build the binary for darwin amd64.
Or you can build all versions:
$$ make binaries
EOF
build: katenary
katenary: *.go generator/*.go compose/*.go helm/*.go
@echo Build using $(CTN)
ifeq ($(CTN),podman)
@podman run --rm -v $(PWD):/go/src/katenary -w /go/src/katenary --userns keep-id -it golang go build -o katenary -ldflags="-X 'main.Version=$(VERSION)'" .
else
@docker run --rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp -it golang go build -o katenary -ldflags="-X 'main.Version=$(VERSION)'" .
endif
## installation and uninstallation
install: build
cp katenary $(PREFIX)/bin/katenary
install -Dm755 katenary $(PREFIX)/bin/katenary
uninstall:
rm -f $(PREFIX)/bin/katenary
clean:
## 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

298
README.md
View File

@@ -1,9 +1,60 @@
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">
<img src="./doc/docs/statics/logo-vertical.svg" alt="Katenary Logo" style="max-width: 90%" align="center"/>
</div>
> **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.
<div style="text-align:center; margin: auto 0 4em 0" align="center">
[![Documentation Status](https://readthedocs.org/projects/katenary/badge/?version=latest)](https://katenary.readthedocs.io/latest/?badge=latest)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=katenary_katenary&metric=coverage)](https://sonarcloud.io/summary/new_code?id=katenary_katenary)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=katenary_katenary&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=katenary_katenary)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=katenary_katenary&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=katenary_katenary)
# Install
</div>
<div style="text-align:center; margin: auto 0 4em 0" align="center">
<h3>🚀 Unleash Productivity with Katenary! 🚀</h3>
</div>
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.
## 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://repo.katenary.io/metal3d).
## Install
You can download the binaries from the [Release](https://repo.katenary.io/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.
On Linux, you can use the `install.sh` from the repository to install it in your `$HOME/.local/bin` directory:
```bash
sh <(curl -sSL https://repo.katenary.io/Katenary/katenary/raw/branch/master/install.sh)
```
All OS, if you've installed Go on your computer, you can install using:
```bash
go install -u katenary.io/cmd@latest
# or use a release
go install -u katenary.io/cmd@v3.0.0
```
## Or, build yourself
If you've got `podman` or `docker`, you can build `katenary` by using:
@@ -12,62 +63,106 @@ 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
```
# Usage
If that goes wrong, you can use your local Go compiler:
```bash
Usage of katenary:
-appname string
sive the helm chart app name (default "MyApp")
-appversion string
set the chart appVersion (default "0.0.1")
-chart-dir string
set the chart directory (default "chart")
-compose string
set the compose file to parse (default "docker-compose.yaml")
-force
force the removal of the chart-dir
-version
Show version and exit
make build GO=local
# To force OS or architecture
make build GO=local GOOS=linux GOARCH=arm64
```
Katenary will try to find a `docker-compose.yaml` 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.
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
source <(katenary completion bash)
# if the documentation breaks a bit your completion:
source <(katenary completion bash --no-description)
# zsh in ~/.zshrc
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
```text
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.
Usage:
katenary [command]
Examples:
katenary convert -c docker-compose.yml -o ./charts
Available Commands:
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
-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.
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/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:
@@ -75,27 +170,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 values 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
- `katenary.io/env-to-service` binds the given (coma separated) variables names to {{ .Release.Name }}-value
- `katenary.io/ingress`: create an ingress and bind it to the given port
- `katenary.io/secret-envfiles`: force the creation of a secret for the given coma separated list of "env_file"
- `katenary.io/ports` is a coma separated list of ports if you want to avoid the "ports" section in your docker-compose for any reason
- `katenary.io/configma-volumes` is a coma separated list of directory (should be declared as volumes also) to transform to a configMap object
These labels could be found by `katenary help-labels`, and can be placed as labels inside your docker-compose file:
```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://repo.katenary.io/Katenary/katenary/raw/branch/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://repo.katenary.io/Katenary/katenary/raw/branch/master/katenary.json"] = "katenary.yaml",
},
},
},
},
},
},
},
}
```
Use this address to validate the `katenary.yaml` file in VSCode:
```json
{
"yaml.schemas": {
"https://repo.katenary.io/Katenary/katenary/raw/branch/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.

320
cmd/katenary/main.go Normal file
View File

@@ -0,0 +1,320 @@
// 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"
"log"
"os"
"strings"
"katenary.io/internal/generator"
"katenary.io/internal/generator/katenaryfile"
"katenary.io/internal/generator/labels"
"katenary.io/internal/utils"
"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
}

78
cmd/katenary/main_test.go Normal file
View File

@@ -0,0 +1,78 @@
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")
} else 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, err := os.Pipe()
if err != nil {
t.Fatalf("Failed to create pipe: %v", err)
}
os.Stdout = w
var buf bytes.Buffer
done := make(chan struct{})
go func() {
schema.Run(cmd, nil)
w.Close()
close(done)
}()
io.Copy(&buf, r)
<-done
os.Stdout = old
// try to parse json
schemaContent := make(map[string]any)
if err := json.Unmarshal(buf.Bytes(), &schemaContent); err != nil {
t.Errorf("Expected valid json")
}
}

View File

@@ -1,80 +0,0 @@
package compose
import (
"fmt"
"katenary/helm"
"log"
"os"
"strings"
"gopkg.in/yaml.v3"
)
// 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"))
}
return p
}
func (p *Parser) Parse(appname string) {
Appname = appname
}

View File

@@ -1,28 +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
}
// 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"`
}

View File

@@ -0,0 +1,9 @@
MD012: false
MD013: false
MD022: false
MD033: false
MD041: false
MD046: false
# list indentation
MD007:
indent: 4

105
doc/docs/coding.md Normal file
View 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
View 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 compose` 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
View 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
View File

@@ -0,0 +1,162 @@
<div class="md-center">
![Katenary Logo](statics/logo-vertical.svg)
</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">
![](statics/workflow.svg)
</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">
![](./statics/klee.svg)
</div>
The main developer is [Patrice FERLET](https://repo.katenary.io/metal3d).
The project source
code is hosted on the [:fontawesome-brands-git: Katenary Repository](https://repo.katenary.io/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://repo.katenary.io/Katenary/katenary/raw/branch/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://repo.katenary.io/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
View 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 !-->

View File

@@ -0,0 +1,12 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# katenary
```go
import "katenary.io/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.

View File

@@ -0,0 +1,923 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# generator
```go
import "katenary.io/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/converter.go#L100>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels.go#L13>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels.go#L26>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/helper.go#L16>)
```go
func Helper(name string) string
```
Helper returns the \_helpers.tpl file for a chart.
<a name="NewCronJob"></a>
## func [NewCronJob](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/cronJob.go#L29>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/utils.go#L91>)
```go
func ToK8SYaml(obj any) ([]byte, error)
```
<a name="UnWrapTPL"></a>
## func [UnWrapTPL](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/utils.go#L87>)
```go
func UnWrapTPL(in []byte) []byte
```
UnWrapTPL removes the line wrapping from a template.
<a name="ChartTemplate"></a>
## type [ChartTemplate](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/chart.go#L22-L25>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L38-L43>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L47>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L120>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L158>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L153>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L167>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L214>)
```go
func (c *ConfigMap) AppendFile(path string) error
```
<a name="ConfigMap.Filename"></a>
### func \(\*ConfigMap\) [Filename](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L238>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L248>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L253>)
```go
func (c *ConfigMap) Yaml() ([]byte, error)
```
Yaml returns the yaml representation of the configmap
<a name="ConfigMapMount"></a>
## type [ConfigMapMount](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L29-L32>)
```go
type ConfigMapMount struct {
// contains filtered or unexported fields
}
```
<a name="ConvertOptions"></a>
## type [ConvertOptions](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/chart.go#L28-L37>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/cronJob.go#L23-L26>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/cronJob.go#L113>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/cronJob.go#L120>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L35-L45>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L49>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L116>)
```go
func (d *Deployment) AddContainer(service types.ServiceConfig)
```
AddContainer adds a container to the deployment.
<a name="Deployment.AddHealthCheck"></a>
### func \(\*Deployment\) [AddHealthCheck](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L163>)
```go
func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container)
```
<a name="Deployment.AddIngress"></a>
### func \(\*Deployment\) [AddIngress](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L192>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L222>)
```go
func (d *Deployment) AddLegacyVolume(name, kind string)
```
<a name="Deployment.AddVolumes"></a>
### func \(\*Deployment\) [AddVolumes](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L198>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L243>)
```go
func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment)
```
<a name="Deployment.BindMapFilesToContainer"></a>
### func \(\*Deployment\) [BindMapFilesToContainer](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L377>)
```go
func (d *Deployment) BindMapFilesToContainer(service types.ServiceConfig, secrets []string, appName string) (*corev1.Container, int)
```
<a name="Deployment.DependsOn"></a>
### func \(\*Deployment\) [DependsOn](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L271>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L302>)
```go
func (d *Deployment) Filename() string
```
Filename returns the filename of the deployment.
<a name="Deployment.MountExchangeVolumes"></a>
### func \(\*Deployment\) [MountExchangeVolumes](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L428>)
```go
func (d *Deployment) MountExchangeVolumes()
```
<a name="Deployment.SetEnvFrom"></a>
### func \(\*Deployment\) [SetEnvFrom](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L307>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/deployment.go#L452>)
```go
func (d *Deployment) Yaml() ([]byte, error)
```
Yaml returns the yaml representation of the deployment.
<a name="FileMapUsage"></a>
## type [FileMapUsage](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/configMap.go#L22>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/chart.go#L41-L54>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/generator.go#L32>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/chart.go#L57>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/chart.go#L72>)
```go
func (chart *HelmChart) SaveTemplates(templateDir string)
```
SaveTemplates the templates of the chart to the given directory.
<a name="Ingress"></a>
## type [Ingress](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/ingress.go#L18-L22>)
```go
type Ingress struct {
*networkv1.Ingress
// contains filtered or unexported fields
}
```
<a name="NewIngress"></a>
### func [NewIngress](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/ingress.go#L25>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/ingress.go#L129>)
```go
func (ingress *Ingress) Filename() string
```
<a name="Ingress.Yaml"></a>
### func \(\*Ingress\) [Yaml](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/ingress.go#L133>)
```go
func (ingress *Ingress) Yaml() ([]byte, error)
```
<a name="IngressValue"></a>
## type [IngressValue](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/rbac.go#L119>)
```go
func (r *Role) Filename() string
```
<a name="Role.Yaml"></a>
### func \(\*Role\) [Yaml](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/rbac.go#L123>)
```go
func (r *Role) Yaml() ([]byte, error)
```
<a name="RoleBinding"></a>
## type [RoleBinding](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/rbac.go#L105>)
```go
func (r *RoleBinding) Filename() string
```
<a name="RoleBinding.Yaml"></a>
### func \(\*RoleBinding\) [Yaml](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/rbac.go#L109>)
```go
func (r *RoleBinding) Yaml() ([]byte, error)
```
<a name="Secret"></a>
## type [Secret](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/secret.go#L23-L26>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/secret.go#L29>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/secret.go#L71>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/secret.go#L87>)
```go
func (s *Secret) Filename() string
```
Filename returns the filename of the secret.
<a name="Secret.SetData"></a>
### func \(\*Secret\) [SetData](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/secret.go#L92>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/secret.go#L99>)
```go
func (s *Secret) Yaml() ([]byte, error)
```
Yaml returns the yaml representation of the secret.
<a name="Service"></a>
## type [Service](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/service.go#L19-L22>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/service.go#L25>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/service.go#L54>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/service.go#L75>)
```go
func (s *Service) Filename() string
```
Filename returns the filename of the service.
<a name="Service.Yaml"></a>
### func \(\*Service\) [Yaml](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/service.go#L80>)
```go
func (s *Service) Yaml() ([]byte, error)
```
Yaml returns the yaml representation of the service.
<a name="ServiceAccount"></a>
## type [ServiceAccount](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/rbac.go#L137>)
```go
func (r *ServiceAccount) Filename() string
```
<a name="ServiceAccount.Yaml"></a>
### func \(\*ServiceAccount\) [Yaml](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/rbac.go#L141>)
```go
func (r *ServiceAccount) Yaml() ([]byte, error)
```
<a name="TLS"></a>
## type [TLS](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/values.go#L23-L26>)
```go
type TLS struct {
Enabled bool `yaml:"enabled"`
SecretName string `yaml:"secretName"`
}
```
<a name="Value"></a>
## type [Value](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/values.go#L90>)
```go
func (v *Value) AddIngress(host, path string)
```
<a name="Value.AddPersistence"></a>
### func \(\*Value\) [AddPersistence](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/volume.go#L19-L24>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/volume.go#L27>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/volume.go#L63>)
```go
func (v *VolumeClaim) Filename() string
```
Filename returns the suggested filename for a VolumeClaim.
<a name="VolumeClaim.Yaml"></a>
### func \(\*VolumeClaim\) [Yaml](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/volume.go#L68>)
```go
func (v *VolumeClaim) Yaml() ([]byte, error)
```
Yaml marshals a VolumeClaim into yaml.
<a name="Yaml"></a>
## type [Yaml](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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>)

View File

@@ -0,0 +1,28 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# extrafiles
```go
import "katenary.io/internal/generator/extrafiles"
```
Package extrafiles provides function to generate the Chart files that are not objects. Like README.md and notes.txt...
## func [NotesFile](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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>)

View File

@@ -0,0 +1,67 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# katenaryfile
```go
import "katenary.io/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/katenaryfile/main.go#L138>)
```go
func GenerateSchema() string
```
GenerateSchema generates the schema for the katenary.yaml file.
<a name="OverrideWithConfig"></a>
## func [OverrideWithConfig](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/katenaryfile/main.go#L50>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/katenaryfile/main.go#L28-L45>)
Service is a struct that contains the service configuration for katenary
```go
type Service struct {
MainApp *bool `yaml:"main-app,omitempty" json:"main-app,omitempty" jsonschema:"title=Is this service the main application"`
Values []StringOrMap `yaml:"values,omitempty" json:"values,omitempty" jsonschema:"description=Environment variables to be set in values.yaml with or without a description"`
Secrets *labelstructs.Secrets `yaml:"secrets,omitempty" json:"secrets,omitempty" jsonschema:"title=Secrets,description=Environment variables to be set as secrets"`
Ports *labelstructs.Ports `yaml:"ports,omitempty" json:"ports,omitempty" jsonschema:"title=Ports,description=Ports to be exposed in services"`
Ingress *labelstructs.Ingress `yaml:"ingress,omitempty" json:"ingress,omitempty" jsonschema:"title=Ingress,description=Ingress configuration"`
HealthCheck *labelstructs.HealthCheck `yaml:"health-check,omitempty" json:"health-check,omitempty" jsonschema:"title=Health Check,description=Health check configuration that respects the kubernetes api"`
SamePod *string `yaml:"same-pod,omitempty" json:"same-pod,omitempty" jsonschema:"title=Same Pod,description=Service that should be in the same pod"`
Description *string `yaml:"description,omitempty" json:"description,omitempty" jsonschema:"title=Description,description=Description of the service that will be injected in the values.yaml file"`
Ignore *bool `yaml:"ignore,omitempty" json:"ignore,omitempty" jsonschema:"title=Ignore,description=Ignore the service in the conversion"`
Dependencies []labelstructs.Dependency `yaml:"dependencies,omitempty" json:"dependencies,omitempty" jsonschema:"title=Dependencies,description=Services that should be injected in the Chart.yaml file"`
ConfigMapFiles *labelstructs.ConfigMapFiles `yaml:"configmap-files,omitempty" json:"configmap-files,omitempty" jsonschema:"title=ConfigMap Files,description=Files that should be injected as ConfigMap"`
MapEnv *labelstructs.MapEnv `yaml:"map-env,omitempty" json:"map-env,omitempty" jsonschema:"title=Map Env,description=Map environment variables to another value"`
CronJob *labelstructs.CronJob `yaml:"cron-job,omitempty" json:"cron-job,omitempty" jsonschema:"title=Cron Job,description=Cron Job configuration"`
EnvFrom *labelstructs.EnvFrom `yaml:"env-from,omitempty" json:"env-from,omitempty" jsonschema:"title=Env From,description=Inject environment variables from another service"`
ExchangeVolumes []*labelstructs.ExchangeVolume `yaml:"exchange-volumes,omitempty" json:"exchange-volumes,omitempty" jsonschema:"title=Exchange Volumes,description=Exchange volumes between services"`
ValuesFrom *labelstructs.ValueFrom `yaml:"values-from,omitempty" 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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/katenaryfile/main.go#L25>)
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>)

View File

@@ -0,0 +1,110 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# labels
```go
import "katenary.io/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/katenaryLabels.go#L89>)
```go
func GetLabelHelp(asMarkdown bool) string
```
GetLabelHelp return the help for the labels.
<a name="GetLabelHelpFor"></a>
## func [GetLabelHelpFor](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/katenaryLabels.go#L98>)
```go
func GetLabelHelpFor(labelname string, asMarkdown bool) string
```
GetLabelHelpFor returns the help for a specific label.
<a name="GetLabelNames"></a>
## func [GetLabelNames](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/katenaryLabels.go#L73>)
```go
func GetLabelNames() []string
```
GetLabelNames returns a sorted list of all katenary label names.
<a name="Prefix"></a>
## func [Prefix](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/katenaryLabels.go#L236>)
```go
func Prefix() string
```
<a name="Help"></a>
## type [Help](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/katenaryLabels.go#L65-L70>)
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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/katenaryLabels.go#L58>)
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"
LabelValuesFrom Label = KatenaryLabelPrefix + "/values-from"
)
```
<a name="LabelName"></a>
### func [LabelName](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/katenaryLabels.go#L60>)
```go
func LabelName(name string) Label
```
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)

View File

@@ -0,0 +1,246 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# labelstructs
```go
import "katenary.io/internal/generator/labels/labelstructs"
```
Package labelstructs is a package that contains the structs used to represent the labels in the yaml files.
## type [ConfigMapFiles](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/configMap.go#L5>)
```go
type ConfigMapFiles []string
```
<a name="ConfigMapFileFrom"></a>
### func [ConfigMapFileFrom](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/configMap.go#L7>)
```go
func ConfigMapFileFrom(data string) (ConfigMapFiles, error)
```
<a name="CronJob"></a>
## type [CronJob](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/cronJob.go#L12>)
```go
func CronJobFrom(data string) (*CronJob, error)
```
<a name="Dependency"></a>
## type [Dependency](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/envFrom.go#L5>)
```go
type EnvFrom []string
```
<a name="EnvFromFrom"></a>
### func [EnvFromFrom](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/exchangeVolume.go#L12>)
```go
func NewExchangeVolumes(data string) ([]*ExchangeVolume, error)
```
<a name="HealthCheck"></a>
## type [HealthCheck](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/probes.go#L16>)
```go
func ProbeFrom(data string) (*HealthCheck, error)
```
<a name="Ingress"></a>
## type [Ingress](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/ingress.go#L15-L23>)
```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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/ingress.go#L26>)
```go
func IngressFrom(data string) (*Ingress, error)
```
IngressFrom creates a new Ingress from a compose service.
<a name="MapEnv"></a>
## type [MapEnv](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/mapenv.go#L5>)
```go
type MapEnv map[string]string
```
<a name="MapEnvFrom"></a>
### func [MapEnvFrom](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/ports.go#L5>)
```go
type Ports []uint32
```
<a name="PortsFrom"></a>
### func [PortsFrom](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/secrets.go#L5>)
```go
type Secrets []string
```
<a name="SecretsFrom"></a>
### func [SecretsFrom](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/secrets.go#L7>)
```go
func SecretsFrom(data string) (Secrets, error)
```
<a name="TLS"></a>
## type [TLS](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/ingress.go#L11-L13>)
```go
type TLS struct {
Enabled bool `yaml:"enabled" json:"enabled,omitempty"`
}
```
<a name="ValueFrom"></a>
## type [ValueFrom](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/valueFrom.go#L5>)
```go
type ValueFrom map[string]string
```
<a name="GetValueFrom"></a>
### func [GetValueFrom](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/generator/labels/labelstructs/valueFrom.go#L7>)
```go
func GetValueFrom(data string) (*ValueFrom, error)
```
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)

View File

@@ -0,0 +1,19 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# parser
```go
import "katenary.io/internal/parser"
```
Package parser is a wrapper around compose\-go to parse compose files.
## func [Parse](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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>)

View File

@@ -0,0 +1,221 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# utils
```go
import "katenary.io/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/utils/utils.go#L35>)
```go
func Int32Ptr(i int32) *int32
```
Int32Ptr returns a pointer to an int32.
<a name="PathToName"></a>
## func [PathToName](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/utils/utils.go#L38>)
```go
func StrPtr(s string) *string
```
StrPtr returns a pointer to a string.
<a name="TplName"></a>
## func [TplName](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/utils/icons.go#L25>)
```go
func Warn(msg ...any)
```
Warn prints a warning message
<a name="WordWrap"></a>
## func [WordWrap](<https://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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://repo.katenary.io/Katenary/katenary/blob/master/internal/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>)

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

117
doc/docs/statics/main.css Normal file
View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 310 KiB

262
doc/docs/usage.md Normal file
View 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
View 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
View File

@@ -0,0 +1,74 @@
site_name: Katenary documentation
docs_dir: ./docs
plugins:
- search
- inline-svg
- manpage:
enabled: true
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 &copy; 2021 - 2024 - Katenary authors
extra:
generator: false
social:
- icon: fontawesome/brands/git
link: https://repo.katenary.io/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/internal/parser.md
- packages/internal/utils.md
- Generator:
- Index: packages/internal/generator.md
- ExtraFiles: packages/internal/generator/extrafiles.md
- labels:
- packages/internal/generator/labels.md
- Labelstructs: packages/internal/generator/labels/labelstructs.md
- KatenaryFile: packages/internal/generator/katenaryfile.md

View 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
View 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
View 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]

View File

@@ -0,0 +1,796 @@
'\" t
.\" Automatically generated by Pandoc 3.1.11.1
.\"
.TH "Katenary" "1" "2025-08-21" "mkdocs-manpage v2.0.1" "Katenary helm chart generator"
.SH Basic Usage
Basically, you can use \f[CR]katenary\f[R] to transpose a docker\-compose file (or any compose file compatible with \f[CR]podman\-compose\f[R] and \f[CR]docker\-compose\f[R]) to a configurable Helm Chart.
This resulting helm chart can be installed with \f[CR]helm\f[R] command to your Kubernetes cluster.
.PP
For very basic compose files, without any specific configuration, Katenary will create a working helm chart using the simple command line:
.IP
.EX
katenary convert
.EE
.PP
This will create a \f[CR]chart\f[R] directory with the helm chart inside.
.PP
But, in general, you will need to add a few configurations to help Katenary to transpose the compose file to a working helm chart.
.PP
There are two ways to configure Katenary:
.IP \[bu] 2
Using the compose files, adding labels to the services
.IP \[bu] 2
Using a specific file named \f[CR]katenary.yaml\f[R]
.PP
The Katenary file \f[CR]katenary.yaml\f[R] has benefits over the labels in the compose file:
.IP \[bu] 2
you can validate the configuration with a schema, and use completion in your editor
.IP \[bu] 2
you separate the configuration and leave the compose file \[dq]intact\[dq]
.IP \[bu] 2
the syntax is a bit simpler, instead of using \f[CR]katenary.v3/xxx: |\-\f[R] you can use \f[CR]xxx: ...\f[R]
.PP
But: \f[B]this implies that you have to maintain two files if the compose file changes.\f[R]
.PP
For example.
With \[dq]labels\[dq], you should do:
.IP
.EX
# 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: \[dq]{{ .Release.Name }}\-database\[dq]
.EE
.PP
Using a Katenary file, you can do:
.IP
.EX
# 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: \[dq]{{ .Release.Name }}\-database\[dq]
.EE
.PP
YAML in multiline label
.PP
Compose only accept text label.
So, to put a complete YAML content in the target label, you need to use a pipe char (\f[CR]|\f[R] or \f[CR]|\-\f[R]) and to \f[B]indent\f[R] your content.
.PP
For example :
.IP
.EX
labels:
# your labels
foo: bar
# katenary labels with multiline
katenary.v3/ingress: |\-
hostname: my.website.tld
port: 80
katenary.v3/ports: |\-
\- 1234
.EE
.PP
Katenary transforms compose services this way:
.IP \[bu] 2
Takes the service and create a \[dq]Deployment\[dq] file
.IP \[bu] 2
if a port is declared, Katenary creates a service (\f[CR]ClusterIP\f[R])
.IP \[bu] 2
if a port is exposed, Katenary creates a service (\f[CR]NodePort\f[R])
.IP \[bu] 2
environment variables will be stored inside a \f[CR]ConfigMap\f[R]
.IP \[bu] 2
image, tags, and ingresses configuration are also stored in \f[CR]values.yaml\f[R] file
.IP \[bu] 2
if named volumes are declared, Katenary create \f[CR]PersistentVolumeClaims\f[R] \- not enabled in values file
.IP \[bu] 2
\f[CR]depends_on\f[R] needs that the pointed service declared a port.
If not, you can use labels to inform Katenary
.PP
For any other specific configuration, like binding local files as \f[CR]ConfigMap\f[R], bind variables, add values with documentation, etc.
You\[aq]ll need to use labels.
.PP
Katenary can also configure containers grouping in pods, declare dependencies, ignore some services, force variables as secrets, mount files as \f[CR]configMap\f[R], and many others things.
To adapt the helm chart generation, you will need to use some specific labels.
.PP
For more complete label usage, see the labels page.
.PP
Overriding file
.PP
It could be sometimes more convinient to separate the configuration related to Katenary inside a secondary file.
.PP
Instead of adding labels inside the \f[CR]compose.yaml\f[R] file, you can create a file named \f[CR]compose.katenary.yaml\f[R] and declare your labels inside.
Katenary will detect it by default.
.PP
\f[B]No need to precise the file in the command line.\f[R]
.SS Make conversion
After having installed \f[CR]katenary\f[R], the standard usage is to call:
.IP
.EX
katenary convert
.EE
.PP
It will search standard compose files in the current directory and try to create a helm chart in \[dq]chart\[dq] directory.
.PP
Info
.PP
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 \f[CR]docker\-compose\f[R] and \f[CR]podman\-compose\f[R] do it.
.PP
Of course, you can provide others files than the default with (cumulative) \f[CR]\-c\f[R] options:
.IP
.EX
katenary convert \-c file1.yaml \-c file2.yaml
.EE
.SS Some common labels to use
Katenary proposes a lot of labels to configure the helm chart generation, but some are very important.
.PP
Info
.PP
For more complete label usage, see the labels page.
.SS Work with Depends On?
Kubernetes does not provide service or pod starting detection from others pods.
But Katenary will create \f[CR]initContainer\f[R] to make you able to wait for a service to respond.
But you\[aq]ll probably need to adapt a bit the compose file.
.PP
See this compose file:
.IP
.EX
version: \[dq]3\[dq]
services:
webapp:
image: php:8\-apache
depends_on:
\- database
database:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: foobar
.EE
.PP
In this case, \f[CR]webapp\f[R] needs to know the \f[CR]database\f[R] port because the \f[CR]depends_on\f[R] points on it and Kubernetes has not (yet) solution to check the database startup.
Katenary wants to create a \f[CR]initContainer\f[R] to hit on the related service.
So, instead of exposing the port in the compose definition, let\[aq]s declare this to Katenary with labels:
.IP
.EX
version: \[dq]3\[dq]
services:
webapp:
image: php:8\-apache
depends_on:
\- database
database:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: foobar
labels:
katenary.v3/ports: |\-
\- 3306
.EE
.SS Declare ingresses
It\[aq]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).
.PP
Katenary can create this resource for you.
You just need to declare the hostname and the port to bind.
.IP
.EX
services:
webapp:
image: ...
ports: 8080:5050
labels:
katenary.v3/ingress: |\-
# the target port is 5050 wich is the \[dq]service\[dq] port
port: 5050
hostname: myapp.example.com
.EE
.PP
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.
.SS 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.
.PP
With a compose file, there is no problem as Docker/Podman allows resolving the name by container name:
.IP
.EX
services:
webapp:
image: php:7\-apache
environment:
DB_HOST: database
database:
image: mariadb
.EE
.PP
Katenary prefixes the services with \f[CR]{{ .Release.Name }}\f[R] (to make it possible to install the application several times in a namespace), so you need to \[dq]remap\[dq] the environment variable to the right one.
.IP
.EX
services:
webapp:
image: php:7\-apache
environment:
DB_HOST: database
labels:
katenary.v3/mapenv: |\-
DB_HOST: \[dq]{{ .Release.Name }}\-database\[dq]
database:
image: mariadb
.EE
.PP
This label can be used to map others environment for any others reason.
E.g.
to change an informational environment variable.
.IP
.EX
services:
webapp:
#...
environment:
RUNNING: docker
labels:
katenary.v3/mapenv: |\-
RUNNING: kubernetes
.EE
.PP
In the above example, \f[CR]RUNNING\f[R] will be set to \f[CR]kubernetes\f[R] when you\[aq]ll deploy the application with helm, and it\[aq]s \f[CR]docker\f[R] for \[dq]Podman\[dq] and \[dq]Docker\[dq] executions.
.SH Labels documentation
Katenary proposes labels to set in \f[CR]compose.yaml\f[R] 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 \f[I]here\-doc\f[R] syntax using \f[CR]|\f[R] to use YAML multiline as value.
.IP
.EX
label\-name: |\-
# this is actually a multiline string here
key1: value1
key2: value2
.EE
.PP
Katenary will try to \f[I]Unmarshal\f[R] these labels.
.SS Label list and types
.PP
.TS
tab(@);
l l l.
T{
Label name
T}@T{
Description
T}@T{
Type
T}
_
T{
\f[CR]katenary.v3/configmap\-files\f[R]
T}@T{
Inject files as Configmap.
T}@T{
\f[CR][]string\f[R]
T}
T{
\f[CR]katenary.v3/cronjob\f[R]
T}@T{
Create a cronjob from the service.
T}@T{
\f[CR]object\f[R]
T}
T{
\f[CR]katenary.v3/dependencies\f[R]
T}@T{
Add Helm dependencies to the service.
T}@T{
\f[CR][]object\f[R]
T}
T{
\f[CR]katenary.v3/description\f[R]
T}@T{
Description of the service
T}@T{
\f[CR]string\f[R]
T}
T{
\f[CR]katenary.v3/env\-from\f[R]
T}@T{
Add environment variables from another service.
T}@T{
\f[CR][]string\f[R]
T}
T{
\f[CR]katenary.v3/exchange\-volumes\f[R]
T}@T{
Add exchange volumes (empty directory on the node) to share data
T}@T{
\f[CR][]object\f[R]
T}
T{
\f[CR]katenary.v3/health\-check\f[R]
T}@T{
Health check to be added to the deployment.
T}@T{
\f[CR]object\f[R]
T}
T{
\f[CR]katenary.v3/ignore\f[R]
T}@T{
Ignore the service
T}@T{
\f[CR]bool\f[R]
T}
T{
\f[CR]katenary.v3/ingress\f[R]
T}@T{
Ingress rules to be added to the service.
T}@T{
\f[CR]object\f[R]
T}
T{
\f[CR]katenary.v3/main\-app\f[R]
T}@T{
Mark the service as the main app.
T}@T{
\f[CR]bool\f[R]
T}
T{
\f[CR]katenary.v3/map\-env\f[R]
T}@T{
Map env vars from the service to the deployment.
T}@T{
\f[CR]map[string]string\f[R]
T}
T{
\f[CR]katenary.v3/ports\f[R]
T}@T{
Ports to be added to the service.
T}@T{
\f[CR][]uint32\f[R]
T}
T{
\f[CR]katenary.v3/same\-pod\f[R]
T}@T{
Move the same\-pod deployment to the target deployment.
T}@T{
\f[CR]string\f[R]
T}
T{
\f[CR]katenary.v3/secrets\f[R]
T}@T{
Env vars to be set as secrets.
T}@T{
\f[CR][]string\f[R]
T}
T{
\f[CR]katenary.v3/values\f[R]
T}@T{
Environment variables to be added to the values.yaml
T}@T{
\f[CR][]string or map[string]string\f[R]
T}
T{
\f[CR]katenary.v3/values\-from\f[R]
T}@T{
Add values from another service.
T}@T{
\f[CR]map[string]string\f[R]
T}
.TE
.SS Detailed description
.SS katenary.v3/configmap\-files
Inject files as Configmap.
.PP
\f[B]Type\f[R]: \f[CR][]string\f[R]
.PP
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.
.PP
If it is a directory, all files inside it are added to the ConfigMap.
.PP
If the directory as subdirectories, so one configmap per subpath are created.
.PP
Warning
.PP
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.
.PP
\f[B]Example:\f[R]
.IP
.EX
volumes
\- ./conf.d:/etc/nginx/conf.d
labels:
katenary.v3/configmap\-files: |\-
\- ./conf.d
.EE
.SS katenary.v3/cronjob
Create a cronjob from the service.
.PP
\f[B]Type\f[R]: \f[CR]object\f[R]
.PP
This adds a cronjob to the chart.
.PP
The label value is a YAML object with the following attributes: \- command: the command to be executed \- schedule: the cron schedule (cron format or \[at]every where \[dq]every\[dq] 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
.PP
\f[B]Example:\f[R]
.IP
.EX
labels:
katenary.v3/cronjob: |\-
command: echo \[dq]hello world\[dq]
schedule: \[dq]* */1 * * *\[dq] # or \[at]hourly for example
.EE
.SS katenary.v3/dependencies
Add Helm dependencies to the service.
.PP
\f[B]Type\f[R]: \f[CR][]object\f[R]
.PP
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.
.PP
It\[aq]s a list of objects with the following attributes:
.IP \[bu] 2
name: the name of the dependency
.IP \[bu] 2
repository: the repository of the dependency
.IP \[bu] 2
alias: the name of the dependency in values.yaml (optional)
.IP \[bu] 2
values: the values to be set in values.yaml (optional)
.PP
Info
.PP
Katenary doesn\[aq]t update the helm depenedencies by default.
.PP
Use \f[CR]\-\-helm\-update\f[R] (or \f[CR]\-u\f[R]) flag to update the dependencies.
.PP
example: \f[CR]katenary convert \-u\f[R]
.PP
By setting an alias, it is possible to change the name of the dependency in values.yaml.
.PP
\f[B]Example:\f[R]
.IP
.EX
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
.EE
.SS katenary.v3/description
Description of the service
.PP
\f[B]Type\f[R]: \f[CR]string\f[R]
.PP
This replaces the default comment in values.yaml file to the given description.
It is useful to document the service and configuration.
.PP
The value can be set with a documentation in multiline format.
.PP
\f[B]Example:\f[R]
.IP
.EX
labels:
katenary.v3/description: |\-
This is a description of the service.
It can be multiline.
.EE
.SS katenary.v3/env\-from
Add environment variables from another service.
.PP
\f[B]Type\f[R]: \f[CR][]string\f[R]
.PP
It adds environment variables from another service to the current service.
.PP
\f[B]Example:\f[R]
.IP
.EX
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
.EE
.SS katenary.v3/exchange\-volumes
Add exchange volumes (empty directory on the node) to share data
.PP
\f[B]Type\f[R]: \f[CR][]object\f[R]
.PP
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 \[dq]same pod\[dq] logic.
For example to let PHP\-FPM and Nginx share the same direcotory.
.PP
This will create:
.IP \[bu] 2
an \f[CR]emptyDir\f[R] volume in the deployment
.IP \[bu] 2
a \f[CR]voumeMount\f[R] in the pod for \f[B]each container\f[R]
.IP \[bu] 2
a \f[CR]initContainer\f[R] for each definition
.PP
Fields: \- name: the name of the volume (manadatory) \- mountPath: the path where the volume is mounted in the pod (optional, default is \f[CR]/opt\f[R]) \- init: a command to run to initialize the volume with data (optional)
.PP
Warning
.PP
This is highly experimental.
This is mainly useful when using the \[dq]same\-pod\[dq] label.
.PP
\f[B]Example:\f[R]
.IP
.EX
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
.EE
.SS katenary.v3/health\-check
Health check to be added to the deployment.
.PP
\f[B]Type\f[R]: \f[CR]object\f[R]
.PP
Health check to be added to the deployment.
.PP
\f[B]Example:\f[R]
.IP
.EX
labels:
katenary.v3/health\-check: |\-
livenessProbe:
httpGet:
path: /health
port: 8080
.EE
.SS katenary.v3/ignore
Ignore the service
.PP
\f[B]Type\f[R]: \f[CR]bool\f[R]
.PP
Ingoring a service to not be exported in helm chart.
.PP
\f[B]Example:\f[R]
.IP
.EX
labels:
katenary.v3/ignore: \[dq]true\[dq]
.EE
.SS katenary.v3/ingress
Ingress rules to be added to the service.
.PP
\f[B]Type\f[R]: \f[CR]object\f[R]
.PP
Declare an ingress rule for the service.
The port should be exposed or declared with \f[CR]katenary.v3/ports\f[R].
.PP
\f[B]Example:\f[R]
.IP
.EX
labels:
katenary.v3/ingress: |\-
port: 80
hostname: mywebsite.com (optional)
.EE
.SS katenary.v3/main\-app
Mark the service as the main app.
.PP
\f[B]Type\f[R]: \f[CR]bool\f[R]
.PP
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.
.PP
Warning
.PP
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.
.PP
\f[B]Example:\f[R]
.IP
.EX
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\[aq]t change the \[dq]tag\[dq] attribute in values.yaml
katenary.v3/main\-app: true
.EE
.SS katenary.v3/map\-env
Map env vars from the service to the deployment.
.PP
\f[B]Type\f[R]: \f[CR]map[string]string\f[R]
.PP
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: \f[CR]{{ tpl .Release.Name . }}\f[R].
.PP
If you use \f[CR]__APP__\f[R] in the value, it will be replaced by the Chart name.
.PP
\f[B]Example:\f[R]
.IP
.EX
env:
DB_HOST: database
RUNNING: docker
OTHER: value
labels:
katenary.v3/map\-env: |\-
RUNNING: kubernetes
DB_HOST: \[aq]{{ include \[dq]__APP__.fullname\[dq] . }}\-database\[aq]
.EE
.SS katenary.v3/ports
Ports to be added to the service.
.PP
\f[B]Type\f[R]: \f[CR][]uint32\f[R]
.PP
Only useful for services without exposed port.
It is mandatory if the service is a dependency of another service.
.PP
\f[B]Example:\f[R]
.IP
.EX
labels:
katenary.v3/ports: |\-
\- 8080
\- 8081
.EE
.SS katenary.v3/same\-pod
Move the same\-pod deployment to the target deployment.
.PP
\f[B]Type\f[R]: \f[CR]string\f[R]
.PP
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.
.PP
Note that volume and VolumeMount are copied from the source to the target deployment.
.PP
\f[B]Example:\f[R]
.IP
.EX
web:
image: nginx:1.19
php:
image: php:7.4\-fpm
labels:
katenary.v3/same\-pod: web
.EE
.SS katenary.v3/secrets
Env vars to be set as secrets.
.PP
\f[B]Type\f[R]: \f[CR][]string\f[R]
.PP
This label allows setting the environment variables as secrets.
The variable is removed from the environment and added to a secret object.
.PP
The variable can be set to the \f[CR]katenary.v3/values\f[R] too, so the secret value can be configured in values.yaml
.PP
\f[B]Example:\f[R]
.IP
.EX
env:
PASSWORD: a very secret password
NOT_A_SECRET: a public value
labels:
katenary.v3/secrets: |\-
\- PASSWORD
.EE
.SS katenary.v3/values
Environment variables to be added to the values.yaml
.PP
\f[B]Type\f[R]: \f[CR][]string or map[string]string\f[R]
.PP
By default, all environment variables in the \[dq]env\[dq] and environment files are added to configmaps with the static values set.
This label allows adding environment variables to the values.yaml file.
.PP
Note that the value inside the configmap is \f[CR]{{ tpl vaname . }}\f[R], so you can set the value to a template that will be rendered with the values.yaml file.
.PP
The value can be set with a documentation.
This may help to understand the purpose of the variable.
.PP
\f[B]Example:\f[R]
.IP
.EX
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.
.EE
.SS katenary.v3/values\-from
Add values from another service.
.PP
\f[B]Type\f[R]: \f[CR]map[string]string\f[R]
.PP
This label allows adding values from another service to the current service.
It avoid duplicating values, environment or secrets that should be the same.
.PP
The key is the value to be added, and the value is the \[dq]key\[dq] to fetch in the form \f[CR]service_name.environment_name\f[R].
.PP
\f[B]Example:\f[R]
.IP
.EX
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\[aq]s duplicated in docker / podman
DB_USER: myuser
DB_PASSWORD: mypassword
labels:
# removes the duplicated, use the configMap and secrets from \[dq]database\[dq]
katenary.v3/values\-from: |\-
DB_USER: database.MARIADB_USER
DB_PASSWORD: database.MARIADB_PASSWORD
.EE

View File

@@ -1,8 +0,0 @@
package generator
import "testing"
func TestColor(t *testing.T) {
Red("Red text")
Grey("Grey text")
}

View File

@@ -1,456 +0,0 @@
package generator
import (
"fmt"
"io/ioutil"
"katenary/compose"
"katenary/helm"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"errors"
)
var servicesMap = make(map[string]int)
var serviceWaiters = make(map[string][]chan int)
var locker = &sync.Mutex{}
const (
ICON_PACKAGE = "📦"
ICON_SERVICE = "🔌"
ICON_SECRET = "🔏"
ICON_CONF = "📝"
ICON_STORE = "⚡"
ICON_INGRESS = "🌐"
)
// Values is kept in memory to create a values.yaml file.
var Values = make(map[string]map[string]interface{})
var VolumeValues = make(map[string]map[string]map[string]interface{})
var dependScript = `
OK=0
echo "Checking __service__ port"
while [ $OK != 1 ]; do
echo -n "."
nc -z {{ .Release.Name }}-__service__ __port__ && OK=1
sleep 1
done
echo
echo "Done"
`
// Create a Deployment for a given compose.Service. It returns a list of objects: a Deployment and a possible Service (kubernetes represnetation as maps).
func CreateReplicaObject(name string, s *compose.Service) chan interface{} {
ret := make(chan interface{}, len(s.Ports)+len(s.Expose)+1)
go parseService(name, s, ret)
return ret
}
// This function will try to yied deployment and services based on a service from the compose file structure.
func parseService(name string, s *compose.Service, ret chan interface{}) {
Magenta(ICON_PACKAGE+" Generating deployment for ", name)
o := helm.NewDeployment(name)
container := helm.NewContainer(name, s.Image, s.Environment, s.Labels)
// prepare secrets
secretsFiles := make([]string, 0)
if v, ok := s.Labels[helm.LABEL_ENV_SECRET]; ok {
secretsFiles = strings.Split(v, ",")
}
// manage environment files (env_file in compose)
for _, envfile := range s.EnvFiles {
f := strings.ReplaceAll(envfile, "_", "-")
f = strings.ReplaceAll(f, ".env", "")
f = strings.ReplaceAll(f, ".", "-")
cf := f + "-" + name
isSecret := false
for _, s := range secretsFiles {
if s == envfile {
isSecret = true
}
}
var store helm.InlineConfig
if !isSecret {
Bluef(ICON_CONF+" Generating configMap %s\n", cf)
store = helm.NewConfigMap(cf)
} else {
Bluef(ICON_SECRET+" Generating secret %s\n", cf)
store = helm.NewSecret(cf)
}
if err := store.AddEnvFile(envfile); err != nil {
ActivateColors = true
Red(err.Error())
ActivateColors = false
os.Exit(2)
}
container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{
"configMapRef": {
"name": store.Metadata().Name,
},
})
ret <- store
}
// check the image, and make it "variable" in values.yaml
container.Image = "{{ .Values." + name + ".image }}"
Values[name] = map[string]interface{}{
"image": s.Image,
}
// manage ports
exists := make(map[int]string)
for _, port := range s.Ports {
_p := strings.Split(port, ":")
port = _p[0]
if len(_p) > 1 {
port = _p[1]
}
portNumber, _ := strconv.Atoi(port)
portName := name
for _, n := range exists {
if name == n {
portName = fmt.Sprintf("%s-%d", name, portNumber)
}
}
container.Ports = append(container.Ports, &helm.ContainerPort{
Name: portName,
ContainerPort: portNumber,
})
exists[portNumber] = name
}
// manage the "expose" section to be a NodePort in Kubernetes
for _, port := range s.Expose {
if _, exist := exists[port]; exist {
continue
}
container.Ports = append(container.Ports, &helm.ContainerPort{
Name: name,
ContainerPort: port,
})
}
// Prepare volumes
volumes := make([]map[string]interface{}, 0)
mountPoints := make([]interface{}, 0)
configMapsVolumes := make([]string, 0)
if v, ok := s.Labels[helm.LABEL_VOL_CM]; ok {
configMapsVolumes = strings.Split(v, ",")
}
for _, volume := range s.Volumes {
parts := strings.Split(volume, ":")
volname := parts[0]
volepath := parts[1]
isCM := false
for _, cmVol := range configMapsVolumes {
cmVol = strings.TrimSpace(cmVol)
if volname == cmVol {
isCM = true
break
}
}
if !isCM && (strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/")) {
// local volume cannt be mounted
ActivateColors = true
Redf("You cannot, at this time, have local volume in %s deployment\n", name)
ActivateColors = false
continue
}
if isCM {
// the volume is a path and it's explicitally asked to be a configmap in labels
cm := buildCMFromPath(volname)
volname = strings.Replace(volname, "./", "", 1)
volname = strings.ReplaceAll(volname, ".", "-")
cm.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + volname + "-" + name
// build a configmap from the volume path
volumes = append(volumes, map[string]interface{}{
"name": volname,
"configMap": map[string]string{
"name": cm.K8sBase.Metadata.Name,
},
})
mountPoints = append(mountPoints, map[string]interface{}{
"name": volname,
"mountPath": volepath,
})
ret <- cm
} else {
pvc := helm.NewPVC(name, volname)
volumes = append(volumes, map[string]interface{}{
"name": volname,
"persistentVolumeClaim": map[string]string{
"claimName": "{{ .Release.Name }}-" + volname,
},
})
mountPoints = append(mountPoints, map[string]interface{}{
"name": volname,
"mountPath": volepath,
})
Yellow(ICON_STORE+" Generate volume values for ", volname, " in deployment ", name)
locker.Lock()
if _, ok := VolumeValues[name]; !ok {
VolumeValues[name] = make(map[string]map[string]interface{})
}
VolumeValues[name][volname] = map[string]interface{}{
"enabled": false,
"capacity": "1Gi",
}
locker.Unlock()
ret <- pvc
}
}
container.VolumeMounts = mountPoints
o.Spec.Template.Spec.Volumes = volumes
o.Spec.Template.Spec.Containers = []*helm.Container{container}
// Add some labels
o.Spec.Selector = map[string]interface{}{
"matchLabels": buildSelector(name, s),
}
o.Spec.Template.Metadata.Labels = buildSelector(name, s)
// Now, for "depends_on" section, it's a bit tricky...
// We need to detect "others" services, but we probably not have parsed them yet, so
// we will wait for them for a while.
initContainers := make([]*helm.Container, 0)
for _, dp := range s.DependsOn {
c := helm.NewContainer("check-"+dp, "busybox", nil, s.Labels)
command := strings.ReplaceAll(strings.TrimSpace(dependScript), "__service__", dp)
foundPort := -1
if defaultPort, err := getPort(dp); err != nil {
// BUG: Sometimes the chan remains opened
foundPort = <-waitPort(dp)
} else {
foundPort = defaultPort
}
if foundPort == -1 {
log.Fatalf(
"ERROR, the %s service is waiting for %s port number, "+
"but it is never discovered. You must declare at least one port in "+
"the \"ports\" section of the service in the docker-compose file",
name,
dp,
)
}
command = strings.ReplaceAll(command, "__port__", strconv.Itoa(foundPort))
c.Command = []string{
"sh",
"-c",
command,
}
initContainers = append(initContainers, c)
}
o.Spec.Template.Spec.InitContainers = initContainers
// Then, create services for "ports" and "expose" section
if len(s.Ports) > 0 || len(s.Expose) > 0 {
for _, s := range createService(name, s) {
ret <- s
}
}
// Special case, it there is no "ports", so there is no associated services...
// But... some other deployment can wait for it, so we alert that this deployment hasn't got any
// associated service.
if len(s.Ports) == 0 {
locker.Lock()
// alert any current or **futur** waiters that this service is not exposed
go func() {
for {
select {
case <-time.Tick(1 * time.Millisecond):
for _, c := range serviceWaiters[name] {
c <- -1
close(c)
}
}
}
}()
locker.Unlock()
}
// add the volumes in Values
if len(VolumeValues[name]) > 0 {
locker.Lock()
Values[name]["persistence"] = VolumeValues[name]
locker.Unlock()
}
// the deployment is ready, give it
ret <- o
// and then, we can say that it's the end
ret <- nil
}
// Create a service (k8s).
func createService(name string, s *compose.Service) []interface{} {
ret := make([]interface{}, 0)
Magenta(ICON_SERVICE+" Generating service for ", name)
ks := helm.NewService(name)
for i, p := range s.Ports {
port := strings.Split(p, ":")
src, _ := strconv.Atoi(port[0])
target := src
if len(port) > 1 {
target, _ = strconv.Atoi(port[1])
}
ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(target, target))
if i == 0 {
detected(name, target)
}
}
ks.Spec.Selector = buildSelector(name, s)
ret = append(ret, ks)
if v, ok := s.Labels[helm.LABEL_INGRESS]; ok {
port, err := strconv.Atoi(v)
if err != nil {
log.Fatalf("The given port \"%v\" as ingress port in %s service is not an integer\n", v, name)
}
Cyanf(ICON_INGRESS+" Create an ingress for port %d on %s service\n", port, name)
ing := createIngress(name, port, s)
ret = append(ret, ing)
}
if len(s.Expose) > 0 {
Magenta(ICON_SERVICE+" Generating service for ", name+"-external")
ks := helm.NewService(name + "-external")
ks.Spec.Type = "NodePort"
for _, p := range s.Expose {
ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(p, p))
}
ks.Spec.Selector = buildSelector(name, s)
ret = append(ret, ks)
}
return ret
}
// Create an ingress.
func createIngress(name string, port int, s *compose.Service) *helm.Ingress {
ingress := helm.NewIngress(name)
Values[name]["ingress"] = map[string]interface{}{
"class": "nginx",
"host": name + "." + helm.Appname + ".tld",
"enabled": false,
}
ingress.Spec.Rules = []helm.IngressRule{
{
Host: fmt.Sprintf("{{ .Values.%s.ingress.host }}", name),
Http: helm.IngressHttp{
Paths: []helm.IngressPath{{
Path: "/",
PathType: "Prefix",
Backend: helm.IngressBackend{
Service: helm.IngressService{
Name: "{{ .Release.Name }}-" + name,
Port: map[string]interface{}{
"number": port,
},
},
},
}},
},
},
}
ingress.SetIngressClass(name)
return ingress
}
// This function is called when a possible service is detected, it append the port in a map to make others
// to be able to get the service name. It also try to send the data to any "waiter" for this service.
func detected(name string, port int) {
locker.Lock()
servicesMap[name] = port
go func() {
cx := serviceWaiters[name]
for _, c := range cx {
if v, ok := servicesMap[name]; ok {
c <- v
//close(c)
}
}
}()
locker.Unlock()
}
func getPort(name string) (int, error) {
if v, ok := servicesMap[name]; ok {
return v, nil
}
return -1, errors.New("Not found")
}
// Waits for a service to be discovered. Sometimes, a deployment depends on another one. See the detected() function.
func waitPort(name string) chan int {
locker.Lock()
c := make(chan int, 0)
serviceWaiters[name] = append(serviceWaiters[name], c)
go func() {
if v, ok := servicesMap[name]; ok {
c <- v
//close(c)
}
}()
locker.Unlock()
return c
}
func buildSelector(name string, s *compose.Service) map[string]string {
return map[string]string{
"katenary.io/component": name,
"katenary.io/release": "{{ .Release.Name }}",
}
}
func buildCMFromPath(path string) *helm.ConfigMap {
stat, err := os.Stat(path)
if err != nil {
return nil
}
files := make(map[string]string, 0)
if stat.IsDir() {
found, _ := filepath.Glob(path + "/*")
for _, f := range found {
if s, err := os.Stat(f); err != nil || s.IsDir() {
if err != nil {
fmt.Fprintf(os.Stderr, "An error occured reading volume path %s\n", err.Error())
} else {
ActivateColors = true
Yellowf("Warning, %s is a directory, at this time we only "+
"can create configmap for first level file list\n", f)
ActivateColors = false
}
continue
}
_, filename := filepath.Split(f)
c, _ := ioutil.ReadFile(f)
files[filename] = string(c)
}
}
cm := helm.NewConfigMap("")
cm.Data = files
return cm
}

View File

@@ -1,89 +0,0 @@
package generator
import (
"fmt"
"os"
"sync"
)
type Color int
var ActivateColors = false
const (
GREY Color = 30 + iota
RED
GREEN
YELLOW
BLUE
MAGENTA
CYAN
)
var waiter = sync.Mutex{}
func color(c Color, args ...interface{}) {
if !ActivateColors {
fmt.Println(args...)
return
}
waiter.Lock()
fmt.Fprintf(os.Stdout, "\x1b[%dm", c)
fmt.Fprint(os.Stdout, args...)
fmt.Fprintf(os.Stdout, "\x1b[0m\n")
waiter.Unlock()
}
func colorf(c Color, format string, args ...interface{}) {
if !ActivateColors {
fmt.Printf(format, args...)
return
}
waiter.Lock()
fmt.Fprintf(os.Stdout, "\x1b[%dm", c)
fmt.Fprintf(os.Stdout, format, args...)
fmt.Fprintf(os.Stdout, "\x1b[0m")
waiter.Unlock()
}
func Grey(args ...interface{}) {
color(GREY, args...)
}
func Red(args ...interface{}) {
color(RED, args...)
}
func Green(args ...interface{}) {
color(GREEN, args...)
}
func Yellow(args ...interface{}) {
color(YELLOW, args...)
}
func Blue(args ...interface{}) {
color(BLUE, args...)
}
func Magenta(args ...interface{}) {
color(MAGENTA, args...)
}
func Greyf(format string, args ...interface{}) {
colorf(GREY, format, args...)
}
func Redf(format string, args ...interface{}) {
colorf(RED, format, args...)
}
func Greenf(format string, args ...interface{}) {
colorf(GREEN, format, args...)
}
func Yellowf(format string, args ...interface{}) {
colorf(YELLOW, format, args...)
}
func Bluef(format string, args ...interface{}) {
colorf(BLUE, format, args...)
}
func Magentaf(format string, args ...interface{}) {
colorf(MAGENTA, format, args...)
}
func Cyanf(format string, args ...interface{}) {
colorf(CYAN, format, args...)
}

58
go.mod
View File

@@ -1,5 +1,57 @@
module katenary
module katenary.io
go 1.16
go 1.25
require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
require (
github.com/compose-spec/compose-go v1.20.2
github.com/invopop/jsonschema v0.13.0
github.com/mitchellh/go-wordwrap v1.0.1
github.com/spf13/cobra v1.9.1
github.com/thediveo/netdb v1.1.2
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.33.4
k8s.io/apimachinery v0.33.4
sigs.k8s.io/yaml v1.6.0
)
require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
)

170
go.sum
View File

@@ -1,4 +1,168 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/compose-spec/compose-go v1.20.2 h1:u/yfZHn4EaHGdidrZycWpxXgFffjYULlTbRfJ51ykjQ=
github.com/compose-spec/compose-go v1.20.2/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/thediveo/netdb v1.1.2 h1:XdLx/YJPutxrSkPYtmCAIY5sgAvxtkS1Tz+Z0UX2I+U=
github.com/thediveo/netdb v1.1.2/go.mod h1:KJczM//7VIIiovQO1qDooHvM8+0pt6RdRt3rVDZxEGM=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk=
k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc=
k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s=
k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -1,101 +0,0 @@
package helm
import (
"errors"
"fmt"
"io/ioutil"
"strings"
)
// InlineConfig is made to represent a configMap or a secret
type InlineConfig interface {
AddEnvFile(filename string) error
Metadata() *Metadata
}
type ConfigMap struct {
*K8sBase `yaml:",inline"`
Data map[string]string `yaml:"data"`
}
func NewConfigMap(name string) *ConfigMap {
base := NewBase()
base.ApiVersion = "v1"
base.Kind = "ConfigMap"
base.Metadata.Name = "{{ .Release.Name }}-" + name
base.Metadata.Labels[K+"/component"] = name
return &ConfigMap{
K8sBase: base,
Data: make(map[string]string),
}
}
func (c *ConfigMap) Metadata() *Metadata {
return c.K8sBase.Metadata
}
func (c *ConfigMap) AddEnvFile(file string) error {
content, err := ioutil.ReadFile(file)
if err != nil {
return err
}
lines := strings.Split(string(content), "\n")
for _, l := range lines {
l = strings.TrimSpace(l)
if len(l) == 0 {
continue
}
parts := strings.SplitN(l, "=", 2)
if len(parts) < 2 {
return errors.New("The environment file " + file + " is not valid")
}
c.Data[parts[0]] = parts[1]
}
return nil
}
type Secret struct {
*K8sBase `yaml:",inline"`
Data map[string]string `yaml:"data"`
}
func NewSecret(name string) *Secret {
base := NewBase()
base.ApiVersion = "v1"
base.Kind = "Secret"
base.Metadata.Name = "{{ .Release.Name }}-" + name
base.Metadata.Labels[K+"/component"] = name
return &Secret{
K8sBase: base,
Data: make(map[string]string),
}
}
func (s *Secret) AddEnvFile(file string) error {
content, err := ioutil.ReadFile(file)
if err != nil {
return err
}
lines := strings.Split(string(content), "\n")
for _, l := range lines {
l = strings.TrimSpace(l)
if len(l) == 0 {
continue
}
parts := strings.SplitN(l, "=", 2)
if len(parts) < 2 {
return errors.New("The environment file " + file + " is not valid")
}
s.Data[parts[0]] = fmt.Sprintf(`{{ "%s" | b64enc }}`, parts[1])
}
return nil
}
func (s *Secret) Metadata() *Metadata {
return s.K8sBase.Metadata
}

View File

@@ -1,88 +0,0 @@
package helm
import "strings"
// Deployment is a k8s deployment.
type Deployment struct {
*K8sBase `yaml:",inline"`
Spec *DepSpec `yaml:"spec"`
}
func NewDeployment(name string) *Deployment {
d := &Deployment{K8sBase: NewBase(), Spec: NewDepSpec()}
d.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name
d.K8sBase.ApiVersion = "apps/v1"
d.K8sBase.Kind = "Deployment"
d.K8sBase.Metadata.Labels[K+"/component"] = name
return d
}
type DepSpec struct {
Replicas int `yaml:"replicas"`
Selector map[string]interface{} `yaml:"selector"`
Template PodTemplate `yaml:"template"`
}
func NewDepSpec() *DepSpec {
return &DepSpec{
Replicas: 1,
}
}
type Value struct {
Name string `yaml:"name"`
Value interface{} `yaml:"value"`
}
type ContainerPort struct {
Name string
ContainerPort int `yaml:"containerPort"`
}
type Container struct {
Name string `yaml:"name,omitempty"`
Image string `yaml:"image"`
Ports []*ContainerPort `yaml:"ports,omitempty"`
Env []Value `yaml:"env,omitempty"`
EnvFrom []map[string]map[string]string `yaml:"envFrom,omitempty"`
Command []string `yaml:"command,omitempty"`
VolumeMounts []interface{} `yaml:"volumeMounts,omitempty"`
}
func NewContainer(name, image string, environment, labels map[string]string) *Container {
container := &Container{
Image: image,
Name: name,
Env: make([]Value, len(environment)),
EnvFrom: make([]map[string]map[string]string, 0),
}
// find bound environment variable to a service
toServices := make([]string, 0)
if bound, ok := labels[LABEL_ENV_SERVICE]; ok {
toServices = strings.Split(bound, ",")
}
idx := 0
for n, v := range environment {
for _, name := range toServices {
if name == n {
v = "{{ .Release.Name }}-" + v
}
}
container.Env[idx] = Value{Name: n, Value: v}
idx++
}
return container
}
type PodSpec struct {
InitContainers []*Container `yaml:"initContainers,omitempty"`
Containers []*Container `yaml:"containers"`
Volumes []map[string]interface{} `yaml:"volumes,omitempty"`
}
type PodTemplate struct {
Metadata Metadata `yaml:"metadata"`
Spec PodSpec `yaml:"spec"`
}

View File

@@ -1,52 +0,0 @@
package helm
type Ingress struct {
*K8sBase `yaml:",inline"`
Spec IngressSpec
}
func NewIngress(name string) *Ingress {
i := &Ingress{}
i.K8sBase = NewBase()
i.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name
i.K8sBase.Kind = "Ingress"
i.ApiVersion = "networking.k8s.io/v1"
i.K8sBase.Metadata.Labels[K+"/component"] = name
return i
}
func (i *Ingress) SetIngressClass(name string) {
class := "{{ .Values." + name + ".ingress.class }}"
i.Metadata.Annotations["kubernetes.io/ingress.class"] = class
i.Spec.IngressClassName = class
}
type IngressSpec struct {
IngressClassName string `yaml:"ingressClassName,omitempty"`
Rules []IngressRule
}
type IngressRule struct {
Host string
Http IngressHttp
}
type IngressHttp struct {
Paths []IngressPath
}
type IngressPath struct {
Path string
PathType string `yaml:"pathType"`
Backend IngressBackend
}
type IngressBackend struct {
Service IngressService
}
type IngressService struct {
Name string
Port map[string]interface{}
}

View File

@@ -1,24 +0,0 @@
package helm
import "strings"
var NOTES = `
Congratulations,
Your application is now deployed. This may take a while to be up and responding.
__list__
`
func GenNotes(ingressess map[string]*Ingress) string {
list := make([]string, 0)
for name, ing := range ingressess {
for _, r := range ing.Spec.Rules {
list = append(list, "{{ if .Values."+name+".ingress.enabled -}}\n- "+name+" is accessible on : http://"+r.Host+"\n{{- end }}")
}
}
return strings.ReplaceAll(NOTES, "__list__", strings.Join(list, "\n"))
}

View File

@@ -1,45 +0,0 @@
package helm
type Service struct {
*K8sBase `yaml:",inline"`
Spec *ServiceSpec `yaml:"spec"`
}
func NewService(name string) *Service {
s := &Service{
K8sBase: NewBase(),
Spec: NewServiceSpec(),
}
s.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + name
s.K8sBase.Kind = "Service"
s.K8sBase.ApiVersion = "v1"
s.K8sBase.Metadata.Labels[K+"/component"] = name
return s
}
type ServicePort struct {
Protocol string `yaml:"protocol"`
Port int `yaml:"port"`
TargetPort int `yaml:"targetPort"`
}
func NewServicePort(port, target int) *ServicePort {
return &ServicePort{
Protocol: "TCP",
Port: port,
TargetPort: port,
}
}
type ServiceSpec struct {
Selector map[string]string
Ports []*ServicePort
Type string `yaml:"type,omitempty"`
}
func NewServiceSpec() *ServiceSpec {
return &ServiceSpec{
Selector: make(map[string]string),
Ports: make([]*ServicePort, 0),
}
}

View File

@@ -1,30 +0,0 @@
package helm
type Storage struct {
*K8sBase `yaml:",inline"`
Spec *PVCSpec
}
func NewPVC(name, storageName string) *Storage {
pvc := &Storage{}
pvc.K8sBase = NewBase()
pvc.K8sBase.Kind = "PersistentVolumeClaim"
pvc.K8sBase.Metadata.Labels[K+"/pvc-name"] = storageName
pvc.K8sBase.ApiVersion = "v1"
pvc.K8sBase.Metadata.Name = "{{ .Release.Name }}-" + storageName
pvc.K8sBase.Metadata.Labels[K+"/component"] = name
pvc.Spec = &PVCSpec{
Resouces: map[string]interface{}{
"requests": map[string]string{
"storage": "{{ .Values." + name + ".persistence." + storageName + ".capacity }}",
},
},
AccessModes: []string{"ReadWriteOnce"},
}
return pvc
}
type PVCSpec struct {
Resouces map[string]interface{} `yaml:"resources"`
AccessModes []string `yaml:"accessModes"`
}

View File

@@ -1,88 +0,0 @@
package helm
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"os"
"strings"
)
const K = "katenary.io"
const (
LABEL_ENV_SECRET = K + "/secret-envfiles"
LABEL_PORT = K + "/ports"
LABEL_INGRESS = K + "/ingress"
LABEL_ENV_SERVICE = K + "/env-to-service"
LABEL_VOL_CM = K + "/configmap-volumes"
)
var Appname = ""
var Version = "1.0" // should be set from main.Version
type Kinded interface {
Get() string
}
type Signable interface {
BuildSHA(filename string)
}
type Named interface {
Name() string
}
type Metadata struct {
Name string `yaml:"name,omitempty"`
Labels map[string]string `yaml:"labels"`
Annotations map[string]string `yaml:"annotations,omitempty"`
}
func NewMetadata() *Metadata {
return &Metadata{
Name: "",
Labels: make(map[string]string),
Annotations: make(map[string]string),
}
}
type K8sBase struct {
ApiVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata *Metadata `yaml:"metadata"`
}
func NewBase() *K8sBase {
b := &K8sBase{
Metadata: NewMetadata(),
}
b.Metadata.Labels[K+"/project"] = GetProjectName()
b.Metadata.Labels[K+"/release"] = "{{ .Release.Name }}"
b.Metadata.Annotations[K+"/version"] = Version
return b
}
func (k *K8sBase) BuildSHA(filename string) {
c, _ := ioutil.ReadFile(filename)
sum := sha256.Sum256(c)
k.Metadata.Annotations[K+"/docker-compose-sha256"] = fmt.Sprintf("%x", string(sum[:]))
}
func (k *K8sBase) Get() string {
return k.Kind
}
func (k *K8sBase) Name() string {
return k.Metadata.Name
}
func GetProjectName() string {
if len(Appname) > 0 {
return Appname
}
p, _ := os.Getwd()
path := strings.Split(p, string(os.PathSeparator))
return path[len(path)-1]
}

65
install.sh Normal file
View File

@@ -0,0 +1,65 @@
#!/bin/sh
# Install the latest version of the Katenary detecting the right OS and architecture.
# Can be launched with the following command:
# sh <(curl -sSL https://raw.githubusercontent.com/Katenary/katenary/master/install.sh)
set -e
# Detect the OS and architecture
OS=$(uname)
ARCH=$(uname -m)
# Detect the home directory "bin" directory, it is commonly:
# - $HOME/.local/bin
# - $HOME/.bin
# - $HOME/bin
COMON_INSTALL_PATHS="$HOME/.local/bin $HOME/.bin $HOME/bin"
INSTALL_PATH=""
for p in $COMON_INSTALL_PATHS; do
if [ -d $p ]; then
INSTALL_PATH=$p
break
fi
done
# check if the user has write access to the INSTALL_PATH
if [ -z "$INSTALL_PATH" ]; then
INSTALL_PATH="/usr/local/bin"
if [ ! -w $INSTALL_PATH ]; then
echo "You don't have write access to $INSTALL_PATH"
echo "Please, run with sudo or install locally"
exit 1
fi
fi
# ensure that $INSTALL_PATH is in the PATH
if ! echo "$PATH" | grep -q "$INSTALL_PATH"; then
echo "Sorry, ${INSTALL_PATH} is not in the PATH"
echo "Please, add it to your PATH in your shell configuration file"
echo "then restart your shell and run this script again"
exit 1
fi
# Where to download the binary
BASE="https://github.com/Katenary/katenary/releases/latest/download/"
# for compatibility with older ARM versions
if [ $ARCH = "x86_64" ]; then
ARCH="amd64"
fi
BIN_URL="$BASE/katenary-$OS-$ARCH"
echo
echo "Downloading $BIN_URL"
T=$(mktemp -u)
curl -SL -# $BIN_URL -o $T || (echo "Failed to download katenary" && rm -f $T && exit 1)
mv "$T" "${INSTALL_PATH}/katenary"
chmod +x "${INSTALL_PATH}/katenary"
echo
echo "Installed to $INSTALL_PATH/katenary"
echo "Installation complete! Run 'katenary help' to get started."

427
internal/generator/chart.go Normal file
View File

@@ -0,0 +1,427 @@
package generator
import (
"fmt"
"log"
"maps"
"os"
"path/filepath"
"slices"
"strings"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/utils"
"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.LabelValuesFrom]; !ok {
return
}
mapping, err := labelstructs.GetValueFrom(service.Labels[labels.LabelValuesFrom])
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
}
}

View File

@@ -0,0 +1,158 @@
package generator
import (
"fmt"
"os"
"strings"
"testing"
"katenary.io/internal/generator/labels"
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))
}
}

View File

@@ -0,0 +1,255 @@
package generator
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"unicode/utf8"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/utils"
"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)
}

View File

@@ -0,0 +1,133 @@
package generator
import (
"fmt"
"io"
"os"
"regexp"
"testing"
"katenary.io/internal/generator/labels"
"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")
}
}
func TestRootConfigmapfile(t *testing.T) {
composeFile := `
services:
web:
image: nginx
volumes:
- ./foo.txt:/etc/foo.txt
labels:
%[1]s/configmap-files: |-
- ./foo.txt
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
fooTxt := "foo content"
fooFp, _ := os.Create("foo.txt")
io.WriteString(fooFp, fooTxt)
fooFp.Close()
output := internalCompileTest(t, "-s", "templates/web/statics/configmap.yaml")
configMap := v1.ConfigMap{}
if err := yaml.Unmarshal([]byte(output), &configMap); err != nil {
t.Errorf(unmarshalError, err)
}
if configMap.Data == nil {
t.Error("Expected configmap data to not be nil")
}
// if the configmap.Name ends by anything that is not alphanumeric, there is a problem
valid := regexp.MustCompile(`.*[a-zA-Z0-9]+$`)
if !valid.MatchString(configMap.Name) {
t.Errorf("ConfigMap name %s is not valid", configMap.Name)
}
}

View File

@@ -0,0 +1,700 @@
package generator
import (
"bytes"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"katenary.io/internal/generator/extrafiles"
"katenary.io/internal/generator/katenaryfile"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/parser"
"katenary.io/internal/utils"
"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
}

View File

@@ -0,0 +1,122 @@
package generator
import (
"log"
"strings"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/utils"
"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\" }}",
Args: []string{
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)
}

View 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].Args, " ")
if combinedCommand != "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)
}
}

View File

@@ -0,0 +1,738 @@
package generator
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/utils"
"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.Entrypoint,
Args: 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)
pathname = strings.TrimSpace(pathname)
if len(pathname) != 0 {
pathname += "-" + pathname
}
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)
}
}
}

View File

@@ -0,0 +1,565 @@
package generator
import (
"fmt"
"os"
"strings"
"testing"
"katenary.io/internal/generator/labels"
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].Args
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, " "))
}
}
func TestEntryPoint(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
entrypoint: /bin/foo
command: bar baz
`
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/web/deployment.yaml")
t.Logf("Output: %s", output)
deployment := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &deployment); err != nil {
t.Errorf(unmarshalError, err)
}
entryPoint := deployment.Spec.Template.Spec.Containers[0].Command
command := deployment.Spec.Template.Spec.Containers[0].Args
if entryPoint[0] != "/bin/foo" {
t.Errorf("Expected entrypoint to be /bin/foo, got %s", entryPoint[0])
}
if len(command) != 2 || command[0] != "bar" || command[1] != "baz" {
t.Errorf("Expected command to be 'bar baz', got %s", strings.Join(command, " "))
}
}

17
internal/generator/doc.go Normal file
View 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

View 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

View 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")
}

View 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!

View 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)
}
}
}

View 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()
}

View 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 }}

View 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])
}
}

View File

@@ -0,0 +1,433 @@
package generator
import (
"bytes"
"fmt"
"log"
"regexp"
"strings"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/utils"
"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.LabelValuesFrom]; 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.LabelValuesFrom] = 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
}
}

View File

@@ -0,0 +1,20 @@
package generator
import (
"regexp"
"katenary.io/internal/generator/labels"
)
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,
}
)

View 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 -}}

View File

@@ -0,0 +1,21 @@
package generator
import (
_ "embed"
"strings"
"katenary.io/internal/generator/labels"
)
// 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
}

View File

@@ -0,0 +1,204 @@
package generator
import (
"log"
"strings"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/utils"
"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
}

View File

@@ -0,0 +1,129 @@
package generator
import (
"fmt"
"os"
"testing"
"katenary.io/internal/generator/labels"
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)
}
}

View 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

View File

@@ -0,0 +1,166 @@
package katenaryfile
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
"reflect"
"strings"
"katenary.io/internal/generator/labels"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/utils"
"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 `yaml:"main-app,omitempty" json:"main-app,omitempty" jsonschema:"title=Is this service the main application"`
Values []StringOrMap `yaml:"values,omitempty" json:"values,omitempty" jsonschema:"description=Environment variables to be set in values.yaml with or without a description"`
Secrets *labelstructs.Secrets `yaml:"secrets,omitempty" json:"secrets,omitempty" jsonschema:"title=Secrets,description=Environment variables to be set as secrets"`
Ports *labelstructs.Ports `yaml:"ports,omitempty" json:"ports,omitempty" jsonschema:"title=Ports,description=Ports to be exposed in services"`
Ingress *labelstructs.Ingress `yaml:"ingress,omitempty" json:"ingress,omitempty" jsonschema:"title=Ingress,description=Ingress configuration"`
HealthCheck *labelstructs.HealthCheck `yaml:"health-check,omitempty" json:"health-check,omitempty" jsonschema:"title=Health Check,description=Health check configuration that respects the kubernetes api"`
SamePod *string `yaml:"same-pod,omitempty" json:"same-pod,omitempty" jsonschema:"title=Same Pod,description=Service that should be in the same pod"`
Description *string `yaml:"description,omitempty" json:"description,omitempty" jsonschema:"title=Description,description=Description of the service that will be injected in the values.yaml file"`
Ignore *bool `yaml:"ignore,omitempty" json:"ignore,omitempty" jsonschema:"title=Ignore,description=Ignore the service in the conversion"`
Dependencies []labelstructs.Dependency `yaml:"dependencies,omitempty" json:"dependencies,omitempty" jsonschema:"title=Dependencies,description=Services that should be injected in the Chart.yaml file"`
ConfigMapFiles *labelstructs.ConfigMapFiles `yaml:"configmap-files,omitempty" json:"configmap-files,omitempty" jsonschema:"title=ConfigMap Files,description=Files that should be injected as ConfigMap"`
MapEnv *labelstructs.MapEnv `yaml:"map-env,omitempty" json:"map-env,omitempty" jsonschema:"title=Map Env,description=Map environment variables to another value"`
CronJob *labelstructs.CronJob `yaml:"cron-job,omitempty" json:"cron-job,omitempty" jsonschema:"title=Cron Job,description=Cron Job configuration"`
EnvFrom *labelstructs.EnvFrom `yaml:"env-from,omitempty" json:"env-from,omitempty" jsonschema:"title=Env From,description=Inject environment variables from another service"`
ExchangeVolumes []*labelstructs.ExchangeVolume `yaml:"exchange-volumes,omitempty" json:"exchange-volumes,omitempty" jsonschema:"title=Exchange Volumes,description=Exchange volumes between services"`
ValuesFrom *labelstructs.ValueFrom `yaml:"values-from,omitempty" 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.ConfigMapFiles, &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.LabelValuesFrom)
}
}
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()
}

View File

@@ -0,0 +1,181 @@
package katenaryfile
import (
"log"
"os"
"path/filepath"
"testing"
"katenary.io/internal/generator/labels"
"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)
}
}
func TestOverrideConfigMapFiles(t *testing.T) {
composeContent := `
services:
webapp:
image: nginx:latest
`
katenaryfileContent := `
webapp:
configmap-files:
- foo/bar
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.LabelConfigMapFiles]; !ok {
t.Fatal("Expected configmap-files to be defined", v)
}
}

View File

@@ -0,0 +1,35 @@
package generator
import (
"fmt"
"katenary.io/internal/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
}

View File

@@ -0,0 +1,2 @@
// Package labels provides functionality to parse and manipulate labels.
package labels

View File

@@ -0,0 +1,14 @@
## {{ .KatenaryPrefix }}/{{ .Name }}
{{ .Help.Short }}
**Type**: `{{ .Help.Type }}`
{{ .Help.Long }}
**Example:**
```yaml
{{ .Help.Example }}
```

View File

@@ -0,0 +1,9 @@
{{ .KatenaryPrefix }}/{{ .Name }}: {{ .Help.Short }}
Type: {{ .Help.Type }}
{{ .Help.Long }}
Example:
{{ .Help.Example }}

View File

@@ -0,0 +1,238 @@
package labels
import (
"bytes"
_ "embed"
"fmt"
"log"
"regexp"
"sort"
"strings"
"text/tabwriter"
"text/template"
"katenary.io/internal/utils"
"sigs.k8s.io/yaml"
)
const KatenaryLabelPrefix = "katenary.v3"
// Known labels.
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"
LabelValuesFrom Label = KatenaryLabelPrefix + "/values-from"
)
var (
// Set the documentation of labels here
//
//go:embed katenaryLabelsDoc.yaml
labelFullHelpYAML []byte
// parsed yaml
labelFullHelp map[string]Help
//go:embed help-template.tpl
helpTemplatePlain string
//go:embed help-template.md.tpl
helpTemplateMarkdown string
)
// Label is a katenary label to find in compose files.
type Label = string
func LabelName(name string) Label {
return Label(KatenaryLabelPrefix + "/" + name)
}
// Help is the documentation of a label.
type Help struct {
Short string `yaml:"short"`
Long string `yaml:"long"`
Example string `yaml:"example"`
Type string `yaml:"type"`
}
// GetLabelNames returns a sorted list of all katenary label names.
func GetLabelNames() []string {
var names []string
for name := range labelFullHelp {
names = append(names, name)
}
sort.Strings(names)
return names
}
func init() {
if err := yaml.Unmarshal(labelFullHelpYAML, &labelFullHelp); err != nil {
panic(err)
}
}
// GetLabelHelp return the help for the labels.
func GetLabelHelp(asMarkdown bool) string {
names := GetLabelNames() // sorted
if !asMarkdown {
return generatePlainHelp(names)
}
return generateMarkdownHelp(names)
}
// GetLabelHelpFor returns the help for a specific label.
func GetLabelHelpFor(labelname string, asMarkdown bool) string {
help, ok := labelFullHelp[labelname]
if !ok {
return "No help available for " + labelname + "."
}
help.Long = strings.TrimPrefix(help.Long, "\n")
help.Example = strings.TrimPrefix(help.Example, "\n")
help.Short = strings.TrimPrefix(help.Short, "\n")
if asMarkdown {
// enclose templates in backticks
help.Long = regexp.MustCompile(`\{\{(.*?)\}\}`).ReplaceAllString(help.Long, "`{{$1}}`")
help.Long = strings.ReplaceAll(help.Long, "__APP__", "`__APP__`")
} else {
help.Long = strings.ReplaceAll(help.Long, " \n", "\n")
help.Long = strings.ReplaceAll(help.Long, "`", "")
help.Long = strings.ReplaceAll(help.Long, "<code>", "")
help.Long = strings.ReplaceAll(help.Long, "</code>", "")
help.Long = utils.WordWrap(help.Long, 80)
}
// get help template
var helpTemplate string
switch asMarkdown {
case true:
helpTemplate = helpTemplateMarkdown
case false:
helpTemplate = helpTemplatePlain
}
var buf bytes.Buffer
var err error
err = template.Must(template.New("shorthelp").Parse(help.Long)).Execute(&buf, struct {
KatenaryPrefix string
}{
KatenaryPrefix: KatenaryLabelPrefix,
})
if err != nil {
log.Fatalf("Error executing template: %v", err)
}
help.Long = buf.String()
buf.Reset()
err = template.Must(template.New("example").Parse(help.Example)).Execute(&buf, struct {
KatenaryPrefix string
}{
KatenaryPrefix: KatenaryLabelPrefix,
})
if err != nil {
log.Fatalf("Error executing template: %v", err)
}
help.Example = buf.String()
buf.Reset()
err = template.Must(template.New("complete").Parse(helpTemplate)).Execute(&buf, struct {
Name string
Help Help
KatenaryPrefix string
}{
Name: labelname,
Help: help,
KatenaryPrefix: KatenaryLabelPrefix,
})
if err != nil {
log.Fatalf("Error executing template: %v", err)
}
return buf.String()
}
func generateMarkdownHelp(names []string) string {
var builder strings.Builder
var maxNameLength, maxDescriptionLength, maxTypeLength int
max := func(a, b int) int {
if a > b {
return a
}
return b
}
for _, name := range names {
help := labelFullHelp[name]
maxNameLength = max(maxNameLength, len(name)+3+len(KatenaryLabelPrefix))
maxDescriptionLength = max(maxDescriptionLength, len(help.Short))
maxTypeLength = max(maxTypeLength, len(help.Type)+3)
}
fmt.Fprintf(&builder, "%s\n", generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength))
fmt.Fprintf(&builder, "%s\n", generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength))
for _, name := range names {
help := labelFullHelp[name]
fmt.Fprintf(&builder, "| %-*s | %-*s | %-*s |\n",
maxNameLength, "`"+LabelName(name)+"`", // enclose in backticks
maxDescriptionLength, help.Short,
maxTypeLength, "`"+help.Type+"`",
)
}
return builder.String()
}
func generatePlainHelp(names []string) string {
var builder strings.Builder
for _, name := range names {
help := labelFullHelp[name]
fmt.Fprintf(&builder, "%s:\t%s\t%s\n", LabelName(name), help.Type, help.Short)
}
// use tabwriter to align the help text
buf := new(strings.Builder)
w := tabwriter.NewWriter(buf, 0, 8, 0, '\t', tabwriter.AlignRight)
fmt.Fprintln(w, builder.String())
w.Flush()
head := "To get more information about a label, use `katenary help-label <name_without_prefix>\ne.g. katenary help-label dependencies\n\n"
return head + buf.String()
}
func generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength int) string {
return fmt.Sprintf(
"| %-*s | %-*s | %-*s |",
maxNameLength, "Label name",
maxDescriptionLength, "Description",
maxTypeLength, "Type",
)
}
func generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength int) string {
return fmt.Sprintf(
"| %s | %s | %s |",
strings.Repeat("-", maxNameLength),
strings.Repeat("-", maxDescriptionLength),
strings.Repeat("-", maxTypeLength),
)
}
func Prefix() string {
return KatenaryLabelPrefix
}

View File

@@ -0,0 +1,356 @@
# Labels documentation.
#
# To create a label documentation:
#
# "labelname":
# type: the label type (bool, string, array, object...)
# short: a short description
# long: |-
# A multiline description to explain the label behavior
# example: |-
# yamlsyntax: here
#
# This file is embed in the Katenary binary and parsed in kanetaryLabels.go init() function.
#
# Note:
# - The short and long texts are parsed with text/template, so you can use template syntax.
# That means that if you want to display double brackets, you need to enclose them to
# prevent template to try to expand the content, for example :
# This is an {{ "{{ example }}" }}.
#
# This will display "This is an {{ exemple }}" in the output.
# - Use {{ .KatenaryPrefix }} to let Katenary replace it with the label prefix (e.g. "katenary.v3")
"main-app":
short: "Mark the service as the main app."
long: |-
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: |-
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
{{ .KatenaryPrefix }}/main-app: true
type: "bool"
"values":
short: "Environment variables to be added to the values.yaml"
long: |-
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: |-
env:
FOO: bar
DB_NAME: mydb
TO_CONFIGURE: something that can be changed in values.yaml
A_COMPLEX_VALUE: example
labels:
{{ .KatenaryPrefix }}/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.
type: "[]string or map[string]string"
"secrets":
short: "Env vars to be set as secrets."
long: |-
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 {{ printf "%s/%s" .KatenaryPrefix "values"}} too,
so the secret value can be configured in values.yaml
example: |-
env:
PASSWORD: a very secret password
NOT_A_SECRET: a public value
labels:
{{ .KatenaryPrefix }}/secrets: |-
- PASSWORD
type: "[]string"
"ports":
short: "Ports to be added to the service."
long: |-
Only useful for services without exposed port. It is mandatory if the
service is a dependency of another service.
example: |-
labels:
{{ .KatenaryPrefix }}/ports: |-
- 8080
- 8081
type: "[]uint32"
"ingress":
short: "Ingress rules to be added to the service."
long: |-
Declare an ingress rule for the service. The port should be exposed or
declared with {{ printf "%s/%s" .KatenaryPrefix "ports" }}.
example: |-
labels:
{{ .KatenaryPrefix }}/ingress: |-
port: 80
hostname: mywebsite.com (optional)
type: "object"
"map-env":
short: "Map env vars from the service to the deployment."
long: |-
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: |-
env:
DB_HOST: database
RUNNING: docker
OTHER: value
labels:
{{ .KatenaryPrefix }}/map-env: |-
RUNNING: kubernetes
DB_HOST: '{{ "{{ include \"__APP__.fullname\" . }}" }}-database'
type: "map[string]string"
"health-check":
short: "Health check to be added to the deployment."
long: "Health check to be added to the deployment."
example: |-
labels:
{{ .KatenaryPrefix }}/health-check: |-
livenessProbe:
httpGet:
path: /health
port: 8080
type: "object"
"same-pod":
short: "Move the same-pod deployment to the target deployment."
long: |-
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: |-
web:
image: nginx:1.19
php:
image: php:7.4-fpm
labels:
{{ .KatenaryPrefix }}/same-pod: web
type: "string"
"description":
short: "Description of the service"
long: |-
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: |-
labels:
{{ .KatenaryPrefix }}/description: |-
This is a description of the service.
It can be multiline.
type: "string"
"ignore":
short: "Ignore the service"
long: "Ingoring a service to not be exported in helm chart."
example: "labels:\n {{ .KatenaryPrefix }}/ignore: \"true\""
type: "bool"
"dependencies":
short: "Add Helm dependencies to the service."
long: |-
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: |-
labels:
{{ .KatenaryPrefix }}/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
type: "[]object"
"configmap-files":
short: "Inject files as Configmap."
long: |-
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: |-
volumes
- ./conf.d:/etc/nginx/conf.d
labels:
{{ .KatenaryPrefix }}/configmap-files: |-
- ./conf.d
type: "[]string"
"cronjob":
short: "Create a cronjob from the service."
long: |-
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: |-
labels:
{{ .KatenaryPrefix }}/cronjob: |-
command: echo "hello world"
schedule: "* */1 * * *" # or @hourly for example
type: "object"
"env-from":
short: "Add environment variables from another service."
type: "[]string"
long: |-
It adds environment variables from another service to the current service.
example: |-
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
{{ .KatenaryPrefix }}/env-from: |-
- myservice1
"exchange-volumes":
short: Add exchange volumes (empty directory on the node) to share data
type: "[]object"
long: |-
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: |-
nginx:
# ...
labels;
{{ .KatenaryPrefix }}/exchange-volumes: |-
- name: php-fpm
mountPath: /var/www/html
php:
# ...
labels:
{{ .KatenaryPrefix }}/exchange-volumes: |-
- name: php-fpm
mountPath: /opt
init: cp -ra /var/www/html/* /opt
"values-from":
short: "Add values from another service."
long: |-
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`.
type: "map[string]string"
example: |-
database:
image: mariadb:10.5
environment:
MARIADB_USER: myuser
MARIADB_PASSWORD: mypassword
labels:
# we can declare secrets
{{ .KatenaryPrefix }}/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"
{{ .KatenaryPrefix }}/values-from: |-
DB_USER: database.MARIADB_USER
DB_PASSWORD: database.MARIADB_PASSWORD
# vim: ft=gotmpl.yaml

View File

@@ -0,0 +1,78 @@
package labels
import (
_ "embed"
"reflect"
"testing"
)
var testingKatenaryPrefix = Prefix()
const mainAppLabel = "main-app"
func TestPrefix(t *testing.T) {
tests := []struct {
name string
want string
}{
{
name: "TestPrefix",
want: "katenary.v3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Prefix(); got != tt.want {
t.Errorf("Prefix() = %v, want %v", got, tt.want)
}
})
}
}
func TestLabelName(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
want Label
}{
{
name: "Test_labelName",
args: args{
name: mainAppLabel,
},
want: testingKatenaryPrefix + "/" + mainAppLabel,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := LabelName(tt.args.name); !reflect.DeepEqual(got, tt.want) {
t.Errorf("labelName() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetLabelHelp(t *testing.T) {
help := GetLabelHelp(false)
if help == "" {
t.Errorf("GetLabelHelp() = %v, want %v", help, "Help")
}
help = GetLabelHelp(true)
if help == "" {
t.Errorf("GetLabelHelp() = %v, want %v", help, "Help")
}
}
func TestGetLabelHelpFor(t *testing.T) {
help := GetLabelHelpFor(mainAppLabel, false)
if help == "" {
t.Errorf("GetLabelHelpFor() = %v, want %v", help, "Help")
}
help = GetLabelHelpFor("main-app", true)
if help == "" {
t.Errorf("GetLabelHelpFor() = %v, want %v", help, "Help")
}
}

View File

@@ -0,0 +1,13 @@
package labelstructs
import "gopkg.in/yaml.v3"
type ConfigMapFiles []string
func ConfigMapFileFrom(data string) (ConfigMapFiles, error) {
var mapping ConfigMapFiles
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return mapping, nil
}

View File

@@ -0,0 +1,18 @@
package labelstructs_test
import (
"testing"
"katenary.io/internal/generator/labels/labelstructs"
)
func TestConfigMapFileFrom(t *testing.T) {
ts := "- foo/bar"
tc2, _ := labelstructs.ConfigMapFileFrom(ts)
if len(tc2) != 1 {
t.Errorf("Expected ConfigMapFile to have 1 item, got %d", len(tc2))
}
if tc2[0] != "foo/bar" {
t.Errorf("Expected ConfigMapFile to contain 'foo/bar', got %s", tc2[0])
}
}

View File

@@ -0,0 +1,18 @@
package labelstructs
import "gopkg.in/yaml.v3"
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"`
}
func CronJobFrom(data string) (*CronJob, error) {
var mapping CronJob
if err := yaml.Unmarshal([]byte(data), &mapping); err != nil {
return nil, err
}
return &mapping, nil
}

Some files were not shown because too many files have changed in this diff Show More