269 Commits

Author SHA1 Message Date
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
164 changed files with 13155 additions and 3536 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

@@ -9,3 +9,4 @@ updates:
directory: "/" # Location of package manifests
schedule:
interval: "daily"
target-branch: develop

47
.github/workflows/go-test.yaml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Go-Tests
on:
pull_request:
branches:
- develop
push:
branches:
- master
- develop
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.23
- name: Install Helm
run: |
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
- name: Launch Test
run: |
go mod tidy
go vet ./... && go test -coverprofile=coverprofile.out -json -v ./... > gotest.json
- uses: actions/upload-artifact@v4
with:
name: tests-results
path: |
coverprofile.out
gotest.json
sonar:
runs-on: ubuntu-latest
needs: tests
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: tests-results
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

22
.gitignore vendored
View File

@@ -1,10 +1,20 @@
dist/*
.cache/*
chart/*
docker-compose.yaml
katenary
*.env
docker-compose*
!examples/**/docker-compose*
.credentials
release.id
cover*
.sq
.aider*
.python_history
.bash_history
.cache/
.aider/
.config/
*/venv
# local binary
./katenary
# will be treated later
/examples/*

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

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Patrice Ferlet
Copyright (c) 2022-2024 Patrice Ferlet
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

158
Makefile
View File

@@ -4,27 +4,56 @@ VERSION=$(shell git describe --exact-match --tags $(CUR_SHA) 2>/dev/null || echo
CTN:=$(shell which podman 2>&1 1>/dev/null && echo "podman" || echo "docker")
PREFIX=~/.local
GOVERSION=1.24
GO=container
OUT=katenary
BLD_CMD=go build -ldflags="-X 'main.Version=$(VERSION)'" -o $(OUT) ./cmd/*.go
RELEASE=""
BLD_CMD=go build -ldflags="-X 'katenary/generator.Version=$(RELEASE)$(VERSION)'" -o $(OUT) ./cmd/katenary
GOOS=linux
GOARCH=amd64
SIGNER=metal3d@gmail.com
UPX_OPTS =
UPX ?= upx $(UPX_OPTS)
BUILD_IMAGE=docker.io/golang:1.18-alpine
BUILD_IMAGE=docker.io/golang:$(GOVERSION)-alpine
# SHELL=/bin/bash
.PHONY: help clean build
# List of source files
SOURCES=$(wildcard ./*.go ./*/*.go ./*/*/*.go)
# List of binaries to build and sign
BINARIES=dist/katenary-linux-amd64 dist/katenary-linux-arm64 dist/katenary.exe dist/katenary-darwin-amd64 dist/katenary-freebsd-amd64 dist/katenary-freebsd-arm64
# List of signatures to build
ASC_BINARIES=$(patsubst %,%.asc,$(BINARIES))
# defaults
BROWSER=$(shell command -v epiphany || echo xdg-open)
SHELL := bash
# strict mode
.SHELLFLAGS := -eu -o pipefail -c
# One session per target
.ONESHELL:
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.PHONY: help clean build install tests test doc
all: build
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
@@ -47,21 +76,40 @@ help:
$$ make build-all
EOF
## Standard build
build: pull katenary
build-all:
rm -f dist/*
$(MAKE) _build-all
_build-all: pull dist dist/katenary-linux-amd64 dist/katenary-linux-arm64 dist/katenary.exe dist/katenary-darwin-amd64 dist/katenary-freebsd-amd64 dist/katenary-freebsd-arm64
pull:
ifneq ($(GO),local)
@echo -e "\033[1;32mPulling $(BUILD_IMAGE) docker image\033[0m"
@$(CTN) pull $(BUILD_IMAGE)
endif
dist:
katenary: $(SOURCES) Makefile go.mod go.sum
ifeq ($(GO),local)
@echo "=> Build on host using go"
else
@echo "=> Build in container using" $(CTN)
endif
echo $(BLD_CMD)
ifeq ($(GO),local)
$(BLD_CMD)
else ifeq ($(CTN),podman)
@podman run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) \
--rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --userns keep-id $(BUILD_IMAGE) $(BLD_CMD)
else
@docker run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) \
--rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp $(BUILD_IMAGE) $(BLD_CMD)
endif
echo "=> Stripping if possible"
strip $(OUT) 2>/dev/null || echo "=> No strip available"
## Release build
dist: prepare $(BINARIES) upx $(ASC_BINARIES)
prepare: pull
mkdir -p dist
dist/katenary-linux-amd64:
@@ -69,7 +117,6 @@ dist/katenary-linux-amd64:
@echo -e "\033[1;32mBuilding katenary $(VERSION) for linux-amd64...\033[0m"
$(MAKE) katenary GOOS=linux GOARCH=amd64 OUT=$@
dist/katenary-linux-arm64:
@echo
@echo -e "\033[1;32mBuilding katenary $(VERSION) for linux-arm...\033[0m"
@@ -95,29 +142,22 @@ dist/katenary-freebsd-arm64:
@echo -e "\033[1;32mBuilding katenary $(VERSION) for freebsd-arm64...\033[0m"
$(MAKE) katenary GOOS=freebsd GOARCH=arm64 OUT=$@
katenary: $(wildcard */*.go Makefile go.mod go.sum)
ifeq ($(GO),local)
@echo "=> Build in host using go"
else
@echo "=> Build in container using" $(CTN)
endif
echo $(BLD_CMD)
ifeq ($(GO),local)
$(BLD_CMD)
else ifeq ($(CTN),podman)
@podman run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) \
--rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --userns keep-id -it docker.io/golang $(BLD_CMD)
else
@docker run -e CGO_ENABLED=0 -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) \
--rm -v $(PWD):/go/src/katenary:z -w /go/src/katenary --user $(shell id -u):$(shell id -g) -e HOME=/tmp -it docker.io/golang $(BLD_CMD)
endif
echo "=> Stripping if possible"
strip $(OUT) 2>/dev/null || echo "=> No strip available"
gpg-sign:
rm -f dist/*.asc
$(MAKE) $(ASC_BINARIES)
dist/%.asc: dist/%
gpg --armor --detach-sign --default-key $(SIGNER) $< &>/dev/null || exit 1
upx:
$(UPX) dist/katenary-linux-amd64
$(UPX) dist/katenary-linux-arm64
$(UPX) dist/katenary.exe
$(UPX) dist/katenary-darwin-amd64 --force-macos
install: build
cp katenary $(PREFIX)/bin/katenary
install -Dm755 katenary $(PREFIX)/bin/katenary
uninstall:
rm -f $(PREFIX)/bin/katenary
@@ -126,13 +166,30 @@ clean:
rm -rf katenary dist/* release.id
serve-doc: __label_doc
@cd doc && \
[ -d venv ] || python -m venv venv; \
source venv/bin/activate && \
echo "==> Installing requirements in the virtual env..."
pip install -qq -r requirements.txt && \
echo "==> Serving doc with mkdocs..." && \
mkdocs serve
tests: test
test:
@echo -e "\033[1;33mTesting katenary $(VERSION)...\033[0m"
go test -v ./...
go test -coverprofile=cover.out ./...
$(MAKE) cover
cover:
go tool cover -func=cover.out | grep "total:"
go tool cover -html=cover.out -o cover.html
if [ "$(BROWSER)" = "xdg-open" ]; then
xdg-open cover.html
else
$(BROWSER) -i --new-window cover.html
fi
.ONESHELL:
push-release: build-all
@rm -f release.id
# read personal access token from .git-credentials
@@ -154,3 +211,34 @@ push-release: build-all
https://uploads.github.com/repos/metal3d/katenary/releases/$$(cat release.id)/assets?name=$$(basename $$i)
done
@rm -f release.id
doc:
@echo "=> Generating documentation..."
# generate the labels doc and code doc
$(MAKE) __label_doc
__label_doc:
@command -v gomarkdoc || (echo "==> We need to install gomarkdoc..." && \
go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest)
@echo "=> Generating labels doc..."
# short label doc
go run ./cmd/katenary help-labels -m | \
sed -i '
/START_LABEL_DOC/,/STOP_LABEL_DOC/{/<!--/!d};
/START_LABEL_DOC/,/STOP_LABEL_DOC/r/dev/stdin
' doc/docs/labels.md
# detailed label doc
go run ./cmd/katenary help-labels -am | sed 's/^##/###/' | \
sed -i '
/START_DETAILED_DOC/,/STOP_DETAILED_DOC/{/<!--/!d};
/START_DETAILED_DOC/,/STOP_DETAILED_DOC/r/dev/stdin
' doc/docs/labels.md
echo "=> Generating Code documentation..."
PACKAGES=$$(for f in $$(find . -name "*.go" -type f); do dirname $$f; done | sort -u)
for pack in $$PACKAGES; do
echo "-> Generating doc for $$pack"
gomarkdoc --repository.default-branch $(shell git branch --show-current) -o doc/docs/packages/$$pack.md $$pack
sed -i '/^## Index/,/^##/ { /## Index/d; /^##/! d }' doc/docs/packages/$$pack.md
done

314
README.md
View File

@@ -1,21 +1,51 @@
<div style="text-align:center">
<img src="./misc/logo.png" alt="Katenary Logo" />
<div style="text-align:center; margin: auto 0 4em 0" align="center">
<img src="./doc/docs/statics/logo-vertical.svg" alt="Katenary Logo" style="max-width: 90%" align="center"/>
</div>
Katenary is a tool to help transforming `docker-compose` files to a working Helm Chart for Kubernetes.
<div style="text-align:center; margin: auto 0 4em 0" align="center">
> **Important Note:** Katenary is a tool to help building Helm Chart from a docker-compose file, but docker-compose doesn't propose as many features as what can do Kubernetes. So, we strongly recommend to use Katenary as a "bootstrap" tool and then to manually enhance the generated helm chart.
[![Documentation Status](https://readthedocs.org/projects/katenary/badge/?version=latest)](https://katenary.readthedocs.io/en/latest/?badge=latest)
[![Go Report Card](https://goreportcard.com/badge/github.com/metal3d/katenary)](https://goreportcard.com/report/github.com/metal3d/katenary)
[![GitHub release](https://img.shields.io/github/v/release/metal3d/katenary)](https://github.com/metal3d/katenary/releases)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=metal3d_katenary&metric=coverage)](https://sonarcloud.io/summary/new_code?id=metal3d_katenary)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=metal3d_katenary&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=metal3d_katenary)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=metal3d_katenary&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=metal3d_katenary)
This project is partially made at [Smile](https://www.smile.eu)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=metal3d_katenary&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=metal3d_katenary)
<div style="text-align:center">
<a href="https://www.smile.eu"><img src="./misc/Logo_Smile.png" alt="Smile Logo" width="250" /></a>
</div>
# Install
<div style="text-align:center; margin: auto 0 4em 0" align="center">
<h3>🚀 Unleash Productivity with Katenary! 🚀</h3>
</div>
You can download the binaries from the [Release](https://github.com/metal3d/katenary/releases) section. Copy the binary and rename it to `katenary`. Place the binary inside your `PATH`. You should now be able to call the `katenary` command.
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://github.com/metal3d).
## Install
You can download the binaries from the [Release](https://github.com/metal3d/katenary/releases) section. Copy the binary
and rename it to `katenary`. Place the binary inside your `PATH`. You should now be able to call the `katenary` command.
You can of course get the binary with `go install -u github.com/metal3d/katenary/cmd/katenary/...` but the `main` branch
is continuously updated. It's preferable to use releases.
You can use this commands on Linux:
@@ -23,7 +53,7 @@ You can use this commands on Linux:
sh <(curl -sSL https://raw.githubusercontent.com/metal3d/katenary/master/install.sh)
```
# Else... Build yourself
## Or, build yourself
If you've got `podman` or `docker`, you can build `katenary` by using:
@@ -32,11 +62,13 @@ 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
@@ -53,12 +85,12 @@ make build GO=local GOOS=linux GOARCH=arm64
Then place the `katenary` binary file inside your PATH.
## Tips
# Tips
We strongly recommend adding the completion call to you SHELL using the common `bashrc`, or whatever the profile file
you use.
We strongly recommand to add the "completion" call to you SHELL using the common bashrc, or whatever the profile file you use.
E.g. :
E.g.,
```bash
# bash in ~/.bashrc file
@@ -67,125 +99,207 @@ source <(katenary completion bash)
source <(katenary completion bash --no-description)
# zsh in ~/.zshrc
source <(helm completion zsh)
source <(katenary completion zsh)
# fish in ~/.config/fish/config.fish
katenary completion fish | source
# experimental
# powershell (as we don't provide any support on Windows yet, please avoid this...)
```
# Usage
## Usage
```
Katenary aims to be a tool to convert docker-compose files to Helm Charts.
It will create deployments, services, volumes, secrets, and ingress resources.
But it will also create initContainers based on depend_on, healthcheck, and other features.
It's not magical, sometimes you'll need to fix the generated charts.
The general way to use it is to call one of these commands:
```text
Katenary is a tool to convert compose files to Helm Charts.
katenary convert
katenary convert -c docker-compose.yml
katenary convert -c docker-compose.yml -o ./charts
In case of, check the help of each command using:
katenary <command> --help
or
"katenary help <command>"
Each [command] and subcommand has got an "help" and "--help" flag to show more information.
Usage:
katenary [command]
Examples:
katenary convert -c docker-compose.yml -o ./charts
Available Commands:
completion Generate the autocompletion script for the specified shell
convert Convert docker-compose to helm chart
help Help about any command
show-labels Show labels of a resource
upgrade Upgrade katenary to the latest version if available
version Display version
completion Generates completion scripts
convert Converts a docker-compose file to a Helm Chart
hash-composefiles Print the hash of the composefiles
help Help about any command
help-labels Print the labels help for all or a specific label
schema Print the schema of the katenary file
version Print the version number of Katenary
Flags:
-h, --help help for katenary
-h, --help help for katenary
-v, --version version for katenary
Use "katenary [command] --help" for more information about a command.
```
Katenary will try to find a `docker-compose.yaml` or `docker-compose.yml` file inside the current directory. It will check *the existence of the `chart` directory to create a new Helm Chart inside a named subdirectory. Katenary will ask you if you want to delete it before recreating.
Katenary will try to find a `docker-compose.yaml` or `docker-compose.yml` file inside the current directory. It will
check *the existence of the `chart` directory to create a new Helm Chart inside a named subdirectory. Katenary will ask
you if you want to delete it before recreating.
It creates a subdirectory inside `chart` that is named with the `appname` option (default is `MyApp`)
> To respect the ability to install the same application in the same namespace, Katenary will create "variable" names like `{{ .Release.Name }}-servicename`. So, you will need to use some labels inside your docker-compose file to help katenary to build a correct helm chart.
> To respect the ability to install the same application in the same namespace, Katenary will create variable names
> like `{{ .Release.Name }}-servicename`. So, you will need to use some labels inside your docker-compose file to help
> Katenary to build a correct helm chart.
What can be interpreted by Katenary:
- Services with "image" section (cannot work with "build" section)
- **Named Volumes** are transformed to persistent volume claims - note that local volume will break the transformation to Helm Chart because there is (for now) no way to make it working (see below for resolution)
- if `ports` and/or `expose` section, katenary will create Services and bind the port to the corresponding container port
- `depends_on` will add init containers to wait for the depending service (using the first port)
- `env_file` list will create a configMap object per environemnt file (⚠ todo: the "to-service" label doesn't work with configMap for now)
- some labels can help to bind values, for example:
- `katenary.io/ingress: 80` will expose the port 80 in a ingress
- `katenary.io/env-to-service: VARNAME` will convert the value to a variable `{{ .Release.Name }}-VARNAME` - it's usefull when you want to pass the name of a service as a variable (think about the service name for mysql to pass to a container that wants to connect to this)
Exemple of a possible `docker-compose.yaml` file:
Example of a possible `docker-compose.yaml` file:
```yaml
version: "3"
services:
webapp:
image: php:7-apache
environment:
# note that "database" is a service name
DB_HOST: database
expose:
- 80
depends_on:
# this will create a init container waiting for 3306 port
# 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
database:
image: mariadb:10
env_file:
# this will create a configMap
- my_env.env
environment:
MARIADB_ROOT_PASSWORD: foobar
labels:
# no need to declare this port in docker-compose
# but katenary will need it
katenary.io/ports: 3306
webapp:
image: php:7-apache
environment:
# 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:
# this will create a init container waiting for 3306 port
# because it's the "exposed" port
- database
labels:
# expose the port 80 as an ingress
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 valuse will be added in a configMap
- my_env.env
environment:
MARIADB_USER: foo
MARIADB_ROOT_PASSWORD: foobar
MARIADB_PASSWORD: bar
labels:
# no need to declare this port in docker-compose
# but katenary will need it
katenary.v3/ports: |-
- 3306
# these variables are secrets
katenary.v3/secrets: |-
- MARIADB_ROOT_PASSWORD
- MARIADB_PASSWORD
```
# Labels
## Labels
These labels could be found by `katenary show-labels`, and can be placed as "labels" inside your docker-compose file:
These labels could be found by `katenary help-labels`, and can be placed as labels inside your docker-compose file:
```
katenary.io/secret-envfiles : set the given file names as a secret instead of configmap
katenary.io/ports : set the ports to expose as a service (coma separated)
katenary.io/ingress : set the port to expose in an ingress (coma separated)
katenary.io/env-to-service : specifies that the environment variable points on a service name (coma separated)
katenary.io/configmap-volumes : specifies that the volumes points on a configmap (coma separated)
katenary.io/same-pod : specifies that the pod should be deployed in the same pod than the given service name
katenary.io/empty-dirs : specifies that the given volume names should be "emptyDir" instead of persistentVolumeClaim (coma separated)
katenary.io/healthcheck : specifies that the container should be monitored by a healthcheck, **it overrides the docker-compose healthcheck**.
You can use these form of label values:
- "http://[not used address][:port][/path]" to specify an http healthcheck
- "tcp://[not used address]:port" to specify a tcp healthcheck
- other string is condidered as a "command" healthcheck
```text
To get more information about a label, use `katenary help-label <name_without_prefix>
e.g. katenary help-label dependencies
katenary.v3/configmap-files: list of strings Add files to the configmap.
katenary.v3/cronjob: object Create a cronjob from the service.
katenary.v3/dependencies: list of objects Add Helm dependencies to the service.
katenary.v3/description: string Description of the service
katenary.v3/env-from: list of strings Add environment variables from antoher service.
katenary.v3/exchange-volumes: list of objects 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: object Map env vars from the service to the deployment.
katenary.v3/ports: list of 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: list of string Env vars to be set as secrets.
katenary.v3/values: list of string or map Environment variables to be added to the values.yaml
katenary.v3/values-from: map[string]string Add values from another service.
```
# What a name...
## 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 kanetary.yaml file:
```yaml
web:
ingress:
hostname: myapp.example.com
port: 80
```
To validate the `katenary.yaml` file, you can use the JSON schema using the "master" raw content:
`https://raw.githubusercontent.com/metal3d/katenary/refs/heads/master/katenary.json`
It's easy to configure in [LazyVim](https://www.lazyvim.org/), using `nvim-lspconfig`,
create a Lua file in your `plugins` directory, or apply the settings as the example below:
```lua
-- yaml.lua
return {
{
"neovim/nvim-lspconfig",
opts = {
servers = {
yamlls = {
settings = {
yaml = {
schemas = {
["https://raw.githubusercontent.com/metal3d/katenary/master/katenary.json"] = "katenary.yaml",
},
},
},
},
},
},
},
}
```
Use this address to validate the `katenary.yaml` file in VSCode:
```json
{
"yaml.schemas": {
"https://raw.githubusercontent.com/metal3d/katenary/master/katenary.json": "katenary.yaml"
}
}
```
> You can, of course, replace the `master` with a specific tag or branch.
## What a name…
Katenary is the stylized name of the project that comes from the "catenary" word.
A catenary is a curve formed by a wire, rope, or chain hanging freely from two points that are not in the same vertical line. For example, the anchor chain between a bot and the anchor.
This "curved link" represents what we try to do, the project is a "streched link from docker-compose to helm chart".
A catenary is a curve formed by a wire, rope, or chain hanging freely from two points that are not in the same vertical
line. For example, the anchor chain between a boat and the anchor.
This curved link represents what we try to do, the project is a stretched link from docker-compose to helm chart.

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

@@ -0,0 +1,319 @@
// Katenary CLI, main package.
//
// This package is not intended to be imported. It contains the
// main function that build the command line with `cobra` package.
package main
import (
"fmt"
"katenary/generator"
"katenary/generator/katenaryfile"
"katenary/generator/labels"
"katenary/utils"
"log"
"os"
"strings"
"github.com/compose-spec/compose-go/cli"
"github.com/spf13/cobra"
)
const longHelp = `Katenary is a tool to convert compose files to Helm Charts.
Each [command] and subcommand has got an "help" and "--help" flag to show more information.
`
func main() {
rootCmd := buildRootCmd()
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
func buildRootCmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "katenary",
Long: longHelp,
Short: "Katenary is a tool to convert docker-compose files to Helm Charts",
}
rootCmd.Example = ` katenary convert -c docker-compose.yml -o ./charts`
rootCmd.Version = generator.GetVersion()
rootCmd.CompletionOptions.DisableDescriptions = false
rootCmd.CompletionOptions.DisableNoDescFlag = false
rootCmd.AddCommand(
generateCompletionCommand(rootCmd.Name()),
generateVersionCommand(),
generateConvertCommand(),
generateHashComposefilesCommand(),
generateLabelHelpCommand(),
generateSchemaCommand(),
)
return rootCmd
}
const completionHelp = `To load completions:
Bash:
# Add this line in your ~/.bashrc or ~/.bash_profile file
$ source <(%[1]s completion bash)
# Or, you can load completions for each users session. Execute once:
# Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS:
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
# You will need to start a new shell for this setup to take effect.
fish:
$ %[1]s completion fish | source
# To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`
func generateCompletionCommand(name string) *cobra.Command {
bashV1 := false
cmd := &cobra.Command{
Use: "completion",
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Short: "Generates completion scripts",
Long: fmt.Sprintf(completionHelp, name),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
switch args[0] {
case "bash":
// get the bash version
if cmd.Flags().Changed("bash-v1") {
return cmd.Root().GenBashCompletion(os.Stdout)
}
return cmd.Root().GenBashCompletionV2(os.Stdout, true)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletion(os.Stdout)
}
return fmt.Errorf("unknown completion type: %s", args[0])
},
}
// add a flag to force bash completion v1
cmd.Flags().Bool("bash-v1", bashV1, "Force bash completion v1")
return cmd
}
func generateConvertCommand() *cobra.Command {
force := false
outputDir := "./chart"
dockerComposeFile := make([]string, 0)
profiles := make([]string, 0)
helmdepUpdate := false
var appVersion *string
givenAppVersion := ""
chartVersion := "0.1.0"
icon := ""
envFiles := []string{}
convertCmd := &cobra.Command{
Use: "convert",
Short: "Converts a docker-compose file to a Helm Chart",
RunE: func(cmd *cobra.Command, args []string) error {
if givenAppVersion != "" {
appVersion = &givenAppVersion
}
return generator.Convert(generator.ConvertOptions{
Force: force,
OutputDir: outputDir,
Profiles: profiles,
HelmUpdate: helmdepUpdate,
AppVersion: appVersion,
ChartVersion: chartVersion,
Icon: icon,
EnvFiles: envFiles,
}, dockerComposeFile...)
},
}
convertCmd.Flags().BoolVarP(
&force,
"force",
"f",
force,
"Force the overwrite of the chart directory",
)
convertCmd.Flags().BoolVarP(
&helmdepUpdate,
"helm-update",
"u",
helmdepUpdate,
"Update helm dependencies if helm is installed",
)
convertCmd.Flags().StringSliceVarP(
&profiles,
"profile",
"p",
profiles,
"Specify the profiles to use",
)
convertCmd.Flags().StringVarP(
&outputDir,
"output-dir",
"o",
outputDir,
"Specify the output directory",
)
convertCmd.Flags().StringSliceVarP(
&dockerComposeFile,
"compose-file",
"c",
cli.DefaultFileNames,
"Specify an alternate compose files - can be specified multiple times or use coma to separate them.\n"+
"Note that overides files are also used whatever the files you specify here.\nThe overides files are:\n"+
strings.Join(cli.DefaultOverrideFileNames, ", \n")+
"\n",
)
convertCmd.Flags().StringVarP(
&givenAppVersion,
"app-version",
"a",
"",
"Specify the app version (in Chart.yaml)",
)
convertCmd.Flags().StringVarP(
&chartVersion,
"chart-version",
"v",
chartVersion,
"Specify the chart version (in Chart.yaml)",
)
convertCmd.Flags().StringVarP(
&icon,
"icon",
"i",
"",
"Specify the icon (in Chart.yaml), use a valid URL, Helm does not support local files at this time.",
)
convertCmd.Flags().StringSliceVarP(
&envFiles,
"env-file",
"e",
envFiles,
"Specify the env file to use additonnaly to the .env file. Can be specified multiple times.",
)
return convertCmd
}
func generateVersionCommand() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print the version number of Katenary",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(generator.GetVersion())
},
}
}
func generateLabelHelpCommand() *cobra.Command {
markdown := false
all := false
cmd := &cobra.Command{
Use: "help-labels [label]",
Short: "Print the labels help for all or a specific label",
Long: `Print the labels help for all or a specific label
If no label is specified, the help for all labels is printed.
If a label is specified, the help for this label is printed.
The name of the label must be specified without the prefix ` + labels.Prefix() + `.
e.g.
kanetary help-labels
katenary help-labels ingress
katenary help-labels map-env
`,
ValidArgs: labels.GetLabelNames(),
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
fmt.Println(labels.GetLabelHelpFor(args[0], markdown))
return
}
if all {
// show the help for all labels
l := len(labels.GetLabelNames())
for i, label := range labels.GetLabelNames() {
fmt.Println(labels.GetLabelHelpFor(label, markdown))
if !markdown && i < l-1 {
fmt.Println(strings.Repeat("-", 80))
}
}
return
}
fmt.Println(labels.GetLabelHelp(markdown))
},
}
cmd.Flags().BoolVarP(&markdown, "markdown", "m", markdown, "Use the markdown format")
cmd.Flags().BoolVarP(&all, "all", "a", all, "Print the full help for all labels")
return cmd
}
func generateHashComposefilesCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "hash-composefiles [composefile]",
Short: "Print the hash of the composefiles",
Long: `Print the hash of the composefiles
If no composefile is specified, the hash of all composefiles is printed.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
if hash, err := utils.HashComposefiles(args); err != nil {
fmt.Println(err)
} else {
fmt.Println(hash)
}
return
}
},
}
return cmd
}
func generateSchemaCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "schema",
Short: "Print the schema of the katenary file",
Long: "Generate a schama for katenary.yaml file that can be used to validate the file or to use with yaml LSP to complete and check your configuration.",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(katenaryfile.GenerateSchema())
},
}
return cmd
}

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

@@ -0,0 +1,70 @@
package main
import (
"bytes"
"encoding/json"
"io"
"os"
"strings"
"testing"
)
func TestBuildCommand(t *testing.T) {
rootCmd := buildRootCmd()
if rootCmd == nil {
t.Errorf("Expected rootCmd to be defined")
}
if rootCmd.Use != "katenary" {
t.Errorf("Expected rootCmd.Use to be katenary, got %s", rootCmd.Use)
}
numCommands := 6
if len(rootCmd.Commands()) != numCommands {
t.Errorf("Expected %d command, got %d", numCommands, len(rootCmd.Commands()))
}
}
func TestGetVersion(t *testing.T) {
cmd := buildRootCmd()
if cmd == nil {
t.Errorf("Expected cmd to be defined")
}
version := generateVersionCommand()
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
version.Run(cmd, nil)
w.Close()
os.Stdout = old
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
if !strings.Contains(output, "(devel)") {
t.Errorf("Expected output to contain '(devel)', got %s", output)
}
}
func TestSchemaCommand(t *testing.T) {
cmd := buildRootCmd()
if cmd == nil {
t.Errorf("Expected cmd to be defined")
}
schema := generateSchemaCommand()
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
schema.Run(cmd, nil)
w.Close()
os.Stdout = old
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// try to parse json
schemaContent := make(map[string]interface{})
if err := json.Unmarshal([]byte(output), &schemaContent); err != nil {
t.Errorf("Expected valid json, got %s", output)
}
}

View File

@@ -1,151 +0,0 @@
package main
import (
"fmt"
"katenary/generator/writers"
"katenary/helm"
"katenary/update"
"strconv"
"github.com/spf13/cobra"
)
var Version = "master" // changed at compile time
var longHelp = `Katenary aims to be a tool to convert docker-compose files to Helm Charts.
It will create deployments, services, volumes, secrets, and ingress resources.
But it will also create initContainers based on depend_on, healthcheck, and other features.
It's not magical, sometimes you'll need to fix the generated charts.
The general way to use it is to call one of these commands:
katenary convert
katenary convert -c docker-compose.yml
katenary convert -c docker-compose.yml -o ./charts
In case of, check the help of each command using:
katenary <command> --help
or
"katenary help <command>"
`
func init() {
// apply the version to the "update" package
update.Version = Version
}
func main() {
// The base command
rootCmd := &cobra.Command{
Use: "katenary",
Long: longHelp,
Short: "Katenary is a tool to convert docker-compose files to Helm Charts",
}
// to display the version
versionCmd := &cobra.Command{
Use: "version",
Short: "Display version",
Run: func(c *cobra.Command, args []string) { c.Println(Version) },
}
// convert command, need some flags
convertCmd := &cobra.Command{
Use: "convert",
Short: "Convert docker-compose to helm chart",
Long: "Convert docker-compose to helm chart. The resulting helm chart will be in the current directory/" +
ChartsDir + "/" + AppName +
".\nThe appversion will be generated that way:\n" +
"- if it's in a git project, it takes git version or tag\n" +
"- if it's not defined, so the version will be get from the --app-version flag \n" +
"- if it's not defined, so the 0.0.1 version is used",
Run: func(c *cobra.Command, args []string) {
force := c.Flag("force").Changed
// TODO: is there a way to get typed values from cobra?
appversion := c.Flag("app-version").Value.String()
composeFile := c.Flag("compose-file").Value.String()
appName := c.Flag("app-name").Value.String()
chartDir := c.Flag("output-dir").Value.String()
indentation, err := strconv.Atoi(c.Flag("indent-size").Value.String())
if err != nil {
writers.IndentSize = indentation
}
Convert(composeFile, appversion, appName, chartDir, force)
},
}
convertCmd.Flags().BoolP(
"force", "f", false, "force overwrite of existing output files")
convertCmd.Flags().StringP(
"app-version", "a", AppVersion, "app version")
convertCmd.Flags().StringP(
"compose-file", "c", ComposeFile, "docker compose file")
convertCmd.Flags().StringP(
"app-name", "n", AppName, "application name")
convertCmd.Flags().StringP(
"output-dir", "o", ChartsDir, "chart directory")
convertCmd.Flags().IntP(
"indent-size", "i", 2, "set the indent size of the YAML files")
// show possible labels to set in docker-compose file
showLabelsCmd := &cobra.Command{
Use: "show-labels",
Short: "Show labels of a resource",
Run: func(c *cobra.Command, args []string) {
c.Println(helm.GetLabelsDocumentation())
},
}
// Update the binary to the latest version
updateCmd := &cobra.Command{
Use: "upgrade",
Short: "Upgrade katenary to the latest version if available",
Run: func(c *cobra.Command, args []string) {
version, assets, err := update.CheckLatestVersion()
if err != nil {
c.Println(err)
return
}
c.Println("Updating to version: " + version)
err = update.DownloadLatestVersion(assets)
if err != nil {
c.Println(err)
return
}
c.Println("Update completed")
},
}
rootCmd.AddCommand(
versionCmd,
convertCmd,
showLabelsCmd,
updateCmd,
)
// in parallel, check if the current katenary version is the latest
ch := make(chan string)
go func() {
version, _, err := update.CheckLatestVersion()
if err != nil {
ch <- ""
return
}
if Version != version {
ch <- fmt.Sprintf("\x1b[33mNew version available: " +
version +
" - to auto upgrade katenary, you can execute: katenary upgrade\x1b[0m\n")
}
}()
// Execute the command
finalize := make(chan error)
go func() {
finalize <- rootCmd.Execute()
}()
// Wait for both goroutines to finish
if err := <-finalize; err != nil {
fmt.Println(err)
}
fmt.Print(<-ch)
}

View File

@@ -1,143 +0,0 @@
package main
import (
"errors"
"fmt"
"katenary/compose"
"katenary/generator"
"os"
"os/exec"
"path/filepath"
"strings"
)
var (
composeFiles = []string{"docker-compose.yaml", "docker-compose.yml"}
ComposeFile = ""
AppName = "MyApp"
ChartsDir = "chart"
AppVersion = "0.0.1"
)
func init() {
FindComposeFile()
SetAppName()
SetAppVersion()
}
func FindComposeFile() bool {
for _, file := range composeFiles {
if _, err := os.Stat(file); err == nil {
ComposeFile = file
return true
}
}
return false
}
// SetAppName sets the application name from the current directory name.
func SetAppName() {
wd, err := os.Getwd()
if err != nil {
return
}
AppName = filepath.Base(wd)
if AppName == "" {
AppName = "MyApp"
}
}
// SetAppVersion set the AppVersion variable to the git version/tag
func SetAppVersion() {
AppVersion, _ = detectGitVersion()
}
// Try to detect the git version/tag.
func detectGitVersion() (string, error) {
defaulVersion := "0.0.1"
// Check if .git directory exists
if s, err := os.Stat(".git"); err != nil {
// .git should be a directory
return defaulVersion, errors.New("no git repository found")
} else if !s.IsDir() {
// .git should be a directory
return defaulVersion, errors.New(".git is not a directory")
}
// check if "git" executable is callable
if _, err := exec.LookPath("git"); err != nil {
return defaulVersion, errors.New("git executable not found")
}
// get the latest commit hash
if out, err := exec.Command("git", "log", "-n1", "--pretty=format:%h").Output(); err == nil {
latestCommit := strings.TrimSpace(string(out))
// then get the current branch/tag
out, err := exec.Command("git", "branch", "--show-current").Output()
if err != nil {
return defaulVersion, errors.New("git branch --show-current failed")
} else {
currentBranch := strings.TrimSpace(string(out))
// finally, check if the current tag (if exists) correspond to the current commit
// git describe --exact-match --tags <latestCommit>
out, err := exec.Command("git", "describe", "--exact-match", "--tags", latestCommit).Output()
if err == nil {
return strings.TrimSpace(string(out)), nil
} else {
return currentBranch + "-" + latestCommit, nil
}
}
}
return defaulVersion, errors.New("git log failed")
}
func Convert(composeFile, appVersion, appName, chartDir string, force bool) {
if len(composeFile) == 0 {
fmt.Println("No compose file given")
return
}
_, err := os.Stat(ComposeFile)
if err != nil {
fmt.Println("No compose file found")
os.Exit(1)
}
// Parse the compose file now
p := compose.NewParser(composeFile)
p.Parse(appName)
dirname := filepath.Join(chartDir, appName)
if _, err := os.Stat(dirname); err == nil && !force {
response := ""
for response != "y" && response != "n" {
response = "n"
fmt.Printf(""+
"The %s directory already exists, it will be \x1b[31;1mremoved\x1b[0m!\n"+
"Do you really want to continue? [y/N]: ", dirname)
fmt.Scanf("%s", &response)
response = strings.ToLower(response)
}
if response == "n" {
fmt.Println("Cancelled")
os.Exit(0)
}
}
// cleanup and create the chart directory (until "templates")
if err := os.RemoveAll(dirname); err != nil {
fmt.Printf("Error removing %s: %s\n", dirname, err)
os.Exit(1)
}
// create the templates directory
templatesDir := filepath.Join(dirname, "templates")
if err := os.MkdirAll(templatesDir, 0755); err != nil {
fmt.Printf("Error creating %s: %s\n", templatesDir, err)
os.Exit(1)
}
// start generator
generator.Generate(p, Version, appName, appVersion, ComposeFile, dirname)
}

View File

@@ -1,236 +0,0 @@
package compose
import (
"fmt"
"katenary/helm"
"log"
"os"
"strings"
"github.com/google/shlex"
"gopkg.in/yaml.v3"
)
const (
ICON_EXCLAMATION = "❕"
)
// Parser is a docker-compose parser.
type Parser struct {
Data *Compose
}
var Appname = ""
// NewParser create a Parser and parse the file given in filename. If filename is empty, we try to parse the content[0] argument that should be a valid YAML content.
func NewParser(filename string, content ...string) *Parser {
c := NewCompose()
if filename != "" {
f, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
dec := yaml.NewDecoder(f)
err = dec.Decode(c)
if err != nil {
log.Fatal(err)
}
} else {
dec := yaml.NewDecoder(strings.NewReader(content[0]))
err := dec.Decode(c)
if err != nil {
log.Fatal(err)
}
}
p := &Parser{Data: c}
return p
}
func (p *Parser) Parse(appname string) {
Appname = appname
services := make(map[string][]string)
// get the service list, to be sure that everything is ok
// fix ugly types
for _, s := range p.Data.Services {
parseEnv(s)
parseCommand(s)
parseEnvFiles(s)
}
c := p.Data
for name, s := range c.Services {
if portlabel, ok := s.Labels[helm.LABEL_PORT]; ok {
services := strings.Split(portlabel, ",")
for _, serviceport := range services {
portexists := false
for _, found := range s.Ports {
if found == serviceport {
portexists = true
}
}
if !portexists {
s.Ports = append(s.Ports, serviceport)
}
}
}
if len(s.Ports) > 0 {
services[name] = s.Ports
}
}
// check if dependencies are resolved
missing := []string{}
for name, s := range c.Services {
for _, dep := range s.DependsOn {
if _, ok := services[dep]; !ok {
missing = append(missing, fmt.Sprintf(
"The service \"%s\" hasn't got "+
"declared port for dependency from \"%s\" - please "+
"append a %s label or a \"ports\" section in the docker-compose file",
dep, name, helm.LABEL_PORT),
)
}
}
}
if len(missing) > 0 {
log.Fatal(strings.Join(missing, "\n"))
}
// check if all "image" properties are set
missing = []string{}
for name, s := range c.Services {
if s.Image == "" {
missing = append(missing, fmt.Sprintf(
"The service \"%s\" hasn't got "+
"an image property - please "+
"append an image property in the docker-compose file",
name,
))
}
}
if len(missing) > 0 {
log.Fatal(strings.Join(missing, "\n"))
}
// check the build element
for name, s := range c.Services {
if s.RawBuild == nil {
continue
}
fmt.Println(ICON_EXCLAMATION +
" \x1b[33myou will need to build and push your image named \"" + s.Image + "\"" +
" for the \"" + name + "\" service \x1b[0m")
}
}
// manage environment variables, if the type is map[string]string so we can use it, else we need to split "=" sign
// and apply this in env variable
func parseEnv(s *Service) {
env := make(map[string]string)
if s.RawEnvironment == nil {
return
}
switch s.RawEnvironment.(type) {
case map[string]string:
env = s.RawEnvironment.(map[string]string)
case map[string]interface{}:
for k, v := range s.RawEnvironment.(map[string]interface{}) {
// force to string
env[k] = fmt.Sprintf("%v", v)
}
case []interface{}:
for _, v := range s.RawEnvironment.([]interface{}) {
// Splot the value of the env variable with "="
parts := strings.Split(v.(string), "=")
env[parts[0]] = parts[1]
}
case string:
parts := strings.Split(s.RawEnvironment.(string), "=")
env[parts[0]] = parts[1]
default:
log.Printf("%+v, %T", s.RawEnvironment, s.RawEnvironment)
log.Fatal("Environment type not supported")
}
s.Environment = env
}
func parseCommand(s *Service) {
if s.RawCommand == nil {
return
}
// following the command type, it can be a "slice" or a simple sting, so we need to check it
switch v := s.RawCommand.(type) {
case string:
// use shlex to parse the command
command, err := shlex.Split(v)
if err != nil {
log.Fatal(err)
}
s.Command = command
case []string:
s.Command = v
case []interface{}:
for _, v := range v {
s.Command = append(s.Command, v.(string))
}
default:
log.Printf("%+v %T", s.RawCommand, s.RawCommand)
log.Fatal("Command type not supported")
}
}
func parseEnvFiles(s *Service) {
// Same than parseEnv, but for env files
if s.RawEnvFiles == nil {
return
}
envfiles := make([]string, 0)
switch v := s.RawEnvFiles.(type) {
case []string:
envfiles = v
case []interface{}:
for _, v := range v {
envfiles = append(envfiles, v.(string))
}
default:
log.Printf("%+v %T", s.RawEnvFiles, s.RawEnvFiles)
log.Fatal("EnvFile type not supported")
}
s.EnvFiles = envfiles
}
func parseHealthCheck(s *Service) {
// HealthCheck command can be a string or slice of strings
if s.HealthCheck.RawTest == nil {
return
}
switch v := s.HealthCheck.RawTest.(type) {
case string:
var err error
s.HealthCheck.Test, err = shlex.Split(v)
if err != nil {
log.Fatal(err)
}
case []string:
s.HealthCheck.Test = v
case []interface{}:
for _, v := range v {
s.HealthCheck.Test = append(s.HealthCheck.Test, v.(string))
}
default:
log.Printf("%+v %T", s.HealthCheck.RawTest, s.HealthCheck.RawTest)
log.Fatal("HealthCheck type not supported")
}
}

View File

@@ -1,138 +0,0 @@
package compose
import (
"katenary/logger"
"testing"
)
const DOCKER_COMPOSE_YML1 = `
version: "3"
services:
# first service, very basic
web:
image: nginx
ports:
- "80:80"
environment:
FOO: bar
BAZ: qux
networks:
- frontend
database:
image: postgres
networks:
- frontend
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: mysecretpassword
POSTGRES_DB: mydb
labels:
katenary.io/ports: "5432"
commander1:
image: foo
command: ["/bin/sh", "-c", "echo 'hello world'"]
commander2:
image: foo
command: echo "hello world"
`
func init() {
logger.NOLOG = true
}
func TestParser(t *testing.T) {
p := NewParser("", DOCKER_COMPOSE_YML1)
p.Parse("test")
// check if the "web" and "database" service is parsed correctly
// by checking if the "ports" and "environment"
for name, service := range p.Data.Services {
if name == "web" {
if len(service.Ports) != 1 {
t.Errorf("Expected 1 port, got %d", len(service.Ports))
}
if service.Ports[0] != "80:80" {
t.Errorf("Expected port 80:80, got %s", service.Ports[0])
}
if len(service.Environment) != 2 {
t.Errorf("Expected 2 environment variables, got %d", len(service.Environment))
}
if service.Environment["FOO"] != "bar" {
t.Errorf("Expected FOO=bar, got %s", service.Environment["FOO"])
}
if service.Environment["BAZ"] != "qux" {
t.Errorf("Expected BAZ=qux, got %s", service.Environment["BAZ"])
}
}
// same for the "database" service
if name == "database" {
if len(service.Ports) != 1 {
t.Errorf("Expected 1 port, got %d", len(service.Ports))
}
if service.Ports[0] != "5432" {
t.Errorf("Expected port 5432, got %s", service.Ports[0])
}
if len(service.Environment) != 3 {
t.Errorf("Expected 3 environment variables, got %d", len(service.Environment))
}
if service.Environment["POSTGRES_USER"] != "postgres" {
t.Errorf("Expected POSTGRES_USER=postgres, got %s", service.Environment["POSTGRES_USER"])
}
if service.Environment["POSTGRES_PASSWORD"] != "mysecretpassword" {
t.Errorf("Expected POSTGRES_PASSWORD=mysecretpassword, got %s", service.Environment["POSTGRES_PASSWORD"])
}
if service.Environment["POSTGRES_DB"] != "mydb" {
t.Errorf("Expected POSTGRES_DB=mydb, got %s", service.Environment["POSTGRES_DB"])
}
// check labels
if len(service.Labels) != 1 {
t.Errorf("Expected 1 label, got %d", len(service.Labels))
}
// is label katenary.io/ports correct?
if service.Labels["katenary.io/ports"] != "5432" {
t.Errorf("Expected katenary.io/ports=5432, got %s", service.Labels["katenary.io/ports"])
}
}
}
}
func TestParseCommand(t *testing.T) {
p := NewParser("", DOCKER_COMPOSE_YML1)
p.Parse("test")
for name, s := range p.Data.Services {
if name == "commander1" {
t.Log(s.Command)
if len(s.Command) != 3 {
t.Errorf("Expected 3 command, got %d", len(s.Command))
}
if s.Command[0] != "/bin/sh" {
t.Errorf("Expected /bin/sh, got %s", s.Command[0])
}
if s.Command[1] != "-c" {
t.Errorf("Expected -c, got %s", s.Command[1])
}
if s.Command[2] != "echo 'hello world'" {
t.Errorf("Expected echo 'hello world', got %s", s.Command[2])
}
}
if name == "commander2" {
t.Log(s.Command)
if len(s.Command) != 2 {
t.Errorf("Expected 1 command, got %d", len(s.Command))
}
if s.Command[0] != "echo" {
t.Errorf("Expected echo, got %s", s.Command[0])
}
if s.Command[1] != "hello world" {
t.Errorf("Expected hello world, got %s", s.Command[1])
}
}
}
}

View File

@@ -1,44 +0,0 @@
package compose
// Compose is a complete docker-compse representation.
type Compose struct {
Version string `yaml:"version"`
Services map[string]*Service `yaml:"services"`
Volumes map[string]interface{} `yaml:"volumes"`
}
// NewCompose resturs a Compose object.
func NewCompose() *Compose {
c := &Compose{}
c.Services = make(map[string]*Service)
c.Volumes = make(map[string]interface{})
return c
}
// HealthCheck manage generic type to handle TCP, HTTP and TCP health check.
type HealthCheck struct {
Test []string `yaml:"-"`
RawTest interface{} `yaml:"test"`
Interval string `yaml:"interval"`
Timeout string `yaml:"timeout"`
Retries int `yaml:"retries"`
StartPeriod string `yaml:"start_period"`
}
// Service represent a "service" in a docker-compose file.
type Service struct {
Image string `yaml:"image"`
Ports []string `yaml:"ports"`
Environment map[string]string `yaml:"-"`
RawEnvironment interface{} `yaml:"environment"`
Labels map[string]string `yaml:"labels"`
DependsOn []string `yaml:"depends_on"`
Volumes []string `yaml:"volumes"`
Expose []int `yaml:"expose"`
EnvFiles []string `yaml:"-"`
RawEnvFiles interface{} `yaml:"env_file"`
RawBuild interface{} `yaml:"build"`
HealthCheck *HealthCheck `yaml:"healthcheck"`
Command []string `yaml:"-"`
RawCommand interface{} `yaml:"command"`
}

View File

@@ -0,0 +1,5 @@
MD012: false
MD013: false
MD022: false
MD033: false
MD046: false

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 copose` does
- `github.com/spf13/cobra`: to parse command line arguments, sub-commands and flags. It also generates completion for
bash, zsh, fish and PowerShell.
- `github.com/thediveo/netdb`: to get the standard names of a service from its port number
- `gopkg.in/yaml.v3`:
- to generate `Chart.yaml` and `values.yaml` files (only)
- to parse Katenary labels in the compose file
- `k8s.io/api` and `k8s.io/apimachinery` to create Kubernetes objects
- `sigs.k8s.io/yaml`: to generate Katenary YAML files in the format of Kubernetes objects
There are also some other packages used in the project, like `gopkg.in/yaml` to parse labels. I'm sorry to not list the
entire dependencies. You can check the `go.mod` file to see all the dependencies.

110
doc/docs/faq.md Normal file
View File

@@ -0,0 +1,110 @@
# 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/metal3d/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://github.com/metal3d).
The project source
code is hosted on the [:fontawesome-brands-github: Katenary GitHub Repository](https://github.com/metal3d/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/metal3d/katenary/releases) of the project in GitHub.
You need to select the right binary for your operating system and architecture, and copy the binary in a directory
that is in your `PATH`.
If you are a Linux user, you can use the "one line installation command" which will download the binary in your
`$HOME/.local/bin` directory if it exists.
```bash
sh <(curl -sSL https://raw.githubusercontent.com/metal3d/katenary/master/install.sh)
```
!!! Info "Upgrading is integrated to the `katenary` command"
Katenary propose a `upgrade` sub-command to update the current binary to the latest stable release.
Of course, you need to install Katenary once :smile:
!!! Note "You prefer to compile it, no need to install Go"
You can also build and install it yourself, the provided Makefile has got a `build` command that uses `podman` or
`docker` to build the binary.
So, you don't need to install Go compiler :+1:.
But, note that the "master" branch is not the "stable" version. It's preferable to switch to a tag, or to use the
releases.
To compile it, you can use the following commands:
```bash
git clone https://github.com/metal3d/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.

480
doc/docs/labels.md Normal file
View File

@@ -0,0 +1,480 @@
# 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 `|` to use Yaml multilines as value.
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` | Add files to the 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 antoher 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
Add files to the 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 antoher 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/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/generator"
```
The generator package generates kubernetes objects from a "compose" file and transforms them into a helm chart.
The generator package is the core of katenary. It is responsible for generating kubernetes objects from a compose file and transforming them into a helm chart. Conversion manipulates Yaml representation of kubernetes object to add conditions, labels, annotations, etc. to the objects. It also create the values to be set to the values.yaml file.
The generate.Convert\(\) create an HelmChart object and call "Generate\(\)" method to convert from a compose file to a helm chart. It saves the helm chart in the given directory.
If you want to change or override the write behavior, you can use the HelmChart.Generate\(\) function and implement your own write function. This function returns the helm chart object containing all kubernetes objects and helm chart ingormation. It does not write the helm chart to the disk.
## Variables
<a name="Annotations"></a>
```go
var (
// Standard annotationss
Annotations = map[string]string{
labels.LabelName("version"): Version,
}
)
```
<a name="Version"></a>Version is the version of katenary. It is set at compile time.
```go
var Version = "master" // changed at compile time
```
<a name="Convert"></a>
## func [Convert](<https://github.com/metal3d/katenary/blob/develop/generator/converter.go#L99>)
```go
func Convert(config ConvertOptions, dockerComposeFile ...string) error
```
Convert a compose \(docker, podman...\) project to a helm chart. It calls Generate\(\) to generate the chart and then write it to the disk.
<a name="GetLabels"></a>
## func [GetLabels](<https://github.com/metal3d/katenary/blob/develop/generator/labels.go#L12>)
```go
func GetLabels(serviceName, appName string) map[string]string
```
GetLabels returns the labels for a service. It uses the appName to replace the \_\_replace\_\_ in the labels. This is used to generate the labels in the templates.
<a name="GetMatchLabels"></a>
## func [GetMatchLabels](<https://github.com/metal3d/katenary/blob/develop/generator/labels.go#L25>)
```go
func GetMatchLabels(serviceName, appName string) map[string]string
```
GetMatchLabels returns the matchLabels for a service. It uses the appName to replace the \_\_replace\_\_ in the labels. This is used to generate the matchLabels in the templates.
<a name="GetVersion"></a>
## func [GetVersion](<https://github.com/metal3d/katenary/blob/develop/generator/version.go#L14>)
```go
func GetVersion() string
```
GetVersion return the version of katneary. It's important to understand that the version is set at compile time for the github release. But, it the user get katneary using \`go install\`, the version should be different.
<a name="Helper"></a>
## func [Helper](<https://github.com/metal3d/katenary/blob/develop/generator/helper.go#L15>)
```go
func Helper(name string) string
```
Helper returns the \_helpers.tpl file for a chart.
<a name="NewCronJob"></a>
## func [NewCronJob](<https://github.com/metal3d/katenary/blob/develop/generator/cronJob.go#L28>)
```go
func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) (*CronJob, *RBAC)
```
NewCronJob creates a new CronJob from a compose service. The appName is the name of the application taken from the project name.
<a name="ToK8SYaml"></a>
## func [ToK8SYaml](<https://github.com/metal3d/katenary/blob/develop/generator/utils.go#L90>)
```go
func ToK8SYaml(obj any) ([]byte, error)
```
<a name="UnWrapTPL"></a>
## func [UnWrapTPL](<https://github.com/metal3d/katenary/blob/develop/generator/utils.go#L86>)
```go
func UnWrapTPL(in []byte) []byte
```
UnWrapTPL removes the line wrapping from a template.
<a name="ChartTemplate"></a>
## type [ChartTemplate](<https://github.com/metal3d/katenary/blob/develop/generator/chart.go#L19-L22>)
ChartTemplate is a template of a chart. It contains the content of the template and the name of the service. This is used internally to generate the templates.
```go
type ChartTemplate struct {
Servicename string
Content []byte
}
```
<a name="ConfigMap"></a>
## type [ConfigMap](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L37-L42>)
ConfigMap is a kubernetes ConfigMap. Implements the DataMap interface.
```go
type ConfigMap struct {
*corev1.ConfigMap
// contains filtered or unexported fields
}
```
<a name="NewConfigMap"></a>
### func [NewConfigMap](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L46>)
```go
func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap
```
NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name. The ConfigMap is filled by environment variables and labels "map\-env".
<a name="NewConfigMapFromDirectory"></a>
### func [NewConfigMapFromDirectory](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L119>)
```go
func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap
```
NewConfigMapFromDirectory creates a new ConfigMap from a compose service. This path is the path to the file or directory. If the path is a directory, all files in the directory are added to the ConfigMap. Each subdirectory are ignored. Note that the Generate\(\) function will create the subdirectories ConfigMaps.
<a name="ConfigMap.AddBinaryData"></a>
### func \(\*ConfigMap\) [AddBinaryData](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L157>)
```go
func (c *ConfigMap) AddBinaryData(key string, value []byte)
```
AddBinaryData adds binary data to the configmap. Append or overwrite the value if the key already exists.
<a name="ConfigMap.AddData"></a>
### func \(\*ConfigMap\) [AddData](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L152>)
```go
func (c *ConfigMap) AddData(key, value string)
```
AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists.
<a name="ConfigMap.AppendDir"></a>
### func \(\*ConfigMap\) [AppendDir](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L166>)
```go
func (c *ConfigMap) AppendDir(path string) error
```
AddFile adds files from given path to the configmap. It is not recursive, to add all files in a directory, you need to call this function for each subdirectory.
<a name="ConfigMap.AppendFile"></a>
### func \(\*ConfigMap\) [AppendFile](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L213>)
```go
func (c *ConfigMap) AppendFile(path string) error
```
<a name="ConfigMap.Filename"></a>
### func \(\*ConfigMap\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L237>)
```go
func (c *ConfigMap) Filename() string
```
Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path.
<a name="ConfigMap.SetData"></a>
### func \(\*ConfigMap\) [SetData](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L247>)
```go
func (c *ConfigMap) SetData(data map[string]string)
```
SetData sets the data of the configmap. It replaces the entire data.
<a name="ConfigMap.Yaml"></a>
### func \(\*ConfigMap\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L252>)
```go
func (c *ConfigMap) Yaml() ([]byte, error)
```
Yaml returns the yaml representation of the configmap
<a name="ConfigMapMount"></a>
## type [ConfigMapMount](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L28-L31>)
```go
type ConfigMapMount struct {
// contains filtered or unexported fields
}
```
<a name="ConvertOptions"></a>
## type [ConvertOptions](<https://github.com/metal3d/katenary/blob/develop/generator/chart.go#L25-L34>)
ConvertOptions are the options to convert a compose project to a helm chart.
```go
type ConvertOptions struct {
AppVersion *string
OutputDir string
ChartVersion string
Icon string
Profiles []string
EnvFiles []string
Force bool
HelmUpdate bool
}
```
<a name="CronJob"></a>
## type [CronJob](<https://github.com/metal3d/katenary/blob/develop/generator/cronJob.go#L22-L25>)
CronJob is a kubernetes CronJob.
```go
type CronJob struct {
*batchv1.CronJob
// contains filtered or unexported fields
}
```
<a name="CronJob.Filename"></a>
### func \(\*CronJob\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/cronJob.go#L114>)
```go
func (c *CronJob) Filename() string
```
Filename returns the filename of the cronjob.
Implements the Yaml interface.
<a name="CronJob.Yaml"></a>
### func \(\*CronJob\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/cronJob.go#L121>)
```go
func (c *CronJob) Yaml() ([]byte, error)
```
Yaml returns the yaml representation of the cronjob.
Implements the Yaml interface.
<a name="CronJobValue"></a>
## type [CronJobValue](<https://github.com/metal3d/katenary/blob/develop/generator/values.go#L118-L123>)
CronJobValue is a cronjob configuration that will be saved in values.yaml.
```go
type CronJobValue struct {
Repository *RepositoryValue `yaml:"repository,omitempty"`
Environment map[string]any `yaml:"environment,omitempty"`
ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
Schedule string `yaml:"schedule"`
}
```
<a name="DataMap"></a>
## type [DataMap](<https://github.com/metal3d/katenary/blob/develop/generator/types.go#L4-L7>)
DataMap is a kubernetes ConfigMap or Secret. It can be used to add data to the ConfigMap or Secret.
```go
type DataMap interface {
SetData(map[string]string)
AddData(string, string)
}
```
<a name="Deployment"></a>
## type [Deployment](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L34-L44>)
Deployment is a kubernetes Deployment.
```go
type Deployment struct {
*appsv1.Deployment `yaml:",inline"`
// contains filtered or unexported fields
}
```
<a name="NewDeployment"></a>
### func [NewDeployment](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L48>)
```go
func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment
```
NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name. It also creates the Values map that will be used to create the values.yaml file.
<a name="Deployment.AddContainer"></a>
### func \(\*Deployment\) [AddContainer](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L115>)
```go
func (d *Deployment) AddContainer(service types.ServiceConfig)
```
AddContainer adds a container to the deployment.
<a name="Deployment.AddHealthCheck"></a>
### func \(\*Deployment\) [AddHealthCheck](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L160>)
```go
func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container)
```
<a name="Deployment.AddIngress"></a>
### func \(\*Deployment\) [AddIngress](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L189>)
```go
func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress
```
AddIngress adds an ingress to the deployment. It creates the ingress object.
<a name="Deployment.AddLegacyVolume"></a>
### func \(\*Deployment\) [AddLegacyVolume](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L219>)
```go
func (d *Deployment) AddLegacyVolume(name, kind string)
```
<a name="Deployment.AddVolumes"></a>
### func \(\*Deployment\) [AddVolumes](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L195>)
```go
func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string)
```
AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment. If the volume is a bind volume it will warn the user that it is not supported yet.
<a name="Deployment.BindFrom"></a>
### func \(\*Deployment\) [BindFrom](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L240>)
```go
func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment)
```
<a name="Deployment.BindMapFilesToContainer"></a>
### func \(\*Deployment\) [BindMapFilesToContainer](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L374>)
```go
func (d *Deployment) BindMapFilesToContainer(service types.ServiceConfig, secrets []string, appName string) (*corev1.Container, int)
```
<a name="Deployment.DependsOn"></a>
### func \(\*Deployment\) [DependsOn](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L268>)
```go
func (d *Deployment) DependsOn(to *Deployment, servicename string) error
```
DependsOn adds a initContainer to the deployment that will wait for the service to be up.
<a name="Deployment.Filename"></a>
### func \(\*Deployment\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L299>)
```go
func (d *Deployment) Filename() string
```
Filename returns the filename of the deployment.
<a name="Deployment.MountExchangeVolumes"></a>
### func \(\*Deployment\) [MountExchangeVolumes](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L425>)
```go
func (d *Deployment) MountExchangeVolumes()
```
<a name="Deployment.SetEnvFrom"></a>
### func \(\*Deployment\) [SetEnvFrom](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L304>)
```go
func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, samePod ...bool)
```
SetEnvFrom sets the environment variables to a configmap. The configmap is created.
<a name="Deployment.Yaml"></a>
### func \(\*Deployment\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/deployment.go#L449>)
```go
func (d *Deployment) Yaml() ([]byte, error)
```
Yaml returns the yaml representation of the deployment.
<a name="FileMapUsage"></a>
## type [FileMapUsage](<https://github.com/metal3d/katenary/blob/develop/generator/configMap.go#L21>)
FileMapUsage is the usage of the filemap.
```go
type FileMapUsage uint8
```
<a name="FileMapUsageConfigMap"></a>FileMapUsage constants.
```go
const (
FileMapUsageConfigMap FileMapUsage = iota // pure configmap for key:values.
FileMapUsageFiles // files in a configmap.
)
```
<a name="HelmChart"></a>
## type [HelmChart](<https://github.com/metal3d/katenary/blob/develop/generator/chart.go#L38-L51>)
HelmChart is a Helm Chart representation. It contains all the templates, values, versions, helpers...
```go
type HelmChart struct {
Templates map[string]*ChartTemplate `yaml:"-"`
Values map[string]any `yaml:"-"`
VolumeMounts map[string]any `yaml:"-"`
Name string `yaml:"name"`
Icon string `yaml:"icon,omitempty"`
ApiVersion string `yaml:"apiVersion"`
Version string `yaml:"version"`
AppVersion string `yaml:"appVersion"`
Description string `yaml:"description"`
Helper string `yaml:"-"`
Dependencies []labelStructs.Dependency `yaml:"dependencies,omitempty"`
// contains filtered or unexported fields
}
```
<a name="Generate"></a>
### func [Generate](<https://github.com/metal3d/katenary/blob/develop/generator/generator.go#L31>)
```go
func Generate(project *types.Project) (*HelmChart, error)
```
Generate a chart from a compose project. This does not write files to disk, it only creates the HelmChart object.
The Generate function will create the HelmChart object this way:
- Detect the service port name or leave the port number if not found.
- Create a deployment for each service that are not ingnore.
- Create a service and ingresses for each service that has ports and/or declared ingresses.
- Create a PVC or Configmap volumes for each volume.
- Create init containers for each service which has dependencies to other services.
- Create a chart dependencies.
- Create a configmap and secrets from the environment variables.
- Merge the same\-pod services.
<a name="NewChart"></a>
### func [NewChart](<https://github.com/metal3d/katenary/blob/develop/generator/chart.go#L54>)
```go
func NewChart(name string) *HelmChart
```
NewChart creates a new empty chart with the given name.
<a name="HelmChart.SaveTemplates"></a>
### func \(\*HelmChart\) [SaveTemplates](<https://github.com/metal3d/katenary/blob/develop/generator/chart.go#L69>)
```go
func (chart *HelmChart) SaveTemplates(templateDir string)
```
SaveTemplates the templates of the chart to the given directory.
<a name="Ingress"></a>
## type [Ingress](<https://github.com/metal3d/katenary/blob/develop/generator/ingress.go#L17-L21>)
```go
type Ingress struct {
*networkv1.Ingress
// contains filtered or unexported fields
}
```
<a name="NewIngress"></a>
### func [NewIngress](<https://github.com/metal3d/katenary/blob/develop/generator/ingress.go#L24>)
```go
func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress
```
NewIngress creates a new Ingress from a compose service.
<a name="Ingress.Filename"></a>
### func \(\*Ingress\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/ingress.go#L128>)
```go
func (ingress *Ingress) Filename() string
```
<a name="Ingress.Yaml"></a>
### func \(\*Ingress\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/ingress.go#L132>)
```go
func (ingress *Ingress) Yaml() ([]byte, error)
```
<a name="IngressValue"></a>
## type [IngressValue](<https://github.com/metal3d/katenary/blob/develop/generator/values.go#L29-L36>)
IngressValue is a ingress configuration that will be saved in values.yaml.
```go
type IngressValue struct {
Annotations map[string]string `yaml:"annotations"`
Host string `yaml:"host"`
Path string `yaml:"path"`
Class string `yaml:"class"`
Enabled bool `yaml:"enabled"`
TLS TLS `yaml:"tls"`
}
```
<a name="PersistenceValue"></a>
## type [PersistenceValue](<https://github.com/metal3d/katenary/blob/develop/generator/values.go#L16-L21>)
PersistenceValue is a persistence configuration that will be saved in values.yaml.
```go
type PersistenceValue struct {
StorageClass string `yaml:"storageClass"`
Size string `yaml:"size"`
AccessMode []string `yaml:"accessMode"`
Enabled bool `yaml:"enabled"`
}
```
<a name="RBAC"></a>
## type [RBAC](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L20-L24>)
RBAC is a kubernetes RBAC containing a role, a rolebinding and an associated serviceaccount.
```go
type RBAC struct {
RoleBinding *RoleBinding
Role *Role
ServiceAccount *ServiceAccount
}
```
<a name="NewRBAC"></a>
### func [NewRBAC](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L27>)
```go
func NewRBAC(service types.ServiceConfig, appName string) *RBAC
```
NewRBAC creates a new RBAC from a compose service. The appName is the name of the application taken from the project name.
<a name="RepositoryValue"></a>
## type [RepositoryValue](<https://github.com/metal3d/katenary/blob/develop/generator/values.go#L10-L13>)
RepositoryValue is a docker repository image and tag that will be saved in values.yaml.
```go
type RepositoryValue struct {
Image string `yaml:"image"`
Tag string `yaml:"tag"`
}
```
<a name="Role"></a>
## type [Role](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L114-L117>)
Role is a kubernetes Role.
```go
type Role struct {
*rbacv1.Role
// contains filtered or unexported fields
}
```
<a name="Role.Filename"></a>
### func \(\*Role\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L119>)
```go
func (r *Role) Filename() string
```
<a name="Role.Yaml"></a>
### func \(\*Role\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L123>)
```go
func (r *Role) Yaml() ([]byte, error)
```
<a name="RoleBinding"></a>
## type [RoleBinding](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L100-L103>)
RoleBinding is a kubernetes RoleBinding.
```go
type RoleBinding struct {
*rbacv1.RoleBinding
// contains filtered or unexported fields
}
```
<a name="RoleBinding.Filename"></a>
### func \(\*RoleBinding\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L105>)
```go
func (r *RoleBinding) Filename() string
```
<a name="RoleBinding.Yaml"></a>
### func \(\*RoleBinding\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L109>)
```go
func (r *RoleBinding) Yaml() ([]byte, error)
```
<a name="Secret"></a>
## type [Secret](<https://github.com/metal3d/katenary/blob/develop/generator/secret.go#L22-L25>)
Secret is a kubernetes Secret.
Implements the DataMap interface.
```go
type Secret struct {
*corev1.Secret
// contains filtered or unexported fields
}
```
<a name="NewSecret"></a>
### func [NewSecret](<https://github.com/metal3d/katenary/blob/develop/generator/secret.go#L28>)
```go
func NewSecret(service types.ServiceConfig, appName string) *Secret
```
NewSecret creates a new Secret from a compose service
<a name="Secret.AddData"></a>
### func \(\*Secret\) [AddData](<https://github.com/metal3d/katenary/blob/develop/generator/secret.go#L70>)
```go
func (s *Secret) AddData(key, value string)
```
AddData adds a key value pair to the secret.
<a name="Secret.Filename"></a>
### func \(\*Secret\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/secret.go#L86>)
```go
func (s *Secret) Filename() string
```
Filename returns the filename of the secret.
<a name="Secret.SetData"></a>
### func \(\*Secret\) [SetData](<https://github.com/metal3d/katenary/blob/develop/generator/secret.go#L91>)
```go
func (s *Secret) SetData(data map[string]string)
```
SetData sets the data of the secret.
<a name="Secret.Yaml"></a>
### func \(\*Secret\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/secret.go#L98>)
```go
func (s *Secret) Yaml() ([]byte, error)
```
Yaml returns the yaml representation of the secret.
<a name="Service"></a>
## type [Service](<https://github.com/metal3d/katenary/blob/develop/generator/service.go#L17-L20>)
Service is a kubernetes Service.
```go
type Service struct {
*v1.Service `yaml:",inline"`
// contains filtered or unexported fields
}
```
<a name="NewService"></a>
### func [NewService](<https://github.com/metal3d/katenary/blob/develop/generator/service.go#L23>)
```go
func NewService(service types.ServiceConfig, appName string) *Service
```
NewService creates a new Service from a compose service.
<a name="Service.AddPort"></a>
### func \(\*Service\) [AddPort](<https://github.com/metal3d/katenary/blob/develop/generator/service.go#L52>)
```go
func (s *Service) AddPort(port types.ServicePortConfig, serviceName ...string)
```
AddPort adds a port to the service.
<a name="Service.Filename"></a>
### func \(\*Service\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/service.go#L76>)
```go
func (s *Service) Filename() string
```
Filename returns the filename of the service.
<a name="Service.Yaml"></a>
### func \(\*Service\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/service.go#L81>)
```go
func (s *Service) Yaml() ([]byte, error)
```
Yaml returns the yaml representation of the service.
<a name="ServiceAccount"></a>
## type [ServiceAccount](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L132-L135>)
ServiceAccount is a kubernetes ServiceAccount.
```go
type ServiceAccount struct {
*corev1.ServiceAccount
// contains filtered or unexported fields
}
```
<a name="ServiceAccount.Filename"></a>
### func \(\*ServiceAccount\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L137>)
```go
func (r *ServiceAccount) Filename() string
```
<a name="ServiceAccount.Yaml"></a>
### func \(\*ServiceAccount\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/rbac.go#L141>)
```go
func (r *ServiceAccount) Yaml() ([]byte, error)
```
<a name="TLS"></a>
## type [TLS](<https://github.com/metal3d/katenary/blob/develop/generator/values.go#L23-L26>)
```go
type TLS struct {
Enabled bool `yaml:"enabled"`
SecretName string `yaml:"secretName"`
}
```
<a name="Value"></a>
## type [Value](<https://github.com/metal3d/katenary/blob/develop/generator/values.go#L39-L50>)
Value will be saved in values.yaml. It contains configuration for all deployment and services.
```go
type Value struct {
Repository *RepositoryValue `yaml:"repository,omitempty"`
Persistence map[string]*PersistenceValue `yaml:"persistence,omitempty"`
Ingress *IngressValue `yaml:"ingress,omitempty"`
Environment map[string]any `yaml:"environment,omitempty"`
Replicas *uint32 `yaml:"replicas,omitempty"`
CronJob *CronJobValue `yaml:"cronjob,omitempty"`
NodeSelector map[string]string `yaml:"nodeSelector"`
Resources map[string]any `yaml:"resources"`
ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
ServiceAccount string `yaml:"serviceAccount"`
}
```
<a name="NewValue"></a>
### func [NewValue](<https://github.com/metal3d/katenary/blob/develop/generator/values.go#L57>)
```go
func NewValue(service types.ServiceConfig, main ...bool) *Value
```
NewValue creates a new Value from a compose service. The value contains the necessary information to deploy the service \(image, tag, replicas, etc.\).
If \`main\` is true, the tag will be empty because it will be set in the helm chart appVersion.
<a name="Value.AddIngress"></a>
### func \(\*Value\) [AddIngress](<https://github.com/metal3d/katenary/blob/develop/generator/values.go#L90>)
```go
func (v *Value) AddIngress(host, path string)
```
<a name="Value.AddPersistence"></a>
### func \(\*Value\) [AddPersistence](<https://github.com/metal3d/katenary/blob/develop/generator/values.go#L104>)
```go
func (v *Value) AddPersistence(volumeName string)
```
AddPersistence adds persistence configuration to the Value.
<a name="VolumeClaim"></a>
## type [VolumeClaim](<https://github.com/metal3d/katenary/blob/develop/generator/volume.go#L18-L23>)
VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim.
```go
type VolumeClaim struct {
*v1.PersistentVolumeClaim
// contains filtered or unexported fields
}
```
<a name="NewVolumeClaim"></a>
### func [NewVolumeClaim](<https://github.com/metal3d/katenary/blob/develop/generator/volume.go#L26>)
```go
func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *VolumeClaim
```
NewVolumeClaim creates a new VolumeClaim from a compose service.
<a name="VolumeClaim.Filename"></a>
### func \(\*VolumeClaim\) [Filename](<https://github.com/metal3d/katenary/blob/develop/generator/volume.go#L62>)
```go
func (v *VolumeClaim) Filename() string
```
Filename returns the suggested filename for a VolumeClaim.
<a name="VolumeClaim.Yaml"></a>
### func \(\*VolumeClaim\) [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/volume.go#L67>)
```go
func (v *VolumeClaim) Yaml() ([]byte, error)
```
Yaml marshals a VolumeClaim into yaml.
<a name="Yaml"></a>
## type [Yaml](<https://github.com/metal3d/katenary/blob/develop/generator/types.go#L10-L13>)
Yaml is a kubernetes object that can be converted to yaml.
```go
type Yaml interface {
Yaml() ([]byte, error)
Filename() string
}
```
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)

View File

@@ -0,0 +1,28 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# extrafiles
```go
import "katenary/generator/extrafiles"
```
extrafiles package provides function to generate the Chart files that are not objects. Like README.md and notes.txt...
## func [NotesFile](<https://github.com/metal3d/katenary/blob/develop/generator/extrafiles/notes.go#L13>)
```go
func NotesFile(services []string) string
```
NotesFile returns the content of the note.txt file.
<a name="ReadMeFile"></a>
## func [ReadMeFile](<https://github.com/metal3d/katenary/blob/develop/generator/extrafiles/readme.go#L46>)
```go
func ReadMeFile(charname, description string, values map[string]any) string
```
ReadMeFile returns the content of the README.md file.
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)

View File

@@ -0,0 +1,67 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# katenaryfile
```go
import "katenary/generator/katenaryfile"
```
Package katenaryfile is a package for reading and writing katenary files.
A katenary file, named "katenary.yml" or "katenary.yaml", is a file where you can define the configuration of the conversion avoiding the use of labels in the compose file.
Formely, the file describe the same structure as in labels, and so that can be validated and completed by LSP. It also ease the use of katenary.
## func [GenerateSchema](<https://github.com/metal3d/katenary/blob/develop/generator/katenaryfile/main.go#L137>)
```go
func GenerateSchema() string
```
GenerateSchema generates the schema for the katenary.yaml file.
<a name="OverrideWithConfig"></a>
## func [OverrideWithConfig](<https://github.com/metal3d/katenary/blob/develop/generator/katenaryfile/main.go#L49>)
```go
func OverrideWithConfig(project *types.Project)
```
OverrideWithConfig overrides the project with the katenary.yaml file. It will set the labels of the services with the values from the katenary.yaml file. It work in memory, so it will not modify the original project.
<a name="Service"></a>
## type [Service](<https://github.com/metal3d/katenary/blob/develop/generator/katenaryfile/main.go#L27-L44>)
Service is a struct that contains the service configuration for katenary
```go
type Service struct {
MainApp *bool `json:"main-app,omitempty" jsonschema:"title=Is this service the main application"`
Values []StringOrMap `json:"values,omitempty" jsonschema:"description=Environment variables to be set in values.yaml with or without a description"`
Secrets *labelStructs.Secrets `json:"secrets,omitempty" jsonschema:"title=Secrets,description=Environment variables to be set as secrets"`
Ports *labelStructs.Ports `json:"ports,omitempty" jsonschema:"title=Ports,description=Ports to be exposed in services"`
Ingress *labelStructs.Ingress `json:"ingress,omitempty" jsonschema:"title=Ingress,description=Ingress configuration"`
HealthCheck *labelStructs.HealthCheck `json:"health-check,omitempty" jsonschema:"title=Health Check,description=Health check configuration that respects the kubernetes api"`
SamePod *string `json:"same-pod,omitempty" jsonschema:"title=Same Pod,description=Service that should be in the same pod"`
Description *string `json:"description,omitempty" jsonschema:"title=Description,description=Description of the service that will be injected in the values.yaml file"`
Ignore *bool `json:"ignore,omitempty" jsonschema:"title=Ignore,description=Ignore the service in the conversion"`
Dependencies []labelStructs.Dependency `json:"dependencies,omitempty" jsonschema:"title=Dependencies,description=Services that should be injected in the Chart.yaml file"`
ConfigMapFile *labelStructs.ConfigMapFile `json:"configmap-files,omitempty" jsonschema:"title=ConfigMap Files,description=Files that should be injected as ConfigMap"`
MapEnv *labelStructs.MapEnv `json:"map-env,omitempty" jsonschema:"title=Map Env,description=Map environment variables to another value"`
CronJob *labelStructs.CronJob `json:"cron-job,omitempty" jsonschema:"title=Cron Job,description=Cron Job configuration"`
EnvFrom *labelStructs.EnvFrom `json:"env-from,omitempty" jsonschema:"title=Env From,description=Inject environment variables from another service"`
ExchangeVolumes []*labelStructs.ExchangeVolume `json:"exchange-volumes,omitempty" jsonschema:"title=Exchange Volumes,description=Exchange volumes between services"`
ValuesFrom *labelStructs.ValueFrom `json:"values-from,omitempty" jsonschema:"title=Values From,description=Inject values from another service (secret or configmap environment variables)"`
}
```
<a name="StringOrMap"></a>
## type [StringOrMap](<https://github.com/metal3d/katenary/blob/develop/generator/katenaryfile/main.go#L24>)
StringOrMap is a struct that can be either a string or a map of strings. It's a helper struct to unmarshal the katenary.yaml file and produce the schema
```go
type StringOrMap any
```
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)

View File

@@ -0,0 +1,108 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# labels
```go
import "katenary/generator/labels"
```
## Constants
<a name="KatenaryLabelPrefix"></a>
```go
const KatenaryLabelPrefix = "katenary.v3"
```
<a name="GetLabelHelp"></a>
## func [GetLabelHelp](<https://github.com/metal3d/katenary/blob/develop/generator/labels/katenaryLabels.go#L88>)
```go
func GetLabelHelp(asMarkdown bool) string
```
Generate the help for the labels.
<a name="GetLabelHelpFor"></a>
## func [GetLabelHelpFor](<https://github.com/metal3d/katenary/blob/develop/generator/labels/katenaryLabels.go#L97>)
```go
func GetLabelHelpFor(labelname string, asMarkdown bool) string
```
GetLabelHelpFor returns the help for a specific label.
<a name="GetLabelNames"></a>
## func [GetLabelNames](<https://github.com/metal3d/katenary/blob/develop/generator/labels/katenaryLabels.go#L72>)
```go
func GetLabelNames() []string
```
GetLabelNames returns a sorted list of all katenary label names.
<a name="Prefix"></a>
## func [Prefix](<https://github.com/metal3d/katenary/blob/develop/generator/labels/katenaryLabels.go#L235>)
```go
func Prefix() string
```
<a name="Help"></a>
## type [Help](<https://github.com/metal3d/katenary/blob/develop/generator/labels/katenaryLabels.go#L64-L69>)
Help is the documentation of a label.
```go
type Help struct {
Short string `yaml:"short"`
Long string `yaml:"long"`
Example string `yaml:"example"`
Type string `yaml:"type"`
}
```
<a name="Label"></a>
## type [Label](<https://github.com/metal3d/katenary/blob/develop/generator/labels/katenaryLabels.go#L57>)
Label is a katenary label to find in compose files.
```go
type Label = string
```
<a name="LabelMainApp"></a>Known labels.
```go
const (
LabelMainApp Label = KatenaryLabelPrefix + "/main-app"
LabelValues Label = KatenaryLabelPrefix + "/values"
LabelSecrets Label = KatenaryLabelPrefix + "/secrets"
LabelPorts Label = KatenaryLabelPrefix + "/ports"
LabelIngress Label = KatenaryLabelPrefix + "/ingress"
LabelMapEnv Label = KatenaryLabelPrefix + "/map-env"
LabelHealthCheck Label = KatenaryLabelPrefix + "/health-check"
LabelSamePod Label = KatenaryLabelPrefix + "/same-pod"
LabelDescription Label = KatenaryLabelPrefix + "/description"
LabelIgnore Label = KatenaryLabelPrefix + "/ignore"
LabelDependencies Label = KatenaryLabelPrefix + "/dependencies"
LabelConfigMapFiles Label = KatenaryLabelPrefix + "/configmap-files"
LabelCronJob Label = KatenaryLabelPrefix + "/cronjob"
LabelEnvFrom Label = KatenaryLabelPrefix + "/env-from"
LabelExchangeVolume Label = KatenaryLabelPrefix + "/exchange-volumes"
LabelValueFrom Label = KatenaryLabelPrefix + "/values-from"
)
```
<a name="LabelName"></a>
### func [LabelName](<https://github.com/metal3d/katenary/blob/develop/generator/labels/katenaryLabels.go#L59>)
```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/generator/labels/labelStructs"
```
labelStructs is a package that contains the structs used to represent the labels in the yaml files.
## type [ConfigMapFile](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/configMap.go#L5>)
```go
type ConfigMapFile []string
```
<a name="ConfigMapFileFrom"></a>
### func [ConfigMapFileFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/configMap.go#L7>)
```go
func ConfigMapFileFrom(data string) (ConfigMapFile, error)
```
<a name="CronJob"></a>
## type [CronJob](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/cronJob.go#L5-L10>)
```go
type CronJob struct {
Image string `yaml:"image,omitempty" json:"image,omitempty"`
Command string `yaml:"command" json:"command,omitempty"`
Schedule string `yaml:"schedule" json:"schedule,omitempty"`
Rbac bool `yaml:"rbac" json:"rbac,omitempty"`
}
```
<a name="CronJobFrom"></a>
### func [CronJobFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/cronJob.go#L12>)
```go
func CronJobFrom(data string) (*CronJob, error)
```
<a name="Dependency"></a>
## type [Dependency](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/dependencies.go#L6-L12>)
Dependency is a dependency of a chart to other charts.
```go
type Dependency struct {
Values map[string]any `yaml:"-" json:"values,omitempty"`
Name string `yaml:"name" json:"name"`
Version string `yaml:"version" json:"version"`
Repository string `yaml:"repository" json:"repository"`
Alias string `yaml:"alias,omitempty" json:"alias,omitempty"`
}
```
<a name="DependenciesFrom"></a>
### func [DependenciesFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/dependencies.go#L15>)
```go
func DependenciesFrom(data string) ([]Dependency, error)
```
DependenciesFrom returns a slice of dependencies from the given string.
<a name="EnvFrom"></a>
## type [EnvFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/envFrom.go#L5>)
```go
type EnvFrom []string
```
<a name="EnvFromFrom"></a>
### func [EnvFromFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/envFrom.go#L8>)
```go
func EnvFromFrom(data string) (EnvFrom, error)
```
EnvFromFrom returns a EnvFrom from the given string.
<a name="ExchangeVolume"></a>
## type [ExchangeVolume](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/exchangeVolume.go#L5-L10>)
```go
type ExchangeVolume struct {
Name string `yaml:"name" json:"name"`
MountPath string `yaml:"mountPath" json:"mountPath"`
Type string `yaml:"type,omitempty" json:"type,omitempty"`
Init string `yaml:"init,omitempty" json:"init,omitempty"`
}
```
<a name="NewExchangeVolumes"></a>
### func [NewExchangeVolumes](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/exchangeVolume.go#L12>)
```go
func NewExchangeVolumes(data string) ([]*ExchangeVolume, error)
```
<a name="HealthCheck"></a>
## type [HealthCheck](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/probes.go#L11-L14>)
```go
type HealthCheck struct {
LivenessProbe *corev1.Probe `yaml:"livenessProbe,omitempty" json:"livenessProbe,omitempty"`
ReadinessProbe *corev1.Probe `yaml:"readinessProbe,omitempty" json:"readinessProbe,omitempty"`
}
```
<a name="ProbeFrom"></a>
### func [ProbeFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/probes.go#L16>)
```go
func ProbeFrom(data string) (*HealthCheck, error)
```
<a name="Ingress"></a>
## type [Ingress](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/ingress.go#L14-L22>)
```go
type Ingress struct {
Port *int32 `yaml:"port,omitempty" json:"port,omitempty"`
Annotations map[string]string `yaml:"annotations,omitempty" jsonschema:"nullable" json:"annotations,omitempty"`
Hostname string `yaml:"hostname" 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" json:"enabled,omitempty"`
TLS *TLS `yaml:"tls,omitempty" json:"tls,omitempty"`
}
```
<a name="IngressFrom"></a>
### func [IngressFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/ingress.go#L25>)
```go
func IngressFrom(data string) (*Ingress, error)
```
IngressFrom creates a new Ingress from a compose service.
<a name="MapEnv"></a>
## type [MapEnv](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/mapenv.go#L5>)
```go
type MapEnv map[string]string
```
<a name="MapEnvFrom"></a>
### func [MapEnvFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/mapenv.go#L8>)
```go
func MapEnvFrom(data string) (MapEnv, error)
```
MapEnvFrom returns a MapEnv from the given string.
<a name="Ports"></a>
## type [Ports](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/ports.go#L5>)
```go
type Ports []uint32
```
<a name="PortsFrom"></a>
### func [PortsFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/ports.go#L8>)
```go
func PortsFrom(data string) (Ports, error)
```
PortsFrom returns a Ports from the given string.
<a name="Secrets"></a>
## type [Secrets](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/secrets.go#L5>)
```go
type Secrets []string
```
<a name="SecretsFrom"></a>
### func [SecretsFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/secrets.go#L7>)
```go
func SecretsFrom(data string) (Secrets, error)
```
<a name="TLS"></a>
## type [TLS](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/ingress.go#L10-L12>)
```go
type TLS struct {
Enabled bool `yaml:"enabled" json:"enabled,omitempty"`
}
```
<a name="ValueFrom"></a>
## type [ValueFrom](<https://github.com/metal3d/katenary/blob/develop/generator/labels/labelStructs/valueFrom.go#L5>)
```go
type ValueFrom map[string]string
```
<a name="GetValueFrom"></a>
### func [GetValueFrom](<https://github.com/metal3d/katenary/blob/develop/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/parser"
```
Parser package is a wrapper around compose\-go to parse compose files.
## func [Parse](<https://github.com/metal3d/katenary/blob/develop/parser/main.go#L29>)
```go
func Parse(profiles []string, envFiles []string, dockerComposeFile ...string) (*types.Project, error)
```
Parse compose files and return a project. The project is parsed with dotenv, osenv and profiles.
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)

212
doc/docs/packages/utils.md Normal file
View File

@@ -0,0 +1,212 @@
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# utils
```go
import "katenary/utils"
```
Utils package provides some utility functions used in katenary. It defines some constants and functions used in the whole project.
## func [AsResourceName](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L193>)
```go
func AsResourceName(name string) string
```
AsResourceName returns a resource name with underscores to respect the kubernetes naming convention. It's the opposite of FixedResourceName.
<a name="Confirm"></a>
## func [Confirm](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L161>)
```go
func Confirm(question string, icon ...Icon) bool
```
Confirm asks a question and returns true if the answer is y.
<a name="CountStartingSpaces"></a>
## func [CountStartingSpaces](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L38>)
```go
func CountStartingSpaces(line string) int
```
CountStartingSpaces counts the number of spaces at the beginning of a string.
<a name="EncodeBasicYaml"></a>
## func [EncodeBasicYaml](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L175>)
```go
func EncodeBasicYaml(data any) ([]byte, error)
```
EncodeBasicYaml encodes a basic yaml from an interface.
<a name="FixedResourceName"></a>
## func [FixedResourceName](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L187>)
```go
func FixedResourceName(name string) string
```
FixedResourceName returns a resource name without underscores to respect the kubernetes naming convention.
<a name="GetContainerByName"></a>
## func [GetContainerByName](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L84>)
```go
func GetContainerByName(name string, containers []corev1.Container) (*corev1.Container, int)
```
GetContainerByName returns a container by name and its index in the array. It returns nil, \-1 if not found.
<a name="GetKind"></a>
## func [GetKind](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L51>)
```go
func GetKind(path string) (kind string)
```
GetKind returns the kind of the resource from the file path.
<a name="GetServiceNameByPort"></a>
## func [GetServiceNameByPort](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L74>)
```go
func GetServiceNameByPort(port int) string
```
GetServiceNameByPort returns the service name for a port. It the service name is not found, it returns an empty string.
<a name="GetValuesFromLabel"></a>
## func [GetValuesFromLabel](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L126>)
```go
func GetValuesFromLabel(service types.ServiceConfig, LabelValues string) map[string]*EnvConfig
```
GetValuesFromLabel returns a map of values from a label.
<a name="HashComposefiles"></a>
## func [HashComposefiles](<https://github.com/metal3d/katenary/blob/develop/utils/hash.go#L12>)
```go
func HashComposefiles(files []string) (string, error)
```
HashComposefiles returns a hash of the compose files.
<a name="Int32Ptr"></a>
## func [Int32Ptr](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L32>)
```go
func Int32Ptr(i int32) *int32
```
Int32Ptr returns a pointer to an int32.
<a name="PathToName"></a>
## func [PathToName](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L103>)
```go
func PathToName(path string) string
```
PathToName converts a path to a kubernetes complient name.
<a name="StrPtr"></a>
## func [StrPtr](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L35>)
```go
func StrPtr(s string) *string
```
StrPtr returns a pointer to a string.
<a name="TplName"></a>
## func [TplName](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L19>)
```go
func TplName(serviceName, appname string, suffix ...string) string
```
TplName returns the name of the kubernetes resource as a template string. It is used in the templates and defined in \_helper.tpl file.
<a name="TplValue"></a>
## func [TplValue](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L94>)
```go
func TplValue(serviceName, variable string, pipes ...string) string
```
GetContainerByName returns a container by name and its index in the array.
<a name="Warn"></a>
## func [Warn](<https://github.com/metal3d/katenary/blob/develop/utils/icons.go#L25>)
```go
func Warn(msg ...any)
```
Warn prints a warning message
<a name="WordWrap"></a>
## func [WordWrap](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L156>)
```go
func WordWrap(text string, lineWidth int) string
```
WordWrap wraps a string to a given line width. Warning: it may break the string. You need to check the result.
<a name="Wrap"></a>
## func [Wrap](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L68>)
```go
func Wrap(src, above, below string) string
```
Wrap wraps a string with a string above and below. It will respect the indentation of the src string.
<a name="EnvConfig"></a>
## type [EnvConfig](<https://github.com/metal3d/katenary/blob/develop/utils/utils.go#L120-L123>)
EnvConfig is a struct to hold the description of an environment variable.
```go
type EnvConfig struct {
Service types.ServiceConfig
Description string
}
```
<a name="Icon"></a>
## type [Icon](<https://github.com/metal3d/katenary/blob/develop/utils/icons.go#L6>)
Icon is a unicode icon
```go
type Icon string
```
<a name="IconSuccess"></a>Icons used in katenary.
```go
const (
IconSuccess Icon = "✅"
IconFailure Icon = "❌"
IconWarning Icon = "❕"
IconNote Icon = "📝"
IconWorld Icon = "🌐"
IconPlug Icon = "🔌"
IconPackage Icon = "📦"
IconCabinet Icon = "🗄️"
IconInfo Icon = "🔵"
IconSecret Icon = "🔒"
IconConfig Icon = "🔧"
IconDependency Icon = "🔗"
)
```
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)

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

263
doc/docs/usage.md Normal file
View File

@@ -0,0 +1,263 @@
# 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 configuration 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 init containers
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])

65
doc/mkdocs.yml Normal file
View File

@@ -0,0 +1,65 @@
site_name: Katenary documentation
docs_dir: ./docs
plugins:
- search
- inline-svg
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/github
link: https://github.com/metal3d/katenary
nav:
- "Home": index.md
- usage.md
- labels.md
- Behind the scene:
- coding.md
- dependencies.md
- FAQ: faq.md
- Go Packages:
- packages/cmd/katenary.md
- packages/parser.md
- packages/utils.md
- Generator:
- Index: packages/generator.md
- ExtraFiles: packages/generator/extrafiles.md
- labels:
- packages/generator/labels.md
- LabelStructs: packages/generator/labels/labelStructs.md
- KatenaryFile: packages/generator/katenaryfile.md

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>

7
doc/requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
mkdocs==1.*
Jinja2==3.*
MarkupSafe==3.*
pymdown-extensions==10.*
mkdocs-material==9.*
mkdocs-material-extensions==1.*
mkdocs-plugin-inline-svg-mod

View File

@@ -1,10 +0,0 @@
# Basic example
This is a basic example of what can do Katenary with standard docker-compose file.
In this example:
- `depends_on` yield a `initContainer` in the webapp ddeployment to wait for database
- so we need to declare the listened port inside `database` container as we don't use it with docker-compose- also, we needed to declare that `DB_HOST` is actually a service name
Take a look on [chart/basic](chart/basic) directory to see what `katenary convert` command has generated.

View File

@@ -1,8 +0,0 @@
# Create on 2022-02-17T10:27:30+01:00
# Katenary command line: katenary convert
apiVersion: v2
appVersion: 0.0.1
description: A helm chart for basic
name: basic
type: application
version: 0.1.0

View File

@@ -1,8 +0,0 @@
Congratulations,
Your application is now deployed. This may take a while to be up and responding.
{{ if .Values.webapp.ingress.enabled -}}
- webapp is accessible on : http://{{ .Values.webapp.ingress.host }}
{{- end }}

View File

@@ -1,39 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}-database'
labels:
katenary.io/component: database
katenary.io/project: basic
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
katenary.io/version: master-3619cc4
spec:
replicas: 1
selector:
matchLabels:
katenary.io/component: database
katenary.io/release: '{{ .Release.Name }}'
template:
metadata:
labels:
katenary.io/component: database
katenary.io/release: '{{ .Release.Name }}'
spec:
containers:
- name: database
image: '{{ .Values.database.image }}'
ports:
- name: database
containerPort: 3306
env:
- name: MARIADB_PASSWORD
value: foo
- name: MARIADB_DATABASE
value: myapp
- name: MARIADB_ROOT_PASSWORD
value: foobar
- name: MARIADB_USER
value: foo

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}-database'
labels:
katenary.io/component: database
katenary.io/project: basic
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
katenary.io/version: master-3619cc4
spec:
selector:
katenary.io/component: database
katenary.io/release: '{{ .Release.Name }}'
ports:
- protocol: TCP
port: 3306
targetPort: 3306

View File

@@ -1,48 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}-webapp'
labels:
katenary.io/component: webapp
katenary.io/project: basic
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
katenary.io/version: master-3619cc4
spec:
replicas: 1
selector:
matchLabels:
katenary.io/component: webapp
katenary.io/release: '{{ .Release.Name }}'
template:
metadata:
labels:
katenary.io/component: webapp
katenary.io/release: '{{ .Release.Name }}'
spec:
initContainers:
- name: check-database
image: busybox
command:
- sh
- -c
- |-
OK=0
echo "Checking database port"
while [ $OK != 1 ]; do
echo -n "."
nc -z {{ .Release.Name }}-database 3306 2>&1 >/dev/null && OK=1 || sleep 1
done
echo
echo "Done"
containers:
- name: webapp
image: '{{ .Values.webapp.image }}'
ports:
- name: webapp
containerPort: 80
env:
- name: DB_HOST
value: '{{ .Release.Name }}-database'

View File

@@ -1,34 +0,0 @@
{{- if .Values.webapp.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: '{{ .Release.Name }}-webapp'
labels:
katenary.io/component: webapp
katenary.io/project: basic
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
katenary.io/version: master-3619cc4
spec:
{{- if and .Values.webapp.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: '{{ .Values.webapp.ingress.class }}'
{{- end }}
rules:
- host: '{{ .Values.webapp.ingress.host }}'
http:
paths:
- path: /
pathType: Prefix
backend:
{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}
service:
name: '{{ .Release.Name }}-webapp'
port:
number: 80
{{- else }}
serviceName: '{{ .Release.Name }}-webapp'
servicePort: 80
{{- end }}
{{- end -}}

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}-webapp'
labels:
katenary.io/component: webapp
katenary.io/project: basic
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: b9f12bb7d1e97901c1d7680394209525763f6640
katenary.io/version: master-3619cc4
spec:
selector:
katenary.io/component: webapp
katenary.io/release: '{{ .Release.Name }}'
ports:
- protocol: TCP
port: 80
targetPort: 80

View File

@@ -1,8 +0,0 @@
database:
image: mariadb:10
webapp:
image: php:7-apache
ingress:
class: nginx
enabled: false
host: webapp.basic.tld

View File

@@ -1,29 +0,0 @@
version: "3"
services:
webapp:
image: php:7-apache
environment:
DB_HOST: database
ports:
- "8080:80"
labels:
# expose an ingress
katenary.io/ingress: 80
# DB_HOST is actually a service name
katenary.io/env-to-service: DB_HOST
depends_on:
- database
database:
image: mariadb:10
environment:
MARIADB_ROOT_PASSWORD: foobar
MARIADB_USER: foo
MARIADB_PASSWORD: foo
MARIADB_DATABASE: myapp
labels:
# because we don't provide "ports" or "expose", alert katenary
# to use the mysql port for service declaration
katenary.io/ports: 3306

View File

@@ -1,13 +0,0 @@
# Make it possible to bind several containers in one pod
In this example, we need to make nginx and php-fpm to run inside the same "pod". The reason is that we configured FPM to listen an unix socket instead of the 9000 port.
Because NGinx will need to connect to the unix socket wich is a file, both containers should share the same node and work together.
So, in the docker-compose file, we need to declare:
- `katenary.io/empty-dirs: socket` where `socket` is the "volume name", this will avoid the creation of a PVC
- `katenary.io/same-pod: http` in `php` container to declare that this will be added in the `containers` section of the `http` deployment
You can note that we also use `configmap-volumes` to declare our configuration as `configMap`.
Take a look on [chart/same-pod](chart/same-pod) directory to see the result of the `katenary convert` command.

View File

@@ -1,8 +0,0 @@
# Create on 2022-02-17T11:36:02+01:00
# Katenary command line: katenary convert --force
apiVersion: v2
appVersion: 0.0.1
description: A helm chart for same-pod
name: same-pod
type: application
version: 0.1.0

View File

@@ -1,8 +0,0 @@
Congratulations,
Your application is now deployed. This may take a while to be up and responding.
{{ if .Values.http.ingress.enabled -}}
- http is accessible on : http://{{ .Values.http.ingress.host }}
{{- end }}

View File

@@ -1,23 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: '{{ .Release.Name }}-config-nginx-http'
labels:
katenary.io/component: ""
katenary.io/project: same-pod
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
katenary.io/version: master-bf44d44
data:
default.conf: |
upstream _php {
server unix:/sock/fpm.sock;
}
server {
listen 80;
location ~ ^/index\.php(/|$) {
fastcgi_pass _php;
include fastcgi_params;
}
}

View File

@@ -1,30 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: '{{ .Release.Name }}-config-php-php'
labels:
katenary.io/component: ""
katenary.io/project: same-pod
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
katenary.io/version: master-bf44d44
data:
www.conf: |
[www]
user = www-data
group = www-data
listen = /sock/fpm.sock
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
access.log = /proc/self/fd/2
log_limit = 8192
clear_env = no
catch_workers_output = yes
decorate_workers_output = no

View File

@@ -1,52 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}-http'
labels:
katenary.io/component: http
katenary.io/project: same-pod
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
katenary.io/version: master-bf44d44
spec:
replicas: 1
selector:
matchLabels:
katenary.io/component: http
katenary.io/release: '{{ .Release.Name }}'
template:
metadata:
labels:
katenary.io/component: http
katenary.io/release: '{{ .Release.Name }}'
spec:
containers:
- name: http
image: '{{ .Values.http.image }}'
ports:
- name: http
containerPort: 80
volumeMounts:
- mountPath: /sock
name: sock
- mountPath: /etc/nginx/conf.d
name: config-nginx
- name: php
image: '{{ .Values.php.image }}'
volumeMounts:
- mountPath: /sock
name: sock
- mountPath: /usr/local/etc/php-fpm.d/www.conf
name: config-php
subPath: www.conf
volumes:
- emptyDir: {}
name: sock
- configMap:
name: '{{ .Release.Name }}-config-nginx-http'
name: config-nginx
- configMap:
name: '{{ .Release.Name }}-config-php-php'
name: config-php

View File

@@ -1,34 +0,0 @@
{{- if .Values.http.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: '{{ .Release.Name }}-http'
labels:
katenary.io/component: http
katenary.io/project: same-pod
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
katenary.io/version: master-bf44d44
spec:
{{- if and .Values.http.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: '{{ .Values.http.ingress.class }}'
{{- end }}
rules:
- host: '{{ .Values.http.ingress.host }}'
http:
paths:
- path: /
pathType: Prefix
backend:
{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}
service:
name: '{{ .Release.Name }}-http'
port:
number: 80
{{- else }}
serviceName: '{{ .Release.Name }}-http'
servicePort: 80
{{- end }}
{{- end -}}

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}-http'
labels:
katenary.io/component: http
katenary.io/project: same-pod
katenary.io/release: '{{ .Release.Name }}'
annotations:
katenary.io/docker-compose-sha1: 74e67695bfdbb829f15531321e158808018280e0
katenary.io/version: master-bf44d44
spec:
selector:
katenary.io/component: http
katenary.io/release: '{{ .Release.Name }}'
ports:
- protocol: TCP
port: 80
targetPort: 80

View File

@@ -1,8 +0,0 @@
http:
image: nginx:alpine
ingress:
class: nginx
enabled: false
host: http.same-pod.tld
php:
image: php:fpm

View File

@@ -1,10 +0,0 @@
upstream _php {
server unix:/sock/fpm.sock;
}
server {
listen 80;
location ~ ^/index\.php(/|$) {
fastcgi_pass _php;
include fastcgi_params;
}
}

View File

@@ -1,17 +0,0 @@
[www]
user = www-data
group = www-data
listen = /sock/fpm.sock
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
access.log = /proc/self/fd/2
log_limit = 8192
clear_env = no
catch_workers_output = yes
decorate_workers_output = no

View File

@@ -1,38 +0,0 @@
version: "3"
services:
http:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- "sock:/sock"
- "./config/nginx:/etc/nginx/conf.d:z"
labels:
# the "sock" volume will need to be shared to the same pod, so let's
# declare that this is not a PVC
katenary.io/empty-dirs: sock
# use ./config/nginx as a configMap
katenary.io/configmap-volumes: ./config/nginx
# declare an ingress
katenary.io/ingress: 80
php:
image: php:fpm
volumes:
- "sock:/sock"
- "./config/php/www.conf:/usr/local/etc/php-fpm.d/www.conf:z"
labels:
# fpm will need to use a unix socket shared
# with nginx (http service above), so we want here
# make a single pod containing nginx and php
katenary.io/same-pod: http
# use the ./config/php files as a configMap
katenary.io/configmap-volumes: ./config/php/www.conf
volumes:
sock:

426
generator/chart.go Normal file
View File

@@ -0,0 +1,426 @@
package generator
import (
"fmt"
"katenary/generator/labels"
"katenary/generator/labels/labelStructs"
"katenary/utils"
"log"
"maps"
"os"
"path/filepath"
"slices"
"strings"
"github.com/compose-spec/compose-go/types"
corev1 "k8s.io/api/core/v1"
)
// ChartTemplate is a template of a chart. It contains the content of the template and the name of the service.
// This is used internally to generate the templates.
type ChartTemplate struct {
Servicename string
Content []byte
}
// ConvertOptions are the options to convert a compose project to a helm chart.
type ConvertOptions struct {
AppVersion *string
OutputDir string
ChartVersion string
Icon string
Profiles []string
EnvFiles []string
Force bool
HelmUpdate bool
}
// HelmChart is a Helm Chart representation. It contains all the
// templates, values, versions, helpers...
type HelmChart struct {
Templates map[string]*ChartTemplate `yaml:"-"`
Values map[string]any `yaml:"-"`
VolumeMounts map[string]any `yaml:"-"`
composeHash *string `yaml:"-"`
Name string `yaml:"name"`
Icon string `yaml:"icon,omitempty"`
APIVersion string `yaml:"apiVersion"`
Version string `yaml:"version"`
AppVersion string `yaml:"appVersion"`
Description string `yaml:"description"`
Helper string `yaml:"-"`
Dependencies []labelStructs.Dependency `yaml:"dependencies,omitempty"`
}
// NewChart creates a new empty chart with the given name.
func NewChart(name string) *HelmChart {
return &HelmChart{
Name: name,
Templates: make(map[string]*ChartTemplate, 0),
Description: "A Helm chart for " + name,
APIVersion: "v2",
Version: "",
AppVersion: "", // set to 0.1.0 by default if no "main-app" label is found
Values: map[string]any{
"pullSecrets": []string{},
},
}
}
// SaveTemplates the templates of the chart to the given directory.
func (chart *HelmChart) SaveTemplates(templateDir string) {
for name, template := range chart.Templates {
t := template.Content
t = removeNewlinesInsideBrackets(t)
t = removeUnwantedLines(t)
// t = addModeline(t)
kind := utils.GetKind(name)
var icon utils.Icon
switch kind {
case "deployment":
icon = utils.IconPackage
case "service":
icon = utils.IconPlug
case "ingress":
icon = utils.IconWorld
case "volumeclaim":
icon = utils.IconCabinet
case "configmap":
icon = utils.IconConfig
case "secret":
icon = utils.IconSecret
default:
icon = utils.IconInfo
}
servicename := template.Servicename
if err := os.MkdirAll(filepath.Join(templateDir, servicename), 0o755); 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), 0o755)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
} else {
// remove the serivce name from the template name
name = strings.Replace(name, servicename+".", "", 1)
name = filepath.Join(templateDir, servicename, name)
}
f, err := os.Create(name)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
defer f.Close()
if _, err := f.Write(t); err != nil {
log.Fatal("error writing template file:", err)
}
}
}
// generateConfigMapsAndSecrets creates the configmaps and secrets from the environment variables.
func (chart *HelmChart) generateConfigMapsAndSecrets(project *types.Project) error {
appName := chart.Name
for _, s := range project.Services {
if len(s.Environment) == 0 {
continue
}
originalEnv := types.MappingWithEquals{}
secretsVar := types.MappingWithEquals{}
// copy env to originalEnv
maps.Copy(originalEnv, s.Environment)
if v, ok := s.Labels[labels.LabelSecrets]; ok {
list, err := labelStructs.SecretsFrom(v)
if err != nil {
log.Fatal("error unmarshaling secrets label:", err)
}
for _, secret := range list {
if secret == "" {
continue
}
if _, ok := s.Environment[secret]; !ok {
fmt.Printf("%s secret %s not found in environment", utils.IconWarning, secret)
continue
}
secretsVar[secret] = s.Environment[secret]
}
}
if len(secretsVar) > 0 {
s.Environment = secretsVar
sec := NewSecret(s, appName)
y, _ := sec.Yaml()
name := sec.service.Name
chart.Templates[name+".secret.yaml"] = &ChartTemplate{
Content: y,
Servicename: s.Name,
}
}
// remove secrets from env
s.Environment = originalEnv // back to original
for k := range secretsVar {
delete(s.Environment, k)
}
if len(s.Environment) > 0 {
cm := NewConfigMap(s, appName, false)
y, _ := cm.Yaml()
name := cm.service.Name
chart.Templates[name+".configmap.yaml"] = &ChartTemplate{
Content: y,
Servicename: s.Name,
}
}
}
return nil
}
func (chart *HelmChart) generateDeployment(service types.ServiceConfig, deployments map[string]*Deployment, services map[string]*Service, podToMerge map[string]*types.ServiceConfig, appName string) error {
// check the "ports" label from container and add it to the service
if err := fixPorts(&service); err != nil {
return err
}
// isgnored service
if isIgnored(service) {
fmt.Printf("%s Ignoring service %s\n", utils.IconInfo, service.Name)
return nil
}
// helm dependency
if isHelmDependency, err := chart.setDependencies(service); err != nil {
return err
} else if isHelmDependency {
return nil
}
// create all deployments
d := NewDeployment(service, chart)
deployments[service.Name] = d
// generate the cronjob if needed
chart.setCronJob(service, appName)
if exchange, ok := service.Labels[labels.LabelExchangeVolume]; ok {
// we need to add a volume and a mount point
ex, err := labelStructs.NewExchangeVolumes(exchange)
if err != nil {
return err
}
for _, exchangeVolume := range ex {
d.AddLegacyVolume("exchange-"+exchangeVolume.Name, exchangeVolume.Type)
d.exchangesVolumes[service.Name] = exchangeVolume
}
}
// get the same-pod label if exists, add it to the list.
// We later will copy some parts to the target deployment and remove this one.
if samePod, ok := service.Labels[labels.LabelSamePod]; ok && samePod != "" {
podToMerge[samePod] = &service
}
// create the needed service for the container port
if len(service.Ports) > 0 {
s := NewService(service, appName)
services[service.Name] = s
}
// create all ingresses
if ingress := d.AddIngress(service, appName); ingress != nil {
y, _ := ingress.Yaml()
chart.Templates[ingress.Filename()] = &ChartTemplate{
Content: y,
Servicename: service.Name,
}
}
return nil
}
// setChartVersion sets the chart version from the service image tag.
func (chart *HelmChart) setChartVersion(service types.ServiceConfig) {
if chart.Version == "" {
image := service.Image
parts := strings.Split(image, ":")
if len(parts) > 1 {
chart.AppVersion = parts[1]
} else {
chart.AppVersion = "0.1.0"
}
}
}
// setCronJob creates a cronjob from the service labels.
func (chart *HelmChart) setCronJob(service types.ServiceConfig, appName string) *CronJob {
if _, ok := service.Labels[labels.LabelCronJob]; !ok {
return nil
}
cronjob, rbac := NewCronJob(service, chart, appName)
y, _ := cronjob.Yaml()
chart.Templates[cronjob.Filename()] = &ChartTemplate{
Content: y,
Servicename: service.Name,
}
if rbac != nil {
y, _ := rbac.RoleBinding.Yaml()
chart.Templates[rbac.RoleBinding.Filename()] = &ChartTemplate{
Content: y,
Servicename: service.Name,
}
y, _ = rbac.Role.Yaml()
chart.Templates[rbac.Role.Filename()] = &ChartTemplate{
Content: y,
Servicename: service.Name,
}
y, _ = rbac.ServiceAccount.Yaml()
chart.Templates[rbac.ServiceAccount.Filename()] = &ChartTemplate{
Content: y,
Servicename: service.Name,
}
}
return cronjob
}
// setDependencies sets the dependencies from the service labels.
func (chart *HelmChart) setDependencies(service types.ServiceConfig) (bool, error) {
// helm dependency
if v, ok := service.Labels[labels.LabelDependencies]; ok {
d, err := labelStructs.DependenciesFrom(v)
if err != nil {
return false, err
}
for _, dep := range d {
fmt.Printf("%s Adding dependency to %s\n", utils.IconDependency, dep.Name)
chart.Dependencies = append(chart.Dependencies, dep)
name := dep.Name
if dep.Alias != "" {
name = dep.Alias
}
// add the dependency env vars to the values.yaml
chart.Values[name] = dep.Values
}
return true, nil
}
return false, nil
}
// setSharedConf sets the shared configmap to the service.
func (chart *HelmChart) setSharedConf(service types.ServiceConfig, deployments map[string]*Deployment) {
// if the service has the "shared-conf" label, we need to add the configmap
// to the chart and add the env vars to the service
if _, ok := service.Labels[labels.LabelEnvFrom]; !ok {
return
}
fromservices, err := labelStructs.EnvFromFrom(service.Labels[labels.LabelEnvFrom])
if err != nil {
log.Fatal("error unmarshaling env-from label:", err)
}
// find the configmap in the chart templates
for _, fromservice := range fromservices {
if _, ok := chart.Templates[fromservice+".configmap.yaml"]; !ok {
log.Printf("configmap %s not found in chart templates", fromservice)
continue
}
// find the corresponding target deployment
target := findDeployment(service.Name, deployments)
if target == nil {
continue
}
// add the configmap to the service
addConfigMapToService(service.Name, fromservice, chart.Name, target)
}
}
// setEnvironmentValuesFrom sets the environment values from another service.
func (chart *HelmChart) setEnvironmentValuesFrom(service types.ServiceConfig, deployments map[string]*Deployment) {
if _, ok := service.Labels[labels.LabelValueFrom]; !ok {
return
}
mapping, err := labelStructs.GetValueFrom(service.Labels[labels.LabelValueFrom])
if err != nil {
log.Fatal("error unmarshaling values-from label:", err)
}
findDeployment := func(name string) *Deployment {
for _, dep := range deployments {
if dep.service.Name == name {
return dep
}
}
return nil
}
// each mapping key is the environment, and the value is serivename.variable name
for env, from := range *mapping {
// find the deployment that has the variable
depName := strings.Split(from, ".")
dep := findDeployment(depName[0])
target := findDeployment(service.Name)
if dep == nil || target == nil {
log.Fatalf("deployment %s or %s not found", depName[0], service.Name)
}
container, index := utils.GetContainerByName(target.service.ContainerName, target.Spec.Template.Spec.Containers)
if container == nil {
log.Fatalf("Container %s not found", target.GetName())
}
reourceName := fmt.Sprintf(`{{ include "%s.fullname" . }}-%s`, chart.Name, depName[0])
// add environment with from
// is it a secret?
isSecret := false
secrets, err := labelStructs.SecretsFrom(dep.service.Labels[labels.LabelSecrets])
if err == nil {
if slices.Contains(secrets, depName[1]) {
isSecret = true
}
}
if !isSecret {
container.Env = append(container.Env, corev1.EnvVar{
Name: env,
ValueFrom: &corev1.EnvVarSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: reourceName,
},
Key: depName[1],
},
},
})
} else {
container.Env = append(container.Env, corev1.EnvVar{
Name: env,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: reourceName,
},
Key: depName[1],
},
},
})
}
// the environment is bound, so we shouldn't add it to the values.yaml or in any other place
delete(service.Environment, env)
// also, remove the values
target.boundEnvVar = append(target.boundEnvVar, env)
// and save the container
target.Spec.Template.Spec.Containers[index] = *container
}
}

157
generator/chart_test.go Normal file
View File

@@ -0,0 +1,157 @@
package generator
import (
"fmt"
"katenary/generator/labels"
"os"
"strings"
"testing"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
func TestValuesFrom(t *testing.T) {
composeFile := `
services:
aa:
image: nginx:latest
environment:
AA_USER: foo
bb:
image: nginx:latest
labels:
%[1]s/values-from: |-
BB_USER: aa.USER
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/aa/configmap.yaml")
configMap := v1.ConfigMap{}
if err := yaml.Unmarshal([]byte(output), &configMap); err != nil {
t.Errorf(unmarshalError, err)
}
data := configMap.Data
if v, ok := data["AA_USER"]; !ok || v != "foo" {
t.Errorf("Expected AA_USER to be foo, got %s", v)
}
}
func TestValuesFromCopy(t *testing.T) {
composeFile := `
services:
aa:
image: nginx:latest
environment:
AA_USER: foo
bb:
image: nginx:latest
labels:
%[1]s/values-from: |-
BB_USER: aa.AA_USER
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/bb/deployment.yaml")
dep := appsv1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dep); err != nil {
t.Errorf(unmarshalError, err)
}
containers := dep.Spec.Template.Spec.Containers
environment := containers[0].Env[0]
envFrom := environment.ValueFrom.ConfigMapKeyRef
if envFrom.Key != "AA_USER" {
t.Errorf("Expected AA_USER, got %s", envFrom.Key)
}
if !strings.Contains(envFrom.Name, "aa") {
t.Errorf("Expected aa, got %s", envFrom.Name)
}
}
func TestValuesFromSecret(t *testing.T) {
composeFile := `
services:
aa:
image: nginx:latest
environment:
AA_USER: foo
labels:
%[1]s/secrets: |-
- AA_USER
bb:
image: nginx:latest
labels:
%[1]s/values-from: |-
BB_USER: aa.AA_USER
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/bb/deployment.yaml")
dep := appsv1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dep); err != nil {
t.Errorf(unmarshalError, err)
}
containers := dep.Spec.Template.Spec.Containers
environment := containers[0].Env[0]
envFrom := environment.ValueFrom.SecretKeyRef
if envFrom.Key != "AA_USER" {
t.Errorf("Expected AA_USER, got %s", envFrom.Key)
}
if !strings.Contains(envFrom.Name, "aa") {
t.Errorf("Expected aa, got %s", envFrom.Name)
}
}
func TestEnvFrom(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
environment:
Foo: bar
BAZ: qux
db:
image: postgres
labels:
%[1]s/env-from: |-
- web
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/db/deployment.yaml")
dep := appsv1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dep); err != nil {
t.Errorf(unmarshalError, err)
}
envFrom := dep.Spec.Template.Spec.Containers[0].EnvFrom
if len(envFrom) != 1 {
t.Fatalf("Expected 1 envFrom, got %d", len(envFrom))
}
}

254
generator/configMap.go Normal file
View File

@@ -0,0 +1,254 @@
package generator
import (
"fmt"
"katenary/generator/labels"
"katenary/generator/labels/labelStructs"
"katenary/utils"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"unicode/utf8"
"github.com/compose-spec/compose-go/types"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// FileMapUsage is the usage of the filemap.
type FileMapUsage uint8
// FileMapUsage constants.
const (
FileMapUsageConfigMap FileMapUsage = iota // pure configmap for key:values.
FileMapUsageFiles // files in a configmap.
)
// only used to check interface implementation
var (
_ DataMap = (*ConfigMap)(nil)
_ Yaml = (*ConfigMap)(nil)
)
// ConfigMap is a kubernetes ConfigMap.
// Implements the DataMap interface.
type ConfigMap struct {
*corev1.ConfigMap
service *types.ServiceConfig
path string
usage FileMapUsage
}
// NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name.
// The ConfigMap is filled by environment variables and labels "map-env".
func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap {
done := map[string]bool{}
drop := map[string]bool{}
labelValues := []string{}
cm := &ConfigMap{
service: &service,
ConfigMap: &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: utils.TplName(service.Name, appName),
Labels: GetLabels(service.Name, appName),
Annotations: Annotations,
},
Data: make(map[string]string),
},
}
// get the secrets from the labels
secrets, err := labelStructs.SecretsFrom(service.Labels[labels.LabelSecrets])
if err != nil {
log.Fatal(err)
}
// drop the secrets from the environment
for _, secret := range secrets {
drop[secret] = true
}
// get the label values from the labels
varDescriptons := utils.GetValuesFromLabel(service, labels.LabelValues)
for value := range varDescriptons {
labelValues = append(labelValues, value)
}
// change the environment variables to the values defined in the values.yaml
for _, value := range labelValues {
if _, ok := service.Environment[value]; !ok {
done[value] = true
continue
}
}
if !forFile {
// do not bind env variables to the configmap
// remove the variables that are already defined in the environment
if l, ok := service.Labels[labels.LabelMapEnv]; ok {
envmap, err := labelStructs.MapEnvFrom(l)
if err != nil {
log.Fatal("Error parsing map-env", err)
}
for key, value := range envmap {
cm.AddData(key, strings.ReplaceAll(value, "__APP__", appName))
done[key] = true
}
}
for key, env := range service.Environment {
_, isDropped := drop[key]
_, isDone := done[key]
if isDropped || isDone {
continue
}
cm.AddData(key, *env)
}
}
return cm
}
// NewConfigMapFromDirectory creates a new ConfigMap from a compose service. This path is the path to the
// file or directory. If the path is a directory, all files in the directory are added to the ConfigMap.
// Each subdirectory are ignored. Note that the Generate() function will create the subdirectories ConfigMaps.
func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap {
normalized := path
normalized = strings.TrimLeft(normalized, ".")
normalized = strings.TrimLeft(normalized, "/")
normalized = regexp.MustCompile(`[^a-zA-Z0-9-]+`).ReplaceAllString(normalized, "-")
cm := &ConfigMap{
path: path,
service: &service,
usage: FileMapUsageFiles,
ConfigMap: &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: utils.TplName(service.Name, appName) + "-" + normalized,
Labels: GetLabels(service.Name, appName),
Annotations: Annotations,
},
Data: make(map[string]string),
},
}
// cumulate the path to the WorkingDir
path = filepath.Join(service.WorkingDir, path)
path = filepath.Clean(path)
if err := cm.AppendDir(path); err != nil {
log.Fatal("Error adding files to configmap:", err)
}
return cm
}
// AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists.
func (c *ConfigMap) AddData(key, value string) {
c.Data[key] = value
}
// AddBinaryData adds binary data to the configmap. Append or overwrite the value if the key already exists.
func (c *ConfigMap) AddBinaryData(key string, value []byte) {
if c.BinaryData == nil {
c.BinaryData = make(map[string][]byte)
}
c.BinaryData[key] = value
}
// AppendDir adds files from given path to the configmap. It is not recursive, to add all files in a directory,
// you need to call this function for each subdirectory.
func (c *ConfigMap) AppendDir(path string) error {
// read all files in the path and add them to the configmap
stat, err := os.Stat(path)
if err != nil {
return fmt.Errorf("path %s does not exist, %w", path, err)
}
// recursively read all files in the path and add them to the configmap
if stat.IsDir() {
files, err := os.ReadDir(path)
if err != nil {
return err
}
for _, file := range files {
if file.IsDir() {
utils.Warn("Subdirectories are ignored for the moment, skipping", filepath.Join(path, file.Name()))
continue
}
path := filepath.Join(path, file.Name())
content, err := os.ReadFile(path)
if err != nil {
return err
}
// remove the path from the file
filename := filepath.Base(path)
if utf8.Valid(content) {
c.AddData(filename, string(content))
} else {
c.AddBinaryData(filename, content)
}
}
} else {
// add the file to the configmap
content, err := os.ReadFile(path)
if err != nil {
return err
}
filename := filepath.Base(path)
if utf8.Valid(content) {
c.AddData(filename, string(content))
} else {
c.AddBinaryData(filename, content)
}
}
return nil
}
func (c *ConfigMap) AppendFile(path string) error {
// read all files in the path and add them to the configmap
stat, err := os.Stat(path)
if err != nil {
return fmt.Errorf("path %s doesn not exists, %w", path, err)
}
// recursively read all files in the path and add them to the configmap
if !stat.IsDir() {
// add the file to the configmap
content, err := os.ReadFile(path)
if err != nil {
return err
}
if utf8.Valid(content) {
c.AddData(filepath.Base(path), string(content))
} else {
c.AddBinaryData(filepath.Base(path), content)
}
}
return nil
}
// Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path.
func (c *ConfigMap) Filename() string {
switch c.usage {
case FileMapUsageFiles:
return filepath.Join(c.service.Name, "statics", c.path, "configmap.yaml")
default:
return c.service.Name + ".configmap.yaml"
}
}
// SetData sets the data of the configmap. It replaces the entire data.
func (c *ConfigMap) SetData(data map[string]string) {
c.Data = data
}
// Yaml returns the yaml representation of the configmap
func (c *ConfigMap) Yaml() ([]byte, error) {
return ToK8SYaml(c)
}

View File

@@ -0,0 +1,92 @@
package generator
import (
"fmt"
"katenary/generator/labels"
"os"
"testing"
"github.com/compose-spec/compose-go/types"
v1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
func TestEnvInConfigMap(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
environment:
- FOO=bar
- BAR=baz
`
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/web/configmap.yaml")
configMap := v1.ConfigMap{}
if err := yaml.Unmarshal([]byte(output), &configMap); err != nil {
t.Errorf(unmarshalError, err)
}
data := configMap.Data
if len(data) != 2 {
t.Errorf("Expected 2 data, got %d", len(data))
}
if data["FOO"] != "bar" {
t.Errorf("Expected FOO to be bar, got %s", data["FOO"])
}
if data["BAR"] != "baz" {
t.Errorf("Expected BAR to be baz, got %s", data["BAR"])
}
}
func TestMapEnv(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
environment:
FOO: bar
labels:
%[1]s/map-env: |-
FOO: 'baz'
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/web/configmap.yaml")
configMap := v1.ConfigMap{}
if err := yaml.Unmarshal([]byte(output), &configMap); err != nil {
t.Errorf(unmarshalError, err)
}
data := configMap.Data
if v, ok := data["FOO"]; !ok || v != "baz" {
t.Errorf("Expected FOO to be baz, got %s", v)
}
}
func TestAppendBadFile(t *testing.T) {
cm := NewConfigMap(types.ServiceConfig{}, "app", true)
err := cm.AppendFile("foo")
if err == nil {
t.Errorf("Expected error, got nil")
}
}
func TestAppendBadDir(t *testing.T) {
cm := NewConfigMap(types.ServiceConfig{}, "app", true)
err := cm.AppendDir("foo")
if err == nil {
t.Errorf("Expected error, got nil")
}
}

700
generator/converter.go Normal file
View File

@@ -0,0 +1,700 @@
package generator
import (
"bytes"
"errors"
"fmt"
"katenary/generator/extrafiles"
"katenary/generator/katenaryfile"
"katenary/generator/labels"
"katenary/generator/labels/labelStructs"
"katenary/parser"
"katenary/utils"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/compose-spec/compose-go/types"
)
const ingressClassHelp = `# Default value for ingress.class annotation
# class: "-"
# If the value is "-", controller will not set ingressClassName
# If the value is "", Ingress will be set to an empty string, so
# controller will use the default value for ingressClass
# If the value is specified, controller will set the named class e.g. "nginx"
`
const storageClassHelp = `# Storage class to use for PVCs
# storageClass: "-" means use default
# storageClass: "" means do not specify
# storageClass: "foo" means use that storageClass
`
const headerHelp = `# This file is autogenerated by katenary
#
# DO NOT EDIT IT BY HAND UNLESS YOU KNOW WHAT YOU ARE DOING
#
# If you want to change the content of this file, you should edit the
# compose file and run katenary again.
# If you need to override some values, you can do it in a override file
# and use the -f flag to specify it when running the helm command.
`
const imagePullSecretHelp = `
# imagePullSecrets allows you to specify a name of an image pull secret.
# You must provide a list of object with the name field set to the name of the
# e.g.
# pullSecrets:
# - name: regcred
# You are, for now, responsible for creating the secret.
`
const imagePullPolicyHelp = `# imagePullPolicy allows you to specify a policy to cache or always pull an image.
# You must provide a string value with one of the following values:
# - Always -> will always pull the image
# - Never -> will never pull the image, the image should be present on the node
# - IfNotPresent -> will pull the image only if it is not present on the node
`
const resourceHelp = `# Resources allows you to specify the resource requests and limits for a service.
# Resources are used to specify the amount of CPU and memory that
# a container needs.
#
# e.g.
# resources:
# requests:
# memory: "64Mi"
# cpu: "250m"
# limits:
# memory: "128Mi"
# cpu: "500m"
`
const mainTagAppDoc = `This is the version of the main application.
Leave it to blank to use the Chart "AppVersion" value.`
var unwantedLines = []string{
"creationTimestamp:",
"status:",
}
var ingressTLSHelp = `# Ingress TLS configuration
# If enabled, a secret containing the certificate and the key should be
# created by the ingress controller. If the name if emtpy, so the secret
# name is generated. You can specify the secret name to use your own secret.
`
// keyRegExp checks if the line starts by a #
var keyRegExp = regexp.MustCompile(`^\s*[^#]+:.*`)
// Convert a compose (docker, podman...) project to a helm chart.
// It calls Generate() to generate the chart and then write it to the disk.
func Convert(config ConvertOptions, dockerComposeFile ...string) error {
var (
templateDir = filepath.Join(config.OutputDir, "templates")
helpersPath = filepath.Join(config.OutputDir, "templates", "_helpers.tpl")
chartPath = filepath.Join(config.OutputDir, "Chart.yaml")
valuesPath = filepath.Join(config.OutputDir, "values.yaml")
readmePath = filepath.Join(config.OutputDir, "README.md")
notesPath = filepath.Join(templateDir, "NOTES.txt")
)
// the current working directory is the directory
currentDir, _ := os.Getwd()
// go to the root of the project
if err := os.Chdir(filepath.Dir(dockerComposeFile[0])); err != nil {
fmt.Println(utils.IconFailure, err)
return err
}
defer func() {
if err := os.Chdir(currentDir); err != nil { // after the generation, go back to the original directory
log.Fatal(err)
}
}()
// repove the directory part of the docker-compose files
for i, f := range dockerComposeFile {
dockerComposeFile[i] = filepath.Base(f)
}
// parse the compose files
project, err := parser.Parse(config.Profiles, config.EnvFiles, dockerComposeFile...)
if err != nil {
fmt.Println(err)
return err
}
// check older version of labels
if err := checkOldLabels(project); err != nil {
fmt.Println(utils.IconFailure, err)
return err
}
// TODO: use katenary.yaml file here to set the labels
katenaryfile.OverrideWithConfig(project)
if !config.Force {
// check if the chart directory exists
// if yes, prevent the user from overwriting it and ask for confirmation
if _, err := os.Stat(config.OutputDir); err == nil {
overwrite := utils.Confirm(
"The chart directory "+config.OutputDir+" already exists, do you want to overwrite it?",
utils.IconWarning,
)
if !overwrite {
fmt.Println("Aborting")
return nil
}
}
fmt.Println() // clean line
}
// Build the objects !
chart, err := Generate(project)
if err != nil {
fmt.Println(err)
return err
}
// if the app version is set from the command line, use it
if config.AppVersion != nil {
chart.AppVersion = *config.AppVersion
}
chart.Version = config.ChartVersion
// remove the chart directory if it exists
os.RemoveAll(config.OutputDir)
// create the chart directory
if err := os.MkdirAll(templateDir, 0o755); err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
// 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([]byte(fmt.Sprintf("# 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([]byte(fmt.Sprintf("# compose files: %s\n", strings.Join(files, ", "))), yamlChart...)
// add generated date
yamlChart = append([]byte(fmt.Sprintf("# generated at: %s\n", time.Now().Format(time.RFC3339))), yamlChart...)
// document Chart.yaml file
yamlChart = addChartDoc(yamlChart, project)
writeContent(chartPath, yamlChart)
}
func buildNotesFile(project *types.Project, notesPath string) {
// get the list of services to write in the notes
services := make([]string, 0)
for _, service := range project.Services {
services = append(services, service.Name)
}
// write the notes to the disk
notes := extrafiles.NotesFile(services)
writeContent(notesPath, []byte(notes))
}
func buildValues(chart *HelmChart, project *types.Project, valuesPath string) {
values, err := utils.EncodeBasicYaml(&chart.Values)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
values = addDescriptions(values, *project)
values = addDependencyDescription(values, chart.Dependencies)
values = addCommentsToValues(values)
values = addStorageClassHelp(values)
values = addImagePullSecretsHelp(values)
values = addImagePullPolicyHelp(values)
values = addVariablesDoc(values, project)
values = addMainTagAppDoc(values, project)
values = addResourceHelp(values)
values = addTLSHelp(values)
values = addYAMLSelectorPath(values)
values = append([]byte(headerHelp), values...)
// add vim modeline
values = append(values, []byte("\n# vim: ft=yaml\n")...)
// write the values to the disk
writeContent(valuesPath, values)
}
func callHelmUpdate(config ConvertOptions) {
executeAndHandleError := func(fn func(ConvertOptions) error, config ConvertOptions, message string) {
if err := fn(config); err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
fmt.Println(utils.IconSuccess, message)
}
if config.HelmUpdate {
executeAndHandleError(helmUpdate, config, "Helm dependencies updated")
executeAndHandleError(helmLint, config, "Helm chart linted")
fmt.Println(utils.IconSuccess, "Helm chart created successfully")
}
}
func removeNewlinesInsideBrackets(values []byte) []byte {
re, err := regexp.Compile(`(?s)\{\{(.*?)\}\}`)
if err != nil {
log.Fatal(err)
}
return re.ReplaceAllFunc(values, func(b []byte) []byte {
// get the first match
matches := re.FindSubmatch(b)
replacement := bytes.ReplaceAll(matches[1], []byte("\n"), []byte(" "))
// remove repeated spaces
replacement = regexp.MustCompile(`\s+`).ReplaceAll(replacement, []byte(" "))
// remove newlines inside brackets
return bytes.ReplaceAll(b, matches[1], replacement)
})
}
func removeUnwantedLines(values []byte) []byte {
lines := strings.Split(string(values), "\n")
output := []string{}
for _, line := range lines {
next := false
for _, unwanted := range unwantedLines {
if strings.Contains(line, unwanted) {
next = true
}
}
if !next {
output = append(output, line)
}
}
return []byte(strings.Join(output, "\n"))
}
func writeContent(path string, content []byte) {
f, err := os.Create(path)
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
defer f.Close()
defer func() {
if _, err := f.Write(content); err != nil {
log.Fatal(err)
}
}()
}
// helmLint runs "helm lint" on the output directory.
func helmLint(config ConvertOptions) error {
fmt.Println(utils.IconInfo, "Linting...")
helm, err := exec.LookPath("helm")
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
cmd := exec.Command(helm, "lint", config.OutputDir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// helmUpdate runs "helm dependency update" on the output directory.
func helmUpdate(config ConvertOptions) error {
// lookup for "helm" binary
fmt.Println(utils.IconInfo, "Updating helm dependencies...")
helm, err := exec.LookPath("helm")
if err != nil {
fmt.Println(utils.IconFailure, err)
os.Exit(1)
}
// run "helm dependency update"
cmd := exec.Command(helm, "dependency", "update", config.OutputDir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// check if the project makes use of older labels (kanetary.[^v3])
func checkOldLabels(project *types.Project) error {
badServices := make([]string, 0)
for _, service := range project.Services {
for label := range service.Labels {
if strings.Contains(label, "katenary.") && !strings.Contains(label, labels.KatenaryLabelPrefix) {
badServices = append(badServices, fmt.Sprintf("- %s: %s", service.Name, label))
}
}
}
if len(badServices) > 0 {
message := fmt.Sprintf(` Old labels detected in project "%s".
The current version of katenary uses labels with the prefix "%s" which are not compatible with previous versions.
Your project is not compatible with this version.
Please upgrade your labels to follow the current version
Services to upgrade:
%s`,
project.Name,
labels.KatenaryLabelPrefix[0:len(labels.KatenaryLabelPrefix)-1],
strings.Join(badServices, "\n"),
)
return errors.New(utils.WordWrap(message, 80))
}
return nil
}

123
generator/cronJob.go Normal file
View File

@@ -0,0 +1,123 @@
package generator
import (
"katenary/generator/labels"
"katenary/generator/labels/labelStructs"
"katenary/utils"
"log"
"strings"
"github.com/compose-spec/compose-go/types"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// only used to check interface implementation
var (
_ Yaml = (*CronJob)(nil)
)
// CronJob is a kubernetes CronJob.
type CronJob struct {
*batchv1.CronJob
service *types.ServiceConfig
}
// NewCronJob creates a new CronJob from a compose service. The appName is the name of the application taken from the project name.
func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) (*CronJob, *RBAC) {
labels, ok := service.Labels[labels.LabelCronJob]
if !ok {
return nil, nil
}
mapping, err := labelStructs.CronJobFrom(labels)
if err != nil {
log.Fatalf("Error parsing cronjob labels: %s", err)
return nil, nil
}
if _, ok := chart.Values[service.Name]; !ok {
chart.Values[service.Name] = NewValue(service, false)
}
if chart.Values[service.Name].(*Value).CronJob == nil {
chart.Values[service.Name].(*Value).CronJob = &CronJobValue{}
}
chart.Values[service.Name].(*Value).CronJob.Schedule = mapping.Schedule
chart.Values[service.Name].(*Value).CronJob.ImagePullPolicy = "IfNotPresent"
chart.Values[service.Name].(*Value).CronJob.Environment = map[string]any{}
image, tag := mapping.Image, ""
if image == "" { // if image is not set, use the image from the service
image = service.Image
}
if strings.Contains(image, ":") {
image = strings.Split(service.Image, ":")[0]
tag = strings.Split(service.Image, ":")[1]
}
chart.Values[service.Name].(*Value).CronJob.Repository = &RepositoryValue{
Image: image,
Tag: tag,
}
cronjob := &CronJob{
CronJob: &batchv1.CronJob{
TypeMeta: metav1.TypeMeta{
Kind: "CronJob",
APIVersion: "batch/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: utils.TplName(service.Name, appName),
Labels: GetLabels(service.Name, appName),
Annotations: Annotations,
},
Spec: batchv1.CronJobSpec{
Schedule: "{{ .Values." + service.Name + ".cronjob.schedule }}",
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "cronjob",
Image: "{{ .Values." + service.Name + ".cronjob.repository.image }}:{{ default .Values." + service.Name + ".cronjob.repository.tag \"latest\" }}",
Command: []string{
"sh",
"-c",
mapping.Command,
},
},
},
},
},
},
},
},
},
service: &service,
}
var rbac *RBAC
if mapping.Rbac {
rbac = NewRBAC(service, appName)
// add the service account to the cronjob
cronjob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName = utils.TplName(service.Name, appName)
}
return cronjob, rbac
}
// Filename returns the filename of the cronjob.
//
// Implements the Yaml interface.
func (c *CronJob) Filename() string {
return c.service.Name + ".cronjob.yaml"
}
// Yaml returns the yaml representation of the cronjob.
//
// Implements the Yaml interface.
func (c *CronJob) Yaml() ([]byte, error) {
return ToK8SYaml(c)
}

115
generator/cronJob_test.go Normal file
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].Command, " ")
if combinedCommand != "sh -c echo hello" {
t.Errorf("Expected command to be sh -c echo hello, got %s", combinedCommand)
}
if cronJob.Spec.Schedule != "*/1 * * * *" {
t.Errorf("Expected schedule to be */1 * * * *, got %s", cronJob.Spec.Schedule)
}
// ensure that there are a deployment for the fedora Container
var err error
output, err = helmTemplate(ConvertOptions{
OutputDir: "./chart",
}, "-s", "templates/cron/deployment.yaml")
if err != nil {
t.Errorf("Error: %s", err)
}
deployment := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &deployment); err != nil {
t.Errorf(unmarshalError, err)
}
if deployment.Spec.Template.Spec.Containers[0].Image != "fedora:latest" {
t.Errorf("Expected image to be fedora, got %s", deployment.Spec.Template.Spec.Containers[0].Image)
}
}
func TestCronJobbWithRBAC(t *testing.T) {
composeFile := `
services:
cron:
image: fedora
labels:
katenary.v3/cronjob: |
image: alpine
command: echo hello
schedule: "*/1 * * * *"
rbac: true
`
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/cron/cronjob.yaml")
cronJob := batchv1.CronJob{}
if err := yaml.Unmarshal([]byte(output), &cronJob); err != nil {
t.Errorf(unmarshalError, err)
}
if cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName == "" {
t.Errorf("Expected ServiceAccountName to be set")
}
// find the service account file
output, err := helmTemplate(ConvertOptions{
OutputDir: "./chart",
}, "-s", "templates/cron/serviceaccount.yaml")
if err != nil {
t.Errorf("Error: %s", err)
}
serviceAccount := corev1.ServiceAccount{}
if err := yaml.Unmarshal([]byte(output), &serviceAccount); err != nil {
t.Errorf(unmarshalError, err)
}
if serviceAccount.Name == "" {
t.Errorf("Expected ServiceAccountName to be set")
}
// ensure that the serviceAccount is equal to the cronJob
if serviceAccount.Name != cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName {
t.Errorf("Expected ServiceAccountName to be %s, got %s", cronJob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName, serviceAccount.Name)
}
}

