2023-12-06 15:24:02 +01:00
|
|
|
package generator
|
|
|
|
|
2024-05-06 21:11:36 +02:00
|
|
|
import (
|
|
|
|
"fmt"
|
2024-11-18 17:12:12 +01:00
|
|
|
"katenary/generator/labels"
|
|
|
|
"katenary/generator/labels/labelStructs"
|
2024-10-17 17:08:42 +02:00
|
|
|
"katenary/utils"
|
2024-05-07 13:18:00 +02:00
|
|
|
"log"
|
2025-06-26 23:57:19 +02:00
|
|
|
"maps"
|
2024-05-06 21:11:36 +02:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2025-06-26 23:57:19 +02:00
|
|
|
"slices"
|
2024-05-06 21:11:36 +02:00
|
|
|
"strings"
|
|
|
|
|
2024-05-07 13:18:00 +02:00
|
|
|
"github.com/compose-spec/compose-go/types"
|
2024-11-26 16:11:12 +01:00
|
|
|
corev1 "k8s.io/api/core/v1"
|
2024-05-06 21:11:36 +02:00
|
|
|
)
|
|
|
|
|
2024-10-17 17:08:42 +02:00
|
|
|
// ChartTemplate is a template of a chart. It contains the content of the template and the name of the service.
|
|
|
|
// This is used internally to generate the templates.
|
|
|
|
type ChartTemplate struct {
|
|
|
|
Servicename string
|
|
|
|
Content []byte
|
|
|
|
}
|
|
|
|
|
2024-05-06 21:11:36 +02:00
|
|
|
// ConvertOptions are the options to convert a compose project to a helm chart.
|
|
|
|
type ConvertOptions struct {
|
|
|
|
AppVersion *string
|
|
|
|
OutputDir string
|
|
|
|
ChartVersion string
|
2024-10-17 17:08:42 +02:00
|
|
|
Icon string
|
2024-05-06 21:11:36 +02:00
|
|
|
Profiles []string
|
2024-10-24 17:24:36 +02:00
|
|
|
EnvFiles []string
|
2024-05-06 21:11:36 +02:00
|
|
|
Force bool
|
|
|
|
HelmUpdate bool
|
2023-12-06 15:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// HelmChart is a Helm Chart representation. It contains all the
|
2024-11-25 12:00:04 +01:00
|
|
|
// templates, values, versions, helpers...
|
2023-12-06 15:24:02 +01:00
|
|
|
type HelmChart struct {
|
2024-05-06 21:11:36 +02:00
|
|
|
Templates map[string]*ChartTemplate `yaml:"-"`
|
|
|
|
Values map[string]any `yaml:"-"`
|
|
|
|
VolumeMounts map[string]any `yaml:"-"`
|
|
|
|
composeHash *string `yaml:"-"`
|
2023-12-06 15:24:02 +01:00
|
|
|
Name string `yaml:"name"`
|
2024-10-17 17:08:42 +02:00
|
|
|
Icon string `yaml:"icon,omitempty"`
|
2025-06-26 23:25:12 +02:00
|
|
|
APIVersion string `yaml:"apiVersion"`
|
2023-12-06 15:24:02 +01:00
|
|
|
Version string `yaml:"version"`
|
|
|
|
AppVersion string `yaml:"appVersion"`
|
|
|
|
Description string `yaml:"description"`
|
2024-05-06 21:11:36 +02:00
|
|
|
Helper string `yaml:"-"`
|
2024-04-24 23:06:45 +02:00
|
|
|
Dependencies []labelStructs.Dependency `yaml:"dependencies,omitempty"`
|
2023-12-06 15:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
2025-06-26 23:25:12 +02:00
|
|
|
APIVersion: "v2",
|
2023-12-06 15:24:02 +01:00
|
|
|
Version: "",
|
|
|
|
AppVersion: "", // set to 0.1.0 by default if no "main-app" label is found
|
|
|
|
Values: map[string]any{
|
|
|
|
"pullSecrets": []string{},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-06 21:11:36 +02:00
|
|
|
// SaveTemplates the templates of the chart to the given directory.
|
|
|
|
func (chart *HelmChart) SaveTemplates(templateDir string) {
|
|
|
|
for name, template := range chart.Templates {
|
|
|
|
t := template.Content
|
|
|
|
t = removeNewlinesInsideBrackets(t)
|
|
|
|
t = removeUnwantedLines(t)
|
2024-10-17 17:08:42 +02:00
|
|
|
// t = addModeline(t)
|
2024-05-06 21:11:36 +02:00
|
|
|
|
|
|
|
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
|
2025-07-06 10:51:09 +02:00
|
|
|
if err := os.MkdirAll(filepath.Join(templateDir, servicename), 0o600); err != nil {
|
2024-05-06 21:11:36 +02:00
|
|
|
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)
|
2025-07-06 10:51:09 +02:00
|
|
|
err := os.MkdirAll(filepath.Dir(name), 0o600)
|
2024-05-06 21:11:36 +02:00
|
|
|
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)
|
|
|
|
}
|
2025-06-04 14:41:53 +02:00
|
|
|
defer f.Close()
|
2025-06-04 14:29:13 +02:00
|
|
|
if _, err := f.Write(t); err != nil {
|
|
|
|
log.Fatal("error writing template file:", err)
|
|
|
|
}
|
|
|
|
|
2024-05-06 21:11:36 +02:00
|
|
|
}
|
2023-12-06 15:24:02 +01:00
|
|
|
}
|
2024-05-07 13:18:00 +02:00
|
|
|
|
|
|
|
// generateConfigMapsAndSecrets creates the configmaps and secrets from the environment variables.
|
|
|
|
func (chart *HelmChart) generateConfigMapsAndSecrets(project *types.Project) error {
|
|
|
|
appName := chart.Name
|
|
|
|
for _, s := range project.Services {
|
2025-06-04 14:29:13 +02:00
|
|
|
if len(s.Environment) == 0 {
|
2024-05-07 13:18:00 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
originalEnv := types.MappingWithEquals{}
|
|
|
|
secretsVar := types.MappingWithEquals{}
|
|
|
|
|
|
|
|
// copy env to originalEnv
|
2025-06-26 23:57:19 +02:00
|
|
|
maps.Copy(originalEnv, s.Environment)
|
2024-05-07 13:18:00 +02:00
|
|
|
|
2024-11-18 17:12:12 +01:00
|
|
|
if v, ok := s.Labels[labels.LabelSecrets]; ok {
|
2024-05-07 13:18:00 +02:00
|
|
|
list, err := labelStructs.SecretsFrom(v)
|
|
|
|
if 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 {
|
2024-10-17 17:08:42 +02:00
|
|
|
cm := NewConfigMap(s, appName, false)
|
2024-05-07 13:18:00 +02:00
|
|
|
y, _ := cm.Yaml()
|
|
|
|
name := cm.service.Name
|
|
|
|
chart.Templates[name+".configmap.yaml"] = &ChartTemplate{
|
|
|
|
Content: y,
|
|
|
|
Servicename: s.Name,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (chart *HelmChart) generateDeployment(service types.ServiceConfig, deployments map[string]*Deployment, services map[string]*Service, podToMerge map[string]*types.ServiceConfig, appName string) error {
|
|
|
|
// check the "ports" label from container and add it to the service
|
|
|
|
if err := fixPorts(&service); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// isgnored service
|
|
|
|
if isIgnored(service) {
|
|
|
|
fmt.Printf("%s Ignoring service %s\n", utils.IconInfo, service.Name)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// helm dependency
|
|
|
|
if isHelmDependency, err := chart.setDependencies(service); err != nil {
|
|
|
|
return err
|
|
|
|
} else if isHelmDependency {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// create all deployments
|
|
|
|
d := NewDeployment(service, chart)
|
|
|
|
deployments[service.Name] = d
|
|
|
|
|
|
|
|
// generate the cronjob if needed
|
|
|
|
chart.setCronJob(service, appName)
|
|
|
|
|
2024-11-22 14:54:36 +01:00
|
|
|
if exchange, ok := service.Labels[labels.LabelExchangeVolume]; ok {
|
|
|
|
// we need to add a volume and a mount point
|
|
|
|
ex, err := labelStructs.NewExchangeVolumes(exchange)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, exchangeVolume := range ex {
|
|
|
|
d.AddLegacyVolume("exchange-"+exchangeVolume.Name, exchangeVolume.Type)
|
|
|
|
d.exchangesVolumes[service.Name] = exchangeVolume
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-07 13:18:00 +02:00
|
|
|
// 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.
|
2024-11-18 17:12:12 +01:00
|
|
|
if samePod, ok := service.Labels[labels.LabelSamePod]; ok && samePod != "" {
|
2024-05-07 13:18:00 +02:00
|
|
|
podToMerge[samePod] = &service
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// setChartVersion sets the chart version from the service image tag.
|
|
|
|
func (chart *HelmChart) setChartVersion(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"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// setCronJob creates a cronjob from the service labels.
|
|
|
|
func (chart *HelmChart) setCronJob(service types.ServiceConfig, appName string) *CronJob {
|
2024-11-18 17:12:12 +01:00
|
|
|
if _, ok := service.Labels[labels.LabelCronJob]; !ok {
|
2024-05-07 13:18:00 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// setDependencies sets the dependencies from the service labels.
|
|
|
|
func (chart *HelmChart) setDependencies(service types.ServiceConfig) (bool, error) {
|
|
|
|
// helm dependency
|
2024-11-18 17:12:12 +01:00
|
|
|
if v, ok := service.Labels[labels.LabelDependencies]; ok {
|
2024-05-07 13:18:00 +02:00
|
|
|
d, err := labelStructs.DependenciesFrom(v)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, dep := range d {
|
|
|
|
fmt.Printf("%s Adding dependency to %s\n", utils.IconDependency, dep.Name)
|
|
|
|
chart.Dependencies = append(chart.Dependencies, dep)
|
|
|
|
name := dep.Name
|
|
|
|
if dep.Alias != "" {
|
|
|
|
name = dep.Alias
|
|
|
|
}
|
|
|
|
// add the dependency env vars to the values.yaml
|
|
|
|
chart.Values[name] = dep.Values
|
|
|
|
}
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// setSharedConf sets the shared configmap to the service.
|
|
|
|
func (chart *HelmChart) setSharedConf(service types.ServiceConfig, 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
|
2024-11-18 17:12:12 +01:00
|
|
|
if _, ok := service.Labels[labels.LabelEnvFrom]; !ok {
|
2024-05-07 13:18:00 +02:00
|
|
|
return
|
|
|
|
}
|
2024-11-18 17:12:12 +01:00
|
|
|
fromservices, err := labelStructs.EnvFromFrom(service.Labels[labels.LabelEnvFrom])
|
2024-05-07 13:18:00 +02:00
|
|
|
if 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
|
|
|
|
target := findDeployment(service.Name, deployments)
|
|
|
|
if target == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// add the configmap to the service
|
|
|
|
addConfigMapToService(service.Name, fromservice, chart.Name, target)
|
|
|
|
}
|
|
|
|
}
|
2024-11-26 16:11:12 +01:00
|
|
|
|
|
|
|
// setEnvironmentValuesFrom sets the environment values from another service.
|
|
|
|
func (chart *HelmChart) setEnvironmentValuesFrom(service types.ServiceConfig, deployments map[string]*Deployment) {
|
|
|
|
if _, ok := service.Labels[labels.LabelValueFrom]; !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
mapping, err := labelStructs.GetValueFrom(service.Labels[labels.LabelValueFrom])
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("error unmarshaling values-from label:", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
findDeployment := func(name string) *Deployment {
|
|
|
|
for _, dep := range deployments {
|
|
|
|
if dep.service.Name == name {
|
|
|
|
return dep
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// each mapping key is the environment, and the value is serivename.variable name
|
|
|
|
for env, from := range *mapping {
|
|
|
|
// find the deployment that has the variable
|
|
|
|
depName := strings.Split(from, ".")
|
|
|
|
dep := findDeployment(depName[0])
|
|
|
|
target := findDeployment(service.Name)
|
|
|
|
if dep == nil || target == nil {
|
|
|
|
log.Fatalf("deployment %s or %s not found", depName[0], service.Name)
|
|
|
|
}
|
2025-06-26 23:23:03 +02:00
|
|
|
container, index := utils.GetContainerByName(target.service.ContainerName, target.Spec.Template.Spec.Containers)
|
2024-11-26 16:11:12 +01:00
|
|
|
if container == nil {
|
|
|
|
log.Fatalf("Container %s not found", target.GetName())
|
|
|
|
}
|
|
|
|
reourceName := fmt.Sprintf(`{{ include "%s.fullname" . }}-%s`, chart.Name, depName[0])
|
|
|
|
// add environment with from
|
|
|
|
|
|
|
|
// is it a secret?
|
|
|
|
isSecret := false
|
|
|
|
secrets, err := labelStructs.SecretsFrom(dep.service.Labels[labels.LabelSecrets])
|
|
|
|
if err == nil {
|
2025-06-26 23:57:19 +02:00
|
|
|
if slices.Contains(secrets, depName[1]) {
|
|
|
|
isSecret = true
|
2024-11-26 16:11:12 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !isSecret {
|
|
|
|
container.Env = append(container.Env, corev1.EnvVar{
|
|
|
|
Name: env,
|
|
|
|
ValueFrom: &corev1.EnvVarSource{
|
|
|
|
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
|
|
|
|
LocalObjectReference: corev1.LocalObjectReference{
|
|
|
|
Name: reourceName,
|
|
|
|
},
|
|
|
|
Key: depName[1],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
container.Env = append(container.Env, corev1.EnvVar{
|
|
|
|
Name: env,
|
|
|
|
ValueFrom: &corev1.EnvVarSource{
|
|
|
|
SecretKeyRef: &corev1.SecretKeySelector{
|
|
|
|
LocalObjectReference: corev1.LocalObjectReference{
|
|
|
|
Name: reourceName,
|
|
|
|
},
|
|
|
|
Key: depName[1],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
// the environment is bound, so we shouldn't add it to the values.yaml or in any other place
|
|
|
|
delete(service.Environment, env)
|
|
|
|
// also, remove the values
|
|
|
|
target.boundEnvVar = append(target.boundEnvVar, env)
|
|
|
|
// and save the container
|
|
|
|
target.Spec.Template.Spec.Containers[index] = *container
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|