Add env_file + ConfigMaps feature to Kompose

When using env_file with Docker Compose, a ConfigMap will be generated

For example:

```sh
▶ ./kompose convert -f
script/test/fixtures/configmaps/docker-compose.yml
INFO Kubernetes file "redis-service.yaml" created
INFO Kubernetes file "redis-deployment.yaml" created
INFO Kubernetes file "foo-env-configmap.yaml" created
INFO Kubernetes file "bar-env-configmap.yaml" created
```

File:

```yaml
version: '3'

services:
  redis:
    image: 'bitnami/redis:latest'
    environment:
      - ALLOW_EMPTY_PASSWORD=no
    # Env file will override environment / warn!
    env_file:
      - "foo.env"
      - bar.env
    labels:
      kompose.service.type: nodeport
    ports:
      - '6379:6379'
```

To:

```yaml
apiVersion: v1
data:
  ALLOW_EMPTY_PASSWORD: "yes"
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: foo-env
```

```yaml
...
      - env:
        - name: ALLOW_EMPTY_PASSWORD
          valueFrom:
            configMapKeyRef:
              key: ALLOW_EMPTY_PASSWORD
              name: foo-env
```
This commit is contained in:
Charlie Drage 2017-09-07 08:50:31 -04:00
parent 935e90febc
commit f4bfe1fcb5
11 changed files with 295 additions and 14 deletions

View File

