Merge pull request #2011 from yuefanxiao/envfile

fix: resolve env_file variable interpolation issues by adding support…
This commit is contained in:
Kubernetes Prow Robot 2025-05-14 07:39:19 -07:00 committed by GitHub
commit 4f59fe88bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 276 additions and 20 deletions

View File

@ -31,6 +31,7 @@ import (
"text/template"
"time"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/types"
"github.com/joho/godotenv"
"github.com/kubernetes/kompose/pkg/kobject"
@ -962,6 +963,10 @@ func GetEnvsFromFile(file string) (map[string]string, error) {
return envLoad, nil
}
func LoadEnvFiles(file string, lookup func(key string) (string, bool)) (map[string]string, error) {
return dotenv.ReadWithLookup(lookup, file)
}
// GetContentFromFile gets the content from the file..
func GetContentFromFile(file string) (string, error) {
fileBytes, err := os.ReadFile(file)

View File

@ -219,6 +219,38 @@ func (k *Kubernetes) InitSvc(name string, service kobject.ServiceConfig) *api.Se
return svc
}
// InitConfigMapForEnvWithLookup initializes a ConfigMap object from an env_file with variable interpolation support
// using the provided lookup function to resolve variable references like ${VAR} or ${VAR:-default}
func (k *Kubernetes) InitConfigMapForEnvWithLookup(name string, opt kobject.ConvertOptions, envFile string, lookup func(key string) (string, bool)) *api.ConfigMap {
workDir, err := transformer.GetComposeFileDir(opt.InputFiles)
if err != nil {
log.Fatalf("Unable to get compose file directory: %s", err)
}
envs, err := LoadEnvFiles(filepath.Join(workDir, envFile), lookup)
if err != nil {
log.Fatalf("Unable to retrieve env file: %s", err)
}
// Remove root pathing
// replace all other slashes / periods
envName := FormatEnvName(envFile, name)
// In order to differentiate files, we append to the name and remove '.env' if applicable from the file name
configMap := &api.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: envName,
Labels: transformer.ConfigLabels(name + "-" + envName),
},
Data: envs,
}
return configMap
}
// InitConfigMapForEnv initializes a ConfigMap object
func (k *Kubernetes) InitConfigMapForEnv(name string, opt kobject.ConvertOptions, envFile string) *api.ConfigMap {
workDir, err := transformer.GetComposeFileDir(opt.InputFiles)
@ -1326,13 +1358,8 @@ func (k *Kubernetes) CreateWorkloadAndConfigMapObjects(name string, service kobj
objects = append(objects, k.InitSS(name, service, replica))
}
if len(service.EnvFile) > 0 {
for _, envFile := range service.EnvFile {
configMap := k.InitConfigMapForEnv(name, opt, envFile)
objects = append(objects, configMap)
}
}
envConfigMaps := k.PargeEnvFiletoConfigMaps(name, service, opt)
objects = append(objects, envConfigMaps...)
return objects
}
@ -1671,13 +1698,8 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.
pod := k.InitPod(name, service)
objects = append(objects, pod)
}
if len(service.EnvFile) > 0 {
for _, envFile := range service.EnvFile {
configMap := k.InitConfigMapForEnv(name, opt, envFile)
objects = append(objects, configMap)
}
}
envConfigMaps := k.PargeEnvFiletoConfigMaps(name, service, opt)
objects = append(objects, envConfigMaps...)
} else {
objects = k.CreateWorkloadAndConfigMapObjects(name, service, opt)
}
@ -1776,3 +1798,19 @@ func (k *Kubernetes) configHorizontalPodScaler(name string, service kobject.Serv
*objects = append(*objects, &hpa)
return nil
}
func (k *Kubernetes) PargeEnvFiletoConfigMaps(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) []runtime.Object {
envs := make(map[string]string)
for _, env := range service.Environment {
envs[env.Name] = env.Value
}
configMaps := make([]runtime.Object, 0)
for _, envFile := range service.EnvFile {
configMap := k.InitConfigMapForEnvWithLookup(name, opt, envFile, func(key string) (string, bool) {
v, ok := envs[key]
return v, ok
})
configMaps = append(configMaps, configMap)
}
return configMaps
}

