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
This commit is contained in:
2024-10-17 17:08:42 +02:00
parent 78dfb15cf5
commit 918f1b845b
22 changed files with 991 additions and 893 deletions

4
.gitignore vendored
View File

@@ -18,3 +18,7 @@ configs/
cover* cover*
.sq .sq
./katenary ./katenary
.aider*
.python_history
.bash_history
katenary

View File

@@ -23,6 +23,7 @@ BINARIES=dist/katenary-linux-amd64 dist/katenary-linux-arm64 dist/katenary.exe d
ASC_BINARIES=$(patsubst %,%.asc,$(BINARIES)) ASC_BINARIES=$(patsubst %,%.asc,$(BINARIES))
# defaults # defaults
BROWSER=$(shell command -v epiphany || echo xdg-open)
SHELL := bash SHELL := bash
# strict mode # strict mode
.SHELLFLAGS := -eu -o pipefail -c .SHELLFLAGS := -eu -o pipefail -c
@@ -35,6 +36,7 @@ MAKEFLAGS += --no-builtin-rules
all: build all: build
help: help:
@cat <<EOF | fold -s -w 80 @cat <<EOF | fold -s -w 80
=== HELP === === HELP ===
@@ -166,7 +168,14 @@ serve-doc: __label_doc
tests: test tests: test
test: test:
@echo -e "\033[1;33mTesting katenary $(VERSION)...\033[0m" @echo -e "\033[1;33mTesting katenary $(VERSION)...\033[0m"
go test -v ./... go test -coverprofile=cover.out ./...
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
push-release: build-all push-release: build-all
@rm -f release.id @rm -f release.id

View File

