diff --git a/cmd/convert.go b/cmd/convert.go index 1ab6c90e..7854c2ac 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -63,6 +63,12 @@ var ( ServiceGroupMode string ServiceGroupName string + + // SecretsAsFiles forces secrets to result in files inside a container instead of symlinked directories containing + // files of the same name. This reproduces the behavior of file-based secrets in docker-compose and should probably + // be the default for kompose, but we must keep compatibility with the previous behavior. + // See https://github.com/kubernetes/kompose/issues/1280 for more details. + SecretsAsFiles bool ) var convertCmd = &cobra.Command{ @@ -109,6 +115,7 @@ var convertCmd = &cobra.Command{ MultipleContainerMode: MultipleContainerMode, ServiceGroupMode: ServiceGroupMode, ServiceGroupName: ServiceGroupName, + SecretsAsFiles: SecretsAsFiles, } if ServiceGroupMode == "" && MultipleContainerMode { @@ -145,6 +152,7 @@ func init() { convertCmd.Flags().StringVar(&ServiceGroupMode, "service-group-mode", "", "Group multiple service to create single workload by `label`(`kompose.service.group`) or `volume`(shared volumes)") convertCmd.Flags().StringVar(&ServiceGroupName, "service-group-name", "", "Using with --service-group-mode=volume to specific a final service name for the group") convertCmd.Flags().MarkDeprecated("multiple-container-mode", "use --service-group-mode=label") + convertCmd.Flags().BoolVar(&SecretsAsFiles, "secrets-as-files", false, "Always convert docker-compose secrets into files instead of symlinked directories.") // OpenShift only convertCmd.Flags().BoolVar(&ConvertDeploymentConfig, "deployment-config", true, "Generate an OpenShift deploymentconfig object") diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index 94a2df34..c5913d6c 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -83,6 +83,7 @@ type ConvertOptions struct { MultipleContainerMode bool ServiceGroupMode string ServiceGroupName string + SecretsAsFiles bool } // IsPodController indicate if the user want to use a controller diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index 2fc2ca3c..fa384145 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -29,6 +29,7 @@ import ( "strconv" "strings" + "github.com/docker/cli/cli/compose/types" "github.com/fatih/structs" "github.com/kubernetes/kompose/pkg/kobject" "github.com/kubernetes/kompose/pkg/loader/compose" @@ -758,7 +759,7 @@ func (k *Kubernetes) ConfigTmpfs(name string, service kobject.ServiceConfig) ([] // In kubernetes' Secret resource, it has a data structure like a map[string]bytes, every key will act like the file name // when mount to a container. This is the part that missing in compose. So we will create a single key secret from compose // config and the key's name will be the secret's name, it's value is the file content. -// compose'secret can only be mounted at `/run/secrets`, so we will hardcoded this. +// compose's secret can only be mounted at `/run/secrets`, so this will be hardcoded. func (k *Kubernetes) ConfigSecretVolumes(name string, service kobject.ServiceConfig) ([]api.VolumeMount, []api.Volume) { var volumeMounts []api.VolumeMount var volumes []api.Volume @@ -771,35 +772,11 @@ func (k *Kubernetes) ConfigSecretVolumes(name string, service kobject.ServiceCon log.Warnf("Ignore gid in secrets for service: %s", name) } - var itemPath string // should be the filename - var mountPath = "" // should be the directory - // if is used the short-syntax - if secretConfig.Target == "" { - // the secret path (mountPath) should be inside the default directory /run/secrets - mountPath = "/run/secrets/" + secretConfig.Source - // the itemPath should be the source itself - itemPath = secretConfig.Source + var secretItemPath, secretMountPath, secretSubPath string + if k.Opt.SecretsAsFiles { + secretItemPath, secretMountPath, secretSubPath = k.getSecretPaths(secretConfig) } else { - // if is the long-syntax, i should get the last part of path and consider it the filename - pathSplitted := strings.Split(secretConfig.Target, "/") - lastPart := pathSplitted[len(pathSplitted)-1] - - // if the filename (lastPart) and the target is the same - if lastPart == secretConfig.Target { - // the secret path should be the source (it need to be inside a directory and only the filename was given) - mountPath = secretConfig.Source - } else { - // should then get the target without the filename (lastPart) - mountPath = mountPath + strings.TrimSuffix(secretConfig.Target, "/"+lastPart) // menos ultima parte - } - - // if the target isn't absolute path - if strings.HasPrefix(secretConfig.Target, "/") == false { - // concat the default secret directory - mountPath = "/run/secrets/" + mountPath - } - - itemPath = lastPart + secretItemPath, secretMountPath, secretSubPath = k.getSecretPathsLegacy(secretConfig) } volSource := api.VolumeSource{ @@ -807,7 +784,7 @@ func (k *Kubernetes) ConfigSecretVolumes(name string, service kobject.ServiceCon SecretName: secretConfig.Source, Items: []api.KeyToPath{{ Key: secretConfig.Source, - Path: itemPath, + Path: secretItemPath, }}, }, } @@ -825,7 +802,8 @@ func (k *Kubernetes) ConfigSecretVolumes(name string, service kobject.ServiceCon volMount := api.VolumeMount{ Name: vol.Name, - MountPath: mountPath, + MountPath: secretMountPath, + SubPath: secretSubPath, } volumeMounts = append(volumeMounts, volMount) } @@ -833,6 +811,75 @@ func (k *Kubernetes) ConfigSecretVolumes(name string, service kobject.ServiceCon return volumeMounts, volumes } +func (k *Kubernetes) getSecretPaths(secretConfig types.ServiceSecretConfig) (secretItemPath, secretMountPath, secretSubPath string) { + // Default secretConfig.Target to secretConfig.Source, just in case user was using short secret syntax or + // otherwise did not define a specific target + target := secretConfig.Target + if target == "" { + target = secretConfig.Source + } + + // If target is an absolute path, set that as the MountPath + if strings.HasPrefix(secretConfig.Target, "/") { + secretMountPath = target + } else { + // If target is a relative path, prefix with "/run/secrets/" to replicate what docker-compose would do + secretMountPath = "/run/secrets/" + target + } + + // Set subPath to the target filename. this ensures that we end up with a file at our MountPath instead + // of a directory with symlinks (see https://stackoverflow.com/a/68332231) + splitPath := strings.Split(target, "/") + secretFilename := splitPath[len(splitPath)-1] + + // `secretItemPath` and `secretSubPath` have to be the same as `secretFilename` to ensure we create a file with + // that name at `secretMountPath`, instead of a directory containing a symlink to the actual file. + secretItemPath = secretFilename + secretSubPath = secretFilename + + return secretItemPath, secretMountPath, secretSubPath +} + +func (k *Kubernetes) getSecretPathsLegacy(secretConfig types.ServiceSecretConfig) (secretItemPath, secretMountPath, secretSubPath string) { + // The old way of setting secret paths. It resulted in files being placed in incorrect locations when compared to + // docker-compose results, but some people might depend on this behavior so this is kept here for compatibility. + // See https://github.com/kubernetes/kompose/issues/1280 for more details. + + var itemPath string // should be the filename + var mountPath = "" // should be the directory + // if is used the short-syntax + if secretConfig.Target == "" { + // the secret path (mountPath) should be inside the default directory /run/secrets + mountPath = "/run/secrets/" + secretConfig.Source + // the itemPath should be the source itself + itemPath = secretConfig.Source + } else { + // if is the long-syntax, i should get the last part of path and consider it the filename + pathSplitted := strings.Split(secretConfig.Target, "/") + lastPart := pathSplitted[len(pathSplitted)-1] + + // if the filename (lastPart) and the target is the same + if lastPart == secretConfig.Target { + // the secret path should be the source (it need to be inside a directory and only the filename was given) + mountPath = secretConfig.Source + } else { + // should then get the target without the filename (lastPart) + mountPath = mountPath + strings.TrimSuffix(secretConfig.Target, "/"+lastPart) // menos ultima parte + } + + // if the target isn't absolute path + if strings.HasPrefix(secretConfig.Target, "/") == false { + // concat the default secret directory + mountPath = "/run/secrets/" + mountPath + } + + itemPath = lastPart + } + + secretSubPath = "" // We didn't set a SubPath in legacy behavior + return itemPath, mountPath, "" +} + // ConfigVolumes configure the container volumes. func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ([]api.VolumeMount, []api.Volume, []*api.PersistentVolumeClaim, []*api.ConfigMap, error) { volumeMounts := []api.VolumeMount{}