From e5e0d5e117829446eb8b558e25b44450f877f63d Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Thu, 4 Dec 2025 06:04:04 +0000 Subject: [PATCH] Generate template placeholders charts and support init containers and jobs (#1) Part of https://plan.wireit.in/deepstack/browse/VUL-265/ Reviewed-on: https://git.vdb.to/LaconicNetwork/kompose/pulls/1 Co-authored-by: Prathamesh Musale Co-committed-by: Prathamesh Musale --- .github/workflows/go.yml | 2 +- .github/workflows/publish.yml | 49 +++ .github/workflows/test.yml | 42 +-- Makefile | 10 +- build/VERSION | 2 +- pkg/app/app.go | 7 +- pkg/kobject/kobject.go | 23 +- pkg/loader/compose/compose.go | 29 +- pkg/loader/compose/utils.go | 8 + pkg/transformer/kubernetes/k8sutils.go | 355 +++++++++++++++++- pkg/transformer/kubernetes/kubernetes.go | 306 ++++++++++++--- pkg/transformer/kubernetes/kubernetes_test.go | 6 +- pkg/transformer/openshift/openshift.go | 19 +- pkg/transformer/openshift/openshift_test.go | 10 +- pkg/version/version.go | 2 +- 15 files changed, 752 insertions(+), 118 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 18acebc1..92ba708f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -29,7 +29,7 @@ jobs: run: make bin - name: Upload a Build Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: "kompose" path: "kompose" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..b3bf2ec3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Release + +on: + release: + types: [created] + +env: + TERM: dumb + +jobs: + build-and-release: + name: Build and Upload Release Assets + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v5 + with: + go-version: ^1.21 + id: go + + - name: Check out code + uses: actions/checkout@v4 + + - name: Build Linux AMD64 binary + run: make cross + + - name: Check binary permissions + run: | + cd bin + ls -la kompose-linux-amd64 + + - name: Get the version + id: vars + run: | + echo ::set-output name=tag::$(echo ${GITHUB_REF#refs/tags/}) + + - name: Upload binary to release assets + uses: https://gitea.com/cerc-io/action-gh-release@gitea-v2 + with: + files: ./bin/kompose-linux-amd64 + token: ${{ secrets.CICD_PUBLISH_TOKEN }} + + - name: Publish binary as generic package + run: | + curl --user "${{ github.repository_owner }}:${{ secrets.CICD_PUBLISH_TOKEN }}" \ + --upload-file ./bin/kompose-linux-amd64 \ + "https://git.vdb.to/api/packages/${{ github.repository_owner }}/generic/kompose/${{ steps.vars.outputs.tag }}/kompose-linux-amd64" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e178aaf3..9b209208 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Kompose CI on: push: branches: - - main + - main pull_request: env: # Avoid noisy outputs like "tput: No value for $TERM and no -T specified" @@ -27,23 +27,23 @@ jobs: - name: Perform cross compile if: ${{ matrix.cross_compile }} run: make cross - docs: - name: Build docs and Coveralls integration - runs-on: ubuntu-latest - needs: test - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ^1.21 - - name: Install dyff - run: go install github.com/homeport/dyff/cmd/dyff@v1.5.8 - - name: Create .coverprofile for each targeted directory by re:running tests - run: make test - - name: Collect all .coverprofile files and save it to one file gover.coverprofile - run: gover - - name: Send coverage - run: goveralls -coverprofile=gover.coverprofile -service=github - env: - # As per https://github.com/mattn/goveralls#github-actions - COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # docs: + # name: Build docs and Coveralls integration + # runs-on: ubuntu-latest + # needs: test + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-go@v5 + # with: + # go-version: ^1.21 + # - name: Install dyff + # run: go install github.com/homeport/dyff/cmd/dyff@v1.5.8 + # - name: Create .coverprofile for each targeted directory by re:running tests + # run: make test + # - name: Collect all .coverprofile files and save it to one file gover.coverprofile + # run: gover + # - name: Send coverage + # run: goveralls -coverprofile=gover.coverprofile -service=github + # env: + # # As per https://github.com/mattn/goveralls#github-actions + # COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index fdeb731d..e666b571 100644 --- a/Makefile +++ b/Makefile @@ -49,11 +49,11 @@ install: .PHONY: cross cross: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-amd64" main.go - GOOS=linux GOARCH=arm CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-arm" main.go - GOOS=linux GOARCH=arm64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-arm64" main.go - GOOS=windows GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-windows-amd64.exe" main.go - GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-darwin-amd64" main.go - GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-darwin-arm64" main.go +# GOOS=linux GOARCH=arm CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-arm" main.go +# GOOS=linux GOARCH=arm64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-arm64" main.go +# GOOS=windows GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-windows-amd64.exe" main.go +# GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-darwin-amd64" main.go +# GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-darwin-arm64" main.go .PHONY: clean clean: diff --git a/build/VERSION b/build/VERSION index bf50e910..b2822451 100644 --- a/build/VERSION +++ b/build/VERSION @@ -1 +1 @@ -1.37.0 +1.37.0-zenith-0.0.1 diff --git a/pkg/app/app.go b/pkg/app/app.go index 64f1ba13..d01ff4fa 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -261,7 +261,12 @@ func Convert(opt kobject.ConvertOptions) ([]runtime.Object, error) { } // Print output - err = kubernetes.PrintList(objects, opt) + // Extract kubernetes transformer for PVC values if applicable + var k8sTransformer *kubernetes.Kubernetes + if k8s, ok := t.(*kubernetes.Kubernetes); ok { + k8sTransformer = k8s + } + err = kubernetes.PrintList(objects, opt, komposeObject, k8sTransformer) if err != nil { log.Fatalf(err.Error()) } diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index 18c431eb..b57cf9d4 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -223,16 +223,19 @@ func (port *Ports) ID() string { // Volumes holds the volume struct of container type Volumes struct { - SvcName string // Service name to which volume is linked - MountPath string // Mountpath extracted from docker-compose file - VFrom string // denotes service name from which volume is coming - VolumeName string // name of volume if provided explicitly - Host string // host machine address - Container string // Mountpath - Mode string // access mode for volume - PVCName string // name of PVC - PVCSize string // PVC size - SelectorValue string // Value of the label selector + SvcName string // Service name to which volume is linked + MountPath string // Mountpath extracted from docker-compose file + VFrom string // denotes service name from which volume is coming + VolumeName string // name of volume if provided explicitly + Host string // host machine address + Container string // Mountpath + Mode string // access mode for volume + 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") + Annotations map[string]string // Annotations to add to PVC (e.g., "helm.sh/resource-policy": "keep") } // Placement holds the placement struct of container diff --git a/pkg/loader/compose/compose.go b/pkg/loader/compose/compose.go index ca1f77ab..fd2a1fdf 100644 --- a/pkg/loader/compose/compose.go +++ b/pkg/loader/compose/compose.go @@ -839,12 +839,15 @@ 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, annotations := getVolumeLabels(vol.VolumeName, volumes) + if len(size) > 0 || len(selector) > 0 || len(volumeType) > 0 || len(secretName) > 0 || len(annotations) > 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 + temp.Annotations = annotations vols[volName] = temp } } @@ -949,20 +952,36 @@ 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, map[string]string) { + size, selector, volumeType, secretName := "", "", "", "" + annotations := make(map[string]string) 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 + } else if strings.HasPrefix(key, "kompose.volume.annotations/") { + // Extract annotation key by removing prefix and replacing ~ with / + annotationKey := strings.TrimPrefix(key, "kompose.volume.annotations/") + annotationKey = strings.ReplaceAll(annotationKey, "~", "/") + annotations[annotationKey] = 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, annotations } // getGroupAdd will return group in int64 format diff --git a/pkg/loader/compose/utils.go b/pkg/loader/compose/utils.go index 38712ea2..25f3c325 100644 --- a/pkg/loader/compose/utils.go +++ b/pkg/loader/compose/utils.go @@ -94,6 +94,14 @@ 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" + // LabelVolumeAnnotationsPrefix is the prefix for volume annotations (e.g., kompose.volume.annotations/helm.sh~resource-policy: keep) + LabelVolumeAnnotationsPrefix = "kompose.volume.annotations/" // 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 82421630..331d23a9 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -82,10 +82,150 @@ type DeploymentMapping struct { TargetDeploymentName string } +// ServiceValues holds the extracted values for templating +type ServiceValues struct { + Image struct { + Repository string + Tag string + PullPolicy string + } + Env map[string]string +} + +type PersistenceValues struct { + Size string +} + +// splitImage splits "repo:tag" or "repo" into repository and tag +func splitImage(image string) (string, string) { + parts := strings.Split(image, ":") + if len(parts) == 2 { + return parts[0], parts[1] + } + // No tag specified, use "latest" + return image, "latest" +} + +// unquoteHelmTemplates removes quotes around Helm template syntax that yaml.Marshal adds +func unquoteHelmTemplates(yamlBytes []byte) []byte { + yamlStr := string(yamlBytes) + lines := strings.Split(yamlStr, "\n") + + for i, line := range lines { + // If line contains Helm template syntax {{...}} + if strings.Contains(line, "{{") && strings.Contains(line, "}}") { + // Remove outer quotes only - find the first and last quote on the line + // that enclose the entire value (after the colon for YAML) + if idx := strings.Index(line, ": "); idx != -1 { + prefix := line[:idx+2] // Keep "key: " + value := line[idx+2:] // Get the value part + + // Remove surrounding quotes from value if present + if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) || + (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) { + value = value[1 : len(value)-1] + } + lines[i] = prefix + value + } + } + } + + return []byte(strings.Join(lines, "\n")) +} + +// templatePVCStorage replaces PVC storage values with Helm templates +// TODO: This post-processes YAML because resource.Quantity validates input and rejects template strings +func templatePVCStorage(yamlBytes []byte, persistenceValues map[string]PersistenceValues) []byte { + yamlStr := string(yamlBytes) + + // Check if this is a PersistentVolumeClaim + if !strings.Contains(yamlStr, "kind: PersistentVolumeClaim") { + return yamlBytes + } + + // Extract PVC name from metadata + var pvcName string + lines := strings.Split(yamlStr, "\n") + inMetadata := false + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "metadata:" { + inMetadata = true + continue + } + if inMetadata && strings.HasPrefix(trimmed, "name:") { + parts := strings.SplitN(trimmed, ":", 2) + if len(parts) == 2 { + pvcName = strings.TrimSpace(parts[1]) + break + } + } + // Exit metadata section if we hit another top-level key + if inMetadata && len(line) > 0 && line[0] != ' ' && line[0] != '\t' && !strings.HasPrefix(trimmed, "name:") { + break + } + } + + // If we found the PVC name and it's in our persistence values, template it + if pvcName != "" { + if _, exists := persistenceValues[pvcName]; exists { + templateStr := "{{ index .Values \"persistence\" \"" + pvcName + "\" \"size\" }}" + + // Replace storage value with template + for i, line := range lines { + if strings.Contains(line, "storage:") { + log.Debugf("Found storage line: %q", line) + prefix := line[:strings.Index(line, ":")+2] + log.Debugf("Prefix: %q, Template: %q", prefix, templateStr) + lines[i] = prefix + templateStr + log.Debugf("New line: %q", lines[i]) + break + } + } + yamlStr = strings.Join(lines, "\n") + log.Debugf("Final templated YAML:\n%s", yamlStr) + } + } + + return []byte(yamlStr) +} + +// extractValuesFromKomposeObject extracts service values from KomposeObject for values.yaml +// Note: PVC values are extracted during transformation and stored in Kubernetes.PersistenceValues +func extractValuesFromKomposeObject(komposeObject kobject.KomposeObject) (map[string]ServiceValues, map[string]PersistenceValues) { + serviceValues := make(map[string]ServiceValues) + + for serviceName, service := range komposeObject.ServiceConfigs { + svcValues := ServiceValues{} + + // Extract image + repo, tag := splitImage(service.Image) + svcValues.Image.Repository = repo + svcValues.Image.Tag = tag + + // Extract pull policy + pullPolicy := service.ImagePullPolicy + if pullPolicy == "" { + pullPolicy = "IfNotPresent" + } + svcValues.Image.PullPolicy = pullPolicy + + // Extract env vars + svcValues.Env = make(map[string]string) + for _, envVar := range service.Environment { + svcValues.Env[envVar.Name] = envVar.Value + } + + serviceValues[serviceName] = svcValues + } + + return serviceValues, nil +} + /** * Generate Helm Chart configuration */ -func generateHelm(dirName string) error { +func generateHelm(dirName string, serviceValues map[string]ServiceValues, persistenceValues map[string]PersistenceValues) error { type ChartDetails struct { Name string } @@ -141,10 +281,81 @@ home: return err } + /* Create the values.yaml file */ + if len(serviceValues) > 0 || len(persistenceValues) > 0 { + valuesYAML, err := generateValuesYAML(serviceValues, persistenceValues) + if err != nil { + return errors.Wrap(err, "Failed to generate values.yaml") + } + err = os.WriteFile(dirName+string(os.PathSeparator)+"values.yaml", valuesYAML, 0644) + if err != nil { + return err + } + } + log.Infof("chart created in %q\n", dirName+string(os.PathSeparator)) return nil } +// generateValuesYAML creates values.yaml content from extracted values +func generateValuesYAML(serviceValues map[string]ServiceValues, persistenceValues map[string]PersistenceValues) ([]byte, error) { + // Build hierarchical structure: serviceName -> image -> {repository, tag, pullPolicy} + valuesMap := make(map[string]interface{}) + + // Sort service names for consistent output + serviceNames := make([]string, 0, len(serviceValues)) + for name := range serviceValues { + serviceNames = append(serviceNames, name) + } + sort.Strings(serviceNames) + + for _, serviceName := range serviceNames { + svcValues := serviceValues[serviceName] + serviceMap := map[string]interface{}{ + "image": map[string]string{ + "repository": svcValues.Image.Repository, + "tag": svcValues.Image.Tag, + "pullPolicy": svcValues.Image.PullPolicy, + }, + } + + // Add env vars if present + if len(svcValues.Env) > 0 { + serviceMap["env"] = svcValues.Env + } + + valuesMap[serviceName] = serviceMap + } + + // Add persistence values if present + if len(persistenceValues) > 0 { + // Sort PVC names for consistent output + pvcNames := make([]string, 0, len(persistenceValues)) + for name := range persistenceValues { + pvcNames = append(pvcNames, name) + } + sort.Strings(pvcNames) + + persistenceMap := make(map[string]interface{}) + for _, pvcName := range pvcNames { + pvcValues := persistenceValues[pvcName] + persistenceMap[pvcName] = map[string]string{ + "size": pvcValues.Size, + } + } + + valuesMap["persistence"] = persistenceMap + } + + // Use marshalWithIndent for consistent 2-space indentation + yamlBytes, err := marshalWithIndent(valuesMap, 2) + if err != nil { + return nil, err + } + + return yamlBytes, nil +} + // Check if given path is a directory func isDir(name string) (bool, error) { // Open file to get stat later @@ -183,7 +394,7 @@ func getDirName(opt kobject.ConvertOptions) string { } // PrintList will take the data converted and decide on the commandline attributes given -func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { +func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObject kobject.KomposeObject, k8sTransformer *Kubernetes) error { var f *os.File dirName := getDirName(opt) log.Debugf("Target Dir: %s", dirName) @@ -215,6 +426,21 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { } var files []string + var serviceValues map[string]ServiceValues + var persistenceValues map[string]PersistenceValues + + // Extract values from KomposeObject for values.yaml + if opt.CreateChart { + serviceValues, _ = extractValuesFromKomposeObject(komposeObject) + // Get persistence values from transformer (populated during PVC creation) + if k8sTransformer != nil && k8sTransformer.PersistenceValues != nil { + persistenceValues = make(map[string]PersistenceValues) + for name, size := range k8sTransformer.PersistenceValues { + persistenceValues[name] = PersistenceValues{Size: size} + } + } + } + // if asked to print to stdout or to put in single file // we will create a list if opt.ToStdout || f != nil { @@ -232,6 +458,13 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { if err != nil { return fmt.Errorf("error in marshalling the List: %v", err) } + + // Unquote Helm templates if generating chart + if opt.CreateChart { + data = unquoteHelmTemplates(data) + data = templatePVCStorage(data, persistenceValues) + } + // this part add --- which unifies the file data = []byte(fmt.Sprintf("---\n%s", data)) printVal, err := transformer.Print("", dirName, "", data, opt.ToStdout, opt.GenerateJSON, f, opt.Provider) @@ -265,6 +498,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { var typeMeta metav1.TypeMeta var objectMeta metav1.ObjectMeta + // Get object metadata first for templating if us, ok := v.(*unstructured.Unstructured); ok { typeMeta = metav1.TypeMeta{ Kind: us.GetKind(), @@ -275,15 +509,16 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { } } else { val := reflect.ValueOf(v).Elem() - // Use reflect to access TypeMeta struct inside runtime.Object. - // cast it to correct type - metav1.TypeMeta typeMeta = val.FieldByName("TypeMeta").Interface().(metav1.TypeMeta) - - // Use reflect to access ObjectMeta struct inside runtime.Object. - // cast it to correct type - api.ObjectMeta objectMeta = val.FieldByName("ObjectMeta").Interface().(metav1.ObjectMeta) } + // Unquote Helm templates if generating chart + if opt.CreateChart { + data = unquoteHelmTemplates(data) + data = templatePVCStorage(data, persistenceValues) + } + file, err = transformer.Print(objectMeta.Name, finalDirName, strings.ToLower(typeMeta.Kind), data, opt.ToStdout, opt.GenerateJSON, f, opt.Provider) if err != nil { return errors.Wrap(err, "transformer.Print failed") @@ -293,7 +528,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { } } if opt.CreateChart { - err = generateHelm(dirName) + err = generateHelm(dirName, serviceValues, persistenceValues) if err != nil { return errors.Wrap(err, "generateHelm failed") } @@ -546,7 +781,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 { @@ -554,7 +789,7 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic } // Configure the container volumes. - volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(name, service) + volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(name, service, opt) if err != nil { return errors.Wrap(err, "k.ConfigVolumes failed") } @@ -586,6 +821,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, opt) + 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) @@ -722,6 +1052,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 66b44b4b..baa7e9c7 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -55,13 +55,26 @@ import ( type Kubernetes struct { // the user provided options from the command line Opt kobject.ConvertOptions + // PersistenceValues stores PVC sizes for helm chart values.yaml + PersistenceValues map[string]string +} + +// helmValuesPath generates a Helm template path using index notation +// This works for all service names including those with hyphens +func helmValuesPath(parts ...string) string { + quotedParts := make([]string, len(parts)+1) + quotedParts[0] = ".Values" + for i, part := range parts { + quotedParts[i+1] = `"` + part + `"` + } + return "index " + strings.Join(quotedParts, " ") } // PVCRequestSize (Persistent Volume Claim) has default size 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 @@ -70,6 +83,8 @@ const ( DaemonSetController = "daemonset" // StatefulStateController is controller type for StatefulSet StatefulStateController = "statefulset" + // JobController is controller type for Job + JobController = "job" ) // CheckUnsupportedKey checks if given komposeObject contains @@ -109,10 +124,17 @@ func (k *Kubernetes) CheckUnsupportedKey(komposeObject *kobject.KomposeObject, u } // InitPodSpec creates the pod specification -func (k *Kubernetes) InitPodSpec(name string, image string, pullSecret string) api.PodSpec { +func (k *Kubernetes) InitPodSpec(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) api.PodSpec { + image := service.Image if image == "" { image = name } + + // Inject Helm template for chart generation + if opt.CreateChart { + image = "{{ " + helmValuesPath(name, "image", "repository") + " }}:{{ " + helmValuesPath(name, "image", "tag") + " }}" + } + pod := api.PodSpec{ Containers: []api.Container{ { @@ -121,10 +143,11 @@ func (k *Kubernetes) InitPodSpec(name string, image string, pullSecret string) a }, }, } - if pullSecret != "" { + + if service.ImagePullSecret != "" { pod.ImagePullSecrets = []api.LocalObjectReference{ { - Name: pullSecret, + Name: service.ImagePullSecret, }, } } @@ -132,7 +155,7 @@ func (k *Kubernetes) InitPodSpec(name string, image string, pullSecret string) a } // InitPodSpecWithConfigMap creates the pod specification -func (k *Kubernetes) InitPodSpecWithConfigMap(name string, image string, service kobject.ServiceConfig) api.PodSpec { +func (k *Kubernetes) InitPodSpecWithConfigMap(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) api.PodSpec { var volumeMounts []api.VolumeMount var volumes []api.Volume @@ -177,6 +200,16 @@ func (k *Kubernetes) InitPodSpecWithConfigMap(name string, image string, service volumes = append(volumes, cmVol) } + image := service.Image + if image == "" { + image = name + } + + // Inject Helm template for chart generation + if opt.CreateChart { + image = "{{ " + helmValuesPath(name, "image", "repository") + " }}:{{ " + helmValuesPath(name, "image", "tag") + " }}" + } + pod := api.PodSpec{ Containers: []api.Container{ { @@ -329,6 +362,10 @@ func (k *Kubernetes) IntiConfigMapFromFileOrDir(name, cmName, filePath string, s configMap.Annotations = map[string]string{ "use-subpath": "true", } + // Check if the file is executable and store that info + if mode&0111 != 0 { // Check if any execute bit is set + configMap.Annotations["executable"] = "true" + } } return configMap, nil @@ -412,12 +449,12 @@ func (k *Kubernetes) InitConfigMapFromFile(name string, service kobject.ServiceC } // InitD initializes Kubernetes Deployment object -func (k *Kubernetes) InitD(name string, service kobject.ServiceConfig, replicas int) *appsv1.Deployment { +func (k *Kubernetes) InitD(name string, service kobject.ServiceConfig, replicas int, opt kobject.ConvertOptions) *appsv1.Deployment { var podSpec api.PodSpec if len(service.Configs) > 0 { - podSpec = k.InitPodSpecWithConfigMap(name, service.Image, service) + podSpec = k.InitPodSpecWithConfigMap(name, service, opt) } else { - podSpec = k.InitPodSpec(name, service.Image, service.ImagePullSecret) + podSpec = k.InitPodSpec(name, service, opt) } rp := int32(replicas) @@ -468,7 +505,7 @@ func (k *Kubernetes) InitD(name string, service kobject.ServiceConfig, replicas } // InitDS initializes Kubernetes DaemonSet object -func (k *Kubernetes) InitDS(name string, service kobject.ServiceConfig) *appsv1.DaemonSet { +func (k *Kubernetes) InitDS(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) *appsv1.DaemonSet { ds := &appsv1.DaemonSet{ TypeMeta: metav1.TypeMeta{ Kind: "DaemonSet", @@ -483,20 +520,66 @@ func (k *Kubernetes) InitDS(name string, service kobject.ServiceConfig) *appsv1. MatchLabels: transformer.ConfigLabels(name), }, Template: api.PodTemplateSpec{ - Spec: k.InitPodSpec(name, service.Image, service.ImagePullSecret), + Spec: k.InitPodSpec(name, service, opt), }, }, } return ds } -// InitSS method initialize a stateful set -func (k *Kubernetes) InitSS(name string, service kobject.ServiceConfig, replicas int) *appsv1.StatefulSet { +// InitJob initializes Kubernetes Job object +func (k *Kubernetes) InitJob(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) *batchv1.Job { var podSpec api.PodSpec if len(service.Configs) > 0 { - podSpec = k.InitPodSpecWithConfigMap(name, service.Image, service) + podSpec = k.InitPodSpecWithConfigMap(name, service, opt) } else { - podSpec = k.InitPodSpec(name, service.Image, service.ImagePullSecret) + podSpec = k.InitPodSpec(name, service, opt) + } + + // Jobs need RestartPolicy set (default is Always which is invalid for Jobs) + // Use Never for jobs unless explicitly set + if podSpec.RestartPolicy == "" || podSpec.RestartPolicy == api.RestartPolicyAlways { + podSpec.RestartPolicy = api.RestartPolicyNever + } + + job := &batchv1.Job{ + TypeMeta: metav1.TypeMeta{ + Kind: "Job", + APIVersion: "batch/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: transformer.ConfigAllLabels(name, &service), + }, + Spec: batchv1.JobSpec{ + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: transformer.ConfigLabels(name), + }, + Spec: podSpec, + }, + }, + } + + // Set backoffLimit from label if specified + if backoffLimit, ok := service.Labels["kompose.job.backoff-limit"]; ok { + limit, err := strconv.ParseInt(backoffLimit, 10, 32) + if err == nil { + limit32 := int32(limit) + job.Spec.BackoffLimit = &limit32 + } + } + + return job +} + +// InitSS method initialize a stateful set +func (k *Kubernetes) InitSS(name string, service kobject.ServiceConfig, replicas int, opt kobject.ConvertOptions) *appsv1.StatefulSet { + var podSpec api.PodSpec + if len(service.Configs) > 0 { + podSpec = k.InitPodSpecWithConfigMap(name, service, opt) + } else { + podSpec = k.InitPodSpec(name, service, opt) } rp := int32(replicas) ds := &appsv1.StatefulSet{ @@ -523,7 +606,7 @@ func (k *Kubernetes) InitSS(name string, service kobject.ServiceConfig, replicas } // InitCJ initializes Kubernetes CronJob object -func (k *Kubernetes) InitCJ(name string, service kobject.ServiceConfig, schedule string, concurrencyPolicy batchv1.ConcurrencyPolicy, backoffLimit *int32) *batchv1.CronJob { +func (k *Kubernetes) InitCJ(name string, service kobject.ServiceConfig, schedule string, concurrencyPolicy batchv1.ConcurrencyPolicy, backoffLimit *int32, opt kobject.ConvertOptions) *batchv1.CronJob { cj := &batchv1.CronJob{ TypeMeta: metav1.TypeMeta{ Kind: "CronJob", @@ -540,7 +623,7 @@ func (k *Kubernetes) InitCJ(name string, service kobject.ServiceConfig, schedule Spec: batchv1.JobSpec{ BackoffLimit: backoffLimit, Template: api.PodTemplateSpec{ - Spec: k.InitPodSpec(name, service.Image, service.ImagePullSecret), + Spec: k.InitPodSpec(name, service, opt), }, }, }, @@ -655,8 +738,12 @@ func (k *Kubernetes) CreateSecrets(komposeObject kobject.KomposeObject) ([]*api. } // CreatePVC initializes PersistentVolumeClaim -func (k *Kubernetes) CreatePVC(name string, mode string, size string, selectorValue string, storageClassName string) (*api.PersistentVolumeClaim, error) { - volSize, err := resource.ParseQuantity(size) +func (k *Kubernetes) CreatePVC(name string, mode string, size string, selectorValue string, storageClassName string, annotations map[string]string, opt kobject.ConvertOptions) (*api.PersistentVolumeClaim, error) { + var volSize resource.Quantity + var err error + + // Parse the size value + volSize, err = resource.ParseQuantity(size) if err != nil { return nil, errors.Wrap(err, "resource.ParseQuantity failed, Error parsing size") } @@ -679,6 +766,16 @@ func (k *Kubernetes) CreatePVC(name string, mode string, size string, selectorVa }, } + // Add annotations if specified + if len(annotations) > 0 { + if pvc.ObjectMeta.Annotations == nil { + pvc.ObjectMeta.Annotations = make(map[string]string) + } + for key, value := range annotations { + pvc.ObjectMeta.Annotations[key] = value + } + } + if len(selectorValue) > 0 { pvc.Spec.Selector = &metav1.LabelSelector{ MatchLabels: transformer.ConfigLabels(selectorValue), @@ -967,7 +1064,11 @@ func (k *Kubernetes) getSecretPathsLegacy(secretConfig types.ServiceSecretConfig } // ConfigVolumes configure the container volumes. -func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ([]api.VolumeMount, []api.Volume, []*api.PersistentVolumeClaim, []*api.ConfigMap, error) { +func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) ([]api.VolumeMount, []api.Volume, []*api.PersistentVolumeClaim, []*api.ConfigMap, error) { + // Store PVC info for helm chart values + if opt.CreateChart && k.PersistenceValues == nil { + k.PersistenceValues = make(map[string]string) + } volumeMounts := []api.VolumeMount{} volumes := []api.Volume{} var PVCs []*api.PersistentVolumeClaim @@ -989,13 +1090,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 @@ -1009,23 +1112,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 @@ -1047,15 +1179,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 { @@ -1067,6 +1199,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 == "" { @@ -1087,12 +1238,17 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( } } - createdPVC, err := k.CreatePVC(volumeName, volume.Mode, defaultSize, volume.SelectorValue, storageClassName) + createdPVC, err := k.CreatePVC(volumeName, volume.Mode, defaultSize, volume.SelectorValue, storageClassName, volume.Annotations, opt) if err != nil { return nil, nil, nil, nil, errors.Wrap(err, "k.CreatePVC failed") } + // Store PVC size for helm chart values + if opt.CreateChart { + k.PersistenceValues[volumeName] = defaultSize + } + PVCs = append(PVCs, createdPVC) } } @@ -1136,6 +1292,13 @@ func (k *Kubernetes) ConfigEmptyVolumeSource(key string) *api.VolumeSource { func (k *Kubernetes) ConfigConfigMapVolumeSource(cmName string, targetPath string, cm *api.ConfigMap) *api.VolumeSource { s := api.ConfigMapVolumeSource{} s.Name = cmName + + // Set default mode to 0755 if the file is executable + if cm.Annotations != nil && cm.Annotations["executable"] == "true" { + mode := int32(0755) + s.DefaultMode = &mode + } + if useSubPathMount(cm) { var keys []string for k := range cm.Data { @@ -1225,12 +1388,18 @@ func ConfigEnvs(service kobject.ServiceConfig, opt kobject.ConvertOptions) ([]ap // Load up the environment variables for _, v := range service.Environment { if !keysFromEnvFile[v.Name] { - if strings.Contains(v.Value, "run/secrets") { - v.Value = FormatResourceName(v.Value) + value := v.Value + if opt.CreateChart { + // Inject Helm template syntax for chart generation + value = "{{ " + helmValuesPath(service.Name, "env", v.Name) + " | quote }}" + } else { + if strings.Contains(value, "run/secrets") { + value = FormatResourceName(value) + } } envs = append(envs, api.EnvVar{ Name: v.Name, - Value: v.Value, + Value: value, }) } } @@ -1347,15 +1516,19 @@ func (k *Kubernetes) CreateWorkloadAndConfigMapObjects(name string, service kobj } if opt.CreateD || opt.Controller == DeploymentController { - objects = append(objects, k.InitD(name, service, replica)) + objects = append(objects, k.InitD(name, service, replica, opt)) } if opt.CreateDS || opt.Controller == DaemonSetController { - objects = append(objects, k.InitDS(name, service)) + objects = append(objects, k.InitDS(name, service, opt)) } if opt.Controller == StatefulStateController { - objects = append(objects, k.InitSS(name, service, replica)) + objects = append(objects, k.InitSS(name, service, replica, opt)) + } + + if opt.Controller == JobController { + objects = append(objects, k.InitJob(name, service, opt)) } envConfigMaps := k.PargeEnvFiletoConfigMaps(name, service, opt) @@ -1392,7 +1565,7 @@ func (k *Kubernetes) createConfigMapFromComposeConfig(name string, service kobje } // InitPod initializes Kubernetes Pod object -func (k *Kubernetes) InitPod(name string, service kobject.ServiceConfig) *api.Pod { +func (k *Kubernetes) InitPod(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) *api.Pod { pod := api.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", @@ -1403,7 +1576,7 @@ func (k *Kubernetes) InitPod(name string, service kobject.ServiceConfig) *api.Po Labels: transformer.ConfigLabels(name), Annotations: transformer.ConfigAnnotations(service), }, - Spec: k.InitPodSpec(name, service.Image, service.ImagePullSecret), + Spec: k.InitPodSpec(name, service, opt), } return &pod } @@ -1613,7 +1786,7 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. k.configKubeServiceAndIngressForService(service, groupName, &objects) // Configure the container volumes. - volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(groupName, service) + volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(groupName, service, opt) if err != nil { return nil, errors.Wrap(err, "k.ConfigVolumes failed") } @@ -1671,6 +1844,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] @@ -1680,6 +1869,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 @@ -1689,13 +1884,16 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. } // Generate pod or cronjob and configmap objects - if (service.Restart == "no" || service.Restart == "on-failure") && !opt.IsPodController() { + // Check if service has explicit controller type label + _, hasControllerLabel := service.Labels[compose.LabelControllerType] + + if (service.Restart == "no" || service.Restart == "on-failure") && !opt.IsPodController() && !hasControllerLabel { if service.CronJobSchedule != "" { log.Infof("Create kubernetes pod instead of pod controller due to restart policy: %s", service.Restart) - cronJob := k.InitCJ(name, service, service.CronJobSchedule, service.CronJobConcurrencyPolicy, service.CronJobBackoffLimit) + cronJob := k.InitCJ(name, service, service.CronJobSchedule, service.CronJobConcurrencyPolicy, service.CronJobBackoffLimit, opt) objects = append(objects, cronJob) } else { - pod := k.InitPod(name, service) + pod := k.InitPod(name, service, opt) objects = append(objects, pod) } envConfigMaps := k.PargeEnvFiletoConfigMaps(name, service, opt) @@ -1707,7 +1905,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") } @@ -1763,6 +1961,12 @@ func (k *Kubernetes) UpdateController(obj runtime.Object, updateTemplate func(*a return errors.Wrap(err, "updateTemplate failed") } updateMeta(&t.ObjectMeta) + case *batchv1.Job: + err = updateTemplate(&t.Spec.Template) + if err != nil { + return errors.Wrap(err, "updateTemplate failed") + } + updateMeta(&t.ObjectMeta) case *deployapi.DeploymentConfig: err = updateTemplate(t.Spec.Template) if err != nil { diff --git a/pkg/transformer/kubernetes/kubernetes_test.go b/pkg/transformer/kubernetes/kubernetes_test.go index f2910821..e3d2d9ef 100644 --- a/pkg/transformer/kubernetes/kubernetes_test.go +++ b/pkg/transformer/kubernetes/kubernetes_test.go @@ -611,7 +611,9 @@ func TestRestartOnFailure(t *testing.T) { func TestInitPodSpec(t *testing.T) { name := "foo" k := Kubernetes{} - result := k.InitPodSpec(name, newServiceConfig().Image, "") + service := newServiceConfig() + opt := kobject.ConvertOptions{} + result := k.InitPodSpec(name, service, opt) if result.Containers[0].Name != "foo" && result.Containers[0].Image != "image" { t.Fatalf("Pod object not found") } @@ -953,7 +955,7 @@ func TestHealthCheckOnMultipleContainers(t *testing.T) { func TestCreatePVC(t *testing.T) { storageClassName := "custom-storage-class-name" k := Kubernetes{} - result, err := k.CreatePVC("", "", PVCRequestSize, "", storageClassName) + result, err := k.CreatePVC("", "", PVCRequestSize, "", storageClassName, nil, kobject.ConvertOptions{}) if err != nil { t.Error(errors.Wrap(err, "k.CreatePVC failed")) } diff --git a/pkg/transformer/openshift/openshift.go b/pkg/transformer/openshift/openshift.go index 2947de4e..2aa54762 100644 --- a/pkg/transformer/openshift/openshift.go +++ b/pkg/transformer/openshift/openshift.go @@ -151,7 +151,7 @@ func initBuildConfig(name string, service kobject.ServiceConfig, repo string, br } // initDeploymentConfig initializes OpenShifts DeploymentConfig object -func (o *OpenShift) initDeploymentConfig(name string, service kobject.ServiceConfig, replicas int) *deployapi.DeploymentConfig { +func (o *OpenShift) initDeploymentConfig(name string, service kobject.ServiceConfig, replicas int, opt kobject.ConvertOptions) *deployapi.DeploymentConfig { containerName := []string{name} // Properly add tags to the image name @@ -164,9 +164,14 @@ func (o *OpenShift) initDeploymentConfig(name string, service kobject.ServiceCon var podSpec corev1.PodSpec if len(service.Configs) > 0 { - podSpec = o.InitPodSpecWithConfigMap(name, " ", service) + podSpec = o.InitPodSpecWithConfigMap(name, service, opt) } else { - podSpec = o.InitPodSpec(name, " ", "") + podSpec = o.InitPodSpec(name, service, opt) + } + + // OpenShift: Set image to a space - actual image comes from ImageStream trigger + for i := range podSpec.Containers { + podSpec.Containers[i].Image = " " } dc := &deployapi.DeploymentConfig{ @@ -333,10 +338,10 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C } if service.CronJobSchedule != "" { - cronJob := o.InitCJ(name, service, service.CronJobSchedule, service.CronJobConcurrencyPolicy, service.CronJobBackoffLimit) + cronJob := o.InitCJ(name, service, service.CronJobSchedule, service.CronJobConcurrencyPolicy, service.CronJobBackoffLimit, opt) objects = append(objects, cronJob) } else { - pod := o.InitPod(name, service) + pod := o.InitPod(name, service, opt) objects = append(objects, pod) } @@ -346,7 +351,7 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C objects = o.CreateWorkloadAndConfigMapObjects(name, service, opt) if opt.CreateDeploymentConfig { - objects = append(objects, o.initDeploymentConfig(name, service, replica)) // OpenShift DeploymentConfigs + objects = append(objects, o.initDeploymentConfig(name, service, replica, opt)) // OpenShift DeploymentConfigs // create ImageStream after deployment (creating IS will trigger new deployment) objects = append(objects, o.initImageStream(name, service, opt)) } @@ -426,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 57813ebf..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)) - o.UpdateKubernetesObjects("foobar", serviceConfig, opt, &object) + object = append(object, o.initDeploymentConfig("foobar", serviceConfig, 3, opt)) + o.UpdateKubernetesObjects("foobar", serviceConfig, opt, &object, &komposeObject) for _, obj := range object { switch tobj := obj.(type) { @@ -95,7 +98,8 @@ func TestOpenShiftUpdateKubernetesObjects(t *testing.T) { func TestInitDeploymentConfig(t *testing.T) { o := OpenShift{} - spec := o.initDeploymentConfig("foobar", newServiceConfig(), 1) + opt := kobject.ConvertOptions{} + spec := o.initDeploymentConfig("foobar", newServiceConfig(), 1, opt) // Check that "foobar" is used correctly as a name if spec.Spec.Template.Spec.Containers[0].Name != "foobar" { diff --git a/pkg/version/version.go b/pkg/version/version.go index ad27e80f..72e080e5 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -2,7 +2,7 @@ package version var ( // VERSION is version number that will be displayed when running ./kompose version - VERSION = "1.37.0" + VERSION = "1.37.0-zenith-0.0.1" // GITCOMMIT is hash of the commit that will be displayed when running ./kompose version // this will be overwritten when running build like this: go build -ldflags="-X github.com/kubernetes/kompose/pkg/version.GITCOMMIT=$(GITCOMMIT)" // HEAD is default indicating that this was not set during build