diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index 18c431eb..eb8a4aae 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -233,6 +233,8 @@ type Volumes struct { PVCName string // name of PVC PVCSize string // PVC size SelectorValue string // Value of the label selector + VolumeType string // Type of volume (e.g., "secret") + SecretName string // Name of Kubernetes Secret (if VolumeType is "secret") } // Placement holds the placement struct of container diff --git a/pkg/loader/compose/compose.go b/pkg/loader/compose/compose.go index ca1f77ab..546fe6ef 100644 --- a/pkg/loader/compose/compose.go +++ b/pkg/loader/compose/compose.go @@ -839,12 +839,14 @@ func handleVolume(komposeObject *kobject.KomposeObject, volumes *types.Volumes) errors.Wrap(err, "could not retrieve vvolume") } for volName, vol := range vols { - size, selector := getVolumeLabels(vol.VolumeName, volumes) - if len(size) > 0 || len(selector) > 0 { + size, selector, volumeType, secretName := getVolumeLabels(vol.VolumeName, volumes) + if len(size) > 0 || len(selector) > 0 || len(volumeType) > 0 || len(secretName) > 0 { // We can't assign value to struct field in map while iterating over it, so temporary variable `temp` is used here var temp = vols[volName] temp.PVCSize = size temp.SelectorValue = selector + temp.VolumeType = volumeType + temp.SecretName = secretName vols[volName] = temp } } @@ -949,20 +951,30 @@ func getVol(toFind kobject.Volumes, Vols []kobject.Volumes) (bool, kobject.Volum return false, kobject.Volumes{} } -func getVolumeLabels(name string, volumes *types.Volumes) (string, string) { - size, selector := "", "" +func getVolumeLabels(name string, volumes *types.Volumes) (string, string, string, string) { + size, selector, volumeType, secretName := "", "", "", "" if volume, ok := (*volumes)[name]; ok { + log.Debugf("Getting labels for volume %s, labels: %v", name, volume.Labels) for key, value := range volume.Labels { if key == "kompose.volume.size" { size = value } else if key == "kompose.volume.selector" { selector = value + } else if key == "kompose.volume.type" { + volumeType = value + } else if key == "kompose.volume.secret-name" { + secretName = value } } + if volumeType != "" { + log.Infof("Volume %s has type %s, secret name: %s", name, volumeType, secretName) + } + } else { + log.Debugf("Volume %s not found in volumes map", name) } - return size, selector + return size, selector, volumeType, secretName } // getGroupAdd will return group in int64 format diff --git a/pkg/loader/compose/utils.go b/pkg/loader/compose/utils.go index 38712ea2..4c562cae 100644 --- a/pkg/loader/compose/utils.go +++ b/pkg/loader/compose/utils.go @@ -94,6 +94,12 @@ const ( LabelInitContainerImage = "kompose.init.containers.image" // LabelInitContainerCommand defines commands LabelInitContainerCommand = "kompose.init.containers.command" + // LabelInitContainerService defines service(s) to use as init containers (comma-separated) + LabelInitContainerService = "kompose.init.containers.service" + // LabelVolumeType defines the type of volume (e.g., "secret" for Kubernetes Secret volumes) + LabelVolumeType = "kompose.volume.type" + // LabelVolumeSecretName defines the name of the Kubernetes Secret to use + LabelVolumeSecretName = "kompose.volume.secret-name" // LabelHpaMinReplicas defines min pod replicas LabelHpaMinReplicas = "kompose.hpa.replicas.min" // LabelHpaMaxReplicas defines max pod replicas diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index aad645d0..fb4802d1 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -689,7 +689,7 @@ func (k *Kubernetes) UpdateKubernetesObjectsMultipleContainers(name string, serv } // UpdateKubernetesObjects loads configurations to k8s objects -func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, objects *[]runtime.Object) error { +func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, objects *[]runtime.Object, komposeObject *kobject.KomposeObject) error { // Configure the environment variables. envs, envsFrom, err := ConfigEnvs(service, opt) if err != nil { @@ -729,6 +729,101 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic // Configure annotations annotations := transformer.ConfigAnnotations(service) + // Process init container services if specified + var initContainers []api.Container + if initServicesStr, ok := service.Labels[compose.LabelInitContainerService]; ok && initServicesStr != "" && komposeObject != nil { + initServiceNames := strings.Split(initServicesStr, ",") + for _, initSvcName := range initServiceNames { + initSvcName = strings.TrimSpace(initSvcName) + if initSvcName == "" { + continue + } + + // Find the init container service in the kompose object + initService, exists := komposeObject.ServiceConfigs[initSvcName] + if !exists { + log.Warnf("Init container service %s not found for service %s", initSvcName, name) + continue + } + + log.Infof("Adding init container %s to service %s", initSvcName, name) + + // Configure init container environment variables + initEnvs, initEnvsFrom, err := ConfigEnvs(initService, opt) + if err != nil { + return errors.Wrapf(err, "Unable to load env variables for init container %s", initSvcName) + } + + // Configure init container volumes + initVolumeMounts, initVolumes, initPvc, initCms, err := k.ConfigVolumes(initSvcName, initService) + if err != nil { + return errors.Wrapf(err, "k.ConfigVolumes failed for init container %s", initSvcName) + } + + // Add init container volumes to the main volumes list (deduplicate by name) + existingVolumeNames := make(map[string]bool) + for _, v := range volumes { + existingVolumeNames[v.Name] = true + } + for _, initVol := range initVolumes { + if !existingVolumeNames[initVol.Name] { + volumes = append(volumes, initVol) + existingVolumeNames[initVol.Name] = true + } + } + + // Add init container PVCs and ConfigMaps to objects (deduplicate by name) + // Build a set of existing object names + existingObjectNames := make(map[string]bool) + for _, obj := range *objects { + switch o := obj.(type) { + case *api.PersistentVolumeClaim: + existingObjectNames[o.Name] = true + case *api.ConfigMap: + existingObjectNames[o.Name] = true + } + } + + if initPvc != nil { + for _, p := range initPvc { + if !existingObjectNames[p.Name] { + *objects = append(*objects, p) + existingObjectNames[p.Name] = true + } + } + } + for _, c := range initCms { + if !existingObjectNames[c.Name] { + *objects = append(*objects, c) + existingObjectNames[c.Name] = true + } + } + + // Get init container image + initImage := initService.Image + if initImage == "" { + initImage = initSvcName + } + if opt.CreateChart { + initImage = "{{ " + helmValuesPath(initSvcName, "image", "repository") + " }}:{{ " + helmValuesPath(initSvcName, "image", "tag") + " }}" + } + + // Create init container spec + initContainer := api.Container{ + Name: FormatContainerName(initSvcName), + Image: initImage, + Command: initService.Command, + Args: GetContainerArgs(initService), + Env: initEnvs, + EnvFrom: initEnvsFrom, + VolumeMounts: initVolumeMounts, + WorkingDir: initService.WorkingDir, + } + + initContainers = append(initContainers, initContainer) + } + } + // fillTemplate fills the pod template with the value calculated from config fillTemplate := func(template *api.PodTemplateSpec) error { template.Spec.Containers[0].Name = GetContainerName(service) @@ -865,6 +960,11 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic template.Spec.ServiceAccountName = serviceAccountName } fillInitContainers(template, service) + + // Add init containers from referenced services + if len(initContainers) > 0 { + template.Spec.InitContainers = append(template.Spec.InitContainers, initContainers...) + } return nil } diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index 2f6f828a..c674b34d 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -72,7 +72,7 @@ func helmValuesPath(parts ...string) string { const PVCRequestSize = "100Mi" // ValidVolumeSet has the different types of valid volumes -var ValidVolumeSet = map[string]struct{}{"emptyDir": {}, "hostPath": {}, "configMap": {}, "persistentVolumeClaim": {}} +var ValidVolumeSet = map[string]struct{}{"emptyDir": {}, "hostPath": {}, "configMap": {}, "persistentVolumeClaim": {}, "secret": {}} const ( // DeploymentController is controller type for Deployment @@ -1022,13 +1022,15 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( } // Override volume type if specified in service labels. - if vt, ok := service.Labels["kompose.volume.type"]; ok { + var useSecret bool + if vt, ok := service.Labels[compose.LabelVolumeType]; ok { if _, okk := ValidVolumeSet[vt]; !okk { - return nil, nil, nil, nil, fmt.Errorf("invalid volume type %s specified in label 'kompose.volume.type' in service %s", vt, service.Name) + return nil, nil, nil, nil, fmt.Errorf("invalid volume type %s specified in label '%s' in service %s", vt, compose.LabelVolumeType, service.Name) } useEmptyVolumes = vt == "emptyDir" useHostPath = vt == "hostPath" useConfigMap = vt == "configMap" + useSecret = vt == "secret" } // config volumes from secret if present @@ -1042,23 +1044,52 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( for _, volume := range service.Volumes { // check if ro/rw mode is defined, default rw readonly := len(volume.Mode) > 0 && (volume.Mode == "ro" || volume.Mode == "rox") - mountHost := volume.Host - if mountHost == "" { - mountHost = volume.MountPath - } - // return useconfigmap and readonly, - // not used asigned readonly because dont break e2e - useConfigMap, _, skip = isConfigFile(mountHost) - if skip { - log.Warnf("Skip file in path %s ", volume.Host) - continue + + // Reset volume type flags for each volume (service-level flags don't apply to individual volumes) + volumeUseSecret := useSecret + volumeUseConfigMap := useConfigMap + volumeUseHostPath := useHostPath + volumeUseEmpty := useEmptyVolumes + + // Check volume type from volume labels first, then fall back to file/path detection + if volume.VolumeType == "secret" { + // Explicit secret volume from volume labels + volumeUseSecret = true + volumeUseConfigMap = false + volumeUseHostPath = false + volumeUseEmpty = false + } else if volume.Host != "" { + // Bind mount: detect if it's a config file, otherwise treat as hostPath + volumeUseConfigMap, _, skip = isConfigFile(volume.Host) + if skip { + log.Warnf("Skip file in path %s ", volume.Host) + continue + } + if volumeUseConfigMap { + // It's a config file, use ConfigMap + volumeUseSecret = false + volumeUseHostPath = false + volumeUseEmpty = false + } else { + // It's a regular bind mount, use hostPath + volumeUseConfigMap = false + volumeUseSecret = false + volumeUseHostPath = true + volumeUseEmpty = false + } + } else { + // Named volume with no host path + // Keep volumeUseSecret if set, otherwise will fall through to PVC + volumeUseConfigMap = false + volumeUseHostPath = false + volumeUseEmpty = false } if volume.VolumeName == "" { - if useEmptyVolumes { + if volumeUseEmpty { volumeName = strings.Replace(volume.PVCName, "claim", "empty", 1) - } else if useHostPath { + } else if volumeUseHostPath { volumeName = strings.Replace(volume.PVCName, "claim", "hostpath", 1) - } else if useConfigMap { + } else if volumeUseConfigMap { volumeName = strings.Replace(volume.PVCName, "claim", "cm", 1) } else { volumeName = volume.PVCName @@ -1080,15 +1111,15 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( // For PVC we will also create a PVC object and add to list var volsource *api.VolumeSource - if useEmptyVolumes { + if volumeUseEmpty { volsource = k.ConfigEmptyVolumeSource("volume") - } else if useHostPath { + } else if volumeUseHostPath { source, err := k.ConfigHostPathVolumeSource(volume.Host) if err != nil { return nil, nil, nil, nil, errors.Wrap(err, "k.ConfigHostPathVolumeSource failed") } volsource = source - } else if useConfigMap { + } else if volumeUseConfigMap { log.Debugf("Use configmap volume") cm, err := k.IntiConfigMapFromFileOrDir(name, volumeName, volume.Host, service) if err != nil { @@ -1100,6 +1131,25 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( if useSubPathMount(cm) { volMount.SubPath = volsource.ConfigMap.Items[0].Path } + } else if volumeUseSecret { + // Get secret name from volume config or service label, or default to volume name + secretName := volume.SecretName + if secretName == "" { + secretName = service.Labels[compose.LabelVolumeSecretName] + } + if secretName == "" { + secretName = volumeName + } + log.Infof("Creating Secret volume %s with secret %s", volumeName, secretName) + + // Create secret volume source (all secrets are optional by default) + optional := true + volsource = &api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: secretName, + Optional: &optional, + }, + } } else { volsource = k.ConfigPVCVolumeSource(volumeName, readonly) if volume.VFrom == "" { @@ -1717,6 +1767,22 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. allobjects = append(allobjects, objects...) } } + // Track services used as init containers (to skip creating deployments for them) + initContainerServices := make(map[string]bool) + for _, service := range komposeObject.ServiceConfigs { + if initServicesStr, ok := service.Labels[compose.LabelInitContainerService]; ok && initServicesStr != "" { + // Parse comma-separated list of init container service names + initServices := strings.Split(initServicesStr, ",") + for _, initSvc := range initServices { + initSvc = strings.TrimSpace(initSvc) + if initSvc != "" { + initContainerServices[initSvc] = true + log.Infof("Service %s will be used as init container for service %s", initSvc, service.Name) + } + } + } + } + sortedKeys := SortedKeys(komposeObject.ServiceConfigs) for _, name := range sortedKeys { service := komposeObject.ServiceConfigs[name] @@ -1726,6 +1792,12 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. continue } + // Skip services that are used as init containers for other services + if initContainerServices[name] { + log.Infof("Skipping deployment creation for service %s (used as init container)", name) + continue + } + var objects []runtime.Object service.WithKomposeAnnotation = opt.WithKomposeAnnotation @@ -1753,7 +1825,7 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. service.ServiceType = "Headless" } k.configKubeServiceAndIngressForService(service, name, &objects) - err := k.UpdateKubernetesObjects(name, service, opt, &objects) + err := k.UpdateKubernetesObjects(name, service, opt, &objects, &komposeObject) if err != nil { return nil, errors.Wrap(err, "Error transforming Kubernetes objects") } diff --git a/pkg/transformer/openshift/openshift.go b/pkg/transformer/openshift/openshift.go index 17f7d620..2aa54762 100644 --- a/pkg/transformer/openshift/openshift.go +++ b/pkg/transformer/openshift/openshift.go @@ -431,7 +431,7 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C } } - err := o.UpdateKubernetesObjects(name, service, opt, &objects) + err := o.UpdateKubernetesObjects(name, service, opt, &objects, &komposeObject) if err != nil { return nil, errors.Wrap(err, "Error transforming Kubernetes objects") } diff --git a/pkg/transformer/openshift/openshift_test.go b/pkg/transformer/openshift/openshift_test.go index e326ba5e..9f38114c 100644 --- a/pkg/transformer/openshift/openshift_test.go +++ b/pkg/transformer/openshift/openshift_test.go @@ -74,9 +74,12 @@ func TestOpenShiftUpdateKubernetesObjects(t *testing.T) { o := OpenShift{} serviceConfig := newServiceConfig() opt := kobject.ConvertOptions{} + komposeObject := kobject.KomposeObject{ + ServiceConfigs: map[string]kobject.ServiceConfig{"foobar": serviceConfig}, + } object = append(object, o.initDeploymentConfig("foobar", serviceConfig, 3, opt)) - o.UpdateKubernetesObjects("foobar", serviceConfig, opt, &object) + o.UpdateKubernetesObjects("foobar", serviceConfig, opt, &object, &komposeObject) for _, obj := range object { switch tobj := obj.(type) {