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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -18,3 +18,7 @@ configs/
|
||||
cover*
|
||||
.sq
|
||||
./katenary
|
||||
.aider*
|
||||
.python_history
|
||||
.bash_history
|
||||
katenary
|
||||
|
11
Makefile
11
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 <<EOF | fold -s -w 80
|
||||
=== HELP ===
|
||||
@@ -166,7 +168,14 @@ serve-doc: __label_doc
|
||||
tests: test
|
||||
test:
|
||||
@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
|
||||
@rm -f release.id
|
||||
|
44
README.md
44
README.md
@@ -8,10 +8,10 @@
|
||||
|
||||
🚀 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.
|
||||
|
||||
🛠️ 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.
|
||||
|
||||
💡 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"
|
||||
> 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 <name_without_prefix>
|
||||
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.
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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{
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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 <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.
|
||||
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 <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 {
|
||||
@@ -234,3 +229,7 @@ Example:
|
||||
{{ .Help.Example }}
|
||||
`
|
||||
}
|
||||
|
||||
func Prefix() string {
|
||||
return katenaryLabelPrefix
|
||||
}
|
||||
|
@@ -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: |-
|
||||
@@ -179,10 +179,10 @@
|
||||
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":
|
||||
short: "Add Helm dependencies to the service."
|
||||
@@ -284,4 +284,5 @@
|
||||
# defined inside this service too
|
||||
{{ .KatenaryPrefix }}/env-from: |-
|
||||
- myservice1
|
||||
|
||||
# vim: ft=gotmpl.yaml
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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"`
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user