View File

@ -19,6 +19,8 @@ package kubernetes
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
@ -36,6 +38,7 @@ import (
networkingv1beta1 "k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
@ -1254,3 +1257,79 @@ func newSecrets(stringsSecretConfig SecretsConfig) types.Secrets {
},
}
}
// TestPargeEnvFiletoConfigMaps tests the conversion of environment variable files to ConfigMap objects
func TestPargeEnvFiletoConfigMaps(t *testing.T) {
// Prepare a temp .env file for the expression test
tempFile, err := os.CreateTemp("", ".env")
if err != nil {
t.Fatalf("Failed to create temp env file: %v", err)
}
defer os.Remove(tempFile.Name())
content := []byte(`FOO=bar
BAR=${FOO}_baz
DOC_ENGINE=${DOC_ENGINE:-elasticsearch}
COMPOSE_PROFILES=${DOC_ENGINE}
UNDEFINED_VAR=${MISSING_VAR:-default_value}
`)
if _, err := tempFile.Write(content); err != nil {
t.Fatalf("Failed to write to temp env file: %v", err)
}
tempFile.Close()
tempFileName := filepath.Base(tempFile.Name())
testCases := map[string]struct {
service kobject.ServiceConfig
opt kobject.ConvertOptions
want int
check func(t *testing.T, cms []runtime.Object)
}{
"Env file with variable expressions": {
service: kobject.ServiceConfig{
Name: "test-app",
Environment: []kobject.EnvVar{
{
Name: "DOC_ENGINE",
Value: "test-env",
},
},
EnvFile: []string{tempFileName},
},
opt: kobject.ConvertOptions{InputFiles: []string{tempFile.Name()}},
want: 1,
check: func(t *testing.T, cms []runtime.Object) {
cm, ok := cms[0].(*api.ConfigMap)
if !ok {
t.Errorf("Returned object is not a ConfigMap")
return
}
if cm.Data["FOO"] != "bar" {
t.Errorf("Expected FOO=bar, got %s", cm.Data["FOO"])
}
if cm.Data["BAR"] != "bar_baz" {
t.Errorf("Expected BAR=bar_baz, got %s", cm.Data["BAR"])
}
if cm.Data["DOC_ENGINE"] != "test-env" {
t.Errorf("Expected DOC_ENGINE=test-env, got %s", cm.Data["DOC_ENGINE"])
}
if cm.Data["COMPOSE_PROFILES"] != "test-env" {
t.Errorf("Expected COMPOSE_PROFILES=test-env, got %s", cm.Data["COMPOSE_PROFILES"])
}
if cm.Data["UNDEFINED_VAR"] != "default_value" {
t.Errorf("Expected UNDEFINED_VAR=default_value, got %s", cm.Data["UNDEFINED_VAR"])
}
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
k := Kubernetes{}
cms := k.PargeEnvFiletoConfigMaps(tc.service.Name, tc.service, tc.opt)
if len(cms) != tc.want {
t.Errorf("Expected %d ConfigMaps, got %d", tc.want, len(cms))
}
tc.check(t, cms)
})
}
}

View File

