diff --git a/generator/chart.go b/generator/chart.go index e9a13fa..8f3a89f 100644 --- a/generator/chart.go +++ b/generator/chart.go @@ -2,10 +2,13 @@ package generator import ( "fmt" + "log" "os" "path/filepath" "strings" + "github.com/compose-spec/compose-go/types" + "katenary/generator/labelStructs" "katenary/utils" ) @@ -114,3 +117,213 @@ func (chart *HelmChart) SaveTemplates(templateDir string) { 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[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) + 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) + + // 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[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[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[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[LabelEnvFrom]; !ok { + return + } + fromservices, err := labelStructs.EnvFromFrom(service.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) + } +} diff --git a/generator/generator.go b/generator/generator.go index d6c479c..5fd3c00 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -5,13 +5,11 @@ import ( "fmt" "log" "regexp" - "strconv" "strings" "github.com/compose-spec/compose-go/types" corev1 "k8s.io/api/core/v1" - "katenary/generator/labelStructs" "katenary/utils" ) @@ -53,7 +51,7 @@ func Generate(project *types.Project) (*HelmChart, error) { if mainCount > 1 { return nil, fmt.Errorf("found more than one main app") } - setChartVersion(chart, service) + chart.setChartVersion(service) } } if mainCount == 0 { @@ -62,51 +60,10 @@ func Generate(project *types.Project) (*HelmChart, error) { // 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 { + err := chart.generateDeployment(service, deployments, services, podToMerge, appName) + if 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[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, - } - } } // now we have all deployments, we can create PVC if needed (it's separated from @@ -116,7 +73,8 @@ func Generate(project *types.Project) (*HelmChart, error) { addStaticVolumes(deployments, service) } for _, service := range project.Services { - if err := buildVolumes(service, chart, deployments); err != nil { + err := buildVolumes(service, chart, deployments) + if err != nil { return nil, err } } @@ -148,12 +106,12 @@ func Generate(project *types.Project) (*HelmChart, error) { } // generate configmaps with environment variables - generateConfigMapsAndSecrets(project, chart) + chart.generateConfigMapsAndSecrets(project) // 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) + chart.setSharedConf(s, deployments) } // generate yaml files @@ -255,114 +213,6 @@ func serviceIsMain(service types.ServiceConfig) bool { return false } -// setChartVersion sets the chart version from the service image tag. -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" - } - } -} - -// fixPorts checks the "ports" label from container and add it to the service. -func fixPorts(service *types.ServiceConfig) error { - // check the "ports" label from container and add it to the service - if portsLabel, ok := service.Labels[LabelPorts]; ok { - ports, err := labelStructs.PortsFrom(portsLabel) - if 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 -} - -// setCronJob creates a cronjob from the service labels. -func setCronJob(service types.ServiceConfig, chart *HelmChart, appName string) *CronJob { - if _, ok := service.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 setDependencies(chart *HelmChart, service types.ServiceConfig) (bool, error) { - // helm dependency - if v, ok := service.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 -} - -// isIgnored returns true if the service is ignored. -func isIgnored(service types.ServiceConfig) bool { - if v, ok := service.Labels[LabelIgnore]; ok { - return v == "true" || v == "yes" || v == "1" - } - return false -} - // buildVolumes creates the volumes for the service. func buildVolumes(service types.ServiceConfig, chart *HelmChart, deployments map[string]*Deployment) error { appName := chart.Name @@ -442,68 +292,6 @@ func addStaticVolumes(deployments map[string]*Deployment, service types.ServiceC d.Spec.Template.Spec.Containers[index] = *container } -// generateConfigMapsAndSecrets creates the configmaps and secrets from the environment variables. -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[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) - y, _ := cm.Yaml() - name := cm.service.Name - chart.Templates[name+".configmap.yaml"] = &ChartTemplate{ - Content: y, - Servicename: s.Name, - } - } - } - return nil -} - // samePodVolume returns true if the volume is already in the target deployment. func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, deployments map[string]*Deployment) bool { // if the service has volumes, and it has "same-pod" label @@ -527,13 +315,7 @@ func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, dep } // get the target deployment - var target *Deployment - for _, d := range deployments { - if d.service.Name == targetDeployment { - target = d - break - } - } + target := findDeployment(targetDeployment, deployments) if target == nil { return false } @@ -547,48 +329,3 @@ func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, dep } return false } - -// setSharedConf sets the shared configmap to the service. -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[LabelEnvFrom]; !ok { - return - } - fromservices, err := labelStructs.EnvFromFrom(service.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 - 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 - } - } -} diff --git a/generator/utils.go b/generator/utils.go new file mode 100644 index 0000000..c278188 --- /dev/null +++ b/generator/utils.go @@ -0,0 +1,79 @@ +package generator + +import ( + "strconv" + "strings" + + "github.com/compose-spec/compose-go/types" + corev1 "k8s.io/api/core/v1" + + "katenary/generator/labelStructs" + "katenary/utils" +) + +// findDeployment finds the corresponding target deployment for a service. +func findDeployment(serviceName string, deployments map[string]*Deployment) *Deployment { + for _, d := range deployments { + if d.service.Name == serviceName { + return d + } + } + return nil +} + +// addConfigMapToService adds the configmap to the service. +func addConfigMapToService(serviceName, fromservice, chartName string, target *Deployment) { + for i, c := range target.Spec.Template.Spec.Containers { + if c.Name != serviceName { + continue + } + c.EnvFrom = append(c.EnvFrom, corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: utils.TplName(fromservice, chartName), + }, + }, + }) + target.Spec.Template.Spec.Containers[i] = c + } +} + +// fixPorts checks the "ports" label from container and add it to the service. +func fixPorts(service *types.ServiceConfig) error { + // check the "ports" label from container and add it to the service + portsLabel := "" + ok := false + if portsLabel, ok = service.Labels[LabelPorts]; !ok { + return nil + } + ports, err := labelStructs.PortsFrom(portsLabel) + if 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 +} + +// isIgnored returns true if the service is ignored. +func isIgnored(service types.ServiceConfig) bool { + if v, ok := service.Labels[LabelIgnore]; ok { + return v == "true" || v == "yes" || v == "1" + } + return false +}