Add "values-from" and more tests #86
@@ -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
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:"-"`
|
||||
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
|
||||
|
@@ -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()
|
||||
|
@@ -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.")
|
||||
|
@@ -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 (
|
||||
|
@@ -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
|
||||
|
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