From e13653fba1820afcf2a5375ed910f57ce440d72d Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 26 Nov 2024 16:47:05 +0100 Subject: [PATCH 1/7] doc(readme): fix the command line help output --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f0cb941..5f13eb1 100644 --- a/README.md +++ b/README.md @@ -108,22 +108,23 @@ Katenary is a tool to convert compose files to Helm Charts. Each [command] and subcommand has got an "help" and "--help" flag to show more information. Usage: -katenary [command] + katenary [command] Examples: -katenary convert -c docker-compose.yml -o ./charts + katenary convert -c docker-compose.yml -o ./charts Available Commands: -completion Generates completion scripts -convert Converts a docker-compose file to a Helm Chart -hash-composefiles Print the hash of the composefiles -help Help about any command -help-labels Print the labels help for all or a specific label -version Print the version number of Katenary + completion Generates completion scripts + convert Converts a docker-compose file to a Helm Chart + hash-composefiles Print the hash of the composefiles + help Help about any command + help-labels Print the labels help for all or a specific label + schema Print the schema of the katenary file + version Print the version number of Katenary Flags: --h, --help help for katenary --v, --version version for katenary + -h, --help help for katenary + -v, --version version for katenary Use "katenary [command] --help" for more information about a command. ``` From 441be3072089af0e4084cbfa16f1187fc9676938 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 26 Nov 2024 16:47:37 +0100 Subject: [PATCH 2/7] chore(version): Get the version following how katenary is installed --- Makefile | 3 ++- cmd/katenary/main.go | 4 ++-- generator/version.go | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 861dffe..0cae1a5 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,8 @@ PREFIX=~/.local GOVERSION=1.23 GO=container OUT=katenary -BLD_CMD=go build -ldflags="-X 'katenary/generator.Version=$(VERSION)'" -o $(OUT) ./cmd/katenary +RELEASE="" +BLD_CMD=go build -ldflags="-X 'katenary/generator.Version=$(RELEASE)$(VERSION)'" -o $(OUT) ./cmd/katenary GOOS=linux GOARCH=amd64 SIGNER=metal3d@gmail.com diff --git a/cmd/katenary/main.go b/cmd/katenary/main.go index f6da974..bcdf2ba 100644 --- a/cmd/katenary/main.go +++ b/cmd/katenary/main.go @@ -35,7 +35,7 @@ func buildRootCmd() *cobra.Command { } rootCmd.Example = ` katenary convert -c docker-compose.yml -o ./charts` - rootCmd.Version = generator.Version + rootCmd.Version = generator.GetVersion() rootCmd.CompletionOptions.DisableDescriptions = false rootCmd.CompletionOptions.DisableNoDescFlag = false @@ -233,7 +233,7 @@ func generateVersionCommand() *cobra.Command { Use: "version", Short: "Print the version number of Katenary", Run: func(cmd *cobra.Command, args []string) { - println(generator.Version) + println(generator.GetVersion()) }, } } diff --git a/generator/version.go b/generator/version.go index 9602118..b528773 100644 --- a/generator/version.go +++ b/generator/version.go @@ -1,4 +1,24 @@ package generator +import ( + "runtime/debug" + "strings" +) + // Version is the version of katenary. It is set at compile time. var Version = "master" // changed at compile time + +// 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. +func GetVersion() string { + if strings.HasPrefix(Version, "release-") { + return Version + } + // get the version from the build info + v, ok := debug.ReadBuildInfo() + if ok { + return v.Main.Version + "-" + v.GoVersion + } + return Version +} From 9766dac7635afbe84f43370b4f1459fe9be7db2f Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 26 Nov 2024 17:02:25 +0100 Subject: [PATCH 3/7] yaml(schema): Propose a schema to use with LSP This schema works, at least, with yaml-lsp (yamlls) in NeoVim. --- katenary.json | 405 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 katenary.json diff --git a/katenary.json b/katenary.json new file mode 100644 index 0000000..bf80b10 --- /dev/null +++ b/katenary.json @@ -0,0 +1,405 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "ConfigMapFile": { + "items": { + "type": "string" + }, + "type": "array" + }, + "CronJob": { + "properties": { + "image": { + "type": "string" + }, + "command": { + "type": "string" + }, + "schedule": { + "type": "string" + }, + "rbac": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Dependency": { + "properties": { + "values": { + "type": "object" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "alias": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "version", + "repository" + ] + }, + "EnvFrom": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ExchangeVolume": { + "properties": { + "name": { + "type": "string" + }, + "mountPath": { + "type": "string" + }, + "type": { + "type": "string" + }, + "init": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "mountPath" + ] + }, + "ExecAction": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "GRPCAction": { + "properties": { + "port": { + "type": "integer" + }, + "service": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "port", + "service" + ] + }, + "HTTPGetAction": { + "properties": { + "path": { + "type": "string" + }, + "port": { + "$ref": "#/$defs/IntOrString" + }, + "host": { + "type": "string" + }, + "scheme": { + "type": "string" + }, + "httpHeaders": { + "items": { + "$ref": "#/$defs/HTTPHeader" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "port" + ] + }, + "HTTPHeader": { + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "value" + ] + }, + "HealthCheck": { + "properties": { + "livenessProbe": { + "$ref": "#/$defs/Probe" + }, + "readinessProbe": { + "$ref": "#/$defs/Probe" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Ingress": { + "properties": { + "port": { + "type": "integer" + }, + "annotations": { + "oneOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ] + }, + "hostname": { + "type": "string" + }, + "path": { + "type": "string" + }, + "class": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "tls": { + "$ref": "#/$defs/TLS" + } + }, + "additionalProperties": false, + "type": "object" + }, + "IntOrString": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "MapEnv": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "Ports": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "Probe": { + "properties": { + "exec": { + "$ref": "#/$defs/ExecAction" + }, + "httpGet": { + "$ref": "#/$defs/HTTPGetAction" + }, + "tcpSocket": { + "$ref": "#/$defs/TCPSocketAction" + }, + "grpc": { + "$ref": "#/$defs/GRPCAction" + }, + "initialDelaySeconds": { + "type": "integer" + }, + "timeoutSeconds": { + "type": "integer" + }, + "periodSeconds": { + "type": "integer" + }, + "successThreshold": { + "type": "integer" + }, + "failureThreshold": { + "type": "integer" + }, + "terminationGracePeriodSeconds": { + "type": "integer" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Secrets": { + "items": { + "type": "string" + }, + "type": "array" + }, + "Service": { + "properties": { + "main-app": { + "type": "boolean", + "title": "Is this service the main application" + }, + "values": { + "items": true, + "type": "array", + "description": "Environment variables to be set in values.yaml with or without a description" + }, + "secrets": { + "$ref": "#/$defs/Secrets", + "title": "Secrets", + "description": "Environment variables to be set as secrets" + }, + "ports": { + "$ref": "#/$defs/Ports", + "title": "Ports", + "description": "Ports to be exposed in services" + }, + "ingress": { + "$ref": "#/$defs/Ingress", + "title": "Ingress", + "description": "Ingress configuration" + }, + "health-check": { + "$ref": "#/$defs/HealthCheck", + "title": "Health Check", + "description": "Health check configuration that respects the kubernetes api" + }, + "same-pod": { + "type": "string", + "title": "Same Pod", + "description": "Service that should be in the same pod" + }, + "description": { + "type": "string", + "title": "Description", + "description": "Description of the service that will be injected in the values.yaml file" + }, + "ignore": { + "type": "boolean", + "title": "Ignore", + "description": "Ignore the service in the conversion" + }, + "dependencies": { + "items": { + "$ref": "#/$defs/Dependency" + }, + "type": "array", + "title": "Dependencies", + "description": "Services that should be injected in the Chart.yaml file" + }, + "configmap-files": { + "$ref": "#/$defs/ConfigMapFile", + "title": "ConfigMap Files", + "description": "Files that should be injected as ConfigMap" + }, + "map-env": { + "$ref": "#/$defs/MapEnv", + "title": "Map Env", + "description": "Map environment variables to another value" + }, + "cron-job": { + "$ref": "#/$defs/CronJob", + "title": "Cron Job", + "description": "Cron Job configuration" + }, + "env-from": { + "$ref": "#/$defs/EnvFrom", + "title": "Env From", + "description": "Inject environment variables from another service" + }, + "exchange-volumes": { + "items": { + "$ref": "#/$defs/ExchangeVolume" + }, + "type": "array", + "title": "Exchange Volumes", + "description": "Exchange volumes between services" + }, + "values-from": { + "$ref": "#/$defs/ValueFrom", + "title": "Values From", + "description": "Inject values from another service (secret or configmap environment variables)" + } + }, + "additionalProperties": false, + "type": "object" + }, + "StringOrMap": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + ] + }, + "TCPSocketAction": { + "properties": { + "port": { + "$ref": "#/$defs/IntOrString" + }, + "host": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "port" + ] + }, + "TLS": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object" + }, + "ValueFrom": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "additionalProperties": { + "$ref": "#/$defs/Service" + }, + "type": "object" +} From b63d8e421030fc82bfba07c2dd6814603c524105 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 26 Nov 2024 17:10:54 +0100 Subject: [PATCH 4/7] doc(readme): Explain the use of the yaml/json schema And set heading level to 2 --- README.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5f13eb1..c3e0627 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ 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 ? +## What ? Katenary is a tool to help to transform `docker-compose` files to a working Helm Chart for Kubernetes. @@ -31,7 +31,7 @@ share it with the community. The main developer is [Patrice FERLET](https://github.com/metal3d). -# Install +## 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. @@ -45,7 +45,7 @@ You can use this commands on Linux: sh <(curl -sSL https://raw.githubusercontent.com/metal3d/katenary/master/install.sh) ``` -# Or, build yourself +## Or, build yourself If you've got `podman` or `docker`, you can build `katenary` by using: @@ -77,7 +77,7 @@ 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. @@ -100,7 +100,7 @@ katenary completion fish | source # powershell (as we don't provide any support on Windows yet, please avoid this...) ``` -# Usage +## Usage ```text Katenary is a tool to convert compose files to Helm Charts. @@ -184,7 +184,7 @@ services: - MARIADB_PASSWORD ``` -# Labels +## Labels These labels could be found by `katenary help-labels`, and can be placed as labels inside your docker-compose file: @@ -197,6 +197,7 @@ 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. @@ -206,9 +207,76 @@ 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, create a Lua file in your plugins directory: + +```lua +-- yaml.lua + +return { + { + "neovim/nvim-lspconfig", + opts = { + servers = { + yamlls = { + settings = { + yaml = { + schemas = { + ["https://raw.githubusercontent.com/metal3d/katenary/refs/heads/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/refs/heads/master/katenary.json": "katenary.yaml" + } +} +``` + +You can, of course, replace the `refs/heads/master` with a specific tag or branch. + +## What a name… Katenary is the stylized name of the project that comes from the "catenary" word. From 921eaff367c3857ac75c8b7dd07dfc5777838c9c Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 26 Nov 2024 17:45:42 +0100 Subject: [PATCH 5/7] chore(output): Version should be printed in stdout It seems that "println" failed to write on stdout --- cmd/katenary/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/katenary/main.go b/cmd/katenary/main.go index bcdf2ba..dc0c9ca 100644 --- a/cmd/katenary/main.go +++ b/cmd/katenary/main.go @@ -233,7 +233,7 @@ func generateVersionCommand() *cobra.Command { Use: "version", Short: "Print the version number of Katenary", Run: func(cmd *cobra.Command, args []string) { - println(generator.GetVersion()) + fmt.Println(generator.GetVersion()) }, } } From a80ddcc0544cd7a16f7d42fa32b2a097d873f5cb Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 26 Nov 2024 17:45:57 +0100 Subject: [PATCH 6/7] test(main): More tests in main command package --- cmd/katenary/main_test.go | 55 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/cmd/katenary/main_test.go b/cmd/katenary/main_test.go index eb530ac..679fee7 100644 --- a/cmd/katenary/main_test.go +++ b/cmd/katenary/main_test.go @@ -1,6 +1,13 @@ package main -import "testing" +import ( + "bytes" + "encoding/json" + "io" + "os" + "strings" + "testing" +) func TestBuildCommand(t *testing.T) { rootCmd := buildRootCmd() @@ -15,3 +22,49 @@ func TestBuildCommand(t *testing.T) { 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) + } +} From 9f1f6c7e78d8a14842928d589acbb4ddedf0e84e Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 26 Nov 2024 17:53:43 +0100 Subject: [PATCH 7/7] test(version): cover the version function --- generator/version_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 generator/version_test.go diff --git a/generator/version_test.go b/generator/version_test.go new file mode 100644 index 0000000..44e45c5 --- /dev/null +++ b/generator/version_test.go @@ -0,0 +1,21 @@ +package generator + +import ( + "strings" + "testing" +) + +func TestVersion(t *testing.T) { + // we build on "devel" branch + v := GetVersion() + if strings.Contains(v, "(devel)") { + t.Errorf("Expected version to be set, got %s", v) + } + + // now, imagine we are on a release branch + Version = "release-1.0.0" + v = GetVersion() + if !strings.Contains(v, "release-1.0.0") { + t.Errorf("Expected version to be set, got %s", v) + } +}