From acf24e94d5d889063c44672953e7227e3732b214 Mon Sep 17 00:00:00 2001 From: Diogo de Campos Date: Wed, 19 Jan 2022 12:44:41 +0100 Subject: [PATCH] Fixed secret file locations to match results from docker-compose when using file-based secrets --- cmd/convert.go | 8 ++ pkg/kobject/kobject.go | 1 + pkg/transformer/kubernetes/kubernetes.go | 109 ++++++++++++++++------- 3 files changed, 87 insertions(+), 31 deletions(-) diff --git a/cmd/convert.go b/cmd/convert.go index 4f419816..dbe1b3dd 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 3cd6aea0..95bfd54a 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -82,6 +82,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 b2fd42f7..84136212 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" @@ -751,7 +752,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 @@ -764,35 +765,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{ @@ -800,7 +777,7 @@ func (k *Kubernetes) ConfigSecretVolumes(name string, service kobject.ServiceCon SecretName: secretConfig.Source, Items: []api.KeyToPath{{ Key: secretConfig.Source, - Path: itemPath, + Path: secretItemPath, }}, }, } @@ -818,7 +795,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) } @@ -826,6 +804,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{}