Add "values-from" and more tests #86
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/types"
|
"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.
|
// 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)
|
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
157
generator/chart_test.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
@@ -40,6 +40,7 @@ type Deployment struct {
|
|||||||
defaultTag string `yaml:"-"`
|
defaultTag string `yaml:"-"`
|
||||||
isMainApp bool `yaml:"-"`
|
isMainApp bool `yaml:"-"`
|
||||||
exchangesVolumes map[string]*labelStructs.ExchangeVolume `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.
|
// 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),
|
configMaps: make(map[string]*ConfigMapMount),
|
||||||
volumeMap: make(map[string]string),
|
volumeMap: make(map[string]string),
|
||||||
exchangesVolumes: map[string]*labelStructs.ExchangeVolume{},
|
exchangesVolumes: map[string]*labelStructs.ExchangeVolume{},
|
||||||
|
boundEnvVar: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// add containers
|
// add containers
|
||||||
|
@@ -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
|
// generate configmaps with environment variables
|
||||||
chart.generateConfigMapsAndSecrets(project)
|
chart.generateConfigMapsAndSecrets(project)
|
||||||
@@ -123,6 +127,16 @@ func Generate(project *types.Project) (*HelmChart, error) {
|
|||||||
chart.setSharedConf(s, deployments)
|
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
|
// generate yaml files
|
||||||
for _, d := range deployments {
|
for _, d := range deployments {
|
||||||
y, err := d.Yaml()
|
y, err := d.Yaml()
|
||||||
|
@@ -40,6 +40,7 @@ type Service struct {
|
|||||||
CronJob *labelStructs.CronJob `json:"cron-job,omitempty" jsonschema:"title=Cron Job,description=Cron Job configuration"`
|
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"`
|
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"`
|
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
|
// 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.CronJob, &project.Services[i], labels.LabelCronJob)
|
||||||
getLabelContent(s.EnvFrom, &project.Services[i], labels.LabelEnvFrom)
|
getLabelContent(s.EnvFrom, &project.Services[i], labels.LabelEnvFrom)
|
||||||
getLabelContent(s.ExchangeVolumes, &project.Services[i], labels.LabelExchangeVolume)
|
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.")
|
fmt.Println(utils.IconInfo, "Katenary file loaded successfully, the services are now configured.")
|
||||||
|
@@ -33,6 +33,7 @@ const (
|
|||||||
LabelCronJob Label = KatenaryLabelPrefix + "/cronjob"
|
LabelCronJob Label = KatenaryLabelPrefix + "/cronjob"
|
||||||
LabelEnvFrom Label = KatenaryLabelPrefix + "/env-from"
|
LabelEnvFrom Label = KatenaryLabelPrefix + "/env-from"
|
||||||
LabelExchangeVolume Label = KatenaryLabelPrefix + "/exchange-volumes"
|
LabelExchangeVolume Label = KatenaryLabelPrefix + "/exchange-volumes"
|
||||||
|
LabelValueFrom Label = KatenaryLabelPrefix + "/values-from"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@@ -321,4 +321,36 @@
|
|||||||
mountPath: /opt
|
mountPath: /opt
|
||||||
init: cp -ra /var/www/html/* /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
|
# vim: ft=gotmpl.yaml
|
||||||
|
13
generator/labels/labelStructs/valueFrom.go
Normal file
13
generator/labels/labelStructs/valueFrom.go
Normal 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
|
||||||
|
}
|
Reference in New Issue
Block a user