731
generator/deployment.go Normal file
View File

@@ -0,0 +1,731 @@
package generator
import (
"fmt"
"katenary/generator/labels"
"katenary/generator/labels/labelStructs"
"katenary/utils"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/compose-spec/compose-go/types"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ Yaml = (*Deployment)(nil)
type mountPathConfig struct {
mountPath string
subPath string
}
type ConfigMapMount struct {
configMap *ConfigMap
mountPath []mountPathConfig
}
// Deployment is a kubernetes Deployment.
type Deployment struct {
*appsv1.Deployment `yaml:",inline"`
chart *HelmChart `yaml:"-"`
configMaps map[string]*ConfigMapMount `yaml:"-"`
volumeMap map[string]string `yaml:"-"` // keep map of fixed named to original volume name
service *types.ServiceConfig `yaml:"-"`
defaultTag string `yaml:"-"`
isMainApp bool `yaml:"-"`
exchangesVolumes map[string]*labelStructs.ExchangeVolume `yaml:"-"`
boundEnvVar []string `yaml:"-"` // environement to remove
}
// NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name.
// It also creates the Values map that will be used to create the values.yaml file.
func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment {
isMainApp := false
if mainLabel, ok := service.Labels[labels.LabelMainApp]; ok {
main := strings.ToLower(mainLabel)
isMainApp = main == "true" || main == "yes" || main == "1"
}
defaultTag := `default "latest"`
if isMainApp {
defaultTag = `default .Chart.AppVersion`
}
chart.Values[service.Name] = NewValue(service, isMainApp)
appName := chart.Name
dep := &Deployment{
isMainApp: isMainApp,
defaultTag: defaultTag,
service: &service,
chart: chart,
Deployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: utils.TplName(service.Name, appName),
Labels: GetLabels(service.Name, appName),
Annotations: Annotations,
},
Spec: appsv1.DeploymentSpec{
Replicas: utils.Int32Ptr(1),
Selector: &metav1.LabelSelector{
MatchLabels: GetMatchLabels(service.Name, appName),
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: GetMatchLabels(service.Name, appName),
},
Spec: corev1.PodSpec{
NodeSelector: map[string]string{
labels.LabelName("node-selector"): "replace",
},
},
},
},
},
configMaps: make(map[string]*ConfigMapMount),
volumeMap: make(map[string]string),
exchangesVolumes: map[string]*labelStructs.ExchangeVolume{},
boundEnvVar: []string{},
}
// add containers
dep.AddContainer(service)
// add volumes
dep.AddVolumes(service, appName)
if service.Environment != nil {
dep.SetEnvFrom(service, appName)
}
return dep
}
// AddContainer adds a container to the deployment.
func (d *Deployment) AddContainer(service types.ServiceConfig) {
ports := []corev1.ContainerPort{}
for _, port := range service.Ports {
name := utils.GetServiceNameByPort(int(port.Target))
if name == "" {
utils.Warn("Port name not found for port ", port.Target, " in service ", service.Name, ". Using port number instead")
name = fmt.Sprintf("port-%d", port.Target)
}
ports = append(ports, corev1.ContainerPort{
ContainerPort: int32(port.Target),
Name: name,
})
}
container := corev1.Container{
Image: utils.TplValue(service.Name, "repository.image") + ":" +
utils.TplValue(service.Name, "repository.tag", d.defaultTag),
Ports: ports,
Name: service.ContainerName,
ImagePullPolicy: corev1.PullIfNotPresent,
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{},
},
}
if _, ok := d.chart.Values[service.Name]; !ok {
d.chart.Values[service.Name] = NewValue(service, d.isMainApp)
}
d.chart.Values[service.Name].(*Value).ImagePullPolicy = string(corev1.PullIfNotPresent)
// add an imagePullSecret, it actually does not work because the secret is not
// created but it add the reference in the YAML file. We'll change it in Yaml()
// method.
d.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{
Name: `{{ .Values.pullSecrets | toYaml | indent __indent__ }}`,
}}
// add ServiceAccount to the deployment
d.Spec.Template.Spec.ServiceAccountName = `{{ .Values.` + service.Name + `.serviceAccount | quote }}`
d.AddHealthCheck(service, &container)
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, container)
}
func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container) {
// get the label for healthcheck
if v, ok := service.Labels[labels.LabelHealthCheck]; ok {
probes, err := labelStructs.ProbeFrom(v)
if err != nil {
log.Fatal(err)
}
container.LivenessProbe = probes.LivenessProbe
container.ReadinessProbe = probes.ReadinessProbe
return
}
if service.HealthCheck != nil {
period := 30.0
if service.HealthCheck.Interval != nil {
period = time.Duration(*service.HealthCheck.Interval).Seconds()
}
container.LivenessProbe = &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
Exec: &corev1.ExecAction{
Command: service.HealthCheck.Test[1:],
},
},
PeriodSeconds: int32(period),
}
}
}
// AddIngress adds an ingress to the deployment. It creates the ingress object.
func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress {
return NewIngress(service, d.chart)
}
// AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment.
// If the volume is a bind volume it will warn the user that it is not supported yet.
func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) {
tobind := map[string]bool{}
if v, ok := service.Labels[labels.LabelConfigMapFiles]; ok {
binds, err := labelStructs.ConfigMapFileFrom(v)
if err != nil {
log.Fatal(err)
}
for _, bind := range binds {
tobind[bind] = true
}
}
isSamePod := false
if v, ok := service.Labels[labels.LabelSamePod]; !ok {
isSamePod = false
} else {
isSamePod = v != ""
}
for _, volume := range service.Volumes {
d.bindVolumes(volume, isSamePod, tobind, service, appName)
}
}
func (d *Deployment) AddLegacyVolume(name, kind string) {
// ensure the volume is not present
for _, v := range d.Spec.Template.Spec.Volumes {
if v.Name == name {
return
}
}
// init
if d.Spec.Template.Spec.Volumes == nil {
d.Spec.Template.Spec.Volumes = []corev1.Volume{}
}
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: name,
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
})
}
func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) {
// find the volume in the binded deployment
for _, bindedVolume := range binded.Spec.Template.Spec.Volumes {
skip := false
for _, targetVol := range d.Spec.Template.Spec.Volumes {
if targetVol.Name == bindedVolume.Name {
skip = true
break
}
}
if !skip {
// add the volume to the current deployment
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, bindedVolume)
// get the container
}
// add volume mount to the container
targetContainer, ti := utils.GetContainerByName(service.ContainerName, d.Spec.Template.Spec.Containers)
sourceContainer, _ := utils.GetContainerByName(service.ContainerName, binded.Spec.Template.Spec.Containers)
for _, bindedMount := range sourceContainer.VolumeMounts {
if bindedMount.Name == bindedVolume.Name {
targetContainer.VolumeMounts = append(targetContainer.VolumeMounts, bindedMount)
}
}
d.Spec.Template.Spec.Containers[ti] = *targetContainer
}
}
// DependsOn adds a initContainer to the deployment that will wait for the service to be up.
func (d *Deployment) DependsOn(to *Deployment, servicename string) error {
// Add a initContainer with busybox:latest using netcat to check if the service is up
// it will wait until the service responds to all ports
for _, container := range to.Spec.Template.Spec.Containers {
commands := []string{}
if len(container.Ports) == 0 {
utils.Warn("No ports found for service ",
servicename,
". You should declare a port in the service or use "+
labels.LabelPorts+
" label.",
)
os.Exit(1)
}
for _, port := range container.Ports {
command := fmt.Sprintf("until nc -z %s %d; do\n sleep 1;\ndone", to.Name, port.ContainerPort)
commands = append(commands, command)
}
command := []string{"/bin/sh", "-c", strings.Join(commands, "\n")}
d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, corev1.Container{
Name: "wait-for-" + to.service.Name,
Image: "busybox:latest",
Command: command,
})
}
return nil
}
// Filename returns the filename of the deployment.
func (d *Deployment) Filename() string {
return d.service.Name + ".deployment.yaml"
}
// SetEnvFrom sets the environment variables to a configmap. The configmap is created.
func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, samePod ...bool) {
if len(service.Environment) == 0 {
return
}
inSamePod := len(samePod) > 0 && samePod[0]
drop := []string{}
secrets := []string{}
defer func() {
c, index := d.BindMapFilesToContainer(service, secrets, appName)
if c == nil || index == -1 {
log.Println("Container not found for service ", service.Name)
return
}
d.Spec.Template.Spec.Containers[index] = *c
}()
// secrets from label
labelSecrets, err := labelStructs.SecretsFrom(service.Labels[labels.LabelSecrets])
if err != nil {
log.Fatal(err)
}
// values from label
varDescriptons := utils.GetValuesFromLabel(service, labels.LabelValues)
labelValues := []string{}
for v := range varDescriptons {
labelValues = append(labelValues, v)
}
for _, secret := range labelSecrets {
// get the secret name
_, ok := service.Environment[secret]
if !ok {
drop = append(drop, secret)
utils.Warn("Secret " + secret + " not found in service " + service.Name + " - skpped")
continue
}
secrets = append(secrets, secret)
}
if inSamePod {
return
}
// for each values from label "values", add it to Values map and change the envFrom
// value to {{ .Values.<service>.<value> }}
for _, value := range labelValues {
// get the environment variable name
val, ok := service.Environment[value]
if !ok {
drop = append(drop, value)
utils.Warn("Environment variable " + value + " not found in service " + service.Name + " - skpped")
continue
}
if d.chart.Values[service.Name].(*Value).Environment == nil {
d.chart.Values[service.Name].(*Value).Environment = make(map[string]any)
}
d.chart.Values[service.Name].(*Value).Environment[value] = *val
// set the environment variable to bind to the values.yaml file
v := utils.TplValue(service.Name, "environment."+value)
service.Environment[value] = &v
}
for _, value := range drop {
delete(service.Environment, value)
}
}
func (d *Deployment) BindMapFilesToContainer(service types.ServiceConfig, secrets []string, appName string) (*corev1.Container, int) {
fromSources := []corev1.EnvFromSource{}
envSize := len(service.Environment)
for _, secret := range secrets {
for k := range service.Environment {
if k == secret {
envSize--
}
}
}
if envSize > 0 {
if service.Name == "db" {
log.Println("Service ", service.Name, " has environment variables")
log.Println(service.Environment)
}
fromSources = append(fromSources, corev1.EnvFromSource{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: utils.TplName(service.Name, appName),
},
},
})
}
if len(secrets) > 0 {
fromSources = append(fromSources, corev1.EnvFromSource{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: utils.TplName(service.Name, appName),
},
},
})
}
container, index := utils.GetContainerByName(service.ContainerName, d.Spec.Template.Spec.Containers)
if container == nil {
utils.Warn("Container not found for service " + service.Name)
return nil, -1
}
container.EnvFrom = append(container.EnvFrom, fromSources...)
if container.Env == nil {
container.Env = []corev1.EnvVar{}
}
return container, index
}
func (d *Deployment) MountExchangeVolumes() {
for name, ex := range d.exchangesVolumes {
for i, c := range d.Spec.Template.Spec.Containers {
c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{
Name: "exchange-" + ex.Name,
MountPath: ex.MountPath,
})
if len(ex.Init) > 0 && name == c.Name {
d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, corev1.Container{
Command: []string{"/bin/sh", "-c", ex.Init},
Image: c.Image,
Name: "exhange-init-" + name,
VolumeMounts: []corev1.VolumeMount{{
Name: "exchange-" + ex.Name,
MountPath: ex.MountPath,
}},
})
}
d.Spec.Template.Spec.Containers[i] = c
}
}
}
// Yaml returns the yaml representation of the deployment.
func (d *Deployment) Yaml() ([]byte, error) {
var y []byte
var err error
serviceName := d.service.Name
if y, err = ToK8SYaml(d); err != nil {
return nil, err
}
// for each volume mount, add a condition "if values has persistence"
changing := false
content := strings.Split(string(y), "\n")
spaces := ""
volumeName := ""
nameDirective := "name: "
// this loop add condition for each volume mount
for line, volume := range content {
// find the volume name
for i := line; i < len(content); i++ {
if strings.Contains(content[i], nameDirective) {
volumeName = strings.TrimSpace(strings.Replace(content[i], "name: ", "", 1))
break
}
}
if volumeName == "" {
continue
}
if _, ok := d.configMaps[volumeName]; ok {
continue
}
if strings.Contains(volume, "mountPath: ") {
spaces = strings.Repeat(" ", utils.CountStartingSpaces(volume))
varName, ok := d.volumeMap[volumeName]
if !ok {
// this case happens when the volume is a "bind" volume comming from a "same-pod" service.
continue
}
varName = strings.ReplaceAll(varName, "-", "_")
content[line] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + varName + `.enabled }}` + "\n" + volume
changing = true
}
if strings.Contains(volume, nameDirective) && changing {
content[line] = volume + "\n" + spaces + "{{- end }}"
changing = false
}
}
changing = false
inVolumes := false
volumeName = ""
// this loop changes imagePullPolicy to {{ .Values.<service>.imagePullPolicy }}
// and the volume definition adding the condition "if values has persistence"
for i, line := range content {
if strings.Contains(line, "imagePullPolicy:") {
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
content[i] = spaces + "imagePullPolicy: {{ .Values." + serviceName + ".imagePullPolicy }}"
}
// find the volume name
for i := i; i < len(content); i++ {
if strings.Contains(content[i], "- name: ") {
volumeName = strings.TrimSpace(strings.Replace(content[i], "- name: ", "", 1))
break
}
}
if strings.Contains(line, "volumes:") {
inVolumes = true
}
if volumeName == "" {
continue
}
if _, ok := d.configMaps[volumeName]; ok {
continue
}
if strings.Contains(line, "- name: ") && inVolumes {
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
varName := d.volumeMap[volumeName]
varName = strings.ReplaceAll(varName, "-", "_")
content[i] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + varName + `.enabled }}` + "\n" + line
changing = true
}
if strings.Contains(line, "claimName: ") && changing {
content[i] = line + "\n" + spaces + "{{- end }}"
changing = false
}
}
// for impagePullSecrets, replace the name with the value from values.yaml
for i, line := range content {
if strings.Contains(line, "imagePullSecrets:") {
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
line = spaces + "{{- if .Values.pullSecrets }}"
line += "\n" + spaces + "imagePullSecrets:\n"
line += spaces + "{{- .Values.pullSecrets | toYaml | nindent __indent__ }}"
line += "\n" + spaces + "{{- end }}"
content[i] = line
}
}
// Find the replicas line and replace it with the value from values.yaml
for i, line := range content {
// manage nodeSelector
if strings.Contains(line, "nodeSelector:") {
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
pre := spaces + `{{- if .Values.` + serviceName + `.nodeSelector }}`
post := spaces + "{{- end }}"
ns := spaces + "nodeSelector:\n"
ns += spaces + ` {{- .Values.` + serviceName + `.nodeSelector | toYaml | nindent __indent__ }}`
line = pre + "\n" + ns + "\n" + post
}
// manage replicas
if strings.Contains(line, "replicas:") {
line = regexp.MustCompile("replicas: .*$").ReplaceAllString(line, "replicas: {{ .Values."+serviceName+".replicas }}")
}
// manage serviceAccount, add condition to use the serviceAccount from values.yaml
if strings.Contains(line, "serviceAccountName:") {
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
pre := spaces + `{{- if ne .Values.` + serviceName + `.serviceAccount "" }}`
post := spaces + "{{- end }}"
line = strings.ReplaceAll(line, "'", "")
line = pre + "\n" + line + "\n" + post
}
if strings.Contains(line, "resources: {}") {
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
pre := spaces + `{{- if .Values.` + serviceName + `.resources }}`
post := spaces + "{{- end }}"
line = strings.ReplaceAll(line, "resources: {}", "resources:")
line += "\n" + spaces + " {{ .Values." + serviceName + ".resources | toYaml | nindent __indent__ }}"
line = pre + "\n" + line + "\n" + post
}
content[i] = line
}
// find the katenary.v3/node-selector line, and remove it
for i, line := range content {
if strings.Contains(line, labels.LabelName("node-selector")) {
content = append(content[:i], content[i+1:]...)
continue
}
if strings.Contains(line, "- name: '{{ .Values.pullSecrets ") {
content = append(content[:i], content[i+1:]...)
continue
}
}
return []byte(strings.Join(content, "\n")), nil
}
func (d *Deployment) appendDirectoryToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) {
pathnme := utils.PathToName(volume.Source)
if _, ok := d.configMaps[pathnme]; !ok {
d.configMaps[pathnme] = &ConfigMapMount{
mountPath: []mountPathConfig{},
}
}
// TODO: make it recursive to add all files in the directory and subdirectories
_, err := os.ReadDir(volume.Source)
if err != nil {
log.Fatal(err)
}
cm := NewConfigMapFromDirectory(service, appName, volume.Source)
d.configMaps[pathnme] = &ConfigMapMount{
configMap: cm,
mountPath: append(d.configMaps[pathnme].mountPath, mountPathConfig{
mountPath: volume.Target,
}),
}
}
func (d *Deployment) appendFileToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) {
// In case of a file, add it to the configmap and use "subPath" to mount it
// Note that the volumes and volume mounts are not added to the deployment yet, they will be added later
// in generate.go
dirname := filepath.Dir(volume.Source)
pathname := utils.PathToName(dirname)
var cm *ConfigMap
if v, ok := d.configMaps[pathname]; !ok {
cm = NewConfigMap(*d.service, appName, true)
cm.usage = FileMapUsageFiles
cm.path = dirname
cm.Name = utils.TplName(service.Name, appName) + "-" + pathname
d.configMaps[pathname] = &ConfigMapMount{
configMap: cm,
mountPath: []mountPathConfig{{
mountPath: volume.Target,
subPath: filepath.Base(volume.Source),
}},
}
} else {
cm = v.configMap
mp := d.configMaps[pathname].mountPath
mp = append(mp, mountPathConfig{
mountPath: volume.Target,
subPath: filepath.Base(volume.Source),
})
d.configMaps[pathname].mountPath = mp
}
if err := cm.AppendFile(volume.Source); err != nil {
log.Fatal("Error adding file to configmap:", err)
}
}
func (d *Deployment) bindVolumes(volume types.ServiceVolumeConfig, isSamePod bool, tobind map[string]bool, service types.ServiceConfig, appName string) {
container, index := utils.GetContainerByName(service.ContainerName, d.Spec.Template.Spec.Containers)
defer func(d *Deployment, container *corev1.Container, index int) {
d.Spec.Template.Spec.Containers[index] = *container
}(d, container, index)
if _, found := tobind[volume.Source]; !isSamePod && volume.Type == "bind" && !found {
utils.Warn(
"Bind volumes are not supported yet, " +
"excepting for those declared as " +
labels.LabelConfigMapFiles +
", skipping volume " + volume.Source +
" from service " + service.Name,
)
return
}
if container == nil {
utils.Warn("Container not found for volume", volume.Source)
return
}
// ensure that the volume is not already present in the container
for _, vm := range container.VolumeMounts {
if vm.Name == volume.Source {
return
}
}
switch volume.Type {
case "volume":
// Add volume to container
fixedName := utils.FixedResourceName(volume.Source)
d.volumeMap[fixedName] = volume.Source
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: fixedName,
MountPath: volume.Target,
})
// Add volume to values.yaml only if it the service is not in the same pod that another service.
// If it is in the same pod, the volume will be added to the other service later
if _, ok := service.Labels[labels.LabelSamePod]; !ok {
d.chart.Values[service.Name].(*Value).AddPersistence(volume.Source)
}
// Add volume to deployment
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: fixedName,
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: utils.TplName(service.Name, appName, volume.Source),
},
},
})
case "bind":
// Add volume to container
stat, err := os.Stat(volume.Source)
if err != nil {
log.Fatal(err)
}
if stat.IsDir() {
d.appendDirectoryToConfigMap(service, appName, volume)
} else {
d.appendFileToConfigMap(service, appName, volume)
}
}
}

