chore(label): new label "values-from"

This labels allow to use some environment variables from another service
and use the configMap / secret instead of the original value. This is
useful to avoid duplication of values for several variables.
This commit is contained in:
2024-11-26 16:11:12 +01:00
parent 4f0298c0a9
commit 3b4dade699
8 changed files with 304 additions and 0 deletions

View File

@@ -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
}
}

157
generator/chart_test.go Normal file
View File

@@ -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))
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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.")

View File

@@ -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 (

View File

@@ -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

View File

@@ -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
}