diff --git a/generator/chart.go b/generator/chart.go index 5482bdf..9d4c564 100644 --- a/generator/chart.go +++ b/generator/chart.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/compose-spec/compose-go/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. @@ -342,3 +343,85 @@ func (chart *HelmChart) setSharedConf(service types.ServiceConfig, deployments m 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 + + } +} diff --git a/generator/chart_test.go b/generator/chart_test.go new file mode 100644 index 0000000..dde6a54 --- /dev/null +++ b/generator/chart_test.go @@ -0,0 +1,157 @@ +package generator + +import ( + "fmt" + "katenary/generator/labels" + "os" + "strings" + "testing" + + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +func TestValuesFrom(t *testing.T) { + composeFile := ` +services: + aa: + image: nginx:latest + environment: + AA_USER: foo + bb: + image: nginx:latest + labels: + %[1]s/values-from: |- + BB_USER: aa.USER +` + composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/aa/configmap.yaml") + configMap := v1.ConfigMap{} + if err := yaml.Unmarshal([]byte(output), &configMap); err != nil { + t.Errorf(unmarshalError, err) + } + data := configMap.Data + if v, ok := data["AA_USER"]; !ok || v != "foo" { + t.Errorf("Expected AA_USER to be foo, got %s", v) + } +} + +func TestValuesFromCopy(t *testing.T) { + composeFile := ` +services: + aa: + image: nginx:latest + environment: + AA_USER: foo + bb: + image: nginx:latest + labels: + %[1]s/values-from: |- + BB_USER: aa.AA_USER +` + composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/bb/deployment.yaml") + dep := appsv1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dep); err != nil { + t.Errorf(unmarshalError, err) + } + containers := dep.Spec.Template.Spec.Containers + environment := containers[0].Env[0] + + envFrom := environment.ValueFrom.ConfigMapKeyRef + if envFrom.Key != "AA_USER" { + t.Errorf("Expected AA_USER, got %s", envFrom.Key) + } + if !strings.Contains(envFrom.Name, "aa") { + t.Errorf("Expected aa, got %s", envFrom.Name) + } +} + +func TestValuesFromSecret(t *testing.T) { + composeFile := ` +services: + aa: + image: nginx:latest + environment: + AA_USER: foo + labels: + %[1]s/secrets: |- + - AA_USER + bb: + image: nginx:latest + labels: + %[1]s/values-from: |- + BB_USER: aa.AA_USER +` + composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/bb/deployment.yaml") + dep := appsv1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dep); err != nil { + t.Errorf(unmarshalError, err) + } + containers := dep.Spec.Template.Spec.Containers + environment := containers[0].Env[0] + + envFrom := environment.ValueFrom.SecretKeyRef + if envFrom.Key != "AA_USER" { + t.Errorf("Expected AA_USER, got %s", envFrom.Key) + } + if !strings.Contains(envFrom.Name, "aa") { + t.Errorf("Expected aa, got %s", envFrom.Name) + } +} + +func TestEnvFrom(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + environment: + Foo: bar + BAZ: qux + db: + image: postgres + labels: + %[1]s/env-from: |- + - web +` + composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix) + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", "templates/db/deployment.yaml") + dep := appsv1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dep); err != nil { + t.Errorf(unmarshalError, err) + } + envFrom := dep.Spec.Template.Spec.Containers[0].EnvFrom + if len(envFrom) != 1 { + t.Fatalf("Expected 1 envFrom, got %d", len(envFrom)) + } +} diff --git a/generator/deployment.go b/generator/deployment.go index 51f0a61..c05de83 100644 --- a/generator/deployment.go +++ b/generator/deployment.go @@ -40,6 +40,7 @@ type Deployment struct { defaultTag string `yaml:"-"` isMainApp bool `yaml:"-"` exchangesVolumes map[string]*labelStructs.ExchangeVolume `yaml:"-"` + boundEnvVar []string `yaml:"-"` // environement to remove } // NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name. @@ -94,6 +95,7 @@ func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment { configMaps: make(map[string]*ConfigMapMount), volumeMap: make(map[string]string), exchangesVolumes: map[string]*labelStructs.ExchangeVolume{}, + boundEnvVar: []string{}, } // add containers diff --git a/generator/generator.go b/generator/generator.go index a96fb5f..bc16941 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -113,6 +113,10 @@ func Generate(project *types.Project) (*HelmChart, error) { } } } + // it's now time to get "value-from", before makeing the secrets and configmaps! + for _, s := range project.Services { + chart.setEnvironmentValuesFrom(s, deployments) + } // generate configmaps with environment variables chart.generateConfigMapsAndSecrets(project) @@ -123,6 +127,16 @@ func Generate(project *types.Project) (*HelmChart, error) { chart.setSharedConf(s, deployments) } + // remove all "boundEnv" from the values + for _, d := range deployments { + if len(d.boundEnvVar) == 0 { + continue + } + for _, e := range d.boundEnvVar { + delete(chart.Values[d.service.Name].(*Value).Environment, e) + } + } + // generate yaml files for _, d := range deployments { y, err := d.Yaml() diff --git a/generator/katenaryfile/main.go b/generator/katenaryfile/main.go index 789855d..312d747 100644 --- a/generator/katenaryfile/main.go +++ b/generator/katenaryfile/main.go @@ -40,6 +40,7 @@ type Service struct { CronJob *labelStructs.CronJob `json:"cron-job,omitempty" jsonschema:"title=Cron Job,description=Cron Job configuration"` EnvFrom *labelStructs.EnvFrom `json:"env-from,omitempty" jsonschema:"title=Env From,description=Inject environment variables from another service"` ExchangeVolumes []*labelStructs.ExchangeVolume `json:"exchange-volumes,omitempty" jsonschema:"title=Exchange Volumes,description=Exchange volumes between services"` + ValuesFrom *labelStructs.ValueFrom `json:"values-from,omitempty" jsonschema:"title=Values From,description=Inject values from another service (secret or configmap environment variables)"` } // OverrideWithConfig overrides the project with the katenary.yaml file. It @@ -91,6 +92,7 @@ func OverrideWithConfig(project *types.Project) { getLabelContent(s.CronJob, &project.Services[i], labels.LabelCronJob) getLabelContent(s.EnvFrom, &project.Services[i], labels.LabelEnvFrom) getLabelContent(s.ExchangeVolumes, &project.Services[i], labels.LabelExchangeVolume) + getLabelContent(s.ValuesFrom, &project.Services[i], labels.LabelValueFrom) } } fmt.Println(utils.IconInfo, "Katenary file loaded successfully, the services are now configured.") diff --git a/generator/labels/katenaryLabels.go b/generator/labels/katenaryLabels.go index 0bc0494..7125f5b 100644 --- a/generator/labels/katenaryLabels.go +++ b/generator/labels/katenaryLabels.go @@ -33,6 +33,7 @@ const ( LabelCronJob Label = KatenaryLabelPrefix + "/cronjob" LabelEnvFrom Label = KatenaryLabelPrefix + "/env-from" LabelExchangeVolume Label = KatenaryLabelPrefix + "/exchange-volumes" + LabelValueFrom Label = KatenaryLabelPrefix + "/values-from" ) var ( diff --git a/generator/labels/katenaryLabelsDoc.yaml b/generator/labels/katenaryLabelsDoc.yaml index 8a922d7..87f2d01 100644 --- a/generator/labels/katenaryLabelsDoc.yaml +++ b/generator/labels/katenaryLabelsDoc.yaml @@ -321,4 +321,36 @@ mountPath: /opt init: cp -ra /var/www/html/* /opt +"values-from": + short: "Add values from another service." + long: |- + This label allows adding values from another service to the current service. + It avoid duplicating values, environment or secrets that should be the same. + + The key is the value to be added, and the value is the "key" to fetch in the + form `service_name.environment_name`. + + type: "map[string]string" + example: |- + database: + image: mariadb:10.5 + environment: + MARIADB_USER: myuser + MARIADB_PASSWORD: mypassword + labels: + # it can be a secret + {{ .KatenaryPrefix }}/secrets: |- + - DB_PASSWORD + php: + image: php:7.4-fpm + environment: + # it's duplicated in docker / podman + DB_USER: myuser + DB_PASSWORD: mypassword + labels: + # removes the duplicated, use the configMap and secrets from "database" + {{ .KatenaryPrefix }}/values-from: |- + DB_USER: database.MARIADB_USER + DB_PASSWORD: database.MARIADB_PASSWORD + # vim: ft=gotmpl.yaml diff --git a/generator/labels/labelStructs/valueFrom.go b/generator/labels/labelStructs/valueFrom.go new file mode 100644 index 0000000..9ad5712 --- /dev/null +++ b/generator/labels/labelStructs/valueFrom.go @@ -0,0 +1,13 @@ +package labelStructs + +import "gopkg.in/yaml.v3" + +type ValueFrom map[string]string + +func GetValueFrom(data string) (*ValueFrom, error) { + vf := ValueFrom{} + if err := yaml.Unmarshal([]byte(data), &vf); err != nil { + return nil, err + } + return &vf, nil +}