diff --git a/cmd/convert.go b/cmd/convert.go index 19a94b2c..9ea25f62 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -135,7 +135,7 @@ func init() { convertCmd.Flags().BoolVar(&ConvertStdout, "stdout", false, "Print converted objects to stdout") convertCmd.Flags().StringVarP(&ConvertOut, "out", "o", "", "Specify a file name or directory to save objects to (if path does not exist, a file will be created)") convertCmd.Flags().IntVar(&ConvertReplicas, "replicas", 1, "Specify the number of replicas in the generated resource spec") - convertCmd.Flags().StringVar(&ConvertVolumes, "volumes", "persistentVolumeClaim", `Volumes to be generated ("persistentVolumeClaim"|"emptyDir"|"hostPath")`) + convertCmd.Flags().StringVar(&ConvertVolumes, "volumes", "persistentVolumeClaim", `Volumes to be generated ("persistentVolumeClaim"|"emptyDir"|"hostPath" | "configMap")`) // Deprecated commands convertCmd.Flags().BoolVar(&ConvertEmptyVols, "emptyvols", false, "Use Empty Volumes. Do not generate PVCs") diff --git a/pkg/app/app.go b/pkg/app/app.go index f0f29396..4b0f82f3 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -151,8 +151,8 @@ func ValidateFlags(bundle string, args []string, cmd *cobra.Command, opt *kobjec log.Fatalf("YAML and JSON format cannot be provided at the same time") } - if opt.Volumes != "persistentVolumeClaim" && opt.Volumes != "emptyDir" && opt.Volumes != "hostPath" { - log.Fatal("Unknown Volume type: ", opt.Volumes, ", possible values are: persistentVolumeClaim and emptyDir") + if opt.Volumes != "persistentVolumeClaim" && opt.Volumes != "emptyDir" && opt.Volumes != "hostPath" && opt.Volumes != "configMap" { + log.Fatal("Unknown Volume type: ", opt.Volumes, ", possible values are: persistentVolumeClaim, configMap and emptyDir") } } diff --git a/pkg/loader/compose/v3.go b/pkg/loader/compose/v3.go index 35036c0c..49296b8b 100755 --- a/pkg/loader/compose/v3.go +++ b/pkg/loader/compose/v3.go @@ -161,6 +161,7 @@ func loadV3Placement(constraints []string) map[string]string { // TODO: Refactor it similar to loadV3Ports // See: https://docs.docker.com/compose/compose-file/#long-syntax-3 func loadV3Volumes(volumes []types.ServiceVolumeConfig) []string { + var volArray []string for _, vol := range volumes { diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index 20e9805a..6dd8e7be 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -365,7 +365,7 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic } // Configure the container volumes. - volumesMount, volumes, pvc, err := k.ConfigVolumes(name, service) + volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(name, service) if err != nil { return errors.Wrap(err, "k.ConfigVolumes failed") } @@ -388,6 +388,12 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic } } + if cms != nil { + for _, c := range cms { + *objects = append(*objects, c) + } + } + // Configure the container ports. ports := k.ConfigPorts(name, service) @@ -702,7 +708,7 @@ func GetEnvsFromFile(file string, opt kobject.ConvertOptions) (map[string]string } // GetContentFromFile gets the content from the file.. -func GetContentFromFile(file string, opt kobject.ConvertOptions) (string, error) { +func GetContentFromFile(file string) (string, error) { fileBytes, err := ioutil.ReadFile(file) if err != nil { return "", errors.Wrap(err, "Unable to read file") diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index 9f646ae6..2c5dce4b 100755 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -19,8 +19,10 @@ package kubernetes import ( "fmt" "github.com/spf13/pflag" + "io/ioutil" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "os" + "path" "reflect" "regexp" "strconv" @@ -264,9 +266,73 @@ func (k *Kubernetes) InitConfigMapForEnv(name string, service kobject.ServiceCon return configMap } +// IntiConfigMapFromFileOrDir will create a configmap from dir or file +// usage: +// 1. volume +func (k *Kubernetes) IntiConfigMapFromFileOrDir(name, cmName, filePath string, service kobject.ServiceConfig) (*api.ConfigMap, error) { + configMap := &api.ConfigMap{ + TypeMeta: unversioned.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: api.ObjectMeta{ + Name: cmName, + Labels: transformer.ConfigLabels(name), + }, + } + dataMap := make(map[string]string) + + fi, err := os.Stat(filePath) + if err != nil { + return nil, err + } + + switch mode := fi.Mode(); { + case mode.IsDir(): + files, err := ioutil.ReadDir(filePath) + if err != nil { + return nil, err + } + + for _, file := range files { + if !file.IsDir() { + log.Debugf("Read file to ConfigMap: %s", file.Name()) + data, err := GetContentFromFile(filePath + "/" + file.Name()) + if err != nil { + return nil, err + } + dataMap[file.Name()] = data + } + } + configMap.Data = dataMap + + case mode.IsRegular(): + // do file stuff + configMap = k.InitConfigMapFromFile(name, service, filePath) + configMap.Name = cmName + configMap.Annotations = map[string]string{ + "use-subpath": "true", + } + } + + return configMap, nil +} + +// useSubPathMount check if a configmap should be mounted as subpath +// in this situation, this configmap will only contains 1 key in data +func useSubPathMount(cm *api.ConfigMap) bool { + if cm.Annotations == nil { + return false + } + if cm.Annotations["use-subpath"] != "true" { + return false + } + return true +} + //InitConfigMapFromFile initializes a ConfigMap object -func (k *Kubernetes) InitConfigMapFromFile(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, fileName string) *api.ConfigMap { - content, err := GetContentFromFile(fileName, opt) +func (k *Kubernetes) InitConfigMapFromFile(name string, service kobject.ServiceConfig, fileName string) *api.ConfigMap { + content, err := GetContentFromFile(fileName) if err != nil { log.Fatalf("Unable to retrieve file: %s", err) } @@ -274,7 +340,9 @@ func (k *Kubernetes) InitConfigMapFromFile(name string, service kobject.ServiceC originFileName := FormatFileName(fileName) dataMap := make(map[string]string) + dataMap[originFileName] = content + configMapName := "" for key, tmpConfig := range service.ConfigsMetaData { if tmpConfig.File == fileName { @@ -410,7 +478,7 @@ func (k *Kubernetes) CreateSecrets(komposeObject kobject.KomposeObject) ([]*api. var objects []*api.Secret for name, config := range komposeObject.Secrets { if config.File != "" { - dataString, err := GetContentFromFile(config.File, k.Opt) + dataString, err := GetContentFromFile(config.File) if err != nil { log.Fatal("unable to read secret from file: ", config.File) return nil, err @@ -673,25 +741,23 @@ func (k *Kubernetes) ConfigSecretVolumes(name string, service kobject.ServiceCon } // ConfigVolumes configure the container volumes. -func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ([]api.VolumeMount, []api.Volume, []*api.PersistentVolumeClaim, error) { +func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ([]api.VolumeMount, []api.Volume, []*api.PersistentVolumeClaim, []*api.ConfigMap, error) { volumeMounts := []api.VolumeMount{} volumes := []api.Volume{} var PVCs []*api.PersistentVolumeClaim + var cms []*api.ConfigMap var volumeName string // Set a var based on if the user wants to use empty volumes // as opposed to persistent volumes and volume claims useEmptyVolumes := k.Opt.EmptyVols - useHostPath := false + useHostPath := k.Opt.Volumes == "hostPath" + useConfigMap := k.Opt.Volumes == "configMap" if k.Opt.Volumes == "emptyDir" { useEmptyVolumes = true } - if k.Opt.Volumes == "hostPath" { - useHostPath = true - } - // config volumes from secret if present secretsVolumeMounts, secretsVolumes := k.ConfigSecretVolumes(name, service) volumeMounts = append(volumeMounts, secretsVolumeMounts...) @@ -709,6 +775,8 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( volumeName = strings.Replace(volume.PVCName, "claim", "empty", 1) } else if useHostPath { volumeName = strings.Replace(volume.PVCName, "claim", "hostpath", 1) + } else if useConfigMap { + volumeName = strings.Replace(volume.PVCName, "claim", "cm", 1) } else { volumeName = volume.PVCName } @@ -721,7 +789,7 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( ReadOnly: readonly, MountPath: volume.Container, } - volumeMounts = append(volumeMounts, volMount) + // Get a volume source based on the type of volume we are using // For PVC we will also create a PVC object and add to list var volsource *api.VolumeSource @@ -731,9 +799,23 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( } else if useHostPath { source, err := k.ConfigHostPathVolumeSource(volume.Host) if err != nil { - return nil, nil, nil, errors.Wrap(err, "k.ConfigHostPathVolumeSource failed") + return nil, nil, nil, nil, errors.Wrap(err, "k.ConfigHostPathVolumeSource failed") } volsource = source + } else if useConfigMap { + log.Debugf("Use configmap volume") + + if cm, err := k.IntiConfigMapFromFileOrDir(name, volumeName, volume.Host, service); err != nil { + return nil, nil, nil, nil, err + } else { + cms = append(cms, cm) + volsource = k.ConfigConfigMapVolumeSource(volumeName, volume.Container, cm) + + if useSubPathMount(cm) { + volMount.SubPath = volsource.ConfigMap.Items[0].Path + } + } + } else { volsource = k.ConfigPVCVolumeSource(volumeName, readonly) if volume.VFrom == "" { @@ -752,13 +834,14 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( createdPVC, err := k.CreatePVC(volumeName, volume.Mode, defaultSize, volume.SelectorValue) if err != nil { - return nil, nil, nil, errors.Wrap(err, "k.CreatePVC failed") + return nil, nil, nil, nil, errors.Wrap(err, "k.CreatePVC failed") } PVCs = append(PVCs, createdPVC) } } + volumeMounts = append(volumeMounts, volMount) // create a new volume object using the volsource and add to list vol := api.Volume{ @@ -767,13 +850,13 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( } volumes = append(volumes, vol) - if len(volume.Host) > 0 && !useHostPath { + if len(volume.Host) > 0 && (!useHostPath && !useConfigMap) { log.Warningf("Volume mount on the host %q isn't supported - ignoring path on the host", volume.Host) } } - return volumeMounts, volumes, PVCs, nil + return volumeMounts, volumes, PVCs, cms, nil } // ConfigEmptyVolumeSource is helper function to create an EmptyDir api.VolumeSource @@ -794,6 +877,30 @@ func (k *Kubernetes) ConfigEmptyVolumeSource(key string) *api.VolumeSource { } +// ConfigHostPathVolumeSource config a configmap to use as volume source +func (k *Kubernetes) ConfigConfigMapVolumeSource(cmName string, targetPath string, cm *api.ConfigMap) *api.VolumeSource { + s := api.ConfigMapVolumeSource{} + s.Name = cmName + if useSubPathMount(cm) { + var keys []string + for k := range cm.Data { + keys = append(keys, k) + } + key := keys[0] + _, p := path.Split(targetPath) + s.Items = []api.KeyToPath{ + { + Key: key, + Path: p, + }, + } + } + return &api.VolumeSource{ + ConfigMap: &s, + } + +} + // ConfigHostPathVolumeSource is a helper function to create a HostPath api.VolumeSource func (k *Kubernetes) ConfigHostPathVolumeSource(path string) (*api.VolumeSource, error) { dir, err := transformer.GetComposeFileDir(k.Opt.InputFiles) @@ -942,7 +1049,7 @@ func (k *Kubernetes) createConfigMapFromComposeConfig(name string, opt kobject.C continue } currentFileName := currentConfigObj.File - configMap := k.InitConfigMapFromFile(name, service, opt, currentFileName) + configMap := k.InitConfigMapFromFile(name, service, currentFileName) objects = append(objects, configMap) } return objects diff --git a/script/test/cmd/tests.sh b/script/test/cmd/tests.sh index ec923486..4f5a9b86 100755 --- a/script/test/cmd/tests.sh +++ b/script/test/cmd/tests.sh @@ -621,6 +621,16 @@ sed -e "s;%VERSION%;$version;g" -e "s;%CMD%;$cmd;g" "$KOMPOSE_ROOT/script/test/ convert::expect_success "$cmd" "/tmp/output-k8s.json" +# Test configmap as volume +cmd="kompose convert --stdout -j --volumes=configMap -f $KOMPOSE_ROOT/script/test/fixtures/configmap-volume/docker-compose.yml" +sed -e "s;%VERSION%;$version;g" -e "s;%CMD%;$cmd;g" "$KOMPOSE_ROOT/script/test/fixtures/configmap-volume/output-k8s.json" > /tmp/output-k8s.json +convert::expect_success "$cmd" "/tmp/output-k8s.json" + +cmd="kompose convert --stdout --provider=openshift -j --volumes=configMap -f $KOMPOSE_ROOT/script/test/fixtures/configmap-volume/docker-compose.yml" +sed -e "s;%VERSION%;$version;g" -e "s;%CMD%;$cmd;g" "$KOMPOSE_ROOT/script/test/fixtures/configmap-volume/output-oc.json" > /tmp/output-oc.json +convert::expect_success "$cmd" "/tmp/output-oc.json" + + # Test V3 Support of Docker Compose # Test deploy mode: global diff --git a/script/test/fixtures/configmap-volume/docker-compose.yml b/script/test/fixtures/configmap-volume/docker-compose.yml new file mode 100644 index 00000000..669014c5 --- /dev/null +++ b/script/test/fixtures/configmap-volume/docker-compose.yml @@ -0,0 +1,7 @@ +version: "3" +services: + web: + image: nginx + volumes: + - ./tls:/etc/tls + - ./tls/a.key:/etc/test-a-key.key diff --git a/script/test/fixtures/configmap-volume/output-k8s.json b/script/test/fixtures/configmap-volume/output-k8s.json new file mode 100644 index 00000000..aaec29f9 --- /dev/null +++ b/script/test/fixtures/configmap-volume/output-k8s.json @@ -0,0 +1,121 @@ +{ + "kind": "List", + "apiVersion": "v1", + "metadata": {}, + "items": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "kompose.cmd": "%CMD%", + "kompose.version": "%VERSION%" + }, + "creationTimestamp": null, + "labels": { + "io.kompose.service": "web" + }, + "name": "web" + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "io.kompose.service": "web" + } + }, + "strategy": { + "type": "Recreate" + }, + "template": { + "metadata": { + "annotations": { + "kompose.cmd": "%CMD%", + "kompose.version": "%VERSION%" + }, + "creationTimestamp": null, + "labels": { + "io.kompose.service": "web" + } + }, + "spec": { + "containers": [ + { + "image": "nginx", + "imagePullPolicy": "", + "name": "web", + "resources": {}, + "volumeMounts": [ + { + "mountPath": "/etc/tls", + "name": "web-cm0" + }, + { + "mountPath": "/etc/test-a-key.key", + "name": "web-cm1", + "subPath": "test-a-key.key" + } + ] + } + ], + "restartPolicy": "Always", + "serviceAccountName": "", + "volumes": [ + { + "configMap": { + "name": "web-cm0" + }, + "name": "web-cm0" + }, + { + "configMap": { + "items": [ + { + "key": "a.key", + "path": "test-a-key.key" + } + ], + "name": "web-cm1" + }, + "name": "web-cm1" + } + ] + } + } + }, + "status": {} + }, + { + "kind": "ConfigMap", + "apiVersion": "v1", + "metadata": { + "name": "web-cm0", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "web" + } + }, + "data": { + "a.crt": "test-crt-data...", + "a.key": "test-key-data...." + } + }, + { + "kind": "ConfigMap", + "apiVersion": "v1", + "metadata": { + "name": "web-cm1", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "web" + }, + "annotations": { + "use-subpath": "true" + } + }, + "data": { + "a.key": "test-key-data...." + } + } + ] +} diff --git a/script/test/fixtures/configmap-volume/output-oc.json b/script/test/fixtures/configmap-volume/output-oc.json new file mode 100644 index 00000000..60b61ad3 --- /dev/null +++ b/script/test/fixtures/configmap-volume/output-oc.json @@ -0,0 +1,161 @@ +{ + "kind": "List", + "apiVersion": "v1", + "metadata": {}, + "items": [ + { + "kind": "DeploymentConfig", + "apiVersion": "v1", + "metadata": { + "name": "web", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "web" + }, + "annotations": { + "kompose.cmd": "%CMD%", + "kompose.version": "%VERSION%" + } + }, + "spec": { + "strategy": { + "type": "Recreate", + "resources": {} + }, + "triggers": [ + { + "type": "ConfigChange" + }, + { + "type": "ImageChange", + "imageChangeParams": { + "automatic": true, + "containerNames": [ + "web" + ], + "from": { + "kind": "ImageStreamTag", + "name": "web:latest" + } + } + } + ], + "replicas": 1, + "test": false, + "selector": { + "io.kompose.service": "web" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "io.kompose.service": "web" + } + }, + "spec": { + "volumes": [ + { + "name": "web-cm0", + "configMap": { + "name": "web-cm0" + } + }, + { + "name": "web-cm1", + "configMap": { + "name": "web-cm1", + "items": [ + { + "key": "a.key", + "path": "test-a-key.key" + } + ] + } + } + ], + "containers": [ + { + "name": "web", + "image": " ", + "resources": {}, + "volumeMounts": [ + { + "name": "web-cm0", + "mountPath": "/etc/tls" + }, + { + "name": "web-cm1", + "mountPath": "/etc/test-a-key.key", + "subPath": "test-a-key.key" + } + ] + } + ], + "restartPolicy": "Always" + } + } + }, + "status": {} + }, + { + "kind": "ImageStream", + "apiVersion": "v1", + "metadata": { + "name": "web", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "web" + } + }, + "spec": { + "tags": [ + { + "name": "latest", + "annotations": null, + "from": { + "kind": "DockerImage", + "name": "nginx" + }, + "generation": null, + "importPolicy": {} + } + ] + }, + "status": { + "dockerImageRepository": "" + } + }, + { + "kind": "ConfigMap", + "apiVersion": "v1", + "metadata": { + "name": "web-cm0", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "web" + } + }, + "data": { + "a.crt": "test-crt-data...", + "a.key": "test-key-data...." + } + }, + { + "kind": "ConfigMap", + "apiVersion": "v1", + "metadata": { + "name": "web-cm1", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "web" + }, + "annotations": { + "use-subpath": "true" + } + }, + "data": { + "a.key": "test-key-data...." + } + } + ] +} diff --git a/script/test/fixtures/configmap-volume/tls/a.crt b/script/test/fixtures/configmap-volume/tls/a.crt new file mode 100644 index 00000000..6fc24b77 --- /dev/null +++ b/script/test/fixtures/configmap-volume/tls/a.crt @@ -0,0 +1 @@ +test-crt-data... \ No newline at end of file diff --git a/script/test/fixtures/configmap-volume/tls/a.key b/script/test/fixtures/configmap-volume/tls/a.key new file mode 100644 index 00000000..1a0782b1 --- /dev/null +++ b/script/test/fixtures/configmap-volume/tls/a.key @@ -0,0 +1 @@ +test-key-data.... \ No newline at end of file