From 918f1b845bdedb04aa9fd2723c5168e94f9fc754 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Thu, 17 Oct 2024 17:08:42 +0200 Subject: [PATCH] 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 --- .gitignore | 4 + Makefile | 11 +- README.md | 44 +- cmd/katenary/main.go | 83 ++- generator/chart.go | 32 +- generator/configMap.go | 55 +- generator/converter.go | 668 +++++++++++++------------ generator/deployment.go | 361 ++++++------- generator/extrafiles/readme.go | 46 +- generator/generator.go | 148 +++--- generator/ingress.go | 8 +- generator/katenaryLabels.go | 209 ++++---- generator/katenaryLabelsDoc.yaml | 51 +- generator/labelStructs/dependencies.go | 2 +- generator/labelStructs/ingress.go | 16 +- generator/rbac.go | 24 +- generator/secret.go | 24 +- generator/service.go | 10 +- generator/values.go | 43 +- generator/volume.go | 14 +- parser/main.go | 21 +- utils/utils.go | 10 +- 22 files changed, 991 insertions(+), 893 deletions(-) diff --git a/.gitignore b/.gitignore index 5d8f05a..618dbfd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ configs/ cover* .sq ./katenary +.aider* +.python_history +.bash_history +katenary diff --git a/Makefile b/Makefile index 5178f56..0ca9d0d 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ BINARIES=dist/katenary-linux-amd64 dist/katenary-linux-arm64 dist/katenary.exe d ASC_BINARIES=$(patsubst %,%.asc,$(BINARIES)) # defaults +BROWSER=$(shell command -v epiphany || echo xdg-open) SHELL := bash # strict mode .SHELLFLAGS := -eu -o pipefail -c @@ -35,6 +36,7 @@ MAKEFLAGS += --no-builtin-rules all: build + help: @cat < 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. - -Today, it's partially developped in collaboration with [Klee Group](https://www.kleegroup.com). Note that Katenary is -and **will stay an opensource and free (as freedom) project**. We are convinced that the best way to make it better is to +Today, it's partially developed in collaboration with [Klee Group](https://www.kleegroup.com). Note that Katenary is +and **will stay an open source and free (as freedom) project**. We are convinced that the best way to make it better is to share it with the community. The main developer is [Patrice FERLET](https://github.com/metal3d). @@ -45,7 +44,7 @@ You can use this commands on Linux: sh <(curl -sSL https://raw.githubusercontent.com/metal3d/katenary/master/install.sh) ``` -# Else... Build yourself +# Or, build yourself If you've got `podman` or `docker`, you can build `katenary` by using: @@ -54,6 +53,7 @@ make build ``` You can then install it with: + ```bash make install ``` @@ -76,13 +76,12 @@ make build GO=local GOOS=linux GOARCH=arm64 Then place the `katenary` binary file inside your PATH. - # 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. -E.g.: +E.g., ```bash # bash in ~/.bashrc file @@ -102,7 +101,7 @@ katenary completion fish | source # Usage -``` +```text Katenary is a tool to convert compose files to Helm Charts. Each [command] and subcommand has got an "help" and "--help" flag to show more information. @@ -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`) - > 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 - > katenary to build a correct helm chart. + > Katenary to build a correct helm chart. - What can be interpreted by Katenary: - - - Services with "image" section (cannot work with "build" section) - - **Named Volumes** are transformed to persistent volume claims - note that local volume will break the transformation -to Helm Chart because there is (for now) no way to make it working (see below for resolution) - - if `ports` and/or `expose` section, katenary will create Services and bind the port to the corresponding container port -- `depends_on` will add init containers to wait for the depending 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: + Example of a possible `docker-compose.yaml` file: ```yaml services: @@ -196,9 +184,9 @@ services: # 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 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 ``` -# What a name... +# What a name… Katenary is the stylized name of the project that comes from the "catenary" word. A catenary is a curve formed by a wire, rope, or chain hanging freely from two points that are not in the same vertical line. For example, the anchor chain between a boat and the anchor. -This "curved link" represents what we try to do, the project is a "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. diff --git a/cmd/katenary/main.go b/cmd/katenary/main.go index 3243a6f..1b67ca0 100644 --- a/cmd/katenary/main.go +++ b/cmd/katenary/main.go @@ -6,14 +6,13 @@ package main import ( "fmt" + "katenary/generator" + "katenary/utils" "os" "strings" "github.com/compose-spec/compose-go/cli" "github.com/spf13/cobra" - - "katenary/generator" - "katenary/utils" ) const longHelp = `Katenary is a tool to convert compose files to Helm Charts. @@ -133,6 +132,8 @@ func generateConvertCommand() *cobra.Command { var appVersion *string givenAppVersion := "" chartVersion := "0.1.0" + icon := "" + envFiles := []string{} convertCmd := &cobra.Command{ Use: "convert", @@ -148,17 +149,79 @@ func generateConvertCommand() *cobra.Command { HelmUpdate: helmdepUpdate, AppVersion: appVersion, ChartVersion: chartVersion, + Icon: icon, + EnvFiles: envFiles, }, dockerComposeFile...) }, } - convertCmd.Flags().BoolVarP(&force, "force", "f", force, "Force the overwrite of the chart directory") - convertCmd.Flags().BoolVarP(&helmdepUpdate, "helm-update", "u", helmdepUpdate, "Update helm dependencies if helm is installed") - convertCmd.Flags().StringSliceVarP(&profiles, "profile", "p", profiles, "Specify the profiles to use") - convertCmd.Flags().StringVarP(&outputDir, "output-dir", "o", outputDir, "Specify the output directory") - convertCmd.Flags().StringSliceVarP(&dockerComposeFile, "compose-file", "c", cli.DefaultFileNames, "Specify an alternate compose files - can be specified multiple times or use coma to separate them.\nNote 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().BoolVarP( + &force, + "force", + "f", + force, + "Force the overwrite of the chart directory", + ) + convertCmd.Flags().BoolVarP( + &helmdepUpdate, + "helm-update", + "u", + helmdepUpdate, + "Update helm dependencies if helm is installed", + ) + convertCmd.Flags().StringSliceVarP( + &profiles, + "profile", + "p", + profiles, + "Specify the profiles to use", + ) + convertCmd.Flags().StringVarP( + &outputDir, + "output-dir", + "o", + outputDir, + "Specify the output directory", + ) + convertCmd.Flags().StringSliceVarP( + &dockerComposeFile, + "compose-file", + "c", + cli.DefaultFileNames, + "Specify an alternate compose files - can be specified multiple times or use coma to separate them.\n"+ + "Note that overides files are also used whatever the files you specify here.\nThe overides files are:\n"+ + strings.Join(cli.DefaultOverrideFileNames, ", \n")+ + "\n", + ) + convertCmd.Flags().StringVarP( + &givenAppVersion, + "app-version", + "a", + "", + "Specify the app version (in Chart.yaml)", + ) + convertCmd.Flags().StringVarP( + &chartVersion, + "chart-version", + "v", + chartVersion, + "Specify the chart version (in Chart.yaml)", + ) + convertCmd.Flags().StringVarP( + &icon, + "icon", + "i", + "", + "Specify the icon (in Chart.yaml), use a valid URL, Helm does not support local files at this time.", + ) + convertCmd.Flags().StringSliceVarP( + &envFiles, + "env-file", + "e", + envFiles, + "Specify the env file to use additonnaly to the .env file. Can be specified multiple times.", + ) + return convertCmd } diff --git a/generator/chart.go b/generator/chart.go index 8f3a89f..8bd7580 100644 --- a/generator/chart.go +++ b/generator/chart.go @@ -2,27 +2,16 @@ package generator import ( "fmt" + "katenary/generator/labelStructs" + "katenary/utils" "log" "os" "path/filepath" "strings" "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. // This is used internally to generate the templates. type ChartTemplate struct { @@ -30,6 +19,18 @@ type ChartTemplate struct { 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 // tempaltes, values, versions, helpers... type HelmChart struct { @@ -38,6 +39,7 @@ type HelmChart struct { VolumeMounts map[string]any `yaml:"-"` composeHash *string `yaml:"-"` Name string `yaml:"name"` + Icon string `yaml:"icon,omitempty"` ApiVersion string `yaml:"apiVersion"` Version string `yaml:"version"` AppVersion string `yaml:"appVersion"` @@ -67,7 +69,7 @@ func (chart *HelmChart) SaveTemplates(templateDir string) { t := template.Content t = removeNewlinesInsideBrackets(t) t = removeUnwantedLines(t) - t = addModeline(t) + // t = addModeline(t) kind := utils.GetKind(name) var icon utils.Icon @@ -168,7 +170,7 @@ func (chart *HelmChart) generateConfigMapsAndSecrets(project *types.Project) err delete(s.Environment, k) } if len(s.Environment) > 0 { - cm := NewConfigMap(s, appName) + cm := NewConfigMap(s, appName, false) y, _ := cm.Yaml() name := cm.service.Name chart.Templates[name+".configmap.yaml"] = &ChartTemplate{ diff --git a/generator/configMap.go b/generator/configMap.go index 5b726d4..9c13bf8 100644 --- a/generator/configMap.go +++ b/generator/configMap.go @@ -1,6 +1,8 @@ package generator import ( + "katenary/generator/labelStructs" + "katenary/utils" "log" "os" "path/filepath" @@ -11,28 +13,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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. type FileMapUsage uint8 @@ -42,6 +24,23 @@ const ( 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. // Implements the DataMap interface. 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. // 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{} drop := map[string]bool{} labelValues := []string{} @@ -99,6 +98,10 @@ func NewConfigMap(service types.ServiceConfig, appName string) *ConfigMap { 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 if l, ok := service.Labels[LabelMapEnv]; ok { envmap, err := labelStructs.MapEnvFrom(l) @@ -155,11 +158,6 @@ func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string 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. func (c *ConfigMap) AddData(key, value string) { 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 func (c *ConfigMap) Yaml() ([]byte, error) { return yaml.Marshal(c) diff --git a/generator/converter.go b/generator/converter.go index 684301e..8490809 100644 --- a/generator/converter.go +++ b/generator/converter.go @@ -4,6 +4,10 @@ import ( "bytes" "errors" "fmt" + "katenary/generator/extrafiles" + "katenary/generator/labelStructs" + "katenary/parser" + "katenary/utils" "log" "os" "os/exec" @@ -13,13 +17,22 @@ import ( "time" "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 # # 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. // It calls Generate() to generate the chart and then write it to the disk. func Convert(config ConvertOptions, dockerComposeFile ...string) { @@ -59,7 +113,7 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) { } // parse the compose files - project, err := parser.Parse(config.Profiles, dockerComposeFile...) + project, err := parser.Parse(config.Profiles, config.EnvFiles, dockerComposeFile...) if err != nil { fmt.Println(err) os.Exit(1) @@ -109,6 +163,11 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) { os.Exit(1) } + // add icon from the command line + if config.Icon != "" { + chart.Icon = config.Icon + } + // write the templates to the disk chart.SaveTemplates(templateDir) @@ -132,141 +191,6 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) { 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 { 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. @@ -303,67 +227,63 @@ func addChartDoc(values []byte, project *types.Project) []byte { return []byte(chartDoc + strings.Join(lines, "\n")) } -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 -` - -func addImagePullPolicyHelp(values []byte) []byte { - // add imagePullPolicy help +func addCommentsToValues(values []byte) []byte { lines := strings.Split(string(values), "\n") for i, line := range lines { - if strings.Contains(line, "imagePullPolicy:") { + if strings.Contains(line, "ingress:") { 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 + // 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 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" -` - -func addResourceHelp(values []byte) []byte { - lines := strings.Split(string(values), "\n") - for i, line := range lines { - if strings.Contains(line, "resources:") { - spaces := utils.CountStartingSpaces(line) - spacesString := strings.Repeat(" ", spaces) - // indent resourceHelp comment - resourceHelp := strings.ReplaceAll(resourceHelp, "\n", "\n"+spacesString) - resourceHelp = strings.TrimRight(resourceHelp, " ") - resourceHelp = spacesString + resourceHelp - lines[i] = resourceHelp + line +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 []byte(strings.Join(lines, "\n")) + return values } -func addVariablesDoc(values []byte, project *types.Project) []byte { - lines := strings.Split(string(values), "\n") - +// 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 { - 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 { @@ -394,25 +314,38 @@ func addDocToVariable(service types.ServiceConfig, lines []string) []string { return lines } -const mainTagAppDoc = `This is the version of the main application. -Leave it to blank to use the Chart "AppVersion" value.` +func addImagePullPolicyHelp(values []byte) []byte { + // add imagePullPolicy help + lines := strings.Split(string(values), "\n") + for i, line := range lines { + if strings.Contains(line, "imagePullPolicy:") { + spaces := utils.CountStartingSpaces(line) + spacesString := strings.Repeat(" ", spaces) + // indent imagePullPolicyHelp comment + imagePullPolicyHelp := strings.ReplaceAll(imagePullPolicyHelp, "\n", "\n"+spacesString) + imagePullPolicyHelp = strings.TrimRight(imagePullPolicyHelp, " ") + imagePullPolicyHelp = spacesString + imagePullPolicyHelp + lines[i] = imagePullPolicyHelp + line + } + } + return []byte(strings.Join(lines, "\n")) +} -func addMainTagAppDoc(values []byte, project *types.Project) []byte { +func addImagePullSecretsHelp(values []byte) []byte { + // add imagePullSecrets help lines := strings.Split(string(values), "\n") - for _, service := range project.Services { - // read the label LabelMainApp - if v, ok := service.Labels[LabelMainApp]; !ok { - continue - } else if v == "false" || v == "no" || v == "0" { - continue - } else { - fmt.Printf("%s Adding main tag app doc %s\n", utils.IconConfig, service.Name) + 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 } - - lines = addMainAppDoc(lines, service) } - return []byte(strings.Join(lines, "\n")) } @@ -440,107 +373,84 @@ func addMainAppDoc(lines []string, service types.ServiceConfig) []string { return lines } -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) - }) -} - -var unwantedLines = []string{ - "creationTimestamp:", - "status:", -} - -func removeUnwantedLines(values []byte) []byte { +func addMainTagAppDoc(values []byte, project *types.Project) []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")) -} -// 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)) - } + // read the label LabelMainApp + if v, ok := service.Labels[LabelMainApp]; !ok { + continue + } else if v == "false" || v == "no" || v == "0" { + continue + } else { + fmt.Printf("%s Adding main tag app doc %s\n", utils.IconConfig, service.Name) + } + + lines = addMainAppDoc(lines, service) + } + + return []byte(strings.Join(lines, "\n")) +} + +// 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)...) +} + +func addResourceHelp(values []byte) []byte { + lines := strings.Split(string(values), "\n") + for i, line := range lines { + if strings.Contains(line, "resources:") { + spaces := utils.CountStartingSpaces(line) + spacesString := strings.Repeat(" ", spaces) + // indent resourceHelp comment + resourceHelp := strings.ReplaceAll(resourceHelp, "\n", "\n"+spacesString) + resourceHelp = strings.TrimRight(resourceHelp, " ") + resourceHelp = spacesString + resourceHelp + lines[i] = resourceHelp + line } } - 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 + return []byte(strings.Join(lines, "\n")) } -// 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) +// 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 + } } - // run "helm dependency update" - cmd := exec.Command(helm, "dependency", "update", config.OutputDir) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() + return []byte(strings.Join(lines, "\n")) } -// 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() -} +func addVariablesDoc(values []byte, project *types.Project) []byte { + lines := strings.Split(string(values), "\n") -// keyRegExp checks if the line starts by a # -var keyRegExp = regexp.MustCompile(`^\s*[^#]+:.*`) + for _, service := range project.Services { + lines = addDocToVariable(service, lines) + } + return []byte(strings.Join(lines, "\n")) +} // addYAMLSelectorPath adds a selector path to the yaml file for each key // as comment. E.g. foo.ingress.host @@ -587,14 +497,40 @@ func addYAMLSelectorPath(values []byte) []byte { return []byte(strings.Join(toReturn, "\n")) } -func writeContent(path string, content []byte) { - f, err := os.Create(path) +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(utils.IconFailure, err) + fmt.Println(err) os.Exit(1) } - defer f.Close() - f.Write(content) + // concat chart adding a comment with hash of services on top + yamlChart = append([]byte(fmt.Sprintf("# compose hash (sha1): %s\n", *chart.composeHash)), yamlChart...) + // add the list of compose files + files := []string{} + for _, file := range project.ComposeFiles { + base := filepath.Base(file) + files = append(files, base) + } + yamlChart = append([]byte(fmt.Sprintf("# compose files: %s\n", strings.Join(files, ", "))), yamlChart...) + // add generated date + yamlChart = append([]byte(fmt.Sprintf("# generated at: %s\n", time.Now().Format(time.RFC3339))), yamlChart...) + + // document Chart.yaml file + yamlChart = addChartDoc(yamlChart, project) + + writeContent(chartPath, yamlChart) +} + +func buildNotesFile(project *types.Project, notesPath string) { + // get the list of services to write in the notes + services := make([]string, 0) + for _, service := range project.Services { + services = append(services, service.Name) + } + // write the notes to the disk + notes := extrafiles.NotesFile(services) + writeContent(notesPath, []byte(notes)) } func buildValues(chart *HelmChart, project *types.Project, valuesPath string) { @@ -622,42 +558,6 @@ func buildValues(chart *HelmChart, project *types.Project, valuesPath string) { 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) { executeAndHandleError := func(fn func(ConvertOptions) error, config ConvertOptions, message string) { if err := fn(config); err != nil { @@ -672,3 +572,107 @@ func callHelmUpdate(config ConvertOptions) { 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 +} diff --git a/generator/deployment.go b/generator/deployment.go index 5e2c637..bad1f6f 100644 --- a/generator/deployment.go +++ b/generator/deployment.go @@ -2,6 +2,8 @@ package generator import ( "fmt" + "katenary/generator/labelStructs" + "katenary/utils" "log" "os" "path/filepath" @@ -14,9 +16,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" - - "katenary/generator/labelStructs" - "katenary/utils" ) var _ Yaml = (*Deployment)(nil) @@ -106,32 +105,6 @@ func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment { 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. func (d *Deployment) AddContainer(service types.ServiceConfig) { 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) } +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. func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress { 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) { // find the volume in the binded deployment 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. func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string) { 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 } -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. func (d *Deployment) Yaml() ([]byte, error) { serviceName := d.service.Name @@ -489,11 +375,13 @@ func (d *Deployment) Yaml() ([]byte, error) { spaces := "" volumeName := "" + nameDirective := "name: " + // this loop add condition for each volume mount for line, volume := range content { // find the volume name for i := line; i < len(content); i++ { - if strings.Contains(content[i], "name: ") { + if strings.Contains(content[i], nameDirective) { volumeName = strings.TrimSpace(strings.Replace(content[i], "name: ", "", 1)) break } @@ -511,7 +399,7 @@ func (d *Deployment) Yaml() ([]byte, error) { content[line] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + volumeName + `.enabled }}` + "\n" + volume changing = true } - if strings.Contains(volume, "name: ") && changing { + if strings.Contains(volume, nameDirective) && changing { content[line] = volume + "\n" + spaces + "{{- end }}" changing = false } @@ -624,7 +512,120 @@ func (d *Deployment) Yaml() ([]byte, error) { return []byte(strings.Join(content, "\n")), nil } -// Filename returns the filename of the deployment. -func (d *Deployment) Filename() string { - return d.service.Name + ".deployment.yaml" +func (d *Deployment) appendDirectoryToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) { + pathnme := utils.PathToName(volume.Source) + if _, ok := d.configMaps[pathnme]; !ok { + d.configMaps[pathnme] = &ConfigMapMount{ + mountPath: []mountPathConfig{}, + } + } + + // TODO: make it recursive to add all files in the directory and subdirectories + _, err := os.ReadDir(volume.Source) + if err != nil { + log.Fatal(err) + } + cm := NewConfigMapFromDirectory(service, appName, volume.Source) + d.configMaps[pathnme] = &ConfigMapMount{ + configMap: cm, + mountPath: append(d.configMaps[pathnme].mountPath, mountPathConfig{ + mountPath: volume.Target, + }), + } +} + +func (d *Deployment) appendFileToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) { + // In case of a file, add it to the configmap and use "subPath" to mount it + // Note that the volumes and volume mounts are not added to the deployment yet, they will be added later + // in generate.go + dirname := filepath.Dir(volume.Source) + pathname := utils.PathToName(dirname) + var cm *ConfigMap + if v, ok := d.configMaps[pathname]; !ok { + cm = NewConfigMap(*d.service, appName, true) + cm.usage = FileMapUsageFiles + cm.path = dirname + cm.Name = utils.TplName(service.Name, appName) + "-" + pathname + d.configMaps[pathname] = &ConfigMapMount{ + configMap: cm, + mountPath: []mountPathConfig{{ + mountPath: volume.Target, + subPath: filepath.Base(volume.Source), + }}, + } + } else { + cm = v.configMap + mp := d.configMaps[pathname].mountPath + mp = append(mp, mountPathConfig{ + mountPath: volume.Target, + subPath: filepath.Base(volume.Source), + }) + d.configMaps[pathname].mountPath = mp + + } + 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) + } + } } diff --git a/generator/extrafiles/readme.go b/generator/extrafiles/readme.go index 01c54a2..b3201fe 100644 --- a/generator/extrafiles/readme.go +++ b/generator/extrafiles/readme.go @@ -11,14 +11,35 @@ import ( "gopkg.in/yaml.v3" ) +//go:embed readme.tpl +var readmeTemplate string + type chart struct { Name string Description string Values []string } -//go:embed readme.tpl -var readmeTemplate 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 + } + } +} // ReadMeFile returns the content of the README.md file. 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() } - -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 - } - } -} diff --git a/generator/generator.go b/generator/generator.go index 5fd3c00..a9eaf72 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -162,49 +162,6 @@ func Generate(project *types.Project) (*HelmChart, error) { 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. func serviceIsMain(service types.ServiceConfig) bool { if main, ok := service.Labels[LabelMainApp]; ok { @@ -213,37 +170,6 @@ func serviceIsMain(service types.ServiceConfig) bool { 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) { // add the bound configMaps files to the deployment containers var d *Deployment @@ -292,6 +218,80 @@ func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceC 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. func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, deployments map[string]*Deployment) bool { // if the service has volumes, and it has "same-pod" label diff --git a/generator/ingress.go b/generator/ingress.go index 669593a..03559d2 100644 --- a/generator/ingress.go +++ b/generator/ingress.go @@ -119,6 +119,10 @@ func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress { return ing } +func (ingress *Ingress) Filename() string { + return ingress.service.Name + ".ingress.yaml" +} + func (ingress *Ingress) Yaml() ([]byte, error) { serviceName := ingress.service.Name ret, err := yaml.Marshal(ingress) @@ -159,7 +163,3 @@ func (ingress *Ingress) Yaml() ([]byte, error) { ret = []byte(strings.Join(out, "\n")) return ret, nil } - -func (ingress *Ingress) Filename() string { - return ingress.service.Name + ".ingress.yaml" -} diff --git a/generator/katenaryLabels.go b/generator/katenaryLabels.go index c1f3448..a373e7c 100644 --- a/generator/katenaryLabels.go +++ b/generator/katenaryLabels.go @@ -4,6 +4,7 @@ import ( "bytes" _ "embed" "fmt" + "katenary/utils" "regexp" "sort" "strings" @@ -11,37 +12,10 @@ import ( "text/template" "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" -func Prefix() string { - return katenaryLabelPrefix -} - // Known labels. const ( LabelMainApp Label = katenaryLabelPrefix + "/main-app" @@ -60,16 +34,47 @@ const ( 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() { if err := yaml.Unmarshal(labelFullHelpYAML, &labelFullHelp); err != nil { panic(err) } } -func labelName(name string) Label { - return Label(katenaryLabelPrefix + "/" + name) -} - // Generate the help for the labels. func GetLabelHelp(asMarkdown bool) string { names := GetLabelNames() // sorted @@ -79,73 +84,6 @@ func GetLabelHelp(asMarkdown bool) string { 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 \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. func GetLabelHelpFor(labelname string, asMarkdown bool) string { help, ok := labelFullHelp[labelname] @@ -202,14 +140,71 @@ func GetLabelHelpFor(labelname string, asMarkdown bool) string { return buf.String() } -// GetLabelNames returns a sorted list of all katenary label names. -func GetLabelNames() []string { - var names []string - for name := range labelFullHelp { - names = append(names, name) +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 } - sort.Strings(names) - 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 \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 { @@ -234,3 +229,7 @@ Example: {{ .Help.Example }} ` } + +func Prefix() string { + return katenaryLabelPrefix +} diff --git a/generator/katenaryLabelsDoc.yaml b/generator/katenaryLabelsDoc.yaml index 9553343..f4b555f 100644 --- a/generator/katenaryLabelsDoc.yaml +++ b/generator/katenaryLabelsDoc.yaml @@ -1,7 +1,7 @@ # Labels documentation. # # To create a label documentation: -# +# # "labelname": # type: the label type (bool, string, array, object...) # short: a short description @@ -13,23 +13,23 @@ # This file is embed in the Katenary binary and parsed in kanetaryLabels.go init() function. # # Note: -# - The short and long texts are parsed with text/template, so you can use template syntax. -# That means that if you want to display double brackets, you need to enclose them to -# prevent template to try to expand the content, for example : +# - The short and long texts are parsed with text/template, so you can use template syntax. +# That means that if you want to display double brackets, you need to enclose them to +# prevent template to try to expand the content, for example : # This is an {{ "{{ example }}" }}. # # This will display "This is an {{ exemple }}" in the output. # - Use {{ .KatenaryPrefix }} to let Katenary replace it with the label prefix (e.g. "katenary.v3") -"main-app": +"main-app": short: "Mark the service as the main app." long: |- This makes the service to be the main application. Its image tag is considered to be the - + Chart appVersion and to be the defaultvalue in Pod container image attribute. - + !!! Warning This label cannot be repeated in others services. If this label is set in more than one service as true, Katenary will return an error. @@ -43,17 +43,17 @@ {{ .KatenaryPrefix }}/main-app: true type: "bool" -"values": +"values": short: "Environment variables to be added to the values.yaml" long: |- By default, all environment variables in the "env" and environment files are added to configmaps with the static values set. This label allows adding environment variables to the values.yaml file. - + Note that the value inside the configmap is {{ "{{ tpl vaname . }}" }}, so you can set the value to a template that will be rendered with the values.yaml file. - + The value can be set with a documentation. This may help to understand the purpose of the variable. example: |- @@ -75,7 +75,7 @@ "secrets": short: "Env vars to be set as secrets." - long: |- + long: |- This label allows setting the environment variables as secrets. The variable is removed from the environment and added to a secret object. @@ -102,7 +102,7 @@ - 8081 type: "list of uint32" -"ingress": +"ingress": short: "Ingress rules to be added to the service." long: |- Declare an ingress rule for the service. The port should be exposed or @@ -114,7 +114,7 @@ hostname: mywebsite.com (optional) type: "object" -"map-env": +"map-env": short: "Map env vars from the service to the deployment." long: |- Because you may need to change the variable for Kubernetes, this label @@ -136,8 +136,8 @@ type: "object" "health-check": - short: "Health check to be added to the deployment." - long: "Health check to be added to the deployment." + short: "Health check to be added to the deployment." + long: "Health check to be added to the deployment." example: |- labels: {{ .KatenaryPrefix }}/health-check: |- @@ -146,12 +146,12 @@ port: 8080 type: "object" -"same-pod": +"same-pod": short: "Move the same-pod deployment to the target deployment." long: |- This will make the service to be included in another service pod. Some services must work together in the same pod, like a sidecar or a proxy or nginx + php-fpm. - + Note that volume and VolumeMount are copied from the source to the target deployment. example: |- @@ -169,7 +169,7 @@ long: |- This replaces the default comment in values.yaml file to the given description. It is useful to document the service and configuration. - + The value can be set with a documentation in multiline format. example: |- labels: @@ -179,12 +179,12 @@ type: "string" "ignore": - short: "Ignore the service" - long: "Ingoring a service to not be exported in helm chart." + short: "Ignore the service" + long: "Ingoring a service to not be exported in helm chart." example: "labels:\n {{ .KatenaryPrefix }}/ignore: \"true\"" - type: "bool" + type: "bool" -"dependencies": +"dependencies": short: "Add Helm dependencies to the service." long: |- Set the service to be, actually, a Helm dependency. This means that the @@ -232,7 +232,7 @@ service directory. If it is a directory, all files inside it are added to the ConfigMap. - + If the directory as subdirectories, so one configmap per subpath are created. !!! Warning @@ -248,11 +248,11 @@ - ./conf.d type: "list of strings" -"cronjob": +"cronjob": short: "Create a cronjob from the service." long: |- This adds a cronjob to the chart. - + The label value is a YAML object with the following attributes: - command: the command to be executed - schedule: the cron schedule (cron format or @every where "every" is a @@ -284,4 +284,5 @@ # defined inside this service too {{ .KatenaryPrefix }}/env-from: |- - myservice1 + # vim: ft=gotmpl.yaml diff --git a/generator/labelStructs/dependencies.go b/generator/labelStructs/dependencies.go index bc94b30..71dde8c 100644 --- a/generator/labelStructs/dependencies.go +++ b/generator/labelStructs/dependencies.go @@ -4,11 +4,11 @@ import "gopkg.in/yaml.v3" // Dependency is a dependency of a chart to other charts. type Dependency struct { + Values map[string]any `yaml:"-"` Name string `yaml:"name"` Version string `yaml:"version"` Repository string `yaml:"repository"` 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. diff --git a/generator/labelStructs/ingress.go b/generator/labelStructs/ingress.go index b01cd36..22c5b01 100644 --- a/generator/labelStructs/ingress.go +++ b/generator/labelStructs/ingress.go @@ -3,18 +3,12 @@ package labelStructs import "gopkg.in/yaml.v3" 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"` - // Annotations is a list of key-value pairs to add to the ingress. + Port *int32 `yaml:"port,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. diff --git a/generator/rbac.go b/generator/rbac.go index f8295ab..f314ab9 100644 --- a/generator/rbac.go +++ b/generator/rbac.go @@ -102,38 +102,38 @@ type RoleBinding struct { service *types.ServiceConfig } -func (r *RoleBinding) Yaml() ([]byte, error) { - return yaml.Marshal(r) -} - func (r *RoleBinding) Filename() string { return r.service.Name + ".rolebinding.yaml" } +func (r *RoleBinding) Yaml() ([]byte, error) { + return yaml.Marshal(r) +} + // Role is a kubernetes Role. type Role struct { *rbacv1.Role service *types.ServiceConfig } -func (r *Role) Yaml() ([]byte, error) { - return yaml.Marshal(r) -} - func (r *Role) Filename() string { return r.service.Name + ".role.yaml" } +func (r *Role) Yaml() ([]byte, error) { + return yaml.Marshal(r) +} + // ServiceAccount is a kubernetes ServiceAccount. type ServiceAccount struct { *corev1.ServiceAccount service *types.ServiceConfig } -func (r *ServiceAccount) Yaml() ([]byte, error) { - return yaml.Marshal(r) -} - func (r *ServiceAccount) Filename() string { return r.service.Name + ".serviceaccount.yaml" } + +func (r *ServiceAccount) Yaml() ([]byte, error) { + return yaml.Marshal(r) +} diff --git a/generator/secret.go b/generator/secret.go index e26869b..bf1c22f 100644 --- a/generator/secret.go +++ b/generator/secret.go @@ -76,13 +76,6 @@ func NewSecret(service types.ServiceConfig, appName string) *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. func (s *Secret) AddData(key, value string) { if value == "" { @@ -91,6 +84,18 @@ func (s *Secret) AddData(key, value string) { 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. func (s *Secret) Yaml() ([]byte, error) { y, err := yaml.Marshal(s) @@ -106,8 +111,3 @@ func (s *Secret) Yaml() ([]byte, error) { return y, nil } - -// Filename returns the filename of the secret. -func (s *Secret) Filename() string { - return s.service.Name + ".secret.yaml" -} diff --git a/generator/service.go b/generator/service.go index 1951a33..a0cc2b7 100644 --- a/generator/service.go +++ b/generator/service.go @@ -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. func (s *Service) Yaml() ([]byte, error) { y, err := yaml.Marshal(s) @@ -88,8 +93,3 @@ func (s *Service) Yaml() ([]byte, error) { return y, err } - -// Filename returns the filename of the service. -func (s *Service) Filename() string { - return s.service.Name + ".service.yaml" -} diff --git a/generator/values.go b/generator/values.go index 59b4ca5..7e2b380 100644 --- a/generator/values.go +++ b/generator/values.go @@ -43,14 +43,6 @@ type Value struct { 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. // 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 tag := "" + split := strings.Split(service.Image, ":") - v.Repository = &RepositoryValue{ - Image: split[0], + if len(split) == 1 { + v.Repository = &RepositoryValue{ + 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. if len(main) > 0 && !main[0] { if len(split) > 1 { - tag = split[1] + tag = split[len(split)-1] } v.Repository.Tag = tag } else { @@ -82,6 +81,15 @@ func NewValue(service types.ServiceConfig, main ...bool) *Value { 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. func (v *Value) AddPersistence(volumeName string) { if v.Persistence == nil { @@ -95,11 +103,10 @@ func (v *Value) AddPersistence(volumeName string) { } } -func (v *Value) AddIngress(host, path string) { - v.Ingress = &IngressValue{ - Enabled: true, - Host: host, - Path: path, - Class: "-", - } +// 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"` } diff --git a/generator/volume.go b/generator/volume.go index 39a02eb..49f189b 100644 --- a/generator/volume.go +++ b/generator/volume.go @@ -12,10 +12,10 @@ import ( "katenary/utils" ) -var _ Yaml = (*VolumeClaim)(nil) - const persistenceKey = "persistence" +var _ Yaml = (*VolumeClaim)(nil) + // VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim. type VolumeClaim struct { *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. func (v *VolumeClaim) Yaml() ([]byte, error) { serviceName := v.service.Name @@ -122,8 +127,3 @@ func (v *VolumeClaim) Yaml() ([]byte, error) { return out, nil } - -// Filename returns the suggested filename for a VolumeClaim. -func (v *VolumeClaim) Filename() string { - return v.service.Name + "." + v.volumeName + ".volumeclaim.yaml" -} diff --git a/parser/main.go b/parser/main.go index fe25920..aea58a9 100644 --- a/parser/main.go +++ b/parser/main.go @@ -2,6 +2,9 @@ package parser import ( + "log" + "path/filepath" + "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/types" ) @@ -12,6 +15,7 @@ func init() { "compose.katenary.yml", "compose.katenary.yaml", }, cli.DefaultOverrideFileNames...) + // add podman-compose files cli.DefaultOverrideFileNames = append(cli.DefaultOverrideFileNames, []string{ "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. -func Parse(profiles []string, dockerComposeFile ...string) (*types.Project, error) { +func Parse(profiles []string, envFiles []string, dockerComposeFile ...string) (*types.Project, error) { if len(dockerComposeFile) == 0 { 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, cli.WithProfiles(profiles), + cli.WithInterpolation(true), cli.WithDefaultConfigPath, + cli.WithEnvFiles(envFiles...), cli.WithOsEnv, cli.WithDotEnv, cli.WithNormalization(true), - cli.WithInterpolation(true), cli.WithResolvedPaths(false), ) if err != nil { diff --git a/utils/utils.go b/utils/utils.go index 29081b7..2ed7428 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -20,6 +20,11 @@ func TplName(serviceName, appname string, suffix ...string) string { if len(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, "-") } @@ -109,8 +114,9 @@ func PathToName(path string) string { if path[0] == '/' || path[0] == '.' { 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 }