@ -62,10 +62,10 @@ type ConvertOptions struct {
// ServiceConfig holds the basic struct of a container
type ServiceConfig struct {
// use tags to mark from what element this value comes
ContainerName string
Image string `compose:"image"`
Environment []EnvVar `compose:"environment"`
EnvFile []string `compose:"env_file"`
Port []Ports `compose:"ports"`
Command []string `compose:"command"`
WorkingDir string `compose:""`

View File

@ -322,6 +322,9 @@ func dockerComposeToKomposeMapping(composeObject *types.Config) (kobject.Kompose
serviceConfig.Environment = append(serviceConfig.Environment, env)
}
// Get env_file
serviceConfig.EnvFile = composeServiceConfig.EnvFile
// Parse the ports
// v3 uses a new format called "long syntax" starting in 3.2
// https://docs.docker.com/compose/compose-file/#ports

View File

@ -22,6 +22,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"strconv"
@ -31,6 +32,7 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/ghodss/yaml"
"github.com/joho/godotenv"
"github.com/kubernetes/kompose/pkg/kobject"
"github.com/kubernetes/kompose/pkg/transformer"
@ -330,9 +332,13 @@ func (k *Kubernetes) CreateHeadlessService(name string, service kobject.ServiceC
}
// UpdateKubernetesObjects loads configurations to k8s objects
func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.ServiceConfig, objects *[]runtime.Object) error {
func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, objects *[]runtime.Object) error {
// Configure the environment variables.
envs := k.ConfigEnvs(name, service)
envs, err := k.ConfigEnvs(name, service, opt)
if err != nil {
return errors.Wrap(err, "Unable to load env variables")
}
// Configure the container volumes.
volumesMount, volumes, pvc, err := k.ConfigVolumes(name, service)
@ -570,3 +576,27 @@ func DurationStrToSecondsInt(s string) (*int64, error) {
r := (int64)(duration.Seconds())
return &r, nil
}
func GetEnvsFromFile(file string, opt kobject.ConvertOptions) (map[string]string, error) {
// Get the correct file context / directory
composeDir, err := transformer.GetComposeFileDir(opt.InputFiles)
if err != nil {
return nil, errors.Wrap(err, "Unable to load file context")
}
fileLocation := path.Join(composeDir, file)
// Load environment variables from file
envLoad, err := godotenv.Read(fileLocation)
if err != nil {
return nil, errors.Wrap(err, "Unable to read env_file")
}
return envLoad, nil
}
func FormatEnvName(name string) string {
envName := strings.Trim(name, "./")
envName = strings.Replace(envName, ".", "-", -1)
envName = strings.Replace(envName, "/", "-", -1)
return envName
}

View File

@ -158,6 +158,33 @@ func (k *Kubernetes) InitSvc(name string, service kobject.ServiceConfig) *api.Se
return svc
}
// InitConfigMap initialized a ConfigMap object
func (k *Kubernetes) InitConfigMap(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, envFile string) *api.ConfigMap {
envs, err := GetEnvsFromFile(envFile, opt)
if err != nil {
log.Fatalf("Unable to retrieve env file: %s", err)
}
// Remove root pathing
// replace all other slashes / preiods
envName := FormatEnvName(envFile)
// In order to differentiate files, we append to the name and remove '.env' if applicate from the file name
configMap := &api.ConfigMap{
TypeMeta: unversioned.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: api.ObjectMeta{
Name: envName,
},
Data: envs,
}
return configMap
}
// InitD initializes Kubernetes Deployment object
func (k *Kubernetes) InitD(name string, service kobject.ServiceConfig, replicas int) *extensions.Deployment {
dc := &extensions.Deployment{
@ -476,19 +503,59 @@ func (k *Kubernetes) ConfigPVCVolumeSource(name string, readonly bool) *api.Volu
}
// ConfigEnvs configures the environment variables.
func (k *Kubernetes) ConfigEnvs(name string, service kobject.ServiceConfig) []api.EnvVar {
func (k *Kubernetes) ConfigEnvs(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) ([]api.EnvVar, error) {
envs := transformer.EnvSort{}
for _, v := range service.Environment {
envs = append(envs, api.EnvVar{
Name: v.Name,
Value: v.Value,
})
// If there is an env_file, use ConfigMaps and ignore the environment variables
// already specified
if len(service.EnvFile) > 0 {
// Load each env_file
for _, file := range service.EnvFile {
envName := FormatEnvName(file)
// Load environment variables from file
envLoad, err := GetEnvsFromFile(file, opt)
if err != nil {
return envs, errors.Wrap(err, "Unable to read env_file")
}
// Add configMapKeyRef to each environment variable
for k, _ := range envLoad {
envs = append(envs, api.EnvVar{
Name: k,
ValueFrom: &api.EnvVarSource{
ConfigMapKeyRef: &api.ConfigMapKeySelector{
LocalObjectReference: api.LocalObjectReference{
Name: envName,
},
Key: k,
}},
})
}
}
} else {
// Load up the environment variables
for _, v := range service.Environment {
envs = append(envs, api.EnvVar{
Name: v.Name,
Value: v.Value,
})
}
}
// Stable sorts data while keeping the original order of equal elements
// we need this because envs are not populated in any random order
// this sorting ensures they are populated in a particular order
sort.Stable(envs)
return envs
return envs, nil
}
// CreateKubernetesObjects generates a Kubernetes artifact for each input type service
@ -517,6 +584,13 @@ func (k *Kubernetes) CreateKubernetesObjects(name string, service kobject.Servic
objects = append(objects, k.InitRC(name, service, replica))
}
if len(service.EnvFile) > 0 {
for _, envFile := range service.EnvFile {
configMap := k.InitConfigMap(name, service, opt, envFile)
objects = append(objects, configMap)
}
}
return objects
}
@ -610,7 +684,10 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.
}
}
k.UpdateKubernetesObjects(name, service, &objects)
err := k.UpdateKubernetesObjects(name, service, opt, &objects)
if err != nil {
return nil, errors.Wrap(err, "Error transforming Kubernetes objects")
}
allobjects = append(allobjects, objects...)
}
@ -744,6 +821,12 @@ func (k *Kubernetes) Deploy(komposeObject kobject.KomposeObject, opt kobject.Con
return err
}
log.Infof("Successfully created Pod: %s", t.Name)
case *api.ConfigMap:
_, err := client.ConfigMaps(namespace).Create(t)
if err != nil {
return err
}
log.Infof("Successfully created Config Map: %s", t.Name)
}
}

View File

@ -398,8 +398,11 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C
}
}
// Update and then append the objects (we're done generating)
o.UpdateKubernetesObjects(name, service, &objects)
err := o.UpdateKubernetesObjects(name, service, opt, &objects)
if err != nil {
return nil, errors.Wrap(err, "Error transforming Kubernetes objects")
}
allobjects = append(allobjects, objects...)
}

View File