View File

@@ -0,0 +1,497 @@
package generator
import (
"fmt"
"katenary/generator/labels"
"os"
"strings"
"testing"
yaml3 "gopkg.in/yaml.v3"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
const webTemplateOutput = `templates/web/deployment.yaml`
func TestGenerate(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
`
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", webTemplateOutput)
// dt := DeploymentTest{}
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
if *dt.Spec.Replicas != 1 {
t.Errorf("Expected replicas to be 1, got %d", dt.Spec.Replicas)
t.Errorf("Output: %s", output)
}
if dt.Spec.Template.Spec.Containers[0].Image != "nginx:1.29" {
t.Errorf("Expected image to be nginx:1.29, got %s", dt.Spec.Template.Spec.Containers[0].Image)
}
}
func TestGenerateOneDeploymentWithSamePod(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
fpm:
image: php:fpm
ports:
- 9000:9000
labels:
katenary.v3/same-pod: web
`
outDir := "./chart"
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", webTemplateOutput)
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
if len(dt.Spec.Template.Spec.Containers) != 2 {
t.Errorf("Expected 2 containers, got %d", len(dt.Spec.Template.Spec.Containers))
}
// endsure that the fpm service is not created
var err error
_, err = helmTemplate(ConvertOptions{
OutputDir: outDir,
}, "-s", "templates/fpm/deployment.yaml")
if err == nil {
t.Errorf("Expected error, got nil")
}
// ensure that the web service is created and has got 2 ports
output, err = helmTemplate(ConvertOptions{
OutputDir: outDir,
}, "-s", "templates/web/service.yaml")
if err != nil {
t.Errorf("Error: %s", err)
}
service := corev1.Service{}
if err := yaml.Unmarshal([]byte(output), &service); err != nil {
t.Errorf(unmarshalError, err)
}
if len(service.Spec.Ports) != 2 {
t.Errorf("Expected 2 ports, got %d", len(service.Spec.Ports))
}
}
func TestDependsOn(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
depends_on:
- database
database:
image: mariadb:10.5
ports:
- 3306:3306
`
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", webTemplateOutput)
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
if len(dt.Spec.Template.Spec.Containers) != 1 {
t.Errorf("Expected 1 container, got %d", len(dt.Spec.Template.Spec.Containers))
}
// find an init container
if len(dt.Spec.Template.Spec.InitContainers) != 1 {
t.Errorf("Expected 1 init container, got %d", len(dt.Spec.Template.Spec.InitContainers))
}
}
func TestHelmDependencies(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
mariadb:
image: mariadb:10.5
ports:
- 3306:3306
labels:
%s/dependencies: |
- name: mariadb
repository: oci://registry-1.docker.io/bitnamicharts
version: 18.x.X
`
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", webTemplateOutput)
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
// ensure that there is no mariasb deployment
_, err := helmTemplate(ConvertOptions{
OutputDir: "./chart",
}, "-s", "templates/mariadb/deployment.yaml")
if err == nil {
t.Errorf("Expected error, got nil")
}
// check that Chart.yaml has the dependency
chart := HelmChart{}
chartFile := "./chart/Chart.yaml"
if _, err := os.Stat(chartFile); os.IsNotExist(err) {
t.Errorf("Chart.yaml does not exist")
}
chartContent, err := os.ReadFile(chartFile)
if err != nil {
t.Errorf("Error reading Chart.yaml: %s", err)
}
if err := yaml.Unmarshal(chartContent, &chart); err != nil {
t.Errorf(unmarshalError, err)
}
if len(chart.Dependencies) != 1 {
t.Errorf("Expected 1 dependency, got %d", len(chart.Dependencies))
}
}
func TestLivenessProbesFromHealthCheck(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 5s
timeout: 3s
retries: 3
`
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", webTemplateOutput)
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
if dt.Spec.Template.Spec.Containers[0].LivenessProbe == nil {
t.Errorf("Expected liveness probe to be set")
}
}
func TestProbesFromLabels(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
labels:
%s/health-check: |
livenessProbe:
httpGet:
path: /healthz
port: 80
readinessProbe:
httpGet:
path: /ready
port: 80
`
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", webTemplateOutput)
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
if dt.Spec.Template.Spec.Containers[0].LivenessProbe == nil {
t.Errorf("Expected liveness probe to be set")
}
if dt.Spec.Template.Spec.Containers[0].ReadinessProbe == nil {
t.Errorf("Expected readiness probe to be set")
}
t.Logf("LivenessProbe: %+v", dt.Spec.Template.Spec.Containers[0].LivenessProbe)
// ensure that the liveness probe is set to /healthz
if dt.Spec.Template.Spec.Containers[0].LivenessProbe.HTTPGet.Path != "/healthz" {
t.Errorf("Expected liveness probe path to be /healthz, got %s", dt.Spec.Template.Spec.Containers[0].LivenessProbe.HTTPGet.Path)
}
// ensure that the readiness probe is set to /ready
if dt.Spec.Template.Spec.Containers[0].ReadinessProbe.HTTPGet.Path != "/ready" {
t.Errorf("Expected readiness probe path to be /ready, got %s", dt.Spec.Template.Spec.Containers[0].ReadinessProbe.HTTPGet.Path)
}
}
func TestSetValues(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
environment:
FOO: bar
BAZ: qux
labels:
%s/values: |
- FOO
`
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", webTemplateOutput)
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
// readh the values.yaml, we must have FOO in web environment but not BAZ
valuesFile := "./chart/values.yaml"
if _, err := os.Stat(valuesFile); os.IsNotExist(err) {
t.Errorf("values.yaml does not exist")
}
valuesContent, err := os.ReadFile(valuesFile)
if err != nil {
t.Errorf("Error reading values.yaml: %s", err)
}
mapping := struct {
Web struct {
Environment map[string]string `yaml:"environment"`
} `yaml:"web"`
}{}
if err := yaml3.Unmarshal(valuesContent, &mapping); err != nil {
t.Errorf(unmarshalError, err)
}
if v, ok := mapping.Web.Environment["FOO"]; !ok {
t.Errorf("Expected FOO in web environment")
if v != "bar" {
t.Errorf("Expected FOO to be bar, got %s", v)
}
}
if v, ok := mapping.Web.Environment["BAZ"]; ok {
t.Errorf("Expected BAZ not in web environment")
if v != "qux" {
t.Errorf("Expected BAZ to be qux, got %s", v)
}
}
}
func TestWithUnderscoreInContainerName(t *testing.T) {
composeFile := `
services:
web-app:
image: nginx:1.29
container_name: web_app_container
environment:
FOO: BAR
labels:
%s/values: |
- FOO
`
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/web_app/deployment.yaml")
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
// find container.name
containerName := dt.Spec.Template.Spec.Containers[0].Name
if strings.Contains(containerName, "_") {
t.Errorf("Expected container name to not contain underscores, got %s", containerName)
}
}
func TestWithDashes(t *testing.T) {
composeFile := `
services:
web-app:
image: nginx:1.29
environment:
FOO: BAR
labels:
%s/values: |
- FOO
`
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/web_app/deployment.yaml")
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
valuesFile := "./chart/values.yaml"
if _, err := os.Stat(valuesFile); os.IsNotExist(err) {
t.Errorf("values.yaml does not exist")
}
valuesContent, err := os.ReadFile(valuesFile)
if err != nil {
t.Errorf("Error reading values.yaml: %s", err)
}
mapping := struct {
Web struct {
Environment map[string]string `yaml:"environment"`
} `yaml:"web_app"`
}{}
if err := yaml3.Unmarshal(valuesContent, &mapping); err != nil {
t.Errorf(unmarshalError, err)
}
// we must have FOO in web_app environment (not web-app)
// this validates that the service name is converted to a valid k8s name
if v, ok := mapping.Web.Environment["FOO"]; !ok {
t.Errorf("Expected FOO in web_app environment")
if v != "BAR" {
t.Errorf("Expected FOO to be BAR, got %s", v)
}
}
}
func TestDashesWithValueFrom(t *testing.T) {
composeFile := `
services:
web-app:
image: nginx:1.29
environment:
FOO: BAR
labels:
%[1]s/values: |
- FOO
web2:
image: nginx:1.29
labels:
%[1]s/values-from: |
BAR: web-app.FOO
`
composeFile = fmt.Sprintf(composeFile, labels.Prefix())
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(t, "-s", "templates/web2/deployment.yaml")
dt := v1.Deployment{}
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
t.Errorf(unmarshalError, err)
}
valuesFile := "./chart/values.yaml"
if _, err := os.Stat(valuesFile); os.IsNotExist(err) {
t.Errorf("values.yaml does not exist")
}
valuesContent, err := os.ReadFile(valuesFile)
if err != nil {
t.Errorf("Error reading values.yaml: %s", err)
}
mapping := struct {
Web struct {
Environment map[string]string `yaml:"environment"`
} `yaml:"web_app"`
}{}
if err := yaml3.Unmarshal(valuesContent, &mapping); err != nil {
t.Errorf(unmarshalError, err)
}
// we must have FOO in web_app environment (not web-app)
// this validates that the service name is converted to a valid k8s name
if v, ok := mapping.Web.Environment["FOO"]; !ok {
t.Errorf("Expected FOO in web_app environment")
if v != "BAR" {
t.Errorf("Expected FOO to be BAR, got %s", v)
}
}
// ensure that the deployment has the value from the other service
barenv := dt.Spec.Template.Spec.Containers[0].Env[0]
if barenv.Value != "" {
t.Errorf("Expected value to be empty")
}
if barenv.ValueFrom == nil {
t.Errorf("Expected valueFrom to be set")
}
}

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

432
generator/generator.go Normal file
View File

@@ -0,0 +1,432 @@
package generator
import (
"bytes"
"fmt"
"katenary/generator/labels"
"katenary/generator/labels/labelStructs"
"katenary/utils"
"log"
"regexp"
"strings"
"github.com/compose-spec/compose-go/types"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
// Generate a chart from a compose project.
// This does not write files to disk, it only creates the HelmChart object.
//
// The Generate function will create the HelmChart object this way:
//
// - Detect the service port name or leave the port number if not found.
// - Create a deployment for each service that are not ingnore.
// - Create a service and ingresses for each service that has ports and/or declared ingresses.
// - Create a PVC or Configmap volumes for each volume.
// - Create init containers for each service which has dependencies to other services.
// - Create a chart dependencies.
// - Create a configmap and secrets from the environment variables.
// - Merge the same-pod services.
func Generate(project *types.Project) (*HelmChart, error) {
var (
appName = project.Name
deployments = make(map[string]*Deployment, len(project.Services))
services = make(map[string]*Service)
podToMerge = make(map[string]*types.ServiceConfig)
)
chart := NewChart(appName)
// Add the compose files hash to the chart annotations
hash, err := utils.HashComposefiles(project.ComposeFiles)
if err != nil {
return nil, err
}
Annotations[labels.LabelName("compose-hash")] = hash
chart.composeHash = &hash
// drop all services with the "ignore" label
dropIngoredServices(project)
fixContainerNames(project)
// rename all services name to remove dashes
if err := fixResourceNames(project); err != nil {
return nil, err
}
// find the "main-app" label, and set chart.AppVersion to the tag if exists
mainCount := 0
for _, service := range project.Services {
if serviceIsMain(service) {
mainCount++
if mainCount > 1 {
return nil, fmt.Errorf("found more than one main app")
}
chart.setChartVersion(service)
}
}
if mainCount == 0 {
chart.AppVersion = "0.1.0"
}
// first pass, create all deployments whatewer they are.
for _, service := range project.Services {
err := chart.generateDeployment(service, deployments, services, podToMerge, appName)
if err != nil {
return nil, err
}
}
// now we have all deployments, we can create PVC if needed (it's separated from
// the above loop because we need all deployments to not duplicate PVC for "same-pod" services)
// bind static volumes
for _, service := range project.Services {
addStaticVolumes(deployments, service)
}
for _, service := range project.Services {
err := buildVolumes(service, chart, deployments)
if err != nil {
return nil, err
}
}
// if we have built exchange volumes, we need to moint them in each deployment
for _, d := range deployments {
d.MountExchangeVolumes()
}
// drop all "same-pod" deployments because the containers and volumes are already
// in the target deployment
for _, service := range podToMerge {
if samepod, ok := service.Labels[labels.LabelSamePod]; ok && samepod != "" {
// move this deployment volumes to the target deployment
if target, ok := deployments[samepod]; ok {
target.AddContainer(*service)
target.BindFrom(*service, deployments[service.Name])
target.SetEnvFrom(*service, appName, true)
// copy all init containers
initContainers := deployments[service.Name].Spec.Template.Spec.InitContainers
target.Spec.Template.Spec.InitContainers = append(target.Spec.Template.Spec.InitContainers, initContainers...)
delete(deployments, service.Name)
} else {
log.Printf("service %[1]s is declared as %[2]s, but %[2]s is not defined", service.Name, labels.LabelSamePod)
}
}
}
// create init containers for all DependsOn
for _, s := range project.Services {
for _, d := range s.GetDependencies() {
if dep, ok := deployments[d]; ok {
err := deployments[s.Name].DependsOn(dep, d)
if err != nil {
log.Printf("error creating init container for service %[1]s: %[2]s", s.Name, err)
}
} else {
log.Printf("service %[1]s depends on %[2]s, but %[2]s is not defined", s.Name, d)
}
}
}
// it's now time to get "value-from", before makeing the secrets and configmaps!
for _, s := range project.Services {
chart.setEnvironmentValuesFrom(s, deployments)
}
// generate configmaps with environment variables
if err := chart.generateConfigMapsAndSecrets(project); err != nil {
log.Fatalf("error generating configmaps and secrets: %s", err)
}
// if the env-from label is set, we need to add the env vars from the configmap
// to the environment of the service
for _, s := range project.Services {
chart.setSharedConf(s, deployments)
}
// remove all "boundEnv" from the values
for _, d := range deployments {
if len(d.boundEnvVar) == 0 {
continue
}
for _, e := range d.boundEnvVar {
delete(chart.Values[d.service.Name].(*Value).Environment, e)
}
}
// generate yaml files
for _, d := range deployments {
y, err := d.Yaml()
if err != nil {
return nil, err
}
chart.Templates[d.Filename()] = &ChartTemplate{
Content: y,
Servicename: d.service.Name,
}
}
// generate all services
for _, s := range services {
// add the service ports to the target service if it's a "same-pod" service
if samePod, ok := podToMerge[s.service.Name]; ok {
// get the target service
target := services[samePod.Name]
// merge the services
s.Spec.Ports = append(s.Spec.Ports, target.Spec.Ports...)
}
y, _ := s.Yaml()
chart.Templates[s.Filename()] = &ChartTemplate{
Content: y,
Servicename: s.service.Name,
}
}
// drop all "same-pod" services
for _, s := range podToMerge {
// get the target service
target := services[s.Name]
if target != nil {
delete(chart.Templates, target.Filename())
}
}
// compute all needed resplacements in YAML templates
for n, v := range chart.Templates {
v.Content = removeReplaceString(v.Content)
v.Content = computeNIndent(v.Content)
chart.Templates[n].Content = v.Content
}
// generate helper
chart.Helper = Helper(appName)
return chart, nil
}
// dropIngoredServices removes all services with the "ignore" label set to true (or yes).
func dropIngoredServices(project *types.Project) {
for i, service := range project.Services {
if isIgnored(service) {
project.Services = append(project.Services[:i], project.Services[i+1:]...)
}
}
}
// fixResourceNames renames all services and related resources to remove dashes.
func fixResourceNames(project *types.Project) error {
// rename all services name to remove dashes
for i, service := range project.Services {
if service.Name != utils.AsResourceName(service.Name) {
fixed := utils.AsResourceName(service.Name)
for j, s := range project.Services {
// for the same-pod services, we need to keep the original name
if samepod, ok := s.Labels[labels.LabelSamePod]; ok && samepod == service.Name {
s.Labels[labels.LabelSamePod] = fixed
project.Services[j] = s
}
// also, the value-from label should be updated
if valuefrom, ok := s.Labels[labels.LabelValueFrom]; ok {
vf, err := labelStructs.GetValueFrom(valuefrom)
if err != nil {
return err
}
for varname, bind := range *vf {
log.Printf("service %s, varname %s, bind %s", service.Name, varname, bind)
bind := strings.ReplaceAll(bind, service.Name, fixed)
(*vf)[varname] = bind
}
output, err := yaml.Marshal(vf)
if err != nil {
return err
}
s.Labels[labels.LabelValueFrom] = string(output)
}
}
service.Name = fixed
project.Services[i] = service
}
}
return nil
}
// serviceIsMain returns true if the service is the main app.
func serviceIsMain(service types.ServiceConfig) bool {
if main, ok := service.Labels[labels.LabelMainApp]; ok {
return main == "true" || main == "yes" || main == "1"
}
return false
}
func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceConfig) {
// add the bound configMaps files to the deployment containers
var d *Deployment
var ok bool
if d, ok = deployments[service.Name]; !ok {
log.Printf("service %s not found in deployments", service.Name)
return
}
container, index := utils.GetContainerByName(service.ContainerName, d.Spec.Template.Spec.Containers)
if container == nil { // may append for the same-pod services
return
}
for volumeName, config := range d.configMaps {
var y []byte
var err error
if y, err = config.configMap.Yaml(); err != nil {
log.Fatal(err)
}
// add the configmap to the chart
d.chart.Templates[config.configMap.Filename()] = &ChartTemplate{
Content: y,
Servicename: d.service.Name,
}
// add the moint path to the container
for _, m := range config.mountPath {
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: utils.PathToName(volumeName),
MountPath: m.mountPath,
SubPath: m.subPath,
})
}
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: utils.PathToName(volumeName),
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: config.configMap.Name,
},
},
},
})
}
d.Spec.Template.Spec.Containers[index] = *container
}
// computeNIndentm replace all __indent__ labels with the number of spaces before the label.
func computeNIndent(b []byte) []byte {
lines := bytes.Split(b, []byte("\n"))
for i, line := range lines {
if !bytes.Contains(line, []byte("__indent__")) {
continue
}
startSpaces := ""
spaces := regexp.MustCompile(`^\s+`).FindAllString(string(line), -1)
if len(spaces) > 0 {
startSpaces = spaces[0]
}
line = []byte(startSpaces + strings.TrimLeft(string(line), " "))
line = bytes.ReplaceAll(line, []byte("__indent__"), fmt.Appendf(nil, "%d", len(startSpaces)))
lines[i] = line
}
return bytes.Join(lines, []byte("\n"))
}
// removeReplaceString replace all __replace_ labels with the value of the
// capture group and remove all new lines and repeated spaces.
//
// we created:
//
// __replace_bar: '{{ include "foo.labels" .
// }}'
//
// note the new line and spaces...
//
// we now want to replace it with {{ include "foo.labels" . }}, without the label name.
func removeReplaceString(b []byte) []byte {
// replace all matches with the value of the capture group
// and remove all new lines and repeated spaces
b = replaceLabelRegexp.ReplaceAllFunc(b, func(b []byte) []byte {
inc := replaceLabelRegexp.FindSubmatch(b)[1]
inc = bytes.ReplaceAll(inc, []byte("\n"), []byte(""))
inc = bytes.ReplaceAll(inc, []byte("\r"), []byte(""))
inc = regexp.MustCompile(`\s+`).ReplaceAll(inc, []byte(" "))
return inc
})
return b
}
// buildVolumes creates the volumes for the service.
func buildVolumes(service types.ServiceConfig, chart *HelmChart, deployments map[string]*Deployment) error {
appName := chart.Name
for _, v := range service.Volumes {
// Do not add volumes if the pod is injected in a deployments
// via "same-pod" and the volume in destination deployment exists
if samePodVolume(service, v, deployments) {
continue
}
switch v.Type {
case "volume":
v.Source = utils.AsResourceName(v.Source)
pvc := NewVolumeClaim(service, v.Source, appName)
// if the service is integrated in another deployment, we need to add the volume
// to the target deployment
if override, ok := service.Labels[labels.LabelSamePod]; ok {
pvc.nameOverride = override
pvc.Spec.StorageClassName = utils.StrPtr(`{{ .Values.` + override + `.persistence.` + v.Source + `.storageClass }}`)
chart.Values[override].(*Value).AddPersistence(v.Source)
}
y, _ := pvc.Yaml()
chart.Templates[pvc.Filename()] = &ChartTemplate{
Content: y,
Servicename: service.Name,
}
}
}
return nil
}
// samePodVolume returns true if the volume is already in the target deployment.
func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, deployments map[string]*Deployment) bool {
// if the service has volumes, and it has "same-pod" label
// - get the target deployment
// - check if it has the same volume
// if not, return false
if v.Source == "" {
return false
}
if len(service.Volumes) == 0 {
return false
}
targetDeployment := ""
if targetName, ok := service.Labels[labels.LabelSamePod]; !ok {
return false
} else {
targetDeployment = targetName
}
// get the target deployment
target := findDeployment(targetDeployment, deployments)
if target == nil {
return false
}
// check if it has the same volume
for _, tv := range target.Spec.Template.Spec.Volumes {
if tv.Name == v.Source {
log.Printf("found same pod volume %s in deployment %s and %s", tv.Name, service.Name, targetDeployment)
return true
}
}
return false
}
func fixContainerNames(project *types.Project) {
// fix container names to be unique
for i, service := range project.Services {
if service.ContainerName == "" {
service.ContainerName = utils.FixedResourceName(service.Name)
} else {
service.ContainerName = utils.FixedResourceName(service.ContainerName)
}
project.Services[i] = service
}
}

19
generator/globals.go Normal file
View File

@@ -0,0 +1,19 @@
package generator
import (
"katenary/generator/labels"
"regexp"
)
var (
// find all labels starting by __replace_ and ending with ":"
// and get the value between the quotes
// ?s => multiline
// (?P<inc>.+?) => named capture group to "inc" variable (so we could use $inc in the replace)
replaceLabelRegexp = regexp.MustCompile(`(?s)__replace_.+?: '(?P<inc>.+?)'`)
// Standard annotationss
Annotations = map[string]string{
labels.LabelName("version"): Version,
}
)

36
generator/helmHelper.tpl Normal file
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 -}}

20
generator/helper.go Normal file
View File

@@ -0,0 +1,20 @@
package generator
import (
_ "embed"
"katenary/generator/labels"
"strings"
)
// helmHelper is a template for the _helpers.tpl file in the chart templates directory.
//
//go:embed helmHelper.tpl
var helmHelper string
// Helper returns the _helpers.tpl file for a chart.
func Helper(name string) string {
helmHelper := strings.ReplaceAll(helmHelper, "__APP__", name)
helmHelper = strings.ReplaceAll(helmHelper, "__PREFIX__", labels.KatenaryLabelPrefix)
helmHelper = strings.ReplaceAll(helmHelper, "__VERSION__", "0.1.0")
return helmHelper
}

203
generator/ingress.go Normal file
View File

@@ -0,0 +1,203 @@
package generator
import (
"katenary/generator/labels"
"katenary/generator/labels/labelStructs"
"katenary/utils"
"log"
"strings"
"github.com/compose-spec/compose-go/types"
networkv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ Yaml = (*Ingress)(nil)
type Ingress struct {
*networkv1.Ingress
service *types.ServiceConfig `yaml:"-"`
appName string `yaml:"-"`
}
// NewIngress creates a new Ingress from a compose service.
func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress {
appName := Chart.Name
if service.Labels == nil {
service.Labels = make(map[string]string)
}
var label string
var ok bool
if label, ok = service.Labels[labels.LabelIngress]; !ok {
return nil
}
mapping, err := labelStructs.IngressFrom(label)
if err != nil {
log.Fatalf("Failed to parse ingress label: %s\n", err)
}
if mapping.Hostname == "" {
mapping.Hostname = service.Name + ".tld"
}
// create the ingress
pathType := networkv1.PathTypeImplementationSpecific
// fix the service name, and create the full name from variable name
// which is injected in the YAML() method
serviceName := strings.ReplaceAll(service.Name, "_", "-")
fullName := `{{ $fullname }}-` + serviceName
// Add the ingress host to the values.yaml
if Chart.Values[service.Name] == nil {
Chart.Values[service.Name] = &Value{}
}
Chart.Values[service.Name].(*Value).Ingress = &IngressValue{
Enabled: mapping.Enabled,
Path: *mapping.Path,
Host: mapping.Hostname,
Class: *mapping.Class,
Annotations: mapping.Annotations,
TLS: TLS{Enabled: mapping.TLS.Enabled},
}
// ingressClassName := `{{ .Values.` + service.Name + `.ingress.class }}`
ingressClassName := utils.TplValue(service.Name, "ingress.class")
servicePortName := utils.GetServiceNameByPort(int(*mapping.Port))
ingressService := &networkv1.IngressServiceBackend{
Name: fullName,
Port: networkv1.ServiceBackendPort{},
}
if servicePortName != "" {
ingressService.Port.Name = servicePortName
} else {
ingressService.Port.Number = *mapping.Port
}
ing := &Ingress{
service: &service,
appName: appName,
Ingress: &networkv1.Ingress{
TypeMeta: metav1.TypeMeta{
Kind: "Ingress",
APIVersion: "networking.k8s.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: fullName,
Labels: GetLabels(serviceName, appName),
Annotations: Annotations,
},
Spec: networkv1.IngressSpec{
IngressClassName: &ingressClassName,
Rules: []networkv1.IngressRule{
{
Host: utils.TplValue(serviceName, "ingress.host"),
IngressRuleValue: networkv1.IngressRuleValue{
HTTP: &networkv1.HTTPIngressRuleValue{
Paths: []networkv1.HTTPIngressPath{
{
Path: utils.TplValue(serviceName, "ingress.path"),
PathType: &pathType,
Backend: networkv1.IngressBackend{
Service: ingressService,
},
},
},
},
},
},
},
TLS: []networkv1.IngressTLS{
{
Hosts: []string{
`{{ tpl .Values.` + serviceName + `.ingress.host . }}`,
},
SecretName: `{{ .Values.` + serviceName + `.ingress.tls.secretName | default $tlsname }}`,
},
},
},
},
}
return ing
}
func (ingress *Ingress) Filename() string {
return ingress.service.Name + ".ingress.yaml"
}
func (ingress *Ingress) Yaml() ([]byte, error) {
var ret []byte
var err error
if ret, err = ToK8SYaml(ingress); err != nil {
return nil, err
}
serviceName := ingress.service.Name
ret = UnWrapTPL(ret)
lines := strings.Split(string(ret), "\n")
// first pass, wrap the tls part with `{{- if .Values.serviceName.ingress.tlsEnabled -}}`
// and `{{- end -}}`
from, to, spaces := -1, -1, -1
for i, line := range lines {
if strings.Contains(line, "tls:") {
from = i
spaces = utils.CountStartingSpaces(line)
continue
}
if from > -1 {
if utils.CountStartingSpaces(line) >= spaces {
to = i
continue
}
}
}
if from > -1 && to > -1 {
lines[from] = strings.Repeat(" ", spaces) +
`{{- if .Values.` + serviceName + `.ingress.tls.enabled }}` +
"\n" +
lines[from]
lines[to] = strings.Repeat(" ", spaces) + `{{ end -}}`
}
out := []string{
`{{- if .Values.` + serviceName + `.ingress.enabled -}}`,
`{{- $fullname := include "` + ingress.appName + `.fullname" . -}}`,
`{{- $tlsname := printf "%s-%s-tls" $fullname "` + ingress.service.Name + `" -}}`,
}
for _, line := range lines {
if strings.Contains(line, "loadBalancer: ") {
continue
}
if strings.Contains(line, "labels:") {
// add annotations above labels from values.yaml
content := `` +
` {{- if .Values.` + serviceName + `.ingress.annotations -}}` + "\n" +
` {{- toYaml .Values.` + serviceName + `.ingress.annotations | nindent 4 }}` + "\n" +
` {{- end }}` + "\n" +
line
out = append(out, content)
} else if strings.Contains(line, "ingressClassName: ") {
content := utils.Wrap(
line,
`{{- if ne .Values.`+serviceName+`.ingress.class "-" }}`,
`{{- end }}`,
)
out = append(out, content)
} else {
out = append(out, line)
}
}
out = append(out, `{{- end -}}`)
ret = []byte(strings.Join(out, "\n"))
return ret, nil
}

128
generator/ingress_test.go Normal file
View File

@@ -0,0 +1,128 @@
package generator
import (
"fmt"
"katenary/generator/labels"
"os"
"testing"
v1 "k8s.io/api/networking/v1"
"sigs.k8s.io/yaml"
)
func TestSimpleIngress(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
- 443:443
labels:
%s/ingress: |-
hostname: my.test.tld
port: 80
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(
t,
"-s", "templates/web/ingress.yaml",
"--set", "web.ingress.enabled=true",
)
ingress := v1.Ingress{}
if err := yaml.Unmarshal([]byte(output), &ingress); err != nil {
t.Errorf(unmarshalError, err)
}
if len(ingress.Spec.Rules) != 1 {
t.Errorf("Expected 1 rule, got %d", len(ingress.Spec.Rules))
}
if ingress.Spec.Rules[0].Host != "my.test.tld" {
t.Errorf("Expected host to be my.test.tld, got %s", ingress.Spec.Rules[0].Host)
}
}
func TestTLS(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
- 443:443
labels:
%s/ingress: |-
hostname: my.test.tld
port: 80
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(
t,
"-s", "templates/web/ingress.yaml",
"--set", "web.ingress.enabled=true",
)
ingress := v1.Ingress{}
if err := yaml.Unmarshal([]byte(output), &ingress); err != nil {
t.Errorf(unmarshalError, err)
}
// find the tls section
tls := ingress.Spec.TLS
if len(tls) != 1 {
t.Errorf("Expected 1 tls section, got %d", len(tls))
}
}
func TestTLSName(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
- 443:443
labels:
%s/ingress: |-
hostname: my.test.tld
port: 80
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(
t,
"-s",
"templates/web/ingress.yaml",
"--set", "web.ingress.enabled=true",
"--set", "web.ingress.tls.secretName=mysecret",
)
ingress := v1.Ingress{}
if err := yaml.Unmarshal([]byte(output), &ingress); err != nil {
t.Errorf(unmarshalError, err)
}
// find the tls section
tls := ingress.Spec.TLS
if len(tls) != 1 {
t.Errorf("Expected 1 tls section, got %d", len(tls))
}
if tls[0].SecretName != "mysecret" {
t.Errorf("Expected secretName to be mysecret, got %s", tls[0].SecretName)
}
}

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,165 @@
package katenaryfile
import (
"bytes"
"encoding/json"
"fmt"
"katenary/generator/labels"
"katenary/generator/labels/labelStructs"
"katenary/utils"
"log"
"os"
"reflect"
"strings"
"github.com/compose-spec/compose-go/types"
"github.com/invopop/jsonschema"
"gopkg.in/yaml.v3"
)
var allowedKatenaryYamlFileNames = []string{"katenary.yaml", "katenary.yml"}
// StringOrMap is a struct that can be either a string or a map of strings.
// It's a helper struct to unmarshal the katenary.yaml file and produce the schema
type StringOrMap any
// Service is a struct that contains the service configuration for katenary
type Service struct {
MainApp *bool `json:"main-app,omitempty" jsonschema:"title=Is this service the main application"`
Values []StringOrMap `json:"values,omitempty" jsonschema:"description=Environment variables to be set in values.yaml with or without a description"`
Secrets *labelStructs.Secrets `json:"secrets,omitempty" jsonschema:"title=Secrets,description=Environment variables to be set as secrets"`
Ports *labelStructs.Ports `json:"ports,omitempty" jsonschema:"title=Ports,description=Ports to be exposed in services"`
Ingress *labelStructs.Ingress `json:"ingress,omitempty" jsonschema:"title=Ingress,description=Ingress configuration"`
HealthCheck *labelStructs.HealthCheck `json:"health-check,omitempty" jsonschema:"title=Health Check,description=Health check configuration that respects the kubernetes api"`
SamePod *string `json:"same-pod,omitempty" jsonschema:"title=Same Pod,description=Service that should be in the same pod"`
Description *string `json:"description,omitempty" jsonschema:"title=Description,description=Description of the service that will be injected in the values.yaml file"`
Ignore *bool `json:"ignore,omitempty" jsonschema:"title=Ignore,description=Ignore the service in the conversion"`
Dependencies []labelStructs.Dependency `json:"dependencies,omitempty" jsonschema:"title=Dependencies,description=Services that should be injected in the Chart.yaml file"`
ConfigMapFile *labelStructs.ConfigMapFile `json:"configmap-files,omitempty" jsonschema:"title=ConfigMap Files,description=Files that should be injected as ConfigMap"`
MapEnv *labelStructs.MapEnv `json:"map-env,omitempty" jsonschema:"title=Map Env,description=Map environment variables to another value"`
CronJob *labelStructs.CronJob `json:"cron-job,omitempty" jsonschema:"title=Cron Job,description=Cron Job configuration"`
EnvFrom *labelStructs.EnvFrom `json:"env-from,omitempty" jsonschema:"title=Env From,description=Inject environment variables from another service"`
ExchangeVolumes []*labelStructs.ExchangeVolume `json:"exchange-volumes,omitempty" jsonschema:"title=Exchange Volumes,description=Exchange volumes between services"`
ValuesFrom *labelStructs.ValueFrom `json:"values-from,omitempty" jsonschema:"title=Values From,description=Inject values from another service (secret or configmap environment variables)"`
}
// OverrideWithConfig overrides the project with the katenary.yaml file. It
// will set the labels of the services with the values from the katenary.yaml file.
// It work in memory, so it will not modify the original project.
func OverrideWithConfig(project *types.Project) {
var yamlFile string
var err error
for _, yamlFile = range allowedKatenaryYamlFileNames {
_, err = os.Stat(yamlFile)
if err == nil {
break
}
}
if err != nil {
// no katenary file found
return
}
fmt.Println(utils.IconInfo, "Using katenary file", yamlFile)
services := make(map[string]Service)
fp, err := os.Open(yamlFile)
if err != nil {
return
}
if err := yaml.NewDecoder(fp).Decode(&services); err != nil {
log.Fatal(err)
return
}
for i, p := range project.Services {
name := p.Name
if project.Services[i].Labels == nil {
project.Services[i].Labels = make(map[string]string)
}
mustGetLabelContent := func(o any, s *types.ServiceConfig, labelName string) {
err := getLabelContent(o, s, labelName)
if err != nil {
log.Fatal(err)
}
}
if s, ok := services[name]; ok {
mustGetLabelContent(s.MainApp, &project.Services[i], labels.LabelMainApp)
mustGetLabelContent(s.Values, &project.Services[i], labels.LabelValues)
mustGetLabelContent(s.Secrets, &project.Services[i], labels.LabelSecrets)
mustGetLabelContent(s.Ports, &project.Services[i], labels.LabelPorts)
mustGetLabelContent(s.Ingress, &project.Services[i], labels.LabelIngress)
mustGetLabelContent(s.HealthCheck, &project.Services[i], labels.LabelHealthCheck)
mustGetLabelContent(s.SamePod, &project.Services[i], labels.LabelSamePod)
mustGetLabelContent(s.Description, &project.Services[i], labels.LabelDescription)
mustGetLabelContent(s.Ignore, &project.Services[i], labels.LabelIgnore)
mustGetLabelContent(s.Dependencies, &project.Services[i], labels.LabelDependencies)
mustGetLabelContent(s.ConfigMapFile, &project.Services[i], labels.LabelConfigMapFiles)
mustGetLabelContent(s.MapEnv, &project.Services[i], labels.LabelMapEnv)
mustGetLabelContent(s.CronJob, &project.Services[i], labels.LabelCronJob)
mustGetLabelContent(s.EnvFrom, &project.Services[i], labels.LabelEnvFrom)
mustGetLabelContent(s.ExchangeVolumes, &project.Services[i], labels.LabelExchangeVolume)
mustGetLabelContent(s.ValuesFrom, &project.Services[i], labels.LabelValueFrom)
}
}
fmt.Println(utils.IconInfo, "Katenary file loaded successfully, the services are now configured.")
}
func getLabelContent(o any, service *types.ServiceConfig, labelName string) error {
if reflect.ValueOf(o).IsZero() {
return nil
}
c, err := yaml.Marshal(o)
if err != nil {
log.Println(err)
return err
}
val := strings.TrimSpace(string(c))
if labelName == labels.LabelIngress {
// special case, values must be set from some defaults
ing, err := labelStructs.IngressFrom(val)
if err != nil {
log.Fatal(err)
return err
}
c, err := yaml.Marshal(ing)
if err != nil {
return err
}
val = strings.TrimSpace(string(c))
}
service.Labels[labelName] = val
return nil
}
// GenerateSchema generates the schema for the katenary.yaml file.
func GenerateSchema() string {
s := jsonschema.Reflect(map[string]Service{})
// redefine the IntOrString type from k8s
s.Definitions["IntOrString"] = &jsonschema.Schema{
OneOf: []*jsonschema.Schema{
{Type: "integer"},
{Type: "string"},
},
}
// same for the StringOrMap type, that can be either a string or a map of string:string
s.Definitions["StringOrMap"] = &jsonschema.Schema{
OneOf: []*jsonschema.Schema{
{Type: "string"},
{Type: "object", AdditionalProperties: &jsonschema.Schema{Type: "string"}},
},
}
c, _ := s.MarshalJSON()
// indent the json
var out bytes.Buffer
err := json.Indent(&out, c, "", " ")
if err != nil {
return err.Error()
}
return out.String()
}

View File

@@ -0,0 +1,119 @@
package katenaryfile
import (
"katenary/generator/labels"
"log"
"os"
"path/filepath"
"testing"
"github.com/compose-spec/compose-go/cli"
)
func TestBuildSchema(t *testing.T) {
sh := GenerateSchema()
if len(sh) == 0 {
t.Errorf("Expected schema to be defined")
}
}
func TestOverrideProjectWithKatenaryFile(t *testing.T) {
composeContent := `
services:
webapp:
image: nginx:latest
`
katenaryfileContent := `
webapp:
ports:
- 80
`
// create /tmp/katenary-test-override directory, save the compose.yaml file
tmpDir, err := os.MkdirTemp("", "katenary-test-override")
if err != nil {
t.Fatalf("Failed to create temp directory: %s", err.Error())
}
composeFile := filepath.Join(tmpDir, "compose.yaml")
katenaryFile := filepath.Join(tmpDir, "katenary.yaml")
os.MkdirAll(tmpDir, 0755)
if err := os.WriteFile(composeFile, []byte(composeContent), 0644); err != nil {
t.Log(err)
}
if err := os.WriteFile(katenaryFile, []byte(katenaryfileContent), 0644); err != nil {
t.Log(err)
}
defer os.RemoveAll(tmpDir)
c, _ := os.ReadFile(composeFile)
log.Println(string(c))
// chand dir to this directory
os.Chdir(tmpDir)
options, _ := cli.NewProjectOptions(nil,
cli.WithWorkingDirectory(tmpDir),
cli.WithDefaultConfigPath,
)
project, err := cli.ProjectFromOptions(options)
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)
OverrideWithConfig(project)
w := project.Services[0].Labels
if v, ok := w[labels.LabelPorts]; !ok {
t.Fatal("Expected ports to be defined", v)
}
if v, ok := w[labels.LabelIngress]; !ok {
t.Fatal("Expected ingress to be defined", v)
}
}

34
generator/labels.go Normal file
View File

@@ -0,0 +1,34 @@
package generator
import (
"fmt"
"katenary/generator/labels"
)
var componentLabel = labels.LabelName("component")
// GetLabels returns the labels for a service. It uses the appName to replace the __replace__ in the labels.
// This is used to generate the labels in the templates.
func GetLabels(serviceName, appName string) map[string]string {
labels := map[string]string{
componentLabel: serviceName,
}
key := `{{- include "%s.labels" . | nindent __indent__ }}`
labels[`__replace_`+serviceName] = fmt.Sprintf(key, appName)
return labels
}
// GetMatchLabels returns the matchLabels for a service. It uses the appName to replace the __replace__ in the labels.
// This is used to generate the matchLabels in the templates.
func GetMatchLabels(serviceName, appName string) map[string]string {
labels := map[string]string{
componentLabel: serviceName,
}
key := `{{- include "%s.selectorLabels" . | nindent __indent__ }}`
labels[`__replace_`+serviceName] = fmt.Sprintf(key, appName)
return labels
}

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

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