From 70909c576c563e76a7fb45d4c50c485906f708ab Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Mon, 3 Nov 2025 16:15:50 +0100 Subject: [PATCH 1/3] fix: readd extract package to commands that need the package --- cli/cmd/build_image.go | 7 + cli/cmd/build_images.go | 7 + cli/cmd/extend_baseimage.go | 5 + cli/cmd/update_dockerfile.go | 5 + internal/installer/config_test.go | 12 - internal/installer/package_test.go | 727 ++++++++++++++++++++--------- 6 files changed, 535 insertions(+), 228 deletions(-) diff --git a/cli/cmd/build_image.go b/cli/cmd/build_image.go index c85ebecf..cec20f60 100644 --- a/cli/cmd/build_image.go +++ b/cli/cmd/build_image.go @@ -28,6 +28,7 @@ type BuildImageOpts struct { Dockerfile string Package string Registry string + Force bool } func (c *BuildImageCmd) RunE(cmd *cobra.Command, args []string) error { @@ -55,6 +56,7 @@ func AddBuildImageCmd(parentCmd *cobra.Command, opts *GlobalOptions) { imageCmd.cmd.Flags().StringVarP(&imageCmd.Opts.Dockerfile, "dockerfile", "d", "", "Path to the Dockerfile to build (required)") imageCmd.cmd.Flags().StringVarP(&imageCmd.Opts.Package, "package", "p", "", "Path to the Codesphere package (required)") imageCmd.cmd.Flags().StringVarP(&imageCmd.Opts.Registry, "registry", "r", "", "Registry URL to push to (e.g., my-registry.com/my-image) (required)") + imageCmd.cmd.Flags().BoolVarP(&imageCmd.Opts.Force, "force", "f", false, "Force new unpacking of the package even if already extracted") util.MarkFlagRequired(imageCmd.cmd, "dockerfile") util.MarkFlagRequired(imageCmd.cmd, "package") @@ -67,6 +69,11 @@ func AddBuildImageCmd(parentCmd *cobra.Command, opts *GlobalOptions) { // AddBuildImageCmd adds the build image command to the parent command func (c *BuildImageCmd) BuildImage(pm installer.PackageManager, im system.ImageManager) error { + err := pm.Extract(c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to extract package: %w", err) + } + codesphereVersion, err := pm.GetCodesphereVersion() if err != nil { return fmt.Errorf("failed to get codesphere version from package: %w", err) diff --git a/cli/cmd/build_images.go b/cli/cmd/build_images.go index 9cda37a4..40a52fa2 100644 --- a/cli/cmd/build_images.go +++ b/cli/cmd/build_images.go @@ -26,6 +26,7 @@ type BuildImagesCmd struct { type BuildImagesOpts struct { *GlobalOptions Config string + Force bool } func (c *BuildImagesCmd) RunE(_ *cobra.Command, args []string) error { @@ -53,6 +54,7 @@ func AddBuildImagesCmd(build *cobra.Command, opts *GlobalOptions) { Env: env.NewEnv(), } buildImages.cmd.Flags().StringVarP(&buildImages.Opts.Config, "config", "c", "", "Path to the configuration YAML file") + buildImages.cmd.Flags().BoolVarP(&buildImages.Opts.Force, "force", "f", false, "Force new unpacking of the package even if already extracted") util.MarkFlagRequired(buildImages.cmd, "config") @@ -74,6 +76,11 @@ func (c *BuildImagesCmd) BuildAndPushImages(pm installer.PackageManager, cm inst return fmt.Errorf("registry server (property registry.server) not defined in the config, please specify a valid registry to which the image shall be pushed") } + err = pm.Extract(c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to extract package: %w", err) + } + codesphereVersion, err := pm.GetCodesphereVersion() if err != nil { return fmt.Errorf("failed to get codesphere version from package: %w", err) diff --git a/cli/cmd/extend_baseimage.go b/cli/cmd/extend_baseimage.go index 248b7dcd..9d0507a0 100644 --- a/cli/cmd/extend_baseimage.go +++ b/cli/cmd/extend_baseimage.go @@ -75,6 +75,11 @@ func AddExtendBaseimageCmd(extend *cobra.Command, opts *GlobalOptions) { } func (c *ExtendBaseimageCmd) ExtendBaseimage(pm installer.PackageManager, im system.ImageManager) error { + err := pm.Extract(c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to extract package: %w", err) + } + imageName, err := pm.GetBaseimageName(c.Opts.Baseimage) if err != nil { return fmt.Errorf("failed to get image name: %w", err) diff --git a/cli/cmd/update_dockerfile.go b/cli/cmd/update_dockerfile.go index 626c4869..8e9ab1b5 100644 --- a/cli/cmd/update_dockerfile.go +++ b/cli/cmd/update_dockerfile.go @@ -81,6 +81,11 @@ in the specified Dockerfile to use that base image. The base image is loaded int } func (c *UpdateDockerfileCmd) UpdateDockerfile(pm installer.PackageManager, im system.ImageManager, args []string) error { + err := pm.Extract(c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to extract package: %w", err) + } + imageName, err := pm.GetBaseimageName(c.Opts.Baseimage) if err != nil { return fmt.Errorf("failed to get image name: %w", err) diff --git a/internal/installer/config_test.go b/internal/installer/config_test.go index 5f09f46e..96f43df6 100644 --- a/internal/installer/config_test.go +++ b/internal/installer/config_test.go @@ -169,18 +169,6 @@ registry: Expect(err).ToNot(HaveOccurred()) }) }) - - Context("ExtractOciImageIndex with various scenarios", func() { - It("handles empty image file path", func() { - // Test moved to package_test.go - Skip("ExtractOciImageIndex tests moved to package_test.go") - }) - - It("handles directory instead of file", func() { - // Test moved to package_test.go - Skip("ExtractOciImageIndex tests moved to package_test.go") - }) - }) }) Describe("Integration scenarios", func() { diff --git a/internal/installer/package_test.go b/internal/installer/package_test.go index 2d65b8fe..e199be90 100644 --- a/internal/installer/package_test.go +++ b/internal/installer/package_test.go @@ -103,7 +103,11 @@ var _ = Describe("Package", func() { BeforeEach(func() { // Create the package tar.gz file packagePath := filepath.Join(tempDir, filename) - err := createTestTarGzPackage(packagePath) + err := createTestPackage(packagePath, PackageFiles{ + MainFiles: map[string]string{ + "test-file.txt": "test content", + }, + }) Expect(err).ToNot(HaveOccurred()) pkg.Filename = packagePath }) @@ -178,7 +182,14 @@ var _ = Describe("Package", func() { BeforeEach(func() { // Create the package tar.gz file with deps.tar.gz inside packagePath = filepath.Join(tempDir, filename) - err := createTestTarGzPackageWithDeps(packagePath) + err := createTestPackage(packagePath, PackageFiles{ + MainFiles: map[string]string{ + "main-file.txt": "main package content", + }, + DepsFiles: map[string]string{ + "test-dep.txt": "dependency content", + }, + }) Expect(err).ToNot(HaveOccurred()) pkg.Filename = packagePath }) @@ -272,7 +283,11 @@ var _ = Describe("Package", func() { It("handles empty workdir", func() { pkg.OmsWorkdir = "" packagePath := filepath.Join(tempDir, filename) - err := createTestTarGzPackage(packagePath) + err := createTestPackage(packagePath, PackageFiles{ + MainFiles: map[string]string{ + "test-file.txt": "test content", + }, + }) Expect(err).ToNot(HaveOccurred()) pkg.Filename = packagePath @@ -286,7 +301,14 @@ var _ = Describe("Package", func() { Context("ExtractDependency with various scenarios", func() { It("handles empty dependency filename", func() { packagePath := filepath.Join(tempDir, filename) - err := createTestTarGzPackageWithDeps(packagePath) + err := createTestPackage(packagePath, PackageFiles{ + MainFiles: map[string]string{ + "main-file.txt": "main package content", + }, + DepsFiles: map[string]string{ + "test-dep.txt": "dependency content", + }, + }) Expect(err).ToNot(HaveOccurred()) pkg.Filename = packagePath @@ -318,7 +340,16 @@ var _ = Describe("Package", func() { BeforeEach(func() { packagePath = filepath.Join(tempDir, "complete-package.tar.gz") - err := createComplexTestPackage(packagePath) + err := createTestPackage(packagePath, PackageFiles{ + MainFiles: map[string]string{ + "main-content.txt": "complex main package content", + }, + DepsFiles: map[string]string{ + "dep1.txt": "dependency 1 content", + "dep2.txt": "dependency 2 content", + "subdep/dep3.txt": "sub dependency 3 content", + }, + }) Expect(err).ToNot(HaveOccurred()) pkg.Filename = packagePath }) @@ -355,216 +386,6 @@ var _ = Describe("Package", func() { }) }) -// Helper functions for creating test tar.gz files - -// createTestTarGzPackage creates a simple tar.gz package for testing -func createTestTarGzPackage(filename string) error { - file, err := os.Create(filename) - if err != nil { - return err - } - defer util.CloseFileIgnoreError(file) - - gzw := gzip.NewWriter(file) - defer func() { _ = gzw.Close() }() - - tw := tar.NewWriter(gzw) - defer func() { _ = tw.Close() }() - - // Add a test file - content := "test content" - header := &tar.Header{ - Name: "test-file.txt", - Mode: 0644, - Size: int64(len(content)), - } - if err := tw.WriteHeader(header); err != nil { - return err - } - if _, err := tw.Write([]byte(content)); err != nil { - return err - } - - return nil -} - -// createTestTarGzPackageWithDeps creates a tar.gz package containing a deps.tar.gz file -func createTestTarGzPackageWithDeps(filename string) error { - file, err := os.Create(filename) - if err != nil { - return err - } - defer util.CloseFileIgnoreError(file) - - gzw := gzip.NewWriter(file) - defer func() { _ = gzw.Close() }() - - tw := tar.NewWriter(gzw) - defer func() { _ = tw.Close() }() - - // Add main content - mainContent := "main package content" - header := &tar.Header{ - Name: "main-file.txt", - Mode: 0644, - Size: int64(len(mainContent)), - } - if err := tw.WriteHeader(header); err != nil { - return err - } - if _, err := tw.Write([]byte(mainContent)); err != nil { - return err - } - - // Create deps.tar.gz content in memory - depsContent, err := createDepsArchive() - if err != nil { - return err - } - - // Add deps.tar.gz file - depsHeader := &tar.Header{ - Name: "deps.tar.gz", - Mode: 0644, - Size: int64(len(depsContent)), - } - if err := tw.WriteHeader(depsHeader); err != nil { - return err - } - if _, err := tw.Write(depsContent); err != nil { - return err - } - - return nil -} - -// createComplexTestPackage creates a complex package for integration testing -func createComplexTestPackage(filename string) error { - file, err := os.Create(filename) - if err != nil { - return err - } - defer util.CloseFileIgnoreError(file) - - gzw := gzip.NewWriter(file) - defer func() { _ = gzw.Close() }() - - tw := tar.NewWriter(gzw) - defer func() { _ = tw.Close() }() - - // Add main content - mainContent := "complex main package content" - header := &tar.Header{ - Name: "main-content.txt", - Mode: 0644, - Size: int64(len(mainContent)), - } - if err := tw.WriteHeader(header); err != nil { - return err - } - if _, err := tw.Write([]byte(mainContent)); err != nil { - return err - } - - // Create complex deps.tar.gz content - depsContent, err := createComplexDepsArchive() - if err != nil { - return err - } - - // Add deps.tar.gz file - depsHeader := &tar.Header{ - Name: "deps.tar.gz", - Mode: 0644, - Size: int64(len(depsContent)), - } - if err := tw.WriteHeader(depsHeader); err != nil { - return err - } - if _, err := tw.Write(depsContent); err != nil { - return err - } - - return nil -} - -// createDepsArchive creates a deps.tar.gz archive content in memory -func createDepsArchive() ([]byte, error) { - var buf []byte - gzw := gzip.NewWriter(&bytesBuffer{data: &buf}) - tw := tar.NewWriter(gzw) - - // Add test dependency - depContent := "dependency content" - header := &tar.Header{ - Name: "test-dep.txt", - Mode: 0644, - Size: int64(len(depContent)), - } - if err := tw.WriteHeader(header); err != nil { - return nil, err - } - if _, err := tw.Write([]byte(depContent)); err != nil { - return nil, err - } - - if err := tw.Close(); err != nil { - return nil, err - } - if err := gzw.Close(); err != nil { - return nil, err - } - - return buf, nil -} - -// createComplexDepsArchive creates a complex deps.tar.gz archive with multiple files -func createComplexDepsArchive() ([]byte, error) { - var buf []byte - gzw := gzip.NewWriter(&bytesBuffer{data: &buf}) - tw := tar.NewWriter(gzw) - - // Add multiple dependencies - deps := map[string]string{ - "dep1.txt": "dependency 1 content", - "dep2.txt": "dependency 2 content", - "subdep/dep3.txt": "sub dependency 3 content", - } - - for name, content := range deps { - header := &tar.Header{ - Name: name, - Mode: 0644, - Size: int64(len(content)), - } - if err := tw.WriteHeader(header); err != nil { - return nil, err - } - if _, err := tw.Write([]byte(content)); err != nil { - return nil, err - } - } - - if err := tw.Close(); err != nil { - return nil, err - } - if err := gzw.Close(); err != nil { - return nil, err - } - - return buf, nil -} - -// bytesBuffer is a simple buffer that implements io.Writer for creating in-memory archives -type bytesBuffer struct { - data *[]byte -} - -func (b *bytesBuffer) Write(p []byte) (n int, err error) { - *b.data = append(*b.data, p...) - return len(p), nil -} - // Tests for ExtractOciImageIndex (moved from config_test.go) var _ = Describe("Package ExtractOciImageIndex", func() { var ( @@ -696,6 +517,389 @@ var _ = Describe("Package ExtractOciImageIndex", func() { }) }) +// Tests for GetBaseimageName +var _ = Describe("Package GetBaseimageName", func() { + var ( + pkg *installer.Package + tempDir string + ) + + BeforeEach(func() { + tempDir = GinkgoT().TempDir() + omsWorkdir := filepath.Join(tempDir, "oms-workdir") + pkg = installer.NewPackage(omsWorkdir, "test-package.tar.gz") + }) + + Describe("GetBaseimageName", func() { + Context("when baseimage parameter is empty", func() { + It("returns an error", func() { + _, err := pkg.GetBaseimageName("") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("baseimage not specified")) + }) + }) + + Context("when bom.json file does not exist", func() { + It("returns an error", func() { + _, err := pkg.GetBaseimageName("workspace-agent-24.04") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to load bom.json")) + }) + }) + + Context("when bom.json exists but is invalid", func() { + BeforeEach(func() { + // Create invalid bom.json + workDir := pkg.GetWorkDir() + err := os.MkdirAll(filepath.Join(workDir, "deps"), 0755) + Expect(err).NotTo(HaveOccurred()) + + bomPath := pkg.GetDependencyPath("bom.json") + err = os.WriteFile(bomPath, []byte("invalid json"), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error", func() { + _, err := pkg.GetBaseimageName("workspace-agent-24.04") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to load bom.json")) + }) + }) + + Context("when bom.json exists but codesphere component is missing", func() { + BeforeEach(func() { + // Create bom.json without codesphere component + workDir := pkg.GetWorkDir() + err := os.MkdirAll(filepath.Join(workDir, "deps"), 0755) + Expect(err).NotTo(HaveOccurred()) + + bomContent := `{ + "components": { + "docker": { + "files": {} + } + } + }` + bomPath := pkg.GetDependencyPath("bom.json") + err = os.WriteFile(bomPath, []byte(bomContent), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error", func() { + _, err := pkg.GetBaseimageName("workspace-agent-24.04") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get codesphere container images from bom.json")) + }) + }) + + Context("when baseimage is not found in bom.json", func() { + BeforeEach(func() { + // Create bom.json with codesphere component but without the requested baseimage + workDir := pkg.GetWorkDir() + err := os.MkdirAll(filepath.Join(workDir, "deps"), 0755) + Expect(err).NotTo(HaveOccurred()) + + bomContent := `{ + "components": { + "codesphere": { + "containerImages": { + "workspace-agent-20.04": "ghcr.io/codesphere-cloud/workspace-agent-20.04:v1.0.0" + } + } + } + }` + bomPath := pkg.GetDependencyPath("bom.json") + err = os.WriteFile(bomPath, []byte(bomContent), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error", func() { + _, err := pkg.GetBaseimageName("workspace-agent-24.04") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("baseimage workspace-agent-24.04 not found in bom.json")) + }) + }) + + Context("when baseimage exists in bom.json", func() { + BeforeEach(func() { + // Create valid bom.json with the requested baseimage + workDir := pkg.GetWorkDir() + err := os.MkdirAll(filepath.Join(workDir, "deps"), 0755) + Expect(err).NotTo(HaveOccurred()) + + bomContent := `{ + "components": { + "codesphere": { + "containerImages": { + "workspace-agent-24.04": "ghcr.io/codesphere-cloud/workspace-agent-24.04:codesphere-v1.66.0", + "workspace-agent-20.04": "ghcr.io/codesphere-cloud/workspace-agent-20.04:codesphere-v1.65.0" + } + } + } + }` + bomPath := pkg.GetDependencyPath("bom.json") + err = os.WriteFile(bomPath, []byte(bomContent), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the correct image name", func() { + imageName, err := pkg.GetBaseimageName("workspace-agent-24.04") + Expect(err).NotTo(HaveOccurred()) + Expect(imageName).To(Equal("ghcr.io/codesphere-cloud/workspace-agent-24.04:codesphere-v1.66.0")) + }) + + It("returns the correct image name for different baseimage", func() { + imageName, err := pkg.GetBaseimageName("workspace-agent-20.04") + Expect(err).NotTo(HaveOccurred()) + Expect(imageName).To(Equal("ghcr.io/codesphere-cloud/workspace-agent-20.04:codesphere-v1.65.0")) + }) + }) + }) +}) + +// Tests for GetBaseimagePath +var _ = Describe("Package GetBaseimagePath", func() { + var ( + pkg *installer.Package + tempDir string + ) + + BeforeEach(func() { + tempDir = GinkgoT().TempDir() + omsWorkdir := filepath.Join(tempDir, "oms-workdir") + pkg = installer.NewPackage(omsWorkdir, "test-package.tar.gz") + }) + + Describe("GetBaseimagePath", func() { + Context("when baseimage parameter is empty", func() { + It("returns an error", func() { + _, err := pkg.GetBaseimagePath("", false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("baseimage not specified")) + }) + }) + + Context("when ExtractDependency fails", func() { + It("returns an error", func() { + // Try to extract non-existent dependency + _, err := pkg.GetBaseimagePath("nonexistent-image", false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract package to workdir")) + }) + }) + + Context("with successful dependency extraction", func() { + BeforeEach(func() { + // Create the main package with deps.tar.gz + packagePath := filepath.Join(tempDir, "test-package.tar.gz") + err := createTestPackage(packagePath, PackageFiles{ + MainFiles: map[string]string{ + "main-file.txt": "main package content", + }, + DepsFiles: map[string]string{ + "./codesphere/images/workspace-agent-24.04.tar": "fake image content", + }, + }) + Expect(err).NotTo(HaveOccurred()) + pkg.Filename = packagePath + }) + + It("returns correct path for baseimage without .tar extension", func() { + path, err := pkg.GetBaseimagePath("workspace-agent-24.04", false) + Expect(err).NotTo(HaveOccurred()) + + expectedPath := pkg.GetDependencyPath("./codesphere/images/workspace-agent-24.04.tar") + Expect(path).To(Equal(expectedPath)) + }) + + It("returns correct path for baseimage with .tar extension", func() { + path, err := pkg.GetBaseimagePath("workspace-agent-24.04.tar", false) + Expect(err).NotTo(HaveOccurred()) + + expectedPath := pkg.GetDependencyPath("./codesphere/images/workspace-agent-24.04.tar") + Expect(path).To(Equal(expectedPath)) + }) + + It("uses force parameter correctly", func() { + // First extraction + _, err := pkg.GetBaseimagePath("workspace-agent-24.04", false) + Expect(err).NotTo(HaveOccurred()) + + // Second extraction with force + path, err := pkg.GetBaseimagePath("workspace-agent-24.04", true) + Expect(err).NotTo(HaveOccurred()) + + expectedPath := pkg.GetDependencyPath("./codesphere/images/workspace-agent-24.04.tar") + Expect(path).To(Equal(expectedPath)) + }) + }) + }) +}) + +// Tests for GetCodesphereVersion +var _ = Describe("Package GetCodesphereVersion", func() { + var ( + pkg *installer.Package + tempDir string + ) + + BeforeEach(func() { + tempDir = GinkgoT().TempDir() + omsWorkdir := filepath.Join(tempDir, "oms-workdir") + pkg = installer.NewPackage(omsWorkdir, "test-package.tar.gz") + }) + + Describe("GetCodesphereVersion", func() { + Context("when bom.json file does not exist", func() { + It("returns an error", func() { + _, err := pkg.GetCodesphereVersion() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to load bom.json")) + }) + }) + + Context("when bom.json exists but is invalid", func() { + BeforeEach(func() { + // Create invalid bom.json + workDir := pkg.GetWorkDir() + err := os.MkdirAll(filepath.Join(workDir, "deps"), 0755) + Expect(err).NotTo(HaveOccurred()) + + bomPath := pkg.GetDependencyPath("bom.json") + err = os.WriteFile(bomPath, []byte("invalid json"), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error", func() { + _, err := pkg.GetCodesphereVersion() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to load bom.json")) + }) + }) + + Context("when bom.json exists but codesphere component is missing", func() { + BeforeEach(func() { + // Create bom.json without codesphere component + workDir := pkg.GetWorkDir() + err := os.MkdirAll(filepath.Join(workDir, "deps"), 0755) + Expect(err).NotTo(HaveOccurred()) + + bomContent := `{ + "components": { + "docker": { + "files": {} + } + } + }` + bomPath := pkg.GetDependencyPath("bom.json") + err = os.WriteFile(bomPath, []byte(bomContent), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error", func() { + _, err := pkg.GetCodesphereVersion() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get codesphere container images from bom.json")) + }) + }) + + Context("when no container images with codesphere-v exist", func() { + BeforeEach(func() { + // Create bom.json with images but no codesphere-v versions + workDir := pkg.GetWorkDir() + err := os.MkdirAll(filepath.Join(workDir, "deps"), 0755) + Expect(err).NotTo(HaveOccurred()) + + bomContent := `{ + "components": { + "codesphere": { + "containerImages": { + "workspace-agent-24.04": "ghcr.io/codesphere-cloud/workspace-agent-24.04:v1.0.0", + "auth-service": "ghcr.io/codesphere-cloud/auth-service:latest" + } + } + } + }` + bomPath := pkg.GetDependencyPath("bom.json") + err = os.WriteFile(bomPath, []byte(bomContent), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error", func() { + _, err := pkg.GetCodesphereVersion() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no container images found in bom.json")) + }) + }) + + Context("when container images exist but have invalid format", func() { + BeforeEach(func() { + // Create bom.json with images that have invalid format (no colon) + workDir := pkg.GetWorkDir() + err := os.MkdirAll(filepath.Join(workDir, "deps"), 0755) + Expect(err).NotTo(HaveOccurred()) + + bomContent := `{ + "components": { + "codesphere": { + "containerImages": { + "workspace-agent-24.04": "invalid-image-format-without-colon-codesphere-v1.66.0" + } + } + } + }` + bomPath := pkg.GetDependencyPath("bom.json") + err = os.WriteFile(bomPath, []byte(bomContent), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error", func() { + _, err := pkg.GetCodesphereVersion() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid image name format")) + }) + }) + + Context("when valid codesphere versions exist", func() { + BeforeEach(func() { + // Create bom.json with multiple different versions (should pick the first one found) + workDir := pkg.GetWorkDir() + err := os.MkdirAll(filepath.Join(workDir, "deps"), 0755) + Expect(err).NotTo(HaveOccurred()) + + bomContent := `{ + "components": { + "codesphere": { + "containerImages": { + "workspace-agent-24.04": "ghcr.io/codesphere-cloud/workspace-agent-24.04:codesphere-v1.66.0", + "auth-service": "ghcr.io/codesphere-cloud/auth-service:codesphere-v1.65.0" + } + } + } + }` + bomPath := pkg.GetDependencyPath("bom.json") + err = os.WriteFile(bomPath, []byte(bomContent), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns a valid codesphere version", func() { + version, err := pkg.GetCodesphereVersion() + Expect(err).NotTo(HaveOccurred()) + // Should return one of the codesphere versions (depends on map iteration order) + Expect(version).To(Or(Equal("codesphere-v1.66.0"), Equal("codesphere-v1.65.0"))) + }) + }) + }) +}) + +// Helper functions for creating test tar.gz files + +// PackageFiles represents files to include in a test package +type PackageFiles struct { + MainFiles map[string]string // filename -> content + DepsFiles map[string]string // filename -> content for deps.tar.gz +} + // createTar creates a tar file containing a file with the given content func createTar(tarName string, fileName string, fileContent string) error { file, err := os.Create(tarName) @@ -707,7 +911,6 @@ func createTar(tarName string, fileName string, fileContent string) error { tw := tar.NewWriter(file) defer func() { _ = tw.Close() }() - // Add the specified file header := &tar.Header{ Name: fileName, Mode: 0644, @@ -722,3 +925,95 @@ func createTar(tarName string, fileName string, fileContent string) error { return nil } + +// createTarGz creates a deps.tar.gz archive content in memory +func createTarGz(files map[string]string) ([]byte, error) { + var buf []byte + gzw := gzip.NewWriter(&bytesBuffer{data: &buf}) + tw := tar.NewWriter(gzw) + + for name, content := range files { + header := &tar.Header{ + Name: name, + Mode: 0644, + Size: int64(len(content)), + } + if err := tw.WriteHeader(header); err != nil { + return nil, err + } + if _, err := tw.Write([]byte(content)); err != nil { + return nil, err + } + } + + if err := tw.Close(); err != nil { + return nil, err + } + if err := gzw.Close(); err != nil { + return nil, err + } + + return buf, nil +} + +// createTestPackage creates a tar.gz package with the specified files +func createTestPackage(filename string, files PackageFiles) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer util.CloseFileIgnoreError(file) + + gzw := gzip.NewWriter(file) + defer func() { _ = gzw.Close() }() + + tw := tar.NewWriter(gzw) + defer func() { _ = tw.Close() }() + + // Add main files + for name, content := range files.MainFiles { + header := &tar.Header{ + Name: name, + Mode: 0644, + Size: int64(len(content)), + } + if err := tw.WriteHeader(header); err != nil { + return err + } + if _, err := tw.Write([]byte(content)); err != nil { + return err + } + } + + // Add deps.tar.gz if dependency files are specified + if len(files.DepsFiles) > 0 { + depsContent, err := createTarGz(files.DepsFiles) + if err != nil { + return err + } + + depsHeader := &tar.Header{ + Name: "deps.tar.gz", + Mode: 0644, + Size: int64(len(depsContent)), + } + if err := tw.WriteHeader(depsHeader); err != nil { + return err + } + if _, err := tw.Write(depsContent); err != nil { + return err + } + } + + return nil +} + +// bytesBuffer is a simple buffer that implements io.Writer for creating in-memory archives +type bytesBuffer struct { + data *[]byte +} + +func (b *bytesBuffer) Write(p []byte) (n int, err error) { + *b.data = append(*b.data, p...) + return len(p), nil +} From 849ef312d395349e0481f7c2eaca0af1df548715 Mon Sep 17 00:00:00 2001 From: siherrmann <25087590+siherrmann@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:24:28 +0000 Subject: [PATCH 2/3] chore(docs): Auto-update docs and licenses Signed-off-by: siherrmann <25087590+siherrmann@users.noreply.github.com> --- docs/oms-cli_build_image.md | 1 + docs/oms-cli_build_images.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/oms-cli_build_image.md b/docs/oms-cli_build_image.md index 0964fae6..6d74e8b8 100644 --- a/docs/oms-cli_build_image.md +++ b/docs/oms-cli_build_image.md @@ -22,6 +22,7 @@ $ oms-cli build image --dockerfile baseimage/Dockerfile --package codesphere-v1. ``` -d, --dockerfile string Path to the Dockerfile to build (required) + -f, --force Force new unpacking of the package even if already extracted -h, --help help for image -p, --package string Path to the Codesphere package (required) -r, --registry string Registry URL to push to (e.g., my-registry.com/my-image) (required) diff --git a/docs/oms-cli_build_images.md b/docs/oms-cli_build_images.md index db98c6de..07ae413f 100644 --- a/docs/oms-cli_build_images.md +++ b/docs/oms-cli_build_images.md @@ -15,6 +15,7 @@ oms-cli build images [flags] ``` -c, --config string Path to the configuration YAML file + -f, --force Force new unpacking of the package even if already extracted -h, --help help for images ``` From 6218b416f8e776e34587fcd6549f9d65358a808b Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Mon, 3 Nov 2025 16:53:53 +0100 Subject: [PATCH 3/3] fix: fix tests with extraction --- cli/cmd/build_image_test.go | 6 +++++- cli/cmd/build_images_test.go | 5 +++++ cli/cmd/extend_baseimage_test.go | 6 ++++++ cli/cmd/update_dockerfile_test.go | 7 +++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/cli/cmd/build_image_test.go b/cli/cmd/build_image_test.go index b952a3a1..ad84ca9a 100644 --- a/cli/cmd/build_image_test.go +++ b/cli/cmd/build_image_test.go @@ -49,7 +49,7 @@ var _ = Describe("BuildImageCmd", func() { err := c.RunE(nil, []string{}) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to get codesphere version from package")) + Expect(err.Error()).To(ContainSubstring("failed to extract package")) }) It("succeeds with valid options", func() { @@ -67,6 +67,7 @@ var _ = Describe("BuildImageCmd", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetCodesphereVersion().Return("", errors.New("failed to extract version")) err := c.BuildImage(mockPackageManager, mockImageManager) @@ -81,6 +82,7 @@ var _ = Describe("BuildImageCmd", func() { c.Opts.Dockerfile = "Dockerfile" c.Opts.Registry = "my-registry.com/my-image" + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetCodesphereVersion().Return("codesphere-v1.66.0", nil) mockImageManager.EXPECT().BuildImage("Dockerfile", "my-registry.com/my-image:codesphere-v1.66.0", ".").Return(errors.New("build failed")) @@ -96,6 +98,7 @@ var _ = Describe("BuildImageCmd", func() { c.Opts.Dockerfile = "Dockerfile" c.Opts.Registry = "my-registry.com/my-image" + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetCodesphereVersion().Return("codesphere-v1.66.0", nil) mockImageManager.EXPECT().BuildImage("Dockerfile", "my-registry.com/my-image:codesphere-v1.66.0", ".").Return(nil) mockImageManager.EXPECT().PushImage("my-registry.com/my-image:codesphere-v1.66.0").Return(errors.New("push failed")) @@ -112,6 +115,7 @@ var _ = Describe("BuildImageCmd", func() { c.Opts.Dockerfile = "Dockerfile" c.Opts.Registry = "my-registry.com/my-image" + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetCodesphereVersion().Return("codesphere-v1.66.0", nil) mockImageManager.EXPECT().BuildImage("Dockerfile", "my-registry.com/my-image:codesphere-v1.66.0", ".").Return(nil) mockImageManager.EXPECT().PushImage("my-registry.com/my-image:codesphere-v1.66.0").Return(nil) diff --git a/cli/cmd/build_images_test.go b/cli/cmd/build_images_test.go index 285762ed..1b76fecb 100644 --- a/cli/cmd/build_images_test.go +++ b/cli/cmd/build_images_test.go @@ -197,6 +197,7 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-without-dockerfile.yaml").Return(configWithoutDockerfile, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetCodesphereVersion().Return("1.0.0", nil) err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) @@ -233,6 +234,7 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-with-dockerfile.yaml").Return(configWithDockerfile, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetCodesphereVersion().Return("1.0.0", nil) mockImageManager.EXPECT().BuildImage("Dockerfile", "registry.example.com/my-ubuntu-24.04-default:1.0.0", ".").Return(errors.New("build failed")) @@ -271,6 +273,7 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-with-dockerfile.yaml").Return(configWithDockerfile, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetCodesphereVersion().Return("1.0.0", nil) mockImageManager.EXPECT().BuildImage("Dockerfile", "registry.example.com/my-ubuntu-24.04-default:1.0.0", ".").Return(nil) mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-default:1.0.0").Return(errors.New("push failed")) @@ -310,6 +313,7 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-with-dockerfile.yaml").Return(configWithDockerfile, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetCodesphereVersion().Return("1.0.0", nil) mockImageManager.EXPECT().BuildImage("Dockerfile", "registry.example.com/my-ubuntu-24.04-default:1.0.0", ".").Return(nil) mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-default:1.0.0").Return(nil) @@ -366,6 +370,7 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-with-multiple-images.yaml").Return(configWithMultipleImages, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetCodesphereVersion().Return("1.0.0", nil) // Expect calls for my-ubuntu-24.04 default flavor diff --git a/cli/cmd/extend_baseimage_test.go b/cli/cmd/extend_baseimage_test.go index 6b5dd8ed..1c14ed5c 100644 --- a/cli/cmd/extend_baseimage_test.go +++ b/cli/cmd/extend_baseimage_test.go @@ -66,6 +66,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("", errors.New("failed to get image name: extraction failed")) err := c.ExtendBaseimage(mockPackageManager, mockImageManager) @@ -77,6 +78,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("", errors.New("failed to extract OCI image index: index extraction failed")) err := c.ExtendBaseimage(mockPackageManager, mockImageManager) @@ -88,6 +90,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("", errors.New("failed to read image tags: no image names found")) err := c.ExtendBaseimage(mockPackageManager, mockImageManager) @@ -106,6 +109,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { defer func() { _ = os.Remove(tempFile.Name()) }() defer func() { _ = tempFile.Close() }() + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("ubuntu:24.04-base", nil) mockPackageManager.EXPECT().GetBaseimagePath("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", nil) mockPackageManager.EXPECT().FileIO().Return(mockFileIO) @@ -122,6 +126,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { mockImageManager := system.NewMockImageManager(GinkgoT()) c.Opts.Force = true + mockPackageManager.EXPECT().Extract(true).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("", errors.New("failed to extract package to workdir: extraction failed")) err := c.ExtendBaseimage(mockPackageManager, mockImageManager) @@ -140,6 +145,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { defer func() { _ = os.Remove(tempFile.Name()) }() defer func() { _ = tempFile.Close() }() + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("ubuntu:24.04-base", nil) mockPackageManager.EXPECT().GetBaseimagePath("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", nil) mockPackageManager.EXPECT().FileIO().Return(mockFileIO) diff --git a/cli/cmd/update_dockerfile_test.go b/cli/cmd/update_dockerfile_test.go index 62c6250c..c25e90a3 100644 --- a/cli/cmd/update_dockerfile_test.go +++ b/cli/cmd/update_dockerfile_test.go @@ -87,6 +87,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "workspace-agent-24.04.tar" c.Opts.Force = false + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("workspace-agent-24.04.tar").Return("", errors.New("failed to extract image")) err := c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) @@ -103,6 +104,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "" c.Opts.Force = false + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("ubuntu:24.04", nil) mockPackageManager.EXPECT().GetBaseimagePath("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", nil) mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(nil) @@ -122,6 +124,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "" c.Opts.Force = false + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("ubuntu:24.04", nil) mockPackageManager.EXPECT().GetBaseimagePath("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", nil) mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(errors.New("load failed")) @@ -152,6 +155,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "" c.Opts.Force = false + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("ubuntu:24.04", nil) mockPackageManager.EXPECT().GetBaseimagePath("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", nil) mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(nil) @@ -185,6 +189,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "" c.Opts.Force = false + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("").Return("ubuntu:24.04", nil) mockPackageManager.EXPECT().GetBaseimagePath("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", nil) mockPackageManager.EXPECT().FileIO().Return(mockFileIO) @@ -217,6 +222,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "workspace-agent-20.04.tar" c.Opts.Force = true + mockPackageManager.EXPECT().Extract(true).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("workspace-agent-20.04.tar").Return("ubuntu:20.04", nil) mockPackageManager.EXPECT().GetBaseimagePath("workspace-agent-20.04.tar", true).Return("/test/workdir/deps/codesphere/images/workspace-agent-20.04.tar", nil) mockPackageManager.EXPECT().FileIO().Return(mockFileIO) @@ -249,6 +255,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "workspace-agent-24.04.tar" c.Opts.Force = false + mockPackageManager.EXPECT().Extract(false).Return(nil) mockPackageManager.EXPECT().GetBaseimageName("workspace-agent-24.04.tar").Return("registry.example.com/workspace-agent:24.04", nil) mockPackageManager.EXPECT().GetBaseimagePath("workspace-agent-24.04.tar", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", nil) mockPackageManager.EXPECT().FileIO().Return(mockFileIO)