@ -62,9 +62,10 @@ func TestOpenShiftUpdateKubernetesObjects(t *testing.T) {
var object []runtime.Object
o := OpenShift{}
serviceConfig := newServiceConfig()
opt := kobject.ConvertOptions{}
object = append(object, o.initDeploymentConfig("foobar", serviceConfig, 3))
o.UpdateKubernetesObjects("foobar", serviceConfig, &object)
o.UpdateKubernetesObjects("foobar", serviceConfig, opt, &object)
for _, obj := range object {
switch tobj := obj.(type) {

View File

@ -418,6 +418,12 @@ cmd="kompose convert --stdout -j --provider=openshift -f $KOMPOSE_ROOT/script/te
sed -e "s;%VERSION%;$version;g" -e "s;%CMD%;$cmd;g" "$KOMPOSE_ROOT/script/test/fixtures/healthcheck/output-os-template.json" > /tmp/output-os.json
convert::expect_success "kompose convert --stdout -j --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/healthcheck/docker-compose.yaml" "/tmp/output-os.json"
# Test ConfigMap generation
cmd="kompose convert --stdout -j -f $KOMPOSE_ROOT/script/test/fixtures/configmap/docker-compose.yaml"
sed -e "s;%VERSION%;$version;g" -e "s;%CMD%;$cmd;g" "$KOMPOSE_ROOT/script/test/fixtures/configmap/output-k8s-template.json" > /tmp/output-k8s.json
convert::expect_success "kompose convert --stdout -j -f $KOMPOSE_ROOT/script/test/fixtures/configmap/docker-compose.yaml" "/tmp/output-k8s.json"
# Test V3 Support of Docker Compose
# Test deploy mode: global

View File

@ -0,0 +1,3 @@
# Multi-line test
FOO=BAR
BAR=FOO

View File

@ -0,0 +1,15 @@
version: '3'
services:
redis:
image: 'bitnami/redis:latest'
environment:
- ALLOW_EMPTY_PASSWORD=no
# Env file will override environment / warn!
env_file:
- "foo.env"
- bar.env
labels:
kompose.service.type: nodeport
ports:
- '6379:6379'

View File

@ -0,0 +1,2 @@
# Test comment!
ALLOW_EMPTY_PASSWORD=yes

View File

@ -0,0 +1,135 @@
{
"kind": "List",
"apiVersion": "v1",
"metadata": {},
"items": [
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "redis",
"creationTimestamp": null,
"labels": {
"io.kompose.service": "redis"
},
"annotations": {
"kompose.cmd": "%CMD%",
"kompose.service.type": "nodeport",
"kompose.version": "%VERSION%"
}
},
"spec": {
"ports": [
{
"name": "6379",
"port": 6379,
"targetPort": 6379
}
],
"selector": {
"io.kompose.service": "redis"
},
"type": "NodePort"
},
"status": {
"loadBalancer": {}
}
},
{
"kind": "Deployment",
"apiVersion": "extensions/v1beta1",
"metadata": {
"name": "redis",
"creationTimestamp": null,
"labels": {
"io.kompose.service": "redis"
},
"annotations": {
"kompose.cmd": "%CMD%",
"kompose.service.type": "nodeport",
"kompose.version": "%VERSION%"
}
},
"spec": {
"replicas": 1,
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"io.kompose.service": "redis"
}
},
"spec": {
"containers": [
{
"name": "redis",
"image": "bitnami/redis:latest",
"ports": [
{
"containerPort": 6379
}
],
"env": [
{
"name": "ALLOW_EMPTY_PASSWORD",
"valueFrom": {
"configMapKeyRef": {
"name": "foo-env",
"key": "ALLOW_EMPTY_PASSWORD"
}
}
},
{
"name": "BAR",
"valueFrom": {
"configMapKeyRef": {
"name": "bar-env",
"key": "BAR"
}
}
},
{
"name": "FOO",
"valueFrom": {
"configMapKeyRef": {
"name": "bar-env",
"key": "FOO"
}
}
}
],
"resources": {}
}
],
"restartPolicy": "Always"
}
},
"strategy": {}
},
"status": {}
},
{
"kind": "ConfigMap",
"apiVersion": "v1",
"metadata": {
"name": "foo-env",
"creationTimestamp": null
},
"data": {
"ALLOW_EMPTY_PASSWORD": "yes"
}
},
{
"kind": "ConfigMap",
"apiVersion": "v1",
"metadata": {
"name": "bar-env",
"creationTimestamp": null
},
"data": {
"BAR": "FOO",
"FOO": "BAR"
}
}
]
}