Go to Katenary V3
This is the next-gen of Katenary
This commit is contained in:
60
generator/chart.go
Normal file
60
generator/chart.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package generator
|
||||
|
||||
// Dependency is a dependency of a chart to other charts.
|
||||
type Dependency struct {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// TODO: maybe we can set it private.
|
||||
type ChartTemplate struct {
|
||||
Content []byte
|
||||
Servicename string
|
||||
}
|
||||
|
||||
// HelmChart is a Helm Chart representation. It contains all the
|
||||
// tempaltes, values, versions, helpers...
|
||||
type HelmChart struct {
|
||||
Name string `yaml:"name"`
|
||||
ApiVersion string `yaml:"apiVersion"`
|
||||
Version string `yaml:"version"`
|
||||
AppVersion string `yaml:"appVersion"`
|
||||
Description string `yaml:"description"`
|
||||
Dependencies []Dependency `yaml:"dependencies,omitempty"`
|
||||
Templates map[string]*ChartTemplate `yaml:"-"` // do not export to yaml
|
||||
Helper string `yaml:"-"` // do not export to yaml
|
||||
Values map[string]any `yaml:"-"` // do not export to yaml
|
||||
VolumeMounts map[string]any `yaml:"-"` // do not export to yaml
|
||||
composeHash *string `yaml:"-"` // do not export to yaml
|
||||
}
|
||||
|
||||
// NewChart creates a new empty chart with the given name.
|
||||
func NewChart(name string) *HelmChart {
|
||||
return &HelmChart{
|
||||
Name: name,
|
||||
Templates: make(map[string]*ChartTemplate, 0),
|
||||
Description: "A Helm chart for " + name,
|
||||
ApiVersion: "v2",
|
||||
Version: "",
|
||||
AppVersion: "", // set to 0.1.0 by default if no "main-app" label is found
|
||||
Values: map[string]any{
|
||||
"pullSecrets": []string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertOptions are the options to convert a compose project to a helm chart.
|
||||
type ConvertOptions struct {
|
||||
Force bool // Force the chart directory deletion if it already exists.
|
||||
OutputDir string // The output directory of the chart.
|
||||
Profiles []string // Profile to use for the conversion.
|
||||
HelmUpdate bool // If true, the "helm dep update" command will be run after the chart generation.
|
||||
AppVersion *string // Set the chart "appVersion" field. If nil, the version will be set to 0.1.0.
|
||||
ChartVersion string // Set the chart "version" field.
|
||||
}
|
224
generator/configMap.go
Normal file
224
generator/configMap.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
goyaml "gopkg.in/yaml.v3"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// 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 string, 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
|
||||
|
||||
// FileMapUsage constants.
|
||||
const (
|
||||
FileMapUsageConfigMap FileMapUsage = iota // pure configmap for key:values.
|
||||
FileMapUsageFiles // files in a configmap.
|
||||
)
|
||||
|
||||
// ConfigMap is a kubernetes ConfigMap.
|
||||
// Implements the DataMap interface.
|
||||
type ConfigMap struct {
|
||||
*corev1.ConfigMap
|
||||
service *types.ServiceConfig
|
||||
usage FileMapUsage
|
||||
path string
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
done := map[string]bool{}
|
||||
drop := map[string]bool{}
|
||||
secrets := []string{}
|
||||
labelValues := []string{}
|
||||
|
||||
cm := &ConfigMap{
|
||||
service: &service,
|
||||
ConfigMap: &corev1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Data: make(map[string]string),
|
||||
},
|
||||
}
|
||||
|
||||
// get the secrets from the labels
|
||||
if v, ok := service.Labels[LABEL_SECRETS]; ok {
|
||||
err := yaml.Unmarshal([]byte(v), &secrets)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// drop the secrets from the environment
|
||||
for _, secret := range secrets {
|
||||
drop[secret] = true
|
||||
}
|
||||
}
|
||||
// get the label values from the labels
|
||||
varDescriptons := utils.GetValuesFromLabel(service, LABEL_VALUES)
|
||||
for value := range varDescriptons {
|
||||
labelValues = append(labelValues, value)
|
||||
}
|
||||
|
||||
// change the environment variables to the values defined in the values.yaml
|
||||
for _, value := range labelValues {
|
||||
if _, ok := service.Environment[value]; !ok {
|
||||
done[value] = true
|
||||
continue
|
||||
}
|
||||
//val := `{{ tpl .Values.` + service.Name + `.environment.` + value + ` $ }}`
|
||||
val := utils.TplValue(service.Name, "environment."+value)
|
||||
service.Environment[value] = &val
|
||||
}
|
||||
|
||||
// remove the variables that are already defined in the environment
|
||||
if l, ok := service.Labels[LABEL_MAP_ENV]; ok {
|
||||
envmap := make(map[string]string)
|
||||
if err := goyaml.Unmarshal([]byte(l), &envmap); err != nil {
|
||||
log.Fatal("Error parsing map-env", err)
|
||||
}
|
||||
for key, value := range envmap {
|
||||
cm.AddData(key, strings.ReplaceAll(value, "__APP__", appName))
|
||||
done[key] = true
|
||||
}
|
||||
}
|
||||
for key, env := range service.Environment {
|
||||
if _, ok := done[key]; ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := drop[key]; ok {
|
||||
continue
|
||||
}
|
||||
cm.AddData(key, *env)
|
||||
}
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// NewConfigMapFromFiles creates a new ConfigMap from a compose service. This path is the path to the
|
||||
// file or directory. If the path is a directory, all files in the directory are added to the ConfigMap.
|
||||
// Each subdirectory are ignored. Note that the Generate() function will create the subdirectories ConfigMaps.
|
||||
func NewConfigMapFromFiles(service types.ServiceConfig, appName string, path string) *ConfigMap {
|
||||
normalized := path
|
||||
normalized = strings.TrimLeft(normalized, ".")
|
||||
normalized = strings.TrimLeft(normalized, "/")
|
||||
normalized = regexp.MustCompile(`[^a-zA-Z0-9-]+`).ReplaceAllString(normalized, "-")
|
||||
|
||||
cm := &ConfigMap{
|
||||
path: path,
|
||||
service: &service,
|
||||
usage: FileMapUsageFiles,
|
||||
ConfigMap: &corev1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName) + "-" + normalized,
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Data: make(map[string]string),
|
||||
},
|
||||
}
|
||||
// cumulate the path to the WorkingDir
|
||||
path = filepath.Join(service.WorkingDir, path)
|
||||
path = filepath.Clean(path)
|
||||
cm.AppendDir(path)
|
||||
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 string, value string) {
|
||||
c.Data[key] = value
|
||||
}
|
||||
|
||||
// AddFile adds files from given path to the configmap. It is not recursive, to add all files in a directory,
|
||||
// you need to call this function for each subdirectory.
|
||||
func (c *ConfigMap) AppendDir(path string) {
|
||||
// read all files in the path and add them to the configmap
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Path %s does not exist\n", path)
|
||||
|
||||
}
|
||||
// recursively read all files in the path and add them to the configmap
|
||||
if stat.IsDir() {
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(path, file.Name())
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// remove the path from the file
|
||||
filename := filepath.Base(path)
|
||||
c.AddData(filename, string(content))
|
||||
}
|
||||
} else {
|
||||
// add the file to the configmap
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
c.AddData(filepath.Base(path), string(content))
|
||||
}
|
||||
}
|
||||
|
||||
// Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path.
|
||||
func (c *ConfigMap) Filename() string {
|
||||
switch c.usage {
|
||||
case FileMapUsageFiles:
|
||||
return filepath.Join(c.service.Name, "statics", c.path, "configmap.yaml")
|
||||
default:
|
||||
return c.service.Name + ".configmap.yaml"
|
||||
}
|
||||
}
|
||||
|
||||
// Yaml returns the yaml representation of the configmap
|
||||
func (c *ConfigMap) Yaml() ([]byte, error) {
|
||||
return yaml.Marshal(c)
|
||||
}
|
@@ -1,200 +0,0 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/helm"
|
||||
"katenary/logger"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
)
|
||||
|
||||
// Generate a container in deployment with all needed objects (volumes, secrets, env, ...).
|
||||
// The deployName shoud be the name of the deployment, we cannot get it from Metadata as this is a variable name.
|
||||
func newContainerForDeployment(
|
||||
deployName, containerName string,
|
||||
deployment *helm.Deployment,
|
||||
s *types.ServiceConfig,
|
||||
fileGeneratorChan HelmFileGenerator) *helm.Container {
|
||||
|
||||
buildCrontab(deployName, deployment, s, fileGeneratorChan)
|
||||
|
||||
container := helm.NewContainer(containerName, s.Image, s.Environment, s.Labels)
|
||||
|
||||
applyEnvMapLabel(s, container)
|
||||
if secretFile := setSecretVar(containerName, s, container); secretFile != nil {
|
||||
fileGeneratorChan <- secretFile
|
||||
container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{
|
||||
"secretRef": {
|
||||
"name": secretFile.Metadata().Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
setEnvToValues(containerName, s, container)
|
||||
prepareContainer(container, s, containerName)
|
||||
prepareEnvFromFiles(deployName, s, container, fileGeneratorChan)
|
||||
|
||||
// add the container in deployment
|
||||
if deployment.Spec.Template.Spec.Containers == nil {
|
||||
deployment.Spec.Template.Spec.Containers = make([]*helm.Container, 0)
|
||||
}
|
||||
deployment.Spec.Template.Spec.Containers = append(
|
||||
deployment.Spec.Template.Spec.Containers,
|
||||
container,
|
||||
)
|
||||
|
||||
// add the volumes
|
||||
if deployment.Spec.Template.Spec.Volumes == nil {
|
||||
deployment.Spec.Template.Spec.Volumes = make([]map[string]interface{}, 0)
|
||||
}
|
||||
// manage LABEL_VOLUMEFROM
|
||||
addVolumeFrom(deployment, container, s)
|
||||
// and then we can add other volumes
|
||||
deployment.Spec.Template.Spec.Volumes = append(
|
||||
deployment.Spec.Template.Spec.Volumes,
|
||||
prepareVolumes(deployName, containerName, s, container, fileGeneratorChan)...,
|
||||
)
|
||||
|
||||
// add init containers
|
||||
if deployment.Spec.Template.Spec.InitContainers == nil {
|
||||
deployment.Spec.Template.Spec.InitContainers = make([]*helm.Container, 0)
|
||||
}
|
||||
deployment.Spec.Template.Spec.InitContainers = append(
|
||||
deployment.Spec.Template.Spec.InitContainers,
|
||||
prepareInitContainers(containerName, s, container)...,
|
||||
)
|
||||
|
||||
// check if there is containerPort assigned in label, add it, and do
|
||||
// not create service for this.
|
||||
if ports, ok := s.Labels[helm.LABEL_CONTAINER_PORT]; ok {
|
||||
for _, port := range strings.Split(ports, ",") {
|
||||
func(port string, container *helm.Container, s *types.ServiceConfig) {
|
||||
port = strings.TrimSpace(port)
|
||||
if port == "" {
|
||||
return
|
||||
}
|
||||
portNumber, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// avoid already declared ports
|
||||
for _, p := range s.Ports {
|
||||
if int(p.Target) == portNumber {
|
||||
return
|
||||
}
|
||||
}
|
||||
container.Ports = append(container.Ports, &helm.ContainerPort{
|
||||
Name: deployName + "-" + port,
|
||||
ContainerPort: portNumber,
|
||||
})
|
||||
}(port, container, s)
|
||||
}
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
// prepareContainer assigns image, command, env, and labels to a container.
|
||||
func prepareContainer(container *helm.Container, service *types.ServiceConfig, servicename string) {
|
||||
// if there is no image name, this should fail!
|
||||
if service.Image == "" {
|
||||
log.Fatal(ICON_PACKAGE+" No image name for service ", servicename)
|
||||
}
|
||||
|
||||
// Get the image tag
|
||||
imageParts := strings.Split(service.Image, ":")
|
||||
tag := ""
|
||||
if len(imageParts) == 2 {
|
||||
container.Image = imageParts[0]
|
||||
tag = imageParts[1]
|
||||
}
|
||||
|
||||
vtag := ".Values." + servicename + ".repository.tag"
|
||||
container.Image = `{{ .Values.` + servicename + `.repository.image }}` +
|
||||
`{{ if ne ` + vtag + ` "" }}:{{ ` + vtag + ` }}{{ end }}`
|
||||
container.Command = service.Command
|
||||
AddValues(servicename, map[string]EnvVal{
|
||||
"repository": map[string]EnvVal{
|
||||
"image": imageParts[0],
|
||||
"tag": tag,
|
||||
},
|
||||
})
|
||||
prepareProbes(servicename, service, container)
|
||||
generateContainerPorts(service, servicename, container)
|
||||
}
|
||||
|
||||
// generateContainerPorts add the container ports of a service.
|
||||
func generateContainerPorts(s *types.ServiceConfig, name string, container *helm.Container) {
|
||||
|
||||
exists := make(map[int]string)
|
||||
for _, port := range s.Ports {
|
||||
portName := name
|
||||
for _, n := range exists {
|
||||
if name == n {
|
||||
portName = fmt.Sprintf("%s-%d", name, port.Target)
|
||||
}
|
||||
}
|
||||
container.Ports = append(container.Ports, &helm.ContainerPort{
|
||||
Name: portName,
|
||||
ContainerPort: int(port.Target),
|
||||
})
|
||||
exists[int(port.Target)] = name
|
||||
}
|
||||
|
||||
// manage the "expose" section to be a NodePort in Kubernetes
|
||||
for _, expose := range s.Expose {
|
||||
|
||||
port, _ := strconv.Atoi(expose)
|
||||
|
||||
if _, exist := exists[port]; exist {
|
||||
continue
|
||||
}
|
||||
container.Ports = append(container.Ports, &helm.ContainerPort{
|
||||
Name: name,
|
||||
ContainerPort: port,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// prepareInitContainers add the init containers of a service.
|
||||
func prepareInitContainers(name string, s *types.ServiceConfig, container *helm.Container) []*helm.Container {
|
||||
|
||||
// We need to detect others services, but we probably not have parsed them yet, so
|
||||
// we will wait for them for a while.
|
||||
initContainers := make([]*helm.Container, 0)
|
||||
for dp := range s.DependsOn {
|
||||
c := helm.NewContainer("check-"+dp, "busybox", nil, s.Labels)
|
||||
command := strings.ReplaceAll(strings.TrimSpace(dependScript), "__service__", dp)
|
||||
|
||||
foundPort := -1
|
||||
locker.Lock()
|
||||
if defaultPort, ok := servicesMap[dp]; !ok {
|
||||
logger.Redf("Error while getting port for service %s\n", dp)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
foundPort = defaultPort
|
||||
}
|
||||
locker.Unlock()
|
||||
if foundPort == -1 {
|
||||
log.Fatalf(
|
||||
"ERROR, the %s service is waiting for %s port number, "+
|
||||
"but it is never discovered. You must declare at least one port in "+
|
||||
"the \"ports\" section of the service in the docker-compose file",
|
||||
name,
|
||||
dp,
|
||||
)
|
||||
}
|
||||
command = strings.ReplaceAll(command, "__port__", strconv.Itoa(foundPort))
|
||||
|
||||
c.Command = []string{
|
||||
"sh",
|
||||
"-c",
|
||||
command,
|
||||
}
|
||||
initContainers = append(initContainers, c)
|
||||
}
|
||||
return initContainers
|
||||
}
|
638
generator/converter.go
Normal file
638
generator/converter.go
Normal file
@@ -0,0 +1,638 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"katenary/generator/extrafiles"
|
||||
"katenary/parser"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
goyaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const headerHelp = `# This file is autogenerated by katenary
|
||||
#
|
||||
# DO NOT EDIT IT BY HAND UNLESS YOU KNOW WHAT YOU ARE DOING
|
||||
#
|
||||
# If you want to change the content of this file, you should edit the
|
||||
# compose file and run katenary again.
|
||||
# If you need to override some values, you can do it in a override file
|
||||
# and use the -f flag to specify it when running the helm command.
|
||||
|
||||
|
||||
`
|
||||
|
||||
// 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) {
|
||||
|
||||
var (
|
||||
templateDir = filepath.Join(config.OutputDir, "templates")
|
||||
helpersPath = filepath.Join(config.OutputDir, "templates", "_helpers.tpl")
|
||||
chartPath = filepath.Join(config.OutputDir, "Chart.yaml")
|
||||
valuesPath = filepath.Join(config.OutputDir, "values.yaml")
|
||||
readmePath = filepath.Join(config.OutputDir, "README.md")
|
||||
notesPath = filepath.Join(templateDir, "NOTES.txt")
|
||||
)
|
||||
|
||||
// the current working directory is the directory
|
||||
currentDir, _ := os.Getwd()
|
||||
// go to the root of the project
|
||||
if err := os.Chdir(filepath.Dir(dockerComposeFile[0])); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer os.Chdir(currentDir) // after the generation, go back to the original directory
|
||||
|
||||
// repove the directory part of the docker-compose files
|
||||
for i, f := range dockerComposeFile {
|
||||
dockerComposeFile[i] = filepath.Base(f)
|
||||
}
|
||||
|
||||
// parse the compose files
|
||||
project, err := parser.Parse(config.Profiles, dockerComposeFile...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// check older version of labels
|
||||
if err := checkOldLabels(project); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !config.Force {
|
||||
// check if the chart directory exists
|
||||
// if yes, prevent the user from overwriting it and ask for confirmation
|
||||
if _, err := os.Stat(config.OutputDir); err == nil {
|
||||
fmt.Print(utils.IconWarning, " The chart directory "+config.OutputDir+" already exists, do you want to overwrite it? [y/N] ")
|
||||
var answer string
|
||||
fmt.Scanln(&answer)
|
||||
if strings.ToLower(answer) != "y" {
|
||||
fmt.Println("Aborting")
|
||||
os.Exit(126) // 126 is the exit code for "Command invoked cannot execute"
|
||||
}
|
||||
}
|
||||
fmt.Println() // clean line
|
||||
}
|
||||
|
||||
// Build the objects !
|
||||
chart, err := Generate(project)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// if the app version is set from the command line, use it
|
||||
if config.AppVersion != nil {
|
||||
chart.AppVersion = *config.AppVersion
|
||||
}
|
||||
chart.Version = config.ChartVersion
|
||||
|
||||
// remove the chart directory if it exists
|
||||
os.RemoveAll(config.OutputDir)
|
||||
|
||||
// create the chart directory
|
||||
if err := os.MkdirAll(templateDir, 0755); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for name, template := range chart.Templates {
|
||||
t := template.Content
|
||||
t = removeNewlinesInsideBrackets(t)
|
||||
t = removeUnwantedLines(t)
|
||||
t = addModeline(t)
|
||||
|
||||
kind := utils.GetKind(name)
|
||||
var icon utils.Icon
|
||||
switch kind {
|
||||
case "deployment":
|
||||
icon = utils.IconPackage
|
||||
case "service":
|
||||
icon = utils.IconPlug
|
||||
case "ingress":
|
||||
icon = utils.IconWorld
|
||||
case "volumeclaim":
|
||||
icon = utils.IconCabinet
|
||||
case "configmap":
|
||||
icon = utils.IconConfig
|
||||
case "secret":
|
||||
icon = utils.IconSecret
|
||||
default:
|
||||
icon = utils.IconInfo
|
||||
}
|
||||
|
||||
servicename := template.Servicename
|
||||
if err := os.MkdirAll(filepath.Join(templateDir, servicename), 0755); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(icon, "Creating", kind, servicename)
|
||||
// if the name is a path, create the directory
|
||||
if strings.Contains(name, string(filepath.Separator)) {
|
||||
name = filepath.Join(templateDir, name)
|
||||
err := os.MkdirAll(filepath.Dir(name), 0755)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
// remove the serivce name from the template name
|
||||
name = strings.Replace(name, servicename+".", "", 1)
|
||||
name = filepath.Join(templateDir, servicename, name)
|
||||
}
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
f.Write(t)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// calculate the sha1 hash of the services
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := goyaml.NewEncoder(buf)
|
||||
encoder.SetIndent(2)
|
||||
if err := encoder.Encode(chart); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
yamlChart := buf.Bytes()
|
||||
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)
|
||||
|
||||
f, err := os.Create(chartPath)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
f.Write(yamlChart)
|
||||
f.Close()
|
||||
|
||||
buf.Reset()
|
||||
encoder = goyaml.NewEncoder(buf)
|
||||
encoder.SetIndent(2)
|
||||
if err = encoder.Encode(&chart.Values); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
values := buf.Bytes()
|
||||
values = addDescriptions(values, *project)
|
||||
values = addDependencyDescription(values, chart.Dependencies)
|
||||
values = addCommentsToValues(values)
|
||||
values = addStorageClassHelp(values)
|
||||
values = addImagePullSecretsHelp(values)
|
||||
values = addImagePullPolicyHelp(values)
|
||||
values = addVariablesDoc(values, project)
|
||||
values = addMainTagAppDoc(values, project)
|
||||
values = append([]byte(headerHelp), values...)
|
||||
|
||||
f, err = os.Create(valuesPath)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
f.Write(values)
|
||||
f.Close()
|
||||
|
||||
f, err = os.Create(helpersPath)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
f.Write([]byte(chart.Helper))
|
||||
f.Close()
|
||||
|
||||
readme := extrafiles.ReadMeFile(chart.Name, chart.Description, chart.Values)
|
||||
f, err = os.Create(readmePath)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
f.Write([]byte(readme))
|
||||
f.Close()
|
||||
|
||||
notes := extrafiles.NotesFile()
|
||||
f, err = os.Create(notesPath)
|
||||
if err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
f.Write([]byte(notes))
|
||||
f.Close()
|
||||
|
||||
if config.HelmUpdate {
|
||||
if err := helmUpdate(config); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
} else if err := helmLint(config); err != nil {
|
||||
fmt.Println(utils.IconFailure, err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Println(utils.IconSuccess, "Helm chart created successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
# More info: https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource
|
||||
`
|
||||
|
||||
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
|
||||
# More info: https://kubernetes.io/docs/concepts/storage/storage-classes/
|
||||
`
|
||||
|
||||
// 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=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[LABEL_DESCRIPTION]; 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 []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.
|
||||
# More info: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
`
|
||||
|
||||
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.
|
||||
#
|
||||
`, project.Name)
|
||||
|
||||
lines := strings.Split(string(values), "\n")
|
||||
for i, line := range lines {
|
||||
if regexp.MustCompile(`(?m)^name:`).MatchString(line) {
|
||||
doc := fmt.Sprintf("\n# Name of the chart (required), basically the name of the project.\n")
|
||||
lines[i] = doc + line
|
||||
} else if regexp.MustCompile(`(?m)^version:`).MatchString(line) {
|
||||
doc := fmt.Sprintf("\n# Version of the chart (required)\n")
|
||||
lines[i] = doc + line
|
||||
} else if strings.Contains(line, "appVersion:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
doc := fmt.Sprintf(
|
||||
"\n%s# Version of the application (required).\n%s# This should be the main application version.\n",
|
||||
strings.Repeat(" ", spaces),
|
||||
strings.Repeat(" ", spaces),
|
||||
)
|
||||
lines[i] = doc + line
|
||||
} else if strings.Contains(line, "dependencies:") {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
doc := fmt.Sprintf("\n"+
|
||||
"%s# Dependencies are external charts that this chart will depend on.\n"+
|
||||
"%s# More information can be found in the chart's README.md file.\n",
|
||||
strings.Repeat(" ", spaces),
|
||||
strings.Repeat(" ", spaces),
|
||||
)
|
||||
lines[i] = doc + line
|
||||
}
|
||||
}
|
||||
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
|
||||
# More info: https://kubernetes.io/docs/concepts/containers/images/#updating-images
|
||||
`
|
||||
|
||||
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 addVariablesDoc(values []byte, project *types.Project) []byte {
|
||||
|
||||
lines := strings.Split(string(values), "\n")
|
||||
|
||||
currentService := ""
|
||||
for _, service := range project.Services {
|
||||
variables := utils.GetValuesFromLabel(service, LABEL_VALUES)
|
||||
for i, line := range lines {
|
||||
if regexp.MustCompile(`(?m)^` + service.Name + `:`).MatchString(line) {
|
||||
currentService = service.Name
|
||||
}
|
||||
for varname, variable := range variables {
|
||||
if variable == nil {
|
||||
continue
|
||||
}
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
if regexp.MustCompile(`(?m)\s*`+varname+`:`).MatchString(line) && currentService == service.Name {
|
||||
|
||||
// add # to the beginning of the Description
|
||||
doc := strings.ReplaceAll("\n"+variable.Description, "\n", "\n"+strings.Repeat(" ", spaces)+"# ")
|
||||
doc = strings.TrimRight(doc, " ")
|
||||
doc += "\n" + line
|
||||
|
||||
lines[i] = doc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
const mainTagAppDoc = `This is the version of the main application.
|
||||
Leave it to blank to use the Chart "AppVersion" value.`
|
||||
|
||||
func addMainTagAppDoc(values []byte, project *types.Project) []byte {
|
||||
lines := strings.Split(string(values), "\n")
|
||||
|
||||
for _, service := range project.Services {
|
||||
inService := false
|
||||
inRegistry := false
|
||||
// read the label LabelMainApp
|
||||
if v, ok := service.Labels[LABEL_MAIN_APP]; !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 regexp.MustCompile(`^` + service.Name + `:`).MatchString(line) {
|
||||
inService = true
|
||||
}
|
||||
if inService && regexp.MustCompile(`^\s*repository:.*`).MatchString(line) {
|
||||
inRegistry = true
|
||||
}
|
||||
if inService && inRegistry {
|
||||
if regexp.MustCompile(`^\s*tag: .*`).MatchString(line) {
|
||||
spaces := utils.CountStartingSpaces(line)
|
||||
doc := strings.ReplaceAll(mainTagAppDoc, "\n", "\n"+strings.Repeat(" ", spaces)+"# ")
|
||||
doc = strings.Repeat(" ", spaces) + "# " + doc
|
||||
|
||||
lines[i] = doc + "\n" + line + "\n"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
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 {
|
||||
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, KATENARY_PREFIX) {
|
||||
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,
|
||||
KATENARY_PREFIX[0:len(KATENARY_PREFIX)-1],
|
||||
strings.Join(badServices, "\n"),
|
||||
)
|
||||
|
||||
return errors.New(utils.WordWrap(message, 80))
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
}
|
133
generator/cronJob.go
Normal file
133
generator/cronJob.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
goyaml "gopkg.in/yaml.v3"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// only used to check interface implementation
|
||||
var (
|
||||
_ Yaml = (*CronJob)(nil)
|
||||
)
|
||||
|
||||
// CronJob is a kubernetes CronJob.
|
||||
type CronJob struct {
|
||||
*batchv1.CronJob
|
||||
service *types.ServiceConfig
|
||||
}
|
||||
|
||||
// NewCronJob creates a new CronJob from a compose service. The appName is the name of the application taken from the project name.
|
||||
func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) (*CronJob, *RBAC) {
|
||||
var labels, ok = service.Labels[LABEL_CRONJOB]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
mapping := struct {
|
||||
Image string `yaml:"image,omitempty"`
|
||||
Command string `yaml:"command"`
|
||||
Schedule string `yaml:"schedule"`
|
||||
Rbac bool `yaml:"rbac"`
|
||||
}{
|
||||
Image: "",
|
||||
Command: "",
|
||||
Schedule: "",
|
||||
Rbac: false,
|
||||
}
|
||||
if err := goyaml.Unmarshal([]byte(labels), &mapping); err != nil {
|
||||
log.Fatalf("Error parsing cronjob labels: %s", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if _, ok := chart.Values[service.Name]; !ok {
|
||||
chart.Values[service.Name] = NewValue(service, false)
|
||||
}
|
||||
if chart.Values[service.Name].(*Value).CronJob == nil {
|
||||
chart.Values[service.Name].(*Value).CronJob = &CronJobValue{}
|
||||
}
|
||||
chart.Values[service.Name].(*Value).CronJob.Schedule = mapping.Schedule
|
||||
chart.Values[service.Name].(*Value).CronJob.ImagePullPolicy = "IfNotPresent"
|
||||
chart.Values[service.Name].(*Value).CronJob.Environment = map[string]any{}
|
||||
|
||||
image, tag := mapping.Image, ""
|
||||
if image == "" { // if image is not set, use the image from the service
|
||||
image = service.Image
|
||||
}
|
||||
|
||||
if strings.Contains(image, ":") {
|
||||
image = strings.Split(service.Image, ":")[0]
|
||||
tag = strings.Split(service.Image, ":")[1]
|
||||
}
|
||||
|
||||
chart.Values[service.Name].(*Value).CronJob.Repository = &RepositoryValue{
|
||||
Image: image,
|
||||
Tag: tag,
|
||||
}
|
||||
|
||||
cronjob := &CronJob{
|
||||
CronJob: &batchv1.CronJob{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "CronJob",
|
||||
APIVersion: "batch/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "{{ .Values." + service.Name + ".cronjob.schedule }}",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "cronjob",
|
||||
Image: "{{ .Values." + service.Name + ".cronjob.repository.image }}:{{ default .Values." + service.Name + ".cronjob.repository.tag \"latest\" }}",
|
||||
Command: []string{
|
||||
"sh",
|
||||
"-c",
|
||||
mapping.Command,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
service: &service,
|
||||
}
|
||||
|
||||
var rbac *RBAC
|
||||
if mapping.Rbac {
|
||||
rbac = NewRBAC(service, appName)
|
||||
// add the service account to the cronjob
|
||||
cronjob.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName = utils.TplName(service.Name, appName)
|
||||
}
|
||||
|
||||
return cronjob, rbac
|
||||
}
|
||||
|
||||
// Filename returns the filename of the cronjob.
|
||||
//
|
||||
// Implements the Yaml interface.
|
||||
func (c *CronJob) Filename() string {
|
||||
return c.service.Name + ".cronjob.yaml"
|
||||
}
|
||||
|
||||
// Yaml returns the yaml representation of the cronjob.
|
||||
//
|
||||
// Implements the Yaml interface.
|
||||
func (c *CronJob) Yaml() ([]byte, error) {
|
||||
return yaml.Marshal(c)
|
||||
}
|
@@ -1,110 +0,0 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"katenary/helm"
|
||||
"katenary/logger"
|
||||
"log"
|
||||
|
||||
"github.com/alessio/shellescape"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
cronMulti = `pods=$(kubectl get pods --selector=%s/component=%s,%s/resource=deployment -o jsonpath='{.items[*].metadata.name}')`
|
||||
cronMultiCmd = `
|
||||
for pod in $pods; do
|
||||
kubectl exec -i $pod -c %s -- sh -c %s
|
||||
done`
|
||||
cronSingle = `pod=$(kubectl get pods --selector=%s/component=%s,%s/resource=deployment -o jsonpath='{.items[0].metadata.name}')`
|
||||
cronCmd = `
|
||||
kubectl exec -i $pod -c %s -- sh -c %s`
|
||||
)
|
||||
|
||||
type CronDef struct {
|
||||
Command string `yaml:"command"`
|
||||
Schedule string `yaml:"schedule"`
|
||||
Image string `yaml:"image"`
|
||||
Multi bool `yaml:"allPods,omitempty"`
|
||||
}
|
||||
|
||||
func buildCrontab(deployName string, deployment *helm.Deployment, s *types.ServiceConfig, fileGeneratorChan HelmFileGenerator) {
|
||||
// get the cron label from the service
|
||||
var crondef string
|
||||
var ok bool
|
||||
if crondef, ok = s.Labels[helm.LABEL_CRON]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// parse yaml
|
||||
crons := []CronDef{}
|
||||
err := yaml.Unmarshal([]byte(crondef), &crons)
|
||||
if err != nil {
|
||||
log.Fatalf("error: %v", err)
|
||||
}
|
||||
|
||||
if len(crons) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// create a serviceAccount
|
||||
sa := helm.NewServiceAccount(deployName)
|
||||
// create a role
|
||||
role := helm.NewCronRole(deployName)
|
||||
|
||||
// create a roleBinding
|
||||
roleBinding := helm.NewRoleBinding(deployName, sa, role)
|
||||
|
||||
// make generation
|
||||
logger.Magenta(ICON_RBAC, "Generating ServiceAccount, Role and RoleBinding for cron jobs", deployName)
|
||||
fileGeneratorChan <- sa
|
||||
fileGeneratorChan <- role
|
||||
fileGeneratorChan <- roleBinding
|
||||
|
||||
numcron := len(crons) - 1
|
||||
index := 1
|
||||
|
||||
// create crontabs
|
||||
for _, cron := range crons {
|
||||
escaped := shellescape.Quote(cron.Command)
|
||||
var cmd, podget string
|
||||
if cron.Multi {
|
||||
podget = cronMulti
|
||||
cmd = cronMultiCmd
|
||||
} else {
|
||||
podget = cronSingle
|
||||
cmd = cronCmd
|
||||
}
|
||||
podget = fmt.Sprintf(podget, helm.K, deployName, helm.K)
|
||||
cmd = fmt.Sprintf(cmd, s.Name, escaped)
|
||||
cmd = podget + cmd
|
||||
|
||||
if cron.Image == "" {
|
||||
cron.Image = `bitnami/kubectl:{{ printf "%s.%s" .Capabilities.KubeVersion.Major .Capabilities.KubeVersion.Minor }}`
|
||||
}
|
||||
|
||||
name := deployName
|
||||
if numcron > 0 {
|
||||
name = fmt.Sprintf("%s-%d", deployName, index)
|
||||
}
|
||||
|
||||
// add crontab
|
||||
suffix := ""
|
||||
if numcron > 0 {
|
||||
suffix = fmt.Sprintf("%d", index)
|
||||
}
|
||||
cronTab := helm.NewCrontab(
|
||||
name,
|
||||
cron.Image,
|
||||
cmd,
|
||||
cron.Schedule,
|
||||
sa,
|
||||
)
|
||||
logger.Magenta(ICON_CRON, "Generating crontab", deployName, suffix)
|
||||
fileGeneratorChan <- cronTab
|
||||
index++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@@ -1,70 +1,569 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/helm"
|
||||
"katenary/logger"
|
||||
"fmt"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// This function will try to yied deployment and services based on a service from the compose file structure.
|
||||
func buildDeployment(name string, s *types.ServiceConfig, linked map[string]types.ServiceConfig, fileGeneratorChan HelmFileGenerator) {
|
||||
var _ Yaml = (*Deployment)(nil)
|
||||
|
||||
logger.Magenta(ICON_PACKAGE+" Generating deployment for ", name)
|
||||
deployment := helm.NewDeployment(name)
|
||||
// Deployment is a kubernetes Deployment.
|
||||
type Deployment struct {
|
||||
*appsv1.Deployment `yaml:",inline"`
|
||||
chart *HelmChart `yaml:"-"`
|
||||
configMaps map[string]bool `yaml:"-"`
|
||||
service *types.ServiceConfig `yaml:"-"`
|
||||
defaultTag string `yaml:"-"`
|
||||
isMainApp bool `yaml:"-"`
|
||||
}
|
||||
|
||||
newContainerForDeployment(name, name, deployment, s, fileGeneratorChan)
|
||||
// NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name.
|
||||
// It also creates the Values map that will be used to create the values.yaml file.
|
||||
func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment {
|
||||
|
||||
// Add selectors
|
||||
selectors := buildSelector(name, s)
|
||||
selectors[helm.K+"/resource"] = "deployment"
|
||||
deployment.Spec.Selector = map[string]interface{}{
|
||||
"matchLabels": selectors,
|
||||
ports := []corev1.ContainerPort{}
|
||||
for _, port := range service.Ports {
|
||||
ports = append(ports, corev1.ContainerPort{
|
||||
ContainerPort: int32(port.Target),
|
||||
})
|
||||
}
|
||||
deployment.Spec.Template.Metadata.Labels = selectors
|
||||
|
||||
// Now, the linked services (same pod)
|
||||
for lname, link := range linked {
|
||||
newContainerForDeployment(name, lname, deployment, &link, fileGeneratorChan)
|
||||
// append ports and expose ports to the deployment,
|
||||
// to be able to generate them in the Service file
|
||||
if len(link.Ports) > 0 || len(link.Expose) > 0 {
|
||||
s.Ports = append(s.Ports, link.Ports...)
|
||||
s.Expose = append(s.Expose, link.Expose...)
|
||||
isMainApp := false
|
||||
if mainLabel, ok := service.Labels[LABEL_MAIN_APP]; ok {
|
||||
main := strings.ToLower(mainLabel)
|
||||
isMainApp = main == "true" || main == "yes" || main == "1"
|
||||
}
|
||||
|
||||
defaultTag := `default "latest"`
|
||||
if isMainApp {
|
||||
defaultTag = `default .Chart.AppVersion "latest"`
|
||||
}
|
||||
|
||||
chart.Values[service.Name] = NewValue(service, isMainApp)
|
||||
appName := chart.Name
|
||||
|
||||
dep := &Deployment{
|
||||
isMainApp: isMainApp,
|
||||
defaultTag: defaultTag,
|
||||
service: &service,
|
||||
chart: chart,
|
||||
Deployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: utils.Int32Ptr(1),
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: GetMatchLabels(service.Name, appName),
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: GetMatchLabels(service.Name, appName),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
configMaps: map[string]bool{},
|
||||
}
|
||||
|
||||
// add containers
|
||||
dep.AddContainer(service)
|
||||
|
||||
// add volumes
|
||||
dep.AddVolumes(service, appName)
|
||||
|
||||
if service.Environment != nil {
|
||||
dep.SetEnvFrom(service, appName)
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
|
||||
// DependsOn adds a initContainer to the deployment that will wait for the service to be up.
|
||||
func (d *Deployment) DependsOn(to *Deployment) 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{}
|
||||
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{}
|
||||
|
||||
for _, port := range service.Ports {
|
||||
name := utils.GetServiceNameByPort(int(port.Target))
|
||||
if name == "" {
|
||||
utils.Warn("Port name not found for port ", port.Target, " in service ", service.Name, ". Using port number instead")
|
||||
}
|
||||
ports = append(ports, corev1.ContainerPort{
|
||||
ContainerPort: int32(port.Target),
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
container := corev1.Container{
|
||||
Image: utils.TplValue(service.Name, "repository.image") + ":" +
|
||||
utils.TplValue(service.Name, "repository.tag", d.defaultTag),
|
||||
Ports: ports,
|
||||
Name: service.Name,
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
}
|
||||
if _, ok := d.chart.Values[service.Name]; !ok {
|
||||
d.chart.Values[service.Name] = NewValue(service, d.isMainApp)
|
||||
}
|
||||
d.chart.Values[service.Name].(*Value).ImagePullPolicy = string(corev1.PullIfNotPresent)
|
||||
|
||||
// add an imagePullSecret, it actually does not work because the secret is not
|
||||
// created but it add the reference in the YAML file. We'll change it in Yaml()
|
||||
// method.
|
||||
d.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{
|
||||
Name: `{{ .Values.pullSecrets | toYaml | indent __indent__ }}`,
|
||||
}}
|
||||
|
||||
d.AddHealthCheck(service, &container)
|
||||
|
||||
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, container)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment.
|
||||
// If the volume is a bind volume it will warn the user that it is not supported yet.
|
||||
func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) {
|
||||
|
||||
tobind := map[string]bool{}
|
||||
if v, ok := service.Labels[LABEL_CM_FILES]; ok {
|
||||
binds := []string{}
|
||||
if err := yaml.Unmarshal([]byte(v), &binds); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, bind := range binds {
|
||||
tobind[bind] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates in volumes
|
||||
volumes := make([]map[string]interface{}, 0)
|
||||
done := make(map[string]bool)
|
||||
for _, vol := range deployment.Spec.Template.Spec.Volumes {
|
||||
name := vol["name"].(string)
|
||||
if _, ok := done[name]; ok {
|
||||
isSamePod := false
|
||||
if v, ok := service.Labels[LABEL_SAME_POD]; !ok {
|
||||
isSamePod = false
|
||||
} else {
|
||||
isSamePod = v != ""
|
||||
}
|
||||
|
||||
for _, volume := range service.Volumes {
|
||||
// not declared as a bind volume, skip
|
||||
if _, ok := tobind[volume.Source]; !isSamePod && volume.Type == "bind" && !ok {
|
||||
utils.Warn(
|
||||
"Bind volumes are not supported yet, " +
|
||||
"excepting for those declared as " +
|
||||
LABEL_CM_FILES +
|
||||
", skipping volume " + volume.Source +
|
||||
" from service " + service.Name,
|
||||
)
|
||||
continue
|
||||
} else {
|
||||
done[name] = true
|
||||
volumes = append(volumes, vol)
|
||||
}
|
||||
}
|
||||
deployment.Spec.Template.Spec.Volumes = volumes
|
||||
|
||||
// Then, create Services and possible Ingresses for ingress labels, "ports" and "expose" section
|
||||
if len(s.Ports) > 0 || len(s.Expose) > 0 {
|
||||
for _, s := range generateServicesAndIngresses(name, s) {
|
||||
if s != nil {
|
||||
fileGeneratorChan <- s
|
||||
container, index := utils.GetContainerByName(service.Name, d.Spec.Template.Spec.Containers)
|
||||
if container == nil {
|
||||
utils.Warn("Container not found for volume", volume.Source)
|
||||
continue
|
||||
}
|
||||
|
||||
// ensure that the volume is not already present in the container
|
||||
for _, vm := range container.VolumeMounts {
|
||||
if vm.Name == volume.Source {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
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[LABEL_SAME_POD]; !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
|
||||
cm := NewConfigMapFromFiles(service, appName, volume.Source)
|
||||
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
Name: utils.PathToName(volume.Source),
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: cm.ObjectMeta.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// add the mount path to the container
|
||||
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
|
||||
Name: utils.PathToName(volume.Source),
|
||||
MountPath: volume.Target,
|
||||
})
|
||||
|
||||
d.configMaps[utils.PathToName(volume.Source)] = true
|
||||
// add all subdirectories to the list of directories
|
||||
stat, err := os.Stat(volume.Source)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if stat.IsDir() {
|
||||
files, err := os.ReadDir(volume.Source)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
cm := NewConfigMapFromFiles(service, appName, filepath.Join(volume.Source, file.Name()))
|
||||
name := utils.PathToName(volume.Source) + "-" + file.Name()
|
||||
d.configMaps[name] = true
|
||||
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
Name: utils.PathToName(volume.Source) + "-" + file.Name(),
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: cm.ObjectMeta.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// add the mount path to the container
|
||||
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
|
||||
Name: name,
|
||||
MountPath: filepath.Join(volume.Target, file.Name()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.Spec.Template.Spec.Containers[index] = *container
|
||||
}
|
||||
|
||||
// add the volumes in Values
|
||||
if len(VolumeValues[name]) > 0 {
|
||||
AddValues(name, map[string]EnvVal{"persistence": VolumeValues[name]})
|
||||
}
|
||||
|
||||
// the deployment is ready, give it
|
||||
fileGeneratorChan <- deployment
|
||||
|
||||
// and then, we can say that it's the end
|
||||
fileGeneratorChan <- nil
|
||||
}
|
||||
|
||||
func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) {
|
||||
log.Printf("In %s deployment, add volumes for service %s from binded deployment %s", d.Name, service.Name, binded.Name)
|
||||
// find the volume in the binded deployment
|
||||
for _, bindedVolume := range binded.Spec.Template.Spec.Volumes {
|
||||
log.Println("bindedVolume.Name found", bindedVolume.Name)
|
||||
skip := false
|
||||
for _, targetVol := range d.Spec.Template.Spec.Volumes {
|
||||
if targetVol.Name == bindedVolume.Name {
|
||||
log.Println("Volume", bindedVolume.Name, "already exists in deployment", d.Name)
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !skip {
|
||||
// add the volume to the current deployment
|
||||
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, bindedVolume)
|
||||
log.Println("d.Spec.Template.Spec.Volumes", d.Spec.Template.Spec.Volumes)
|
||||
// get the container
|
||||
|
||||
}
|
||||
// add volume mount to the container
|
||||
targetContainer, ti := utils.GetContainerByName(service.Name, d.Spec.Template.Spec.Containers)
|
||||
sourceContainer, _ := utils.GetContainerByName(service.Name, binded.Spec.Template.Spec.Containers)
|
||||
for _, bindedMount := range sourceContainer.VolumeMounts {
|
||||
if bindedMount.Name == bindedVolume.Name {
|
||||
log.Println("bindedMount.Name found", bindedMount.Name)
|
||||
targetContainer.VolumeMounts = append(targetContainer.VolumeMounts, bindedMount)
|
||||
}
|
||||
}
|
||||
d.Spec.Template.Spec.Containers[ti] = *targetContainer
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
|
||||
drop := []string{}
|
||||
secrets := []string{}
|
||||
|
||||
// secrets from label
|
||||
labelSecrets := []string{}
|
||||
if v, ok := service.Labels[LABEL_SECRETS]; ok {
|
||||
err := yaml.Unmarshal([]byte(v), &labelSecrets)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// values from label
|
||||
varDescriptons := utils.GetValuesFromLabel(service, LABEL_VALUES)
|
||||
labelValues := []string{}
|
||||
for v := range varDescriptons {
|
||||
labelValues = append(labelValues, v)
|
||||
}
|
||||
|
||||
for _, secret := range labelSecrets {
|
||||
// get the secret name
|
||||
_, ok := service.Environment[secret]
|
||||
if !ok {
|
||||
drop = append(drop, secret)
|
||||
utils.Warn("Secret " + secret + " not found in service " + service.Name + " - skpped")
|
||||
continue
|
||||
}
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
|
||||
// for each values from label "values", add it to Values map and change the envFrom
|
||||
// value to {{ .Values.<service>.<value> }}
|
||||
for _, value := range labelValues {
|
||||
// get the environment variable name
|
||||
val, ok := service.Environment[value]
|
||||
if !ok {
|
||||
drop = append(drop, value)
|
||||
utils.Warn("Environment variable " + value + " not found in service " + service.Name + " - skpped")
|
||||
continue
|
||||
}
|
||||
if d.chart.Values[service.Name].(*Value).Environment == nil {
|
||||
d.chart.Values[service.Name].(*Value).Environment = make(map[string]any)
|
||||
}
|
||||
d.chart.Values[service.Name].(*Value).Environment[value] = *val
|
||||
// set the environment variable to bind to the values.yaml file
|
||||
v := utils.TplValue(service.Name, "environment."+value)
|
||||
service.Environment[value] = &v
|
||||
}
|
||||
|
||||
for _, value := range drop {
|
||||
delete(service.Environment, value)
|
||||
}
|
||||
|
||||
fromSources := []corev1.EnvFromSource{}
|
||||
|
||||
if len(service.Environment) > 0 {
|
||||
fromSources = append(fromSources, corev1.EnvFromSource{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(secrets) > 0 {
|
||||
fromSources = append(fromSources, corev1.EnvFromSource{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
container, index := utils.GetContainerByName(service.Name, d.Spec.Template.Spec.Containers)
|
||||
if container == nil {
|
||||
utils.Warn("Container not found for service " + service.Name)
|
||||
return
|
||||
}
|
||||
|
||||
container.EnvFrom = append(container.EnvFrom, fromSources...)
|
||||
|
||||
if container.Env == nil {
|
||||
container.Env = []corev1.EnvVar{}
|
||||
}
|
||||
|
||||
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[LABEL_HEALTHCHECK]; ok {
|
||||
probes := struct {
|
||||
LivenessProbe *corev1.Probe `yaml:"livenessProbe"`
|
||||
ReadinessProbe *corev1.Probe `yaml:"readinessProbe"`
|
||||
}{}
|
||||
err := yaml.Unmarshal([]byte(v), &probes)
|
||||
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
|
||||
y, err := yaml.Marshal(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// for each volume mount, add a condition "if values has persistence"
|
||||
changing := false
|
||||
content := strings.Split(string(y), "\n")
|
||||
spaces := ""
|
||||
volumeName := ""
|
||||
|
||||
// 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: ") {
|
||||
volumeName = strings.TrimSpace(strings.Replace(content[i], "name: ", "", 1))
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
if volumeName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := d.configMaps[volumeName]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(volume, "- mountPath: ") {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(volume))
|
||||
content[line] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + volumeName + `.enabled }}` + "\n" + volume
|
||||
changing = true
|
||||
}
|
||||
if strings.Contains(volume, "name: ") && changing {
|
||||
content[line] = volume + "\n" + spaces + "{{- end }}"
|
||||
changing = false
|
||||
}
|
||||
}
|
||||
|
||||
changing = false
|
||||
inVolumes := false
|
||||
volumeName = ""
|
||||
// this loop changes imagePullPolicy to {{ .Values.<service>.imagePullPolicy }}
|
||||
// and the volume definition adding the condition "if values has persistence"
|
||||
for i, line := range content {
|
||||
|
||||
if strings.Contains(line, "imagePullPolicy:") {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
|
||||
content[i] = spaces + "imagePullPolicy: {{ .Values." + serviceName + ".imagePullPolicy }}"
|
||||
}
|
||||
|
||||
// find the volume name
|
||||
for i := i; i < len(content); i++ {
|
||||
if strings.Contains(content[i], "- name: ") {
|
||||
volumeName = strings.TrimSpace(strings.Replace(content[i], "- name: ", "", 1))
|
||||
break
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "volumes:") {
|
||||
inVolumes = true
|
||||
}
|
||||
|
||||
if volumeName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := d.configMaps[volumeName]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, "- name: ") && inVolumes {
|
||||
spaces = strings.Repeat(" ", utils.CountStartingSpaces(line))
|
||||
content[i] = spaces + `{{- if .Values.` + serviceName + `.persistence.` + volumeName + `.enabled }}` + "\n" + line
|
||||
changing = true
|
||||
}
|
||||
if strings.Contains(line, "claimName: ") && changing {
|
||||
content[i] = line + "\n" + spaces + "{{- end }}"
|
||||
changing = false
|
||||
}
|
||||
}
|
||||
|
||||
// for impagePullSecrets, replace the name with the value from values.yaml
|
||||
inpullsecrets := false
|
||||
for i, line := range content {
|
||||
if strings.Contains(line, "imagePullSecrets:") {
|
||||
inpullsecrets = true
|
||||
}
|
||||
if inpullsecrets && strings.Contains(line, "- name: ") && inpullsecrets {
|
||||
line = strings.Replace(line, "- name: ", "", 1)
|
||||
line = strings.ReplaceAll(line, "'", "")
|
||||
content[i] = line
|
||||
inpullsecrets = false
|
||||
}
|
||||
}
|
||||
|
||||
// Find the replicas line and replace it with the value from values.yaml
|
||||
for i, line := range content {
|
||||
if strings.Contains(line, "replicas:") {
|
||||
line = regexp.MustCompile("replicas: .*$").ReplaceAllString(line, "replicas: {{ .Values."+serviceName+".replicas }}")
|
||||
content[i] = line
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(strings.Join(content, "\n")), nil
|
||||
}
|
||||
|
||||
func (d *Deployment) Filename() string {
|
||||
return d.service.Name + ".deployment.yaml"
|
||||
}
|
||||
|
18
generator/doc.go
Normal file
18
generator/doc.go
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
The generator package generates kubernetes objects from a compose file and transforms them into a helm chart.
|
||||
|
||||
The generator package is the core of katenary. It is responsible for generating kubernetes objects from a compose file and transforming them into a helm chart.
|
||||
Convertion manipulates Yaml representation of kubernetes object to add conditions, labels, annotations, etc. to the objects. It also create the values to be set to
|
||||
the values.yaml file.
|
||||
|
||||
The generate.Convert() create an HelmChart object and call "Generate()" method to convert from a compose file to a helm chart.
|
||||
It saves the helm chart in the given directory.
|
||||
|
||||
If you want to change or override the write behavior, you can use the HelmChart.Generate() function and implement your own write function. This function returns
|
||||
the helm chart object containing all kubernetes objects and helm chart ingormation. It does not write the helm chart to the disk.
|
||||
|
||||
TODO: Manage cronjob + rbac
|
||||
TODO: create note.txt
|
||||
TODO: manage emptyDirs
|
||||
*/
|
||||
package generator
|
154
generator/env.go
154
generator/env.go
@@ -1,154 +0,0 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"katenary/compose"
|
||||
"katenary/helm"
|
||||
"katenary/logger"
|
||||
"katenary/tools"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// applyEnvMapLabel will get all LABEL_MAP_ENV to rebuild the env map with tpl.
|
||||
func applyEnvMapLabel(s *types.ServiceConfig, c *helm.Container) {
|
||||
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
mapenv, ok := s.Labels[helm.LABEL_MAP_ENV]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// the mapenv is a YAML string
|
||||
var envmap map[string]EnvVal
|
||||
err := yaml.Unmarshal([]byte(mapenv), &envmap)
|
||||
if err != nil {
|
||||
logger.ActivateColors = true
|
||||
logger.Red(err.Error())
|
||||
logger.ActivateColors = false
|
||||
return
|
||||
}
|
||||
|
||||
// add in envmap
|
||||
for k, v := range envmap {
|
||||
vstring := fmt.Sprintf("%v", v)
|
||||
s.Environment[k] = &vstring
|
||||
touched := false
|
||||
if c.Env != nil {
|
||||
c.Env = make([]*helm.Value, 0)
|
||||
}
|
||||
for _, env := range c.Env {
|
||||
if env.Name == k {
|
||||
env.Value = v
|
||||
touched = true
|
||||
}
|
||||
}
|
||||
if !touched {
|
||||
c.Env = append(c.Env, &helm.Value{Name: k, Value: v})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readEnvFile read environment file and add to the values.yaml map.
|
||||
func readEnvFile(envfilename string) map[string]EnvVal {
|
||||
env := make(map[string]EnvVal)
|
||||
content, err := ioutil.ReadFile(envfilename)
|
||||
if err != nil {
|
||||
logger.ActivateColors = true
|
||||
logger.Red(err.Error())
|
||||
logger.ActivateColors = false
|
||||
os.Exit(2)
|
||||
}
|
||||
// each value is on a separate line with KEY=value
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "=") {
|
||||
kv := strings.SplitN(line, "=", 2)
|
||||
env[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
// prepareEnvFromFiles generate configMap or secrets from environment files.
|
||||
func prepareEnvFromFiles(name string, s *types.ServiceConfig, container *helm.Container, fileGeneratorChan HelmFileGenerator) {
|
||||
|
||||
// prepare secrets
|
||||
secretsFiles := make([]string, 0)
|
||||
if v, ok := s.Labels[helm.LABEL_ENV_SECRET]; ok {
|
||||
secretsFiles = strings.Split(v, ",")
|
||||
}
|
||||
|
||||
var secretVars []string
|
||||
if v, ok := s.Labels[helm.LABEL_SECRETVARS]; ok {
|
||||
secretVars = strings.Split(v, ",")
|
||||
}
|
||||
|
||||
for i, s := range secretVars {
|
||||
secretVars[i] = strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// manage environment files (env_file in compose)
|
||||
for _, envfile := range s.EnvFile {
|
||||
f := tools.PathToName(envfile)
|
||||
f = strings.ReplaceAll(f, ".env", "")
|
||||
isSecret := false
|
||||
for _, s := range secretsFiles {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == envfile {
|
||||
isSecret = true
|
||||
}
|
||||
}
|
||||
var store helm.InlineConfig
|
||||
if !isSecret {
|
||||
logger.Bluef(ICON_CONF+" Generating configMap from %s\n", envfile)
|
||||
store = helm.NewConfigMap(name, envfile)
|
||||
} else {
|
||||
logger.Bluef(ICON_SECRET+" Generating secret from %s\n", envfile)
|
||||
store = helm.NewSecret(name, envfile)
|
||||
}
|
||||
|
||||
envfile = filepath.Join(compose.GetCurrentDir(), envfile)
|
||||
if err := store.AddEnvFile(envfile, secretVars); err != nil {
|
||||
logger.ActivateColors = true
|
||||
logger.Red(err.Error())
|
||||
logger.ActivateColors = false
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
section := "configMapRef"
|
||||
if isSecret {
|
||||
section = "secretRef"
|
||||
}
|
||||
|
||||
container.EnvFrom = append(container.EnvFrom, map[string]map[string]string{
|
||||
section: {
|
||||
"name": store.Metadata().Name,
|
||||
},
|
||||
})
|
||||
|
||||
// read the envfile and remove them from the container environment or secret
|
||||
envs := readEnvFile(envfile)
|
||||
for varname := range envs {
|
||||
if !isSecret {
|
||||
// remove varname from container
|
||||
for i, s := range container.Env {
|
||||
if s.Name == varname {
|
||||
container.Env = append(container.Env[:i], container.Env[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if store != nil {
|
||||
fileGeneratorChan <- store.(HelmFile)
|
||||
}
|
||||
}
|
||||
}
|
2
generator/extrafiles/doc.go
Normal file
2
generator/extrafiles/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
/* extrafiles package provides function to generate the Chart files that are not objects. Like README.md and notes.txt... */
|
||||
package extrafiles
|
11
generator/extrafiles/notes.go
Normal file
11
generator/extrafiles/notes.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package extrafiles
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed notes.tpl
|
||||
var notesTemplate string
|
||||
|
||||
// NoteTXTFile returns the content of the note.txt file.
|
||||
func NotesFile() string {
|
||||
return notesTemplate
|
||||
}
|
27
generator/extrafiles/notes.tpl
Normal file
27
generator/extrafiles/notes.tpl
Normal file
@@ -0,0 +1,27 @@
|
||||
Your release is named {{ .Release.Name }}.
|
||||
|
||||
To learn more about the release, try:
|
||||
|
||||
$ helm -n {{ .Release.Namespace }} status {{ .Release.Name }}
|
||||
$ helm -n {{ .Release.Namespace }} get all {{ .Release.Name }}
|
||||
|
||||
To delete the release, run:
|
||||
|
||||
$ helm -n {{ .Release.Namespace }} delete {{ .Release.Name }}
|
||||
|
||||
You can see this notes again by running:
|
||||
|
||||
$ helm -n {{ .Release.Namespace }} get notes {{ .Release.Name }}
|
||||
|
||||
{{- $count := 0 -}}
|
||||
{{- range $s, $v := .Values -}}
|
||||
{{- if and $v $v.ingress -}}
|
||||
{{- $count = add $count 1 -}}
|
||||
{{- if eq $count 1 }}
|
||||
|
||||
The ingress list is:
|
||||
{{ end }}
|
||||
- {{ $s }}: http://{{ $v.ingress.host }}{{ $v.ingress.path }}
|
||||
{{- end -}}
|
||||
{{ end -}}
|
||||
|
99
generator/extrafiles/readme.go
Normal file
99
generator/extrafiles/readme.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package extrafiles
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type chart struct {
|
||||
Name string
|
||||
Description string
|
||||
Values []string
|
||||
}
|
||||
|
||||
//go:embed readme.tpl
|
||||
var readmeTemplate string
|
||||
|
||||
// ReadMeFile returns the content of the README.md file.
|
||||
func ReadMeFile(charname, description string, values map[string]any) string {
|
||||
|
||||
// values is a yaml structure with keys and structured values...
|
||||
// we want to make list of dot separated keys and their values
|
||||
|
||||
vv := map[string]any{}
|
||||
out, _ := yaml.Marshal(values)
|
||||
yaml.Unmarshal(out, &vv)
|
||||
|
||||
result := make(map[string]string)
|
||||
parseValues("", vv, result)
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"repeat": func(s string, count int) string {
|
||||
return strings.Repeat(s, count)
|
||||
},
|
||||
}
|
||||
tpl, err := template.New("readme").Funcs(funcMap).Parse(readmeTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
valuesLines := []string{}
|
||||
maxParamLen := 0
|
||||
maxDefaultLen := 0
|
||||
for key, value := range result {
|
||||
if len(key) > maxParamLen {
|
||||
maxParamLen = len(key)
|
||||
}
|
||||
if len(value) > maxDefaultLen {
|
||||
maxDefaultLen = len(value)
|
||||
}
|
||||
}
|
||||
for key, value := range result {
|
||||
valuesLines = append(valuesLines, fmt.Sprintf("| %-*s | %-*s |", maxParamLen, key, maxDefaultLen, value))
|
||||
}
|
||||
sort.Strings(valuesLines)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
err = tpl.Execute(buf, map[string]any{
|
||||
"DescrptionPadding": maxParamLen,
|
||||
"DefaultPadding": maxDefaultLen,
|
||||
"Chart": chart{
|
||||
Name: charname,
|
||||
Description: description,
|
||||
Values: valuesLines,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
32
generator/extrafiles/readme.tpl
Normal file
32
generator/extrafiles/readme.tpl
Normal file
@@ -0,0 +1,32 @@
|
||||
# {{ .Chart.Name }}
|
||||
|
||||
{{ .Chart.Description }}
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
To install the chart with the release name `my-release`:
|
||||
|
||||
```bash
|
||||
# Standard Helm install
|
||||
$ helm install my-release {{ .Chart.Name }}
|
||||
|
||||
# To use a custom namespace and force the creation of the namespace
|
||||
$ helm install my-release --namespace my-namespace --create-namespace {{ .Chart.Name }}
|
||||
|
||||
# To use a custom values file
|
||||
$ helm install my-release -f my-values.yaml {{ .Chart.Name }}
|
||||
```
|
||||
|
||||
See the [Helm documentation](https://helm.sh/docs/intro/using_helm/) for more information on installing and managing the chart.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following table lists the configurable parameters of the {{ .Chart.Name }} chart and their default values.
|
||||
|
||||
| {{ printf "%-*s" .DescrptionPadding "Parameter" }} | {{ printf "%-*s" .DefaultPadding "Default" }} |
|
||||
| {{ repeat "-" .DescrptionPadding }} | {{ repeat "-" .DefaultPadding }} |
|
||||
{{- range .Chart.Values }}
|
||||
{{ . }}
|
||||
{{- end }}
|
||||
|
||||
|
658
generator/generator.go
Normal file
658
generator/generator.go
Normal file
@@ -0,0 +1,658 @@
|
||||
package generator
|
||||
|
||||
// TODO: configmap from files 20%
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
goyaml "gopkg.in/yaml.v3"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Generate a chart from a compose project.
|
||||
// This does not write files to disk, it only creates the HelmChart object.
|
||||
//
|
||||
// The Generate function will create the HelmChart object this way:
|
||||
//
|
||||
// 1. Detect the service port name or leave the port number if not found.
|
||||
//
|
||||
// 2. Create a deployment for each service that are not ingnore.
|
||||
//
|
||||
// 3. Create a service and ingresses for each service that has ports and/or declared ingresses.
|
||||
//
|
||||
// 4. Create a PVC or Configmap volumes for each volume.
|
||||
//
|
||||
// 5. Create init containers for each service which has dependencies to other services.
|
||||
//
|
||||
// 6. Create a chart dependencies.
|
||||
//
|
||||
// 7. Create a configmap and secrets from the environment variables.
|
||||
//
|
||||
// 8. Merge the same-pod services.
|
||||
func Generate(project *types.Project) (*HelmChart, error) {
|
||||
|
||||
var (
|
||||
appName = project.Name
|
||||
deployments = make(map[string]*Deployment, len(project.Services))
|
||||
services = make(map[string]*Service)
|
||||
podToMerge = make(map[string]*Deployment)
|
||||
)
|
||||
chart := NewChart(appName)
|
||||
|
||||
// Add the compose files hash to the chart annotations
|
||||
hash, err := utils.HashComposefiles(project.ComposeFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Annotations[KATENARY_PREFIX+"compose-hash"] = hash
|
||||
chart.composeHash = &hash
|
||||
|
||||
// find the "main-app" label, and set chart.AppVersion to the tag if exists
|
||||
mainCount := 0
|
||||
for _, service := range project.Services {
|
||||
if serviceIsMain(service) {
|
||||
log.Printf("Found main app %s", service.Name)
|
||||
mainCount++
|
||||
if mainCount > 1 {
|
||||
return nil, fmt.Errorf("found more than one main app")
|
||||
}
|
||||
setChartVersion(chart, service)
|
||||
}
|
||||
}
|
||||
if mainCount == 0 {
|
||||
chart.AppVersion = "0.1.0"
|
||||
}
|
||||
|
||||
// first pass, create all deployments whatewer they are.
|
||||
for _, service := range project.Services {
|
||||
// check the "ports" label from container and add it to the service
|
||||
if err := fixPorts(&service); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// isgnored service
|
||||
if isIgnored(service) {
|
||||
fmt.Printf("%s Ignoring service %s\n", utils.IconInfo, service.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// helm dependency
|
||||
if isHelmDependency, err := setDependencies(chart, service); err != nil {
|
||||
return nil, err
|
||||
} else if isHelmDependency {
|
||||
continue
|
||||
}
|
||||
|
||||
// create all deployments
|
||||
d := NewDeployment(service, chart)
|
||||
deployments[service.Name] = d
|
||||
|
||||
// generate the cronjob if needed
|
||||
setCronJob(service, chart, appName)
|
||||
|
||||
// get the same-pod label if exists, add it to the list.
|
||||
// We later will copy some parts to the target deployment and remove this one.
|
||||
if samePod, ok := service.Labels[LABEL_SAME_POD]; ok && samePod != "" {
|
||||
podToMerge[samePod] = d
|
||||
}
|
||||
|
||||
// create the needed service for the container port
|
||||
if len(service.Ports) > 0 {
|
||||
s := NewService(service, appName)
|
||||
services[service.Name] = s
|
||||
}
|
||||
|
||||
// create all ingresses
|
||||
if ingress := d.AddIngress(service, appName); ingress != nil {
|
||||
y, _ := ingress.Yaml()
|
||||
chart.Templates[ingress.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now we have all deployments, we can create PVC if needed (it's separated from
|
||||
// the above loop because we need all deployments to not duplicate PVC for "same-pod" services)
|
||||
for _, service := range project.Services {
|
||||
if err := buildVolumes(service, chart, deployments); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// drop all "same-pod" deployments because the containers and volumes are already
|
||||
// in the target deployment
|
||||
for _, service := range project.Services {
|
||||
if samepod, ok := service.Labels[LABEL_SAME_POD]; ok && samepod != "" {
|
||||
// move this deployment volumes to the target deployment
|
||||
if target, ok := deployments[samepod]; ok {
|
||||
target.AddContainer(service)
|
||||
target.BindFrom(service, deployments[service.Name])
|
||||
delete(deployments, service.Name)
|
||||
} else {
|
||||
log.Printf("service %[1]s is declared as %[2]s, but %[2]s is not defined", service.Name, LABEL_SAME_POD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create init containers for all DependsOn
|
||||
for _, s := range project.Services {
|
||||
for _, d := range s.GetDependencies() {
|
||||
if dep, ok := deployments[d]; ok {
|
||||
deployments[s.Name].DependsOn(dep)
|
||||
} else {
|
||||
log.Printf("service %[1]s depends on %[2]s, but %[2]s is not defined", s.Name, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate configmaps with environment variables
|
||||
generateConfigMapsAndSecrets(project, chart)
|
||||
|
||||
// if the env-from label is set, we need to add the env vars from the configmap
|
||||
// to the environment of the service
|
||||
for _, s := range project.Services {
|
||||
setSharedConf(s, chart, deployments)
|
||||
}
|
||||
|
||||
// generate yaml files
|
||||
for _, d := range deployments {
|
||||
y, _ := d.Yaml()
|
||||
chart.Templates[d.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: d.service.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// generate all services
|
||||
for _, s := range services {
|
||||
y, _ := s.Yaml()
|
||||
chart.Templates[s.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: s.service.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// compute all needed resplacements in YAML templates
|
||||
for n, v := range chart.Templates {
|
||||
v.Content = removeReplaceString(v.Content)
|
||||
v.Content = computeNIndent(v.Content)
|
||||
chart.Templates[n].Content = v.Content
|
||||
}
|
||||
|
||||
// generate helper
|
||||
chart.Helper = Helper(appName)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func serviceIsMain(service types.ServiceConfig) bool {
|
||||
if main, ok := service.Labels[LABEL_MAIN_APP]; ok {
|
||||
return main == "true" || main == "yes" || main == "1"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setChartVersion(chart *HelmChart, service types.ServiceConfig) {
|
||||
if chart.Version == "" {
|
||||
image := service.Image
|
||||
parts := strings.Split(image, ":")
|
||||
if len(parts) > 1 {
|
||||
chart.AppVersion = parts[1]
|
||||
} else {
|
||||
chart.AppVersion = "0.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fixPorts(service *types.ServiceConfig) error {
|
||||
// check the "ports" label from container and add it to the service
|
||||
if portsLabel, ok := service.Labels[LABEL_PORTS]; ok {
|
||||
ports := []uint32{}
|
||||
if err := goyaml.Unmarshal([]byte(portsLabel), &ports); err != nil {
|
||||
// maybe it's a string, comma separated
|
||||
parts := strings.Split(portsLabel, ",")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
port, err := strconv.ParseUint(part, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ports = append(ports, uint32(port))
|
||||
}
|
||||
}
|
||||
for _, port := range ports {
|
||||
service.Ports = append(service.Ports, types.ServicePortConfig{
|
||||
Target: port,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCronJob(service types.ServiceConfig, chart *HelmChart, appName string) *CronJob {
|
||||
if _, ok := service.Labels[LABEL_CRONJOB]; !ok {
|
||||
return nil
|
||||
}
|
||||
cronjob, rbac := NewCronJob(service, chart, appName)
|
||||
y, _ := cronjob.Yaml()
|
||||
chart.Templates[cronjob.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
|
||||
if rbac != nil {
|
||||
y, _ := rbac.RoleBinding.Yaml()
|
||||
chart.Templates[rbac.RoleBinding.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
y, _ = rbac.Role.Yaml()
|
||||
chart.Templates[rbac.Role.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
y, _ = rbac.ServiceAccount.Yaml()
|
||||
chart.Templates[rbac.ServiceAccount.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return cronjob
|
||||
}
|
||||
|
||||
func setDependencies(chart *HelmChart, service types.ServiceConfig) (bool, error) {
|
||||
// helm dependency
|
||||
if v, ok := service.Labels[LABEL_DEPENDENCIES]; ok {
|
||||
d := Dependency{}
|
||||
if err := yaml.Unmarshal([]byte(v), &d); err != nil {
|
||||
return false, err
|
||||
}
|
||||
fmt.Printf("%s Adding dependency to %s\n", utils.IconDependency, d.Name)
|
||||
chart.Dependencies = append(chart.Dependencies, d)
|
||||
|
||||
name := d.Name
|
||||
if d.Alias != "" {
|
||||
name = d.Alias
|
||||
}
|
||||
// add the dependency env vars to the values.yaml
|
||||
chart.Values[name] = d.Values
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func isIgnored(service types.ServiceConfig) bool {
|
||||
if v, ok := service.Labels[LABEL_IGNORE]; ok {
|
||||
return v == "true" || v == "yes" || v == "1"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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[LABEL_SAME_POD]; ok {
|
||||
pvc.nameOverride = override
|
||||
pvc.PersistentVolumeClaim.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, //TODO, use name
|
||||
}
|
||||
|
||||
case "bind":
|
||||
// ensure the path is in labels
|
||||
bindPath := map[string]string{}
|
||||
if _, ok := service.Labels[LABEL_CM_FILES]; ok {
|
||||
files := []string{}
|
||||
if err := yaml.Unmarshal([]byte(service.Labels[LABEL_CM_FILES]), &files); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range files {
|
||||
bindPath[f] = f
|
||||
}
|
||||
}
|
||||
if _, ok := bindPath[v.Source]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
cm := NewConfigMapFromFiles(service, appName, v.Source)
|
||||
var err error
|
||||
var y []byte
|
||||
if y, err = cm.Yaml(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
chart.Templates[cm.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
|
||||
// continue with subdirectories
|
||||
stat, err := os.Stat(v.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stat.IsDir() {
|
||||
files, err := filepath.Glob(filepath.Join(v.Source, "*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range files {
|
||||
if f == v.Source {
|
||||
continue
|
||||
}
|
||||
if stat, err := os.Stat(f); err != nil || !stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
cm := NewConfigMapFromFiles(service, appName, f)
|
||||
var err error
|
||||
var y []byte
|
||||
if y, err = cm.Yaml(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Adding configmap %s %s", cm.Filename(), f)
|
||||
chart.Templates[cm.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: service.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateConfigMapsAndSecrets(project *types.Project, chart *HelmChart) error {
|
||||
appName := chart.Name
|
||||
for _, s := range project.Services {
|
||||
if s.Environment == nil || len(s.Environment) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
originalEnv := types.MappingWithEquals{}
|
||||
secretsVar := types.MappingWithEquals{}
|
||||
|
||||
// copy env to originalEnv
|
||||
for k, v := range s.Environment {
|
||||
originalEnv[k] = v
|
||||
}
|
||||
|
||||
if v, ok := s.Labels[LABEL_SECRETS]; ok {
|
||||
list := []string{}
|
||||
if err := yaml.Unmarshal([]byte(v), &list); err != nil {
|
||||
log.Fatal("error unmarshaling secrets label:", err)
|
||||
}
|
||||
for _, secret := range list {
|
||||
if secret == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := s.Environment[secret]; !ok {
|
||||
fmt.Printf("%s secret %s not found in environment", utils.IconWarning, secret)
|
||||
continue
|
||||
}
|
||||
secretsVar[secret] = s.Environment[secret]
|
||||
}
|
||||
}
|
||||
|
||||
if len(secretsVar) > 0 {
|
||||
s.Environment = secretsVar
|
||||
sec := NewSecret(s, appName)
|
||||
y, _ := sec.Yaml()
|
||||
name := sec.service.Name
|
||||
chart.Templates[name+".secret.yaml"] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: s.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// remove secrets from env
|
||||
s.Environment = originalEnv // back to original
|
||||
for k := range secretsVar {
|
||||
delete(s.Environment, k)
|
||||
}
|
||||
if len(s.Environment) > 0 {
|
||||
cm := NewConfigMap(s, appName)
|
||||
y, _ := cm.Yaml()
|
||||
name := cm.service.Name
|
||||
chart.Templates[name+".configmap.yaml"] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: s.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergePods(target, from *Deployment, services map[string]*Service, chart *HelmChart) {
|
||||
|
||||
targetName := target.service.Name
|
||||
fromName := from.service.Name
|
||||
|
||||
// copy the volumes from the source deployment
|
||||
for _, v := range from.Spec.Template.Spec.Volumes {
|
||||
// ensure that the volume is not already present
|
||||
found := false
|
||||
for _, tv := range target.Spec.Template.Spec.Volumes {
|
||||
if tv.Name == v.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
target.Spec.Template.Spec.Volumes = append(target.Spec.Template.Spec.Volumes, v)
|
||||
}
|
||||
// copy the containers from the source deployment
|
||||
for _, c := range from.Spec.Template.Spec.Containers {
|
||||
target.Spec.Template.Spec.Containers = append(target.Spec.Template.Spec.Containers, c)
|
||||
}
|
||||
// copy the init containers from the source deployment
|
||||
for _, c := range from.Spec.Template.Spec.InitContainers {
|
||||
target.Spec.Template.Spec.InitContainers = append(target.Spec.Template.Spec.InitContainers, c)
|
||||
}
|
||||
// drop the deployment from the chart
|
||||
delete(chart.Templates, fromName+".deployment.yaml")
|
||||
|
||||
// rewite the target deployment
|
||||
y, err := target.Yaml()
|
||||
if err != nil {
|
||||
log.Fatal("error rewriting deployment:", err)
|
||||
}
|
||||
chart.Templates[target.Filename()] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: targetName,
|
||||
}
|
||||
|
||||
// now, if the source deployment has a service, we need to merge it with the target service
|
||||
if _, ok := chart.Templates[targetName+".service.yaml"]; ok {
|
||||
container, _ := utils.GetContainerByName(fromName, target.Spec.Template.Spec.Containers)
|
||||
if container.Ports == nil || len(container.Ports) == 0 {
|
||||
return
|
||||
}
|
||||
targetService := services[targetName]
|
||||
for _, port := range container.Ports {
|
||||
targetService.AddPort(types.ServicePortConfig{
|
||||
Target: uint32(port.ContainerPort),
|
||||
Protocol: "TCP",
|
||||
}, port.Name)
|
||||
}
|
||||
// rewrite the tartget service
|
||||
y, _ := targetService.Yaml()
|
||||
chart.Templates[targetName+".service.yaml"] = &ChartTemplate{
|
||||
Content: y,
|
||||
Servicename: target.service.Name,
|
||||
}
|
||||
|
||||
// and remove the source service from the chart
|
||||
delete(chart.Templates, fromName+".service.yaml")
|
||||
|
||||
// In Valuses, remove the "replicas" key from the source service
|
||||
if v, ok := chart.Values[fromName]; ok {
|
||||
// if v is a Value
|
||||
if v, ok := v.(*Value); ok {
|
||||
v.Replicas = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, deployments map[string]*Deployment) bool {
|
||||
// if the service has volumes, and it has "same-pod" label
|
||||
// - get the target deployment
|
||||
// - check if it has the same volume
|
||||
// if not, return false
|
||||
|
||||
if v.Source == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if service.Volumes == nil || len(service.Volumes) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
targetDeployment := ""
|
||||
if targetName, ok := service.Labels[LABEL_SAME_POD]; !ok {
|
||||
return false
|
||||
} else {
|
||||
targetDeployment = targetName
|
||||
}
|
||||
|
||||
// get the target deployment
|
||||
var target *Deployment
|
||||
for _, d := range deployments {
|
||||
if d.service.Name == targetDeployment {
|
||||
target = d
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if it has the same volume
|
||||
for _, tv := range target.Spec.Template.Spec.Volumes {
|
||||
if tv.Name == v.Source {
|
||||
log.Printf("found same pod volume %s in deployment %s and %s", tv.Name, service.Name, targetDeployment)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setSharedConf(service types.ServiceConfig, chart *HelmChart, deployments map[string]*Deployment) {
|
||||
// if the service has the "shared-conf" label, we need to add the configmap
|
||||
// to the chart and add the env vars to the service
|
||||
if _, ok := service.Labels[LABEL_ENV_FROM]; !ok {
|
||||
return
|
||||
}
|
||||
fromservices := []string{}
|
||||
if err := yaml.Unmarshal([]byte(service.Labels[LABEL_ENV_FROM]), &fromservices); err != nil {
|
||||
log.Fatal("error unmarshaling env-from label:", err)
|
||||
}
|
||||
// find the configmap in the chart templates
|
||||
for _, fromservice := range fromservices {
|
||||
if _, ok := chart.Templates[fromservice+".configmap.yaml"]; !ok {
|
||||
log.Printf("configmap %s not found in chart templates", fromservice)
|
||||
continue
|
||||
}
|
||||
// find the corresponding target deployment
|
||||
var target *Deployment
|
||||
for _, d := range deployments {
|
||||
if d.service.Name == service.Name {
|
||||
target = d
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
// add the configmap to the service
|
||||
for i, c := range target.Spec.Template.Spec.Containers {
|
||||
if c.Name != service.Name {
|
||||
continue
|
||||
}
|
||||
c.EnvFrom = append(c.EnvFrom, corev1.EnvFromSource{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: utils.TplName(fromservice, chart.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
target.Spec.Template.Spec.Containers[i] = c
|
||||
}
|
||||
}
|
||||
|
||||
}
|
19
generator/globals.go
Normal file
19
generator/globals.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package generator
|
||||
|
||||
import "regexp"
|
||||
|
||||
var (
|
||||
// regexp to all tpl strings
|
||||
tplValueRegexp = regexp.MustCompile(`\{\{.*\}\}-`)
|
||||
|
||||
// find all labels starting by __replace_ and ending with ":"
|
||||
// and get the value between the quotes
|
||||
// ?s => multiline
|
||||
// (?P<inc>.+?) => named capture group to "inc" variable (so we could use $inc in the replace)
|
||||
replaceLabelRegexp = regexp.MustCompile(`(?s)__replace_.+?: '(?P<inc>.+?)'`)
|
||||
|
||||
// Standard annotationss
|
||||
Annotations = map[string]string{
|
||||
KATENARY_PREFIX + "version": Version,
|
||||
}
|
||||
)
|
36
generator/helmHelper.tpl
Normal file
36
generator/helmHelper.tpl
Normal file
@@ -0,0 +1,36 @@
|
||||
{{- define "__APP__.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "__APP__.name" -}}
|
||||
{{- if .Values.nameOverride -}}
|
||||
{{- .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "__APP__.labels" -}}
|
||||
{{ include "__APP__.selectorLabels" .}}
|
||||
{{ if .Chart.Version -}}
|
||||
{{ printf "__PREFIX__chart-version: %s" .Chart.Version }}
|
||||
{{- end }}
|
||||
{{ if .Chart.AppVersion -}}
|
||||
{{ printf "__PREFIX__app-version: %s" .Chart.AppVersion }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "__APP__.selectorLabels" -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{ printf "__PREFIX__name: %s" $name }}
|
||||
{{ printf "__PREFIX__instance: %s" .Release.Name }}
|
||||
{{- end -}}
|
19
generator/helper.go
Normal file
19
generator/helper.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// helmHelper is a template for the _helpers.tpl file in the chart templates directory.
|
||||
//
|
||||
//go:embed helmHelper.tpl
|
||||
var helmHelper string
|
||||
|
||||
// Helper returns the _helpers.tpl file for a chart.
|
||||
func Helper(name string) string {
|
||||
helmHelper := strings.ReplaceAll(helmHelper, "__APP__", name)
|
||||
helmHelper = strings.ReplaceAll(helmHelper, "__PREFIX__", KATENARY_PREFIX)
|
||||
helmHelper = strings.ReplaceAll(helmHelper, "__VERSION__", "0.1.0")
|
||||
return helmHelper
|
||||
}
|
175
generator/ingress.go
Normal file
175
generator/ingress.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/utils"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
goyaml "gopkg.in/yaml.v3"
|
||||
networkv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var _ Yaml = (*Ingress)(nil)
|
||||
|
||||
type Ingress struct {
|
||||
*networkv1.Ingress
|
||||
service *types.ServiceConfig `yaml:"-"`
|
||||
}
|
||||
|
||||
// NewIngress creates a new Ingress from a compose service.
|
||||
func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress {
|
||||
|
||||
appName := Chart.Name
|
||||
|
||||
// parse the KATENARY_PREFIX/ingress label from the service
|
||||
if service.Labels == nil {
|
||||
service.Labels = make(map[string]string)
|
||||
}
|
||||
var label string
|
||||
var ok bool
|
||||
if label, ok = service.Labels[LABEL_INGRESS]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
mapping := map[string]interface{}{
|
||||
"enabled": false,
|
||||
"host": service.Name + ".tld",
|
||||
"path": "/",
|
||||
"class": "-",
|
||||
}
|
||||
if err := goyaml.Unmarshal([]byte(label), &mapping); err != nil {
|
||||
log.Fatalf("Failed to parse ingress label: %s\n", err)
|
||||
}
|
||||
|
||||
// create the ingress
|
||||
pathType := networkv1.PathTypeImplementationSpecific
|
||||
serviceName := `{{ include "` + appName + `.fullname" . }}-` + service.Name
|
||||
if v, ok := mapping["port"]; ok {
|
||||
if port, ok := v.(int); ok {
|
||||
mapping["port"] = int32(port)
|
||||
}
|
||||
} else {
|
||||
log.Fatalf("No port provided for ingress target in service %s\n", service.Name)
|
||||
}
|
||||
|
||||
// Add the ingress host to the values.yaml
|
||||
if Chart.Values[service.Name] == nil {
|
||||
Chart.Values[service.Name] = &Value{}
|
||||
}
|
||||
Chart.Values[service.Name].(*Value).Ingress = &IngressValue{
|
||||
Enabled: mapping["enabled"].(bool),
|
||||
Path: mapping["path"].(string),
|
||||
Host: mapping["host"].(string),
|
||||
Class: mapping["class"].(string),
|
||||
Annotations: map[string]string{},
|
||||
}
|
||||
|
||||
//ingressClassName := `{{ .Values.` + service.Name + `.ingress.class }}`
|
||||
ingressClassName := utils.TplValue(service.Name, "ingress.class")
|
||||
|
||||
servicePortName := utils.GetServiceNameByPort(int(mapping["port"].(int32)))
|
||||
ingressService := &networkv1.IngressServiceBackend{
|
||||
Name: serviceName,
|
||||
Port: networkv1.ServiceBackendPort{},
|
||||
}
|
||||
if servicePortName != "" {
|
||||
ingressService.Port.Name = servicePortName
|
||||
} else {
|
||||
ingressService.Port.Number = mapping["port"].(int32)
|
||||
}
|
||||
|
||||
ing := &Ingress{
|
||||
service: &service,
|
||||
Ingress: &networkv1.Ingress{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Ingress",
|
||||
APIVersion: "networking.k8s.io/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Spec: networkv1.IngressSpec{
|
||||
IngressClassName: &ingressClassName,
|
||||
Rules: []networkv1.IngressRule{
|
||||
{
|
||||
Host: utils.TplValue(service.Name, "ingress.host"),
|
||||
IngressRuleValue: networkv1.IngressRuleValue{
|
||||
HTTP: &networkv1.HTTPIngressRuleValue{
|
||||
Paths: []networkv1.HTTPIngressPath{
|
||||
{
|
||||
Path: utils.TplValue(service.Name, "ingress.path"),
|
||||
PathType: &pathType,
|
||||
Backend: networkv1.IngressBackend{
|
||||
Service: ingressService,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: []networkv1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{
|
||||
`{{ tpl .Values.` + service.Name + `.ingress.host . }}`,
|
||||
},
|
||||
SecretName: `{{ include "` + appName + `.fullname" . }}-` + service.Name + `-tls`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return ing
|
||||
}
|
||||
|
||||
func (ingress *Ingress) Yaml() ([]byte, error) {
|
||||
serviceName := ingress.service.Name
|
||||
ret, err := yaml.Marshal(ingress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(ret), "\n")
|
||||
out := []string{
|
||||
`{{- if .Values.` + serviceName + `.ingress.enabled -}}`,
|
||||
}
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "loadBalancer: ") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, "labels:") {
|
||||
// add annotations above labels from values.yaml
|
||||
content := `` +
|
||||
` {{- if .Values.` + serviceName + `.ingress.annotations -}}` + "\n" +
|
||||
` {{- toYaml .Values.` + serviceName + `.ingress.annotations | nindent 4 }}` + "\n" +
|
||||
` {{- end }}` + "\n" +
|
||||
line
|
||||
|
||||
out = append(out, content)
|
||||
} else if strings.Contains(line, "ingressClassName: ") {
|
||||
content := utils.Wrap(
|
||||
line,
|
||||
`{{- if ne .Values.`+serviceName+`.ingress.class "-" }}`,
|
||||
`{{- end }}`,
|
||||
)
|
||||
out = append(out, content)
|
||||
} else {
|
||||
out = append(out, line)
|
||||
}
|
||||
}
|
||||
out = append(out, `{{- end -}}`)
|
||||
ret = []byte(strings.Join(out, "\n"))
|
||||
return ret, nil
|
||||
|
||||
}
|
||||
|
||||
func (ingress *Ingress) Filename() string {
|
||||
return ingress.service.Name + ".ingress.yaml"
|
||||
}
|
229
generator/katenaryLabels.go
Normal file
229
generator/katenaryLabels.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"katenary/utils"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"text/template"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
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 KATENARY_PREFIX = "katenary.v3/"
|
||||
|
||||
// Known labels.
|
||||
const (
|
||||
LABEL_MAIN_APP Label = KATENARY_PREFIX + "main-app"
|
||||
LABEL_VALUES Label = KATENARY_PREFIX + "values"
|
||||
LABEL_SECRETS Label = KATENARY_PREFIX + "secrets"
|
||||
LABEL_PORTS Label = KATENARY_PREFIX + "ports"
|
||||
LABEL_INGRESS Label = KATENARY_PREFIX + "ingress"
|
||||
LABEL_MAP_ENV Label = KATENARY_PREFIX + "map-env"
|
||||
LABEL_HEALTHCHECK Label = KATENARY_PREFIX + "health-check"
|
||||
LABEL_SAME_POD Label = KATENARY_PREFIX + "same-pod"
|
||||
LABEL_DESCRIPTION Label = KATENARY_PREFIX + "description"
|
||||
LABEL_IGNORE Label = KATENARY_PREFIX + "ignore"
|
||||
LABEL_DEPENDENCIES Label = KATENARY_PREFIX + "dependencies"
|
||||
LABEL_CM_FILES Label = KATENARY_PREFIX + "configmap-files"
|
||||
LABEL_CRONJOB Label = KATENARY_PREFIX + "cronjob"
|
||||
LABEL_ENV_FROM Label = KATENARY_PREFIX + "env-from"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := yaml.Unmarshal(labelFullHelpYAML, &labelFullHelp); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the help for the labels.
|
||||
func GetLabelHelp(asMarkdown bool) string {
|
||||
names := GetLabelNames() // sorted
|
||||
if !asMarkdown {
|
||||
return generatePlainHelp(names)
|
||||
}
|
||||
return generateMarkdownHelp(names)
|
||||
}
|
||||
|
||||
func generatePlainHelp(names []string) string {
|
||||
var builder strings.Builder
|
||||
for _, name := range names {
|
||||
help := labelFullHelp[name]
|
||||
fmt.Fprintf(&builder, "%s%s:\t%s\t%s\n", KATENARY_PREFIX, 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(KATENARY_PREFIX))
|
||||
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, "`"+KATENARY_PREFIX+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]
|
||||
if !ok {
|
||||
return "No help available for " + labelname + "."
|
||||
}
|
||||
|
||||
help.Long = strings.TrimPrefix(help.Long, "\n")
|
||||
help.Example = strings.TrimPrefix(help.Example, "\n")
|
||||
help.Short = strings.TrimPrefix(help.Short, "\n")
|
||||
|
||||
// get help template
|
||||
helpTemplate := getHelpTemplate(asMarkdown)
|
||||
|
||||
if asMarkdown {
|
||||
// enclose templates in backticks
|
||||
help.Long = regexp.MustCompile(`\{\{(.*?)\}\}`).ReplaceAllString(help.Long, "`{{$1}}`")
|
||||
help.Long = strings.ReplaceAll(help.Long, "__APP__", "`__APP__`")
|
||||
} else {
|
||||
help.Long = strings.ReplaceAll(help.Long, " \n", "\n")
|
||||
help.Long = strings.ReplaceAll(help.Long, "`", "")
|
||||
help.Long = strings.ReplaceAll(help.Long, "<code>", "")
|
||||
help.Long = strings.ReplaceAll(help.Long, "</code>", "")
|
||||
help.Long = utils.WordWrap(help.Long, 80)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
template.Must(template.New("shorthelp").Parse(help.Long)).Execute(&buf, struct {
|
||||
KATENARY_PREFIX string
|
||||
}{
|
||||
KATENARY_PREFIX: KATENARY_PREFIX,
|
||||
})
|
||||
help.Long = buf.String()
|
||||
buf.Reset()
|
||||
|
||||
template.Must(template.New("example").Parse(help.Example)).Execute(&buf, struct {
|
||||
KATENARY_PREFIX string
|
||||
}{
|
||||
KATENARY_PREFIX: KATENARY_PREFIX,
|
||||
})
|
||||
help.Example = buf.String()
|
||||
buf.Reset()
|
||||
|
||||
template.Must(template.New("complete").Parse(helpTemplate)).Execute(&buf, struct {
|
||||
Name string
|
||||
Help Help
|
||||
KATENARY_PREFIX string
|
||||
}{
|
||||
Name: labelname,
|
||||
Help: help,
|
||||
KATENARY_PREFIX: KATENARY_PREFIX,
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func getHelpTemplate(asMarkdown bool) string {
|
||||
if asMarkdown {
|
||||
return `## {{ .KATENARY_PREFIX }}{{ .Name }}
|
||||
|
||||
{{ .Help.Short }}
|
||||
|
||||
**Type**: ` + "`" + `{{ .Help.Type }}` + "`" + `
|
||||
|
||||
{{ .Help.Long }}
|
||||
|
||||
**Example:**` + "\n\n```yaml\n" + `{{ .Help.Example }}` + "\n```\n"
|
||||
}
|
||||
|
||||
return `{{ .KATENARY_PREFIX }}{{ .Name }}: {{ .Help.Short }}
|
||||
Type: {{ .Help.Type }}
|
||||
|
||||
{{ .Help.Long }}
|
||||
|
||||
Example:
|
||||
{{ .Help.Example }}
|
||||
`
|
||||
|
||||
}
|
36
generator/labels.go
Normal file
36
generator/labels.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// LabelType identifies the type of label to generate in objects.
|
||||
// TODO: is this still needed?
|
||||
type LabelType uint8
|
||||
|
||||
const (
|
||||
DeploymentLabel LabelType = iota
|
||||
ServiceLabel
|
||||
)
|
||||
|
||||
func GetLabels(serviceName, appName string) map[string]string {
|
||||
labels := map[string]string{
|
||||
KATENARY_PREFIX + "component": serviceName,
|
||||
}
|
||||
|
||||
key := `{{- include "%s.labels" . | nindent __indent__ }}`
|
||||
labels[`__replace_`+serviceName] = fmt.Sprintf(key, appName)
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func GetMatchLabels(serviceName, appName string) map[string]string {
|
||||
labels := map[string]string{
|
||||
KATENARY_PREFIX + "component": serviceName,
|
||||
}
|
||||
|
||||
key := `{{- include "%s.selectorLabels" . | nindent __indent__ }}`
|
||||
labels[`__replace_`+serviceName] = fmt.Sprintf(key, appName)
|
||||
|
||||
return labels
|
||||
}
|
@@ -1,304 +0,0 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"katenary/helm"
|
||||
"katenary/logger"
|
||||
"katenary/tools"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
)
|
||||
|
||||
type EnvVal = helm.EnvValue
|
||||
|
||||
const (
|
||||
ICON_PACKAGE = "📦"
|
||||
ICON_SERVICE = "🔌"
|
||||
ICON_SECRET = "🔏"
|
||||
ICON_CONF = "📝"
|
||||
ICON_STORE = "⚡"
|
||||
ICON_INGRESS = "🌐"
|
||||
ICON_RBAC = "🔑"
|
||||
ICON_CRON = "🕒"
|
||||
)
|
||||
|
||||
var (
|
||||
EmptyDirs = []string{}
|
||||
servicesMap = make(map[string]int)
|
||||
locker = &sync.Mutex{}
|
||||
|
||||
dependScript = `
|
||||
OK=0
|
||||
echo "Checking __service__ port"
|
||||
while [ $OK != 1 ]; do
|
||||
echo -n "."
|
||||
nc -z ` + helm.ReleaseNameTpl + `-__service__ __port__ 2>&1 >/dev/null && OK=1 || sleep 1
|
||||
done
|
||||
echo
|
||||
echo "Done"
|
||||
`
|
||||
|
||||
madeDeployments = make(map[string]helm.Deployment, 0)
|
||||
)
|
||||
|
||||
// Create a Deployment for a given compose.Service. It returns a list chan
|
||||
// of HelmFileGenerator which will be used to generate the files (deployment, secrets, configMap...).
|
||||
func CreateReplicaObject(name string, s types.ServiceConfig, linked map[string]types.ServiceConfig) HelmFileGenerator {
|
||||
ret := make(chan HelmFile, runtime.NumCPU())
|
||||
// there is a bug woth typs.ServiceConfig if we use the pointer. So we need to dereference it.
|
||||
go buildDeployment(name, &s, linked, ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
// Create a service (k8s).
|
||||
func generateServicesAndIngresses(name string, s *types.ServiceConfig) []HelmFile {
|
||||
|
||||
ret := make([]HelmFile, 0) // can handle helm.Service or helm.Ingress
|
||||
logger.Magenta(ICON_SERVICE+" Generating service for ", name)
|
||||
ks := helm.NewService(name)
|
||||
|
||||
for _, p := range s.Ports {
|
||||
target := int(p.Target)
|
||||
ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(target, target))
|
||||
}
|
||||
ks.Spec.Selector = buildSelector(name, s)
|
||||
|
||||
ret = append(ret, ks)
|
||||
if v, ok := s.Labels[helm.LABEL_INGRESS]; ok {
|
||||
port, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
log.Fatalf("The given port \"%v\" as ingress port in \"%s\" service is not an integer\n", v, name)
|
||||
}
|
||||
logger.Cyanf(ICON_INGRESS+" Create an ingress for port %d on %s service\n", port, name)
|
||||
ing := createIngress(name, port, s)
|
||||
ret = append(ret, ing)
|
||||
}
|
||||
|
||||
if len(s.Expose) > 0 {
|
||||
logger.Magenta(ICON_SERVICE+" Generating service for ", name+"-external")
|
||||
ks := helm.NewService(name + "-external")
|
||||
ks.Spec.Type = "NodePort"
|
||||
for _, expose := range s.Expose {
|
||||
|
||||
p, _ := strconv.Atoi(expose)
|
||||
ks.Spec.Ports = append(ks.Spec.Ports, helm.NewServicePort(p, p))
|
||||
}
|
||||
ks.Spec.Selector = buildSelector(name, s)
|
||||
ret = append(ret, ks)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Create an ingress.
|
||||
func createIngress(name string, port int, s *types.ServiceConfig) *helm.Ingress {
|
||||
ingress := helm.NewIngress(name)
|
||||
|
||||
annotations := map[string]string{}
|
||||
ingressVal := map[string]interface{}{
|
||||
"class": "nginx",
|
||||
"host": name + "." + helm.Appname + ".tld",
|
||||
"enabled": false,
|
||||
"annotations": annotations,
|
||||
}
|
||||
|
||||
// add Annotations in values
|
||||
AddValues(name, map[string]EnvVal{"ingress": ingressVal})
|
||||
|
||||
ingress.Spec.Rules = []helm.IngressRule{
|
||||
{
|
||||
Host: fmt.Sprintf("{{ .Values.%s.ingress.host }}", name),
|
||||
Http: helm.IngressHttp{
|
||||
Paths: []helm.IngressPath{{
|
||||
Path: "/",
|
||||
PathType: "Prefix",
|
||||
Backend: &helm.IngressBackend{
|
||||
Service: helm.IngressService{
|
||||
Name: helm.ReleaseNameTpl + "-" + name,
|
||||
Port: map[string]interface{}{
|
||||
"number": port,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
ingress.SetIngressClass(name)
|
||||
|
||||
return ingress
|
||||
}
|
||||
|
||||
// Build the selector for the service.
|
||||
func buildSelector(name string, s *types.ServiceConfig) map[string]string {
|
||||
return map[string]string{
|
||||
"katenary.io/component": name,
|
||||
"katenary.io/release": helm.ReleaseNameTpl,
|
||||
}
|
||||
}
|
||||
|
||||
// buildConfigMapFromPath generates a ConfigMap from a path.
|
||||
func buildConfigMapFromPath(name, path string) *helm.ConfigMap {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
files := make(map[string]string, 0)
|
||||
if stat.IsDir() {
|
||||
found, _ := filepath.Glob(path + "/*")
|
||||
for _, f := range found {
|
||||
if s, err := os.Stat(f); err != nil || s.IsDir() {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "An error occured reading volume path %s\n", err.Error())
|
||||
} else {
|
||||
logger.ActivateColors = true
|
||||
logger.Yellowf("Warning, %s is a directory, at this time we only "+
|
||||
"can create configmap for first level file list\n", f)
|
||||
logger.ActivateColors = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, filename := filepath.Split(f)
|
||||
c, _ := ioutil.ReadFile(f)
|
||||
files[filename] = string(c)
|
||||
}
|
||||
} else {
|
||||
c, _ := ioutil.ReadFile(path)
|
||||
_, filename := filepath.Split(path)
|
||||
files[filename] = string(c)
|
||||
}
|
||||
|
||||
cm := helm.NewConfigMap(name, tools.GetRelPath(path))
|
||||
cm.Data = files
|
||||
return cm
|
||||
}
|
||||
|
||||
// prepareProbes generate http/tcp/command probes for a service.
|
||||
func prepareProbes(name string, s *types.ServiceConfig, container *helm.Container) {
|
||||
// first, check if there a label for the probe
|
||||
if check, ok := s.Labels[helm.LABEL_HEALTHCHECK]; ok {
|
||||
check = strings.TrimSpace(check)
|
||||
p := helm.NewProbeFromService(s)
|
||||
// get the port of the "url" check
|
||||
if checkurl, err := url.Parse(check); err == nil {
|
||||
if err == nil {
|
||||
container.LivenessProbe = buildProtoProbe(p, checkurl)
|
||||
}
|
||||
} else {
|
||||
// it's a command
|
||||
container.LivenessProbe = p
|
||||
container.LivenessProbe.Exec = &helm.Exec{
|
||||
Command: []string{
|
||||
"sh",
|
||||
"-c",
|
||||
check,
|
||||
},
|
||||
}
|
||||
}
|
||||
return // label overrides everything
|
||||
}
|
||||
|
||||
// if not, we will use the default one
|
||||
if s.HealthCheck != nil {
|
||||
container.LivenessProbe = buildCommandProbe(s)
|
||||
}
|
||||
}
|
||||
|
||||
// buildProtoProbe builds a probe from a url that can be http or tcp.
|
||||
func buildProtoProbe(probe *helm.Probe, u *url.URL) *helm.Probe {
|
||||
port, err := strconv.Atoi(u.Port())
|
||||
if err != nil {
|
||||
port = 80
|
||||
}
|
||||
|
||||
path := "/"
|
||||
if u.Path != "" {
|
||||
path = u.Path
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
probe.HttpGet = &helm.HttpGet{
|
||||
Path: path,
|
||||
Port: port,
|
||||
}
|
||||
case "tcp":
|
||||
probe.TCP = &helm.TCP{
|
||||
Port: port,
|
||||
}
|
||||
default:
|
||||
logger.Redf("Error while parsing healthcheck url %s\n", u.String())
|
||||
os.Exit(1)
|
||||
}
|
||||
return probe
|
||||
}
|
||||
|
||||
func buildCommandProbe(s *types.ServiceConfig) *helm.Probe {
|
||||
|
||||
// Get the first element of the command from ServiceConfig
|
||||
first := s.HealthCheck.Test[0]
|
||||
|
||||
p := helm.NewProbeFromService(s)
|
||||
switch first {
|
||||
case "CMD", "CMD-SHELL":
|
||||
// CMD or CMD-SHELL
|
||||
p.Exec = &helm.Exec{
|
||||
Command: s.HealthCheck.Test[1:],
|
||||
}
|
||||
return p
|
||||
default:
|
||||
// badly made but it should work...
|
||||
p.Exec = &helm.Exec{
|
||||
Command: []string(s.HealthCheck.Test),
|
||||
}
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
func setSecretVar(name string, s *types.ServiceConfig, c *helm.Container) *helm.Secret {
|
||||
// get the list of secret vars
|
||||
secretvars, ok := s.Labels[helm.LABEL_SECRETVARS]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
store := helm.NewSecret(name, "")
|
||||
for _, secretvar := range strings.Split(secretvars, ",") {
|
||||
secretvar = strings.TrimSpace(secretvar)
|
||||
// get the value from env
|
||||
_, ok := s.Environment[secretvar]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// add the secret
|
||||
store.AddEnv(secretvar, ".Values."+name+".environment."+secretvar)
|
||||
AddEnvironment(name, secretvar, *s.Environment[secretvar])
|
||||
|
||||
// Finally remove the secret var from the environment on the service
|
||||
// and the helm container definition.
|
||||
defer func(secretvar string) { // defered because AddEnvironment locks the memory
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
for i, env := range c.Env {
|
||||
if env.Name == secretvar {
|
||||
c.Env = append(c.Env[:i], c.Env[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
||||
delete(s.Environment, secretvar)
|
||||
}(secretvar)
|
||||
}
|
||||
return store
|
||||
}
|
@@ -1,397 +0,0 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"katenary/compose"
|
||||
"katenary/helm"
|
||||
"katenary/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
)
|
||||
|
||||
const DOCKER_COMPOSE_YML = `version: '3'
|
||||
services:
|
||||
# first service, very simple
|
||||
http:
|
||||
image: nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
# second service, with environment variables
|
||||
http2:
|
||||
image: nginx
|
||||
environment:
|
||||
SOME_ENV_VAR: some_value
|
||||
ANOTHER_ENV_VAR: another_value
|
||||
|
||||
# third service with ingress label
|
||||
web:
|
||||
image: nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
labels:
|
||||
katenary.io/ingress: 80
|
||||
|
||||
web2:
|
||||
image: nginx
|
||||
command: ["/bin/sh", "-c", "while true; do echo hello; sleep 1; done"]
|
||||
|
||||
# fourth service is a php service depending on database
|
||||
php:
|
||||
image: php:7.2-apache
|
||||
depends_on:
|
||||
- database
|
||||
environment:
|
||||
SOME_ENV_VAR: some_value
|
||||
ANOTHER_ENV_VAR: another_value
|
||||
DB_HOST: database
|
||||
labels:
|
||||
katenary.io/mapenv: |
|
||||
DB_HOST: {{ .Release.Name }}-database
|
||||
|
||||
database:
|
||||
image: mysql:5.7
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: database
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: password
|
||||
volumes:
|
||||
- data:/var/lib/mysql
|
||||
labels:
|
||||
katenary.io/ports: 3306
|
||||
|
||||
|
||||
# try to deploy 2 services but one is in the same pod than the other
|
||||
http3:
|
||||
image: nginx
|
||||
|
||||
http4:
|
||||
image: nginx
|
||||
labels:
|
||||
katenary.io/same-pod: http3
|
||||
|
||||
# unmapped volumes
|
||||
novol:
|
||||
image: nginx
|
||||
volumes:
|
||||
- /tmp/data
|
||||
labels:
|
||||
katenary.io/ports: 80
|
||||
|
||||
# use = sign for environment variables
|
||||
eqenv:
|
||||
image: nginx
|
||||
environment:
|
||||
- SOME_ENV_VAR=some_value
|
||||
- ANOTHER_ENV_VAR=another_value
|
||||
|
||||
# use environment file
|
||||
useenvfile:
|
||||
image: nginx
|
||||
env_file:
|
||||
- config/env
|
||||
|
||||
volumes:
|
||||
data:
|
||||
`
|
||||
|
||||
var defaultCliFiles = cli.DefaultFileNames
|
||||
var TMP_DIR = ""
|
||||
var TMPWORK_DIR = ""
|
||||
|
||||
func init() {
|
||||
logger.NOLOG = len(os.Getenv("NOLOG")) < 1
|
||||
}
|
||||
|
||||
func setUp(t *testing.T) (string, *compose.Parser) {
|
||||
|
||||
// cleanup "made" files
|
||||
helm.ResetMadePVC()
|
||||
|
||||
cli.DefaultFileNames = defaultCliFiles
|
||||
|
||||
// create a temporary directory
|
||||
tmp, err := os.MkdirTemp(os.TempDir(), "katenary-test-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tmpwork, err := os.MkdirTemp(os.TempDir(), "katenary-test-work-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
composefile := filepath.Join(tmpwork, "docker-compose.yaml")
|
||||
p := compose.NewParser([]string{composefile}, DOCKER_COMPOSE_YML)
|
||||
|
||||
// create envfile for "useenvfile" service
|
||||
err = os.Mkdir(filepath.Join(tmpwork, "config"), 0777)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
envfile := filepath.Join(tmpwork, "config", "env")
|
||||
fp, err := os.Create(envfile)
|
||||
if err != nil {
|
||||
t.Fatal("MKFILE", err)
|
||||
}
|
||||
fp.WriteString("FILEENV1=some_value\n")
|
||||
fp.WriteString("FILEENV2=another_value\n")
|
||||
fp.Close()
|
||||
|
||||
TMP_DIR = tmp
|
||||
TMPWORK_DIR = tmpwork
|
||||
|
||||
p.Parse("testapp")
|
||||
|
||||
Generate(p, "test-0", "testapp", "1.2.3", "4.5.6", DOCKER_COMPOSE_YML, tmp)
|
||||
|
||||
return tmp, p
|
||||
}
|
||||
|
||||
func tearDown() {
|
||||
if len(TMP_DIR) > 0 {
|
||||
os.RemoveAll(TMP_DIR)
|
||||
}
|
||||
if len(TMPWORK_DIR) > 0 {
|
||||
os.RemoveAll(TMPWORK_DIR)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the web2 service has got a command.
|
||||
func TestCommand(t *testing.T) {
|
||||
tmp, p := setUp(t)
|
||||
defer tearDown()
|
||||
|
||||
for _, service := range p.Data.Services {
|
||||
name := service.Name
|
||||
if name == "web2" {
|
||||
// Ensure that the command is correctly set
|
||||
// The command should be a string array
|
||||
path := filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||
path = filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||
fp, _ := os.Open(path)
|
||||
defer fp.Close()
|
||||
lines, _ := ioutil.ReadAll(fp)
|
||||
next := false
|
||||
commands := make([]string, 0)
|
||||
for _, line := range strings.Split(string(lines), "\n") {
|
||||
if strings.Contains(line, "command") {
|
||||
next = true
|
||||
continue
|
||||
}
|
||||
if next {
|
||||
commands = append(commands, line)
|
||||
}
|
||||
}
|
||||
ok := 0
|
||||
for _, command := range commands {
|
||||
if strings.Contains(command, "- /bin/sh") {
|
||||
ok++
|
||||
}
|
||||
if strings.Contains(command, "- -c") {
|
||||
ok++
|
||||
}
|
||||
if strings.Contains(command, "while true; do") {
|
||||
ok++
|
||||
}
|
||||
}
|
||||
if ok != 3 {
|
||||
t.Error("Command is not correctly set")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if environment is correctly set.
|
||||
func TestEnvs(t *testing.T) {
|
||||
tmp, p := setUp(t)
|
||||
defer tearDown()
|
||||
|
||||
for _, service := range p.Data.Services {
|
||||
name := service.Name
|
||||
|
||||
if name == "php" {
|
||||
// the "DB_HOST" environment variable inside the template must be set to '{{ .Release.Name }}-database'
|
||||
path := filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||
// read the file and find the DB_HOST variable
|
||||
matched := false
|
||||
fp, _ := os.Open(path)
|
||||
defer fp.Close()
|
||||
lines, _ := ioutil.ReadAll(fp)
|
||||
next := false
|
||||
for _, line := range strings.Split(string(lines), "\n") {
|
||||
if !next && strings.Contains(line, "name: DB_HOST") {
|
||||
next = true
|
||||
continue
|
||||
} else if next && strings.Contains(line, "value:") {
|
||||
matched = true
|
||||
if !strings.Contains(line, "{{ tpl .Values.php.environment.DB_HOST . }}") {
|
||||
t.Error("DB_HOST variable should be set to {{ tpl .Values.php.environment.DB_HOST . }}", line, string(lines))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
t.Error("DB_HOST variable not found in ", path)
|
||||
t.Log(string(lines))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the same pod is not deployed twice.
|
||||
func TestSamePod(t *testing.T) {
|
||||
tmp, p := setUp(t)
|
||||
defer tearDown()
|
||||
|
||||
for _, service := range p.Data.Services {
|
||||
name := service.Name
|
||||
path := filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||
|
||||
if _, found := service.Labels[helm.LABEL_SAMEPOD]; found {
|
||||
// fail if the service has a deployment
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
t.Error("Service ", name, " should not have a deployment")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// others should have a deployment file
|
||||
t.Log("Checking ", name, " deployment file")
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the ports are correctly set.
|
||||
func TestPorts(t *testing.T) {
|
||||
tmp, p := setUp(t)
|
||||
defer tearDown()
|
||||
|
||||
for _, service := range p.Data.Services {
|
||||
name := service.Name
|
||||
path := ""
|
||||
|
||||
// if the service has a port found in helm.LABEL_PORT or ports, so the service file should exist
|
||||
hasPort := false
|
||||
if _, found := service.Labels[helm.LABEL_PORT]; found {
|
||||
hasPort = true
|
||||
}
|
||||
if service.Ports != nil {
|
||||
hasPort = true
|
||||
}
|
||||
if hasPort {
|
||||
path = filepath.Join(tmp, "templates", name+".service.yaml")
|
||||
t.Log("Checking ", name, " service file")
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the volumes are correctly set.
|
||||
func TestPVC(t *testing.T) {
|
||||
tmp, p := setUp(t)
|
||||
defer tearDown()
|
||||
|
||||
for _, service := range p.Data.Services {
|
||||
name := service.Name
|
||||
path := filepath.Join(tmp, "templates", name+"-data.pvc.yaml")
|
||||
|
||||
// the "database" service should have a pvc file in templates (name-data.pvc.yaml)
|
||||
if name == "database" {
|
||||
path = filepath.Join(tmp, "templates", name+"-data.pvc.yaml")
|
||||
t.Log("Checking ", name, " pvc file")
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
list, _ := filepath.Glob(tmp + "/templates/*")
|
||||
t.Log(list)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Check if web service has got a ingress.
|
||||
func TestIngress(t *testing.T) {
|
||||
tmp, p := setUp(t)
|
||||
defer tearDown()
|
||||
|
||||
for _, service := range p.Data.Services {
|
||||
name := service.Name
|
||||
path := filepath.Join(tmp, "templates", name+".ingress.yaml")
|
||||
|
||||
// the "web" service should have a ingress file in templates (name.ingress.yaml)
|
||||
if name == "web" {
|
||||
path = filepath.Join(tmp, "templates", name+".ingress.yaml")
|
||||
t.Log("Checking ", name, " ingress file")
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check unmapped volumes
|
||||
func TestUnmappedVolumes(t *testing.T) {
|
||||
tmp, p := setUp(t)
|
||||
defer tearDown()
|
||||
|
||||
for _, service := range p.Data.Services {
|
||||
name := service.Name
|
||||
if name == "novol" {
|
||||
path := filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||
fp, _ := os.Open(path)
|
||||
defer fp.Close()
|
||||
lines, _ := ioutil.ReadAll(fp)
|
||||
for _, line := range strings.Split(string(lines), "\n") {
|
||||
if strings.Contains(line, "novol-data") {
|
||||
t.Error("novol service should not have a volume")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if service using equal sign for environment works
|
||||
func TestEqualSignOnEnv(t *testing.T) {
|
||||
tmp, p := setUp(t)
|
||||
defer tearDown()
|
||||
|
||||
// if the name is eqenv, the service should habe environment
|
||||
for _, service := range p.Data.Services {
|
||||
name := service.Name
|
||||
if name == "eqenv" {
|
||||
path := filepath.Join(tmp, "templates", name+".deployment.yaml")
|
||||
fp, _ := os.Open(path)
|
||||
defer fp.Close()
|
||||
lines, _ := ioutil.ReadAll(fp)
|
||||
match := 0
|
||||
for _, line := range strings.Split(string(lines), "\n") {
|
||||
// we must find the line with the environment variable name
|
||||
if strings.Contains(line, "SOME_ENV_VAR") {
|
||||
// we must find the line with the environment variable value
|
||||
match++
|
||||
}
|
||||
if strings.Contains(line, "ANOTHER_ENV_VAR") {
|
||||
// we must find the line with the environment variable value
|
||||
match++
|
||||
}
|
||||
}
|
||||
if match != 4 { // because the value points on .Values...
|
||||
t.Error("eqenv service should have 2 environment variables")
|
||||
t.Log(string(lines))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
139
generator/rbac.go
Normal file
139
generator/rbac.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/utils"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Yaml = (*RoleBinding)(nil)
|
||||
_ Yaml = (*Role)(nil)
|
||||
_ Yaml = (*ServiceAccount)(nil)
|
||||
)
|
||||
|
||||
// RBAC is a kubernetes RBAC containing a role, a rolebinding and an associated serviceaccount.
|
||||
type RBAC struct {
|
||||
RoleBinding *RoleBinding
|
||||
Role *Role
|
||||
ServiceAccount *ServiceAccount
|
||||
}
|
||||
|
||||
// NewRBAC creates a new RBAC from a compose service. The appName is the name of the application taken from the project name.
|
||||
func NewRBAC(service types.ServiceConfig, appName string) *RBAC {
|
||||
role := &Role{
|
||||
Role: &rbacv1.Role{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Role",
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"", "extensions", "apps"},
|
||||
Resources: []string{"*"},
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
service: &service,
|
||||
}
|
||||
|
||||
rolebinding := &RoleBinding{
|
||||
RoleBinding: &rbacv1.RoleBinding{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "RoleBinding",
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Namespace: "{{ .Release.Namespace }}",
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
Kind: "Role",
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
},
|
||||
},
|
||||
service: &service,
|
||||
}
|
||||
|
||||
serviceaccount := &ServiceAccount{
|
||||
ServiceAccount: &corev1.ServiceAccount{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ServiceAccount",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
},
|
||||
service: &service,
|
||||
}
|
||||
|
||||
return &RBAC{
|
||||
RoleBinding: rolebinding,
|
||||
Role: role,
|
||||
ServiceAccount: serviceaccount,
|
||||
}
|
||||
}
|
||||
|
||||
// RoleBinding is a kubernetes RoleBinding.
|
||||
type RoleBinding struct {
|
||||
*rbacv1.RoleBinding
|
||||
service *types.ServiceConfig
|
||||
}
|
||||
|
||||
func (r *RoleBinding) Yaml() ([]byte, error) {
|
||||
return yaml.Marshal(r)
|
||||
}
|
||||
|
||||
func (r *RoleBinding) Filename() string {
|
||||
return r.service.Name + ".rolebinding.yaml"
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
111
generator/secret.go
Normal file
111
generator/secret.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"katenary/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var _ DataMap = (*Secret)(nil)
|
||||
var _ Yaml = (*Secret)(nil)
|
||||
|
||||
// Secret is a kubernetes Secret.
|
||||
//
|
||||
// Implements the DataMap interface.
|
||||
type Secret struct {
|
||||
*corev1.Secret
|
||||
service types.ServiceConfig `yaml:"-"`
|
||||
}
|
||||
|
||||
// NewSecret creates a new Secret from a compose service
|
||||
func NewSecret(service types.ServiceConfig, appName string) *Secret {
|
||||
secret := &Secret{
|
||||
service: service,
|
||||
Secret: &corev1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Data: make(map[string][]byte),
|
||||
},
|
||||
}
|
||||
|
||||
// check if the value should be in values.yaml
|
||||
valueList := []string{}
|
||||
varDescriptons := utils.GetValuesFromLabel(service, LABEL_VALUES)
|
||||
for value := range varDescriptons {
|
||||
valueList = append(valueList, value)
|
||||
}
|
||||
|
||||
// wrap values with quotes
|
||||
for _, value := range service.Environment {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
*value = fmt.Sprintf(`"%s"`, *value)
|
||||
}
|
||||
|
||||
for _, value := range valueList {
|
||||
if val, ok := service.Environment[value]; ok {
|
||||
value = strings.TrimPrefix(value, `"`)
|
||||
*val = `.Values.` + service.Name + `.environment.` + value
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range service.Environment {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
secret.AddData(key, *value)
|
||||
}
|
||||
|
||||
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, fmt.Sprintf("%s", value))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// AddData adds a key value pair to the secret.
|
||||
func (s *Secret) AddData(key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
s.Data[key] = []byte(`{{ tpl ` + value + ` $ | quote | b64enc }}`)
|
||||
}
|
||||
|
||||
// Yaml returns the yaml representation of the secret.
|
||||
func (s *Secret) Yaml() ([]byte, error) {
|
||||
y, err := yaml.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// replace the b64 value by the real value
|
||||
for _, value := range s.Data {
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(value))
|
||||
y = []byte(strings.ReplaceAll(string(y), encoded, string(value)))
|
||||
}
|
||||
|
||||
return y, nil
|
||||
}
|
||||
|
||||
// Filename returns the filename of the secret.
|
||||
func (s *Secret) Filename() string {
|
||||
return s.service.Name + ".secret.yaml"
|
||||
}
|
95
generator/service.go
Normal file
95
generator/service.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var _ Yaml = (*Service)(nil)
|
||||
|
||||
// Service is a kubernetes Service.
|
||||
type Service struct {
|
||||
*v1.Service `yaml:",inline"`
|
||||
service *types.ServiceConfig `yaml:"-"`
|
||||
}
|
||||
|
||||
// NewService creates a new Service from a compose service.
|
||||
func NewService(service types.ServiceConfig, appName string) *Service {
|
||||
|
||||
ports := []v1.ServicePort{}
|
||||
|
||||
s := &Service{
|
||||
service: &service,
|
||||
Service: &v1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName),
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: GetMatchLabels(service.Name, appName),
|
||||
Ports: ports,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, port := range service.Ports {
|
||||
s.AddPort(port)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// AddPort adds a port to the service.
|
||||
func (s *Service) AddPort(port types.ServicePortConfig, serviceName ...string) {
|
||||
name := s.service.Name
|
||||
if len(serviceName) > 0 {
|
||||
name = serviceName[0]
|
||||
}
|
||||
|
||||
var finalport intstr.IntOrString
|
||||
|
||||
if targetPort := utils.GetServiceNameByPort(int(port.Target)); targetPort == "" {
|
||||
finalport = intstr.FromInt(int(port.Target))
|
||||
} else {
|
||||
finalport = intstr.FromString(targetPort)
|
||||
name = targetPort
|
||||
}
|
||||
|
||||
s.Spec.Ports = append(s.Spec.Ports, v1.ServicePort{
|
||||
Protocol: v1.ProtocolTCP,
|
||||
Port: int32(port.Target),
|
||||
TargetPort: finalport,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
// Yaml returns the yaml representation of the service.
|
||||
func (s *Service) Yaml() ([]byte, error) {
|
||||
y, err := yaml.Marshal(s)
|
||||
lines := []string{}
|
||||
for _, line := range strings.Split(string(y), "\n") {
|
||||
if regexp.MustCompile(`^\s*loadBalancer:\s*`).MatchString(line) {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
y = []byte(strings.Join(lines, "\n"))
|
||||
|
||||
return y, err
|
||||
}
|
||||
|
||||
// Filename returns the filename of the service.
|
||||
func (s *Service) Filename() string {
|
||||
return s.service.Name + ".service.yaml"
|
||||
}
|
13
generator/types.go
Normal file
13
generator/types.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package generator
|
||||
|
||||
// DataMap is a kubernetes ConfigMap or Secret. It can be used to add data to the ConfigMap or Secret.
|
||||
type DataMap interface {
|
||||
SetData(map[string]string)
|
||||
AddData(string, string)
|
||||
}
|
||||
|
||||
// Yaml is a kubernetes object that can be converted to yaml.
|
||||
type Yaml interface {
|
||||
Yaml() ([]byte, error)
|
||||
Filename() string
|
||||
}
|
@@ -1,77 +1,121 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/helm"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
)
|
||||
|
||||
var (
|
||||
// Values is kept in memory to create a values.yaml file.
|
||||
Values = make(map[string]map[string]interface{})
|
||||
)
|
||||
// Values is a map of all values for all services. Written to values.yaml.
|
||||
// var Values = map[string]any{}
|
||||
|
||||
// AddValues adds values to the values.yaml map.
|
||||
func AddValues(servicename string, values map[string]EnvVal) {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
if _, ok := Values[servicename]; !ok {
|
||||
Values[servicename] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
for k, v := range values {
|
||||
Values[servicename][k] = v
|
||||
}
|
||||
// RepositoryValue is a docker repository image and tag that will be saved in values.yaml.
|
||||
type RepositoryValue struct {
|
||||
Image string `yaml:"image"`
|
||||
Tag string `yaml:"tag"`
|
||||
}
|
||||
|
||||
func AddEnvironment(servicename string, key string, val EnvVal) {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
if _, ok := Values[servicename]; !ok {
|
||||
Values[servicename] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if _, ok := Values[servicename]["environment"]; !ok {
|
||||
Values[servicename]["environment"] = make(map[string]EnvVal)
|
||||
}
|
||||
Values[servicename]["environment"].(map[string]EnvVal)[key] = val
|
||||
|
||||
// PersistenceValue is a persistence configuration that will be saved in values.yaml.
|
||||
type PersistenceValue struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
StorageClass string `yaml:"storageClass"`
|
||||
Size string `yaml:"size"`
|
||||
AccessMode []string `yaml:"accessMode"`
|
||||
}
|
||||
|
||||
// setEnvToValues will set the environment variables to the values.yaml map.
|
||||
func setEnvToValues(name string, s *types.ServiceConfig, c *helm.Container) {
|
||||
// crete the "environment" key
|
||||
// IngressValue is a ingress configuration that will be saved in values.yaml.
|
||||
type IngressValue struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Host string `yaml:"host"`
|
||||
Path string `yaml:"path"`
|
||||
Class string `yaml:"class"`
|
||||
Annotations map[string]string `yaml:"annotations"`
|
||||
}
|
||||
|
||||
env := make(map[string]EnvVal)
|
||||
for k, v := range s.Environment {
|
||||
env[k] = v
|
||||
}
|
||||
if len(env) == 0 {
|
||||
return
|
||||
// Value will be saved in values.yaml. It contains configuraiton for all deployment and services.
|
||||
// The content will be lile:
|
||||
//
|
||||
// name_of_component:
|
||||
// repository:
|
||||
// image: image_name
|
||||
// tag: image_tag
|
||||
// persistence:
|
||||
// enabled: true
|
||||
// storageClass: storage_class_name
|
||||
// ingress:
|
||||
// enabled: true
|
||||
// host: host_name
|
||||
// path: path_name
|
||||
// environment:
|
||||
// ENV_VAR_1: value_1
|
||||
// ENV_VAR_2: value_2
|
||||
type Value struct {
|
||||
Repository *RepositoryValue `yaml:"repository,omitempty"`
|
||||
Persistence map[string]*PersistenceValue `yaml:"persistence,omitempty"`
|
||||
Ingress *IngressValue `yaml:"ingress,omitempty"`
|
||||
ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
|
||||
Environment map[string]any `yaml:"environment,omitempty"`
|
||||
Replicas *uint32 `yaml:"replicas,omitempty"`
|
||||
CronJob *CronJobValue `yaml:"cronjob,omitempty"`
|
||||
}
|
||||
|
||||
// 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.).
|
||||
//
|
||||
// If `main` is true, the tag will be empty because
|
||||
// it will be set in the helm chart appVersion.
|
||||
func NewValue(service types.ServiceConfig, main ...bool) *Value {
|
||||
replicas := uint32(1)
|
||||
v := &Value{
|
||||
Replicas: &replicas,
|
||||
}
|
||||
|
||||
for k, v := range env {
|
||||
k = strings.ReplaceAll(k, ".", "_")
|
||||
AddEnvironment(name, k, v)
|
||||
// find the image tag
|
||||
tag := ""
|
||||
split := strings.Split(service.Image, ":")
|
||||
v.Repository = &RepositoryValue{
|
||||
Image: split[0],
|
||||
}
|
||||
|
||||
//AddValues(name, map[string]EnvVal{"environment": valuesEnv})
|
||||
for k := range env {
|
||||
fixedK := strings.ReplaceAll(k, ".", "_")
|
||||
v := "{{ tpl .Values." + name + ".environment." + fixedK + " . }}"
|
||||
s.Environment[k] = &v
|
||||
touched := false
|
||||
for _, c := range c.Env {
|
||||
if c.Name == k {
|
||||
c.Value = v
|
||||
touched = true
|
||||
}
|
||||
}
|
||||
if !touched {
|
||||
c.Env = append(c.Env, &helm.Value{Name: k, Value: v})
|
||||
// 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]
|
||||
}
|
||||
v.Repository.Tag = tag
|
||||
} else {
|
||||
v.Repository.Tag = ""
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// AddPersistence adds persistence configuration to the Value.
|
||||
func (v *Value) AddPersistence(volumeName string) {
|
||||
if v.Persistence == nil {
|
||||
v.Persistence = make(map[string]*PersistenceValue, 0)
|
||||
}
|
||||
v.Persistence[volumeName] = &PersistenceValue{
|
||||
Enabled: true,
|
||||
StorageClass: "-",
|
||||
Size: "1Gi",
|
||||
AccessMode: []string{"ReadWriteOnce"},
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Value) AddIngress(host, path string) {
|
||||
v.Ingress = &IngressValue{
|
||||
Enabled: true,
|
||||
Host: host,
|
||||
Path: path,
|
||||
Class: "-",
|
||||
}
|
||||
}
|
||||
|
4
generator/version.go
Normal file
4
generator/version.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package generator
|
||||
|
||||
// Version is the version of katenary. It is set at compile time.
|
||||
var Version = "master" // changed at compile time
|
119
generator/volume.go
Normal file
119
generator/volume.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var _ Yaml = (*VolumeClaim)(nil)
|
||||
|
||||
// VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim.
|
||||
type VolumeClaim struct {
|
||||
*v1.PersistentVolumeClaim
|
||||
service *types.ServiceConfig `yaml:"-"`
|
||||
volumeName string
|
||||
nameOverride string
|
||||
}
|
||||
|
||||
// NewVolumeClaim creates a new VolumeClaim from a compose service.
|
||||
func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *VolumeClaim {
|
||||
return &VolumeClaim{
|
||||
volumeName: volumeName,
|
||||
service: &service,
|
||||
PersistentVolumeClaim: &v1.PersistentVolumeClaim{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "PersistentVolumeClaim",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: utils.TplName(service.Name, appName) + "-" + volumeName,
|
||||
Labels: GetLabels(service.Name, appName),
|
||||
Annotations: Annotations,
|
||||
},
|
||||
Spec: v1.PersistentVolumeClaimSpec{
|
||||
AccessModes: []v1.PersistentVolumeAccessMode{
|
||||
v1.ReadWriteOnce,
|
||||
},
|
||||
StorageClassName: utils.StrPtr(`{{ .Values.` + service.Name + `.persistence.` + volumeName + `.storageClass }}`),
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceStorage: resource.MustParse("1Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Yaml marshals a VolumeClaim into yaml.
|
||||
func (v *VolumeClaim) Yaml() ([]byte, error) {
|
||||
serviceName := v.service.Name
|
||||
if v.nameOverride != "" {
|
||||
serviceName = v.nameOverride
|
||||
}
|
||||
volumeName := v.volumeName
|
||||
out, err := yaml.Marshal(v)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// replace 1Gi to {{ .Values.serviceName.volume.size }}
|
||||
out = []byte(
|
||||
strings.Replace(
|
||||
string(out),
|
||||
"1Gi",
|
||||
utils.TplValue(serviceName, "persistence."+volumeName+".size"),
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
||||
out = []byte(
|
||||
strings.Replace(
|
||||
string(out),
|
||||
"- ReadWriteOnce",
|
||||
"{{- .Values."+
|
||||
serviceName+
|
||||
".persistence."+
|
||||
volumeName+
|
||||
".accessMode | toYaml | nindent __indent__ }}",
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "storageClass") {
|
||||
lines[i] = utils.Wrap(
|
||||
line,
|
||||
"{{- if ne .Values."+serviceName+".persistence."+volumeName+".storageClass \"-\" }}",
|
||||
"{{- end }}",
|
||||
)
|
||||
}
|
||||
}
|
||||
out = []byte(strings.Join(lines, "\n"))
|
||||
|
||||
// add condition
|
||||
out = []byte(
|
||||
"{{- if .Values." +
|
||||
serviceName +
|
||||
".persistence." +
|
||||
volumeName +
|
||||
".enabled }}\n" +
|
||||
string(out) +
|
||||
"\n{{- end }}",
|
||||
)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Filename returns the suggested filename for a VolumeClaim.
|
||||
func (v *VolumeClaim) Filename() string {
|
||||
return v.service.Name + "." + v.volumeName + ".volumeclaim.yaml"
|
||||
}
|
@@ -1,236 +0,0 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/helm"
|
||||
"katenary/logger"
|
||||
"katenary/tools"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// VolumeValues is the map of volumes for each deployment
|
||||
// containing volume configuration
|
||||
VolumeValues = make(map[string]map[string]map[string]EnvVal)
|
||||
)
|
||||
|
||||
// AddVolumeValues add a volume to the values.yaml map for the given deployment name.
|
||||
func AddVolumeValues(deployment string, volname string, values map[string]EnvVal) {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
if _, ok := VolumeValues[deployment]; !ok {
|
||||
VolumeValues[deployment] = make(map[string]map[string]EnvVal)
|
||||
}
|
||||
VolumeValues[deployment][volname] = values
|
||||
}
|
||||
|
||||
// addVolumeFrom takes the LABEL_VOLUMEFROM to get volumes from another container. This can only work with
|
||||
// container that has got LABEL_SAMEPOD as we need to get the volumes from another container in the same deployment.
|
||||
func addVolumeFrom(deployment *helm.Deployment, container *helm.Container, s *types.ServiceConfig) {
|
||||
labelfrom, ok := s.Labels[helm.LABEL_VOLUMEFROM]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// decode Yaml from the label
|
||||
var volumesFrom map[string]map[string]string
|
||||
err := yaml.Unmarshal([]byte(labelfrom), &volumesFrom)
|
||||
if err != nil {
|
||||
logger.ActivateColors = true
|
||||
logger.Red(err.Error())
|
||||
logger.ActivateColors = false
|
||||
return
|
||||
}
|
||||
|
||||
// for each declared volume "from", we will find it from the deployment volumes and add it to the container.
|
||||
// Then, to avoid duplicates, we will remove it from the ServiceConfig object.
|
||||
for name, volumes := range volumesFrom {
|
||||
for volumeName := range volumes {
|
||||
initianame := volumeName
|
||||
volumeName = tools.PathToName(volumeName)
|
||||
// get the volume from the deployment container "name"
|
||||
var ctn *helm.Container
|
||||
for _, c := range deployment.Spec.Template.Spec.Containers {
|
||||
if c.Name == name {
|
||||
ctn = c
|
||||
break
|
||||
}
|
||||
}
|
||||
if ctn == nil {
|
||||
logger.ActivateColors = true
|
||||
logger.Redf("VolumeFrom: container %s not found", name)
|
||||
logger.ActivateColors = false
|
||||
continue
|
||||
}
|
||||
// get the volume from the container
|
||||
for _, v := range ctn.VolumeMounts {
|
||||
switch v := v.(type) {
|
||||
case map[string]interface{}:
|
||||
if v["name"] == volumeName {
|
||||
if container.VolumeMounts == nil {
|
||||
container.VolumeMounts = make([]interface{}, 0)
|
||||
}
|
||||
// make a copy of the volume mount and then add it to the VolumeMounts
|
||||
var mountpoint = make(map[string]interface{})
|
||||
for k, v := range v {
|
||||
mountpoint[k] = v
|
||||
}
|
||||
container.VolumeMounts = append(container.VolumeMounts, mountpoint)
|
||||
|
||||
// remove the volume from the ServiceConfig
|
||||
for i, vol := range s.Volumes {
|
||||
if vol.Source == initianame {
|
||||
s.Volumes = append(s.Volumes[:i], s.Volumes[i+1:]...)
|
||||
i--
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prepareVolumes add the volumes of a service.
|
||||
func prepareVolumes(
|
||||
deployment, name string,
|
||||
s *types.ServiceConfig,
|
||||
container *helm.Container,
|
||||
fileGeneratorChan HelmFileGenerator) []map[string]interface{} {
|
||||
|
||||
volumes := make([]map[string]interface{}, 0)
|
||||
mountPoints := make([]interface{}, 0)
|
||||
configMapsVolumes := make([]string, 0)
|
||||
if v, ok := s.Labels[helm.LABEL_VOL_CM]; ok {
|
||||
configMapsVolumes = strings.Split(v, ",")
|
||||
for i, cm := range configMapsVolumes {
|
||||
configMapsVolumes[i] = strings.TrimSpace(cm)
|
||||
}
|
||||
}
|
||||
|
||||
for _, vol := range s.Volumes {
|
||||
|
||||
volname := vol.Source
|
||||
volepath := vol.Target
|
||||
|
||||
if volname == "" {
|
||||
logger.ActivateColors = true
|
||||
logger.Yellowf("Warning, volume source to %s is empty for %s -- skipping\n", volepath, name)
|
||||
logger.ActivateColors = false
|
||||
continue
|
||||
}
|
||||
|
||||
isConfigMap := false
|
||||
for _, cmVol := range configMapsVolumes {
|
||||
if tools.GetRelPath(volname) == cmVol {
|
||||
isConfigMap = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// local volume cannt be mounted
|
||||
if !isConfigMap && (strings.HasPrefix(volname, ".") || strings.HasPrefix(volname, "/")) {
|
||||
logger.ActivateColors = true
|
||||
logger.Redf("You cannot, at this time, have local volume in %s deployment\n", name)
|
||||
logger.ActivateColors = false
|
||||
continue
|
||||
}
|
||||
if isConfigMap {
|
||||
// check if the volname path points on a file, if so, we need to add subvolume to the interface
|
||||
stat, err := os.Stat(volname)
|
||||
if err != nil {
|
||||
logger.ActivateColors = true
|
||||
logger.Redf("An error occured reading volume path %s\n", err.Error())
|
||||
logger.ActivateColors = false
|
||||
continue
|
||||
}
|
||||
pointToFile := ""
|
||||
if !stat.IsDir() {
|
||||
pointToFile = filepath.Base(volname)
|
||||
}
|
||||
|
||||
// the volume is a path and it's explicitally asked to be a configmap in labels
|
||||
cm := buildConfigMapFromPath(name, volname)
|
||||
cm.K8sBase.Metadata.Name = helm.ReleaseNameTpl + "-" + name + "-" + tools.PathToName(volname)
|
||||
|
||||
// build a configmapRef for this volume
|
||||
volname := tools.PathToName(volname)
|
||||
volumes = append(volumes, map[string]interface{}{
|
||||
"name": volname,
|
||||
"configMap": map[string]string{
|
||||
"name": cm.K8sBase.Metadata.Name,
|
||||
},
|
||||
})
|
||||
if len(pointToFile) > 0 {
|
||||
mountPoints = append(mountPoints, map[string]interface{}{
|
||||
"name": volname,
|
||||
"mountPath": volepath,
|
||||
"subPath": pointToFile,
|
||||
})
|
||||
} else {
|
||||
mountPoints = append(mountPoints, map[string]interface{}{
|
||||
"name": volname,
|
||||
"mountPath": volepath,
|
||||
})
|
||||
}
|
||||
if cm != nil {
|
||||
fileGeneratorChan <- cm
|
||||
}
|
||||
} else {
|
||||
// It's a Volume. Mount this from PVC to declare.
|
||||
|
||||
volname = strings.ReplaceAll(volname, "-", "")
|
||||
|
||||
isEmptyDir := false
|
||||
for _, v := range EmptyDirs {
|
||||
v = strings.ReplaceAll(v, "-", "")
|
||||
if v == volname {
|
||||
volumes = append(volumes, map[string]interface{}{
|
||||
"name": volname,
|
||||
"emptyDir": map[string]string{},
|
||||
})
|
||||
mountPoints = append(mountPoints, map[string]interface{}{
|
||||
"name": volname,
|
||||
"mountPath": volepath,
|
||||
})
|
||||
container.VolumeMounts = append(container.VolumeMounts, mountPoints...)
|
||||
isEmptyDir = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isEmptyDir {
|
||||
continue
|
||||
}
|
||||
|
||||
volumes = append(volumes, map[string]interface{}{
|
||||
"name": volname,
|
||||
"persistentVolumeClaim": map[string]string{
|
||||
"claimName": helm.ReleaseNameTpl + "-" + volname,
|
||||
},
|
||||
})
|
||||
mountPoints = append(mountPoints, map[string]interface{}{
|
||||
"name": volname,
|
||||
"mountPath": volepath,
|
||||
})
|
||||
|
||||
logger.Yellow(ICON_STORE+" Generate volume values", volname, "for container named", name, "in deployment", deployment)
|
||||
AddVolumeValues(deployment, volname, map[string]EnvVal{
|
||||
"enabled": false,
|
||||
"capacity": "1Gi",
|
||||
})
|
||||
|
||||
if pvc := helm.NewPVC(deployment, volname); pvc != nil {
|
||||
fileGeneratorChan <- pvc
|
||||
}
|
||||
}
|
||||
}
|
||||
// add the volume in the container and return the volume definition to add in Deployment
|
||||
container.VolumeMounts = append(container.VolumeMounts, mountPoints...)
|
||||
return volumes
|
||||
}
|
@@ -1,236 +0,0 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"katenary/compose"
|
||||
"katenary/generator/writers"
|
||||
"katenary/helm"
|
||||
"katenary/tools"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// HelmFile represents a helm file from helm package that has got some necessary methods
|
||||
// to generate a helm file.
|
||||
type HelmFile interface {
|
||||
GetType() string
|
||||
GetPathRessource() string
|
||||
}
|
||||
|
||||
// HelmFileGenerator is a chanel of HelmFile.
|
||||
type HelmFileGenerator chan HelmFile
|
||||
|
||||
var PrefixRE = regexp.MustCompile(`\{\{.*\}\}-?`)
|
||||
|
||||
func portExists(port int, ports []types.ServicePortConfig) bool {
|
||||
for _, p := range ports {
|
||||
if p.Target == uint32(port) {
|
||||
log.Println("portExists:", port, p.Target)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate get a parsed compose file, and generate the helm files.
|
||||
func Generate(p *compose.Parser, katernayVersion, appName, appVersion, chartVersion, composeFile, dirName string) {
|
||||
|
||||
// make the appname global (yes... ugly but easy)
|
||||
helm.Appname = appName
|
||||
helm.Version = katernayVersion
|
||||
templatesDir := filepath.Join(dirName, "templates")
|
||||
|
||||
// try to create the directory
|
||||
err := os.MkdirAll(templatesDir, 0755)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
generators := make(map[string]HelmFileGenerator)
|
||||
|
||||
// remove skipped services from the parsed data
|
||||
for i, service := range p.Data.Services {
|
||||
if v, ok := service.Labels[helm.LABEL_IGNORE]; !ok || v != "true" {
|
||||
continue
|
||||
}
|
||||
p.Data.Services = append(p.Data.Services[:i], p.Data.Services[i+1:]...)
|
||||
i--
|
||||
|
||||
// find this service in others as "depends_on" and remove it
|
||||
for _, service2 := range p.Data.Services {
|
||||
delete(service2.DependsOn, service.Name)
|
||||
}
|
||||
}
|
||||
|
||||
for i, service := range p.Data.Services {
|
||||
n := service.Name
|
||||
|
||||
// if the service port is declared in labels, add it to the service.
|
||||
if ports, ok := service.Labels[helm.LABEL_PORT]; ok {
|
||||
if service.Ports == nil {
|
||||
service.Ports = make([]types.ServicePortConfig, 0)
|
||||
}
|
||||
for _, port := range strings.Split(ports, ",") {
|
||||
port = strings.TrimSpace(port)
|
||||
target, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if portExists(target, service.Ports) {
|
||||
continue
|
||||
}
|
||||
service.Ports = append(service.Ports, types.ServicePortConfig{
|
||||
Target: uint32(target),
|
||||
})
|
||||
}
|
||||
}
|
||||
// find port and store it in servicesMap
|
||||
for _, port := range service.Ports {
|
||||
target := int(port.Target)
|
||||
if target != 0 {
|
||||
servicesMap[n] = target
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// manage emptyDir volumes
|
||||
if empty, ok := service.Labels[helm.LABEL_EMPTYDIRS]; ok {
|
||||
//split empty list by coma
|
||||
emptyDirs := strings.Split(empty, ",")
|
||||
for i, emptyDir := range emptyDirs {
|
||||
emptyDirs[i] = strings.TrimSpace(emptyDir)
|
||||
}
|
||||
//append them in EmptyDirs
|
||||
EmptyDirs = append(EmptyDirs, emptyDirs...)
|
||||
}
|
||||
p.Data.Services[i] = service
|
||||
|
||||
}
|
||||
|
||||
// for all services in linked map, and not in samePods map, generate the service
|
||||
for _, s := range p.Data.Services {
|
||||
name := s.Name
|
||||
|
||||
// do not make a deployment for services declared to be in the same pod than another
|
||||
if _, ok := s.Labels[helm.LABEL_SAMEPOD]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// find services that is in the same pod
|
||||
linked := make(map[string]types.ServiceConfig, 0)
|
||||
for _, service := range p.Data.Services {
|
||||
n := service.Name
|
||||
if linkname, ok := service.Labels[helm.LABEL_SAMEPOD]; ok && linkname == name {
|
||||
linked[n] = service
|
||||
delete(s.DependsOn, n)
|
||||
}
|
||||
}
|
||||
|
||||
generators[name] = CreateReplicaObject(name, s, linked)
|
||||
}
|
||||
|
||||
// to generate notes, we need to keep an Ingresses list
|
||||
ingresses := make(map[string]*helm.Ingress)
|
||||
|
||||
for n, generator := range generators { // generators is a map : name -> generator
|
||||
for helmFile := range generator { // generator is a chan
|
||||
if helmFile == nil { // generator finished
|
||||
break
|
||||
}
|
||||
kind := helmFile.(helm.Kinded).Get()
|
||||
kind = strings.ToLower(kind)
|
||||
|
||||
// Add a SHA inside the generated file, it's only
|
||||
// to make it easy to check it the compose file corresponds to the
|
||||
// generated helm chart
|
||||
helmFile.(helm.Signable).BuildSHA(composeFile)
|
||||
|
||||
// Some types need special fixes in yaml generation
|
||||
switch c := helmFile.(type) {
|
||||
case *helm.Storage:
|
||||
// For storage, we need to add a "condition" to activate it
|
||||
writers.BuildStorage(c, n, templatesDir)
|
||||
|
||||
case *helm.Deployment:
|
||||
// for the deployment, we need to fix persitence volumes
|
||||
// to be activated only when the storage is "enabled",
|
||||
// either we use an "emptyDir"
|
||||
writers.BuildDeployment(c, n, templatesDir)
|
||||
|
||||
case *helm.Service:
|
||||
// Change the type for service if it's an "exposed" port
|
||||
writers.BuildService(c, n, templatesDir)
|
||||
|
||||
case *helm.Ingress:
|
||||
// we need to make ingresses "activable" from values
|
||||
ingresses[n] = c // keep it to generate notes
|
||||
writers.BuildIngress(c, n, templatesDir)
|
||||
|
||||
case *helm.ConfigMap, *helm.Secret:
|
||||
// there could be several files, so let's force the filename
|
||||
name := c.(helm.Named).Name() + "." + c.GetType()
|
||||
suffix := c.GetPathRessource()
|
||||
suffix = tools.PathToName(suffix)
|
||||
name += suffix
|
||||
name = PrefixRE.ReplaceAllString(name, "")
|
||||
writers.BuildConfigMap(c, kind, n, name, templatesDir)
|
||||
|
||||
default:
|
||||
name := c.(helm.Named).Name() + "." + c.GetType()
|
||||
name = PrefixRE.ReplaceAllString(name, "")
|
||||
fname := filepath.Join(templatesDir, name+".yaml")
|
||||
fp, err := os.Create(fname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer fp.Close()
|
||||
enc := yaml.NewEncoder(fp)
|
||||
enc.SetIndent(writers.IndentSize)
|
||||
enc.Encode(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create the values.yaml file
|
||||
valueFile, err := os.Create(filepath.Join(dirName, "values.yaml"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer valueFile.Close()
|
||||
enc := yaml.NewEncoder(valueFile)
|
||||
enc.SetIndent(writers.IndentSize)
|
||||
enc.Encode(Values)
|
||||
|
||||
// Create tht Chart.yaml file
|
||||
chartFile, err := os.Create(filepath.Join(dirName, "Chart.yaml"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer chartFile.Close()
|
||||
chartFile.WriteString(`# Create on ` + time.Now().Format(time.RFC3339) + "\n")
|
||||
chartFile.WriteString(`# Katenary command line: ` + strings.Join(os.Args, " ") + "\n")
|
||||
enc = yaml.NewEncoder(chartFile)
|
||||
enc.SetIndent(writers.IndentSize)
|
||||
enc.Encode(map[string]interface{}{
|
||||
"apiVersion": "v2",
|
||||
"name": appName,
|
||||
"description": "A helm chart for " + appName,
|
||||
"type": "application",
|
||||
"version": chartVersion,
|
||||
"appVersion": appVersion,
|
||||
})
|
||||
|
||||
// And finally, create a NOTE.txt file
|
||||
noteFile, err := os.Create(filepath.Join(templatesDir, "NOTES.txt"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer noteFile.Close()
|
||||
noteFile.WriteString(helm.GenerateNotesFile(ingresses))
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
package writers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// BuildConfigMap writes the configMap.
|
||||
func BuildConfigMap(c interface{}, kind, servicename, name, templatesDir string) {
|
||||
fname := filepath.Join(templatesDir, name+"."+kind+".yaml")
|
||||
fp, _ := os.Create(fname)
|
||||
enc := yaml.NewEncoder(fp)
|
||||
enc.SetIndent(IndentSize)
|
||||
enc.Encode(c)
|
||||
fp.Close()
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
package writers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"katenary/helm"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// BuildDeployment builds a deployment.
|
||||
func BuildDeployment(deployment *helm.Deployment, name, templatesDir string) {
|
||||
kind := "deployment"
|
||||
fname := filepath.Join(templatesDir, name+"."+kind+".yaml")
|
||||
fp, _ := os.Create(fname)
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
enc := yaml.NewEncoder(buffer)
|
||||
enc.SetIndent(IndentSize)
|
||||
enc.Encode(deployment)
|
||||
_content := string(buffer.Bytes())
|
||||
content := strings.Split(string(_content), "\n")
|
||||
dataname := ""
|
||||
component := deployment.Spec.Selector["matchLabels"].(map[string]string)[helm.K+"/component"]
|
||||
n := 0 // will be count of lines only on "persistentVolumeClaim" line, to indent "else" and "end" at the right place
|
||||
for _, line := range content {
|
||||
if strings.Contains(line, "name:") {
|
||||
dataname = strings.Split(line, ":")[1]
|
||||
dataname = strings.TrimSpace(dataname)
|
||||
} else if strings.Contains(line, "persistentVolumeClaim") {
|
||||
n = CountSpaces(line)
|
||||
line = strings.Repeat(" ", n) + "{{- if .Values." + component + ".persistence." + dataname + ".enabled }}\n" + line
|
||||
} else if strings.Contains(line, "claimName") {
|
||||
spaces := strings.Repeat(" ", n)
|
||||
line += "\n" + spaces + "{{ else }}"
|
||||
line += "\n" + spaces + "emptyDir: {}"
|
||||
line += "\n" + spaces + "{{- end }}"
|
||||
}
|
||||
fp.WriteString(line + "\n")
|
||||
}
|
||||
fp.Close()
|
||||
|
||||
}
|
@@ -1,101 +0,0 @@
|
||||
package writers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"katenary/helm"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
classAndVersionCondition = `{{- if and .Values.__name__.ingress.class (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}` + "\n"
|
||||
versionCondition118 = `{{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion }}` + "\n"
|
||||
versionCondition119 = `{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }}` + "\n"
|
||||
apiVersion = `{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}`
|
||||
)
|
||||
|
||||
// BuildIngress generates the ingress yaml file with conditions.
|
||||
func BuildIngress(ingress *helm.Ingress, name, templatesDir string) {
|
||||
// Set the backend for 1.18
|
||||
for _, b := range ingress.Spec.Rules {
|
||||
for _, p := range b.Http.Paths {
|
||||
p.Backend.ServiceName = p.Backend.Service.Name
|
||||
if n, ok := p.Backend.Service.Port["number"]; ok {
|
||||
p.Backend.ServicePort = n
|
||||
}
|
||||
}
|
||||
}
|
||||
kind := "ingress"
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
fname := filepath.Join(templatesDir, name+"."+kind+".yaml")
|
||||
enc := yaml.NewEncoder(buffer)
|
||||
enc.SetIndent(IndentSize)
|
||||
buffer.WriteString("{{- if .Values." + name + ".ingress.enabled -}}\n")
|
||||
enc.Encode(ingress)
|
||||
buffer.WriteString("{{- end -}}")
|
||||
|
||||
fp, err := os.Create(fname)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
content := string(buffer.Bytes())
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
backendHit := false
|
||||
for _, l := range lines {
|
||||
// apiVersion is a pain...
|
||||
if strings.Contains(l, "apiVersion:") {
|
||||
l = apiVersion
|
||||
}
|
||||
|
||||
// add annotations linked to the Values
|
||||
if strings.Contains(l, "annotations:") {
|
||||
n := CountSpaces(l) + IndentSize
|
||||
l += "\n" + strings.Repeat(" ", n) + "{{- range $k, $v := .Values.__name__.ingress.annotations }}\n"
|
||||
l += strings.Repeat(" ", n) + "{{ $k }}: {{ $v }}\n"
|
||||
l += strings.Repeat(" ", n) + "{{- end }}"
|
||||
l = strings.ReplaceAll(l, "__name__", name)
|
||||
}
|
||||
|
||||
// pathTyype is ony for 1.19+
|
||||
if strings.Contains(l, "pathType:") {
|
||||
n := CountSpaces(l)
|
||||
l = strings.Repeat(" ", n) + versionCondition118 +
|
||||
l + "\n" +
|
||||
strings.Repeat(" ", n) + "{{- end }}"
|
||||
}
|
||||
|
||||
if strings.Contains(l, "ingressClassName") {
|
||||
// should be set only if the version of Kubernetes is 1.18-0 or higher
|
||||
cond := strings.ReplaceAll(classAndVersionCondition, "__name__", name)
|
||||
l = ` ` + cond + l + "\n" + ` {{- end }}`
|
||||
}
|
||||
|
||||
// manage the backend format following the Kubernetes 1.19-0 version or higher
|
||||
if strings.Contains(l, "service:") {
|
||||
n := CountSpaces(l)
|
||||
l = strings.Repeat(" ", n) + versionCondition119 + l
|
||||
}
|
||||
if strings.Contains(l, "serviceName:") || strings.Contains(l, "servicePort:") {
|
||||
n := CountSpaces(l)
|
||||
if !backendHit {
|
||||
l = strings.Repeat(" ", n) + "{{- else }}\n" + l
|
||||
} else {
|
||||
l = l + "\n" + strings.Repeat(" ", n) + "{{- end }}\n"
|
||||
}
|
||||
backendHit = true
|
||||
}
|
||||
fp.WriteString(l + "\n")
|
||||
}
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
package writers
|
||||
|
||||
import (
|
||||
"katenary/helm"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// BuildService writes the service (external or not).
|
||||
func BuildService(service *helm.Service, name, templatesDir string) {
|
||||
kind := "service"
|
||||
suffix := ""
|
||||
if service.Spec.Type == "NodePort" {
|
||||
suffix = "-external"
|
||||
}
|
||||
fname := filepath.Join(templatesDir, name+suffix+"."+kind+".yaml")
|
||||
fp, _ := os.Create(fname)
|
||||
enc := yaml.NewEncoder(fp)
|
||||
enc.SetIndent(IndentSize)
|
||||
enc.Encode(service)
|
||||
fp.Close()
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
package writers
|
||||
|
||||
import (
|
||||
"katenary/helm"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// BuildStorage writes the persistentVolumeClaim.
|
||||
func BuildStorage(storage *helm.Storage, name, templatesDir string) {
|
||||
kind := "pvc"
|
||||
name = storage.Metadata.Labels[helm.K+"/component"]
|
||||
pvcname := storage.Metadata.Labels[helm.K+"/pvc-name"]
|
||||
fname := filepath.Join(templatesDir, name+"-"+pvcname+"."+kind+".yaml")
|
||||
fp, err := os.Create(fname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer fp.Close()
|
||||
volname := storage.K8sBase.Metadata.Labels[helm.K+"/pvc-name"]
|
||||
|
||||
fp.WriteString("{{ if .Values." + name + ".persistence." + volname + ".enabled }}\n")
|
||||
enc := yaml.NewEncoder(fp)
|
||||
enc.SetIndent(IndentSize)
|
||||
if err := enc.Encode(storage); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fp.WriteString("{{- end -}}")
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
package writers
|
||||
|
||||
// IndentSize set the indentation size for yaml output. Could ba changed by command line argument.
|
||||
var IndentSize = 2
|
||||
|
||||
// CountSpaces returns the number of spaces from the begining of the line.
|
||||
func CountSpaces(line string) int {
|
||||
var spaces int
|
||||
for _, char := range line {
|
||||
if char == ' ' {
|
||||
spaces++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return spaces
|
||||
}
|
Reference in New Issue
Block a user