@@ -8,10 +8,10 @@
🚀 Unleash Productivity with Katenary! 🚀 🚀 Unleash Productivity with Katenary! 🚀
Tired of manual conversions? Katenary harnesses the labels from your "compose" file to craft complete Helm Charts Tired of manual conversions? Katenary harnesses the labels from your "`compose`" file to craft complete Helm Charts
effortlessly, saving you time and energy. effortlessly, saving you time and energy.
🛠️ Simple autmated CLI: Katenary handles the grunt work, generating everything needed for seamless service binding 🛠️ Simple automated CLI: Katenary handles the grunt work, generating everything needed for seamless service binding
and Helm Chart creation. 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. 💡 Effortless Efficiency: You only need to add labels when it's necessary to precise things. Then call `katenary convert` and let the magic happen.
@@ -24,9 +24,8 @@ Katenary is a tool to help to transform `docker-compose` files to a working Helm
> doesn't propose as many features as what can do Kubernetes. So, we strongly recommend to use Katenary as a "bootstrap" > 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. > tool and then to manually enhance the generated helm chart.
Today, it's partially developed in collaboration with [Klee Group](https://www.kleegroup.com). Note that Katenary is
Today, it's partially developped 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
and **will stay an opensource and free (as freedom) project**. We are convinced that the best way to make it better is to
share it with the community. share it with the community.
The main developer is [Patrice FERLET](https://github.com/metal3d). The main developer is [Patrice FERLET](https://github.com/metal3d).
@@ -45,7 +44,7 @@ You can use this commands on Linux:
sh <(curl -sSL https://raw.githubusercontent.com/metal3d/katenary/master/install.sh) 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: If you've got `podman` or `docker`, you can build `katenary` by using:
@@ -54,6 +53,7 @@ make build
``` ```
You can then install it with: You can then install it with:
```bash ```bash
make install make install
``` ```
@@ -76,13 +76,12 @@ make build GO=local GOOS=linux GOARCH=arm64
Then place the `katenary` binary file inside your PATH. Then place the `katenary` binary file inside your PATH.
# Tips # Tips
We strongly recommand to add the "completion" call to you SHELL using the common bashrc, or whatever the profile file We strongly recommend adding the completion call to you SHELL using the common `bashrc`, or whatever the profile file
you use. you use.
E.g.: E.g.,
```bash ```bash
# bash in ~/.bashrc file # bash in ~/.bashrc file
@@ -102,7 +101,7 @@ katenary completion fish | source
# Usage # Usage
``` ```text
Katenary is a tool to convert compose files to Helm Charts. 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. Each [command] and subcommand has got an "help" and "--help" flag to show more information.
@@ -134,22 +133,11 @@ Use "katenary [command] --help" for more information about a command.
It creates a subdirectory inside `chart` that is named with the `appname` option (default is `MyApp`) 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 > 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 > 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. > Katenary to build a correct helm chart.
What can be interpreted by Katenary: Example of a possible `docker-compose.yaml` file:
- 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 on service (using the first port)
- `env_file` list will create a configMap object per environemnt file (⚠ to-do: the "to-service" label doesn't work with
configMap for now)
- some labels can help to bind values, see examples below
Exemple of a possible `docker-compose.yaml` file:
```yaml ```yaml
services: services:
@@ -196,9 +184,9 @@ services:
# Labels # Labels
These labels could be found by `katenary help-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:
``` ```text
To get more information about a label, use `katenary help-label <name_without_prefix> To get more information about a label, use `katenary help-label <name_without_prefix>
e.g. katenary help-label dependencies e.g. katenary help-label dependencies
@@ -218,11 +206,11 @@ 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: list of string or map Environment variables to be added to the values.yaml
``` ```
# What a name... # What a name
Katenary is the stylized name of the project that comes from the "catenary" word. 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 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. 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 "streched link from docker-compose to helm chart". This curved link represents what we try to do, the project is a stretched link from docker-compose to helm chart.

View File

@@ -6,14 +6,13 @@ package main
import ( import (
"fmt" "fmt"
"katenary/generator"
"katenary/utils"
"os" "os"
"strings" "strings"
"github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"katenary/generator"
"katenary/utils"
) )
const longHelp = `Katenary is a tool to convert compose files to Helm Charts. const longHelp = `Katenary is a tool to convert compose files to Helm Charts.
@@ -133,6 +132,8 @@ func generateConvertCommand() *cobra.Command {
var appVersion *string var appVersion *string
givenAppVersion := "" givenAppVersion := ""
chartVersion := "0.1.0" chartVersion := "0.1.0"
icon := ""
envFiles := []string{}
convertCmd := &cobra.Command{ convertCmd := &cobra.Command{
Use: "convert", Use: "convert",
@@ -148,17 +149,79 @@ func generateConvertCommand() *cobra.Command {
HelmUpdate: helmdepUpdate, HelmUpdate: helmdepUpdate,
AppVersion: appVersion, AppVersion: appVersion,
ChartVersion: chartVersion, ChartVersion: chartVersion,
Icon: icon,
EnvFiles: envFiles,
}, dockerComposeFile...) }, dockerComposeFile...)
}, },
} }
convertCmd.Flags().BoolVarP(&force, "force", "f", force, "Force the overwrite of the chart directory") convertCmd.Flags().BoolVarP(
convertCmd.Flags().BoolVarP(&helmdepUpdate, "helm-update", "u", helmdepUpdate, "Update helm dependencies if helm is installed") &force,
convertCmd.Flags().StringSliceVarP(&profiles, "profile", "p", profiles, "Specify the profiles to use") "force",
convertCmd.Flags().StringVarP(&outputDir, "output-dir", "o", outputDir, "Specify the output directory") "f",
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.\nNote that overides files are also used whatever the files you specify here.\nThe overides files are:\n"+strings.Join(cli.DefaultOverrideFileNames, ", \n")+"\n") force,
convertCmd.Flags().StringVarP(&givenAppVersion, "app-version", "a", "", "Specify the app version (in Chart.yaml)") "Force the overwrite of the chart directory",
convertCmd.Flags().StringVarP(&chartVersion, "chart-version", "v", chartVersion, "Specify the chart version (in Chart.yaml)") )
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 return convertCmd
} }

View File

@@ -2,27 +2,16 @@ package generator
import ( import (
"fmt" "fmt"
"katenary/generator/labelStructs"
"katenary/utils"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"katenary/generator/labelStructs"
"katenary/utils"
) )
// ConvertOptions are the options to convert a compose project to a helm chart.
type ConvertOptions struct {
AppVersion *string
OutputDir string
ChartVersion string
Profiles []string
Force bool
HelmUpdate bool
}
// ChartTemplate is a template of a chart. It contains the content of the template and the name of the service. // 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. // This is used internally to generate the templates.
type ChartTemplate struct { type ChartTemplate struct {
@@ -30,6 +19,18 @@ type ChartTemplate struct {
Content []byte 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
Force bool
HelmUpdate bool
EnvFiles []string
}
// HelmChart is a Helm Chart representation. It contains all the // HelmChart is a Helm Chart representation. It contains all the
// tempaltes, values, versions, helpers... // tempaltes, values, versions, helpers...
type HelmChart struct { type HelmChart struct {
@@ -38,6 +39,7 @@ type HelmChart struct {
VolumeMounts map[string]any `yaml:"-"` VolumeMounts map[string]any `yaml:"-"`
composeHash *string `yaml:"-"` composeHash *string `yaml:"-"`
Name string `yaml:"name"` Name string `yaml:"name"`
Icon string `yaml:"icon,omitempty"`
ApiVersion string `yaml:"apiVersion"` ApiVersion string `yaml:"apiVersion"`
Version string `yaml:"version"` Version string `yaml:"version"`
AppVersion string `yaml:"appVersion"` AppVersion string `yaml:"appVersion"`
@@ -67,7 +69,7 @@ func (chart *HelmChart) SaveTemplates(templateDir string) {
t := template.Content t := template.Content
t = removeNewlinesInsideBrackets(t) t = removeNewlinesInsideBrackets(t)
t = removeUnwantedLines(t) t = removeUnwantedLines(t)
t = addModeline(t) // t = addModeline(t)
kind := utils.GetKind(name) kind := utils.GetKind(name)
var icon utils.Icon var icon utils.Icon
@@ -168,7 +170,7 @@ func (chart *HelmChart) generateConfigMapsAndSecrets(project *types.Project) err
delete(s.Environment, k) delete(s.Environment, k)
} }
if len(s.Environment) > 0 { if len(s.Environment) > 0 {
cm := NewConfigMap(s, appName) cm := NewConfigMap(s, appName, false)
y, _ := cm.Yaml() y, _ := cm.Yaml()
name := cm.service.Name name := cm.service.Name
chart.Templates[name+".configmap.yaml"] = &ChartTemplate{ chart.Templates[name+".configmap.yaml"] = &ChartTemplate{

View File

@@ -1,6 +1,8 @@
package generator package generator
import ( import (
"katenary/generator/labelStructs"
"katenary/utils"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -11,28 +13,8 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"katenary/generator/labelStructs"
"katenary/utils"
) )
// only used to check interface implementation
var (
_ DataMap = (*ConfigMap)(nil)
_ Yaml = (*ConfigMap)(nil)
)
// NewFileMap creates a new DataMap from a compose service. The appName is the name of the application taken from the project name.
func NewFileMap(service types.ServiceConfig, appName, kind string) DataMap {
switch kind {
case "configmap":
return NewConfigMap(service, appName)
default:
log.Fatalf("Unknown filemap kind: %s", kind)
}
return nil
}
// FileMapUsage is the usage of the filemap. // FileMapUsage is the usage of the filemap.
type FileMapUsage uint8 type FileMapUsage uint8
@@ -42,6 +24,23 @@ const (
FileMapUsageFiles // files in a configmap. FileMapUsageFiles // files in a configmap.
) )
// NewFileMap creates a new DataMap from a compose service. The appName is the name of the application taken from the project name.
func NewFileMap(service types.ServiceConfig, appName, kind string) DataMap {
switch kind {
case "configmap":
return NewConfigMap(service, appName, true)
default:
log.Fatalf("Unknown filemap kind: %s", kind)
}
return nil
}
// only used to check interface implementation
var (
_ DataMap = (*ConfigMap)(nil)
_ Yaml = (*ConfigMap)(nil)
)
// ConfigMap is a kubernetes ConfigMap. // ConfigMap is a kubernetes ConfigMap.
// Implements the DataMap interface. // Implements the DataMap interface.
type ConfigMap struct { type ConfigMap struct {
@@ -53,7 +52,7 @@ type ConfigMap struct {
// NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name. // 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". // The ConfigMap is filled by environment variables and labels "map-env".
func NewConfigMap(service types.ServiceConfig, appName string) *ConfigMap { func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap {
done := map[string]bool{} done := map[string]bool{}
drop := map[string]bool{} drop := map[string]bool{}
labelValues := []string{} labelValues := []string{}
@@ -99,6 +98,10 @@ func NewConfigMap(service types.ServiceConfig, appName string) *ConfigMap {
service.Environment[value] = &val service.Environment[value] = &val
} }
if forFile {
// do not bind env variables to the configmap
return cm
}
// remove the variables that are already defined in the environment // remove the variables that are already defined in the environment
if l, ok := service.Labels[LabelMapEnv]; ok { if l, ok := service.Labels[LabelMapEnv]; ok {
envmap, err := labelStructs.MapEnvFrom(l) envmap, err := labelStructs.MapEnvFrom(l)
@@ -155,11 +158,6 @@ func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string
return cm return cm
} }
// SetData sets the data of the configmap. It replaces the entire data.
func (c *ConfigMap) SetData(data map[string]string) {
c.Data = data
}
// AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists. // 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) { func (c *ConfigMap) AddData(key, value string) {
c.Data[key] = value c.Data[key] = value
@@ -230,6 +228,11 @@ func (c *ConfigMap) Filename() string {
} }
} }
// 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 // Yaml returns the yaml representation of the configmap
func (c *ConfigMap) Yaml() ([]byte, error) { func (c *ConfigMap) Yaml() ([]byte, error) {
return yaml.Marshal(c) return yaml.Marshal(c)

View File

@@ -4,6 +4,10 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"katenary/generator/extrafiles"
"katenary/generator/labelStructs"
"katenary/parser"
"katenary/utils"
"log" "log"
"os" "os"
"os/exec" "os/exec"
@@ -13,13 +17,22 @@ import (
"time" "time"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"katenary/generator/extrafiles"
"katenary/generator/labelStructs"
"katenary/parser"
"katenary/utils"
) )
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 const headerHelp = `# This file is autogenerated by katenary
# #
# DO NOT EDIT IT BY HAND UNLESS YOU KNOW WHAT YOU ARE DOING # DO NOT EDIT IT BY HAND UNLESS YOU KNOW WHAT YOU ARE DOING
@@ -32,6 +45,47 @@ const headerHelp = `# This file is autogenerated by katenary
` `
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, repsonsible 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:",
}
// keyRegExp checks if the line starts by a #
var keyRegExp = regexp.MustCompile(`^\s*[^#]+:.*`)
// Convert a compose (docker, podman...) project to a helm chart. // Convert a compose (docker, podman...) project to a helm chart.
// It calls Generate() to generate the chart and then write it to the disk. // It calls Generate() to generate the chart and then write it to the disk.
func Convert(config ConvertOptions, dockerComposeFile ...string) { func Convert(config ConvertOptions, dockerComposeFile ...string) {
@@ -59,7 +113,7 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
} }
// parse the compose files // parse the compose files
project, err := parser.Parse(config.Profiles, dockerComposeFile...) project, err := parser.Parse(config.Profiles, config.EnvFiles, dockerComposeFile...)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@@ -109,6 +163,11 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
os.Exit(1) os.Exit(1)
} }
// add icon from the command line
if config.Icon != "" {
chart.Icon = config.Icon
}
// write the templates to the disk // write the templates to the disk
chart.SaveTemplates(templateDir) chart.SaveTemplates(templateDir)
@@ -132,141 +191,6 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) {
callHelmUpdate(config) callHelmUpdate(config)
} }
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"
`
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"))
}
const storageClassHelp = `# Storage class to use for PVCs
# storageClass: "-" means use default
# storageClass: "" means do not specify
# storageClass: "foo" means use that storageClass
`
// 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"))
}
// addModeline adds a modeline to the values.yaml file to make sure that vim
// will use the correct syntax highlighting.
func addModeline(values []byte) []byte {
modeline := "# vi" + "m: ft=helm.gotmpl.yaml"
// if the values ends by `{{- end }}` we need to add the modeline before
lines := strings.Split(string(values), "\n")
if lines[len(lines)-1] == "{{- end }}" || lines[len(lines)-1] == "{{- end -}}" {
lines = lines[:len(lines)-1]
lines = append(lines, modeline, "{{- end }}")
return []byte(strings.Join(lines, "\n"))
}
return append(values, []byte(modeline)...)
}
// 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[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 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
}
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, repsonsible for creating the secret.
`
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 addChartDoc(values []byte, project *types.Project) []byte { func addChartDoc(values []byte, project *types.Project) []byte {
chartDoc := fmt.Sprintf(`# This is the main values.yaml file for the %s chart. 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. # More information can be found in the chart's README.md file.
@@ -303,67 +227,63 @@ func addChartDoc(values []byte, project *types.Project) []byte {
return []byte(chartDoc + strings.Join(lines, "\n")) return []byte(chartDoc + strings.Join(lines, "\n"))
} }
const imagePullPolicyHelp = `# imagePullPolicy allows you to specify a policy to cache or always pull an image. func addCommentsToValues(values []byte) []byte {
# 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
`
func addImagePullPolicyHelp(values []byte) []byte {
// add imagePullPolicy help
lines := strings.Split(string(values), "\n") lines := strings.Split(string(values), "\n")
for i, line := range lines { for i, line := range lines {
if strings.Contains(line, "imagePullPolicy:") { if strings.Contains(line, "ingress:") {
spaces := utils.CountStartingSpaces(line) spaces := utils.CountStartingSpaces(line)
spacesString := strings.Repeat(" ", spaces) spacesString := strings.Repeat(" ", spaces)
// indent imagePullPolicyHelp comment // indent ingressClassHelper comment
imagePullPolicyHelp := strings.ReplaceAll(imagePullPolicyHelp, "\n", "\n"+spacesString) ingressClassHelp := strings.ReplaceAll(ingressClassHelp, "\n", "\n"+spacesString)
imagePullPolicyHelp = strings.TrimRight(imagePullPolicyHelp, " ") ingressClassHelp = strings.TrimRight(ingressClassHelp, " ")
imagePullPolicyHelp = spacesString + imagePullPolicyHelp ingressClassHelp = spacesString + ingressClassHelp
lines[i] = imagePullPolicyHelp + line lines[i] = ingressClassHelp + line
} }
} }
return []byte(strings.Join(lines, "\n")) return []byte(strings.Join(lines, "\n"))
} }
const resourceHelp = `# Resources allows you to specify the resource requests and limits for a service. func addDependencyDescription(values []byte, dependencies []labelStructs.Dependency) []byte {
# Resources are used to specify the amount of CPU and memory that for _, d := range dependencies {
# a container needs. name := d.Name
# if d.Alias != "" {
# e.g. name = d.Alias
# resources: }
# requests:
# memory: "64Mi"
# cpu: "250m"
# limits:
# memory: "128Mi"
# cpu: "500m"
`
func addResourceHelp(values []byte) []byte { values = regexp.MustCompile(
lines := strings.Split(string(values), "\n") `(?m)^`+name+`:$`,
for i, line := range lines { ).ReplaceAll(
if strings.Contains(line, "resources:") { values,
spaces := utils.CountStartingSpaces(line) []byte("\n# "+d.Name+" helm dependency configuration\n"+name+":"),
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 values
return []byte(strings.Join(lines, "\n"))
} }
func addVariablesDoc(values []byte, project *types.Project) []byte { // addDescriptions adds the description from the label to the values.yaml file on top
lines := strings.Split(string(values), "\n") // of the service definition.
func addDescriptions(values []byte, project types.Project) []byte {
for _, service := range project.Services { for _, service := range project.Services {
lines = addDocToVariable(service, lines) if description, ok := service.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 []byte(strings.Join(lines, "\n")) }
return values
} }
func addDocToVariable(service types.ServiceConfig, lines []string) []string { func addDocToVariable(service types.ServiceConfig, lines []string) []string {
@@ -394,25 +314,38 @@ func addDocToVariable(service types.ServiceConfig, lines []string) []string {
return lines return lines
} }
const mainTagAppDoc = `This is the version of the main application. func addImagePullPolicyHelp(values []byte) []byte {
Leave it to blank to use the Chart "AppVersion" value.` // 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 addMainTagAppDoc(values []byte, project *types.Project) []byte { func addImagePullSecretsHelp(values []byte) []byte {
// add imagePullSecrets help
lines := strings.Split(string(values), "\n") lines := strings.Split(string(values), "\n")
for _, service := range project.Services { for i, line := range lines {
// read the label LabelMainApp if strings.Contains(line, "pullSecrets:") {
if v, ok := service.Labels[LabelMainApp]; !ok { spaces := utils.CountStartingSpaces(line)
continue spacesString := strings.Repeat(" ", spaces)
} else if v == "false" || v == "no" || v == "0" { // indent imagePullSecretHelp comment
continue imagePullSecretHelp := strings.ReplaceAll(imagePullSecretHelp, "\n", "\n"+spacesString)
} else { imagePullSecretHelp = strings.TrimRight(imagePullSecretHelp, " ")
fmt.Printf("%s Adding main tag app doc %s\n", utils.IconConfig, service.Name) imagePullSecretHelp = spacesString + imagePullSecretHelp
lines[i] = imagePullSecretHelp + line
} }
lines = addMainAppDoc(lines, service)
} }
return []byte(strings.Join(lines, "\n")) return []byte(strings.Join(lines, "\n"))
} }
@@ -440,107 +373,84 @@ func addMainAppDoc(lines []string, service types.ServiceConfig) []string {
return lines return lines
} }
func removeNewlinesInsideBrackets(values []byte) []byte { func addMainTagAppDoc(values []byte, project *types.Project) []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)
})
}
var unwantedLines = []string{
"creationTimestamp:",
"status:",
}
func removeUnwantedLines(values []byte) []byte {
lines := strings.Split(string(values), "\n") 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"))
}
// 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 _, service := range project.Services {
for label := range service.Labels { // read the label LabelMainApp
if strings.Contains(label, "katenary.") && !strings.Contains(label, katenaryLabelPrefix) { if v, ok := service.Labels[LabelMainApp]; !ok {
badServices = append(badServices, fmt.Sprintf("- %s: %s", service.Name, label)) 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)
} }
}
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. return []byte(strings.Join(lines, "\n"))
Your project is not compatible with this version.
Please upgrade your labels to follow the current version
Services to upgrade:
%s`,
project.Name,
katenaryLabelPrefix[0:len(katenaryLabelPrefix)-1],
strings.Join(badServices, "\n"),
)
return errors.New(utils.WordWrap(message, 80))
}
return nil
} }
// helmUpdate runs "helm dependency update" on the output directory. // addModeline adds a modeline to the values.yaml file to make sure that vim
func helmUpdate(config ConvertOptions) error { // will use the correct syntax highlighting.
// lookup for "helm" binary func addModeline(values []byte) []byte {
fmt.Println(utils.IconInfo, "Updating helm dependencies...") modeline := "# vi" + "m: ft=helm.gotmpl.yaml"
helm, err := exec.LookPath("helm")
if err != nil { // if the values ends by `{{- end }}` we need to add the modeline before
fmt.Println(utils.IconFailure, err) lines := strings.Split(string(values), "\n")
os.Exit(1)
if lines[len(lines)-1] == "{{- end }}" || lines[len(lines)-1] == "{{- end -}}" {
lines = lines[:len(lines)-1]
lines = append(lines, modeline, "{{- end }}")
return []byte(strings.Join(lines, "\n"))
} }
// run "helm dependency update"
cmd := exec.Command(helm, "dependency", "update", config.OutputDir) return append(values, []byte(modeline)...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
} }
// helmLint runs "helm lint" on the output directory. func addResourceHelp(values []byte) []byte {
func helmLint(config ConvertOptions) error { lines := strings.Split(string(values), "\n")
fmt.Println(utils.IconInfo, "Linting...") for i, line := range lines {
helm, err := exec.LookPath("helm") if strings.Contains(line, "resources:") {
if err != nil { spaces := utils.CountStartingSpaces(line)
fmt.Println(utils.IconFailure, err) spacesString := strings.Repeat(" ", spaces)
os.Exit(1) // indent resourceHelp comment
resourceHelp := strings.ReplaceAll(resourceHelp, "\n", "\n"+spacesString)
resourceHelp = strings.TrimRight(resourceHelp, " ")
resourceHelp = spacesString + resourceHelp
lines[i] = resourceHelp + line
} }
cmd := exec.Command(helm, "lint", config.OutputDir) }
cmd.Stdout = os.Stdout return []byte(strings.Join(lines, "\n"))
cmd.Stderr = os.Stderr
return cmd.Run()
} }
// keyRegExp checks if the line starts by a # // addStorageClassHelp adds a comment to the values.yaml file to explain how to
var keyRegExp = regexp.MustCompile(`^\s*[^#]+:.*`) // 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 // addYAMLSelectorPath adds a selector path to the yaml file for each key
// as comment. E.g. foo.ingress.host // as comment. E.g. foo.ingress.host
@@ -587,14 +497,40 @@ func addYAMLSelectorPath(values []byte) []byte {
return []byte(strings.Join(toReturn, "\n")) return []byte(strings.Join(toReturn, "\n"))
} }
func writeContent(path string, content []byte) { func buildCharYamlFile(chart *HelmChart, project *types.Project, chartPath string) {
f, err := os.Create(path) // calculate the sha1 hash of the services
yamlChart, err := utils.EncodeBasicYaml(chart)
if err != nil { if err != nil {
fmt.Println(utils.IconFailure, err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
defer f.Close() // concat chart adding a comment with hash of services on top
f.Write(content) 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) { func buildValues(chart *HelmChart, project *types.Project, valuesPath string) {
@@ -622,42 +558,6 @@ func buildValues(chart *HelmChart, project *types.Project, valuesPath string) {
writeContent(valuesPath, values) writeContent(valuesPath, values)
} }
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 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 callHelmUpdate(config ConvertOptions) { func callHelmUpdate(config ConvertOptions) {
executeAndHandleError := func(fn func(ConvertOptions) error, config ConvertOptions, message string) { executeAndHandleError := func(fn func(ConvertOptions) error, config ConvertOptions, message string) {
if err := fn(config); err != nil { if err := fn(config); err != nil {
@@ -672,3 +572,107 @@ func callHelmUpdate(config ConvertOptions) {
fmt.Println(utils.IconSuccess, "Helm chart created successfully") 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()
f.Write(content)
}
// 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, 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,
katenaryLabelPrefix[0:len(katenaryLabelPrefix)-1],
strings.Join(badServices, "\n"),
)
return errors.New(utils.WordWrap(message, 80))
}
return nil
}

View File

@@ -2,6 +2,8 @@ package generator
import ( import (
"fmt" "fmt"
"katenary/generator/labelStructs"
"katenary/utils"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -14,9 +16,6 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"katenary/generator/labelStructs"
"katenary/utils"
) )
var _ Yaml = (*Deployment)(nil) var _ Yaml = (*Deployment)(nil)
@@ -106,32 +105,6 @@ func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment {
return dep return dep
} }
// 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 "+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
}
// AddContainer adds a container to the deployment. // AddContainer adds a container to the deployment.
func (d *Deployment) AddContainer(service types.ServiceConfig) { func (d *Deployment) AddContainer(service types.ServiceConfig) {
ports := []corev1.ContainerPort{} ports := []corev1.ContainerPort{}
@@ -178,6 +151,34 @@ func (d *Deployment) AddContainer(service types.ServiceConfig) {
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, 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[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. // AddIngress adds an ingress to the deployment. It creates the ingress object.
func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress { func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress {
return NewIngress(service, d.chart) return NewIngress(service, d.chart)
@@ -209,124 +210,6 @@ func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) {
} }
} }
func (d *Deployment) bindVolumes(volume types.ServiceVolumeConfig, isSamePod bool, tobind map[string]bool, service types.ServiceConfig, appName string) {
container, index := utils.GetContainerByName(service.Name, 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 _, ok := tobind[volume.Source]; !isSamePod && volume.Type == "bind" && !ok {
utils.Warn(
"Bind volumes are not supported yet, " +
"excepting for those declared as " +
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
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: volume.Source,
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[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: volume.Source,
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)
}
}
}
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)
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
}
cm.AppendFile(volume.Source)
}
func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) { func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) {
// find the volume in the binded deployment // find the volume in the binded deployment
for _, bindedVolume := range binded.Spec.Template.Spec.Volumes { for _, bindedVolume := range binded.Spec.Template.Spec.Volumes {
@@ -354,6 +237,37 @@ func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) {
} }
} }
// 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 "+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. // SetEnvFrom sets the environment variables to a configmap. The configmap is created.
func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string) { func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string) {
if len(service.Environment) == 0 { if len(service.Environment) == 0 {
@@ -447,34 +361,6 @@ func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string) {
d.Spec.Template.Spec.Containers[index] = *container d.Spec.Template.Spec.Containers[index] = *container
} }
func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container) {
// get the label for healthcheck
if v, ok := service.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),
}
}
}
// Yaml returns the yaml representation of the deployment. // Yaml returns the yaml representation of the deployment.
func (d *Deployment) Yaml() ([]byte, error) { func (d *Deployment) Yaml() ([]byte, error) {
serviceName := d.service.Name serviceName := d.service.Name
@@ -489,11 +375,13 @@ func (d *Deployment) Yaml() ([]byte, error) {
spaces := "" spaces := ""
volumeName := "" volumeName := ""
nameDirective := "name: "
// this loop add condition for each volume mount // this loop add condition for each volume mount
for line, volume := range content { for line, volume := range content {
// find the volume name // find the volume name
for i := line; i < len(content); i++ { for i := line; i < len(content); i++ {
if strings.Contains(content[i], "name: ") { if strings.Contains(content[i], nameDirective) {
volumeName = strings.TrimSpace(strings.Replace(content[i], "name: ", "", 1)) volumeName = strings.TrimSpace(strings.Replace(content[i], "name: ", "", 1))
break break
} }
@@ -511,7 +399,7 @@ func (d *Deployment) Yaml() ([]byte, error) {
content[line] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + volumeName + `.enabled }}` + "\n" + volume content[line] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + volumeName + `.enabled }}` + "\n" + volume
changing = true changing = true
} }
if strings.Contains(volume, "name: ") && changing { if strings.Contains(volume, nameDirective) && changing {
content[line] = volume + "\n" + spaces + "{{- end }}" content[line] = volume + "\n" + spaces + "{{- end }}"
changing = false changing = false
} }
@@ -624,7 +512,120 @@ func (d *Deployment) Yaml() ([]byte, error) {
return []byte(strings.Join(content, "\n")), nil return []byte(strings.Join(content, "\n")), nil
} }
// Filename returns the filename of the deployment. func (d *Deployment) appendDirectoryToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) {
func (d *Deployment) Filename() string { pathnme := utils.PathToName(volume.Source)
return d.service.Name + ".deployment.yaml" 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
}
cm.AppendFile(volume.Source)
}
func (d *Deployment) bindVolumes(volume types.ServiceVolumeConfig, isSamePod bool, tobind map[string]bool, service types.ServiceConfig, appName string) {
container, index := utils.GetContainerByName(service.Name, 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 _, ok := tobind[volume.Source]; !isSamePod && volume.Type == "bind" && !ok {
utils.Warn(
"Bind volumes are not supported yet, " +
"excepting for those declared as " +
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
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: volume.Source,
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[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: volume.Source,
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

@@ -11,14 +11,35 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
//go:embed readme.tpl
var readmeTemplate string
type chart struct { type chart struct {
Name string Name string
Description string Description string
Values []string Values []string
} }
//go:embed readme.tpl func parseValues(prefix string, values map[string]interface{}, result map[string]string) {
var readmeTemplate string for key, value := range values {
path := key
if prefix != "" {
path = prefix + "." + key
}
switch v := value.(type) {
case []interface{}:
for i, u := range v {
parseValues(fmt.Sprintf("%s[%d]", path, i), map[string]interface{}{"value": u}, result)
}
case map[string]interface{}:
parseValues(path, v, result)
default:
strValue := fmt.Sprintf("`%v`", value)
result["`"+path+"`"] = strValue
}
}
}
// ReadMeFile returns the content of the README.md file. // ReadMeFile returns the content of the README.md file.
func ReadMeFile(charname, description string, values map[string]any) string { func ReadMeFile(charname, description string, values map[string]any) string {
@@ -74,24 +95,3 @@ func ReadMeFile(charname, description string, values map[string]any) string {
return buf.String() return buf.String()
} }
func parseValues(prefix string, values map[string]interface{}, result map[string]string) {
for key, value := range values {
path := key
if prefix != "" {
path = prefix + "." + key
}
switch v := value.(type) {
case []interface{}:
for i, u := range v {
parseValues(fmt.Sprintf("%s[%d]", path, i), map[string]interface{}{"value": u}, result)
}
case map[string]interface{}:
parseValues(path, v, result)
default:
strValue := fmt.Sprintf("`%v`", value)
result["`"+path+"`"] = strValue
}
}
}

View File

@@ -162,49 +162,6 @@ func Generate(project *types.Project) (*HelmChart, error) {
return chart, nil return chart, nil
} }
// 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__"), []byte(fmt.Sprintf("%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
}
// serviceIsMain returns true if the service is the main app. // serviceIsMain returns true if the service is the main app.
func serviceIsMain(service types.ServiceConfig) bool { func serviceIsMain(service types.ServiceConfig) bool {
if main, ok := service.Labels[LabelMainApp]; ok { if main, ok := service.Labels[LabelMainApp]; ok {
@@ -213,37 +170,6 @@ func serviceIsMain(service types.ServiceConfig) bool {
return false return false
} }
// 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":
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[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
}
func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceConfig) { func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceConfig) {
// add the bound configMaps files to the deployment containers // add the bound configMaps files to the deployment containers
var d *Deployment var d *Deployment
@@ -292,6 +218,80 @@ func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceC
d.Spec.Template.Spec.Containers[index] = *container 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__"), []byte(fmt.Sprintf("%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":
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[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. // 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 { func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, deployments map[string]*Deployment) bool {
// if the service has volumes, and it has "same-pod" label // if the service has volumes, and it has "same-pod" label

View File

@@ -119,6 +119,10 @@ func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress {
return ing return ing
} }
func (ingress *Ingress) Filename() string {
return ingress.service.Name + ".ingress.yaml"
}
func (ingress *Ingress) Yaml() ([]byte, error) { func (ingress *Ingress) Yaml() ([]byte, error) {
serviceName := ingress.service.Name serviceName := ingress.service.Name
ret, err := yaml.Marshal(ingress) ret, err := yaml.Marshal(ingress)
@@ -159,7 +163,3 @@ func (ingress *Ingress) Yaml() ([]byte, error) {
ret = []byte(strings.Join(out, "\n")) ret = []byte(strings.Join(out, "\n"))
return ret, nil return ret, nil
} }
func (ingress *Ingress) Filename() string {
return ingress.service.Name + ".ingress.yaml"
}

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
_ "embed" _ "embed"
"fmt" "fmt"
"katenary/utils"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
@@ -11,37 +12,10 @@ import (
"text/template" "text/template"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"katenary/utils"
) )
var (
// Set the documentation of labels here
//
//go:embed katenaryLabelsDoc.yaml
labelFullHelpYAML []byte
// parsed yaml
labelFullHelp map[string]Help
)
// Label is a katenary label to find in compose files.
type Label = string
// Help is the documentation of a label.
type Help struct {
Short string `yaml:"short"`
Long string `yaml:"long"`
Example string `yaml:"example"`
Type string `yaml:"type"`
}
const katenaryLabelPrefix = "katenary.v3" const katenaryLabelPrefix = "katenary.v3"
func Prefix() string {
return katenaryLabelPrefix
}
// Known labels. // Known labels.
const ( const (
LabelMainApp Label = katenaryLabelPrefix + "/main-app" LabelMainApp Label = katenaryLabelPrefix + "/main-app"
@@ -60,16 +34,47 @@ const (
LabelEnvFrom Label = katenaryLabelPrefix + "/env-from" LabelEnvFrom Label = katenaryLabelPrefix + "/env-from"
) )
var (
// Set the documentation of labels here
//
//go:embed katenaryLabelsDoc.yaml
labelFullHelpYAML []byte
// parsed yaml
labelFullHelp map[string]Help
)
// Label is a katenary label to find in compose files.
type Label = string
func labelName(name string) Label {
return Label(katenaryLabelPrefix + "/" + name)
}
// Help is the documentation of a label.
type Help struct {
Short string `yaml:"short"`
Long string `yaml:"long"`
Example string `yaml:"example"`
Type string `yaml:"type"`
}
// GetLabelNames returns a sorted list of all katenary label names.
func GetLabelNames() []string {
var names []string
for name := range labelFullHelp {
names = append(names, name)
}
sort.Strings(names)
return names
}
func init() { func init() {
if err := yaml.Unmarshal(labelFullHelpYAML, &labelFullHelp); err != nil { if err := yaml.Unmarshal(labelFullHelpYAML, &labelFullHelp); err != nil {
panic(err) panic(err)
} }
} }
func labelName(name string) Label {
return Label(katenaryLabelPrefix + "/" + name)
}
// Generate the help for the labels. // Generate the help for the labels.
func GetLabelHelp(asMarkdown bool) string { func GetLabelHelp(asMarkdown bool) string {
names := GetLabelNames() // sorted names := GetLabelNames() // sorted
@@ -79,73 +84,6 @@ func GetLabelHelp(asMarkdown bool) string {
return generateMarkdownHelp(names) return generateMarkdownHelp(names)
} }
func generatePlainHelp(names []string) string {
var builder strings.Builder
for _, name := range names {
help := labelFullHelp[name]
fmt.Fprintf(&builder, "%s:\t%s\t%s\n", labelName(name), help.Type, help.Short)
}
// use tabwriter to align the help text
buf := new(strings.Builder)
w := tabwriter.NewWriter(buf, 0, 8, 0, '\t', tabwriter.AlignRight)
fmt.Fprintln(w, builder.String())
w.Flush()
head := "To get more information about a label, use `katenary help-label <name_without_prefix>\ne.g. katenary help-label dependencies\n\n"
return head + buf.String()
}
func generateMarkdownHelp(names []string) string {
var builder strings.Builder
var maxNameLength, maxDescriptionLength, maxTypeLength int
max := func(a, b int) int {
if a > b {
return a
}
return b
}
for _, name := range names {
help := labelFullHelp[name]
maxNameLength = max(maxNameLength, len(name)+2+len(katenaryLabelPrefix))
maxDescriptionLength = max(maxDescriptionLength, len(help.Short))
maxTypeLength = max(maxTypeLength, len(help.Type))
}
fmt.Fprintf(&builder, "%s\n", generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength))
fmt.Fprintf(&builder, "%s\n", generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength))
for _, name := range names {
help := labelFullHelp[name]
fmt.Fprintf(&builder, "| %-*s | %-*s | %-*s |\n",
maxNameLength, "`"+labelName(name)+"`", // enclose in backticks
maxDescriptionLength, help.Short,
maxTypeLength, help.Type,
)
}
return builder.String()
}
func generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength int) string {
return fmt.Sprintf(
"| %-*s | %-*s | %-*s |",
maxNameLength, "Label name",
maxDescriptionLength, "Description",
maxTypeLength, "Type",
)
}
func generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength int) string {
return fmt.Sprintf(
"| %s | %s | %s |",
strings.Repeat("-", maxNameLength),
strings.Repeat("-", maxDescriptionLength),
strings.Repeat("-", maxTypeLength),
)
}
// GetLabelHelpFor returns the help for a specific label. // GetLabelHelpFor returns the help for a specific label.
func GetLabelHelpFor(labelname string, asMarkdown bool) string { func GetLabelHelpFor(labelname string, asMarkdown bool) string {
help, ok := labelFullHelp[labelname] help, ok := labelFullHelp[labelname]
@@ -202,14 +140,71 @@ func GetLabelHelpFor(labelname string, asMarkdown bool) string {
return buf.String() return buf.String()
} }
// GetLabelNames returns a sorted list of all katenary label names. func generateMarkdownHelp(names []string) string {
func GetLabelNames() []string { var builder strings.Builder
var names []string var maxNameLength, maxDescriptionLength, maxTypeLength int
for name := range labelFullHelp {
names = append(names, name) max := func(a, b int) int {
if a > b {
return a
} }
sort.Strings(names) return b
return names }
for _, name := range names {
help := labelFullHelp[name]
maxNameLength = max(maxNameLength, len(name)+2+len(katenaryLabelPrefix))
maxDescriptionLength = max(maxDescriptionLength, len(help.Short))
maxTypeLength = max(maxTypeLength, len(help.Type))
}
fmt.Fprintf(&builder, "%s\n", generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength))
fmt.Fprintf(&builder, "%s\n", generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength))
for _, name := range names {
help := labelFullHelp[name]
fmt.Fprintf(&builder, "| %-*s | %-*s | %-*s |\n",
maxNameLength, "`"+labelName(name)+"`", // enclose in backticks
maxDescriptionLength, help.Short,
maxTypeLength, help.Type,
)
}
return builder.String()
}
func generatePlainHelp(names []string) string {
var builder strings.Builder
for _, name := range names {
help := labelFullHelp[name]
fmt.Fprintf(&builder, "%s:\t%s\t%s\n", labelName(name), help.Type, help.Short)
}
// use tabwriter to align the help text
buf := new(strings.Builder)
w := tabwriter.NewWriter(buf, 0, 8, 0, '\t', tabwriter.AlignRight)
fmt.Fprintln(w, builder.String())
w.Flush()
head := "To get more information about a label, use `katenary help-label <name_without_prefix>\ne.g. katenary help-label dependencies\n\n"
return head + buf.String()
}
func generateTableHeader(maxNameLength, maxDescriptionLength, maxTypeLength int) string {
return fmt.Sprintf(
"| %-*s | %-*s | %-*s |",
maxNameLength, "Label name",
maxDescriptionLength, "Description",
maxTypeLength, "Type",
)
}
func generateTableHeaderSeparator(maxNameLength, maxDescriptionLength, maxTypeLength int) string {
return fmt.Sprintf(
"| %s | %s | %s |",
strings.Repeat("-", maxNameLength),
strings.Repeat("-", maxDescriptionLength),
strings.Repeat("-", maxTypeLength),
)
} }
func getHelpTemplate(asMarkdown bool) string { func getHelpTemplate(asMarkdown bool) string {
@@ -234,3 +229,7 @@ Example:
{{ .Help.Example }} {{ .Help.Example }}
` `
} }
func Prefix() string {
return katenaryLabelPrefix
}

View File

@@ -284,4 +284,5 @@
# defined inside this service too # defined inside this service too
{{ .KatenaryPrefix }}/env-from: |- {{ .KatenaryPrefix }}/env-from: |-
- myservice1 - myservice1
# vim: ft=gotmpl.yaml # vim: ft=gotmpl.yaml

View File

@@ -4,11 +4,11 @@ import "gopkg.in/yaml.v3"
// Dependency is a dependency of a chart to other charts. // Dependency is a dependency of a chart to other charts.
type Dependency struct { type Dependency struct {
Values map[string]any `yaml:"-"`
Name string `yaml:"name"` Name string `yaml:"name"`
Version string `yaml:"version"` Version string `yaml:"version"`
Repository string `yaml:"repository"` Repository string `yaml:"repository"`
Alias string `yaml:"alias,omitempty"` Alias string `yaml:"alias,omitempty"`
Values map[string]any `yaml:"-"` // do not export to Chart.yaml
} }
// DependenciesFrom returns a slice of dependencies from the given string. // DependenciesFrom returns a slice of dependencies from the given string.

View File

@@ -3,18 +3,12 @@ package labelStructs
import "gopkg.in/yaml.v3" import "gopkg.in/yaml.v3"
type Ingress struct { type Ingress struct {
// Hostname is the hostname to match against the request. It can contain wildcards.
Hostname string `yaml:"hostname"`
// Path is the path to match against the request. It can contain wildcards.
Path string `yaml:"path"`
// Enabled is a flag to enable or disable the ingress.
Enabled bool `yaml:"enabled"`
// Class is the ingress class to use.
Class string `yaml:"class"`
// Port is the port to use.
Port *int32 `yaml:"port,omitempty"` Port *int32 `yaml:"port,omitempty"`
// Annotations is a list of key-value pairs to add to the ingress.
Annotations map[string]string `yaml:"annotations,omitempty"` Annotations map[string]string `yaml:"annotations,omitempty"`
Hostname string `yaml:"hostname"`
Path string `yaml:"path"`
Class string `yaml:"class"`
Enabled bool `yaml:"enabled"`
} }
// IngressFrom creates a new Ingress from a compose service. // IngressFrom creates a new Ingress from a compose service.

View File

@@ -102,38 +102,38 @@ type RoleBinding struct {
service *types.ServiceConfig service *types.ServiceConfig
} }
func (r *RoleBinding) Yaml() ([]byte, error) {
return yaml.Marshal(r)
}
func (r *RoleBinding) Filename() string { func (r *RoleBinding) Filename() string {
return r.service.Name + ".rolebinding.yaml" return r.service.Name + ".rolebinding.yaml"
} }
func (r *RoleBinding) Yaml() ([]byte, error) {
return yaml.Marshal(r)
}
// Role is a kubernetes Role. // Role is a kubernetes Role.
type Role struct { type Role struct {
*rbacv1.Role *rbacv1.Role
service *types.ServiceConfig service *types.ServiceConfig
} }
func (r *Role) Yaml() ([]byte, error) {
return yaml.Marshal(r)
}
func (r *Role) Filename() string { func (r *Role) Filename() string {
return r.service.Name + ".role.yaml" return r.service.Name + ".role.yaml"
} }
func (r *Role) Yaml() ([]byte, error) {
return yaml.Marshal(r)
}
// ServiceAccount is a kubernetes ServiceAccount. // ServiceAccount is a kubernetes ServiceAccount.
type ServiceAccount struct { type ServiceAccount struct {
*corev1.ServiceAccount *corev1.ServiceAccount
service *types.ServiceConfig service *types.ServiceConfig
} }
func (r *ServiceAccount) Yaml() ([]byte, error) {
return yaml.Marshal(r)
}
func (r *ServiceAccount) Filename() string { func (r *ServiceAccount) Filename() string {
return r.service.Name + ".serviceaccount.yaml" return r.service.Name + ".serviceaccount.yaml"
} }
func (r *ServiceAccount) Yaml() ([]byte, error) {
return yaml.Marshal(r)
}

View File

@@ -76,13 +76,6 @@ func NewSecret(service types.ServiceConfig, appName string) *Secret {
return secret return secret
} }
// SetData sets the data of the secret.
func (s *Secret) SetData(data map[string]string) {
for key, value := range data {
s.AddData(key, value)
}
}
// AddData adds a key value pair to the secret. // AddData adds a key value pair to the secret.
func (s *Secret) AddData(key, value string) { func (s *Secret) AddData(key, value string) {
if value == "" { if value == "" {
@@ -91,6 +84,18 @@ func (s *Secret) AddData(key, value string) {
s.Data[key] = []byte(`{{ tpl ` + value + ` $ | b64enc }}`) s.Data[key] = []byte(`{{ tpl ` + value + ` $ | b64enc }}`)
} }
// Filename returns the filename of the secret.
func (s *Secret) Filename() string {
return s.service.Name + ".secret.yaml"
}
// SetData sets the data of the secret.
func (s *Secret) SetData(data map[string]string) {
for key, value := range data {
s.AddData(key, value)
}
}
// Yaml returns the yaml representation of the secret. // Yaml returns the yaml representation of the secret.
func (s *Secret) Yaml() ([]byte, error) { func (s *Secret) Yaml() ([]byte, error) {
y, err := yaml.Marshal(s) y, err := yaml.Marshal(s)
@@ -106,8 +111,3 @@ func (s *Secret) Yaml() ([]byte, error) {
return y, nil return y, nil
} }
// Filename returns the filename of the secret.
func (s *Secret) Filename() string {
return s.service.Name + ".secret.yaml"
}

View File

@@ -74,6 +74,11 @@ func (s *Service) AddPort(port types.ServicePortConfig, serviceName ...string) {
}) })
} }
// Filename returns the filename of the service.
func (s *Service) Filename() string {
return s.service.Name + ".service.yaml"
}
// Yaml returns the yaml representation of the service. // Yaml returns the yaml representation of the service.
func (s *Service) Yaml() ([]byte, error) { func (s *Service) Yaml() ([]byte, error) {
y, err := yaml.Marshal(s) y, err := yaml.Marshal(s)
@@ -88,8 +93,3 @@ func (s *Service) Yaml() ([]byte, error) {
return y, err return y, err
} }
// Filename returns the filename of the service.
func (s *Service) Filename() string {
return s.service.Name + ".service.yaml"
}

View File

@@ -43,14 +43,6 @@ type Value struct {
ServiceAccount string `yaml:"serviceAccount"` ServiceAccount string `yaml:"serviceAccount"`
} }
// CronJobValue is a cronjob configuration that will be saved in values.yaml.
type CronJobValue struct {
Repository *RepositoryValue `yaml:"repository,omitempty"`
Environment map[string]any `yaml:"environment,omitempty"`
ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
Schedule string `yaml:"schedule"`
}
// NewValue creates a new Value from a compose service. // NewValue creates a new Value from a compose service.
// The value contains the necessary information to deploy the service (image, tag, replicas, etc.). // The value contains the necessary information to deploy the service (image, tag, replicas, etc.).
// //
@@ -64,15 +56,22 @@ func NewValue(service types.ServiceConfig, main ...bool) *Value {
// find the image tag // find the image tag
tag := "" tag := ""
split := strings.Split(service.Image, ":") split := strings.Split(service.Image, ":")
if len(split) == 1 {
v.Repository = &RepositoryValue{ v.Repository = &RepositoryValue{
Image: split[0], Image: service.Image,
}
} else {
v.Repository = &RepositoryValue{
Image: strings.Join(split[:len(split)-1], ":"),
}
} }
// for main service, the tag should the appVersion. So here we set it to empty. // for main service, the tag should the appVersion. So here we set it to empty.
if len(main) > 0 && !main[0] { if len(main) > 0 && !main[0] {
if len(split) > 1 { if len(split) > 1 {
tag = split[1] tag = split[len(split)-1]
} }
v.Repository.Tag = tag v.Repository.Tag = tag
} else { } else {
@@ -82,6 +81,15 @@ func NewValue(service types.ServiceConfig, main ...bool) *Value {
return v return v
} }
func (v *Value) AddIngress(host, path string) {
v.Ingress = &IngressValue{
Enabled: true,
Host: host,
Path: path,
Class: "-",
}
}
// AddPersistence adds persistence configuration to the Value. // AddPersistence adds persistence configuration to the Value.
func (v *Value) AddPersistence(volumeName string) { func (v *Value) AddPersistence(volumeName string) {
if v.Persistence == nil { if v.Persistence == nil {
@@ -95,11 +103,10 @@ func (v *Value) AddPersistence(volumeName string) {
} }
} }
func (v *Value) AddIngress(host, path string) { // CronJobValue is a cronjob configuration that will be saved in values.yaml.
v.Ingress = &IngressValue{ type CronJobValue struct {
Enabled: true, Repository *RepositoryValue `yaml:"repository,omitempty"`
Host: host, Environment map[string]any `yaml:"environment,omitempty"`
Path: path, ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
Class: "-", Schedule string `yaml:"schedule"`
}
} }

View File

@@ -12,10 +12,10 @@ import (
"katenary/utils" "katenary/utils"
) )
var _ Yaml = (*VolumeClaim)(nil)
const persistenceKey = "persistence" const persistenceKey = "persistence"
var _ Yaml = (*VolumeClaim)(nil)
// VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim. // VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim.
type VolumeClaim struct { type VolumeClaim struct {
*v1.PersistentVolumeClaim *v1.PersistentVolumeClaim
@@ -59,6 +59,11 @@ func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *Vo
} }
} }
// Filename returns the suggested filename for a VolumeClaim.
func (v *VolumeClaim) Filename() string {
return v.service.Name + "." + v.volumeName + ".volumeclaim.yaml"
}
// Yaml marshals a VolumeClaim into yaml. // Yaml marshals a VolumeClaim into yaml.
func (v *VolumeClaim) Yaml() ([]byte, error) { func (v *VolumeClaim) Yaml() ([]byte, error) {
serviceName := v.service.Name serviceName := v.service.Name
@@ -122,8 +127,3 @@ func (v *VolumeClaim) Yaml() ([]byte, error) {
return out, nil return out, nil
} }
// Filename returns the suggested filename for a VolumeClaim.
func (v *VolumeClaim) Filename() string {
return v.service.Name + "." + v.volumeName + ".volumeclaim.yaml"
}

View File

@@ -2,6 +2,9 @@
package parser package parser
import ( import (
"log"
"path/filepath"
"github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
) )
@@ -12,6 +15,7 @@ func init() {
"compose.katenary.yml", "compose.katenary.yml",
"compose.katenary.yaml", "compose.katenary.yaml",
}, cli.DefaultOverrideFileNames...) }, cli.DefaultOverrideFileNames...)
// add podman-compose files
cli.DefaultOverrideFileNames = append(cli.DefaultOverrideFileNames, cli.DefaultOverrideFileNames = append(cli.DefaultOverrideFileNames,
[]string{ []string{
"podman-compose.katenary.yml", "podman-compose.katenary.yml",
@@ -22,18 +26,31 @@ func init() {
} }
// Parse compose files and return a project. The project is parsed with dotenv, osenv and profiles. // Parse compose files and return a project. The project is parsed with dotenv, osenv and profiles.
func Parse(profiles []string, dockerComposeFile ...string) (*types.Project, error) { func Parse(profiles []string, envFiles []string, dockerComposeFile ...string) (*types.Project, error) {
if len(dockerComposeFile) == 0 { if len(dockerComposeFile) == 0 {
cli.DefaultOverrideFileNames = append(cli.DefaultOverrideFileNames, dockerComposeFile...) cli.DefaultOverrideFileNames = append(cli.DefaultOverrideFileNames, dockerComposeFile...)
} }
log.Println("Loading compose files: ", cli.DefaultOverrideFileNames)
// resolve absolute paths of envFiles
for i := range envFiles {
var err error
envFiles[i], err = filepath.Abs(envFiles[i])
if err != nil {
log.Fatal(err)
}
}
log.Println("Loading env files: ", envFiles)
options, err := cli.NewProjectOptions(nil, options, err := cli.NewProjectOptions(nil,
cli.WithProfiles(profiles), cli.WithProfiles(profiles),
cli.WithInterpolation(true),
cli.WithDefaultConfigPath, cli.WithDefaultConfigPath,
cli.WithEnvFiles(envFiles...),
cli.WithOsEnv, cli.WithOsEnv,
cli.WithDotEnv, cli.WithDotEnv,
cli.WithNormalization(true), cli.WithNormalization(true),
cli.WithInterpolation(true),
cli.WithResolvedPaths(false), cli.WithResolvedPaths(false),
) )
if err != nil { if err != nil {

View File

@@ -20,6 +20,11 @@ func TplName(serviceName, appname string, suffix ...string) string {
if len(suffix) > 0 { if len(suffix) > 0 {
suffix[0] = "-" + suffix[0] suffix[0] = "-" + suffix[0]
} }
for i, s := range suffix {
// replae all "_" with "-"
suffix[i] = strings.ReplaceAll(s, "_", "-")
}
serviceName = strings.ReplaceAll(serviceName, "_", "-")
return `{{ include "` + appname + `.fullname" . }}-` + serviceName + strings.Join(suffix, "-") return `{{ include "` + appname + `.fullname" . }}-` + serviceName + strings.Join(suffix, "-")
} }
@@ -109,8 +114,9 @@ func PathToName(path string) string {
if path[0] == '/' || path[0] == '.' { if path[0] == '/' || path[0] == '.' {
path = path[1:] path = path[1:]
} }
path = strings.ReplaceAll(path, "/", "_") path = strings.ReplaceAll(path, "_", "-")
path = strings.ReplaceAll(path, ".", "_") path = strings.ReplaceAll(path, "/", "-")
path = strings.ReplaceAll(path, ".", "-")
return path return path
} }