From a5704fd69d2e2d8769d07d93309c1423482e8280 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:11:15 +0100 Subject: [PATCH 1/6] feat: add integration test for kubernetes first attempt --- cli/cmd/generate_docker.go | 21 +- cli/cmd/generate_docker_test.go | 6 +- int/export_kubernetes_test.go | 633 ++++++++++++++++++++++++++++++++ int/util/workspace.go | 39 ++ 4 files changed, 689 insertions(+), 10 deletions(-) create mode 100644 int/export_kubernetes_test.go diff --git a/cli/cmd/generate_docker.go b/cli/cmd/generate_docker.go index ae164cd..0d71684 100644 --- a/cli/cmd/generate_docker.go +++ b/cli/cmd/generate_docker.go @@ -30,15 +30,15 @@ type GenerateDockerOpts struct { func (c *GenerateDockerCmd) RunE(cc *cobra.Command, args []string) error { log.Println(c.Opts.Force) fs := cs.NewOSFileSystem(".") - git := git.NewGitService(fs) + gitSvc := git.NewGitService(fs) - client, err := NewClient(c.Opts.GlobalOptions) - if err != nil { - return fmt.Errorf("failed to create Codesphere client: %w", err) + exporter := exporter.NewExporterService(fs, c.Opts.Output, c.Opts.BaseImage, c.Opts.Envs, c.Opts.RepoRoot, c.Opts.Force) + + clientFactory := func() (Client, error) { + return NewClient(c.Opts.GlobalOptions) } - exporter := exporter.NewExporterService(fs, c.Opts.Output, c.Opts.BaseImage, c.Opts.Envs, c.Opts.RepoRoot, c.Opts.Force) - if err := c.GenerateDocker(fs, exporter, git, client); err != nil { + if err := c.GenerateDocker(fs, exporter, gitSvc, clientFactory); err != nil { return fmt.Errorf("failed to generate docker: %w", err) } @@ -95,7 +95,7 @@ func AddGenerateDockerCmd(generate *cobra.Command, opts *GenerateOpts) { docker.cmd.RunE = docker.RunE } -func (c *GenerateDockerCmd) GenerateDocker(fs *cs.FileSystem, exp exporter.Exporter, git git.Git, csClient Client) error { +func (c *GenerateDockerCmd) GenerateDocker(fs *cs.FileSystem, exp exporter.Exporter, git git.Git, clientFactory func() (Client, error)) error { if c.Opts.BaseImage == "" { return errors.New("baseimage is required") } @@ -104,7 +104,12 @@ func (c *GenerateDockerCmd) GenerateDocker(fs *cs.FileSystem, exp exporter.Expor if !fs.FileExists(ciInput) { log.Printf("Input file %s not found attempting to clone workspace repository...\n", c.Opts.Input) - if err := c.CloneRepository(csClient, fs, git, c.Opts.RepoRoot); err != nil { + client, err := clientFactory() + if err != nil { + return fmt.Errorf("failed to create Codesphere client: %w", err) + } + + if err := c.CloneRepository(client, fs, git, c.Opts.RepoRoot); err != nil { return fmt.Errorf("failed to clone repository: %w", err) } if !fs.FileExists(ciInput) { diff --git a/cli/cmd/generate_docker_test.go b/cli/cmd/generate_docker_test.go index e4401bc..8d2ad43 100644 --- a/cli/cmd/generate_docker_test.go +++ b/cli/cmd/generate_docker_test.go @@ -55,7 +55,8 @@ var _ = Describe("GenerateDocker", func() { Context("the baseimage is not provided", func() { It("should return an error", func() { - err := c.GenerateDocker(memoryFs, mockExporter, mockGit, mockClient) + clientFactory := func() (cmd.Client, error) { return mockClient, nil } + err := c.GenerateDocker(memoryFs, mockExporter, mockGit, clientFactory) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("baseimage is required")) }) @@ -78,7 +79,8 @@ var _ = Describe("GenerateDocker", func() { It("should not return an error", func() { mockExporter.EXPECT().ReadYmlFile(ciYmlPath).Return(&ci.CiYml{}, nil) mockExporter.EXPECT().ExportDockerArtifacts().Return(nil) - err := c.GenerateDocker(memoryFs, mockExporter, mockGit, mockClient) + clientFactory := func() (cmd.Client, error) { return mockClient, nil } + err := c.GenerateDocker(memoryFs, mockExporter, mockGit, clientFactory) Expect(err).To(Not(HaveOccurred())) }) }) diff --git a/int/export_kubernetes_test.go b/int/export_kubernetes_test.go new file mode 100644 index 0000000..622f052 --- /dev/null +++ b/int/export_kubernetes_test.go @@ -0,0 +1,633 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Sample ci.yml for testing - simulates the flask-demo structure from the blog post +const flaskDemoCiYml = `schemaVersion: v0.2 +prepare: + steps: + - name: install dependencies + command: pip install -r requirements.txt +test: + steps: [] +run: + frontend-service: + steps: + - command: python app.py + plan: 21 + replicas: 1 + isPublic: true + network: + paths: + - port: 3000 + path: / + stripPath: false + ports: + - port: 3000 + isPublic: true + backend-service: + steps: + - command: python backend.py + plan: 21 + replicas: 1 + isPublic: true + network: + paths: + - port: 3000 + path: /api + stripPath: true + ports: + - port: 3000 + isPublic: true +` + +// Simple ci.yml with a single service +const simpleCiYml = `schemaVersion: v0.2 +prepare: + steps: + - name: install + command: npm install +test: + steps: [] +run: + web: + steps: + - command: npm start + plan: 21 + replicas: 1 + isPublic: true + network: + paths: + - port: 8080 + path: / + stripPath: false + ports: + - port: 8080 + isPublic: true +` + +// Legacy ci.yml format with path directly in network +const legacyCiYml = `schemaVersion: v0.2 +prepare: + steps: [] +test: + steps: [] +run: + app: + steps: + - command: ./start.sh + plan: 21 + replicas: 1 + isPublic: true + network: + path: / + stripPath: true +` + +var _ = Describe("Kubernetes Export Integration Tests", func() { + var ( + tempDir string + ) + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "cs-export-test-") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + if tempDir != "" { + Expect(os.RemoveAll(tempDir)).NotTo(HaveOccurred()) + } + }) + + Context("Generate Docker Command", func() { + It("should generate Dockerfiles and docker-compose from flask-demo ci.yml", func() { + By("Creating ci.yml in temp directory") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(flaskDemoCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker command") + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate docker output: %s\n", output) + + Expect(output).To(ContainSubstring("docker artifacts created")) + Expect(output).To(ContainSubstring("docker compose up")) + + By("Verifying frontend-service Dockerfile was created") + frontendDockerfile := filepath.Join(tempDir, "export", "frontend-service", "Dockerfile") + Expect(frontendDockerfile).To(BeAnExistingFile()) + content, err := os.ReadFile(frontendDockerfile) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("FROM ubuntu:latest")) + Expect(string(content)).To(ContainSubstring("pip install")) + + By("Verifying frontend-service entrypoint was created") + frontendEntrypoint := filepath.Join(tempDir, "export", "frontend-service", "entrypoint.sh") + Expect(frontendEntrypoint).To(BeAnExistingFile()) + content, err = os.ReadFile(frontendEntrypoint) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("python app.py")) + + By("Verifying backend-service Dockerfile was created") + backendDockerfile := filepath.Join(tempDir, "export", "backend-service", "Dockerfile") + Expect(backendDockerfile).To(BeAnExistingFile()) + + By("Verifying backend-service entrypoint was created") + backendEntrypoint := filepath.Join(tempDir, "export", "backend-service", "entrypoint.sh") + Expect(backendEntrypoint).To(BeAnExistingFile()) + content, err = os.ReadFile(backendEntrypoint) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("python backend.py")) + + By("Verifying docker-compose.yml was created") + dockerComposePath := filepath.Join(tempDir, "export", "docker-compose.yml") + Expect(dockerComposePath).To(BeAnExistingFile()) + content, err = os.ReadFile(dockerComposePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("frontend-service")) + Expect(string(content)).To(ContainSubstring("backend-service")) + + By("Verifying nginx config was created") + nginxConfigPath := filepath.Join(tempDir, "export", "nginx.conf") + Expect(nginxConfigPath).To(BeAnExistingFile()) + }) + + It("should generate Docker artifacts with different base image", func() { + By("Creating ci.yml in temp directory") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker with alpine base image") + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "alpine:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate docker output: %s\n", output) + + Expect(output).To(ContainSubstring("docker artifacts created")) + + By("Verifying Dockerfile uses alpine base image") + dockerfile := filepath.Join(tempDir, "export", "web", "Dockerfile") + Expect(dockerfile).To(BeAnExistingFile()) + content, err := os.ReadFile(dockerfile) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("FROM alpine:latest")) + }) + + It("should fail when baseimage is not provided", func() { + By("Creating ci.yml in temp directory") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker without baseimage") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "docker", + "--reporoot", tempDir, + "-i", "ci.yml", + ) + fmt.Printf("Generate docker without baseimage output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(ContainSubstring("baseimage is required")) + }) + + It("should fail when ci.yml does not exist", func() { + By("Running generate docker without ci.yml") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "nonexistent.yml", + ) + fmt.Printf("Generate docker with nonexistent file output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + }) + }) + + Context("Generate Kubernetes Command", func() { + BeforeEach(func() { + By("Creating ci.yml and generating docker artifacts first") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(flaskDemoCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + Expect(output).To(ContainSubstring("docker artifacts created")) + }) + + It("should generate Kubernetes artifacts with registry and namespace", func() { + By("Running generate kubernetes command") + output := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "ghcr.io/codesphere-cloud/flask-demo", + "-p", "cs-demo", + "-i", "ci.yml", + "-o", "export", + "-n", "flask-demo", + "--hostname", "flask-demo.local", + ) + fmt.Printf("Generate kubernetes output: %s\n", output) + + Expect(output).To(ContainSubstring("Kubernetes artifacts export successful")) + Expect(output).To(ContainSubstring("kubectl apply")) + + By("Verifying kubernetes directory was created") + kubernetesDir := filepath.Join(tempDir, "export", "kubernetes") + info, err := os.Stat(kubernetesDir) + Expect(err).NotTo(HaveOccurred()) + Expect(info.IsDir()).To(BeTrue()) + + By("Verifying frontend-service deployment was created") + frontendService := filepath.Join(kubernetesDir, "service-frontend-service.yml") + Expect(frontendService).To(BeAnExistingFile()) + content, err := os.ReadFile(frontendService) + Expect(err).NotTo(HaveOccurred()) + contentStr := string(content) + Expect(contentStr).To(ContainSubstring("kind: Deployment")) + Expect(contentStr).To(ContainSubstring("kind: Service")) + Expect(contentStr).To(ContainSubstring("namespace: flask-demo")) + Expect(contentStr).To(ContainSubstring("ghcr.io/codesphere-cloud/flask-demo/cs-demo-frontend-service:latest")) + + By("Verifying backend-service deployment was created") + backendService := filepath.Join(kubernetesDir, "service-backend-service.yml") + Expect(backendService).To(BeAnExistingFile()) + content, err = os.ReadFile(backendService) + Expect(err).NotTo(HaveOccurred()) + contentStr = string(content) + Expect(contentStr).To(ContainSubstring("kind: Deployment")) + Expect(contentStr).To(ContainSubstring("namespace: flask-demo")) + Expect(contentStr).To(ContainSubstring("cs-demo-backend-service:latest")) + + By("Verifying ingress was created") + ingressPath := filepath.Join(kubernetesDir, "ingress.yml") + Expect(ingressPath).To(BeAnExistingFile()) + content, err = os.ReadFile(ingressPath) + Expect(err).NotTo(HaveOccurred()) + contentStr = string(content) + Expect(contentStr).To(ContainSubstring("kind: Ingress")) + Expect(contentStr).To(ContainSubstring("namespace: flask-demo")) + Expect(contentStr).To(ContainSubstring("host: flask-demo.local")) + Expect(contentStr).To(ContainSubstring("ingressClassName: nginx")) + }) + + It("should generate Kubernetes artifacts with custom ingress class", func() { + By("Running generate kubernetes with custom ingress class") + output := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "docker.io/myorg", + "-i", "ci.yml", + "-o", "export", + "-n", "production", + "--hostname", "myapp.example.com", + "--ingressClass", "traefik", + ) + fmt.Printf("Generate kubernetes with traefik output: %s\n", output) + + Expect(output).To(ContainSubstring("Kubernetes artifacts export successful")) + + By("Verifying ingress uses traefik class") + ingressPath := filepath.Join(tempDir, "export", "kubernetes", "ingress.yml") + content, err := os.ReadFile(ingressPath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("ingressClassName: traefik")) + }) + + It("should generate Kubernetes artifacts with pull secret", func() { + By("Running generate kubernetes with pull secret") + output := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "private-registry.io/myorg", + "-i", "ci.yml", + "-o", "export", + "-n", "staging", + "--hostname", "staging.myapp.com", + "--pullsecret", "my-registry-secret", + ) + fmt.Printf("Generate kubernetes with pull secret output: %s\n", output) + + Expect(output).To(ContainSubstring("Kubernetes artifacts export successful")) + + By("Verifying deployment includes pull secret") + frontendService := filepath.Join(tempDir, "export", "kubernetes", "service-frontend-service.yml") + content, err := os.ReadFile(frontendService) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("my-registry-secret")) + }) + + It("should fail when registry is not provided", func() { + By("Running generate kubernetes without registry") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "kubernetes", + "--reporoot", tempDir, + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate kubernetes without registry output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(ContainSubstring("registry is required")) + }) + }) + + Context("Full Export Workflow", func() { + It("should complete the full export workflow from ci.yml to Kubernetes artifacts", func() { + By("Step 1: Creating ci.yml with multi-service application") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(flaskDemoCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Step 2: Generate Docker artifacts") + dockerOutput := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Docker generation output: %s\n", dockerOutput) + Expect(dockerOutput).To(ContainSubstring("docker artifacts created")) + + // Verify Docker artifacts + Expect(filepath.Join(tempDir, "export", "frontend-service", "Dockerfile")).To(BeAnExistingFile()) + Expect(filepath.Join(tempDir, "export", "backend-service", "Dockerfile")).To(BeAnExistingFile()) + Expect(filepath.Join(tempDir, "export", "docker-compose.yml")).To(BeAnExistingFile()) + + By("Step 3: Generate Kubernetes artifacts") + k8sOutput := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "ghcr.io/codesphere-cloud/flask-demo", + "-p", "cs-demo", + "-i", "ci.yml", + "-o", "export", + "-n", "flask-demo-ns", + "--hostname", "colima-cluster", + ) + fmt.Printf("Kubernetes generation output: %s\n", k8sOutput) + Expect(k8sOutput).To(ContainSubstring("Kubernetes artifacts export successful")) + + By("Step 4: Verify all expected files exist") + expectedFiles := []string{ + "export/frontend-service/Dockerfile", + "export/frontend-service/entrypoint.sh", + "export/backend-service/Dockerfile", + "export/backend-service/entrypoint.sh", + "export/docker-compose.yml", + "export/nginx.conf", + "export/Dockerfile.nginx", + "export/kubernetes/service-frontend-service.yml", + "export/kubernetes/service-backend-service.yml", + "export/kubernetes/ingress.yml", + } + + for _, file := range expectedFiles { + fullPath := filepath.Join(tempDir, file) + Expect(fullPath).To(BeAnExistingFile(), fmt.Sprintf("Expected file %s to exist", file)) + } + + By("Step 5: Verify Kubernetes manifests are valid YAML with correct content") + kubernetesDir := filepath.Join(tempDir, "export", "kubernetes") + + // Check ingress contains all services + ingressContent, err := os.ReadFile(filepath.Join(kubernetesDir, "ingress.yml")) + Expect(err).NotTo(HaveOccurred()) + ingressStr := string(ingressContent) + Expect(ingressStr).To(ContainSubstring("host: colima-cluster")) + Expect(ingressStr).To(ContainSubstring("frontend-service")) + Expect(ingressStr).To(ContainSubstring("backend-service")) + Expect(ingressStr).To(ContainSubstring("path: /")) + Expect(ingressStr).To(ContainSubstring("path: /api")) + + // Check frontend service has correct image + frontendContent, err := os.ReadFile(filepath.Join(kubernetesDir, "service-frontend-service.yml")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(frontendContent)).To(ContainSubstring("image: ghcr.io/codesphere-cloud/flask-demo/cs-demo-frontend-service:latest")) + + // Check backend service has correct image + backendContent, err := os.ReadFile(filepath.Join(kubernetesDir, "service-backend-service.yml")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(backendContent)).To(ContainSubstring("image: ghcr.io/codesphere-cloud/flask-demo/cs-demo-backend-service:latest")) + }) + + It("should handle different ci.yml profiles", func() { + By("Creating multiple ci.yml profiles") + // Dev profile + devCiYml := strings.Replace(simpleCiYml, "npm start", "npm run dev", 1) + err := os.WriteFile(filepath.Join(tempDir, "ci.dev.yml"), []byte(devCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + // Prod profile + prodCiYml := strings.Replace(simpleCiYml, "npm start", "npm run prod", 1) + err = os.WriteFile(filepath.Join(tempDir, "ci.prod.yml"), []byte(prodCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Generating Docker artifacts for dev profile") + devDockerOutput := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "node:18", + "-i", "ci.dev.yml", + "-o", "export-dev", + ) + Expect(devDockerOutput).To(ContainSubstring("docker artifacts created")) + + By("Generating Docker artifacts for prod profile") + prodDockerOutput := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "node:18-alpine", + "-i", "ci.prod.yml", + "-o", "export-prod", + ) + Expect(prodDockerOutput).To(ContainSubstring("docker artifacts created")) + + By("Verifying dev and prod have different configurations") + devEntrypoint, err := os.ReadFile(filepath.Join(tempDir, "export-dev", "web", "entrypoint.sh")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(devEntrypoint)).To(ContainSubstring("npm run dev")) + + prodEntrypoint, err := os.ReadFile(filepath.Join(tempDir, "export-prod", "web", "entrypoint.sh")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(prodEntrypoint)).To(ContainSubstring("npm run prod")) + + devDockerfile, err := os.ReadFile(filepath.Join(tempDir, "export-dev", "web", "Dockerfile")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(devDockerfile)).To(ContainSubstring("FROM node:18")) + + prodDockerfile, err := os.ReadFile(filepath.Join(tempDir, "export-prod", "web", "Dockerfile")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(prodDockerfile)).To(ContainSubstring("FROM node:18-alpine")) + }) + }) + + Context("Legacy ci.yml Format Support", func() { + It("should handle legacy ci.yml with path directly in network", func() { + By("Creating legacy format ci.yml") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(legacyCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Generating Docker artifacts") + dockerOutput := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Legacy Docker generation output: %s\n", dockerOutput) + Expect(dockerOutput).To(ContainSubstring("docker artifacts created")) + + By("Generating Kubernetes artifacts") + k8sOutput := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "docker.io/myorg", + "-i", "ci.yml", + "-o", "export", + "-n", "legacy-app", + "--hostname", "legacy.local", + ) + fmt.Printf("Legacy Kubernetes generation output: %s\n", k8sOutput) + Expect(k8sOutput).To(ContainSubstring("Kubernetes artifacts export successful")) + + By("Verifying artifacts were created correctly") + Expect(filepath.Join(tempDir, "export", "app", "Dockerfile")).To(BeAnExistingFile()) + Expect(filepath.Join(tempDir, "export", "kubernetes", "service-app.yml")).To(BeAnExistingFile()) + Expect(filepath.Join(tempDir, "export", "kubernetes", "ingress.yml")).To(BeAnExistingFile()) + }) + }) + + Context("Environment Variables in Docker Artifacts", func() { + It("should include environment variables in generated artifacts", func() { + By("Creating ci.yml") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Generating Docker artifacts with environment variables") + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "node:18", + "-i", "ci.yml", + "-o", "export", + "-e", "NODE_ENV=production", + "-e", "API_URL=https://api.example.com", + ) + fmt.Printf("Docker generation with envs output: %s\n", output) + Expect(output).To(ContainSubstring("docker artifacts created")) + + By("Verifying docker-compose contains environment variables") + dockerCompose, err := os.ReadFile(filepath.Join(tempDir, "export", "docker-compose.yml")) + Expect(err).NotTo(HaveOccurred()) + content := string(dockerCompose) + Expect(content).To(ContainSubstring("NODE_ENV")) + Expect(content).To(ContainSubstring("API_URL")) + }) + }) + + Context("Force Overwrite Behavior", func() { + It("should overwrite existing files when --force is specified", func() { + By("Creating ci.yml") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("First generation") + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + Expect(output).To(ContainSubstring("docker artifacts created")) + + By("Second generation with --force") + output = intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "alpine:latest", + "-i", "ci.yml", + "-o", "export", + "--force", + ) + Expect(output).To(ContainSubstring("docker artifacts created")) + + By("Verifying files were overwritten with new base image") + dockerfile, err := os.ReadFile(filepath.Join(tempDir, "export", "web", "Dockerfile")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(dockerfile)).To(ContainSubstring("FROM alpine:latest")) + }) + }) + + Context("Generate Command Help", func() { + It("should display help for generate docker command", func() { + output := intutil.RunCommand("generate", "docker", "--help") + fmt.Printf("Generate docker help: %s\n", output) + + Expect(output).To(ContainSubstring("generated artifacts")) + Expect(output).To(ContainSubstring("-b, --baseimage")) + Expect(output).To(ContainSubstring("-i, --input")) + Expect(output).To(ContainSubstring("-o, --output")) + }) + + It("should display help for generate kubernetes command", func() { + output := intutil.RunCommand("generate", "kubernetes", "--help") + fmt.Printf("Generate kubernetes help: %s\n", output) + + Expect(output).To(ContainSubstring("generated artifacts")) + Expect(output).To(ContainSubstring("-r, --registry")) + Expect(output).To(ContainSubstring("-p, --imagePrefix")) + Expect(output).To(ContainSubstring("-n, --namespace")) + Expect(output).To(ContainSubstring("--hostname")) + Expect(output).To(ContainSubstring("--pullsecret")) + Expect(output).To(ContainSubstring("--ingressClass")) + }) + + It("should display help for generate images command", func() { + output := intutil.RunCommand("generate", "images", "--help") + fmt.Printf("Generate images help: %s\n", output) + + Expect(output).To(ContainSubstring("generated images will be pushed")) + Expect(output).To(ContainSubstring("-r, --registry")) + Expect(output).To(ContainSubstring("-p, --imagePrefix")) + }) + }) +}) diff --git a/int/util/workspace.go b/int/util/workspace.go index c6269cc..ce43214 100644 --- a/int/util/workspace.go +++ b/int/util/workspace.go @@ -5,9 +5,11 @@ package util import ( "bytes" + "fmt" "log" "os" "os/exec" + "path/filepath" "regexp" "strings" "time" @@ -66,6 +68,43 @@ func RunCommandWithExitCode(args ...string) (string, int) { return outputBuffer.String(), exitCode } +// RunCommandInDir runs a command from a specific working directory. +// The cs binary path is resolved relative to the int/ directory. +func RunCommandInDir(dir string, args ...string) string { + output, _ := RunCommandInDirWithExitCode(dir, args...) + return output +} + +// RunCommandInDirWithExitCode runs a command from a specific working directory and returns exit code. +func RunCommandInDirWithExitCode(dir string, args ...string) (string, int) { + // Get absolute path to cs binary (relative to int/ directory) + csBinary, err := filepath.Abs("../cs") + if err != nil { + return fmt.Sprintf("failed to get cs binary path: %v", err), -1 + } + + command := exec.Command(csBinary, args...) + command.Dir = dir + command.Env = os.Environ() + + var outputBuffer bytes.Buffer + command.Stdout = &outputBuffer + command.Stderr = &outputBuffer + + err = command.Run() + + exitCode := 0 + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + exitCode = exitError.ExitCode() + } else { + exitCode = -1 + } + } + + return outputBuffer.String(), exitCode +} + func ExtractWorkspaceId(output string) string { re := regexp.MustCompile(`ID:\s*(\d+)`) matches := re.FindStringSubmatch(output) From 955167b86634d5f6f227785ddc164f820e9af6cf Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:11:56 +0100 Subject: [PATCH 2/6] fix: merge error --- cli/cmd/generate_docker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cmd/generate_docker.go b/cli/cmd/generate_docker.go index 0d71684..7857ad5 100644 --- a/cli/cmd/generate_docker.go +++ b/cli/cmd/generate_docker.go @@ -35,7 +35,7 @@ func (c *GenerateDockerCmd) RunE(cc *cobra.Command, args []string) error { exporter := exporter.NewExporterService(fs, c.Opts.Output, c.Opts.BaseImage, c.Opts.Envs, c.Opts.RepoRoot, c.Opts.Force) clientFactory := func() (Client, error) { - return NewClient(c.Opts.GlobalOptions) + return NewClient(*c.Opts.GlobalOptions) } if err := c.GenerateDocker(fs, exporter, gitSvc, clientFactory); err != nil { From 3f57bdc8f1aa9203f3311883454f70293d3e169b Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:13:39 +0000 Subject: [PATCH 3/6] chore(docs): Auto-update docs and licenses Signed-off-by: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> --- NOTICE | 12 ++++++------ pkg/tmpl/NOTICE | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/NOTICE b/NOTICE index c861e91..16e5f1e 100644 --- a/NOTICE +++ b/NOTICE @@ -5,9 +5,9 @@ This project includes code licensed under the following terms: ---------- Module: code.gitea.io/sdk/gitea -Version: v0.22.1 +Version: v0.23.2 License: MIT -License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.22.1/gitea/LICENSE +License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.23.2/gitea/LICENSE ---------- Module: dario.cat/mergo @@ -365,9 +365,9 @@ License URL: https://github.com/xanzy/ssh-agent/blob/v0.3.3/LICENSE ---------- Module: gitlab.com/gitlab-org/api/client-go -Version: v1.11.0 +Version: v1.39.0 License: Apache-2.0 -License URL: https://gitlab.com/gitlab-org/api/blob/client-go/v1.11.0/client-go/LICENSE +License URL: https://gitlab.com/gitlab-org/api/blob/client-go/v1.39.0/client-go/LICENSE ---------- Module: go.yaml.in/yaml/v2 @@ -401,9 +401,9 @@ License URL: https://cs.opensource.google/go/x/net/+/v0.50.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.34.0 +Version: v0.35.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.34.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.35.0:LICENSE ---------- Module: golang.org/x/sync/errgroup diff --git a/pkg/tmpl/NOTICE b/pkg/tmpl/NOTICE index c861e91..16e5f1e 100644 --- a/pkg/tmpl/NOTICE +++ b/pkg/tmpl/NOTICE @@ -5,9 +5,9 @@ This project includes code licensed under the following terms: ---------- Module: code.gitea.io/sdk/gitea -Version: v0.22.1 +Version: v0.23.2 License: MIT -License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.22.1/gitea/LICENSE +License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.23.2/gitea/LICENSE ---------- Module: dario.cat/mergo @@ -365,9 +365,9 @@ License URL: https://github.com/xanzy/ssh-agent/blob/v0.3.3/LICENSE ---------- Module: gitlab.com/gitlab-org/api/client-go -Version: v1.11.0 +Version: v1.39.0 License: Apache-2.0 -License URL: https://gitlab.com/gitlab-org/api/blob/client-go/v1.11.0/client-go/LICENSE +License URL: https://gitlab.com/gitlab-org/api/blob/client-go/v1.39.0/client-go/LICENSE ---------- Module: go.yaml.in/yaml/v2 @@ -401,9 +401,9 @@ License URL: https://cs.opensource.google/go/x/net/+/v0.50.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.34.0 +Version: v0.35.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.34.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.35.0:LICENSE ---------- Module: golang.org/x/sync/errgroup From 09c2f2337dbd14c409b40c3e453089f716edfdbe Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:37:44 +0100 Subject: [PATCH 4/6] ref: streamline file path handling in Docker and Kubernetes commands --- cli/cmd/generate_docker.go | 6 ++---- cli/cmd/generate_images.go | 5 ++--- cli/cmd/generate_images_test.go | 4 +--- cli/cmd/generate_kubernetes.go | 4 ++-- cli/cmd/generate_kubernetes_test.go | 6 +----- int/export_kubernetes_test.go | 32 +++++++++++++++++++++++++++++ pkg/exporter/exporter.go | 10 ++------- pkg/exporter/exporter_test.go | 16 +++++++-------- 8 files changed, 50 insertions(+), 33 deletions(-) diff --git a/cli/cmd/generate_docker.go b/cli/cmd/generate_docker.go index 7857ad5..9ce62bb 100644 --- a/cli/cmd/generate_docker.go +++ b/cli/cmd/generate_docker.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "log" - "path" "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/exporter" @@ -28,8 +27,7 @@ type GenerateDockerOpts struct { } func (c *GenerateDockerCmd) RunE(cc *cobra.Command, args []string) error { - log.Println(c.Opts.Force) - fs := cs.NewOSFileSystem(".") + fs := cs.NewOSFileSystem(c.Opts.RepoRoot) gitSvc := git.NewGitService(fs) exporter := exporter.NewExporterService(fs, c.Opts.Output, c.Opts.BaseImage, c.Opts.Envs, c.Opts.RepoRoot, c.Opts.Force) @@ -100,7 +98,7 @@ func (c *GenerateDockerCmd) GenerateDocker(fs *cs.FileSystem, exp exporter.Expor return errors.New("baseimage is required") } - ciInput := path.Join(c.Opts.RepoRoot, c.Opts.Input) + ciInput := c.Opts.Input if !fs.FileExists(ciInput) { log.Printf("Input file %s not found attempting to clone workspace repository...\n", c.Opts.Input) diff --git a/cli/cmd/generate_images.go b/cli/cmd/generate_images.go index 2df7b8b..7c8c6e5 100644 --- a/cli/cmd/generate_images.go +++ b/cli/cmd/generate_images.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "log" - "path" "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/exporter" @@ -28,7 +27,7 @@ type GenerateImagesOpts struct { } func (c *GenerateImagesCmd) RunE(_ *cobra.Command, args []string) error { - fs := cs.NewOSFileSystem(".") + fs := cs.NewOSFileSystem(c.Opts.RepoRoot) exporter := exporter.NewExporterService(fs, c.Opts.Output, "", []string{}, c.Opts.RepoRoot, c.Opts.Force) if err := c.GenerateImages(fs, exporter); err != nil { @@ -69,7 +68,7 @@ func AddGenerateImagesCmd(generate *cobra.Command, opts *GenerateOpts) { } func (c *GenerateImagesCmd) GenerateImages(fs *cs.FileSystem, exp exporter.Exporter) error { - ciInput := path.Join(c.Opts.RepoRoot, c.Opts.Input) + ciInput := c.Opts.Input if c.Opts.Registry == "" { return errors.New("registry is required") } diff --git a/cli/cmd/generate_images_test.go b/cli/cmd/generate_images_test.go index 577c2ef..01977fd 100644 --- a/cli/cmd/generate_images_test.go +++ b/cli/cmd/generate_images_test.go @@ -5,7 +5,6 @@ package cmd_test import ( "context" - "path" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -75,8 +74,7 @@ var _ = Describe("GenerateImages", func() { Expect(err).To(Not(HaveOccurred())) }) It("should not return an error", func() { - ciYmlPath := path.Join(c.Opts.RepoRoot, "ci.dev.yml") - mockExporter.EXPECT().ReadYmlFile(ciYmlPath).Return(&ci.CiYml{}, nil) + mockExporter.EXPECT().ReadYmlFile("ci.dev.yml").Return(&ci.CiYml{}, nil) mockExporter.EXPECT().ExportImages(context.Background(), "my-registry.com", "").Return(nil) err := c.GenerateImages(memoryFs, mockExporter) Expect(err).To(Not(HaveOccurred())) diff --git a/cli/cmd/generate_kubernetes.go b/cli/cmd/generate_kubernetes.go index c8d68a5..87f66c7 100644 --- a/cli/cmd/generate_kubernetes.go +++ b/cli/cmd/generate_kubernetes.go @@ -31,7 +31,7 @@ type GenerateKubernetesOpts struct { } func (c *GenerateKubernetesCmd) RunE(_ *cobra.Command, args []string) error { - fs := cs.NewOSFileSystem(".") + fs := cs.NewOSFileSystem(c.Opts.RepoRoot) exporter := exporter.NewExporterService(fs, c.Opts.Output, "", []string{}, c.Opts.RepoRoot, c.Opts.Force) if err := c.GenerateKubernetes(fs, exporter); err != nil { @@ -88,7 +88,7 @@ func AddGenerateKubernetesCmd(generate *cobra.Command, opts *GenerateOpts) { } func (c *GenerateKubernetesCmd) GenerateKubernetes(fs *cs.FileSystem, exp exporter.Exporter) error { - ciInput := path.Join(c.Opts.RepoRoot, c.Opts.Input) + ciInput := c.Opts.Input if c.Opts.Registry == "" { return errors.New("registry is required") } diff --git a/cli/cmd/generate_kubernetes_test.go b/cli/cmd/generate_kubernetes_test.go index 986d955..77bd955 100644 --- a/cli/cmd/generate_kubernetes_test.go +++ b/cli/cmd/generate_kubernetes_test.go @@ -4,8 +4,6 @@ package cmd_test import ( - "path" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" @@ -61,7 +59,6 @@ var _ = Describe("GenerateKubernetes", func() { }) Context("A new input file and registry is provided", func() { - var ciYmlPath string BeforeEach(func() { c.Opts.Registry = "my-registry.com" }) @@ -71,8 +68,7 @@ var _ = Describe("GenerateKubernetes", func() { Expect(err).To(Not(HaveOccurred())) }) It("should not return an error", func() { - ciYmlPath = path.Join(c.Opts.RepoRoot, "ci.dev.yml") - mockExporter.EXPECT().ReadYmlFile(ciYmlPath).Return(&ci.CiYml{}, nil) + mockExporter.EXPECT().ReadYmlFile("ci.dev.yml").Return(&ci.CiYml{}, nil) mockExporter.EXPECT().ExportKubernetesArtifacts("my-registry.com", "", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) err := c.GenerateKubernetes(memoryFs, mockExporter) Expect(err).To(Not(HaveOccurred())) diff --git a/int/export_kubernetes_test.go b/int/export_kubernetes_test.go index 622f052..5830662 100644 --- a/int/export_kubernetes_test.go +++ b/int/export_kubernetes_test.go @@ -364,6 +364,38 @@ var _ = Describe("Kubernetes Export Integration Tests", func() { }) }) + Context("Generate Images Command", func() { + BeforeEach(func() { + By("Creating ci.yml and generating docker artifacts first") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + Expect(output).To(ContainSubstring("docker artifacts created")) + }) + + It("should fail when registry is not provided for generate images", func() { + By("Running generate images without registry") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "images", + "--reporoot", tempDir, + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate images without registry output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(ContainSubstring("registry is required")) + }) + }) + Context("Full Export Workflow", func() { It("should complete the full export workflow from ci.yml to Kubernetes artifacts", func() { By("Step 1: Creating ci.yml with multi-service application") diff --git a/pkg/exporter/exporter.go b/pkg/exporter/exporter.go index c7cb21d..821d41b 100644 --- a/pkg/exporter/exporter.go +++ b/pkg/exporter/exporter.go @@ -58,13 +58,11 @@ func (e *ExporterService) ReadYmlFile(path string) (*ci.CiYml, error) { } func (e *ExporterService) GetExportDir() string { - return filepath.Join(e.repoRoot, e.outputPath) - + return e.outputPath } func (e *ExporterService) GetKubernetesDir() string { - return filepath.Join(e.repoRoot, e.outputPath, "kubernetes") - + return filepath.Join(e.outputPath, "kubernetes") } // ExportDockerArtifacts exports Docker artifacts based on the provided input path, output path, base image, and environment variables. @@ -90,9 +88,6 @@ func (e *ExporterService) ExportDockerArtifacts() error { if err != nil { return fmt.Errorf("error creating dockerfile for service %s: %w", serviceName, err) } - log.Println(e.outputPath) - log.Println(e.GetExportDir()) - log.Println(filepath.Join(e.GetExportDir(), serviceName)) err = e.fs.WriteFile(filepath.Join(e.GetExportDir(), serviceName), "Dockerfile", dockerfile, e.force) if err != nil { return fmt.Errorf("error writing dockerfile for service %s: %w", serviceName, err) @@ -239,7 +234,6 @@ func (e *ExporterService) ExportImages(ctx context.Context, registry string, ima // CreateImageTag creates a Docker image tag from the registry, image prefix and service name. // It returns the full image tag in the format: /-:latest. func (e *ExporterService) CreateImageTag(registry string, imagePrefix string, serviceName string) (string, error) { - log.Println(imagePrefix) if imagePrefix == "" { tag, err := url.JoinPath(registry, fmt.Sprintf("%s:latest", serviceName)) if err != nil { diff --git a/pkg/exporter/exporter_test.go b/pkg/exporter/exporter_test.go index d2613c3..e0b5bd8 100644 --- a/pkg/exporter/exporter_test.go +++ b/pkg/exporter/exporter_test.go @@ -71,20 +71,20 @@ var _ = Describe("GenerateDockerfile", func() { err = e.ExportDockerArtifacts() Expect(err).To(Not(HaveOccurred())) - Expect(memoryFs.DirExists("workspace-repo/export")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/docker-compose.yml")).To(BeTrue()) + Expect(memoryFs.DirExists("./export")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/docker-compose.yml")).To(BeTrue()) - Expect(memoryFs.DirExists("workspace-repo/export/frontend")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/frontend/Dockerfile")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/frontend/entrypoint.sh")).To(BeTrue()) + Expect(memoryFs.DirExists("./export/frontend")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/frontend/Dockerfile")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/frontend/entrypoint.sh")).To(BeTrue()) err = e.ExportKubernetesArtifacts("registry", "image", mock.Anything, mock.Anything, mock.Anything, mock.Anything) Expect(err).To(Not(HaveOccurred())) - Expect(memoryFs.DirExists("workspace-repo/export/kubernetes")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/kubernetes/ingress.yml")).To(BeTrue()) + Expect(memoryFs.DirExists("./export/kubernetes")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/kubernetes/ingress.yml")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/kubernetes/service-frontend.yml")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/kubernetes/service-frontend.yml")).To(BeTrue()) }) }) }) From 483d1c454f256653cff97e4e10e7eeedb37f80db Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:12:15 +0100 Subject: [PATCH 5/6] test: add integration tests for invalid and empty ci.yml scenarios --- int/export_kubernetes_test.go | 52 +++++++++++++++++++++++++++++++++++ int/util/workspace.go | 39 -------------------------- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/int/export_kubernetes_test.go b/int/export_kubernetes_test.go index 5830662..c56d493 100644 --- a/int/export_kubernetes_test.go +++ b/int/export_kubernetes_test.go @@ -96,6 +96,19 @@ run: stripPath: true ` +const invalidYaml = `this is not valid yaml: + - missing proper structure + broken: [indentation +` + +const emptyCiYml = `schemaVersion: v0.2 +prepare: + steps: [] +test: + steps: [] +run: {} +` + var _ = Describe("Kubernetes Export Integration Tests", func() { var ( tempDir string @@ -228,6 +241,45 @@ var _ = Describe("Kubernetes Export Integration Tests", func() { Expect(exitCode).NotTo(Equal(0)) }) + + It("should fail with invalid YAML content", func() { + By("Creating invalid ci.yml") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(invalidYaml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker with invalid YAML") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate docker with invalid YAML output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + }) + + It("should fail with ci.yml with no services", func() { + By("Creating ci.yml with empty run section") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(emptyCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker with empty services") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate docker with empty services output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(ContainSubstring("at least one service is required")) + }) }) Context("Generate Kubernetes Command", func() { diff --git a/int/util/workspace.go b/int/util/workspace.go index ce43214..c6269cc 100644 --- a/int/util/workspace.go +++ b/int/util/workspace.go @@ -5,11 +5,9 @@ package util import ( "bytes" - "fmt" "log" "os" "os/exec" - "path/filepath" "regexp" "strings" "time" @@ -68,43 +66,6 @@ func RunCommandWithExitCode(args ...string) (string, int) { return outputBuffer.String(), exitCode } -// RunCommandInDir runs a command from a specific working directory. -// The cs binary path is resolved relative to the int/ directory. -func RunCommandInDir(dir string, args ...string) string { - output, _ := RunCommandInDirWithExitCode(dir, args...) - return output -} - -// RunCommandInDirWithExitCode runs a command from a specific working directory and returns exit code. -func RunCommandInDirWithExitCode(dir string, args ...string) (string, int) { - // Get absolute path to cs binary (relative to int/ directory) - csBinary, err := filepath.Abs("../cs") - if err != nil { - return fmt.Sprintf("failed to get cs binary path: %v", err), -1 - } - - command := exec.Command(csBinary, args...) - command.Dir = dir - command.Env = os.Environ() - - var outputBuffer bytes.Buffer - command.Stdout = &outputBuffer - command.Stderr = &outputBuffer - - err = command.Run() - - exitCode := 0 - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - exitCode = exitError.ExitCode() - } else { - exitCode = -1 - } - } - - return outputBuffer.String(), exitCode -} - func ExtractWorkspaceId(output string) string { re := regexp.MustCompile(`ID:\s*(\d+)`) matches := re.FindStringSubmatch(output) From f54ac3f7584b7f77137d87eea3de4229d07042f2 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:57:17 +0100 Subject: [PATCH 6/6] fix: integration tests --- api/openapi_client/client.go | 172 ++++++++++++++++++----------------- int/integration_test.go | 20 ++-- int/util/test_helpers.go | 2 +- 3 files changed, 102 insertions(+), 92 deletions(-) diff --git a/api/openapi_client/client.go b/api/openapi_client/client.go index 85a2f0e..7f3f140 100644 --- a/api/openapi_client/client.go +++ b/api/openapi_client/client.go @@ -9,7 +9,6 @@ API version: 0.1.0 // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - package openapi_client import ( @@ -33,14 +32,13 @@ import ( "strings" "time" "unicode/utf8" - ) var ( JsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:[^;]+\+)?json)`) XmlCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:[^;]+\+)?xml)`) queryParamSplit = regexp.MustCompile(`(^|&)([^&]+)`) - queryDescape = strings.NewReplacer( "%5B", "[", "%5D", "]" ) + queryDescape = strings.NewReplacer("%5B", "[", "%5D", "]") ) // APIClient manages communication with the Codesphere Public API API v0.1.0 @@ -139,23 +137,35 @@ func typeCheckParameter(obj interface{}, expected string, name string) error { return nil } -func parameterValueToString( obj interface{}, key string ) string { +func parameterValueToString(obj interface{}, key string) string { if reflect.TypeOf(obj).Kind() != reflect.Ptr { if actualObj, ok := obj.(interface{ GetActualInstanceValue() interface{} }); ok { - return fmt.Sprintf("%v", actualObj.GetActualInstanceValue()) + return formatValue(actualObj.GetActualInstanceValue()) } - return fmt.Sprintf("%v", obj) + return formatValue(obj) } - var param,ok = obj.(MappedNullable) + var param, ok = obj.(MappedNullable) if !ok { return "" } - dataMap,err := param.ToMap() + dataMap, err := param.ToMap() if err != nil { return "" } - return fmt.Sprintf("%v", dataMap[key]) + return formatValue(dataMap[key]) +} + +// formatValue converts a value to string, avoiding scientific notation for floats +func formatValue(obj interface{}) string { + switch v := obj.(type) { + case float32: + return fmt.Sprintf("%.0f", v) + case float64: + return fmt.Sprintf("%.0f", v) + default: + return fmt.Sprintf("%v", obj) + } } // parameterAddToHeaderOrQuery adds the provided object to the request header or url query @@ -167,85 +177,85 @@ func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix stri value = "null" } else { switch v.Kind() { - case reflect.Invalid: - value = "invalid" + case reflect.Invalid: + value = "invalid" - case reflect.Struct: - if t,ok := obj.(MappedNullable); ok { - dataMap,err := t.ToMap() - if err != nil { - return - } - parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, dataMap, style, collectionType) - return - } - if t, ok := obj.(time.Time); ok { - parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, t.Format(time.RFC3339Nano), style, collectionType) - return - } - value = v.Type().String() + " value" - case reflect.Slice: - var indValue = reflect.ValueOf(obj) - if indValue == reflect.ValueOf(nil) { + case reflect.Struct: + if t, ok := obj.(MappedNullable); ok { + dataMap, err := t.ToMap() + if err != nil { return } - var lenIndValue = indValue.Len() - for i:=0;i