@ -340,12 +340,8 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C
objects = append(objects, pod)
}
if len(service.EnvFile) > 0 {
for _, envFile := range service.EnvFile {
configMap := o.InitConfigMapForEnv(name, opt, envFile)
objects = append(objects, configMap)
}
}
envConfigMaps := o.PargeEnvFiletoConfigMaps(name, service, opt)
objects = append(objects, envConfigMaps...)
} else {
objects = o.CreateWorkloadAndConfigMapObjects(name, service, opt)

View File

@ -282,6 +282,14 @@ k8s_output="$KOMPOSE_ROOT/script/test/fixtures/compose-env-no-interpolation/outp
convert::expect_success "$k8s_cmd" "$k8s_output" || exit 1
convert::expect_success "$os_cmd" "$os_output" || exit 1
# Test configmap generated by env_file with variable interpolation
k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/envfile-interpolation/compose.yaml convert --stdout --with-kompose-annotation=false"
os_cmd="kompose --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/envfile-interpolation/compose.yaml convert --stdout --with-kompose-annotation=false"
k8s_output="$KOMPOSE_ROOT/script/test/fixtures/envfile-interpolation/output-k8s.yaml"
os_output="$KOMPOSE_ROOT/script/test/fixtures/envfile-interpolation/output-os.yaml"
convert::expect_success_and_warning "$k8s_cmd" "$k8s_output" || exit 1
convert::expect_success_and_warning "$os_cmd" "$os_output" || exit 1
# Test support for subpath volume
k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/vols-subpath/compose.yaml convert --stdout --with-kompose-annotation=false"
k8s_output="$KOMPOSE_ROOT/script/test/fixtures/vols-subpath/output-k8s.yaml"

View File

@ -0,0 +1,4 @@
DOC_ENGINE=${DOC_ENGINE:-elasticsearch}
COMPOSE_PROFILES=${DOC_ENGINE}
MINIO_CONSOLE_PORT=9001
MINIO_PORT=9000

View File

@ -0,0 +1,14 @@
version: '3'
services:
minio:
image: quay.io/minio/minio:RELEASE.2023-12-20T01-00-02Z
container_name: ragflow-minio
command: server --console-address ":9001" /data
ports:
- ${MINIO_PORT}:9000
- ${MINIO_CONSOLE_PORT}:9001
env_file: .env
environment:
- DOC_ENGINE=test-env
restart: on-failure

View File

@ -0,0 +1,56 @@
---
apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: minio
name: minio
spec:
ports:
- name: "9000"
port: 9000
targetPort: 9000
- name: "9001"
port: 9001
targetPort: 9001
selector:
io.kompose.service: minio
---
apiVersion: v1
kind: Pod
metadata:
labels:
io.kompose.service: minio
name: minio
spec:
containers:
- args:
- server
- --console-address
- :9001
- /data
envFrom:
- configMapRef:
name: env
image: quay.io/minio/minio:RELEASE.2023-12-20T01-00-02Z
name: ragflow-minio
ports:
- containerPort: 9000
protocol: TCP
- containerPort: 9001
protocol: TCP
restartPolicy: OnFailure
---
apiVersion: v1
data:
COMPOSE_PROFILES: test-env
DOC_ENGINE: test-env
MINIO_CONSOLE_PORT: "9001"
MINIO_PORT: "9000"
kind: ConfigMap
metadata:
labels:
io.kompose.service: minio-env
name: env

View File

@ -0,0 +1,56 @@
---
apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: minio
name: minio
spec:
ports:
- name: "9000"
port: 9000
targetPort: 9000
- name: "9001"
port: 9001
targetPort: 9001
selector:
io.kompose.service: minio
---
apiVersion: v1
kind: Pod
metadata:
labels:
io.kompose.service: minio
name: minio
spec:
containers:
- args:
- server
- --console-address
- :9001
- /data
envFrom:
- configMapRef:
name: env
image: quay.io/minio/minio:RELEASE.2023-12-20T01-00-02Z
name: ragflow-minio
ports:
- containerPort: 9000
protocol: TCP
- containerPort: 9001
protocol: TCP
restartPolicy: OnFailure
---
apiVersion: v1
data:
COMPOSE_PROFILES: test-env
DOC_ENGINE: test-env
MINIO_CONSOLE_PORT: "9001"
MINIO_PORT: "9000"
kind: ConfigMap
metadata:
labels:
io.kompose.service: minio-env
name: env