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/README.md b/README.md index da52242..0eb5a85 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,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. @@ -33,7 +33,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. @@ -47,7 +47,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: @@ -79,7 +79,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. @@ -102,7 +102,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. @@ -110,22 +110,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. ``` @@ -185,7 +186,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: @@ -198,6 +199,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. @@ -207,9 +209,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. diff --git a/cmd/katenary/main.go b/cmd/katenary/main.go index f6da974..dc0c9ca 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) + fmt.Println(generator.GetVersion()) }, } } 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) + } +} 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 +} 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) + } +} 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" +}