As explained in #102, the compose-go package changes types and needs massive changes. This branches is OK for now, but needs some tests. It MUST be quickly fixed to be integrated in main branch.
428 lines
12 KiB
Go
428 lines
12 KiB
Go
package generator
|
|
|
|
import (
|
|
"fmt"
|
|
"katenary/generator/labels"
|
|
"katenary/generator/labels/labelStructs"
|
|
"katenary/utils"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/compose-spec/compose-go/v2/types"
|
|
corev1 "k8s.io/api/core/v1"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// ConvertOptions are the options to convert a compose project to a helm chart.
|
|
type ConvertOptions struct {
|
|
AppVersion *string
|
|
OutputDir string
|
|
ChartVersion string
|
|
Icon string
|
|
Profiles []string
|
|
EnvFiles []string
|
|
Force bool
|
|
HelmUpdate bool
|
|
}
|
|
|
|
// HelmChart is a Helm Chart representation. It contains all the
|
|
// templates, values, versions, helpers...
|
|
type HelmChart struct {
|
|
Templates map[string]*ChartTemplate `yaml:"-"`
|
|
Values map[string]any `yaml:"-"`
|
|
VolumeMounts map[string]any `yaml:"-"`
|
|
composeHash *string `yaml:"-"`
|
|
Name string `yaml:"name"`
|
|
Icon string `yaml:"icon,omitempty"`
|
|
ApiVersion string `yaml:"apiVersion"`
|
|
Version string `yaml:"version"`
|
|
AppVersion string `yaml:"appVersion"`
|
|
Description string `yaml:"description"`
|
|
Helper string `yaml:"-"`
|
|
Dependencies []labelStructs.Dependency `yaml:"dependencies,omitempty"`
|
|
}
|
|
|
|
// 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{},
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
// 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), 0o755); 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), 0o755)
|
|
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()
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
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[labels.LabelSecrets]; ok {
|
|
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 {
|
|
cm := NewConfigMap(s, appName, false)
|
|
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)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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[labels.LabelSamePod]; ok && samePod != "" {
|
|
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 {
|
|
if _, ok := service.Labels[labels.LabelCronJob]; !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
|
|
}
|
|
|
|
// setDependencies sets the dependencies from the service labels.
|
|
func (chart *HelmChart) setDependencies(service types.ServiceConfig) (bool, error) {
|
|
// helm dependency
|
|
if v, ok := service.Labels[labels.LabelDependencies]; ok {
|
|
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
|
|
if _, ok := service.Labels[labels.LabelEnvFrom]; !ok {
|
|
return
|
|
}
|
|
fromservices, err := labelStructs.EnvFromFrom(service.Labels[labels.LabelEnvFrom])
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
container, index := utils.GetContainerByName(target.service.Name, target.Spec.Template.Spec.Containers)
|
|
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 {
|
|
for _, secret := range secrets {
|
|
if secret == depName[1] {
|
|
isSecret = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
}
|