From c12f131811e18f5cc10d927c5f03e99d07f9f46b Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Tue, 28 Oct 2025 13:03:47 +0100 Subject: [PATCH 01/22] update: add build image to install codesphere, refactor installer, add tests --- .mockery.yml | 8 + cli/cmd/build.go | 26 + cli/cmd/build_images.go | 105 ++++ cli/cmd/build_images_test.go | 401 ++++++++++++++ cli/cmd/extend_baseimage.go | 25 +- cli/cmd/extend_baseimage_test.go | 199 ++++--- cli/cmd/install_codesphere.go | 78 ++- cli/cmd/install_codesphere_test.go | 391 ++++++++------ cli/cmd/root.go | 1 + go.mod | 2 +- internal/installer/config.go | 51 ++ internal/installer/config_test.go | 398 ++++++++++++++ internal/installer/files/config_yaml.go | 81 +++ internal/installer/files/oci_image_index.go | 55 ++ internal/installer/installer_suite_test.go | 16 + internal/installer/mocks.go | 399 ++++++++++++++ internal/installer/package.go | 27 +- internal/installer/package_test.go | 566 ++++++++++++++++++++ internal/system/container.go | 107 ---- internal/system/image.go | 99 ++++ internal/system/mocks.go | 173 ++++++ 21 files changed, 2809 insertions(+), 399 deletions(-) create mode 100644 cli/cmd/build.go create mode 100644 cli/cmd/build_images.go create mode 100644 cli/cmd/build_images_test.go create mode 100644 internal/installer/config.go create mode 100644 internal/installer/config_test.go create mode 100644 internal/installer/files/config_yaml.go create mode 100644 internal/installer/files/oci_image_index.go create mode 100644 internal/installer/installer_suite_test.go create mode 100644 internal/installer/mocks.go create mode 100644 internal/installer/package_test.go delete mode 100644 internal/system/container.go create mode 100644 internal/system/image.go create mode 100644 internal/system/mocks.go diff --git a/.mockery.yml b/.mockery.yml index 22cced8c..fba11d22 100644 --- a/.mockery.yml +++ b/.mockery.yml @@ -18,10 +18,18 @@ packages: config: all: true interfaces: + github.com/codesphere-cloud/oms/internal/installer: + config: + all: true + interfaces: github.com/codesphere-cloud/oms/internal/portal: config: all: true interfaces: + github.com/codesphere-cloud/oms/internal/system: + config: + all: true + interfaces: github.com/codesphere-cloud/oms/internal/util: config: all: true diff --git a/cli/cmd/build.go b/cli/cmd/build.go new file mode 100644 index 00000000..04802e0c --- /dev/null +++ b/cli/cmd/build.go @@ -0,0 +1,26 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/spf13/cobra" +) + +// BuildCmd represents the build command +type BuildCmd struct { + cmd *cobra.Command +} + +func AddBuildCmd(rootCmd *cobra.Command, opts *GlobalOptions) { + build := BuildCmd{ + cmd: &cobra.Command{ + Use: "build", + Short: "Build and push images to a registry", + Long: io.Long(`Build and push container images to a registry using the provided configuration.`), + }, + } + rootCmd.AddCommand(build.cmd) + AddBuildImagesCmd(build.cmd, opts) +} diff --git a/cli/cmd/build_images.go b/cli/cmd/build_images.go new file mode 100644 index 00000000..ff550710 --- /dev/null +++ b/cli/cmd/build_images.go @@ -0,0 +1,105 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "context" + "fmt" + "log" + + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/system" + "github.com/codesphere-cloud/oms/internal/util" + "github.com/codesphere-cloud/oms/internal/version" + "github.com/spf13/cobra" +) + +// BuildImagesCmd represents the build images command +type BuildImagesCmd struct { + cmd *cobra.Command + Opts *BuildImagesOpts + Env env.Env +} + +type BuildImagesOpts struct { + *GlobalOptions + Config string +} + +func (c *BuildImagesCmd) RunE(_ *cobra.Command, args []string) error { + cm := installer.NewConfig() + im := system.NewImage(context.Background()) + + err := c.BuildAndPushImages(cm, im) + if err != nil { + return fmt.Errorf("failed to build and push images: %w", err) + } + + return nil +} + +func AddBuildImagesCmd(build *cobra.Command, opts *GlobalOptions) { + buildImages := BuildImagesCmd{ + cmd: &cobra.Command{ + Use: "images", + Short: "Build and push container images", + Long: io.Long(`Build and push container images based on the configuration file. + Extracts necessary images configuration to get the bomRef and processes them.`), + }, + Opts: &BuildImagesOpts{GlobalOptions: opts}, + Env: env.NewEnv(), + } + buildImages.cmd.Flags().StringVarP(&buildImages.Opts.Config, "config", "c", "", "Path to the configuration YAML file") + + util.MarkFlagRequired(buildImages.cmd, "config") + + build.AddCommand(buildImages.cmd) + + buildImages.cmd.RunE = buildImages.RunE +} + +func (c *BuildImagesCmd) BuildAndPushImages(cm installer.ConfigManager, im system.ImageManager) error { + config, err := cm.ParseConfigYaml(c.Opts.Config) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + if len(config.Codesphere.DeployConfig.Images) == 0 { + return fmt.Errorf("no images defined in the config") + } + if len(config.Registry.Server) == 0 { + return fmt.Errorf("registry server not defined in the config") + } + + v := &version.Build{} + codesphereVersion := v.Version() + + for imageName, imageConfig := range config.Codesphere.DeployConfig.Images { + for flavorName, flavorConfig := range imageConfig.Flavors { + log.Printf("Processing image '%s' with flavor '%s'", imageName, flavorName) + if flavorConfig.Image.Dockerfile == "" { + log.Printf("Skipping flavor '%s', no dockerfile defined", flavorName) + continue + } + + targetImage := fmt.Sprintf("%s/%s-%s:%s", config.Registry.Server, imageName, flavorName, codesphereVersion) + + err := im.BuildImage(flavorConfig.Image.Dockerfile, targetImage, ".") + if err != nil { + return fmt.Errorf("failed to build image %s: %w", targetImage, err) + } + + err = im.PushImage(targetImage) + if err != nil { + return fmt.Errorf("failed to push image %s: %w", targetImage, err) + } + + log.Printf("Successfully built and pushed image: %s", targetImage) + } + } + + return nil +} diff --git a/cli/cmd/build_images_test.go b/cli/cmd/build_images_test.go new file mode 100644 index 00000000..9d638035 --- /dev/null +++ b/cli/cmd/build_images_test.go @@ -0,0 +1,401 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd_test + +import ( + "errors" + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/codesphere-cloud/oms/cli/cmd" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/files" + "github.com/codesphere-cloud/oms/internal/system" +) + +const validConfigYaml = `registry: + server: "registry.example.com" +codesphere: + deployConfig: + images: + my-ubuntu-24.04: + name: "my-ubuntu-24.04" + supportedUntil: "2025-12-31" + flavors: + default: + image: + bomRef: "registry.example.com/my-ubuntu-24.04:latest" + dockerfile: "Dockerfile" + pool: + 1: 2 +` + +var _ = Describe("BuildImagesCmd", func() { + var ( + c cmd.BuildImagesCmd + opts *cmd.BuildImagesOpts + globalOpts cmd.GlobalOptions + mockEnv *env.MockEnv + ) + + BeforeEach(func() { + mockEnv = env.NewMockEnv(GinkgoT()) + globalOpts = cmd.GlobalOptions{} + opts = &cmd.BuildImagesOpts{ + GlobalOptions: &globalOpts, + Config: "", + } + c = cmd.BuildImagesCmd{ + Opts: opts, + Env: mockEnv, + } + }) + + AfterEach(func() { + mockEnv.AssertExpectations(GinkgoT()) + }) + + Context("RunE method", func() { + It("calls BuildAndPushImages and fails on config parsing", func() { + c.Opts.Config = "non-existent-config.yaml" + + err := c.RunE(nil, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to build and push images")) + }) + + It("succeeds with valid config and operations", func() { + tempConfigFile, err := os.CreateTemp("", "test-config.yaml") + Expect(err).To(BeNil()) + defer os.Remove(tempConfigFile.Name()) + + _, err = tempConfigFile.WriteString(validConfigYaml) + Expect(err).To(BeNil()) + tempConfigFile.Close() + + c.Opts.Config = tempConfigFile.Name() + + err = c.RunE(nil, []string{}) + // This will fail because the dockerfile doesn't exist and build will fail + // But it should at least parse the config successfully + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to build and push images")) + }) + }) + + Context("BuildAndPushImages method", func() { + It("fails when config manager fails to parse config", func() { + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Config = "non-existent-config.yaml" + mockConfigManager.EXPECT().ParseConfigYaml("non-existent-config.yaml").Return(files.RootConfig{}, errors.New("failed to parse config")) + + err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse config")) + }) + + It("fails when no images are defined in config", func() { + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Config = "empty-config.yaml" + emptyConfig := files.RootConfig{ + Codesphere: files.CodesphereConfig{ + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{}, + }, + }, + } + mockConfigManager.EXPECT().ParseConfigYaml("empty-config.yaml").Return(emptyConfig, nil) + + err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no images defined in the config")) + }) + + It("fails when registry server is empty", func() { + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Config = "config-without-registry.yaml" + configWithoutRegistry := files.RootConfig{ + Registry: files.RegistryConfig{ + // Empty server + }, + Codesphere: files.CodesphereConfig{ + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{ + "my-ubuntu-24.04": { + Name: "my-ubuntu-24.04", + SupportedUntil: "2025-12-31", + Flavors: map[string]files.FlavorConfig{ + "default": { + Image: files.ImageRef{ + BomRef: "my-ubuntu-24.04:latest", + Dockerfile: "Dockerfile", + }, + }, + }, + }, + }, + }, + }, + } + mockConfigManager.EXPECT().ParseConfigYaml("config-without-registry.yaml").Return(configWithoutRegistry, nil) + + err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("registry server not defined in the config")) + }) + + It("skips flavors without dockerfile", func() { + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Config = "config-without-dockerfile.yaml" + configWithoutDockerfile := files.RootConfig{ + Registry: files.RegistryConfig{ + Server: "registry.example.com", + }, + Codesphere: files.CodesphereConfig{ + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{ + "my-ubuntu-24.04": { + Name: "my-ubuntu-24.04", + SupportedUntil: "2025-12-31", + Flavors: map[string]files.FlavorConfig{ + "default": { + Image: files.ImageRef{ + BomRef: "registry.example.com/my-ubuntu-24.04:latest", + // No dockerfile specified + }, + }, + }, + }, + }, + }, + }, + } + mockConfigManager.EXPECT().ParseConfigYaml("config-without-dockerfile.yaml").Return(configWithoutDockerfile, nil) + + err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + Expect(err).To(BeNil()) + }) + + It("fails when image build fails", func() { + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Config = "config-with-dockerfile.yaml" + configWithDockerfile := files.RootConfig{ + Registry: files.RegistryConfig{ + Server: "registry.example.com", + }, + Codesphere: files.CodesphereConfig{ + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{ + "my-ubuntu-24.04": { + Name: "my-ubuntu-24.04", + SupportedUntil: "2025-12-31", + Flavors: map[string]files.FlavorConfig{ + "default": { + Image: files.ImageRef{ + BomRef: "registry.example.com/my-ubuntu-24.04:latest", + Dockerfile: "Dockerfile", + }, + }, + }, + }, + }, + }, + }, + } + mockConfigManager.EXPECT().ParseConfigYaml("config-with-dockerfile.yaml").Return(configWithDockerfile, nil) + mockImageManager.EXPECT().BuildImage("Dockerfile", "registry.example.com/my-ubuntu-24.04-default:0.0.0", ".").Return(errors.New("build failed")) + + err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to build image")) + }) + + It("fails when image push fails", func() { + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Config = "config-with-dockerfile.yaml" + configWithDockerfile := files.RootConfig{ + Registry: files.RegistryConfig{ + Server: "registry.example.com", + }, + Codesphere: files.CodesphereConfig{ + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{ + "my-ubuntu-24.04": { + Name: "my-ubuntu-24.04", + SupportedUntil: "2025-12-31", + Flavors: map[string]files.FlavorConfig{ + "default": { + Image: files.ImageRef{ + BomRef: "registry.example.com/my-ubuntu-24.04:latest", + Dockerfile: "Dockerfile", + }, + }, + }, + }, + }, + }, + }, + } + mockConfigManager.EXPECT().ParseConfigYaml("config-with-dockerfile.yaml").Return(configWithDockerfile, nil) + mockImageManager.EXPECT().BuildImage("Dockerfile", "registry.example.com/my-ubuntu-24.04-default:0.0.0", ".").Return(nil) + mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-default:0.0.0").Return(errors.New("push failed")) + + err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to push image")) + }) + + It("successfully builds and pushes single image", func() { + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Config = "config-with-dockerfile.yaml" + configWithDockerfile := files.RootConfig{ + Registry: files.RegistryConfig{ + Server: "registry.example.com", + }, + Codesphere: files.CodesphereConfig{ + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{ + "my-ubuntu-24.04": { + Name: "my-ubuntu-24.04", + SupportedUntil: "2025-12-31", + Flavors: map[string]files.FlavorConfig{ + "default": { + Image: files.ImageRef{ + BomRef: "registry.example.com/my-ubuntu-24.04:latest", + Dockerfile: "Dockerfile", + }, + }, + }, + }, + }, + }, + }, + } + mockConfigManager.EXPECT().ParseConfigYaml("config-with-dockerfile.yaml").Return(configWithDockerfile, nil) + mockImageManager.EXPECT().BuildImage("Dockerfile", "registry.example.com/my-ubuntu-24.04-default:0.0.0", ".").Return(nil) + mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-default:0.0.0").Return(nil) + + err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + Expect(err).To(BeNil()) + }) + + It("successfully builds and pushes multiple images with different flavors", func() { + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Config = "config-with-multiple-images.yaml" + configWithMultipleImages := files.RootConfig{ + Registry: files.RegistryConfig{ + Server: "registry.example.com", + }, + Codesphere: files.CodesphereConfig{ + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{ + "my-ubuntu-24.04": { + Name: "my-ubuntu-24.04", + SupportedUntil: "2025-12-31", + Flavors: map[string]files.FlavorConfig{ + "default": { + Image: files.ImageRef{ + BomRef: "registry.example.com/my-ubuntu-24.04:latest", + Dockerfile: "Dockerfile.default", + }, + }, + "minimal": { + Image: files.ImageRef{ + BomRef: "registry.example.com/my-ubuntu-24.04:minimal", + Dockerfile: "Dockerfile.minimal", + }, + }, + }, + }, + "my-alpine-3.18": { + Name: "my-alpine-3.18", + SupportedUntil: "2025-12-31", + Flavors: map[string]files.FlavorConfig{ + "default": { + Image: files.ImageRef{ + BomRef: "registry.example.com/my-alpine-3.18:latest", + Dockerfile: "Dockerfile.alpine", + }, + }, + }, + }, + }, + }, + }, + } + mockConfigManager.EXPECT().ParseConfigYaml("config-with-multiple-images.yaml").Return(configWithMultipleImages, nil) + + // Expect calls for my-ubuntu-24.04 default flavor + mockImageManager.EXPECT().BuildImage("Dockerfile.default", "registry.example.com/my-ubuntu-24.04-default:0.0.0", ".").Return(nil) + mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-default:0.0.0").Return(nil) + + // Expect calls for my-ubuntu-24.04 minimal flavor + mockImageManager.EXPECT().BuildImage("Dockerfile.minimal", "registry.example.com/my-ubuntu-24.04-minimal:0.0.0", ".").Return(nil) + mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-minimal:0.0.0").Return(nil) + + // Expect calls for my-alpine-3.18 default flavor + mockImageManager.EXPECT().BuildImage("Dockerfile.alpine", "registry.example.com/my-alpine-3.18-default:0.0.0", ".").Return(nil) + mockImageManager.EXPECT().PushImage("registry.example.com/my-alpine-3.18-default:0.0.0").Return(nil) + + err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + Expect(err).To(BeNil()) + }) + }) +}) + +var _ = Describe("AddBuildImagesCmd", func() { + var ( + parentCmd *cobra.Command + globalOpts *cmd.GlobalOptions + ) + + BeforeEach(func() { + parentCmd = &cobra.Command{Use: "build"} + globalOpts = &cmd.GlobalOptions{} + }) + + It("adds the images command with correct properties and flags", func() { + cmd.AddBuildImagesCmd(parentCmd, globalOpts) + + var imagesCmd *cobra.Command + for _, c := range parentCmd.Commands() { + if c.Use == "images" { + imagesCmd = c + break + } + } + + Expect(imagesCmd).NotTo(BeNil()) + Expect(imagesCmd.Use).To(Equal("images")) + Expect(imagesCmd.Short).To(Equal("Build and push container images")) + Expect(imagesCmd.Long).To(ContainSubstring("Build and push container images based on the configuration file")) + Expect(imagesCmd.RunE).NotTo(BeNil()) + + // Check flags + configFlag := imagesCmd.Flags().Lookup("config") + Expect(configFlag).NotTo(BeNil()) + Expect(configFlag.Shorthand).To(Equal("c")) + Expect(configFlag.Usage).To(ContainSubstring("Path to the configuration YAML file")) + }) +}) diff --git a/cli/cmd/extend_baseimage.go b/cli/cmd/extend_baseimage.go index 9cca02c3..3540d91d 100644 --- a/cli/cmd/extend_baseimage.go +++ b/cli/cmd/extend_baseimage.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "log" @@ -41,9 +42,11 @@ func (c *ExtendBaseimageCmd) RunE(_ *cobra.Command, args []string) error { } workdir := c.Env.GetOmsWorkdir() - p := installer.NewPackage(workdir, c.Opts.Package) + pm := installer.NewPackage(workdir, c.Opts.Package) + cm := installer.NewConfig() + im := system.NewImage(context.Background()) - err := c.ExtendBaseimage(p, args) + err := c.ExtendBaseimage(pm, cm, im, args) if err != nil { return fmt.Errorf("failed to extend baseimage: %w", err) } @@ -72,29 +75,33 @@ func AddExtendBaseimageCmd(extend *cobra.Command, opts *GlobalOptions) { baseimage.cmd.RunE = baseimage.RunE } -func (c *ExtendBaseimageCmd) ExtendBaseimage(p *installer.Package, args []string) error { +func (c *ExtendBaseimageCmd) ExtendBaseimage(pm installer.PackageManager, cm installer.ConfigManager, im system.ImageManager, args []string) error { baseImageTarPath := path.Join(baseimagePath, defaultBaseimage) - err := p.ExtractDependency(baseImageTarPath, c.Opts.Force) + err := pm.ExtractDependency(baseImageTarPath, c.Opts.Force) if err != nil { return fmt.Errorf("failed to extract package to workdir: %w", err) } - extractedBaseImagePath := p.GetDependencyPath(baseImageTarPath) - d := system.NewDockerEngine() + extractedBaseImagePath := pm.GetDependencyPath(baseImageTarPath) - imagenames, err := d.GetImageNames(p.FileIO, extractedBaseImagePath) + index, err := cm.ExtractOciImageIndex(baseImageTarPath) + if err != nil { + return fmt.Errorf("failed to extract OCI image index: %w", err) + } + + imagenames, err := index.ExtractImageNames() if err != nil || len(imagenames) == 0 { return fmt.Errorf("failed to read image tags: %w", err) } log.Println(imagenames) - err = tmpl.GenerateDockerfile(p.FileIO, c.Opts.Dockerfile, imagenames[0]) + err = tmpl.GenerateDockerfile(pm.FileIO(), c.Opts.Dockerfile, imagenames[0]) if err != nil { return fmt.Errorf("failed to generate dockerfile: %w", err) } log.Printf("Loading container image from package into local docker daemon: %s", extractedBaseImagePath) - err = d.LoadLocalContainerImage(extractedBaseImagePath) + err = im.LoadImage(extractedBaseImagePath) if err != nil { return fmt.Errorf("failed to load baseimage file %s: %w", baseImageTarPath, err) } diff --git a/cli/cmd/extend_baseimage_test.go b/cli/cmd/extend_baseimage_test.go index 4c345b08..759416e9 100644 --- a/cli/cmd/extend_baseimage_test.go +++ b/cli/cmd/extend_baseimage_test.go @@ -4,9 +4,7 @@ package cmd_test import ( - "archive/tar" - "compress/gzip" - "fmt" + "errors" "os" . "github.com/onsi/ginkgo/v2" @@ -16,49 +14,11 @@ import ( "github.com/codesphere-cloud/oms/cli/cmd" "github.com/codesphere-cloud/oms/internal/env" "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/files" + "github.com/codesphere-cloud/oms/internal/system" "github.com/codesphere-cloud/oms/internal/util" ) -// Helper function to create a test tar.gz file with binary data -func createTestTarGz(filename string, files map[string][]byte) error { - file, err := os.Create(filename) - if err != nil { - return err - } - defer func() { - _ = file.Close() - }() - - gzWriter := gzip.NewWriter(file) - defer func() { - _ = gzWriter.Close() - }() - - tarWriter := tar.NewWriter(gzWriter) - defer func() { - _ = tarWriter.Close() - }() - - for name, content := range files { - header := &tar.Header{ - Name: name, - Size: int64(len(content)), - Mode: 0644, - } - err := tarWriter.WriteHeader(header) - if err != nil { - return fmt.Errorf("failed to write header for file %q to tar: %w", name, err) - } - - _, err = tarWriter.Write(content) - if err != nil { - return fmt.Errorf("failed to write file %q to tar: %w", name, err) - } - } - - return nil -} - var _ = Describe("ExtendBaseimageCmd", func() { var ( c cmd.ExtendBaseimageCmd @@ -103,61 +63,132 @@ var _ = Describe("ExtendBaseimageCmd", func() { }) Context("ExtendBaseimage method", func() { - It("fails when package extraction fails due to missing package file", func() { - pkg := &installer.Package{ - OmsWorkdir: "/test/workdir", - Filename: "non-existent-package.tar.gz", - FileIO: &util.FilesystemWriter{}, - } + It("fails when package manager extraction fails", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(errors.New("extraction failed")) - err := c.ExtendBaseimage(pkg, []string{}) + err := c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to extract package to workdir")) }) - It("successfully extracts mocked package file", func() { - tempDir, err := os.MkdirTemp("", "oms-test-*") - Expect(err).To(BeNil()) - defer func() { - err := os.RemoveAll(tempDir) - Expect(err).To(BeNil()) - }() + It("fails when config manager fails to extract OCI image index", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) - origWd, err := os.Getwd() - Expect(err).To(BeNil()) - err = os.Chdir(tempDir) - Expect(err).To(BeNil()) - defer func() { - err := os.Chdir(origWd) - Expect(err).To(BeNil()) - }() - - depsFile := "deps.tar.gz" - depsFiles := map[string][]byte{ - "codesphere/images/workspace-agent-24.04.tar": []byte("fake container image content"), + mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(nil) + mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/workspace-agent-24.04.tar").Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar") + mockConfigManager.EXPECT().ExtractOciImageIndex("codesphere/images/workspace-agent-24.04.tar").Return(files.OCIImageIndex{}, errors.New("failed to extract index")) + + err := c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract OCI image index")) + }) + + It("fails when OCI image index has no image names", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + // Create empty OCI index that will return no image names + ociIndex := files.OCIImageIndex{ + Manifests: []files.ManifestEntry{}, } - err = createTestTarGz(depsFile, depsFiles) - Expect(err).To(BeNil()) - depsContent, err := os.ReadFile(depsFile) - Expect(err).To(BeNil()) + mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(nil) + mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/workspace-agent-24.04.tar").Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar") + mockConfigManager.EXPECT().ExtractOciImageIndex("codesphere/images/workspace-agent-24.04.tar").Return(ociIndex, nil) - testPackageFile := "test-package.tar.gz" - packageFiles := map[string][]byte{ - "deps.tar.gz": depsContent, + err := c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to read image tags")) + }) + + It("fails when image manager fails to load image", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + // Create OCI index with valid image names + ociIndex := files.OCIImageIndex{ + Manifests: []files.ManifestEntry{ + { + Annotations: map[string]string{ + "io.containerd.image.name": "ubuntu:24.04-base", + }, + }, + }, } - err = createTestTarGz(testPackageFile, packageFiles) + mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(nil) + mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/workspace-agent-24.04.tar").Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar") + mockConfigManager.EXPECT().ExtractOciImageIndex("codesphere/images/workspace-agent-24.04.tar").Return(ociIndex, nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + + // Create a temporary file for the Dockerfile generation to work with + tempFile, err := os.CreateTemp("", "dockerfile-test-*") Expect(err).To(BeNil()) + defer os.Remove(tempFile.Name()) + defer tempFile.Close() + + // Mock Dockerfile generation + mockFileIO.EXPECT().Create("Dockerfile").Return(tempFile, nil) + mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(errors.New("load failed")) + + err = c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to load baseimage file")) + }) + + It("uses force flag when extracting dependencies", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) c.Opts.Force = true + mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", true).Return(errors.New("extraction failed")) - pkg := &installer.Package{ - OmsWorkdir: tempDir, - Filename: testPackageFile, - FileIO: &util.FilesystemWriter{}, - } - err = c.ExtendBaseimage(pkg, []string{}) + err := c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to read image tags")) + Expect(err.Error()).To(ContainSubstring("failed to extract package to workdir")) + }) + + It("successfully completes workflow until dockerfile generation", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + // Create OCI index with valid image names + ociIndex := files.OCIImageIndex{ + Manifests: []files.ManifestEntry{ + { + Annotations: map[string]string{ + "io.containerd.image.name": "ubuntu:24.04-base", + }, + }, + }, + } + mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(nil) + mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/workspace-agent-24.04.tar").Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar") + mockConfigManager.EXPECT().ExtractOciImageIndex("codesphere/images/workspace-agent-24.04.tar").Return(ociIndex, nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + + // Create a temporary file for the Dockerfile generation to work with + tempFile, err := os.CreateTemp("", "dockerfile-test-*") + Expect(err).To(BeNil()) + defer os.Remove(tempFile.Name()) + defer tempFile.Close() + + // Mock Dockerfile generation + mockFileIO.EXPECT().Create("Dockerfile").Return(tempFile, nil) + mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(nil) + + err = c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + Expect(err).To(BeNil()) }) }) }) diff --git a/cli/cmd/install_codesphere.go b/cli/cmd/install_codesphere.go index 59b65794..b8740ef4 100644 --- a/cli/cmd/install_codesphere.go +++ b/cli/cmd/install_codesphere.go @@ -4,17 +4,21 @@ package cmd import ( + "context" "fmt" "log" "os" "os/exec" + "path" "path/filepath" "runtime" "slices" + "strings" "github.com/codesphere-cloud/cs-go/pkg/io" "github.com/codesphere-cloud/oms/internal/env" "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/system" "github.com/codesphere-cloud/oms/internal/util" "github.com/spf13/cobra" ) @@ -37,9 +41,11 @@ type InstallCodesphereOpts struct { func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, args []string) error { workdir := c.Env.GetOmsWorkdir() - p := installer.NewPackage(workdir, c.Opts.Package) + pm := installer.NewPackage(workdir, c.Opts.Package) + cm := installer.NewConfig() + im := system.NewImage(context.Background()) - err := c.ExtractAndInstall(p, runtime.GOOS, runtime.GOARCH) + err := c.ExtractAndInstall(pm, cm, im, runtime.GOOS, runtime.GOARCH) if err != nil { return fmt.Errorf("failed to extract and install package: %w", err) } @@ -72,17 +78,22 @@ func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) { codesphere.cmd.RunE = codesphere.RunE } -func (c *InstallCodesphereCmd) ExtractAndInstall(p *installer.Package, goos string, goarch string) error { +func (c *InstallCodesphereCmd) ExtractAndInstall(pm installer.PackageManager, cm installer.ConfigManager, im system.ImageManager, goos string, goarch string) error { if goos != "linux" || goarch != "amd64" { return fmt.Errorf("codesphere installation is only supported on Linux amd64. Current platform: %s/%s", goos, goarch) } - err := p.Extract(c.Opts.Force) + config, err := cm.ParseConfigYaml(c.Opts.Config) + if err != nil { + return fmt.Errorf("failed to extract config.yaml: %w", err) + } + + err = pm.Extract(c.Opts.Force) if err != nil { return fmt.Errorf("failed to extract package to workdir: %w", err) } - foundFiles, err := c.ListPackageContents(p) + foundFiles, err := c.ListPackageContents(pm) if err != nil { return fmt.Errorf("failed to list available files: %w", err) } @@ -97,7 +108,40 @@ func (c *InstallCodesphereCmd) ExtractAndInstall(p *installer.Package, goos stri return fmt.Errorf("node executable not found in package") } - nodePath := filepath.Join(".", p.GetWorkDir(), "node") + // If workspace image is extended extract bom.json and load workspace image + dockerfiles := config.ExtractWorkspaceDockerfiles() + if len(dockerfiles) > 0 { + err = pm.ExtractDependency("bom.json", c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to extract package to workdir: %w", err) + } + + for dockerfile, bomRef := range dockerfiles { + rootImageName := c.ExtractRootImageName(bomRef) + imagePath := filepath.Join("codesphere", "images", fmt.Sprintf("%s.tar", rootImageName)) + err = pm.ExtractDependency(imagePath, c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to extract root image %s: %w", imagePath, err) + } + + extractedImagePath := pm.GetDependencyPath(imagePath) + err = im.LoadImage(extractedImagePath) + if err != nil { + return fmt.Errorf("failed to load workspace image from Dockerfile %s: %w", dockerfile, err) + } + log.Printf("Loaded root image '%s'", extractedImagePath) + + dockerfileName := filepath.Base(dockerfile) + dockerfileDir := filepath.Dir(dockerfile) + err = im.BuildImage(dockerfileName, rootImageName, dockerfileDir) + if err != nil { + return fmt.Errorf("failed to build workspace image from Dockerfile %s: %w", dockerfile, err) + } + } + } + + // Install codesphere with node + nodePath := filepath.Join(".", pm.GetWorkDir(), "node") err = os.Chmod(nodePath, 0755) if err != nil { return fmt.Errorf("failed to make node executable: %w", err) @@ -105,10 +149,9 @@ func (c *InstallCodesphereCmd) ExtractAndInstall(p *installer.Package, goos stri log.Printf("Using Node.js executable: %s", nodePath) log.Println("Starting private cloud installer script...") - installerPath := filepath.Join(".", p.GetWorkDir(), "private-cloud-installer.js") - archivePath := filepath.Join(".", p.GetWorkDir(), "deps.tar.gz") + installerPath := filepath.Join(".", pm.GetWorkDir(), "private-cloud-installer.js") + archivePath := filepath.Join(".", pm.GetWorkDir(), "deps.tar.gz") - // Build command cmdArgs := []string{installerPath, "--archive", archivePath, "--config", c.Opts.Config, "--privKey", c.Opts.PrivKey} if len(c.Opts.SkipSteps) > 0 { for _, step := range c.Opts.SkipSteps { @@ -130,13 +173,13 @@ func (c *InstallCodesphereCmd) ExtractAndInstall(p *installer.Package, goos stri return nil } -func (c *InstallCodesphereCmd) ListPackageContents(p *installer.Package) ([]string, error) { - packageDir := p.GetWorkDir() - if !p.FileIO.Exists(packageDir) { +func (c *InstallCodesphereCmd) ListPackageContents(pm installer.PackageManager) ([]string, error) { + packageDir := pm.GetWorkDir() + if !pm.FileIO().Exists(packageDir) { return nil, fmt.Errorf("work dir not found: %s", packageDir) } - entries, err := p.FileIO.ReadDir(packageDir) + entries, err := pm.FileIO().ReadDir(packageDir) if err != nil { return nil, fmt.Errorf("failed to read directory contents: %w", err) } @@ -151,3 +194,12 @@ func (c *InstallCodesphereCmd) ListPackageContents(p *installer.Package) ([]stri return foundFiles, nil } + +func (c *InstallCodesphereCmd) ExtractRootImageName(bomRef string) string { + parts := strings.Split(bomRef, ":") + if len(parts) < 2 { + return bomRef + } + + return path.Base(parts[0]) +} diff --git a/cli/cmd/install_codesphere_test.go b/cli/cmd/install_codesphere_test.go index bcb04db7..c03bba10 100644 --- a/cli/cmd/install_codesphere_test.go +++ b/cli/cmd/install_codesphere_test.go @@ -4,6 +4,7 @@ package cmd_test import ( + "errors" "os" "runtime" @@ -14,6 +15,8 @@ import ( "github.com/codesphere-cloud/oms/cli/cmd" "github.com/codesphere-cloud/oms/internal/env" "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/files" + "github.com/codesphere-cloud/oms/internal/system" "github.com/codesphere-cloud/oms/internal/util" ) @@ -46,9 +49,19 @@ var _ = Describe("InstallCodesphereCmd", func() { Context("RunE method", func() { It("calls GetOmsWorkdir and fails on non-linux platform", func() { c.Opts.Package = "test-package.tar.gz" + + tempConfigFile, err := os.CreateTemp("", "test-config.yaml") + Expect(err).To(BeNil()) + defer os.Remove(tempConfigFile.Name()) + + _, err = tempConfigFile.WriteString("codesphere:\n deployConfig:\n images: {}\n") + Expect(err).To(BeNil()) + tempConfigFile.Close() + + c.Opts.Config = tempConfigFile.Name() mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") - err := c.RunE(nil, []string{}) + err = c.RunE(nil, []string{}) Expect(err).To(HaveOccurred()) if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" { @@ -56,254 +69,275 @@ var _ = Describe("InstallCodesphereCmd", func() { Expect(err.Error()).To(ContainSubstring("codesphere installation is only supported on Linux amd64")) } else { // On Linux amd64, it should fail on package extraction since the package doesn't exist - Expect(err.Error()).To(ContainSubstring("failed to extract package to workdir")) + Expect(err.Error()).To(ContainSubstring("failed to extract and install package")) } }) }) Context("ExtractAndInstall method", func() { It("fails on non-linux amd64 platforms", func() { - pkg := &installer.Package{ - OmsWorkdir: "/test/workdir", - Filename: "test-package.tar.gz", - FileIO: &util.FilesystemWriter{}, - } + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) // Test with Windows platform - err := c.ExtractAndInstall(pkg, "windows", "amd64") + err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "windows", "amd64") Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("codesphere installation is only supported on Linux amd64")) Expect(err.Error()).To(ContainSubstring("windows/amd64")) // Test with ARM64 architecture - err = c.ExtractAndInstall(pkg, "linux", "arm64") + err = c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "arm64") Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("codesphere installation is only supported on Linux amd64")) Expect(err.Error()).To(ContainSubstring("linux/arm64")) }) Context("when on Linux amd64", func() { + It("fails when config parsing fails", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Config = "invalid-config.yaml" + mockConfigManager.EXPECT().ParseConfigYaml("invalid-config.yaml").Return(files.RootConfig{}, errors.New("config parse error")) + + err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "amd64") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract config.yaml")) + }) + It("fails when package extraction fails", func() { - pkg := &installer.Package{ - OmsWorkdir: "/test/workdir", - Filename: "non-existent-package.tar.gz", - FileIO: &util.FilesystemWriter{}, - } + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) - err := c.ExtractAndInstall(pkg, "linux", "amd64") + c.Opts.Config = "valid-config.yaml" + mockConfigManager.EXPECT().ParseConfigYaml("valid-config.yaml").Return(files.RootConfig{}, nil) + mockPackageManager.EXPECT().Extract(false).Return(errors.New("extraction failed")) + + err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "amd64") Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to extract package to workdir")) }) + It("fails when package listing fails", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + c.Opts.Config = "valid-config.yaml" + mockConfigManager.EXPECT().ParseConfigYaml("valid-config.yaml").Return(files.RootConfig{}, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) + mockPackageManager.EXPECT().GetWorkDir().Return("/test/workdir/package") + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Exists("/test/workdir/package").Return(false) + + err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "amd64") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to list available files")) + }) + It("fails when deps.tar.gz is missing from package", func() { - tempDir, err := os.MkdirTemp("", "oms-test-*") - Expect(err).To(BeNil()) - defer func() { - err := os.RemoveAll(tempDir) - Expect(err).To(BeNil()) - }() - - origWd, err := os.Getwd() - Expect(err).To(BeNil()) - err = os.Chdir(tempDir) - Expect(err).To(BeNil()) - defer func() { - err := os.Chdir(origWd) - Expect(err).To(BeNil()) - }() - - // Create package without deps.tar.gz - testPackageFile := "test-package.tar.gz" - packageFiles := map[string][]byte{ - "node": []byte("fake node binary"), - "private-cloud-installer.js": []byte("console.log('installer');"), - "kubectl": []byte("fake kubectl binary"), - // deps.tar.gz missing - } - err = createTestTarGz(testPackageFile, packageFiles) - Expect(err).To(BeNil()) - - c.Opts.Force = true - pkg := &installer.Package{ - OmsWorkdir: tempDir, - Filename: testPackageFile, - FileIO: &util.FilesystemWriter{}, + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + c.Opts.Config = "valid-config.yaml" + mockConfigManager.EXPECT().ParseConfigYaml("valid-config.yaml").Return(files.RootConfig{}, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) + mockPackageManager.EXPECT().GetWorkDir().Return("/test/workdir/package") + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Exists("/test/workdir/package").Return(true) + + // Create mock directory entries without deps.tar.gz + mockEntries := []os.DirEntry{ + &MockDirEntry{name: "node", isDir: false}, + &MockDirEntry{name: "private-cloud-installer.js", isDir: false}, + &MockDirEntry{name: "kubectl", isDir: false}, } + mockFileIO.EXPECT().ReadDir("/test/workdir/package").Return(mockEntries, nil) - err = c.ExtractAndInstall(pkg, "linux", "amd64") + err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "amd64") Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("deps.tar.gz not found in package")) }) It("fails when private-cloud-installer.js is missing from package", func() { - tempDir, err := os.MkdirTemp("", "oms-test-*") - Expect(err).To(BeNil()) - defer func() { - err := os.RemoveAll(tempDir) - Expect(err).To(BeNil()) - }() - - origWd, err := os.Getwd() - Expect(err).To(BeNil()) - err = os.Chdir(tempDir) - Expect(err).To(BeNil()) - defer func() { - err := os.Chdir(origWd) - Expect(err).To(BeNil()) - }() - - // Create package without private-cloud-installer.js - testPackageFile := "test-package.tar.gz" - packageFiles := map[string][]byte{ - "deps.tar.gz": []byte("fake deps archive"), - "node": []byte("fake node binary"), - "kubectl": []byte("fake kubectl binary"), - // private-cloud-installer.js missing - } - err = createTestTarGz(testPackageFile, packageFiles) - Expect(err).To(BeNil()) - - c.Opts.Force = true - pkg := &installer.Package{ - OmsWorkdir: tempDir, - Filename: testPackageFile, - FileIO: &util.FilesystemWriter{}, + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + c.Opts.Config = "valid-config.yaml" + mockConfigManager.EXPECT().ParseConfigYaml("valid-config.yaml").Return(files.RootConfig{}, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) + mockPackageManager.EXPECT().GetWorkDir().Return("/test/workdir/package") + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Exists("/test/workdir/package").Return(true) + + // Create mock directory entries without private-cloud-installer.js + mockEntries := []os.DirEntry{ + &MockDirEntry{name: "deps.tar.gz", isDir: false}, + &MockDirEntry{name: "node", isDir: false}, + &MockDirEntry{name: "kubectl", isDir: false}, } + mockFileIO.EXPECT().ReadDir("/test/workdir/package").Return(mockEntries, nil) - err = c.ExtractAndInstall(pkg, "linux", "amd64") + err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "amd64") Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("private-cloud-installer.js not found in package")) }) It("fails when node executable is missing from package", func() { - tempDir, err := os.MkdirTemp("", "oms-test-*") - Expect(err).To(BeNil()) - defer func() { - err := os.RemoveAll(tempDir) - Expect(err).To(BeNil()) - }() - - origWd, err := os.Getwd() - Expect(err).To(BeNil()) - err = os.Chdir(tempDir) - Expect(err).To(BeNil()) - defer func() { - err := os.Chdir(origWd) - Expect(err).To(BeNil()) - }() - - // Create package without node executable - testPackageFile := "test-package.tar.gz" - packageFiles := map[string][]byte{ - "deps.tar.gz": []byte("fake deps archive"), - "private-cloud-installer.js": []byte("console.log('installer');"), - "kubectl": []byte("fake kubectl binary"), - // node missing - } - err = createTestTarGz(testPackageFile, packageFiles) - Expect(err).To(BeNil()) - - c.Opts.Force = true - pkg := &installer.Package{ - OmsWorkdir: tempDir, - Filename: testPackageFile, - FileIO: &util.FilesystemWriter{}, + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + c.Opts.Config = "valid-config.yaml" + mockConfigManager.EXPECT().ParseConfigYaml("valid-config.yaml").Return(files.RootConfig{}, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) + mockPackageManager.EXPECT().GetWorkDir().Return("/test/workdir/package") + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Exists("/test/workdir/package").Return(true) + + // Create mock directory entries without node executable + mockEntries := []os.DirEntry{ + &MockDirEntry{name: "deps.tar.gz", isDir: false}, + &MockDirEntry{name: "private-cloud-installer.js", isDir: false}, + &MockDirEntry{name: "kubectl", isDir: false}, } + mockFileIO.EXPECT().ReadDir("/test/workdir/package").Return(mockEntries, nil) - err = c.ExtractAndInstall(pkg, "linux", "amd64") + err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "amd64") Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("node executable not found in package")) }) - It("successfully extracts package with all required files but fails on execution", func() { - tempDir, err := os.MkdirTemp("", "oms-test-*") - Expect(err).To(BeNil()) - defer func() { - err := os.RemoveAll(tempDir) - Expect(err).To(BeNil()) - }() - - origWd, err := os.Getwd() - Expect(err).To(BeNil()) - err = os.Chdir(tempDir) - Expect(err).To(BeNil()) - defer func() { - err := os.Chdir(origWd) - Expect(err).To(BeNil()) - }() - - // Create complete package with all required files - testPackageFile := "test-package.tar.gz" - packageFiles := map[string][]byte{ - "deps.tar.gz": []byte("fake deps archive"), - "node": []byte("fake node binary that will fail to execute"), - "private-cloud-installer.js": []byte("console.log('installer');"), - "kubectl": []byte("fake kubectl binary"), + It("successfully validates all required files but fails on execution", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + c.Opts.Config = "valid-config.yaml" + c.Opts.PrivKey = "test-key.pem" + mockConfigManager.EXPECT().ParseConfigYaml("valid-config.yaml").Return(files.RootConfig{}, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) + mockPackageManager.EXPECT().GetWorkDir().Return("/test/workdir/package") + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Exists("/test/workdir/package").Return(true) + + // Create complete mock directory entries with all required files + mockEntries := []os.DirEntry{ + &MockDirEntry{name: "deps.tar.gz", isDir: false}, + &MockDirEntry{name: "node", isDir: false}, + &MockDirEntry{name: "private-cloud-installer.js", isDir: false}, + &MockDirEntry{name: "kubectl", isDir: false}, + } + mockFileIO.EXPECT().ReadDir("/test/workdir/package").Return(mockEntries, nil) + + err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "amd64") + Expect(err).To(HaveOccurred()) + // Should fail when trying to make fake node executable + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to make node executable")) + }) + + It("successfully builds and pushes custom workspace images", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockConfigManager := installer.NewMockConfigManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + c.Opts.Config = "valid-config.yaml" + c.Opts.PrivKey = "test-key.pem" + + // Create config with workspace dockerfiles + config := files.RootConfig{ + Codesphere: files.CodesphereConfig{ + DeployConfig: files.DeployConfig{ + Images: map[string]files.ImageConfig{ + "ubuntu-24.04": { + Flavors: map[string]files.FlavorConfig{ + "default": { + Image: files.ImageRef{ + BomRef: "docker.io/library/ubuntu:24.04", + Dockerfile: "workspace.Dockerfile", + }, + }, + }, + }, + }, + }, + }, } - err = createTestTarGz(testPackageFile, packageFiles) - Expect(err).To(BeNil()) - - c.Opts.Force = true - pkg := &installer.Package{ - OmsWorkdir: tempDir, - Filename: testPackageFile, - FileIO: &util.FilesystemWriter{}, + + mockConfigManager.EXPECT().ParseConfigYaml("valid-config.yaml").Return(config, nil) + mockPackageManager.EXPECT().Extract(false).Return(nil) + mockPackageManager.EXPECT().GetWorkDir().Return("/test/workdir/package") + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Exists("/test/workdir/package").Return(true) + + // Create complete mock directory entries with all required files + mockEntries := []os.DirEntry{ + &MockDirEntry{name: "deps.tar.gz", isDir: false}, + &MockDirEntry{name: "node", isDir: false}, + &MockDirEntry{name: "private-cloud-installer.js", isDir: false}, + &MockDirEntry{name: "kubectl", isDir: false}, } + mockFileIO.EXPECT().ReadDir("/test/workdir/package").Return(mockEntries, nil) - err = c.ExtractAndInstall(pkg, "linux", "amd64") + mockPackageManager.EXPECT().ExtractDependency("bom.json", false).Return(nil) + mockPackageManager.EXPECT().ExtractDependency("codesphere/images/ubuntu.tar", false).Return(nil) + mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/ubuntu.tar").Return("/test/workdir/deps/codesphere/images/ubuntu.tar") + mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/ubuntu.tar").Return(nil) + mockImageManager.EXPECT().BuildImage("workspace.Dockerfile", "ubuntu", ".").Return(nil) + + err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "amd64") + // Should fail when trying to make fake node executable Expect(err).To(HaveOccurred()) - // Should fail when trying to chmod or execute the fake node binary - Expect(err.Error()).To(SatisfyAny( - ContainSubstring("failed to make node executable"), - ContainSubstring("failed to run installer script"), - )) + Expect(err.Error()).To(ContainSubstring("failed to make node executable")) }) }) }) Context("listPackageContents method", func() { It("fails when work directory doesn't exist", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockFileIO := util.NewMockFileIO(GinkgoT()) - pkg := &installer.Package{ - OmsWorkdir: "/test/workdir", - Filename: "test-package.tar.gz", - FileIO: mockFileIO, - } - mockFileIO.EXPECT().Exists("/test/workdir/test-package").Return(false) + mockPackageManager.EXPECT().GetWorkDir().Return("/test/workdir/package") + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Exists("/test/workdir/package").Return(false) - filenames, err := c.ListPackageContents(pkg) + filenames, err := c.ListPackageContents(mockPackageManager) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("work dir not found")) Expect(filenames).To(BeNil()) - mockFileIO.AssertExpectations(GinkgoT()) }) It("fails when ReadDir fails", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockFileIO := util.NewMockFileIO(GinkgoT()) - pkg := &installer.Package{ - OmsWorkdir: "/test/workdir", - Filename: "test-package.tar.gz", - FileIO: mockFileIO, - } - mockFileIO.EXPECT().Exists("/test/workdir/test-package").Return(true) - mockFileIO.EXPECT().ReadDir("/test/workdir/test-package").Return(nil, os.ErrPermission) + mockPackageManager.EXPECT().GetWorkDir().Return("/test/workdir/package") + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Exists("/test/workdir/package").Return(true) + mockFileIO.EXPECT().ReadDir("/test/workdir/package").Return(nil, os.ErrPermission) - filenames, err := c.ListPackageContents(pkg) + filenames, err := c.ListPackageContents(mockPackageManager) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to read directory contents")) Expect(filenames).To(BeNil()) - mockFileIO.AssertExpectations(GinkgoT()) }) It("successfully lists package contents", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockFileIO := util.NewMockFileIO(GinkgoT()) - pkg := &installer.Package{ - OmsWorkdir: "/test/workdir", - Filename: "test-package.tar.gz", - FileIO: mockFileIO, - } // Create mock directory entries mockEntries := []os.DirEntry{ @@ -313,17 +347,18 @@ var _ = Describe("InstallCodesphereCmd", func() { &MockDirEntry{name: "kubectl", isDir: false}, } - mockFileIO.EXPECT().Exists("/test/workdir/test-package").Return(true) - mockFileIO.EXPECT().ReadDir("/test/workdir/test-package").Return(mockEntries, nil) + mockPackageManager.EXPECT().GetWorkDir().Return("/test/workdir/package") + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Exists("/test/workdir/package").Return(true) + mockFileIO.EXPECT().ReadDir("/test/workdir/package").Return(mockEntries, nil) - filenames, err := c.ListPackageContents(pkg) + filenames, err := c.ListPackageContents(mockPackageManager) Expect(err).To(BeNil()) Expect(filenames).To(HaveLen(4)) Expect(filenames).To(ContainElement("deps.tar.gz")) Expect(filenames).To(ContainElement("node")) Expect(filenames).To(ContainElement("private-cloud-installer.js")) Expect(filenames).To(ContainElement("kubectl")) - mockFileIO.AssertExpectations(GinkgoT()) }) }) }) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 2264f7d6..5010cddb 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -35,6 +35,7 @@ func GetRootCmd() *cobra.Command { AddListCmd(rootCmd, opts) AddDownloadCmd(rootCmd, opts) AddInstallCmd(rootCmd, &opts) + AddBuildCmd(rootCmd, &opts) AddLicensesCmd(rootCmd) // OMS API key management commands diff --git a/go.mod b/go.mod index bb4ffc20..8cbe5533 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/rhysd/go-github-selfupdate v1.2.3 github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -45,5 +46,4 @@ require ( golang.org/x/text v0.29.0 // indirect golang.org/x/tools v0.37.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/installer/config.go b/internal/installer/config.go new file mode 100644 index 00000000..0a813b06 --- /dev/null +++ b/internal/installer/config.go @@ -0,0 +1,51 @@ +package installer + +import ( + "fmt" + "path/filepath" + + "github.com/codesphere-cloud/oms/internal/installer/files" + "github.com/codesphere-cloud/oms/internal/util" +) + +type Config struct { + FileIO util.FileIO +} + +type ConfigManager interface { + ParseConfigYaml(configPath string) (files.RootConfig, error) + ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) +} + +func NewConfig() *Config { + return &Config{ + FileIO: &util.FilesystemWriter{}, + } +} + +// ParseConfigYaml reads and parses the configuration YAML file at the given path. +func (c *Config) ParseConfigYaml(configPath string) (files.RootConfig, error) { + var rootConfig files.RootConfig + err := rootConfig.ParseConfig(configPath) + if err != nil { + return rootConfig, fmt.Errorf("failed to extract config.yaml: %w", err) + } + + return rootConfig, nil +} + +// ExtractOciImageIndex extracts and parses the OCI image index from the given image file path. +func (c *Config) ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) { + var ociImageIndex files.OCIImageIndex + err := util.ExtractTarSingleFile(c.FileIO, imagefile, "index.json", filepath.Dir(imagefile)) + if err != nil { + return ociImageIndex, fmt.Errorf("failed to extract index.json: %w", err) + } + + err = ociImageIndex.ParseOCIImageConfig(imagefile) + if err != nil { + return ociImageIndex, fmt.Errorf("failed to parse OCI image config: %w", err) + } + + return ociImageIndex, nil +} diff --git a/internal/installer/config_test.go b/internal/installer/config_test.go new file mode 100644 index 00000000..dce139d7 --- /dev/null +++ b/internal/installer/config_test.go @@ -0,0 +1,398 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package installer_test + +import ( + "archive/tar" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/util" +) + +var _ = Describe("Config", func() { + var ( + config *installer.Config + ) + + BeforeEach(func() { + config = installer.NewConfig() + }) + + Describe("NewConfig", func() { + It("creates a new Config with FilesystemWriter", func() { + newConfig := installer.NewConfig() + Expect(newConfig).ToNot(BeNil()) + Expect(newConfig.FileIO).ToNot(BeNil()) + Expect(newConfig.FileIO).To(BeAssignableToTypeOf(&util.FilesystemWriter{})) + }) + }) + + Describe("ParseConfigYaml", func() { + Context("when config file exists and is valid", func() { + It("successfully parses the configuration", func() { + tempDir := GinkgoT().TempDir() + tempConfigFile := filepath.Join(tempDir, "config.yaml") + + validConfigContent := ` +registry: + server: "registry.example.com" +codesphere: + deployConfig: + images: + ubuntu-24.04: + name: "ubuntu-24.04" + supportedUntil: "2025-12-31" + flavors: + default: + image: + bomRef: "ubuntu:24.04" + dockerfile: "Dockerfile" +` + err := os.WriteFile(tempConfigFile, []byte(validConfigContent), 0644) + Expect(err).ToNot(HaveOccurred()) + + rootConfig, err := config.ParseConfigYaml(tempConfigFile) + + Expect(err).ToNot(HaveOccurred()) + Expect(rootConfig.Registry.Server).To(Equal("registry.example.com")) + Expect(rootConfig.Codesphere.DeployConfig.Images).To(HaveKey("ubuntu-24.04")) + }) + }) + + Context("when config file does not exist", func() { + It("returns an error", func() { + nonExistentFile := "/path/to/nonexistent/config.yaml" + + _, err := config.ParseConfigYaml(nonExistentFile) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract config.yaml")) + }) + }) + + Context("when config file has invalid YAML", func() { + It("returns an error", func() { + tempDir := GinkgoT().TempDir() + tempConfigFile := filepath.Join(tempDir, "invalid-config.yaml") + + invalidConfigContent := ` +registry: + server: "registry.example.com" + username: "testuser" + password: "testpass" +codesphere: + deploy_config: + images: + ubuntu-24.04: + name: "ubuntu-24.04" + supported_until: "2025-12-31" + flavors: + default: + image: + bom_ref: "ubuntu:24.04" + dockerfile: "Dockerfile" +invalid_yaml: [unclosed_bracket +` + err := os.WriteFile(tempConfigFile, []byte(invalidConfigContent), 0644) + Expect(err).ToNot(HaveOccurred()) + + _, err = config.ParseConfigYaml(tempConfigFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract config.yaml")) + }) + }) + + Context("when config file is empty", func() { + It("returns empty config without error", func() { + tempDir := GinkgoT().TempDir() + tempConfigFile := filepath.Join(tempDir, "empty-config.yaml") + + err := os.WriteFile(tempConfigFile, []byte(""), 0644) + Expect(err).ToNot(HaveOccurred()) + + rootConfig, err := config.ParseConfigYaml(tempConfigFile) + + Expect(err).ToNot(HaveOccurred()) + Expect(rootConfig.Registry.Server).To(BeEmpty()) + Expect(rootConfig.Codesphere.DeployConfig.Images).To(BeEmpty()) + }) + }) + }) + + Describe("ExtractOciImageIndex", func() { + Context("with real filesystem operations", func() { + var ( + tempDir string + imageFile string + ) + + BeforeEach(func() { + config = installer.NewConfig() // Use real FileIO + tempDir = GinkgoT().TempDir() + imageFile = filepath.Join(tempDir, "test-image.tar") + }) + + Context("when image file does not exist", func() { + It("returns an error", func() { + _, err := config.ExtractOciImageIndex(imageFile) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + + Context("when image file is empty", func() { + It("returns an error", func() { + // Create empty tar file + err := os.WriteFile(imageFile, []byte(""), 0644) + Expect(err).ToNot(HaveOccurred()) + + _, err = config.ExtractOciImageIndex(imageFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + + Context("when image file is a directory", func() { + It("returns an error", func() { + // Create directory instead of file + err := os.Mkdir(imageFile, 0755) + Expect(err).ToNot(HaveOccurred()) + + _, err = config.ExtractOciImageIndex(imageFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + + Context("when index.json file doesn't exist after extraction", func() { + It("returns an error", func() { + // Create a minimal tar file without index.json + err := createTar(imageFile, "not_index.json", "fake content") + Expect(err).ToNot(HaveOccurred()) + + _, err = config.ExtractOciImageIndex(imageFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + + Context("when tar contains valid index.json", func() { + It("successfully extracts and parses OCI image index", func() { + // Create a tar file with a valid index.json + validIndex := `{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 1234, + "digest": "sha256:abc123def456" + } + ] + }` + err := createTar(imageFile, "index.json", validIndex) + Expect(err).ToNot(HaveOccurred()) + + ociImageIndex, err := config.ExtractOciImageIndex(imageFile) + Expect(err).ToNot(HaveOccurred()) + Expect(ociImageIndex.SchemaVersion).To(Equal(2)) + Expect(ociImageIndex.MediaType).To(Equal("application/vnd.oci.image.index.v1+json")) + Expect(ociImageIndex.Manifests).To(HaveLen(1)) + Expect(ociImageIndex.Manifests[0].Digest).To(Equal("sha256:abc123def456")) + Expect(ociImageIndex.Manifests[0].Size).To(Equal(int64(1234))) + }) + }) + + Context("when index.json has invalid JSON", func() { + It("returns an error", func() { + // Create a tar file with invalid JSON in index.json + invalidIndex := `{ + "schemaVersion": 2, + "manifests": [ + { + "size": "invalid_json_here", + ` + err := createTar(imageFile, "index.json", invalidIndex) + Expect(err).ToNot(HaveOccurred()) + + _, err = config.ExtractOciImageIndex(imageFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse OCI image config")) + }) + }) + }) + }) + + Describe("ConfigManager interface", func() { + It("implements ConfigManager interface", func() { + var configManager installer.ConfigManager = config + Expect(configManager).ToNot(BeNil()) + }) + + It("has all required methods", func() { + var configManager installer.ConfigManager = config + + // Test that methods exist by calling them with invalid parameters + // and checking that we get errors (proving the methods are callable) + _, err1 := configManager.ParseConfigYaml("/nonexistent/path") + Expect(err1).To(HaveOccurred()) + + _, err2 := configManager.ExtractOciImageIndex("/nonexistent/path") + Expect(err2).To(HaveOccurred()) + }) + }) + + Describe("Config struct fields", func() { + It("has FileIO field", func() { + Expect(config.FileIO).ToNot(BeNil()) + }) + + It("allows FileIO to be replaced with mock", func() { + mockFileIO := util.NewMockFileIO(GinkgoT()) + config.FileIO = mockFileIO + Expect(config.FileIO).To(Equal(mockFileIO)) + }) + }) + + Describe("Error handling and edge cases", func() { + Context("ParseConfigYaml with various file permissions", func() { + It("handles file with read permissions", func() { + tempDir := GinkgoT().TempDir() + tempConfigFile := filepath.Join(tempDir, "config.yaml") + + validConfigContent := ` +registry: + server: "registry.example.com" +` + err := os.WriteFile(tempConfigFile, []byte(validConfigContent), 0644) + Expect(err).ToNot(HaveOccurred()) + + _, err = config.ParseConfigYaml(tempConfigFile) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("ExtractOciImageIndex with various scenarios", func() { + var tempDir string + + BeforeEach(func() { + tempDir = GinkgoT().TempDir() + }) + + It("handles empty image file path", func() { + _, err := config.ExtractOciImageIndex("") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + + It("handles directory instead of file", func() { + dirPath := filepath.Join(tempDir, "not-a-file") + err := os.Mkdir(dirPath, 0755) + Expect(err).ToNot(HaveOccurred()) + + _, err = config.ExtractOciImageIndex(dirPath) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + }) + + Describe("Integration scenarios", func() { + Context("full workflow simulation", func() { + It("can parse complex configuration successfully", func() { + tempDir := GinkgoT().TempDir() + configFile := filepath.Join(tempDir, "config.yaml") + + // Create a realistic config file + configContent := ` +registry: + server: "my-registry.example.com" +codesphere: + deployConfig: + images: + ubuntu-24.04: + name: "ubuntu-24.04" + supportedUntil: "2025-12-31" + flavors: + default: + image: + bomRef: "ubuntu:24.04" + dockerfile: "Dockerfile" + minimal: + image: + bomRef: "ubuntu:24.04-minimal" + dockerfile: "Dockerfile.minimal" + alpine-3.18: + name: "alpine-3.18" + supportedUntil: "2024-12-31" + flavors: + default: + image: + bomRef: "alpine:3.18" + dockerfile: "Dockerfile.alpine" +` + err := os.WriteFile(configFile, []byte(configContent), 0644) + Expect(err).ToNot(HaveOccurred()) + + rootConfig, err := config.ParseConfigYaml(configFile) + + Expect(err).ToNot(HaveOccurred()) + Expect(rootConfig.Registry.Server).To(Equal("my-registry.example.com")) + + // Check images + Expect(rootConfig.Codesphere.DeployConfig.Images).To(HaveLen(2)) + Expect(rootConfig.Codesphere.DeployConfig.Images).To(HaveKey("ubuntu-24.04")) + Expect(rootConfig.Codesphere.DeployConfig.Images).To(HaveKey("alpine-3.18")) + + // Check ubuntu image details + ubuntuImage := rootConfig.Codesphere.DeployConfig.Images["ubuntu-24.04"] + Expect(ubuntuImage.Name).To(Equal("ubuntu-24.04")) + Expect(ubuntuImage.SupportedUntil).To(Equal("2025-12-31")) + Expect(ubuntuImage.Flavors).To(HaveLen(2)) + Expect(ubuntuImage.Flavors).To(HaveKey("default")) + Expect(ubuntuImage.Flavors).To(HaveKey("minimal")) + + // Check alpine image details + alpineImage := rootConfig.Codesphere.DeployConfig.Images["alpine-3.18"] + Expect(alpineImage.Name).To(Equal("alpine-3.18")) + Expect(alpineImage.SupportedUntil).To(Equal("2024-12-31")) + Expect(alpineImage.Flavors).To(HaveLen(1)) + Expect(alpineImage.Flavors).To(HaveKey("default")) + }) + }) + }) +}) + +// 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) + if err != nil { + return err + } + defer file.Close() + + tw := tar.NewWriter(file) + defer tw.Close() + + // Add index.json file + header := &tar.Header{ + Name: fileName, + Mode: 0644, + Size: int64(len(fileContent)), + } + if err := tw.WriteHeader(header); err != nil { + return err + } + if _, err := tw.Write([]byte(fileContent)); err != nil { + return err + } + + return nil +} diff --git a/internal/installer/files/config_yaml.go b/internal/installer/files/config_yaml.go new file mode 100644 index 00000000..041324ed --- /dev/null +++ b/internal/installer/files/config_yaml.go @@ -0,0 +1,81 @@ +package files + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +// RootConfig represents the relevant parts of the configuration file +type RootConfig struct { + Registry RegistryConfig `yaml:"registry"` + Codesphere CodesphereConfig `yaml:"codesphere"` +} + +type RegistryConfig struct { + Server string `yaml:"server"` +} + +type CodesphereConfig struct { + DeployConfig DeployConfig `yaml:"deployConfig"` +} + +type DeployConfig struct { + Images map[string]ImageConfig `yaml:"images"` +} + +type ImageConfig struct { + Name string `yaml:"name"` + SupportedUntil string `yaml:"supportedUntil"` + Flavors map[string]FlavorConfig `yaml:"flavors"` +} + +type FlavorConfig struct { + Image ImageRef `yaml:"image"` + Pool map[int]int `yaml:"pool"` +} + +type ImageRef struct { + BomRef string `yaml:"bomRef"` + Dockerfile string `yaml:"dockerfile"` +} + +func (c *RootConfig) ParseConfig(filePath string) error { + configData, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + err = yaml.Unmarshal(configData, c) + if err != nil { + return fmt.Errorf("failed to parse YAML config: %w", err) + } + + return nil +} + +func (c *RootConfig) ExtractBomRefs() []string { + var bomRefs []string + for _, imageConfig := range c.Codesphere.DeployConfig.Images { + for _, flavor := range imageConfig.Flavors { + if flavor.Image.BomRef != "" { + bomRefs = append(bomRefs, flavor.Image.BomRef) + } + } + } + + return bomRefs +} + +func (c *RootConfig) ExtractWorkspaceDockerfiles() map[string]string { + dockerfiles := make(map[string]string) + for _, imageConfig := range c.Codesphere.DeployConfig.Images { + for _, flavor := range imageConfig.Flavors { + if flavor.Image.Dockerfile != "" { + dockerfiles[flavor.Image.Dockerfile] = flavor.Image.BomRef + } + } + } + return dockerfiles +} diff --git a/internal/installer/files/oci_image_index.go b/internal/installer/files/oci_image_index.go new file mode 100644 index 00000000..4e63f9dd --- /dev/null +++ b/internal/installer/files/oci_image_index.go @@ -0,0 +1,55 @@ +package files + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/codesphere-cloud/oms/internal/util" +) + +// OCIImageIndex represents the top-level structure of an OCI Image Index (manifest list). +type OCIImageIndex struct { + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType"` + Manifests []ManifestEntry `json:"manifests"` +} + +// ManifestEntry represents a single manifest entry within the index. +type ManifestEntry struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + Size int64 `json:"size"` + Annotations map[string]string `json:"annotations,omitempty"` // Use omitempty just in case, though usually present +} + +func (o *OCIImageIndex) ParseOCIImageConfig(filePath string) error { + indexfile := filepath.Join(filepath.Dir(filePath), "index.json") + file, err := os.Open(indexfile) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", indexfile, err) + } + defer util.CloseFileIgnoreError(file) + + decoder := json.NewDecoder(file) + err = decoder.Decode(o) + if err != nil { + return fmt.Errorf("failed to decode file %s: %w", indexfile, err) + } + + return nil +} + +// ExtractImageNames extracts the image names from the OCI image index file. +func (o *OCIImageIndex) ExtractImageNames() ([]string, error) { + var names []string + for _, manifest := range o.Manifests { + name := manifest.Annotations["io.containerd.image.name"] + if name == "" { + continue + } + names = append(names, name) + } + return names, nil +} diff --git a/internal/installer/installer_suite_test.go b/internal/installer/installer_suite_test.go new file mode 100644 index 00000000..b1184b7c --- /dev/null +++ b/internal/installer/installer_suite_test.go @@ -0,0 +1,16 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package installer_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestInstaller(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Installer Suite") +} diff --git a/internal/installer/mocks.go b/internal/installer/mocks.go new file mode 100644 index 00000000..869382db --- /dev/null +++ b/internal/installer/mocks.go @@ -0,0 +1,399 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package installer + +import ( + "github.com/codesphere-cloud/oms/internal/installer/files" + "github.com/codesphere-cloud/oms/internal/util" + mock "github.com/stretchr/testify/mock" +) + +// NewMockConfigManager creates a new instance of MockConfigManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockConfigManager(t interface { + mock.TestingT + Cleanup(func()) +}) *MockConfigManager { + mock := &MockConfigManager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockConfigManager is an autogenerated mock type for the ConfigManager type +type MockConfigManager struct { + mock.Mock +} + +type MockConfigManager_Expecter struct { + mock *mock.Mock +} + +func (_m *MockConfigManager) EXPECT() *MockConfigManager_Expecter { + return &MockConfigManager_Expecter{mock: &_m.Mock} +} + +// ExtractOciImageIndex provides a mock function for the type MockConfigManager +func (_mock *MockConfigManager) ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) { + ret := _mock.Called(imagefile) + + if len(ret) == 0 { + panic("no return value specified for ExtractOciImageIndex") + } + + var r0 files.OCIImageIndex + var r1 error + if returnFunc, ok := ret.Get(0).(func(string) (files.OCIImageIndex, error)); ok { + return returnFunc(imagefile) + } + if returnFunc, ok := ret.Get(0).(func(string) files.OCIImageIndex); ok { + r0 = returnFunc(imagefile) + } else { + r0 = ret.Get(0).(files.OCIImageIndex) + } + if returnFunc, ok := ret.Get(1).(func(string) error); ok { + r1 = returnFunc(imagefile) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockConfigManager_ExtractOciImageIndex_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExtractOciImageIndex' +type MockConfigManager_ExtractOciImageIndex_Call struct { + *mock.Call +} + +// ExtractOciImageIndex is a helper method to define mock.On call +// - imagefile +func (_e *MockConfigManager_Expecter) ExtractOciImageIndex(imagefile interface{}) *MockConfigManager_ExtractOciImageIndex_Call { + return &MockConfigManager_ExtractOciImageIndex_Call{Call: _e.mock.On("ExtractOciImageIndex", imagefile)} +} + +func (_c *MockConfigManager_ExtractOciImageIndex_Call) Run(run func(imagefile string)) *MockConfigManager_ExtractOciImageIndex_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockConfigManager_ExtractOciImageIndex_Call) Return(oCIImageIndex files.OCIImageIndex, err error) *MockConfigManager_ExtractOciImageIndex_Call { + _c.Call.Return(oCIImageIndex, err) + return _c +} + +func (_c *MockConfigManager_ExtractOciImageIndex_Call) RunAndReturn(run func(imagefile string) (files.OCIImageIndex, error)) *MockConfigManager_ExtractOciImageIndex_Call { + _c.Call.Return(run) + return _c +} + +// ParseConfigYaml provides a mock function for the type MockConfigManager +func (_mock *MockConfigManager) ParseConfigYaml(configPath string) (files.RootConfig, error) { + ret := _mock.Called(configPath) + + if len(ret) == 0 { + panic("no return value specified for ParseConfigYaml") + } + + var r0 files.RootConfig + var r1 error + if returnFunc, ok := ret.Get(0).(func(string) (files.RootConfig, error)); ok { + return returnFunc(configPath) + } + if returnFunc, ok := ret.Get(0).(func(string) files.RootConfig); ok { + r0 = returnFunc(configPath) + } else { + r0 = ret.Get(0).(files.RootConfig) + } + if returnFunc, ok := ret.Get(1).(func(string) error); ok { + r1 = returnFunc(configPath) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockConfigManager_ParseConfigYaml_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ParseConfigYaml' +type MockConfigManager_ParseConfigYaml_Call struct { + *mock.Call +} + +// ParseConfigYaml is a helper method to define mock.On call +// - configPath +func (_e *MockConfigManager_Expecter) ParseConfigYaml(configPath interface{}) *MockConfigManager_ParseConfigYaml_Call { + return &MockConfigManager_ParseConfigYaml_Call{Call: _e.mock.On("ParseConfigYaml", configPath)} +} + +func (_c *MockConfigManager_ParseConfigYaml_Call) Run(run func(configPath string)) *MockConfigManager_ParseConfigYaml_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockConfigManager_ParseConfigYaml_Call) Return(rootConfig files.RootConfig, err error) *MockConfigManager_ParseConfigYaml_Call { + _c.Call.Return(rootConfig, err) + return _c +} + +func (_c *MockConfigManager_ParseConfigYaml_Call) RunAndReturn(run func(configPath string) (files.RootConfig, error)) *MockConfigManager_ParseConfigYaml_Call { + _c.Call.Return(run) + return _c +} + +// NewMockPackageManager creates a new instance of MockPackageManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockPackageManager(t interface { + mock.TestingT + Cleanup(func()) +}) *MockPackageManager { + mock := &MockPackageManager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockPackageManager is an autogenerated mock type for the PackageManager type +type MockPackageManager struct { + mock.Mock +} + +type MockPackageManager_Expecter struct { + mock *mock.Mock +} + +func (_m *MockPackageManager) EXPECT() *MockPackageManager_Expecter { + return &MockPackageManager_Expecter{mock: &_m.Mock} +} + +// Extract provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) Extract(force bool) error { + ret := _mock.Called(force) + + if len(ret) == 0 { + panic("no return value specified for Extract") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(bool) error); ok { + r0 = returnFunc(force) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockPackageManager_Extract_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Extract' +type MockPackageManager_Extract_Call struct { + *mock.Call +} + +// Extract is a helper method to define mock.On call +// - force +func (_e *MockPackageManager_Expecter) Extract(force interface{}) *MockPackageManager_Extract_Call { + return &MockPackageManager_Extract_Call{Call: _e.mock.On("Extract", force)} +} + +func (_c *MockPackageManager_Extract_Call) Run(run func(force bool)) *MockPackageManager_Extract_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *MockPackageManager_Extract_Call) Return(err error) *MockPackageManager_Extract_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockPackageManager_Extract_Call) RunAndReturn(run func(force bool) error) *MockPackageManager_Extract_Call { + _c.Call.Return(run) + return _c +} + +// ExtractDependency provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) ExtractDependency(file string, force bool) error { + ret := _mock.Called(file, force) + + if len(ret) == 0 { + panic("no return value specified for ExtractDependency") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, bool) error); ok { + r0 = returnFunc(file, force) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockPackageManager_ExtractDependency_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExtractDependency' +type MockPackageManager_ExtractDependency_Call struct { + *mock.Call +} + +// ExtractDependency is a helper method to define mock.On call +// - file +// - force +func (_e *MockPackageManager_Expecter) ExtractDependency(file interface{}, force interface{}) *MockPackageManager_ExtractDependency_Call { + return &MockPackageManager_ExtractDependency_Call{Call: _e.mock.On("ExtractDependency", file, force)} +} + +func (_c *MockPackageManager_ExtractDependency_Call) Run(run func(file string, force bool)) *MockPackageManager_ExtractDependency_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *MockPackageManager_ExtractDependency_Call) Return(err error) *MockPackageManager_ExtractDependency_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockPackageManager_ExtractDependency_Call) RunAndReturn(run func(file string, force bool) error) *MockPackageManager_ExtractDependency_Call { + _c.Call.Return(run) + return _c +} + +// FileIO provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) FileIO() util.FileIO { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for FileIO") + } + + var r0 util.FileIO + if returnFunc, ok := ret.Get(0).(func() util.FileIO); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(util.FileIO) + } + } + return r0 +} + +// MockPackageManager_FileIO_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FileIO' +type MockPackageManager_FileIO_Call struct { + *mock.Call +} + +// FileIO is a helper method to define mock.On call +func (_e *MockPackageManager_Expecter) FileIO() *MockPackageManager_FileIO_Call { + return &MockPackageManager_FileIO_Call{Call: _e.mock.On("FileIO")} +} + +func (_c *MockPackageManager_FileIO_Call) Run(run func()) *MockPackageManager_FileIO_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockPackageManager_FileIO_Call) Return(fileIO util.FileIO) *MockPackageManager_FileIO_Call { + _c.Call.Return(fileIO) + return _c +} + +func (_c *MockPackageManager_FileIO_Call) RunAndReturn(run func() util.FileIO) *MockPackageManager_FileIO_Call { + _c.Call.Return(run) + return _c +} + +// GetDependencyPath provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) GetDependencyPath(filename string) string { + ret := _mock.Called(filename) + + if len(ret) == 0 { + panic("no return value specified for GetDependencyPath") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func(string) string); ok { + r0 = returnFunc(filename) + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// MockPackageManager_GetDependencyPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDependencyPath' +type MockPackageManager_GetDependencyPath_Call struct { + *mock.Call +} + +// GetDependencyPath is a helper method to define mock.On call +// - filename +func (_e *MockPackageManager_Expecter) GetDependencyPath(filename interface{}) *MockPackageManager_GetDependencyPath_Call { + return &MockPackageManager_GetDependencyPath_Call{Call: _e.mock.On("GetDependencyPath", filename)} +} + +func (_c *MockPackageManager_GetDependencyPath_Call) Run(run func(filename string)) *MockPackageManager_GetDependencyPath_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockPackageManager_GetDependencyPath_Call) Return(s string) *MockPackageManager_GetDependencyPath_Call { + _c.Call.Return(s) + return _c +} + +func (_c *MockPackageManager_GetDependencyPath_Call) RunAndReturn(run func(filename string) string) *MockPackageManager_GetDependencyPath_Call { + _c.Call.Return(run) + return _c +} + +// GetWorkDir provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) GetWorkDir() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetWorkDir") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// MockPackageManager_GetWorkDir_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkDir' +type MockPackageManager_GetWorkDir_Call struct { + *mock.Call +} + +// GetWorkDir is a helper method to define mock.On call +func (_e *MockPackageManager_Expecter) GetWorkDir() *MockPackageManager_GetWorkDir_Call { + return &MockPackageManager_GetWorkDir_Call{Call: _e.mock.On("GetWorkDir")} +} + +func (_c *MockPackageManager_GetWorkDir_Call) Run(run func()) *MockPackageManager_GetWorkDir_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockPackageManager_GetWorkDir_Call) Return(s string) *MockPackageManager_GetWorkDir_Call { + _c.Call.Return(s) + return _c +} + +func (_c *MockPackageManager_GetWorkDir_Call) RunAndReturn(run func() string) *MockPackageManager_GetWorkDir_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/installer/package.go b/internal/installer/package.go index 553e3dac..1c7ff4c8 100644 --- a/internal/installer/package.go +++ b/internal/installer/package.go @@ -15,20 +15,33 @@ import ( const depsDir = "deps" +type PackageManager interface { + FileIO() util.FileIO + Extract(force bool) error + ExtractDependency(file string, force bool) error + GetWorkDir() string + GetDependencyPath(filename string) string +} + type Package struct { OmsWorkdir string Filename string - FileIO util.FileIO + fileIO util.FileIO } func NewPackage(omsWorkdir, filename string) *Package { return &Package{ Filename: filename, OmsWorkdir: omsWorkdir, - FileIO: &util.FilesystemWriter{}, + fileIO: &util.FilesystemWriter{}, } } +// FileIO returns the FileIO interface used by the package. +func (p *Package) FileIO() util.FileIO { + return p.fileIO +} + // Extract extracts the package tar.gz file into its working directory. // If force is true, it will overwrite existing files. func (p *Package) Extract(force bool) error { @@ -47,7 +60,7 @@ func (p *Package) Extract(force bool) error { return nil } - err = util.ExtractTarGz(p.FileIO, p.Filename, workDir) + err = util.ExtractTarGz(p.fileIO, p.Filename, workDir) if err != nil { return fmt.Errorf("failed to extract package %s to %s: %w", p.Filename, workDir, err) } @@ -63,12 +76,12 @@ func (p *Package) ExtractDependency(file string, force bool) error { } workDir := p.GetWorkDir() - if p.FileIO.Exists(p.GetDependencyPath(file)) && !force { + if p.fileIO.Exists(p.GetDependencyPath(file)) && !force { log.Println("skipping extraction, dependency already unpacked. Use force option to overwrite.") return nil } - err = util.ExtractTarGzSingleFile(p.FileIO, path.Join(workDir, "deps.tar.gz"), file, path.Join(workDir, depsDir)) + err = util.ExtractTarGzSingleFile(p.fileIO, path.Join(workDir, "deps.tar.gz"), file, path.Join(workDir, depsDir)) if err != nil { return fmt.Errorf("failed to extract dependency %s from deps archive to %s: %w", file, workDir, err) } @@ -77,10 +90,10 @@ func (p *Package) ExtractDependency(file string, force bool) error { } func (p *Package) alreadyExtracted(dir string) (bool, error) { - if !p.FileIO.Exists(dir) { + if !p.fileIO.Exists(dir) { return false, nil } - isDir, err := p.FileIO.IsDirectory(dir) + isDir, err := p.fileIO.IsDirectory(dir) if err != nil { return false, fmt.Errorf("failed to determine if %s is a folder: %w", dir, err) } diff --git a/internal/installer/package_test.go b/internal/installer/package_test.go new file mode 100644 index 00000000..5b510250 --- /dev/null +++ b/internal/installer/package_test.go @@ -0,0 +1,566 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package installer_test + +import ( + "archive/tar" + "compress/gzip" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/util" +) + +var _ = Describe("Package", func() { + var ( + pkg *installer.Package + tempDir string + omsWorkdir string + filename string + ) + + BeforeEach(func() { + tempDir = GinkgoT().TempDir() + omsWorkdir = filepath.Join(tempDir, "oms-workdir") + filename = "test-package.tar.gz" + pkg = installer.NewPackage(omsWorkdir, filename) + }) + + Describe("NewPackage", func() { + It("creates a new Package with correct parameters", func() { + newPkg := installer.NewPackage("/test/workdir", "package.tar.gz") + Expect(newPkg).ToNot(BeNil()) + Expect(newPkg.OmsWorkdir).To(Equal("/test/workdir")) + Expect(newPkg.Filename).To(Equal("package.tar.gz")) + Expect(newPkg.FileIO()).ToNot(BeNil()) + Expect(newPkg.FileIO()).To(BeAssignableToTypeOf(&util.FilesystemWriter{})) + }) + }) + + Describe("FileIO", func() { + It("returns the FileIO interface", func() { + fileIO := pkg.FileIO() + Expect(fileIO).ToNot(BeNil()) + Expect(fileIO).To(BeAssignableToTypeOf(&util.FilesystemWriter{})) + }) + }) + + Describe("GetWorkDir", func() { + It("returns correct working directory path", func() { + expected := filepath.Join(omsWorkdir, "test-package") + Expect(pkg.GetWorkDir()).To(Equal(expected)) + }) + + It("removes .tar.gz extension from filename", func() { + pkg.Filename = "my-package.tar.gz" + expected := filepath.Join(omsWorkdir, "my-package") + Expect(pkg.GetWorkDir()).To(Equal(expected)) + }) + + It("handles filename without .tar.gz extension", func() { + pkg.Filename = "my-package" + expected := filepath.Join(omsWorkdir, "my-package") + Expect(pkg.GetWorkDir()).To(Equal(expected)) + }) + + It("handles complex filenames", func() { + pkg.Filename = "complex-package-v1.2.3.tar.gz" + expected := filepath.Join(omsWorkdir, "complex-package-v1.2.3") + Expect(pkg.GetWorkDir()).To(Equal(expected)) + }) + }) + + Describe("GetDependencyPath", func() { + It("returns correct dependency path", func() { + filename := "dependency.txt" + workDir := pkg.GetWorkDir() + expected := filepath.Join(workDir, "deps", filename) + Expect(pkg.GetDependencyPath(filename)).To(Equal(expected)) + }) + + It("handles dependency files with paths", func() { + filename := "subfolder/dependency.txt" + workDir := pkg.GetWorkDir() + expected := filepath.Join(workDir, "deps", filename) + Expect(pkg.GetDependencyPath(filename)).To(Equal(expected)) + }) + + It("handles empty filename", func() { + filename := "" + workDir := pkg.GetWorkDir() + expected := filepath.Join(workDir, "deps", filename) + Expect(pkg.GetDependencyPath(filename)).To(Equal(expected)) + }) + }) + + Describe("Extract", func() { + Context("with real filesystem operations", func() { + BeforeEach(func() { + // Create the package tar.gz file + packagePath := filepath.Join(tempDir, filename) + err := createTestTarGzPackage(packagePath) + Expect(err).ToNot(HaveOccurred()) + pkg.Filename = packagePath + }) + + Context("when package doesn't exist", func() { + It("returns an error", func() { + pkg.Filename = "/nonexistent/package.tar.gz" + err := pkg.Extract(false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract package")) + }) + }) + + Context("when package exists and workdir doesn't exist", func() { + It("successfully extracts the package", func() { + err := pkg.Extract(false) + Expect(err).ToNot(HaveOccurred()) + + // Verify that the workdir was created + workDir := pkg.GetWorkDir() + Expect(workDir).To(BeADirectory()) + + // Verify that the content was extracted + testFile := filepath.Join(workDir, "test-file.txt") + Expect(testFile).To(BeAnExistingFile()) + }) + }) + + Context("when package is already extracted", func() { + BeforeEach(func() { + // First extraction + err := pkg.Extract(false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("skips extraction without force", func() { + err := pkg.Extract(false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("re-extracts with force", func() { + err := pkg.Extract(true) + Expect(err).ToNot(HaveOccurred()) + + // Verify content still exists + workDir := pkg.GetWorkDir() + testFile := filepath.Join(workDir, "test-file.txt") + Expect(testFile).To(BeAnExistingFile()) + }) + }) + + Context("when workdir creation fails", func() { + It("returns an error for invalid workdir", func() { + // Use a path that can't be created (file exists as directory) + invalidWorkdir := filepath.Join(tempDir, "invalid-workdir") + err := os.WriteFile(invalidWorkdir, []byte("content"), 0644) + Expect(err).ToNot(HaveOccurred()) + + pkg.OmsWorkdir = invalidWorkdir + err = pkg.Extract(false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to ensure workdir exists")) + }) + }) + }) + }) + + Describe("ExtractDependency", func() { + Context("with real filesystem operations", func() { + var packagePath string + + BeforeEach(func() { + // Create the package tar.gz file with deps.tar.gz inside + packagePath = filepath.Join(tempDir, filename) + err := createTestTarGzPackageWithDeps(packagePath) + Expect(err).ToNot(HaveOccurred()) + pkg.Filename = packagePath + }) + + Context("when dependency file exists in deps.tar.gz", func() { + It("successfully extracts the dependency", func() { + err := pkg.ExtractDependency("test-dep.txt", false) + Expect(err).ToNot(HaveOccurred()) + + // Verify that the dependency was extracted + depPath := pkg.GetDependencyPath("test-dep.txt") + Expect(depPath).To(BeAnExistingFile()) + }) + }) + + Context("when dependency is already extracted", func() { + BeforeEach(func() { + // First extraction + err := pkg.ExtractDependency("test-dep.txt", false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("skips extraction without force", func() { + err := pkg.ExtractDependency("test-dep.txt", false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("re-extracts with force", func() { + err := pkg.ExtractDependency("test-dep.txt", true) + Expect(err).ToNot(HaveOccurred()) + + // Verify dependency still exists + depPath := pkg.GetDependencyPath("test-dep.txt") + Expect(depPath).To(BeAnExistingFile()) + }) + }) + + Context("when package extraction fails", func() { + It("returns an error", func() { + pkg.Filename = "/nonexistent/package.tar.gz" + err := pkg.ExtractDependency("test-dep.txt", false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract package")) + }) + }) + + Context("when dependency doesn't exist in deps.tar.gz", func() { + It("returns an error", func() { + err := pkg.ExtractDependency("nonexistent-dep.txt", false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract dependency")) + }) + }) + }) + }) + + Describe("PackageManager interface", func() { + It("implements PackageManager interface", func() { + var packageManager installer.PackageManager = pkg + Expect(packageManager).ToNot(BeNil()) + }) + + It("has all required methods", func() { + var packageManager installer.PackageManager = pkg + + // Test that methods exist by calling them + fileIO := packageManager.FileIO() + Expect(fileIO).ToNot(BeNil()) + + workDir := packageManager.GetWorkDir() + Expect(workDir).ToNot(BeEmpty()) + + depPath := packageManager.GetDependencyPath("test.txt") + Expect(depPath).ToNot(BeEmpty()) + + // Extract methods would need actual files to test properly + // These are tested in the method-specific sections above + }) + }) + + Describe("Error handling and edge cases", func() { + Context("Extract with various scenarios", func() { + It("handles empty filename gracefully", func() { + pkg.Filename = "" + _ = pkg.Extract(false) + // Note: Empty filename may not always cause an error at Extract level + // The error might occur later during actual file operations + // This test verifies the behavior is predictable + }) + + It("handles empty workdir", func() { + pkg.OmsWorkdir = "" + packagePath := filepath.Join(tempDir, filename) + err := createTestTarGzPackage(packagePath) + Expect(err).ToNot(HaveOccurred()) + pkg.Filename = packagePath + + // Empty workdir should cause an error when trying to create directories + err = pkg.Extract(false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to ensure workdir exists")) + }) + }) + + Context("ExtractDependency with various scenarios", func() { + It("handles empty dependency filename", func() { + packagePath := filepath.Join(tempDir, filename) + err := createTestTarGzPackageWithDeps(packagePath) + Expect(err).ToNot(HaveOccurred()) + pkg.Filename = packagePath + + // Empty dependency filename may succeed (extracts everything) or fail + // depending on the underlying tar extraction implementation + _ = pkg.ExtractDependency("", false) + // This test verifies the behavior is predictable + }) + }) + + Context("Path handling edge cases", func() { + It("handles special characters in filenames", func() { + pkg.Filename = "test-package-with-special-chars_v1.0.tar.gz" + expected := filepath.Join(omsWorkdir, "test-package-with-special-chars_v1.0") + Expect(pkg.GetWorkDir()).To(Equal(expected)) + }) + + It("handles multiple .tar.gz occurrences in filename", func() { + pkg.Filename = "package.tar.gz.backup.tar.gz" + expected := filepath.Join(omsWorkdir, "package.backup") + Expect(pkg.GetWorkDir()).To(Equal(expected)) + }) + }) + }) + + Describe("Integration scenarios", func() { + Context("full workflow simulation", func() { + var packagePath string + + BeforeEach(func() { + packagePath = filepath.Join(tempDir, "complete-package.tar.gz") + err := createComplexTestPackage(packagePath) + Expect(err).ToNot(HaveOccurred()) + pkg.Filename = packagePath + }) + + It("can extract package and multiple dependencies successfully", func() { + // Extract main package + err := pkg.Extract(false) + Expect(err).ToNot(HaveOccurred()) + + // Verify main package content + workDir := pkg.GetWorkDir() + Expect(workDir).To(BeADirectory()) + mainFile := filepath.Join(workDir, "main-content.txt") + Expect(mainFile).To(BeAnExistingFile()) + + // Extract multiple dependencies + dependencies := []string{"dep1.txt", "dep2.txt", "subdep/dep3.txt"} + for _, dep := range dependencies { + err = pkg.ExtractDependency(dep, false) + Expect(err).ToNot(HaveOccurred()) + + depPath := pkg.GetDependencyPath(dep) + Expect(depPath).To(BeAnExistingFile()) + } + + // Verify all paths are correct + for _, dep := range dependencies { + depPath := pkg.GetDependencyPath(dep) + expectedPath := filepath.Join(workDir, "deps", dep) + Expect(depPath).To(Equal(expectedPath)) + } + }) + }) + }) +}) + +// 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 file.Close() + + gzw := gzip.NewWriter(file) + defer gzw.Close() + + tw := tar.NewWriter(gzw) + defer 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 file.Close() + + gzw := gzip.NewWriter(file) + defer gzw.Close() + + tw := tar.NewWriter(gzw) + defer 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 file.Close() + + gzw := gzip.NewWriter(file) + defer gzw.Close() + + tw := tar.NewWriter(gzw) + defer 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 +} diff --git a/internal/system/container.go b/internal/system/container.go deleted file mode 100644 index c64bc95b..00000000 --- a/internal/system/container.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Codesphere Inc. -// SPDX-License-Identifier: Apache-2.0 - -package system - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/codesphere-cloud/oms/internal/util" -) - -type DockerEngine struct { -} - -type ContainerEngine interface { - LoadLocalContainerImage(filename string) error - BuildImage(dockerfile string) error -} - -func NewDockerEngine() *DockerEngine { - return &DockerEngine{} -} - -func (d *DockerEngine) LoadLocalContainerImage(imagefile string) error { - err := d.RunCommand([]string{"load", "--input", imagefile}) - - if err != nil { - return fmt.Errorf("failed to load image %s: %w", imagefile, err) - } - - return nil -} - -// OCIImageIndex represents the top-level structure of an OCI Image Index (manifest list). -type OCIImageIndex struct { - SchemaVersion int `json:"schemaVersion"` - MediaType string `json:"mediaType"` - Manifests []ManifestEntry `json:"manifests"` -} - -// ManifestEntry represents a single manifest entry within the index. -type ManifestEntry struct { - MediaType string `json:"mediaType"` - Digest string `json:"digest"` - Size int64 `json:"size"` - Annotations map[string]string `json:"annotations,omitempty"` // Use omitempty just in case, though usually present -} - -func (d *DockerEngine) GetImageNames(fileIo util.FileIO, imagefile string) ([]string, error) { - names := []string{} - err := util.ExtractTarSingleFile(fileIo, imagefile, "index.json", filepath.Dir(imagefile)) - - if err != nil { - return names, fmt.Errorf("failed to extract index.json: %w", err) - } - - indexfile := filepath.Join(filepath.Dir(imagefile), "index.json") - file, err := os.Open(indexfile) - if err != nil { - return names, fmt.Errorf("failed to open file %s: %w", indexfile, err) - } - defer util.CloseFileIgnoreError(file) - - var index OCIImageIndex - decoder := json.NewDecoder(file) - err = decoder.Decode(&index) - - if err != nil { - return names, fmt.Errorf("failed to decode file %s: %w", indexfile, err) - } - - for _, manifest := range index.Manifests { - name := manifest.Annotations["io.containerd.image.name"] - if name == "" { - continue - } - names = append(names, name) - } - return names, nil -} - -func (d *DockerEngine) RunCommand(dockerCmd []string) error { - err := RunCommandAndStreamOutput("docker", dockerCmd...) - if err != nil { - return fmt.Errorf("failed to run docker command `docker \"%s\"`: %w", strings.Join(dockerCmd, "\" \""), err) - } - - return nil -} - -func RunCommandAndStreamOutput(name string, args ...string) error { - cmd := exec.Command(name, args...) - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() - if err != nil { - return fmt.Errorf("failed to run command: %w", err) - } - return nil -} diff --git a/internal/system/image.go b/internal/system/image.go new file mode 100644 index 00000000..390d41dc --- /dev/null +++ b/internal/system/image.go @@ -0,0 +1,99 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package system + +import ( + "context" + "fmt" + "os" + "os/exec" +) + +type Image struct { + ctx context.Context +} + +type ImageManager interface { + LoadImage(imageTarPath string) error + BuildImage(dockerfile string, tag string, buildContext string) error + PushImage(tag string) error +} + +func NewImage(ctx context.Context) *Image { + return &Image{ + ctx: ctx, + } +} + +func isCommandAvailable(name string) bool { + cmd := exec.Command("command", "-v", name) + if err := cmd.Run(); err != nil { + return false + } + return true +} + +func (c *Image) LoadImage(imageTarPath string) error { + var cmd *exec.Cmd + if isCommandAvailable("docker") { + cmd = exec.CommandContext(c.ctx, "docker", "load", "-i", imageTarPath) + } else if isCommandAvailable("podman") { + cmd = exec.CommandContext(c.ctx, "podman", "load", "-i", imageTarPath) + } else { + return fmt.Errorf("neither 'docker' nor 'podman' command is available") + } + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return fmt.Errorf("load failed with exit status %w", err) + } + + return nil +} + +func (c *Image) BuildImage(dockerfile string, tag string, buildContext string) error { + var cmd *exec.Cmd + if isCommandAvailable("docker") { + cmd = exec.CommandContext(c.ctx, "docker", "build", "-f", dockerfile, "-t", tag, ".") + } else if isCommandAvailable("podman") { + cmd = exec.CommandContext(c.ctx, "podman", "build", "-f", dockerfile, "-t", tag, ".") + } else { + return fmt.Errorf("neither 'docker' nor 'podman' command is available") + } + + cmd.Dir = buildContext + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return fmt.Errorf("build failed with exit status %w", err) + } + + return nil +} + +func (c *Image) PushImage(tag string) error { + var cmd *exec.Cmd + if isCommandAvailable("docker") { + cmd = exec.CommandContext(c.ctx, "docker", "push", tag) + } else if isCommandAvailable("podman") { + cmd = exec.CommandContext(c.ctx, "podman", "push", tag) + } else { + return fmt.Errorf("neither 'docker' nor 'podman' command is available") + } + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return fmt.Errorf("push failed with exit status %w", err) + } + + return nil +} diff --git a/internal/system/mocks.go b/internal/system/mocks.go new file mode 100644 index 00000000..cf8e9292 --- /dev/null +++ b/internal/system/mocks.go @@ -0,0 +1,173 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package system + +import ( + mock "github.com/stretchr/testify/mock" +) + +// NewMockImageManager creates a new instance of MockImageManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockImageManager(t interface { + mock.TestingT + Cleanup(func()) +}) *MockImageManager { + mock := &MockImageManager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockImageManager is an autogenerated mock type for the ImageManager type +type MockImageManager struct { + mock.Mock +} + +type MockImageManager_Expecter struct { + mock *mock.Mock +} + +func (_m *MockImageManager) EXPECT() *MockImageManager_Expecter { + return &MockImageManager_Expecter{mock: &_m.Mock} +} + +// BuildImage provides a mock function for the type MockImageManager +func (_mock *MockImageManager) BuildImage(dockerfile string, tag string, buildContext string) error { + ret := _mock.Called(dockerfile, tag, buildContext) + + if len(ret) == 0 { + panic("no return value specified for BuildImage") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, string, string) error); ok { + r0 = returnFunc(dockerfile, tag, buildContext) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockImageManager_BuildImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BuildImage' +type MockImageManager_BuildImage_Call struct { + *mock.Call +} + +// BuildImage is a helper method to define mock.On call +// - dockerfile +// - tag +// - buildContext +func (_e *MockImageManager_Expecter) BuildImage(dockerfile interface{}, tag interface{}, buildContext interface{}) *MockImageManager_BuildImage_Call { + return &MockImageManager_BuildImage_Call{Call: _e.mock.On("BuildImage", dockerfile, tag, buildContext)} +} + +func (_c *MockImageManager_BuildImage_Call) Run(run func(dockerfile string, tag string, buildContext string)) *MockImageManager_BuildImage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockImageManager_BuildImage_Call) Return(err error) *MockImageManager_BuildImage_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockImageManager_BuildImage_Call) RunAndReturn(run func(dockerfile string, tag string, buildContext string) error) *MockImageManager_BuildImage_Call { + _c.Call.Return(run) + return _c +} + +// LoadImage provides a mock function for the type MockImageManager +func (_mock *MockImageManager) LoadImage(imageTarPath string) error { + ret := _mock.Called(imageTarPath) + + if len(ret) == 0 { + panic("no return value specified for LoadImage") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(imageTarPath) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockImageManager_LoadImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadImage' +type MockImageManager_LoadImage_Call struct { + *mock.Call +} + +// LoadImage is a helper method to define mock.On call +// - imageTarPath +func (_e *MockImageManager_Expecter) LoadImage(imageTarPath interface{}) *MockImageManager_LoadImage_Call { + return &MockImageManager_LoadImage_Call{Call: _e.mock.On("LoadImage", imageTarPath)} +} + +func (_c *MockImageManager_LoadImage_Call) Run(run func(imageTarPath string)) *MockImageManager_LoadImage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockImageManager_LoadImage_Call) Return(err error) *MockImageManager_LoadImage_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockImageManager_LoadImage_Call) RunAndReturn(run func(imageTarPath string) error) *MockImageManager_LoadImage_Call { + _c.Call.Return(run) + return _c +} + +// PushImage provides a mock function for the type MockImageManager +func (_mock *MockImageManager) PushImage(tag string) error { + ret := _mock.Called(tag) + + if len(ret) == 0 { + panic("no return value specified for PushImage") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(tag) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockImageManager_PushImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PushImage' +type MockImageManager_PushImage_Call struct { + *mock.Call +} + +// PushImage is a helper method to define mock.On call +// - tag +func (_e *MockImageManager_Expecter) PushImage(tag interface{}) *MockImageManager_PushImage_Call { + return &MockImageManager_PushImage_Call{Call: _e.mock.On("PushImage", tag)} +} + +func (_c *MockImageManager_PushImage_Call) Run(run func(tag string)) *MockImageManager_PushImage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockImageManager_PushImage_Call) Return(err error) *MockImageManager_PushImage_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockImageManager_PushImage_Call) RunAndReturn(run func(tag string) error) *MockImageManager_PushImage_Call { + _c.Call.Return(run) + return _c +} From 9009a5b58546f9280061aa88a6c41e74fc3a751e Mon Sep 17 00:00:00 2001 From: siherrmann <25087590+siherrmann@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:56:12 +0000 Subject: [PATCH 02/22] chore(docs): Auto-update docs and licenses Signed-off-by: siherrmann <25087590+siherrmann@users.noreply.github.com> --- docs/README.md | 3 ++- docs/oms-cli.md | 3 ++- docs/oms-cli_beta.md | 2 +- docs/oms-cli_beta_extend.md | 2 +- docs/oms-cli_beta_extend_baseimage.md | 2 +- docs/oms-cli_build.md | 20 +++++++++++++++++ docs/oms-cli_build_images.md | 25 +++++++++++++++++++++ docs/oms-cli_download.md | 2 +- docs/oms-cli_download_package.md | 2 +- docs/oms-cli_install.md | 2 +- docs/oms-cli_install_codesphere.md | 2 +- docs/oms-cli_licenses.md | 2 +- docs/oms-cli_list.md | 2 +- docs/oms-cli_list_api-keys.md | 2 +- docs/oms-cli_list_packages.md | 2 +- docs/oms-cli_register.md | 2 +- docs/oms-cli_revoke.md | 2 +- docs/oms-cli_revoke_api-key.md | 2 +- docs/oms-cli_update.md | 2 +- docs/oms-cli_update_api-key.md | 2 +- docs/oms-cli_update_oms.md | 2 +- docs/oms-cli_update_package.md | 2 +- docs/oms-cli_version.md | 2 +- internal/installer/config.go | 3 +++ internal/installer/files/config_yaml.go | 3 +++ internal/installer/files/oci_image_index.go | 3 +++ 26 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 docs/oms-cli_build.md create mode 100644 docs/oms-cli_build_images.md diff --git a/docs/README.md b/docs/README.md index 66c5ff68..f97c5899 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ like downloading new versions. ### SEE ALSO * [oms-cli beta](oms-cli_beta.md) - Commands for early testing +* [oms-cli build](oms-cli_build.md) - Build and push images to a registry * [oms-cli download](oms-cli_download.md) - Download resources available through OMS * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components * [oms-cli licenses](oms-cli_licenses.md) - Print license information @@ -27,4 +28,4 @@ like downloading new versions. * [oms-cli update](oms-cli_update.md) - Update OMS related resources * [oms-cli version](oms-cli_version.md) - Print version -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli.md b/docs/oms-cli.md index 66c5ff68..f97c5899 100644 --- a/docs/oms-cli.md +++ b/docs/oms-cli.md @@ -18,6 +18,7 @@ like downloading new versions. ### SEE ALSO * [oms-cli beta](oms-cli_beta.md) - Commands for early testing +* [oms-cli build](oms-cli_build.md) - Build and push images to a registry * [oms-cli download](oms-cli_download.md) - Download resources available through OMS * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components * [oms-cli licenses](oms-cli_licenses.md) - Print license information @@ -27,4 +28,4 @@ like downloading new versions. * [oms-cli update](oms-cli_update.md) - Update OMS related resources * [oms-cli version](oms-cli_version.md) - Print version -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_beta.md b/docs/oms-cli_beta.md index 7bb12f54..704b43ff 100644 --- a/docs/oms-cli_beta.md +++ b/docs/oms-cli_beta.md @@ -18,4 +18,4 @@ Be aware that that usage and behavior may change as the features are developed. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli beta extend](oms-cli_beta_extend.md) - Extend Codesphere ressources such as base images. -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_beta_extend.md b/docs/oms-cli_beta_extend.md index 386d605f..daf42e41 100644 --- a/docs/oms-cli_beta_extend.md +++ b/docs/oms-cli_beta_extend.md @@ -17,4 +17,4 @@ Extend Codesphere ressources such as base images to customize them for your need * [oms-cli beta](oms-cli_beta.md) - Commands for early testing * [oms-cli beta extend baseimage](oms-cli_beta_extend_baseimage.md) - Extend Codesphere's workspace base image for customization -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_beta_extend_baseimage.md b/docs/oms-cli_beta_extend_baseimage.md index 20df5370..93196f81 100644 --- a/docs/oms-cli_beta_extend_baseimage.md +++ b/docs/oms-cli_beta_extend_baseimage.md @@ -27,4 +27,4 @@ oms-cli beta extend baseimage [flags] * [oms-cli beta extend](oms-cli_beta_extend.md) - Extend Codesphere ressources such as base images. -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_build.md b/docs/oms-cli_build.md new file mode 100644 index 00000000..5f554214 --- /dev/null +++ b/docs/oms-cli_build.md @@ -0,0 +1,20 @@ +## oms-cli build + +Build and push images to a registry + +### Synopsis + +Build and push container images to a registry using the provided configuration. + +### Options + +``` + -h, --help help for build +``` + +### SEE ALSO + +* [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) +* [oms-cli build images](oms-cli_build_images.md) - Build and push container images + +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_build_images.md b/docs/oms-cli_build_images.md new file mode 100644 index 00000000..bc3f0e15 --- /dev/null +++ b/docs/oms-cli_build_images.md @@ -0,0 +1,25 @@ +## oms-cli build images + +Build and push container images + +### Synopsis + +Build and push container images based on the configuration file. +Extracts necessary images configuration to get the bomRef and processes them. + +``` +oms-cli build images [flags] +``` + +### Options + +``` + -c, --config string Path to the configuration YAML file + -h, --help help for images +``` + +### SEE ALSO + +* [oms-cli build](oms-cli_build.md) - Build and push images to a registry + +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_download.md b/docs/oms-cli_download.md index e8e52e89..2a3f3f6c 100644 --- a/docs/oms-cli_download.md +++ b/docs/oms-cli_download.md @@ -18,4 +18,4 @@ e.g. available Codesphere packages * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli download package](oms-cli_download_package.md) - Download a codesphere package -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_download_package.md b/docs/oms-cli_download_package.md index 1792f415..772a539f 100644 --- a/docs/oms-cli_download_package.md +++ b/docs/oms-cli_download_package.md @@ -36,4 +36,4 @@ $ oms-cli download package --version codesphere-v1.55.0 --file installer-lite.ta * [oms-cli download](oms-cli_download.md) - Download resources available through OMS -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_install.md b/docs/oms-cli_install.md index f40e5ad4..d456baa5 100644 --- a/docs/oms-cli_install.md +++ b/docs/oms-cli_install.md @@ -17,4 +17,4 @@ Coming soon: Install Codesphere and other components like Ceph and PostgreSQL. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli install codesphere](oms-cli_install_codesphere.md) - Install a Codesphere instance -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_install_codesphere.md b/docs/oms-cli_install_codesphere.md index 030d3dc3..3e593a94 100644 --- a/docs/oms-cli_install_codesphere.md +++ b/docs/oms-cli_install_codesphere.md @@ -26,4 +26,4 @@ oms-cli install codesphere [flags] * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_licenses.md b/docs/oms-cli_licenses.md index 4ee07539..e799ac10 100644 --- a/docs/oms-cli_licenses.md +++ b/docs/oms-cli_licenses.md @@ -20,4 +20,4 @@ oms-cli licenses [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_list.md b/docs/oms-cli_list.md index fdcda207..de31339e 100644 --- a/docs/oms-cli_list.md +++ b/docs/oms-cli_list.md @@ -19,4 +19,4 @@ eg. available Codesphere packages * [oms-cli list api-keys](oms-cli_list_api-keys.md) - List API keys * [oms-cli list packages](oms-cli_list_packages.md) - List available packages -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_list_api-keys.md b/docs/oms-cli_list_api-keys.md index df732f3c..930ac330 100644 --- a/docs/oms-cli_list_api-keys.md +++ b/docs/oms-cli_list_api-keys.md @@ -20,4 +20,4 @@ oms-cli list api-keys [flags] * [oms-cli list](oms-cli_list.md) - List resources available through OMS -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_list_packages.md b/docs/oms-cli_list_packages.md index 20845527..d34af695 100644 --- a/docs/oms-cli_list_packages.md +++ b/docs/oms-cli_list_packages.md @@ -20,4 +20,4 @@ oms-cli list packages [flags] * [oms-cli list](oms-cli_list.md) - List resources available through OMS -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_register.md b/docs/oms-cli_register.md index 787764e8..a8e93b8b 100644 --- a/docs/oms-cli_register.md +++ b/docs/oms-cli_register.md @@ -24,4 +24,4 @@ oms-cli register [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_revoke.md b/docs/oms-cli_revoke.md index 25ef5d13..9a3f930f 100644 --- a/docs/oms-cli_revoke.md +++ b/docs/oms-cli_revoke.md @@ -18,4 +18,4 @@ eg. api keys. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli revoke api-key](oms-cli_revoke_api-key.md) - Revoke an API key -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_revoke_api-key.md b/docs/oms-cli_revoke_api-key.md index 90a31831..30d29c7a 100644 --- a/docs/oms-cli_revoke_api-key.md +++ b/docs/oms-cli_revoke_api-key.md @@ -21,4 +21,4 @@ oms-cli revoke api-key [flags] * [oms-cli revoke](oms-cli_revoke.md) - Revoke resources available through OMS -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_update.md b/docs/oms-cli_update.md index 9fa9de78..982a7806 100644 --- a/docs/oms-cli_update.md +++ b/docs/oms-cli_update.md @@ -23,4 +23,4 @@ oms-cli update [flags] * [oms-cli update oms](oms-cli_update_oms.md) - Update the OMS CLI * [oms-cli update package](oms-cli_update_package.md) - Download a codesphere package -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_update_api-key.md b/docs/oms-cli_update_api-key.md index d7f35102..0442106a 100644 --- a/docs/oms-cli_update_api-key.md +++ b/docs/oms-cli_update_api-key.md @@ -22,4 +22,4 @@ oms-cli update api-key [flags] * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_update_oms.md b/docs/oms-cli_update_oms.md index 875562a0..3652afa9 100644 --- a/docs/oms-cli_update_oms.md +++ b/docs/oms-cli_update_oms.md @@ -20,4 +20,4 @@ oms-cli update oms [flags] * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_update_package.md b/docs/oms-cli_update_package.md index d608c2dc..b19d2e4a 100644 --- a/docs/oms-cli_update_package.md +++ b/docs/oms-cli_update_package.md @@ -36,4 +36,4 @@ $ oms-cli download package --version codesphere-v1.55.0 --file installer-lite.ta * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/docs/oms-cli_version.md b/docs/oms-cli_version.md index 96cd471f..729cbb2e 100644 --- a/docs/oms-cli_version.md +++ b/docs/oms-cli_version.md @@ -20,4 +20,4 @@ oms-cli version [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 28-Oct-2025 diff --git a/internal/installer/config.go b/internal/installer/config.go index 0a813b06..c8494e65 100644 --- a/internal/installer/config.go +++ b/internal/installer/config.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package installer import ( diff --git a/internal/installer/files/config_yaml.go b/internal/installer/files/config_yaml.go index 041324ed..d31eab68 100644 --- a/internal/installer/files/config_yaml.go +++ b/internal/installer/files/config_yaml.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package files import ( diff --git a/internal/installer/files/oci_image_index.go b/internal/installer/files/oci_image_index.go index 4e63f9dd..bb9aa5de 100644 --- a/internal/installer/files/oci_image_index.go +++ b/internal/installer/files/oci_image_index.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package files import ( From 012061953402f0310d534418d44c288ad97d57ff Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Tue, 28 Oct 2025 14:01:17 +0100 Subject: [PATCH 03/22] fix: fix issues after main merge, small updates --- cli/cmd/install_codesphere.go | 1 + go.mod | 26 ++++++++--------- go.sum | 54 +++++++++++++++++------------------ internal/installer/config.go | 2 +- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/cli/cmd/install_codesphere.go b/cli/cmd/install_codesphere.go index b8740ef4..bada1753 100644 --- a/cli/cmd/install_codesphere.go +++ b/cli/cmd/install_codesphere.go @@ -195,6 +195,7 @@ func (c *InstallCodesphereCmd) ListPackageContents(pm installer.PackageManager) return foundFiles, nil } +// ExtractRootImageName extracts the root image name from a bomRef string. func (c *InstallCodesphereCmd) ExtractRootImageName(bomRef string) string { parts := strings.Split(bomRef, ":") if len(parts) < 2 { diff --git a/go.mod b/go.mod index 270d713b..6adf68fa 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.24.2 require ( github.com/blang/semver v3.5.1+incompatible github.com/codesphere-cloud/cs-go v0.13.0 - github.com/jedib0t/go-pretty/v6 v6.6.8 - github.com/onsi/ginkgo/v2 v2.27.1 + github.com/jedib0t/go-pretty/v6 v6.6.9 + github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 github.com/rhysd/go-github-selfupdate v1.2.3 github.com/spf13/cobra v1.10.1 @@ -16,7 +16,8 @@ require ( require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -24,7 +25,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v30 v30.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 // indirect + github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect @@ -32,18 +33,17 @@ require ( github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect github.com/ulikunitz/xz v0.5.15 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.38.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 1a095acf..0f2f1c1f 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,10 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= -github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/codesphere-cloud/cs-go v0.13.0 h1:UlClugSlTtFeEpEVZSbQdegblVZnrKlYyjX+51PYQ1w= github.com/codesphere-cloud/cs-go v0.13.0/go.mod h1:Wj8rWS0VcXEBS6+58PwggDe6ZTz06gpJbbioLZc+uHo= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -34,15 +36,15 @@ github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQF github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 h1:ZI8gCoCjGzPsum4L21jHdQs8shFBIQih1TM9Rd/c+EQ= -github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= +github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= -github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.6.9 h1:PQecJLK3L8ODuVyMe2223b61oRJjrKnmXAncbWTv9MY= +github.com/jedib0t/go-pretty/v6 v6.6.9/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -60,15 +62,13 @@ github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhg github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo/v2 v2.27.1 h1:0LJC8MpUSQnfnp4n/3W3GdlmJP3ENGF0ZPzjQGLPP7s= -github.com/onsi/ginkgo/v2 v2.27.1/go.mod h1:wmy3vCqiBjirARfVhAqFpYt8uvX0yaFe+GudAqqcCqA= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag= github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -80,8 +80,8 @@ github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4 github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw= @@ -97,42 +97,40 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6 github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/internal/installer/config.go b/internal/installer/config.go index 0a813b06..72b9becb 100644 --- a/internal/installer/config.go +++ b/internal/installer/config.go @@ -28,7 +28,7 @@ func (c *Config) ParseConfigYaml(configPath string) (files.RootConfig, error) { var rootConfig files.RootConfig err := rootConfig.ParseConfig(configPath) if err != nil { - return rootConfig, fmt.Errorf("failed to extract config.yaml: %w", err) + return rootConfig, fmt.Errorf("failed to parse config.yaml: %w", err) } return rootConfig, nil From 889dc3a4e04b625c694073d34828006a72237495 Mon Sep 17 00:00:00 2001 From: siherrmann <25087590+siherrmann@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:48:31 +0000 Subject: [PATCH 04/22] chore(docs): Auto-update docs and licenses Signed-off-by: siherrmann <25087590+siherrmann@users.noreply.github.com> --- NOTICE | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/NOTICE b/NOTICE index 2aaf1c7f..7881e7f0 100644 --- a/NOTICE +++ b/NOTICE @@ -9,11 +9,17 @@ Version: v3.5.1 License: MIT License URL: https://github.com/blang/semver/blob/v3.5.1/LICENSE +---------- +Module: github.com/clipperhouse/stringish +Version: v0.1.1 +License: MIT +License URL: https://github.com/clipperhouse/stringish/blob/v0.1.1/LICENSE + ---------- Module: github.com/clipperhouse/uax29/v2 -Version: v2.2.0 +Version: v2.3.0 License: MIT -License URL: https://github.com/clipperhouse/uax29/blob/v2.2.0/LICENSE +License URL: https://github.com/clipperhouse/uax29/blob/v2.3.0/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go/pkg/io @@ -71,9 +77,9 @@ License URL: https://github.com/inconshreveable/go-update/blob/8152e7eb6ccf/inte ---------- Module: github.com/jedib0t/go-pretty/v6 -Version: v6.6.8 +Version: v6.6.9 License: MIT -License URL: https://github.com/jedib0t/go-pretty/blob/v6.6.8/LICENSE +License URL: https://github.com/jedib0t/go-pretty/blob/v6.6.9/LICENSE ---------- Module: github.com/mattn/go-runewidth @@ -113,9 +119,9 @@ License URL: https://github.com/spf13/pflag/blob/v1.0.10/LICENSE ---------- Module: github.com/stretchr/objx -Version: v0.5.2 +Version: v0.5.3 License: MIT -License URL: https://github.com/stretchr/objx/blob/v0.5.2/LICENSE +License URL: https://github.com/stretchr/objx/blob/v0.5.3/LICENSE ---------- Module: github.com/stretchr/testify @@ -137,21 +143,21 @@ License URL: https://github.com/ulikunitz/xz/blob/v0.5.15/LICENSE ---------- Module: golang.org/x/crypto -Version: v0.42.0 +Version: v0.43.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/crypto/+/v0.42.0:LICENSE +License URL: https://cs.opensource.google/go/x/crypto/+/v0.43.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.30.0 +Version: v0.32.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.30.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.32.0:LICENSE ---------- Module: golang.org/x/text -Version: v0.29.0 +Version: v0.30.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/text/+/v0.29.0:LICENSE +License URL: https://cs.opensource.google/go/x/text/+/v0.30.0:LICENSE ---------- Module: gopkg.in/yaml.v3 From b2ecd0a3d0d4329157fccab4f8b7140e08c934c0 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Thu, 30 Oct 2025 17:08:25 +0100 Subject: [PATCH 05/22] refac: refactor image file to not repeat docker podman if else --- internal/system/image.go | 57 +++++++++++++++------------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/internal/system/image.go b/internal/system/image.go index 390d41dc..96224617 100644 --- a/internal/system/image.go +++ b/internal/system/image.go @@ -27,72 +27,57 @@ func NewImage(ctx context.Context) *Image { } func isCommandAvailable(name string) bool { - cmd := exec.Command("command", "-v", name) + cmd := exec.Command(name, "-v") if err := cmd.Run(); err != nil { return false } return true } -func (c *Image) LoadImage(imageTarPath string) error { - var cmd *exec.Cmd - if isCommandAvailable("docker") { - cmd = exec.CommandContext(c.ctx, "docker", "load", "-i", imageTarPath) - } else if isCommandAvailable("podman") { - cmd = exec.CommandContext(c.ctx, "podman", "load", "-i", imageTarPath) - } else { - return fmt.Errorf("neither 'docker' nor 'podman' command is available") - } - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() +func (i *Image) LoadImage(imageTarPath string) error { + err := i.runCommand("", "load", "-i", imageTarPath) if err != nil { - return fmt.Errorf("load failed with exit status %w", err) + return fmt.Errorf("load failed: %w", err) } - return nil } -func (c *Image) BuildImage(dockerfile string, tag string, buildContext string) error { - var cmd *exec.Cmd - if isCommandAvailable("docker") { - cmd = exec.CommandContext(c.ctx, "docker", "build", "-f", dockerfile, "-t", tag, ".") - } else if isCommandAvailable("podman") { - cmd = exec.CommandContext(c.ctx, "podman", "build", "-f", dockerfile, "-t", tag, ".") - } else { - return fmt.Errorf("neither 'docker' nor 'podman' command is available") +func (i *Image) BuildImage(dockerfile string, tag string, buildContext string) error { + err := i.runCommand(buildContext, "build", "-f", dockerfile, "-t", tag, ".") + if err != nil { + return fmt.Errorf("build failed: %w", err) } + return nil +} - cmd.Dir = buildContext - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() +func (i *Image) PushImage(tag string) error { + err := i.runCommand("", "push", tag) if err != nil { - return fmt.Errorf("build failed with exit status %w", err) + return fmt.Errorf("push failed: %w", err) } - return nil } -func (c *Image) PushImage(tag string) error { +func (i *Image) runCommand(cmdDir string, args ...string) error { var cmd *exec.Cmd if isCommandAvailable("docker") { - cmd = exec.CommandContext(c.ctx, "docker", "push", tag) + cmd = exec.CommandContext(i.ctx, "docker", args...) } else if isCommandAvailable("podman") { - cmd = exec.CommandContext(c.ctx, "podman", "push", tag) + cmd = exec.CommandContext(i.ctx, "podman", args...) } else { return fmt.Errorf("neither 'docker' nor 'podman' command is available") } + if cmdDir != "" { + cmd.Dir = cmdDir + } + cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { - return fmt.Errorf("push failed with exit status %w", err) + return fmt.Errorf("command failed with exit status %w", err) } return nil From cfbb6e941a085c09111f334d55a73bd5e1ccdc94 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Thu, 30 Oct 2025 17:09:35 +0100 Subject: [PATCH 06/22] update: clean up filewriter, add write function, add dockerfile update functionality --- internal/util/docker.go | 65 ++++++++++++++++++ internal/util/filewriter.go | 13 ++-- internal/util/mocks.go | 130 ++++++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 internal/util/docker.go diff --git a/internal/util/docker.go b/internal/util/docker.go new file mode 100644 index 00000000..63bfd29c --- /dev/null +++ b/internal/util/docker.go @@ -0,0 +1,65 @@ +package util + +import ( + "fmt" + "io" + "strings" +) + +// DockerfileManager provides functionality to parse and modify Dockerfiles +type DockerfileManager interface { + UpdateFromStatement(dockerfile io.Reader, baseImage string) (string, error) +} + +type Dockerfile struct{} + +// NewDockerfileManager creates a new instance of DockerfileManager +func NewDockerfileManager() DockerfileManager { + return &Dockerfile{} +} + +// UpdateFromStatement updates the FROM statement in a Dockerfile with a new base image +func (dm *Dockerfile) UpdateFromStatement(dockerfile io.Reader, baseImage string) (string, error) { + content, err := io.ReadAll(dockerfile) + if err != nil { + return "", fmt.Errorf("error reading dockerfile: %w", err) + } + + lines := strings.Split(string(content), "\n") + + // Find and update the first FROM line + updated := false + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(strings.ToUpper(trimmed), "FROM ") { + // Preserve original indentation + indent := "" + for _, char := range line { + if char == ' ' || char == '\t' { + indent += string(char) + } else { + break + } + } + + // Check for platform flag + platformFlag := "" + parts := strings.Fields(trimmed) + if len(parts) >= 2 && strings.HasPrefix(parts[1], "--platform=") { + platformFlag = parts[1] + " " + } + + // Update the line + lines[i] = fmt.Sprintf("%sFROM %s%s", indent, platformFlag, baseImage) + updated = true + break + } + } + + if !updated { + return "", fmt.Errorf("no FROM statement found in dockerfile") + } + + // Join lines back together + return strings.Join(lines, "\n"), nil +} diff --git a/internal/util/filewriter.go b/internal/util/filewriter.go index b686039d..9cca2f45 100644 --- a/internal/util/filewriter.go +++ b/internal/util/filewriter.go @@ -8,12 +8,13 @@ import ( ) type FileIO interface { - MkdirAll(path string, perm os.FileMode) error - OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) - Open(filename string) (*os.File, error) Create(filename string) (*os.File, error) - IsDirectory(filename string) (bool, error) + Open(filename string) (*os.File, error) Exists(filename string) bool + IsDirectory(filename string) (bool, error) + MkdirAll(path string, perm os.FileMode) error + OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) + WriteFile(filename string, data []byte, perm os.FileMode) error ReadDir(dirname string) ([]os.DirEntry, error) } @@ -56,6 +57,10 @@ func (fs *FilesystemWriter) OpenFile(name string, flag int, perm os.FileMode) (* return os.OpenFile(name, flag, perm) } +func (fs *FilesystemWriter) WriteFile(filename string, data []byte, perm os.FileMode) error { + return os.WriteFile(filename, data, perm) +} + func (fs *FilesystemWriter) ReadDir(dirname string) ([]os.DirEntry, error) { return os.ReadDir(dirname) } diff --git a/internal/util/mocks.go b/internal/util/mocks.go index ae55b5ba..69eedc26 100644 --- a/internal/util/mocks.go +++ b/internal/util/mocks.go @@ -7,9 +7,92 @@ package util import ( "github.com/jedib0t/go-pretty/v6/table" mock "github.com/stretchr/testify/mock" + "io" "os" ) +// NewMockDockerfileManager creates a new instance of MockDockerfileManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockDockerfileManager(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDockerfileManager { + mock := &MockDockerfileManager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockDockerfileManager is an autogenerated mock type for the DockerfileManager type +type MockDockerfileManager struct { + mock.Mock +} + +type MockDockerfileManager_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDockerfileManager) EXPECT() *MockDockerfileManager_Expecter { + return &MockDockerfileManager_Expecter{mock: &_m.Mock} +} + +// UpdateFromStatement provides a mock function for the type MockDockerfileManager +func (_mock *MockDockerfileManager) UpdateFromStatement(dockerfile io.Reader, baseImage string) (string, error) { + ret := _mock.Called(dockerfile, baseImage) + + if len(ret) == 0 { + panic("no return value specified for UpdateFromStatement") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func(io.Reader, string) (string, error)); ok { + return returnFunc(dockerfile, baseImage) + } + if returnFunc, ok := ret.Get(0).(func(io.Reader, string) string); ok { + r0 = returnFunc(dockerfile, baseImage) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(io.Reader, string) error); ok { + r1 = returnFunc(dockerfile, baseImage) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockDockerfileManager_UpdateFromStatement_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateFromStatement' +type MockDockerfileManager_UpdateFromStatement_Call struct { + *mock.Call +} + +// UpdateFromStatement is a helper method to define mock.On call +// - dockerfile +// - baseImage +func (_e *MockDockerfileManager_Expecter) UpdateFromStatement(dockerfile interface{}, baseImage interface{}) *MockDockerfileManager_UpdateFromStatement_Call { + return &MockDockerfileManager_UpdateFromStatement_Call{Call: _e.mock.On("UpdateFromStatement", dockerfile, baseImage)} +} + +func (_c *MockDockerfileManager_UpdateFromStatement_Call) Run(run func(dockerfile io.Reader, baseImage string)) *MockDockerfileManager_UpdateFromStatement_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(io.Reader), args[1].(string)) + }) + return _c +} + +func (_c *MockDockerfileManager_UpdateFromStatement_Call) Return(s string, err error) *MockDockerfileManager_UpdateFromStatement_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *MockDockerfileManager_UpdateFromStatement_Call) RunAndReturn(run func(dockerfile io.Reader, baseImage string) (string, error)) *MockDockerfileManager_UpdateFromStatement_Call { + _c.Call.Return(run) + return _c +} + // NewMockFileIO creates a new instance of MockFileIO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockFileIO(t interface { @@ -408,6 +491,53 @@ func (_c *MockFileIO_ReadDir_Call) RunAndReturn(run func(dirname string) ([]os.D return _c } +// WriteFile provides a mock function for the type MockFileIO +func (_mock *MockFileIO) WriteFile(filename string, data []byte, perm os.FileMode) error { + ret := _mock.Called(filename, data, perm) + + if len(ret) == 0 { + panic("no return value specified for WriteFile") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, []byte, os.FileMode) error); ok { + r0 = returnFunc(filename, data, perm) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockFileIO_WriteFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFile' +type MockFileIO_WriteFile_Call struct { + *mock.Call +} + +// WriteFile is a helper method to define mock.On call +// - filename +// - data +// - perm +func (_e *MockFileIO_Expecter) WriteFile(filename interface{}, data interface{}, perm interface{}) *MockFileIO_WriteFile_Call { + return &MockFileIO_WriteFile_Call{Call: _e.mock.On("WriteFile", filename, data, perm)} +} + +func (_c *MockFileIO_WriteFile_Call) Run(run func(filename string, data []byte, perm os.FileMode)) *MockFileIO_WriteFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].([]byte), args[2].(os.FileMode)) + }) + return _c +} + +func (_c *MockFileIO_WriteFile_Call) Return(err error) *MockFileIO_WriteFile_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockFileIO_WriteFile_Call) RunAndReturn(run func(filename string, data []byte, perm os.FileMode) error) *MockFileIO_WriteFile_Call { + _c.Call.Return(run) + return _c +} + // NewMockClosableFile creates a new instance of MockClosableFile. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockClosableFile(t interface { From b0caceeede995b0aa9dc79f366c3a8a69101c0d4 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Thu, 30 Oct 2025 17:10:27 +0100 Subject: [PATCH 07/22] fix: fix lima-config, go did not install --- hack/lima-oms.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/hack/lima-oms.yaml b/hack/lima-oms.yaml index 93822fc5..8dabfd2b 100644 --- a/hack/lima-oms.yaml +++ b/hack/lima-oms.yaml @@ -50,7 +50,11 @@ provision: curl -fsSL https://get.docker.com | sh systemctl disable --now docker apt-get install -y uidmap dbus-user-session - + + # Install Go and build tools + apt-get update + apt-get install -y golang-go make git + - mode: user script: | #!/bin/bash @@ -59,11 +63,6 @@ provision: # Install Docker in rootless mode dockerd-rootless-setuptool.sh install - # Install Go 1.24 and build tools - sudo apt update - sudo apt install golang-go - sudo apt-get install -y make git - # Set up the OMS project cd /home/user/oms export PATH=$PATH:/usr/local/go/bin From 03cf176bdd9acd2b031dd9866435b16ce17d5578 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Thu, 30 Oct 2025 17:20:16 +0100 Subject: [PATCH 08/22] feat: add build image command, add update dockerfile command, unify package functions, clean up GlobalOptions usage --- cli/cmd/beta.go | 2 +- cli/cmd/build.go | 6 +- cli/cmd/build_image.go | 87 ++++++++ cli/cmd/build_image_test.go | 170 +++++++++++++++ cli/cmd/build_images.go | 16 +- cli/cmd/build_images_test.go | 66 ++++-- cli/cmd/extend.go | 2 +- cli/cmd/extend_baseimage.go | 44 ++-- cli/cmd/extend_baseimage_test.go | 85 ++------ cli/cmd/install.go | 2 +- cli/cmd/install_codesphere.go | 26 ++- cli/cmd/install_codesphere_test.go | 14 +- cli/cmd/root.go | 6 +- cli/cmd/update.go | 1 + cli/cmd/update_dockerfile.go | 112 ++++++++++ cli/cmd/update_dockerfile_test.go | 323 +++++++++++++++++++++++++++++ internal/installer/config.go | 18 -- internal/installer/config_test.go | 156 +------------- internal/installer/mocks.go | 222 +++++++++++++++----- internal/installer/package.go | 104 ++++++++-- internal/installer/package_test.go | 158 ++++++++++++++ 21 files changed, 1240 insertions(+), 380 deletions(-) create mode 100644 cli/cmd/build_image.go create mode 100644 cli/cmd/build_image_test.go create mode 100644 cli/cmd/update_dockerfile.go create mode 100644 cli/cmd/update_dockerfile_test.go diff --git a/cli/cmd/beta.go b/cli/cmd/beta.go index 63f5b418..b180201c 100644 --- a/cli/cmd/beta.go +++ b/cli/cmd/beta.go @@ -12,7 +12,7 @@ type BetaCmd struct { cmd *cobra.Command } -func AddBetaCmd(rootCmd *cobra.Command, opts *GlobalOptions) { +func AddBetaCmd(rootCmd *cobra.Command, opts GlobalOptions) { beta := BetaCmd{ cmd: &cobra.Command{ Use: "beta", diff --git a/cli/cmd/build.go b/cli/cmd/build.go index 04802e0c..71f7d849 100644 --- a/cli/cmd/build.go +++ b/cli/cmd/build.go @@ -13,7 +13,7 @@ type BuildCmd struct { cmd *cobra.Command } -func AddBuildCmd(rootCmd *cobra.Command, opts *GlobalOptions) { +func AddBuildCmd(rootCmd *cobra.Command, opts GlobalOptions) { build := BuildCmd{ cmd: &cobra.Command{ Use: "build", @@ -21,6 +21,8 @@ func AddBuildCmd(rootCmd *cobra.Command, opts *GlobalOptions) { Long: io.Long(`Build and push container images to a registry using the provided configuration.`), }, } - rootCmd.AddCommand(build.cmd) AddBuildImagesCmd(build.cmd, opts) + AddBuildImageCmd(build.cmd, opts) + + rootCmd.AddCommand(build.cmd) } diff --git a/cli/cmd/build_image.go b/cli/cmd/build_image.go new file mode 100644 index 00000000..565d8b1d --- /dev/null +++ b/cli/cmd/build_image.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "context" + "fmt" + "log" + + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/system" + "github.com/codesphere-cloud/oms/internal/util" + "github.com/spf13/cobra" +) + +// BuildImageCmd represents the build image command +type BuildImageCmd struct { + cmd *cobra.Command + Opts BuildImageOpts + Env env.Env +} + +type BuildImageOpts struct { + GlobalOptions + Dockerfile string + Package string + Registry string +} + +func (c *BuildImageCmd) RunE(cmd *cobra.Command, args []string) error { + pm := installer.NewPackage(c.Env.GetOmsWorkdir(), c.Opts.Package) + im := system.NewImage(context.Background()) + + return c.BuildImage(pm, im) +} + +func AddBuildImageCmd(parentCmd *cobra.Command, opts GlobalOptions) { + imageCmd := &BuildImageCmd{ + cmd: &cobra.Command{ + Use: "image", + Short: "Build and push Docker image using Dockerfile and Codesphere package version", + Long: `Build a Docker image from a Dockerfile and push it to a registry, tagged with the Codesphere version from the package.`, + Example: formatExamplesWithBinary("build image", []io.Example{ + {Cmd: "--dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz --registry my-registry.com/my-image", Desc: "Build image for Codesphere version 1.68.0 and push to specified registry"}, + }, "oms-cli"), + Args: cobra.ExactArgs(0), + }, + Opts: BuildImageOpts{GlobalOptions: opts}, + Env: env.NewEnv(), + } + + 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)") + + util.MarkFlagRequired(imageCmd.cmd, "dockerfile") + util.MarkFlagRequired(imageCmd.cmd, "package") + util.MarkFlagRequired(imageCmd.cmd, "registry") + + parentCmd.AddCommand(imageCmd.cmd) + + imageCmd.cmd.RunE = imageCmd.RunE +} + +// AddBuildImageCmd adds the build image command to the parent command +func (c *BuildImageCmd) BuildImage(pm installer.PackageManager, im system.ImageManager) error { + codesphereVersion, err := pm.GetCodesphereVersion() + if err != nil { + return fmt.Errorf("failed to get codesphere version from package: %w", err) + } + + targetImage := fmt.Sprintf("%s:%s", c.Opts.Registry, codesphereVersion) + + err = im.BuildImage(c.Opts.Dockerfile, targetImage, ".") + if err != nil { + return fmt.Errorf("failed to build image %s: %w", targetImage, err) + } + + err = im.PushImage(targetImage) + if err != nil { + return fmt.Errorf("failed to push image %s: %w", targetImage, err) + } + + log.Printf("Successfully built and pushed image: %s", targetImage) + + return nil +} diff --git a/cli/cmd/build_image_test.go b/cli/cmd/build_image_test.go new file mode 100644 index 00000000..0bcef9d0 --- /dev/null +++ b/cli/cmd/build_image_test.go @@ -0,0 +1,170 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd_test + +import ( + "errors" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/codesphere-cloud/oms/cli/cmd" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/system" +) + +var _ = Describe("BuildImageCmd", func() { + var ( + c cmd.BuildImageCmd + opts cmd.BuildImageOpts + globalOpts cmd.GlobalOptions + mockEnv *env.MockEnv + ) + + BeforeEach(func() { + mockEnv = env.NewMockEnv(GinkgoT()) + globalOpts = cmd.GlobalOptions{} + opts = cmd.BuildImageOpts{ + GlobalOptions: globalOpts, + Dockerfile: "Dockerfile", + Package: "codesphere-vcodesphere-v1.66.0.tar.gz", + Registry: "my-registry.com/my-image", + } + c = cmd.BuildImageCmd{ + Opts: opts, + Env: mockEnv, + } + }) + + AfterEach(func() { + mockEnv.AssertExpectations(GinkgoT()) + }) + + Context("RunE method", func() { + It("calls BuildImage and fails when package manager fails", func() { + mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") + + err := c.RunE(nil, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get codesphere version from package")) + }) + + It("succeeds with valid options", func() { + mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") + + // This will fail in real scenario because it tries to extract from real package + // But it should at least call the correct methods + err := c.RunE(nil, []string{}) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("BuildImage method", func() { + It("fails when package manager fails to get codesphere version", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + mockPackageManager.EXPECT().GetCodesphereVersion().Return("", errors.New("failed to extract version")) + + err := c.BuildImage(mockPackageManager, mockImageManager) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get codesphere version from package")) + }) + + It("fails when image build fails", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Dockerfile = "Dockerfile" + c.Opts.Registry = "my-registry.com/my-image" + + 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")) + + err := c.BuildImage(mockPackageManager, mockImageManager) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to build image")) + }) + + It("fails when image push fails", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Dockerfile = "Dockerfile" + c.Opts.Registry = "my-registry.com/my-image" + + 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")) + + err := c.BuildImage(mockPackageManager, mockImageManager) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to push image")) + }) + + It("successfully builds and pushes image", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Dockerfile = "Dockerfile" + c.Opts.Registry = "my-registry.com/my-image" + + 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) + + err := c.BuildImage(mockPackageManager, mockImageManager) + Expect(err).To(BeNil()) + }) + }) +}) + +var _ = Describe("AddBuildImageCmd", func() { + var ( + parentCmd *cobra.Command + globalOpts cmd.GlobalOptions + ) + + BeforeEach(func() { + parentCmd = &cobra.Command{Use: "build"} + globalOpts = cmd.GlobalOptions{} + }) + + It("adds the image command with correct properties and flags", func() { + cmd.AddBuildImageCmd(parentCmd, globalOpts) + + var imageCmd *cobra.Command + for _, c := range parentCmd.Commands() { + if c.Use == "image" { + imageCmd = c + break + } + } + + Expect(imageCmd).NotTo(BeNil()) + Expect(imageCmd.Use).To(Equal("image")) + Expect(imageCmd.Short).To(Equal("Build and push Docker image using Dockerfile and Codesphere package version")) + Expect(imageCmd.Long).To(ContainSubstring("Build a Docker image from a Dockerfile and push it to a registry")) + Expect(imageCmd.Long).To(ContainSubstring("tagged with the Codesphere version")) + Expect(imageCmd.RunE).NotTo(BeNil()) + + // Check required flags + dockerfileFlag := imageCmd.Flags().Lookup("dockerfile") + Expect(dockerfileFlag).NotTo(BeNil()) + Expect(dockerfileFlag.Shorthand).To(Equal("d")) + Expect(dockerfileFlag.Usage).To(ContainSubstring("Path to the Dockerfile to build")) + + packageFlag := imageCmd.Flags().Lookup("package") + Expect(packageFlag).NotTo(BeNil()) + Expect(packageFlag.Shorthand).To(Equal("p")) + Expect(packageFlag.Usage).To(ContainSubstring("Path to the Codesphere package")) + + registryFlag := imageCmd.Flags().Lookup("registry") + Expect(registryFlag).NotTo(BeNil()) + Expect(registryFlag.Shorthand).To(Equal("r")) + Expect(registryFlag.Usage).To(ContainSubstring("Registry URL to push to")) + }) +}) diff --git a/cli/cmd/build_images.go b/cli/cmd/build_images.go index ff550710..cfff9fe3 100644 --- a/cli/cmd/build_images.go +++ b/cli/cmd/build_images.go @@ -13,7 +13,6 @@ import ( "github.com/codesphere-cloud/oms/internal/installer" "github.com/codesphere-cloud/oms/internal/system" "github.com/codesphere-cloud/oms/internal/util" - "github.com/codesphere-cloud/oms/internal/version" "github.com/spf13/cobra" ) @@ -25,15 +24,16 @@ type BuildImagesCmd struct { } type BuildImagesOpts struct { - *GlobalOptions + GlobalOptions Config string } func (c *BuildImagesCmd) RunE(_ *cobra.Command, args []string) error { + pm := installer.NewPackage(c.Env.GetOmsWorkdir(), c.Opts.Config) cm := installer.NewConfig() im := system.NewImage(context.Background()) - err := c.BuildAndPushImages(cm, im) + err := c.BuildAndPushImages(pm, cm, im) if err != nil { return fmt.Errorf("failed to build and push images: %w", err) } @@ -41,7 +41,7 @@ func (c *BuildImagesCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddBuildImagesCmd(build *cobra.Command, opts *GlobalOptions) { +func AddBuildImagesCmd(build *cobra.Command, opts GlobalOptions) { buildImages := BuildImagesCmd{ cmd: &cobra.Command{ Use: "images", @@ -61,7 +61,7 @@ func AddBuildImagesCmd(build *cobra.Command, opts *GlobalOptions) { buildImages.cmd.RunE = buildImages.RunE } -func (c *BuildImagesCmd) BuildAndPushImages(cm installer.ConfigManager, im system.ImageManager) error { +func (c *BuildImagesCmd) BuildAndPushImages(pm installer.PackageManager, cm installer.ConfigManager, im system.ImageManager) error { config, err := cm.ParseConfigYaml(c.Opts.Config) if err != nil { return fmt.Errorf("failed to parse config: %w", err) @@ -74,8 +74,10 @@ func (c *BuildImagesCmd) BuildAndPushImages(cm installer.ConfigManager, im syste return fmt.Errorf("registry server not defined in the config") } - v := &version.Build{} - codesphereVersion := v.Version() + codesphereVersion, err := pm.GetCodesphereVersion() + if err != nil { + return fmt.Errorf("failed to get codesphere version from package: %w", err) + } for imageName, imageConfig := range config.Codesphere.DeployConfig.Images { for flavorName, flavorConfig := range imageConfig.Flavors { diff --git a/cli/cmd/build_images_test.go b/cli/cmd/build_images_test.go index 9d638035..f023ca72 100644 --- a/cli/cmd/build_images_test.go +++ b/cli/cmd/build_images_test.go @@ -47,7 +47,7 @@ var _ = Describe("BuildImagesCmd", func() { mockEnv = env.NewMockEnv(GinkgoT()) globalOpts = cmd.GlobalOptions{} opts = &cmd.BuildImagesOpts{ - GlobalOptions: &globalOpts, + GlobalOptions: globalOpts, Config: "", } c = cmd.BuildImagesCmd{ @@ -62,14 +62,21 @@ var _ = Describe("BuildImagesCmd", func() { Context("RunE method", func() { It("calls BuildAndPushImages and fails on config parsing", func() { + mockEnv := env.NewMockEnv(GinkgoT()) + c.Env = mockEnv c.Opts.Config = "non-existent-config.yaml" + mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") + err := c.RunE(nil, []string{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to build and push images")) }) It("succeeds with valid config and operations", func() { + mockEnv := env.NewMockEnv(GinkgoT()) + c.Env = mockEnv + tempConfigFile, err := os.CreateTemp("", "test-config.yaml") Expect(err).To(BeNil()) defer os.Remove(tempConfigFile.Name()) @@ -80,6 +87,8 @@ var _ = Describe("BuildImagesCmd", func() { c.Opts.Config = tempConfigFile.Name() + mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") + err = c.RunE(nil, []string{}) // This will fail because the dockerfile doesn't exist and build will fail // But it should at least parse the config successfully @@ -90,18 +99,20 @@ var _ = Describe("BuildImagesCmd", func() { Context("BuildAndPushImages method", func() { It("fails when config manager fails to parse config", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) c.Opts.Config = "non-existent-config.yaml" mockConfigManager.EXPECT().ParseConfigYaml("non-existent-config.yaml").Return(files.RootConfig{}, errors.New("failed to parse config")) - err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to parse config")) }) It("fails when no images are defined in config", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) @@ -115,12 +126,13 @@ var _ = Describe("BuildImagesCmd", func() { } mockConfigManager.EXPECT().ParseConfigYaml("empty-config.yaml").Return(emptyConfig, nil) - err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no images defined in the config")) }) It("fails when registry server is empty", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) @@ -150,12 +162,13 @@ var _ = Describe("BuildImagesCmd", func() { } mockConfigManager.EXPECT().ParseConfigYaml("config-without-registry.yaml").Return(configWithoutRegistry, nil) - err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("registry server not defined in the config")) }) It("skips flavors without dockerfile", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) @@ -184,12 +197,14 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-without-dockerfile.yaml").Return(configWithoutDockerfile, nil) + mockPackageManager.EXPECT().GetCodesphereVersion().Return("1.0.0", nil) - err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) Expect(err).To(BeNil()) }) It("fails when image build fails", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) @@ -218,14 +233,16 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-with-dockerfile.yaml").Return(configWithDockerfile, nil) - mockImageManager.EXPECT().BuildImage("Dockerfile", "registry.example.com/my-ubuntu-24.04-default:0.0.0", ".").Return(errors.New("build failed")) + 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")) - err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to build image")) }) It("fails when image push fails", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) @@ -254,15 +271,17 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-with-dockerfile.yaml").Return(configWithDockerfile, nil) - mockImageManager.EXPECT().BuildImage("Dockerfile", "registry.example.com/my-ubuntu-24.04-default:0.0.0", ".").Return(nil) - mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-default:0.0.0").Return(errors.New("push failed")) + 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")) - err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to push image")) }) It("successfully builds and pushes single image", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) @@ -291,14 +310,16 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-with-dockerfile.yaml").Return(configWithDockerfile, nil) - mockImageManager.EXPECT().BuildImage("Dockerfile", "registry.example.com/my-ubuntu-24.04-default:0.0.0", ".").Return(nil) - mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-default:0.0.0").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) - err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) Expect(err).To(BeNil()) }) It("successfully builds and pushes multiple images with different flavors", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) @@ -345,20 +366,21 @@ var _ = Describe("BuildImagesCmd", func() { }, } mockConfigManager.EXPECT().ParseConfigYaml("config-with-multiple-images.yaml").Return(configWithMultipleImages, nil) + mockPackageManager.EXPECT().GetCodesphereVersion().Return("1.0.0", nil) // Expect calls for my-ubuntu-24.04 default flavor - mockImageManager.EXPECT().BuildImage("Dockerfile.default", "registry.example.com/my-ubuntu-24.04-default:0.0.0", ".").Return(nil) - mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-default:0.0.0").Return(nil) + mockImageManager.EXPECT().BuildImage("Dockerfile.default", "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) // Expect calls for my-ubuntu-24.04 minimal flavor - mockImageManager.EXPECT().BuildImage("Dockerfile.minimal", "registry.example.com/my-ubuntu-24.04-minimal:0.0.0", ".").Return(nil) - mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-minimal:0.0.0").Return(nil) + mockImageManager.EXPECT().BuildImage("Dockerfile.minimal", "registry.example.com/my-ubuntu-24.04-minimal:1.0.0", ".").Return(nil) + mockImageManager.EXPECT().PushImage("registry.example.com/my-ubuntu-24.04-minimal:1.0.0").Return(nil) // Expect calls for my-alpine-3.18 default flavor - mockImageManager.EXPECT().BuildImage("Dockerfile.alpine", "registry.example.com/my-alpine-3.18-default:0.0.0", ".").Return(nil) - mockImageManager.EXPECT().PushImage("registry.example.com/my-alpine-3.18-default:0.0.0").Return(nil) + mockImageManager.EXPECT().BuildImage("Dockerfile.alpine", "registry.example.com/my-alpine-3.18-default:1.0.0", ".").Return(nil) + mockImageManager.EXPECT().PushImage("registry.example.com/my-alpine-3.18-default:1.0.0").Return(nil) - err := c.BuildAndPushImages(mockConfigManager, mockImageManager) + err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) Expect(err).To(BeNil()) }) }) @@ -367,12 +389,12 @@ var _ = Describe("BuildImagesCmd", func() { var _ = Describe("AddBuildImagesCmd", func() { var ( parentCmd *cobra.Command - globalOpts *cmd.GlobalOptions + globalOpts cmd.GlobalOptions ) BeforeEach(func() { parentCmd = &cobra.Command{Use: "build"} - globalOpts = &cmd.GlobalOptions{} + globalOpts = cmd.GlobalOptions{} }) It("adds the images command with correct properties and flags", func() { diff --git a/cli/cmd/extend.go b/cli/cmd/extend.go index a0b0571c..768f560d 100644 --- a/cli/cmd/extend.go +++ b/cli/cmd/extend.go @@ -13,7 +13,7 @@ type ExtendCmd struct { cmd *cobra.Command } -func AddExtendCmd(rootCmd *cobra.Command, opts *GlobalOptions) { +func AddExtendCmd(rootCmd *cobra.Command, opts GlobalOptions) { extend := ExtendCmd{ cmd: &cobra.Command{ Use: "extend", diff --git a/cli/cmd/extend_baseimage.go b/cli/cmd/extend_baseimage.go index 3540d91d..61fd670d 100644 --- a/cli/cmd/extend_baseimage.go +++ b/cli/cmd/extend_baseimage.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "log" - "path" "github.com/spf13/cobra" @@ -27,15 +26,13 @@ type ExtendBaseimageCmd struct { } type ExtendBaseimageOpts struct { - *GlobalOptions + GlobalOptions Package string Dockerfile string + Baseimage string Force bool } -const baseimagePath = "./codesphere/images" -const defaultBaseimage = "workspace-agent-24.04.tar" - func (c *ExtendBaseimageCmd) RunE(_ *cobra.Command, args []string) error { if c.Opts.Package == "" { return errors.New("required option package not set") @@ -43,10 +40,9 @@ func (c *ExtendBaseimageCmd) RunE(_ *cobra.Command, args []string) error { workdir := c.Env.GetOmsWorkdir() pm := installer.NewPackage(workdir, c.Opts.Package) - cm := installer.NewConfig() im := system.NewImage(context.Background()) - err := c.ExtendBaseimage(pm, cm, im, args) + err := c.ExtendBaseimage(pm, im) if err != nil { return fmt.Errorf("failed to extend baseimage: %w", err) } @@ -54,7 +50,7 @@ func (c *ExtendBaseimageCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddExtendBaseimageCmd(extend *cobra.Command, opts *GlobalOptions) { +func AddExtendBaseimageCmd(extend *cobra.Command, opts GlobalOptions) { baseimage := ExtendBaseimageCmd{ cmd: &cobra.Command{ Use: "baseimage", @@ -70,40 +66,30 @@ func AddExtendBaseimageCmd(extend *cobra.Command, opts *GlobalOptions) { } baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load base image from") baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Dockerfile, "dockerfile", "d", "Dockerfile", "Output Dockerfile to generate for extending the base image") + baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Baseimage, "baseimage", "b", "", "Base image file name inside the package to extend (default: 'workspace-agent-24.04.tar')") baseimage.cmd.Flags().BoolVarP(&baseimage.Opts.Force, "force", "f", false, "Enforce package extraction") + extend.AddCommand(baseimage.cmd) + baseimage.cmd.RunE = baseimage.RunE } -func (c *ExtendBaseimageCmd) ExtendBaseimage(pm installer.PackageManager, cm installer.ConfigManager, im system.ImageManager, args []string) error { - baseImageTarPath := path.Join(baseimagePath, defaultBaseimage) - err := pm.ExtractDependency(baseImageTarPath, c.Opts.Force) - if err != nil { - return fmt.Errorf("failed to extract package to workdir: %w", err) - } - - extractedBaseImagePath := pm.GetDependencyPath(baseImageTarPath) - - index, err := cm.ExtractOciImageIndex(baseImageTarPath) +func (c *ExtendBaseimageCmd) ExtendBaseimage(pm installer.PackageManager, im system.ImageManager) error { + imagePath, imageName, err := pm.GetImagePathAndName(c.Opts.Baseimage, c.Opts.Force) if err != nil { - return fmt.Errorf("failed to extract OCI image index: %w", err) + return fmt.Errorf("failed to get image name: %w", err) } - imagenames, err := index.ExtractImageNames() - if err != nil || len(imagenames) == 0 { - return fmt.Errorf("failed to read image tags: %w", err) - } - log.Println(imagenames) - - err = tmpl.GenerateDockerfile(pm.FileIO(), c.Opts.Dockerfile, imagenames[0]) + err = tmpl.GenerateDockerfile(pm.FileIO(), c.Opts.Dockerfile, imageName) if err != nil { return fmt.Errorf("failed to generate dockerfile: %w", err) } - log.Printf("Loading container image from package into local docker daemon: %s", extractedBaseImagePath) - err = im.LoadImage(extractedBaseImagePath) + log.Printf("Loading container image from package into local docker daemon: %s", imagePath) + + err = im.LoadImage(imagePath) if err != nil { - return fmt.Errorf("failed to load baseimage file %s: %w", baseImageTarPath, err) + return fmt.Errorf("failed to load baseimage file %s: %w", imagePath, err) } return nil diff --git a/cli/cmd/extend_baseimage_test.go b/cli/cmd/extend_baseimage_test.go index 759416e9..32de8c19 100644 --- a/cli/cmd/extend_baseimage_test.go +++ b/cli/cmd/extend_baseimage_test.go @@ -14,7 +14,6 @@ import ( "github.com/codesphere-cloud/oms/cli/cmd" "github.com/codesphere-cloud/oms/internal/env" "github.com/codesphere-cloud/oms/internal/installer" - "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/system" "github.com/codesphere-cloud/oms/internal/util" ) @@ -31,7 +30,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { mockEnv = env.NewMockEnv(GinkgoT()) globalOpts = cmd.GlobalOptions{} opts = &cmd.ExtendBaseimageOpts{ - GlobalOptions: &globalOpts, + GlobalOptions: globalOpts, Dockerfile: "Dockerfile", Force: false, } @@ -65,129 +64,87 @@ var _ = Describe("ExtendBaseimageCmd", func() { Context("ExtendBaseimage method", func() { It("fails when package manager extraction fails", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) - mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(errors.New("extraction failed")) + mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("", "", errors.New("failed to extract package to workdir: extraction failed")) - err := c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + err := c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract package to workdir")) + Expect(err.Error()).To(ContainSubstring("failed to get image name")) }) It("fails when config manager fails to extract OCI image index", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) - mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(nil) - mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/workspace-agent-24.04.tar").Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar") - mockConfigManager.EXPECT().ExtractOciImageIndex("codesphere/images/workspace-agent-24.04.tar").Return(files.OCIImageIndex{}, errors.New("failed to extract index")) + mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("", "", errors.New("failed to extract OCI image index: index extraction failed")) - err := c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + err := c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract OCI image index")) + Expect(err.Error()).To(ContainSubstring("failed to get image name")) }) It("fails when OCI image index has no image names", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) - // Create empty OCI index that will return no image names - ociIndex := files.OCIImageIndex{ - Manifests: []files.ManifestEntry{}, - } - mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(nil) - mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/workspace-agent-24.04.tar").Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar") - mockConfigManager.EXPECT().ExtractOciImageIndex("codesphere/images/workspace-agent-24.04.tar").Return(ociIndex, nil) + mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("", "", errors.New("failed to read image tags: no image names found")) - err := c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + err := c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to read image tags")) + Expect(err.Error()).To(ContainSubstring("failed to get image name")) }) It("fails when image manager fails to load image", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) mockFileIO := util.NewMockFileIO(GinkgoT()) - // Create OCI index with valid image names - ociIndex := files.OCIImageIndex{ - Manifests: []files.ManifestEntry{ - { - Annotations: map[string]string{ - "io.containerd.image.name": "ubuntu:24.04-base", - }, - }, - }, - } - mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(nil) - mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/workspace-agent-24.04.tar").Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar") - mockConfigManager.EXPECT().ExtractOciImageIndex("codesphere/images/workspace-agent-24.04.tar").Return(ociIndex, nil) - mockPackageManager.EXPECT().FileIO().Return(mockFileIO) - // Create a temporary file for the Dockerfile generation to work with tempFile, err := os.CreateTemp("", "dockerfile-test-*") Expect(err).To(BeNil()) defer os.Remove(tempFile.Name()) defer tempFile.Close() - // Mock Dockerfile generation + mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04-base", nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) mockFileIO.EXPECT().Create("Dockerfile").Return(tempFile, nil) mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(errors.New("load failed")) - err = c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + err = c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to load baseimage file")) }) It("uses force flag when extracting dependencies", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) c.Opts.Force = true - mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", true).Return(errors.New("extraction failed")) + mockPackageManager.EXPECT().GetImagePathAndName("", true).Return("", "", errors.New("failed to extract package to workdir: extraction failed")) - err := c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + err := c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract package to workdir")) + Expect(err.Error()).To(ContainSubstring("failed to get image name")) }) It("successfully completes workflow until dockerfile generation", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) - mockConfigManager := installer.NewMockConfigManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) mockFileIO := util.NewMockFileIO(GinkgoT()) - // Create OCI index with valid image names - ociIndex := files.OCIImageIndex{ - Manifests: []files.ManifestEntry{ - { - Annotations: map[string]string{ - "io.containerd.image.name": "ubuntu:24.04-base", - }, - }, - }, - } - mockPackageManager.EXPECT().ExtractDependency("codesphere/images/workspace-agent-24.04.tar", false).Return(nil) - mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/workspace-agent-24.04.tar").Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar") - mockConfigManager.EXPECT().ExtractOciImageIndex("codesphere/images/workspace-agent-24.04.tar").Return(ociIndex, nil) - mockPackageManager.EXPECT().FileIO().Return(mockFileIO) - // Create a temporary file for the Dockerfile generation to work with tempFile, err := os.CreateTemp("", "dockerfile-test-*") Expect(err).To(BeNil()) defer os.Remove(tempFile.Name()) defer tempFile.Close() - // Mock Dockerfile generation + mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04-base", nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) mockFileIO.EXPECT().Create("Dockerfile").Return(tempFile, nil) mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(nil) - err = c.ExtendBaseimage(mockPackageManager, mockConfigManager, mockImageManager, []string{}) + err = c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(BeNil()) }) }) @@ -196,12 +153,12 @@ var _ = Describe("ExtendBaseimageCmd", func() { var _ = Describe("AddExtendBaseimageCmd", func() { var ( parentCmd *cobra.Command - globalOpts *cmd.GlobalOptions + globalOpts cmd.GlobalOptions ) BeforeEach(func() { parentCmd = &cobra.Command{Use: "extend"} - globalOpts = &cmd.GlobalOptions{} + globalOpts = cmd.GlobalOptions{} }) It("adds the baseimage command with correct properties and flags", func() { diff --git a/cli/cmd/install.go b/cli/cmd/install.go index 7402281d..85899d9a 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -13,7 +13,7 @@ type InstallCmd struct { cmd *cobra.Command } -func AddInstallCmd(rootCmd *cobra.Command, opts *GlobalOptions) { +func AddInstallCmd(rootCmd *cobra.Command, opts GlobalOptions) { install := InstallCmd{ cmd: &cobra.Command{ Use: "install", diff --git a/cli/cmd/install_codesphere.go b/cli/cmd/install_codesphere.go index bada1753..82a4cadd 100644 --- a/cli/cmd/install_codesphere.go +++ b/cli/cmd/install_codesphere.go @@ -31,7 +31,7 @@ type InstallCodesphereCmd struct { } type InstallCodesphereOpts struct { - *GlobalOptions + GlobalOptions Package string Force bool Config string @@ -53,7 +53,7 @@ func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) { +func AddInstallCodesphereCmd(install *cobra.Command, opts GlobalOptions) { codesphere := InstallCodesphereCmd{ cmd: &cobra.Command{ Use: "codesphere", @@ -75,6 +75,7 @@ func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) { util.MarkFlagRequired(codesphere.cmd, "priv-key") install.AddCommand(codesphere.cmd) + codesphere.cmd.RunE = codesphere.RunE } @@ -131,6 +132,27 @@ func (c *InstallCodesphereCmd) ExtractAndInstall(pm installer.PackageManager, cm } log.Printf("Loaded root image '%s'", extractedImagePath) + // TODO: This is duplicated from update_dockerfile.go, refactor into shared function + dockerfileFile, err := pm.FileIO().Open(dockerfile) + if err != nil { + return fmt.Errorf("failed to open dockerfile %s: %w", dockerfile, err) + } + defer util.CloseFileIgnoreError(dockerfileFile) + + dockerfileManager := util.NewDockerfileManager() + updatedContent, err := dockerfileManager.UpdateFromStatement(dockerfileFile, rootImageName) + if err != nil { + return fmt.Errorf("failed to update FROM statement: %w", err) + } + + err = pm.FileIO().WriteFile(dockerfile, []byte(updatedContent), 0644) + if err != nil { + return fmt.Errorf("failed to write updated dockerfile: %w", err) + } + + log.Printf("Successfully updated FROM statement in %s to use %s", dockerfile, rootImageName) + // TODO: End duplicated code + dockerfileName := filepath.Base(dockerfile) dockerfileDir := filepath.Dir(dockerfile) err = im.BuildImage(dockerfileName, rootImageName, dockerfileDir) diff --git a/cli/cmd/install_codesphere_test.go b/cli/cmd/install_codesphere_test.go index c03bba10..e56af332 100644 --- a/cli/cmd/install_codesphere_test.go +++ b/cli/cmd/install_codesphere_test.go @@ -32,7 +32,7 @@ var _ = Describe("InstallCodesphereCmd", func() { mockEnv = env.NewMockEnv(GinkgoT()) globalOpts = cmd.GlobalOptions{} opts = &cmd.InstallCodesphereOpts{ - GlobalOptions: &globalOpts, + GlobalOptions: globalOpts, Package: "codesphere-v1.66.0-installer.tar.gz", Force: false, } @@ -295,12 +295,14 @@ var _ = Describe("InstallCodesphereCmd", func() { mockPackageManager.EXPECT().ExtractDependency("codesphere/images/ubuntu.tar", false).Return(nil) mockPackageManager.EXPECT().GetDependencyPath("codesphere/images/ubuntu.tar").Return("/test/workdir/deps/codesphere/images/ubuntu.tar") mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/ubuntu.tar").Return(nil) - mockImageManager.EXPECT().BuildImage("workspace.Dockerfile", "ubuntu", ".").Return(nil) + + // Mock for reading the Dockerfile + mockFileIO.EXPECT().Open("workspace.Dockerfile").Return(nil, errors.New("dockerfile not found")) err := c.ExtractAndInstall(mockPackageManager, mockConfigManager, mockImageManager, "linux", "amd64") - // Should fail when trying to make fake node executable + // Should fail when trying to read the dockerfile Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to make node executable")) + Expect(err.Error()).To(ContainSubstring("dockerfile not found")) }) }) }) @@ -366,12 +368,12 @@ var _ = Describe("InstallCodesphereCmd", func() { var _ = Describe("AddInstallCodesphereCmd", func() { var ( parentCmd *cobra.Command - globalOpts *cmd.GlobalOptions + globalOpts cmd.GlobalOptions ) BeforeEach(func() { parentCmd = &cobra.Command{Use: "install"} - globalOpts = &cmd.GlobalOptions{} + globalOpts = cmd.GlobalOptions{} }) It("adds the codesphere command with correct properties and flags", func() { diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 5010cddb..e0ce49e2 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -28,14 +28,14 @@ func GetRootCmd() *cobra.Command { } // General commands AddVersionCmd(rootCmd) - AddBetaCmd(rootCmd, &opts) + AddBetaCmd(rootCmd, opts) AddUpdateCmd(rootCmd, opts) // Package commands AddListCmd(rootCmd, opts) AddDownloadCmd(rootCmd, opts) - AddInstallCmd(rootCmd, &opts) - AddBuildCmd(rootCmd, &opts) + AddInstallCmd(rootCmd, opts) + AddBuildCmd(rootCmd, opts) AddLicensesCmd(rootCmd) // OMS API key management commands diff --git a/cli/cmd/update.go b/cli/cmd/update.go index e41a8244..106c9ef2 100644 --- a/cli/cmd/update.go +++ b/cli/cmd/update.go @@ -33,6 +33,7 @@ func AddUpdateCmd(rootCmd *cobra.Command, opts GlobalOptions) { AddDownloadPackageCmd(updateCmd.cmd, opts) AddOmsUpdateCmd(updateCmd.cmd) AddApiKeyUpdateCmd(updateCmd.cmd) + AddUpdateDockerfileCmd(updateCmd.cmd, opts) rootCmd.AddCommand(updateCmd.cmd) } diff --git a/cli/cmd/update_dockerfile.go b/cli/cmd/update_dockerfile.go new file mode 100644 index 00000000..8022669f --- /dev/null +++ b/cli/cmd/update_dockerfile.go @@ -0,0 +1,112 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "log" + + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/system" + "github.com/codesphere-cloud/oms/internal/util" + "github.com/spf13/cobra" +) + +type UpdateDockerfileCmd struct { + cmd *cobra.Command + Opts UpdateDockerfileOpts + Env env.Env +} + +type UpdateDockerfileOpts struct { + GlobalOptions + Package string + Dockerfile string + Baseimage string + Force bool +} + +func (c *UpdateDockerfileCmd) RunE(_ *cobra.Command, args []string) error { + if c.Opts.Package == "" { + return errors.New("required option package not set") + } + + workdir := c.Env.GetOmsWorkdir() + pm := installer.NewPackage(workdir, c.Opts.Package) + im := system.NewImage(context.Background()) + + err := c.UpdateDockerfile(pm, im, args) + if err != nil { + return fmt.Errorf("failed to update dockerfile: %w", err) + } + + return nil +} + +func AddUpdateDockerfileCmd(parentCmd *cobra.Command, opts GlobalOptions) { + dockerfileCmd := &UpdateDockerfileCmd{ + cmd: &cobra.Command{ + Use: "dockerfile", + Short: "Update FROM statement in Dockerfile with base image from package", + Long: `Update the FROM statement in a Dockerfile to use the base image from a Codesphere package. + +This command extracts the base image from a Codesphere package and updates the FROM statement +in the specified Dockerfile to use that base image. The base image is loaded into the local Docker daemon so it can be used for building.`, + Example: formatExamplesWithBinary("update dockerfile", []io.Example{ + {Cmd: "--dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz", Desc: "Update Dockerfile to use the default base image from the package (workspace-agent-24.04.tar)"}, + {Cmd: "--dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz --baseimage workspace-agent-20.04.tar", Desc: "Update Dockerfile to use the workspace-agent-20.04.tar base image from the package"}, + }, "oms-cli"), + Args: cobra.ExactArgs(0), + }, + Opts: UpdateDockerfileOpts{GlobalOptions: opts}, + Env: env.NewEnv(), + } + + dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Dockerfile, "dockerfile", "d", "", "Path to the Dockerfile to update (required)") + dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Package, "package", "p", "", "Path to the Codesphere package (required)") + dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Baseimage, "baseimage", "b", "", "Name of the base image to use (required)") + dockerfileCmd.cmd.Flags().BoolVarP(&dockerfileCmd.Opts.Force, "force", "f", false, "Force update even if Dockerfile already exists") + + util.MarkFlagRequired(dockerfileCmd.cmd, "dockerfile") + util.MarkFlagRequired(dockerfileCmd.cmd, "package") + + parentCmd.AddCommand(dockerfileCmd.cmd) + + dockerfileCmd.cmd.RunE = dockerfileCmd.RunE +} + +func (c *UpdateDockerfileCmd) UpdateDockerfile(pm installer.PackageManager, im system.ImageManager, args []string) error { + imagePath, imageName, err := pm.GetImagePathAndName(c.Opts.Baseimage, c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to get image name: %w", err) + } + + dockerfileFile, err := pm.FileIO().Open(c.Opts.Dockerfile) + if err != nil { + return fmt.Errorf("failed to open dockerfile %s: %w", c.Opts.Dockerfile, err) + } + defer util.CloseFileIgnoreError(dockerfileFile) + + dockerfileManager := util.NewDockerfileManager() + updatedContent, err := dockerfileManager.UpdateFromStatement(dockerfileFile, imageName) + if err != nil { + return fmt.Errorf("failed to update FROM statement: %w", err) + } + + err = pm.FileIO().WriteFile(c.Opts.Dockerfile, []byte(updatedContent), 0644) + if err != nil { + return fmt.Errorf("failed to write updated dockerfile: %w", err) + } + + log.Printf("Successfully updated FROM statement in %s to use %s", c.Opts.Dockerfile, imageName) + log.Printf("Loading container image from package into local docker daemon: %s", imagePath) + + err = im.LoadImage(imagePath) + if err != nil { + return fmt.Errorf("failed to load baseimage file %s: %w", imagePath, err) + } + + return nil +} diff --git a/cli/cmd/update_dockerfile_test.go b/cli/cmd/update_dockerfile_test.go new file mode 100644 index 00000000..09f874f6 --- /dev/null +++ b/cli/cmd/update_dockerfile_test.go @@ -0,0 +1,323 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd_test + +import ( + "errors" + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/codesphere-cloud/oms/cli/cmd" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/system" + "github.com/codesphere-cloud/oms/internal/util" +) + +const sampleDockerfileContent = `FROM ubuntu:20.04 +RUN apt-get update && apt-get install -y curl +WORKDIR /app +COPY . . +CMD ["./start.sh"]` + +var _ = Describe("UpdateDockerfileCmd", func() { + var ( + c cmd.UpdateDockerfileCmd + opts cmd.UpdateDockerfileOpts + globalOpts cmd.GlobalOptions + mockEnv *env.MockEnv + ) + + BeforeEach(func() { + mockEnv = env.NewMockEnv(GinkgoT()) + globalOpts = cmd.GlobalOptions{} + opts = cmd.UpdateDockerfileOpts{ + GlobalOptions: globalOpts, + Package: "codesphere-v1.68.0.tar.gz", + Dockerfile: "Dockerfile", + Baseimage: "", + Force: false, + } + c = cmd.UpdateDockerfileCmd{ + Opts: opts, + Env: mockEnv, + } + }) + + AfterEach(func() { + mockEnv.AssertExpectations(GinkgoT()) + }) + + Context("RunE method", func() { + It("fails when package is not set", func() { + c.Opts.Package = "" + + err := c.RunE(nil, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("required option package not set")) + }) + + It("calls UpdateDockerfile and fails when package manager fails", func() { + mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") + + err := c.RunE(nil, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to update dockerfile")) + }) + + It("succeeds with valid options", func() { + mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") + + // This will fail in real scenario because it tries to extract from real package + // But it should at least call the correct methods + err := c.RunE(nil, []string{}) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("UpdateDockerfile method", func() { + It("fails when package manager fails to get image path and name", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + + c.Opts.Baseimage = "workspace-agent-24.04.tar" + c.Opts.Force = false + + mockPackageManager.EXPECT().GetImagePathAndName("workspace-agent-24.04.tar", false).Return("", "", errors.New("failed to extract image")) + + err := c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get image name")) + }) + + It("fails when dockerfile cannot be opened", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + c.Opts.Dockerfile = "Dockerfile" + c.Opts.Baseimage = "" + c.Opts.Force = false + + mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04", nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Open("Dockerfile").Return(nil, errors.New("file not found")) + + err := c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to open dockerfile")) + }) + + It("fails when image manager fails to load image", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + // Create a temporary file for the Dockerfile + tempFile, err := os.CreateTemp("", "dockerfile-test-*") + Expect(err).To(BeNil()) + DeferCleanup(func() { + tempFile.Close() + os.Remove(tempFile.Name()) + }) + _, err = tempFile.WriteString(sampleDockerfileContent) + Expect(err).To(BeNil()) + // Reset file position to beginning + tempFile.Seek(0, 0) + + c.Opts.Dockerfile = "Dockerfile" + c.Opts.Baseimage = "" + c.Opts.Force = false + + mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04", nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Open("Dockerfile").Return(tempFile, nil) + mockFileIO.EXPECT().WriteFile("Dockerfile", []byte("FROM ubuntu:24.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).Return(nil) + mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(errors.New("load failed")) + + err = c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to load baseimage file")) + }) + + It("fails when writing updated dockerfile fails", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + // Create a temporary file for the Dockerfile + tempFile, err := os.CreateTemp("", "dockerfile-test-*") + Expect(err).To(BeNil()) + DeferCleanup(func() { + tempFile.Close() + os.Remove(tempFile.Name()) + }) + _, err = tempFile.WriteString(sampleDockerfileContent) + Expect(err).To(BeNil()) + // Reset file position to beginning + tempFile.Seek(0, 0) + + c.Opts.Dockerfile = "Dockerfile" + c.Opts.Baseimage = "" + c.Opts.Force = false + + mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04", nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Open("Dockerfile").Return(tempFile, nil) + mockFileIO.EXPECT().WriteFile("Dockerfile", []byte("FROM ubuntu:24.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).Return(errors.New("write failed")) + + err = c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to write updated dockerfile")) + }) + + It("successfully updates dockerfile and loads image", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + // Create a temporary file for the Dockerfile + tempFile, err := os.CreateTemp("", "dockerfile-test-*") + Expect(err).To(BeNil()) + DeferCleanup(func() { + tempFile.Close() + os.Remove(tempFile.Name()) + }) + _, err = tempFile.WriteString(sampleDockerfileContent) + Expect(err).To(BeNil()) + // Reset file position to beginning + tempFile.Seek(0, 0) + + c.Opts.Dockerfile = "Dockerfile" + c.Opts.Baseimage = "" + c.Opts.Force = false + + mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04", nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Open("Dockerfile").Return(tempFile, nil) + mockFileIO.EXPECT().WriteFile("Dockerfile", []byte("FROM ubuntu:24.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).Return(nil) + mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(nil) + + err = c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) + Expect(err).To(BeNil()) + }) + + It("uses force flag when extracting dependencies", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + // Create a temporary file for the Dockerfile + tempFile, err := os.CreateTemp("", "dockerfile-test-*") + Expect(err).To(BeNil()) + DeferCleanup(func() { + tempFile.Close() + os.Remove(tempFile.Name()) + }) + _, err = tempFile.WriteString(sampleDockerfileContent) + Expect(err).To(BeNil()) + // Reset file position to beginning + tempFile.Seek(0, 0) + + c.Opts.Dockerfile = "Dockerfile" + c.Opts.Baseimage = "workspace-agent-20.04.tar" + c.Opts.Force = true + + mockPackageManager.EXPECT().GetImagePathAndName("workspace-agent-20.04.tar", true).Return("/test/workdir/deps/codesphere/images/workspace-agent-20.04.tar", "ubuntu:20.04", nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Open("Dockerfile").Return(tempFile, nil) + mockFileIO.EXPECT().WriteFile("Dockerfile", []byte("FROM ubuntu:20.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).Return(nil) + mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-20.04.tar").Return(nil) + + err = c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) + Expect(err).To(BeNil()) + }) + + It("handles different base image names correctly", func() { + mockPackageManager := installer.NewMockPackageManager(GinkgoT()) + mockImageManager := system.NewMockImageManager(GinkgoT()) + mockFileIO := util.NewMockFileIO(GinkgoT()) + + // Create a temporary file for the Dockerfile + tempFile, err := os.CreateTemp("", "dockerfile-test-*") + Expect(err).To(BeNil()) + DeferCleanup(func() { + tempFile.Close() + os.Remove(tempFile.Name()) + }) + _, err = tempFile.WriteString(sampleDockerfileContent) + Expect(err).To(BeNil()) + // Reset file position to beginning + tempFile.Seek(0, 0) + + c.Opts.Dockerfile = "custom/Dockerfile" + c.Opts.Baseimage = "workspace-agent-24.04.tar" + c.Opts.Force = false + + mockPackageManager.EXPECT().GetImagePathAndName("workspace-agent-24.04.tar", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "registry.example.com/workspace-agent:24.04", nil) + mockPackageManager.EXPECT().FileIO().Return(mockFileIO) + mockFileIO.EXPECT().Open("custom/Dockerfile").Return(tempFile, nil) + mockFileIO.EXPECT().WriteFile("custom/Dockerfile", []byte("FROM registry.example.com/workspace-agent:24.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).Return(nil) + mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(nil) + + err = c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) + Expect(err).To(BeNil()) + }) + }) +}) + +var _ = Describe("AddUpdateDockerfileCmd", func() { + var ( + parentCmd *cobra.Command + globalOpts cmd.GlobalOptions + ) + + BeforeEach(func() { + parentCmd = &cobra.Command{Use: "update"} + globalOpts = cmd.GlobalOptions{} + }) + + It("adds the dockerfile command with correct properties and flags", func() { + cmd.AddUpdateDockerfileCmd(parentCmd, globalOpts) + + var dockerfileCmd *cobra.Command + for _, c := range parentCmd.Commands() { + if c.Use == "dockerfile" { + dockerfileCmd = c + break + } + } + + Expect(dockerfileCmd).NotTo(BeNil()) + Expect(dockerfileCmd.Use).To(Equal("dockerfile")) + Expect(dockerfileCmd.Short).To(Equal("Update FROM statement in Dockerfile with base image from package")) + Expect(dockerfileCmd.Long).To(ContainSubstring("Update the FROM statement in a Dockerfile")) + Expect(dockerfileCmd.Long).To(ContainSubstring("base image from a Codesphere package")) + Expect(dockerfileCmd.RunE).NotTo(BeNil()) + + // Check required flags + dockerfileFlag := dockerfileCmd.Flags().Lookup("dockerfile") + Expect(dockerfileFlag).NotTo(BeNil()) + Expect(dockerfileFlag.Shorthand).To(Equal("d")) + Expect(dockerfileFlag.Usage).To(ContainSubstring("Path to the Dockerfile to update")) + + packageFlag := dockerfileCmd.Flags().Lookup("package") + Expect(packageFlag).NotTo(BeNil()) + Expect(packageFlag.Shorthand).To(Equal("p")) + Expect(packageFlag.Usage).To(ContainSubstring("Path to the Codesphere package")) + + basimageFlag := dockerfileCmd.Flags().Lookup("baseimage") + Expect(basimageFlag).NotTo(BeNil()) + Expect(basimageFlag.Shorthand).To(Equal("b")) + Expect(basimageFlag.Usage).To(ContainSubstring("Name of the base image to use")) + + forceFlag := dockerfileCmd.Flags().Lookup("force") + Expect(forceFlag).NotTo(BeNil()) + Expect(forceFlag.Shorthand).To(Equal("f")) + Expect(forceFlag.Usage).To(ContainSubstring("Force update even if Dockerfile already exists")) + }) +}) diff --git a/internal/installer/config.go b/internal/installer/config.go index 41f7b7ad..45c96761 100644 --- a/internal/installer/config.go +++ b/internal/installer/config.go @@ -5,7 +5,6 @@ package installer import ( "fmt" - "path/filepath" "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/util" @@ -17,7 +16,6 @@ type Config struct { type ConfigManager interface { ParseConfigYaml(configPath string) (files.RootConfig, error) - ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) } func NewConfig() *Config { @@ -36,19 +34,3 @@ func (c *Config) ParseConfigYaml(configPath string) (files.RootConfig, error) { return rootConfig, nil } - -// ExtractOciImageIndex extracts and parses the OCI image index from the given image file path. -func (c *Config) ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) { - var ociImageIndex files.OCIImageIndex - err := util.ExtractTarSingleFile(c.FileIO, imagefile, "index.json", filepath.Dir(imagefile)) - if err != nil { - return ociImageIndex, fmt.Errorf("failed to extract index.json: %w", err) - } - - err = ociImageIndex.ParseOCIImageConfig(imagefile) - if err != nil { - return ociImageIndex, fmt.Errorf("failed to parse OCI image config: %w", err) - } - - return ociImageIndex, nil -} diff --git a/internal/installer/config_test.go b/internal/installer/config_test.go index dce139d7..9510af50 100644 --- a/internal/installer/config_test.go +++ b/internal/installer/config_test.go @@ -4,7 +4,6 @@ package installer_test import ( - "archive/tar" "os" "path/filepath" @@ -125,111 +124,6 @@ invalid_yaml: [unclosed_bracket }) }) - Describe("ExtractOciImageIndex", func() { - Context("with real filesystem operations", func() { - var ( - tempDir string - imageFile string - ) - - BeforeEach(func() { - config = installer.NewConfig() // Use real FileIO - tempDir = GinkgoT().TempDir() - imageFile = filepath.Join(tempDir, "test-image.tar") - }) - - Context("when image file does not exist", func() { - It("returns an error", func() { - _, err := config.ExtractOciImageIndex(imageFile) - - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) - }) - }) - - Context("when image file is empty", func() { - It("returns an error", func() { - // Create empty tar file - err := os.WriteFile(imageFile, []byte(""), 0644) - Expect(err).ToNot(HaveOccurred()) - - _, err = config.ExtractOciImageIndex(imageFile) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) - }) - }) - - Context("when image file is a directory", func() { - It("returns an error", func() { - // Create directory instead of file - err := os.Mkdir(imageFile, 0755) - Expect(err).ToNot(HaveOccurred()) - - _, err = config.ExtractOciImageIndex(imageFile) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) - }) - }) - - Context("when index.json file doesn't exist after extraction", func() { - It("returns an error", func() { - // Create a minimal tar file without index.json - err := createTar(imageFile, "not_index.json", "fake content") - Expect(err).ToNot(HaveOccurred()) - - _, err = config.ExtractOciImageIndex(imageFile) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) - }) - }) - - Context("when tar contains valid index.json", func() { - It("successfully extracts and parses OCI image index", func() { - // Create a tar file with a valid index.json - validIndex := `{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.index.v1+json", - "manifests": [ - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 1234, - "digest": "sha256:abc123def456" - } - ] - }` - err := createTar(imageFile, "index.json", validIndex) - Expect(err).ToNot(HaveOccurred()) - - ociImageIndex, err := config.ExtractOciImageIndex(imageFile) - Expect(err).ToNot(HaveOccurred()) - Expect(ociImageIndex.SchemaVersion).To(Equal(2)) - Expect(ociImageIndex.MediaType).To(Equal("application/vnd.oci.image.index.v1+json")) - Expect(ociImageIndex.Manifests).To(HaveLen(1)) - Expect(ociImageIndex.Manifests[0].Digest).To(Equal("sha256:abc123def456")) - Expect(ociImageIndex.Manifests[0].Size).To(Equal(int64(1234))) - }) - }) - - Context("when index.json has invalid JSON", func() { - It("returns an error", func() { - // Create a tar file with invalid JSON in index.json - invalidIndex := `{ - "schemaVersion": 2, - "manifests": [ - { - "size": "invalid_json_here", - ` - err := createTar(imageFile, "index.json", invalidIndex) - Expect(err).ToNot(HaveOccurred()) - - _, err = config.ExtractOciImageIndex(imageFile) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to parse OCI image config")) - }) - }) - }) - }) - Describe("ConfigManager interface", func() { It("implements ConfigManager interface", func() { var configManager installer.ConfigManager = config @@ -243,9 +137,6 @@ invalid_yaml: [unclosed_bracket // and checking that we get errors (proving the methods are callable) _, err1 := configManager.ParseConfigYaml("/nonexistent/path") Expect(err1).To(HaveOccurred()) - - _, err2 := configManager.ExtractOciImageIndex("/nonexistent/path") - Expect(err2).To(HaveOccurred()) }) }) @@ -280,26 +171,14 @@ registry: }) Context("ExtractOciImageIndex with various scenarios", func() { - var tempDir string - - BeforeEach(func() { - tempDir = GinkgoT().TempDir() - }) - It("handles empty image file path", func() { - _, err := config.ExtractOciImageIndex("") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + // Test moved to package_test.go + Skip("ExtractOciImageIndex tests moved to package_test.go") }) It("handles directory instead of file", func() { - dirPath := filepath.Join(tempDir, "not-a-file") - err := os.Mkdir(dirPath, 0755) - Expect(err).ToNot(HaveOccurred()) - - _, err = config.ExtractOciImageIndex(dirPath) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + // Test moved to package_test.go + Skip("ExtractOciImageIndex tests moved to package_test.go") }) }) }) @@ -369,30 +248,3 @@ codesphere: }) }) }) - -// 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) - if err != nil { - return err - } - defer file.Close() - - tw := tar.NewWriter(file) - defer tw.Close() - - // Add index.json file - header := &tar.Header{ - Name: fileName, - Mode: 0644, - Size: int64(len(fileContent)), - } - if err := tw.WriteHeader(header); err != nil { - return err - } - if _, err := tw.Write([]byte(fileContent)); err != nil { - return err - } - - return nil -} diff --git a/internal/installer/mocks.go b/internal/installer/mocks.go index 869382db..9e43554a 100644 --- a/internal/installer/mocks.go +++ b/internal/installer/mocks.go @@ -37,60 +37,6 @@ func (_m *MockConfigManager) EXPECT() *MockConfigManager_Expecter { return &MockConfigManager_Expecter{mock: &_m.Mock} } -// ExtractOciImageIndex provides a mock function for the type MockConfigManager -func (_mock *MockConfigManager) ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) { - ret := _mock.Called(imagefile) - - if len(ret) == 0 { - panic("no return value specified for ExtractOciImageIndex") - } - - var r0 files.OCIImageIndex - var r1 error - if returnFunc, ok := ret.Get(0).(func(string) (files.OCIImageIndex, error)); ok { - return returnFunc(imagefile) - } - if returnFunc, ok := ret.Get(0).(func(string) files.OCIImageIndex); ok { - r0 = returnFunc(imagefile) - } else { - r0 = ret.Get(0).(files.OCIImageIndex) - } - if returnFunc, ok := ret.Get(1).(func(string) error); ok { - r1 = returnFunc(imagefile) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockConfigManager_ExtractOciImageIndex_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExtractOciImageIndex' -type MockConfigManager_ExtractOciImageIndex_Call struct { - *mock.Call -} - -// ExtractOciImageIndex is a helper method to define mock.On call -// - imagefile -func (_e *MockConfigManager_Expecter) ExtractOciImageIndex(imagefile interface{}) *MockConfigManager_ExtractOciImageIndex_Call { - return &MockConfigManager_ExtractOciImageIndex_Call{Call: _e.mock.On("ExtractOciImageIndex", imagefile)} -} - -func (_c *MockConfigManager_ExtractOciImageIndex_Call) Run(run func(imagefile string)) *MockConfigManager_ExtractOciImageIndex_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) - }) - return _c -} - -func (_c *MockConfigManager_ExtractOciImageIndex_Call) Return(oCIImageIndex files.OCIImageIndex, err error) *MockConfigManager_ExtractOciImageIndex_Call { - _c.Call.Return(oCIImageIndex, err) - return _c -} - -func (_c *MockConfigManager_ExtractOciImageIndex_Call) RunAndReturn(run func(imagefile string) (files.OCIImageIndex, error)) *MockConfigManager_ExtractOciImageIndex_Call { - _c.Call.Return(run) - return _c -} - // ParseConfigYaml provides a mock function for the type MockConfigManager func (_mock *MockConfigManager) ParseConfigYaml(configPath string) (files.RootConfig, error) { ret := _mock.Called(configPath) @@ -263,6 +209,60 @@ func (_c *MockPackageManager_ExtractDependency_Call) RunAndReturn(run func(file return _c } +// ExtractOciImageIndex provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) { + ret := _mock.Called(imagefile) + + if len(ret) == 0 { + panic("no return value specified for ExtractOciImageIndex") + } + + var r0 files.OCIImageIndex + var r1 error + if returnFunc, ok := ret.Get(0).(func(string) (files.OCIImageIndex, error)); ok { + return returnFunc(imagefile) + } + if returnFunc, ok := ret.Get(0).(func(string) files.OCIImageIndex); ok { + r0 = returnFunc(imagefile) + } else { + r0 = ret.Get(0).(files.OCIImageIndex) + } + if returnFunc, ok := ret.Get(1).(func(string) error); ok { + r1 = returnFunc(imagefile) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockPackageManager_ExtractOciImageIndex_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExtractOciImageIndex' +type MockPackageManager_ExtractOciImageIndex_Call struct { + *mock.Call +} + +// ExtractOciImageIndex is a helper method to define mock.On call +// - imagefile +func (_e *MockPackageManager_Expecter) ExtractOciImageIndex(imagefile interface{}) *MockPackageManager_ExtractOciImageIndex_Call { + return &MockPackageManager_ExtractOciImageIndex_Call{Call: _e.mock.On("ExtractOciImageIndex", imagefile)} +} + +func (_c *MockPackageManager_ExtractOciImageIndex_Call) Run(run func(imagefile string)) *MockPackageManager_ExtractOciImageIndex_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockPackageManager_ExtractOciImageIndex_Call) Return(oCIImageIndex files.OCIImageIndex, err error) *MockPackageManager_ExtractOciImageIndex_Call { + _c.Call.Return(oCIImageIndex, err) + return _c +} + +func (_c *MockPackageManager_ExtractOciImageIndex_Call) RunAndReturn(run func(imagefile string) (files.OCIImageIndex, error)) *MockPackageManager_ExtractOciImageIndex_Call { + _c.Call.Return(run) + return _c +} + // FileIO provides a mock function for the type MockPackageManager func (_mock *MockPackageManager) FileIO() util.FileIO { ret := _mock.Called() @@ -309,6 +309,59 @@ func (_c *MockPackageManager_FileIO_Call) RunAndReturn(run func() util.FileIO) * return _c } +// GetCodesphereVersion provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) GetCodesphereVersion() (string, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetCodesphereVersion") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func() (string, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockPackageManager_GetCodesphereVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCodesphereVersion' +type MockPackageManager_GetCodesphereVersion_Call struct { + *mock.Call +} + +// GetCodesphereVersion is a helper method to define mock.On call +func (_e *MockPackageManager_Expecter) GetCodesphereVersion() *MockPackageManager_GetCodesphereVersion_Call { + return &MockPackageManager_GetCodesphereVersion_Call{Call: _e.mock.On("GetCodesphereVersion")} +} + +func (_c *MockPackageManager_GetCodesphereVersion_Call) Run(run func()) *MockPackageManager_GetCodesphereVersion_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockPackageManager_GetCodesphereVersion_Call) Return(s string, err error) *MockPackageManager_GetCodesphereVersion_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *MockPackageManager_GetCodesphereVersion_Call) RunAndReturn(run func() (string, error)) *MockPackageManager_GetCodesphereVersion_Call { + _c.Call.Return(run) + return _c +} + // GetDependencyPath provides a mock function for the type MockPackageManager func (_mock *MockPackageManager) GetDependencyPath(filename string) string { ret := _mock.Called(filename) @@ -354,6 +407,67 @@ func (_c *MockPackageManager_GetDependencyPath_Call) RunAndReturn(run func(filen return _c } +// GetImagePathAndName provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) GetImagePathAndName(baseimage string, force bool) (string, string, error) { + ret := _mock.Called(baseimage, force) + + if len(ret) == 0 { + panic("no return value specified for GetImagePathAndName") + } + + var r0 string + var r1 string + var r2 error + if returnFunc, ok := ret.Get(0).(func(string, bool) (string, string, error)); ok { + return returnFunc(baseimage, force) + } + if returnFunc, ok := ret.Get(0).(func(string, bool) string); ok { + r0 = returnFunc(baseimage, force) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(string, bool) string); ok { + r1 = returnFunc(baseimage, force) + } else { + r1 = ret.Get(1).(string) + } + if returnFunc, ok := ret.Get(2).(func(string, bool) error); ok { + r2 = returnFunc(baseimage, force) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// MockPackageManager_GetImagePathAndName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetImagePathAndName' +type MockPackageManager_GetImagePathAndName_Call struct { + *mock.Call +} + +// GetImagePathAndName is a helper method to define mock.On call +// - baseimage +// - force +func (_e *MockPackageManager_Expecter) GetImagePathAndName(baseimage interface{}, force interface{}) *MockPackageManager_GetImagePathAndName_Call { + return &MockPackageManager_GetImagePathAndName_Call{Call: _e.mock.On("GetImagePathAndName", baseimage, force)} +} + +func (_c *MockPackageManager_GetImagePathAndName_Call) Run(run func(baseimage string, force bool)) *MockPackageManager_GetImagePathAndName_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *MockPackageManager_GetImagePathAndName_Call) Return(s string, s1 string, err error) *MockPackageManager_GetImagePathAndName_Call { + _c.Call.Return(s, s1, err) + return _c +} + +func (_c *MockPackageManager_GetImagePathAndName_Call) RunAndReturn(run func(baseimage string, force bool) (string, string, error)) *MockPackageManager_GetImagePathAndName_Call { + _c.Call.Return(run) + return _c +} + // GetWorkDir provides a mock function for the type MockPackageManager func (_mock *MockPackageManager) GetWorkDir() string { ret := _mock.Called() diff --git a/internal/installer/package.go b/internal/installer/package.go index 1c7ff4c8..f83cb048 100644 --- a/internal/installer/package.go +++ b/internal/installer/package.go @@ -8,8 +8,10 @@ import ( "log" "os" "path" + "path/filepath" "strings" + "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/util" ) @@ -17,10 +19,13 @@ const depsDir = "deps" type PackageManager interface { FileIO() util.FileIO - Extract(force bool) error - ExtractDependency(file string, force bool) error GetWorkDir() string GetDependencyPath(filename string) string + Extract(force bool) error + ExtractDependency(file string, force bool) error + ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) + GetImagePathAndName(baseimage string, force bool) (string, string, error) + GetCodesphereVersion() (string, error) } type Package struct { @@ -42,6 +47,30 @@ func (p *Package) FileIO() util.FileIO { return p.fileIO } +// GetWorkDir returns the working directory path for the package +// by joining the OmsWorkdir and the filename (without the .tar.gz extension). +func (p *Package) GetWorkDir() string { + return path.Join(p.OmsWorkdir, strings.ReplaceAll(p.Filename, ".tar.gz", "")) +} + +// GetDependencyPath returns the full path to a dependency file within the package's deps directory. +func (p *Package) GetDependencyPath(filename string) string { + workDir := p.GetWorkDir() + return path.Join(workDir, depsDir, filename) +} + +// alreadyExtracted checks if the package has already been extracted to the given directory. +func (p *Package) alreadyExtracted(dir string) (bool, error) { + if !p.fileIO.Exists(dir) { + return false, nil + } + isDir, err := p.fileIO.IsDirectory(dir) + if err != nil { + return false, fmt.Errorf("failed to determine if %s is a folder: %w", dir, err) + } + return isDir, nil +} + // Extract extracts the package tar.gz file into its working directory. // If force is true, it will overwrite existing files. func (p *Package) Extract(force bool) error { @@ -56,7 +85,7 @@ func (p *Package) Extract(force bool) error { return fmt.Errorf("failed to figure out if package %s is already extracted in %s: %w", p.Filename, workDir, err) } if alreadyExtracted && !force { - log.Println("skipping extraction, package already unpacked. Use force option to overwrite.") + log.Println("Skipping extraction, package already unpacked. Use force option to overwrite.") return nil } @@ -77,7 +106,7 @@ func (p *Package) ExtractDependency(file string, force bool) error { workDir := p.GetWorkDir() if p.fileIO.Exists(p.GetDependencyPath(file)) && !force { - log.Println("skipping extraction, dependency already unpacked. Use force option to overwrite.") + log.Println("Skipping extraction, dependency already unpacked. Use force option to overwrite.") return nil } @@ -89,25 +118,64 @@ func (p *Package) ExtractDependency(file string, force bool) error { return err } -func (p *Package) alreadyExtracted(dir string) (bool, error) { - if !p.fileIO.Exists(dir) { - return false, nil +// ExtractOciImageIndex extracts and parses the OCI image index from the given image file path. +func (p *Package) ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) { + var ociImageIndex files.OCIImageIndex + err := util.ExtractTarSingleFile(p.fileIO, imagefile, "index.json", filepath.Dir(imagefile)) + if err != nil { + return ociImageIndex, fmt.Errorf("failed to extract index.json: %w", err) } - isDir, err := p.fileIO.IsDirectory(dir) + + err = ociImageIndex.ParseOCIImageConfig(imagefile) if err != nil { - return false, fmt.Errorf("failed to determine if %s is a folder: %w", dir, err) + return ociImageIndex, fmt.Errorf("failed to parse OCI image config: %w", err) } - return isDir, nil + + return ociImageIndex, nil } -// GetWorkDir returns the working directory path for the package -// by joining the OmsWorkdir and the filename (without the .tar.gz extension). -func (p *Package) GetWorkDir() string { - return path.Join(p.OmsWorkdir, strings.ReplaceAll(p.Filename, ".tar.gz", "")) +const baseimagePath = "./codesphere/images" +const defaultBaseimage = "workspace-agent-24.04.tar" + +func (p *Package) GetImagePathAndName(baseimage string, force bool) (string, string, error) { + if baseimage == "" { + baseimage = defaultBaseimage + } + + baseImageTarPath := path.Join(baseimagePath, defaultBaseimage) + err := p.ExtractDependency(baseImageTarPath, force) + if err != nil { + return "", "", fmt.Errorf("failed to extract package to workdir: %w", err) + } + + baseimagePath := p.GetDependencyPath(baseImageTarPath) + index, err := p.ExtractOciImageIndex(baseimagePath) + if err != nil { + return "", "", fmt.Errorf("failed to extract OCI image index: %w", err) + } + + imagenames, err := index.ExtractImageNames() + if err != nil || len(imagenames) == 0 { + return "", "", fmt.Errorf("failed to read image tags: %w", err) + } + + log.Printf("Extracted image names: %s", strings.Join(imagenames, ", ")) + + baseimageName := imagenames[0] + + return baseimagePath, baseimageName, nil } -// GetDependencyPath returns the full path to a dependency file within the package's deps directory. -func (p *Package) GetDependencyPath(filename string) string { - workDir := p.GetWorkDir() - return path.Join(workDir, depsDir, filename) +func (p *Package) GetCodesphereVersion() (string, error) { + _, imageName, err := p.GetImagePathAndName("", false) + if err != nil { + return "", fmt.Errorf("failed to get Codesphere version from package: %w", err) + } + + parts := strings.Split(imageName, ":") + if len(parts) < 2 { + return "", fmt.Errorf("invalid image name format: %s", imageName) + } + + return parts[len(parts)-1], nil } diff --git a/internal/installer/package_test.go b/internal/installer/package_test.go index 5b510250..405ac4ee 100644 --- a/internal/installer/package_test.go +++ b/internal/installer/package_test.go @@ -564,3 +564,161 @@ 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 ( + pkg *installer.Package + tempDir string + ) + + BeforeEach(func() { + tempDir = GinkgoT().TempDir() + pkg = installer.NewPackage(tempDir, "test-package.tar.gz") + }) + + Describe("ExtractOciImageIndex", func() { + Context("with real filesystem operations", func() { + var imageFile string + + BeforeEach(func() { + imageFile = filepath.Join(tempDir, "test-image.tar") + }) + + Context("when image file does not exist", func() { + It("returns an error", func() { + _, err := pkg.ExtractOciImageIndex(imageFile) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + + Context("when image file is empty", func() { + It("returns an error", func() { + // Create empty tar file + err := os.WriteFile(imageFile, []byte(""), 0644) + Expect(err).ToNot(HaveOccurred()) + + _, err = pkg.ExtractOciImageIndex(imageFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + + Context("when image file is a directory", func() { + It("returns an error", func() { + // Create directory instead of file + err := os.Mkdir(imageFile, 0755) + Expect(err).ToNot(HaveOccurred()) + + _, err = pkg.ExtractOciImageIndex(imageFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + + Context("when index.json file doesn't exist after extraction", func() { + It("returns an error", func() { + // Create a minimal tar file without index.json + err := createTar(imageFile, "not_index.json", "fake content") + Expect(err).ToNot(HaveOccurred()) + + _, err = pkg.ExtractOciImageIndex(imageFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + + Context("when tar contains valid index.json", func() { + It("successfully extracts and parses OCI image index", func() { + // Create a tar file with a valid index.json + validIndex := `{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 1234, + "digest": "sha256:abc123def456" + } + ] + }` + err := createTar(imageFile, "index.json", validIndex) + Expect(err).ToNot(HaveOccurred()) + + ociImageIndex, err := pkg.ExtractOciImageIndex(imageFile) + Expect(err).ToNot(HaveOccurred()) + Expect(ociImageIndex.SchemaVersion).To(Equal(2)) + Expect(ociImageIndex.MediaType).To(Equal("application/vnd.oci.image.index.v1+json")) + Expect(ociImageIndex.Manifests).To(HaveLen(1)) + Expect(ociImageIndex.Manifests[0].Digest).To(Equal("sha256:abc123def456")) + Expect(ociImageIndex.Manifests[0].Size).To(Equal(int64(1234))) + }) + }) + + Context("when index.json has invalid JSON", func() { + It("returns an error", func() { + // Create a tar file with invalid JSON in index.json + invalidIndex := `{ + "schemaVersion": 2, + "manifests": [ + { + "size": "invalid_json_here", + ` + err := createTar(imageFile, "index.json", invalidIndex) + Expect(err).ToNot(HaveOccurred()) + + _, err = pkg.ExtractOciImageIndex(imageFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse OCI image config")) + }) + }) + }) + + Context("additional edge cases", func() { + It("handles empty image file path", func() { + _, err := pkg.ExtractOciImageIndex("") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + + It("handles directory instead of file", func() { + dirPath := filepath.Join(tempDir, "not-a-file") + err := os.Mkdir(dirPath, 0755) + Expect(err).ToNot(HaveOccurred()) + + _, err = pkg.ExtractOciImageIndex(dirPath) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract index.json")) + }) + }) + }) +}) + +// 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) + if err != nil { + return err + } + defer file.Close() + + tw := tar.NewWriter(file) + defer tw.Close() + + // Add the specified file + header := &tar.Header{ + Name: fileName, + Mode: 0644, + Size: int64(len(fileContent)), + } + if err := tw.WriteHeader(header); err != nil { + return err + } + if _, err := tw.Write([]byte(fileContent)); err != nil { + return err + } + + return nil +} From 12b9f1e0fd50675314ace1dded885e4ace1711a6 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Thu, 30 Oct 2025 17:22:43 +0100 Subject: [PATCH 09/22] update: update build image command description --- cli/cmd/build_images.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cmd/build_images.go b/cli/cmd/build_images.go index cfff9fe3..cb0108ee 100644 --- a/cli/cmd/build_images.go +++ b/cli/cmd/build_images.go @@ -47,7 +47,7 @@ func AddBuildImagesCmd(build *cobra.Command, opts GlobalOptions) { Use: "images", Short: "Build and push container images", Long: io.Long(`Build and push container images based on the configuration file. - Extracts necessary images configuration to get the bomRef and processes them.`), + Extracts necessary image configurations from the provided install config and the downloaded package.`), }, Opts: &BuildImagesOpts{GlobalOptions: opts}, Env: env.NewEnv(), From b8c09a0aef71c6f2a71dcc70a1674673df05c76c Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Thu, 30 Oct 2025 17:24:12 +0100 Subject: [PATCH 10/22] update: update build image error on missing registry --- cli/cmd/build_images.go | 2 +- cli/cmd/build_images_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/cmd/build_images.go b/cli/cmd/build_images.go index cb0108ee..4c97d236 100644 --- a/cli/cmd/build_images.go +++ b/cli/cmd/build_images.go @@ -71,7 +71,7 @@ func (c *BuildImagesCmd) BuildAndPushImages(pm installer.PackageManager, cm inst return fmt.Errorf("no images defined in the config") } if len(config.Registry.Server) == 0 { - return fmt.Errorf("registry server not defined in the config") + 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") } codesphereVersion, err := pm.GetCodesphereVersion() diff --git a/cli/cmd/build_images_test.go b/cli/cmd/build_images_test.go index f023ca72..e66edc33 100644 --- a/cli/cmd/build_images_test.go +++ b/cli/cmd/build_images_test.go @@ -164,7 +164,7 @@ var _ = Describe("BuildImagesCmd", func() { err := c.BuildAndPushImages(mockPackageManager, mockConfigManager, mockImageManager) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("registry server not defined in the config")) + Expect(err.Error()).To(ContainSubstring("registry server (property registry.server) not defined in the config")) }) It("skips flavors without dockerfile", func() { From 98fac5ada1511bf6e7d697d17a88cc0837ce975b Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Fri, 31 Oct 2025 10:58:57 +0100 Subject: [PATCH 11/22] update: update docker from update to regexp, add tests --- internal/util/docker.go | 34 ++---- internal/util/docker_test.go | 207 +++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 24 deletions(-) create mode 100644 internal/util/docker_test.go diff --git a/internal/util/docker.go b/internal/util/docker.go index 63bfd29c..390fb3e7 100644 --- a/internal/util/docker.go +++ b/internal/util/docker.go @@ -3,6 +3,7 @@ package util import ( "fmt" "io" + "regexp" "strings" ) @@ -25,32 +26,18 @@ func (dm *Dockerfile) UpdateFromStatement(dockerfile io.Reader, baseImage string return "", fmt.Errorf("error reading dockerfile: %w", err) } - lines := strings.Split(string(content), "\n") + // Regex to match FROM statements and capture parts separately + // Group 1: whitespace + FROM + whitespace + // Group 2: image name until AS or end of line (also replaces --platform if present) + // Group 3: AS + alias (optional) + fromRegex := regexp.MustCompile(`(?i)^(\s*FROM\s+)\S+(\s+AS\s+\S+)?(.*)$`) - // Find and update the first FROM line updated := false + lines := strings.Split(string(content), "\n") for i, line := range lines { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(strings.ToUpper(trimmed), "FROM ") { - // Preserve original indentation - indent := "" - for _, char := range line { - if char == ' ' || char == '\t' { - indent += string(char) - } else { - break - } - } - - // Check for platform flag - platformFlag := "" - parts := strings.Fields(trimmed) - if len(parts) >= 2 && strings.HasPrefix(parts[1], "--platform=") { - platformFlag = parts[1] + " " - } - - // Update the line - lines[i] = fmt.Sprintf("%sFROM %s%s", indent, platformFlag, baseImage) + newLine := fromRegex.ReplaceAllString(line, fmt.Sprintf("${1}%s${2}", baseImage)) + if newLine != line { + lines[i] = newLine updated = true break } @@ -60,6 +47,5 @@ func (dm *Dockerfile) UpdateFromStatement(dockerfile io.Reader, baseImage string return "", fmt.Errorf("no FROM statement found in dockerfile") } - // Join lines back together return strings.Join(lines, "\n"), nil } diff --git a/internal/util/docker_test.go b/internal/util/docker_test.go new file mode 100644 index 00000000..cd30a37c --- /dev/null +++ b/internal/util/docker_test.go @@ -0,0 +1,207 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package util_test + +import ( + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/codesphere-cloud/oms/internal/util" +) + +var _ = Describe("Docker", func() { + var dockerfileManager util.DockerfileManager + + BeforeEach(func() { + dockerfileManager = util.NewDockerfileManager() + }) + + Describe("UpdateFromStatement", func() { + It("updates a simple FROM statement", func() { + dockerfile := `FROM ubuntu:20.04 +RUN apt-get update +WORKDIR /app` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`FROM ubuntu:22.04 +RUN apt-get update +WORKDIR /app`)) + }) + + It("updates FROM statement with platform flag", func() { + dockerfile := `FROM --platform=linux/amd64 ubuntu:20.04 +RUN apt-get update +WORKDIR /app` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`FROM ubuntu:22.04 +RUN apt-get update +WORKDIR /app`)) + }) + + It("updates FROM statement with tabs", func() { + dockerfile := "\tFROM ubuntu:20.04\nRUN apt-get update\nWORKDIR /app" + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal("\tFROM ubuntu:22.04\nRUN apt-get update\nWORKDIR /app")) + }) + + It("handles case-insensitive FROM statement", func() { + dockerfile := `from ubuntu:20.04 +RUN apt-get update +WORKDIR /app` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`from ubuntu:22.04 +RUN apt-get update +WORKDIR /app`)) + }) + + It("updates only the first FROM statement in multi-stage builds", func() { + dockerfile := `FROM ubuntu:20.04 as builder +RUN apt-get update +FROM alpine:3.14 +COPY --from=builder /app /app` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`FROM ubuntu:22.04 as builder +RUN apt-get update +FROM alpine:3.14 +COPY --from=builder /app /app`)) + }) + + It("handles FROM statement with AS alias", func() { + dockerfile := `FROM ubuntu:20.04 AS builder +RUN apt-get update +WORKDIR /app` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`FROM ubuntu:22.04 AS builder +RUN apt-get update +WORKDIR /app`)) + }) + + Context("edge cases", func() { + It("handles ARG statements before FROM", func() { + dockerfile := `ARG BASE_IMAGE=ubuntu:20.04 +ARG VERSION=latest +FROM ${BASE_IMAGE} +RUN apt-get update +WORKDIR /app` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`ARG BASE_IMAGE=ubuntu:20.04 +ARG VERSION=latest +FROM ubuntu:22.04 +RUN apt-get update +WORKDIR /app`)) + }) + + It("handles comments before FROM", func() { + dockerfile := `# This is a comment +# Another comment about the base image +FROM ubuntu:20.04 +RUN apt-get update +WORKDIR /app` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`# This is a comment +# Another comment about the base image +FROM ubuntu:22.04 +RUN apt-get update +WORKDIR /app`)) + }) + + It("handles empty lines before FROM", func() { + dockerfile := ` + +ARG BASE_IMAGE=ubuntu:20.04 + +FROM ${BASE_IMAGE} +RUN apt-get update` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(` + +ARG BASE_IMAGE=ubuntu:20.04 + +FROM ubuntu:22.04 +RUN apt-get update`)) + }) + }) + + Context("error cases", func() { + It("returns error when no FROM statement found", func() { + dockerfile := `RUN apt-get update +WORKDIR /app +COPY . .` + + _, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no FROM statement found in dockerfile")) + }) + + It("returns error when dockerfile is empty", func() { + dockerfile := "" + + _, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no FROM statement found in dockerfile")) + }) + }) + + Context("regression tests", func() { + It("handles FROM with multiple spaces", func() { + dockerfile := `FROM ubuntu:20.04 +RUN apt-get update` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`FROM ubuntu:22.04 +RUN apt-get update`)) + }) + + It("handles FROM at end of file without newline", func() { + dockerfile := `FROM ubuntu:20.04` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`FROM ubuntu:22.04`)) + }) + + It("handles FROM with trailing spaces", func() { + dockerfile := `FROM ubuntu:20.04 +RUN apt-get update` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`FROM ubuntu:22.04 +RUN apt-get update`)) + }) + + It("handles FROM with leading spaces", func() { + dockerfile := ` FROM ubuntu:20.04 +RUN apt-get update +WORKDIR /app` + + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(` FROM ubuntu:22.04 +RUN apt-get update +WORKDIR /app`)) + }) + }) + }) +}) From 3a7a00aab8683dd9d57a26ba5c1a1e91baf6118a Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Fri, 31 Oct 2025 11:18:04 +0100 Subject: [PATCH 12/22] update: update global options to pointer --- cli/cmd/beta.go | 2 +- cli/cmd/build.go | 2 +- cli/cmd/build_image.go | 4 ++-- cli/cmd/build_image_test.go | 8 ++++---- cli/cmd/build_images.go | 4 ++-- cli/cmd/build_images_test.go | 8 ++++---- cli/cmd/download.go | 2 +- cli/cmd/download_package.go | 4 ++-- cli/cmd/extend.go | 2 +- cli/cmd/extend_baseimage.go | 6 +++--- cli/cmd/extend_baseimage_test.go | 8 ++++---- cli/cmd/install.go | 2 +- cli/cmd/install_codesphere.go | 4 ++-- cli/cmd/install_codesphere_test.go | 8 ++++---- cli/cmd/list.go | 2 +- cli/cmd/list_api_keys.go | 2 +- cli/cmd/list_packages.go | 4 ++-- cli/cmd/register.go | 4 ++-- cli/cmd/register_test.go | 2 +- cli/cmd/revoke.go | 2 +- cli/cmd/revoke_api_key.go | 4 ++-- cli/cmd/revoke_api_key_test.go | 2 +- cli/cmd/root.go | 2 +- cli/cmd/update.go | 2 +- cli/cmd/update_api_key.go | 2 +- cli/cmd/update_dockerfile.go | 8 ++++---- cli/cmd/update_dockerfile_test.go | 8 ++++---- internal/installer/package.go | 5 ++--- 28 files changed, 56 insertions(+), 57 deletions(-) diff --git a/cli/cmd/beta.go b/cli/cmd/beta.go index b180201c..63f5b418 100644 --- a/cli/cmd/beta.go +++ b/cli/cmd/beta.go @@ -12,7 +12,7 @@ type BetaCmd struct { cmd *cobra.Command } -func AddBetaCmd(rootCmd *cobra.Command, opts GlobalOptions) { +func AddBetaCmd(rootCmd *cobra.Command, opts *GlobalOptions) { beta := BetaCmd{ cmd: &cobra.Command{ Use: "beta", diff --git a/cli/cmd/build.go b/cli/cmd/build.go index 71f7d849..1bf87ca0 100644 --- a/cli/cmd/build.go +++ b/cli/cmd/build.go @@ -13,7 +13,7 @@ type BuildCmd struct { cmd *cobra.Command } -func AddBuildCmd(rootCmd *cobra.Command, opts GlobalOptions) { +func AddBuildCmd(rootCmd *cobra.Command, opts *GlobalOptions) { build := BuildCmd{ cmd: &cobra.Command{ Use: "build", diff --git a/cli/cmd/build_image.go b/cli/cmd/build_image.go index 565d8b1d..5b91abe5 100644 --- a/cli/cmd/build_image.go +++ b/cli/cmd/build_image.go @@ -21,7 +21,7 @@ type BuildImageCmd struct { } type BuildImageOpts struct { - GlobalOptions + *GlobalOptions Dockerfile string Package string Registry string @@ -34,7 +34,7 @@ func (c *BuildImageCmd) RunE(cmd *cobra.Command, args []string) error { return c.BuildImage(pm, im) } -func AddBuildImageCmd(parentCmd *cobra.Command, opts GlobalOptions) { +func AddBuildImageCmd(parentCmd *cobra.Command, opts *GlobalOptions) { imageCmd := &BuildImageCmd{ cmd: &cobra.Command{ Use: "image", diff --git a/cli/cmd/build_image_test.go b/cli/cmd/build_image_test.go index 0bcef9d0..b952a3a1 100644 --- a/cli/cmd/build_image_test.go +++ b/cli/cmd/build_image_test.go @@ -20,13 +20,13 @@ var _ = Describe("BuildImageCmd", func() { var ( c cmd.BuildImageCmd opts cmd.BuildImageOpts - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions mockEnv *env.MockEnv ) BeforeEach(func() { mockEnv = env.NewMockEnv(GinkgoT()) - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} opts = cmd.BuildImageOpts{ GlobalOptions: globalOpts, Dockerfile: "Dockerfile", @@ -125,12 +125,12 @@ var _ = Describe("BuildImageCmd", func() { var _ = Describe("AddBuildImageCmd", func() { var ( parentCmd *cobra.Command - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions ) BeforeEach(func() { parentCmd = &cobra.Command{Use: "build"} - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} }) It("adds the image command with correct properties and flags", func() { diff --git a/cli/cmd/build_images.go b/cli/cmd/build_images.go index 4c97d236..9cda37a4 100644 --- a/cli/cmd/build_images.go +++ b/cli/cmd/build_images.go @@ -24,7 +24,7 @@ type BuildImagesCmd struct { } type BuildImagesOpts struct { - GlobalOptions + *GlobalOptions Config string } @@ -41,7 +41,7 @@ func (c *BuildImagesCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddBuildImagesCmd(build *cobra.Command, opts GlobalOptions) { +func AddBuildImagesCmd(build *cobra.Command, opts *GlobalOptions) { buildImages := BuildImagesCmd{ cmd: &cobra.Command{ Use: "images", diff --git a/cli/cmd/build_images_test.go b/cli/cmd/build_images_test.go index e66edc33..bb9485b3 100644 --- a/cli/cmd/build_images_test.go +++ b/cli/cmd/build_images_test.go @@ -39,13 +39,13 @@ var _ = Describe("BuildImagesCmd", func() { var ( c cmd.BuildImagesCmd opts *cmd.BuildImagesOpts - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions mockEnv *env.MockEnv ) BeforeEach(func() { mockEnv = env.NewMockEnv(GinkgoT()) - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} opts = &cmd.BuildImagesOpts{ GlobalOptions: globalOpts, Config: "", @@ -389,12 +389,12 @@ var _ = Describe("BuildImagesCmd", func() { var _ = Describe("AddBuildImagesCmd", func() { var ( parentCmd *cobra.Command - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions ) BeforeEach(func() { parentCmd = &cobra.Command{Use: "build"} - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} }) It("adds the images command with correct properties and flags", func() { diff --git a/cli/cmd/download.go b/cli/cmd/download.go index af62a489..20b530c1 100644 --- a/cli/cmd/download.go +++ b/cli/cmd/download.go @@ -13,7 +13,7 @@ type DownloadCmd struct { cmd *cobra.Command } -func AddDownloadCmd(rootCmd *cobra.Command, opts GlobalOptions) { +func AddDownloadCmd(rootCmd *cobra.Command, opts *GlobalOptions) { download := DownloadCmd{ cmd: &cobra.Command{ Use: "download", diff --git a/cli/cmd/download_package.go b/cli/cmd/download_package.go index f05544cc..193a8d13 100644 --- a/cli/cmd/download_package.go +++ b/cli/cmd/download_package.go @@ -22,7 +22,7 @@ type DownloadPackageCmd struct { } type DownloadPackageOpts struct { - GlobalOptions + *GlobalOptions Version string Hash string Filename string @@ -50,7 +50,7 @@ func (c *DownloadPackageCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddDownloadPackageCmd(download *cobra.Command, opts GlobalOptions) { +func AddDownloadPackageCmd(download *cobra.Command, opts *GlobalOptions) { pkg := DownloadPackageCmd{ cmd: &cobra.Command{ Use: "package", diff --git a/cli/cmd/extend.go b/cli/cmd/extend.go index 768f560d..a0b0571c 100644 --- a/cli/cmd/extend.go +++ b/cli/cmd/extend.go @@ -13,7 +13,7 @@ type ExtendCmd struct { cmd *cobra.Command } -func AddExtendCmd(rootCmd *cobra.Command, opts GlobalOptions) { +func AddExtendCmd(rootCmd *cobra.Command, opts *GlobalOptions) { extend := ExtendCmd{ cmd: &cobra.Command{ Use: "extend", diff --git a/cli/cmd/extend_baseimage.go b/cli/cmd/extend_baseimage.go index 61fd670d..3768a29c 100644 --- a/cli/cmd/extend_baseimage.go +++ b/cli/cmd/extend_baseimage.go @@ -26,7 +26,7 @@ type ExtendBaseimageCmd struct { } type ExtendBaseimageOpts struct { - GlobalOptions + *GlobalOptions Package string Dockerfile string Baseimage string @@ -50,7 +50,7 @@ func (c *ExtendBaseimageCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddExtendBaseimageCmd(extend *cobra.Command, opts GlobalOptions) { +func AddExtendBaseimageCmd(extend *cobra.Command, opts *GlobalOptions) { baseimage := ExtendBaseimageCmd{ cmd: &cobra.Command{ Use: "baseimage", @@ -66,7 +66,7 @@ func AddExtendBaseimageCmd(extend *cobra.Command, opts GlobalOptions) { } baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load base image from") baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Dockerfile, "dockerfile", "d", "Dockerfile", "Output Dockerfile to generate for extending the base image") - baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Baseimage, "baseimage", "b", "", "Base image file name inside the package to extend (default: 'workspace-agent-24.04.tar')") + baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Baseimage, "baseimage", "b", "workspace-agent-24.04.tar", "Base image file name inside the package to extend (default: 'workspace-agent-24.04.tar')") baseimage.cmd.Flags().BoolVarP(&baseimage.Opts.Force, "force", "f", false, "Enforce package extraction") extend.AddCommand(baseimage.cmd) diff --git a/cli/cmd/extend_baseimage_test.go b/cli/cmd/extend_baseimage_test.go index 32de8c19..01eba1a4 100644 --- a/cli/cmd/extend_baseimage_test.go +++ b/cli/cmd/extend_baseimage_test.go @@ -22,13 +22,13 @@ var _ = Describe("ExtendBaseimageCmd", func() { var ( c cmd.ExtendBaseimageCmd opts *cmd.ExtendBaseimageOpts - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions mockEnv *env.MockEnv ) BeforeEach(func() { mockEnv = env.NewMockEnv(GinkgoT()) - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} opts = &cmd.ExtendBaseimageOpts{ GlobalOptions: globalOpts, Dockerfile: "Dockerfile", @@ -153,12 +153,12 @@ var _ = Describe("ExtendBaseimageCmd", func() { var _ = Describe("AddExtendBaseimageCmd", func() { var ( parentCmd *cobra.Command - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions ) BeforeEach(func() { parentCmd = &cobra.Command{Use: "extend"} - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} }) It("adds the baseimage command with correct properties and flags", func() { diff --git a/cli/cmd/install.go b/cli/cmd/install.go index 85899d9a..7402281d 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -13,7 +13,7 @@ type InstallCmd struct { cmd *cobra.Command } -func AddInstallCmd(rootCmd *cobra.Command, opts GlobalOptions) { +func AddInstallCmd(rootCmd *cobra.Command, opts *GlobalOptions) { install := InstallCmd{ cmd: &cobra.Command{ Use: "install", diff --git a/cli/cmd/install_codesphere.go b/cli/cmd/install_codesphere.go index 82a4cadd..ebbfa8be 100644 --- a/cli/cmd/install_codesphere.go +++ b/cli/cmd/install_codesphere.go @@ -31,7 +31,7 @@ type InstallCodesphereCmd struct { } type InstallCodesphereOpts struct { - GlobalOptions + *GlobalOptions Package string Force bool Config string @@ -53,7 +53,7 @@ func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddInstallCodesphereCmd(install *cobra.Command, opts GlobalOptions) { +func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) { codesphere := InstallCodesphereCmd{ cmd: &cobra.Command{ Use: "codesphere", diff --git a/cli/cmd/install_codesphere_test.go b/cli/cmd/install_codesphere_test.go index e56af332..57ac48ed 100644 --- a/cli/cmd/install_codesphere_test.go +++ b/cli/cmd/install_codesphere_test.go @@ -24,13 +24,13 @@ var _ = Describe("InstallCodesphereCmd", func() { var ( c cmd.InstallCodesphereCmd opts *cmd.InstallCodesphereOpts - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions mockEnv *env.MockEnv ) BeforeEach(func() { mockEnv = env.NewMockEnv(GinkgoT()) - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} opts = &cmd.InstallCodesphereOpts{ GlobalOptions: globalOpts, Package: "codesphere-v1.66.0-installer.tar.gz", @@ -368,12 +368,12 @@ var _ = Describe("InstallCodesphereCmd", func() { var _ = Describe("AddInstallCodesphereCmd", func() { var ( parentCmd *cobra.Command - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions ) BeforeEach(func() { parentCmd = &cobra.Command{Use: "install"} - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} }) It("adds the codesphere command with correct properties and flags", func() { diff --git a/cli/cmd/list.go b/cli/cmd/list.go index 6620bed0..5b406e71 100644 --- a/cli/cmd/list.go +++ b/cli/cmd/list.go @@ -12,7 +12,7 @@ type ListCmd struct { cmd *cobra.Command } -func AddListCmd(rootCmd *cobra.Command, opts GlobalOptions) { +func AddListCmd(rootCmd *cobra.Command, opts *GlobalOptions) { list := ListCmd{ cmd: &cobra.Command{ Use: "list", diff --git a/cli/cmd/list_api_keys.go b/cli/cmd/list_api_keys.go index da0429d8..96d318a1 100644 --- a/cli/cmd/list_api_keys.go +++ b/cli/cmd/list_api_keys.go @@ -30,7 +30,7 @@ func (c *ListAPIKeysCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddListAPIKeysCmd(list *cobra.Command, opts GlobalOptions) { +func AddListAPIKeysCmd(list *cobra.Command, opts *GlobalOptions) { c := ListAPIKeysCmd{ cmd: &cobra.Command{ Use: "api-keys", diff --git a/cli/cmd/list_packages.go b/cli/cmd/list_packages.go index c58538ec..2a0bbdfd 100644 --- a/cli/cmd/list_packages.go +++ b/cli/cmd/list_packages.go @@ -21,7 +21,7 @@ type ListBuildsCmd struct { } type ListBuildsOpts struct { - GlobalOptions + *GlobalOptions Internal bool } @@ -36,7 +36,7 @@ func (c *ListBuildsCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddListPackagesCmd(list *cobra.Command, opts GlobalOptions) { +func AddListPackagesCmd(list *cobra.Command, opts *GlobalOptions) { builds := ListBuildsCmd{ cmd: &cobra.Command{ Use: "packages", diff --git a/cli/cmd/register.go b/cli/cmd/register.go index e621a604..c1939ecf 100644 --- a/cli/cmd/register.go +++ b/cli/cmd/register.go @@ -18,7 +18,7 @@ type RegisterCmd struct { } type RegisterOpts struct { - GlobalOptions + *GlobalOptions Owner string Organization string Role string @@ -57,7 +57,7 @@ func (c *RegisterCmd) Register(p portal.Portal) (*portal.ApiKey, error) { return newKey, nil } -func AddRegisterCmd(list *cobra.Command, opts GlobalOptions) { +func AddRegisterCmd(list *cobra.Command, opts *GlobalOptions) { c := RegisterCmd{ cmd: &cobra.Command{ Use: "register", diff --git a/cli/cmd/register_test.go b/cli/cmd/register_test.go index 11713077..e3a9bd09 100644 --- a/cli/cmd/register_test.go +++ b/cli/cmd/register_test.go @@ -74,7 +74,7 @@ var _ = Describe("RegisterCmd", func() { var _ = Describe("AddRegisterCmd", func() { It("adds the register command to the parent", func() { parent := &cobra.Command{} - opts := cmd.GlobalOptions{} + opts := &cmd.GlobalOptions{} cmd.AddRegisterCmd(parent, opts) found := false for _, c := range parent.Commands() { diff --git a/cli/cmd/revoke.go b/cli/cmd/revoke.go index 14c73562..f0ace88c 100644 --- a/cli/cmd/revoke.go +++ b/cli/cmd/revoke.go @@ -12,7 +12,7 @@ type RevokeCmd struct { cmd *cobra.Command } -func AddRevokeCmd(rootCmd *cobra.Command, opts GlobalOptions) { +func AddRevokeCmd(rootCmd *cobra.Command, opts *GlobalOptions) { revoke := RevokeCmd{ cmd: &cobra.Command{ Use: "revoke", diff --git a/cli/cmd/revoke_api_key.go b/cli/cmd/revoke_api_key.go index 5ab5248b..3af7148a 100644 --- a/cli/cmd/revoke_api_key.go +++ b/cli/cmd/revoke_api_key.go @@ -18,7 +18,7 @@ type RevokeAPIKeyCmd struct { } type RevokeAPIKeyOpts struct { - GlobalOptions + *GlobalOptions ID string } @@ -36,7 +36,7 @@ func (c *RevokeAPIKeyCmd) Revoke(p portal.Portal) error { return nil } -func AddRevokeAPIKeyCmd(list *cobra.Command, opts GlobalOptions) { +func AddRevokeAPIKeyCmd(list *cobra.Command, opts *GlobalOptions) { c := RevokeAPIKeyCmd{ cmd: &cobra.Command{ Use: "api-key", diff --git a/cli/cmd/revoke_api_key_test.go b/cli/cmd/revoke_api_key_test.go index 0cd10724..a2ed0c95 100644 --- a/cli/cmd/revoke_api_key_test.go +++ b/cli/cmd/revoke_api_key_test.go @@ -51,7 +51,7 @@ var _ = Describe("RevokeCmd", func() { var _ = Describe("AddRevokeAPIKeyCmd", func() { It("adds the api-key command to the parent", func() { parent := &cobra.Command{} - opts := cmd.GlobalOptions{} + opts := &cmd.GlobalOptions{} cmd.AddRevokeAPIKeyCmd(parent, opts) found := false for _, c := range parent.Commands() { diff --git a/cli/cmd/root.go b/cli/cmd/root.go index e0ce49e2..431e5cd9 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -17,7 +17,7 @@ type GlobalOptions struct { // GetRootCmd adds all child commands to the root command and sets flags appropriately. func GetRootCmd() *cobra.Command { - opts := GlobalOptions{} + opts := &GlobalOptions{} rootCmd := &cobra.Command{ Use: "oms", Short: "Codesphere Operations Management System (OMS)", diff --git a/cli/cmd/update.go b/cli/cmd/update.go index 106c9ef2..ba961958 100644 --- a/cli/cmd/update.go +++ b/cli/cmd/update.go @@ -19,7 +19,7 @@ func (c *UpdateCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddUpdateCmd(rootCmd *cobra.Command, opts GlobalOptions) { +func AddUpdateCmd(rootCmd *cobra.Command, opts *GlobalOptions) { updateCmd := UpdateCmd{ cmd: &cobra.Command{ Use: "update", diff --git a/cli/cmd/update_api_key.go b/cli/cmd/update_api_key.go index 44345bf2..fe7afa22 100644 --- a/cli/cmd/update_api_key.go +++ b/cli/cmd/update_api_key.go @@ -18,7 +18,7 @@ type UpdateAPIKeyCmd struct { } type UpdateAPIKeyOpts struct { - GlobalOptions + *GlobalOptions APIKeyID string ExpiresAtStr string } diff --git a/cli/cmd/update_dockerfile.go b/cli/cmd/update_dockerfile.go index 8022669f..a56330aa 100644 --- a/cli/cmd/update_dockerfile.go +++ b/cli/cmd/update_dockerfile.go @@ -21,7 +21,7 @@ type UpdateDockerfileCmd struct { } type UpdateDockerfileOpts struct { - GlobalOptions + *GlobalOptions Package string Dockerfile string Baseimage string @@ -45,7 +45,7 @@ func (c *UpdateDockerfileCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddUpdateDockerfileCmd(parentCmd *cobra.Command, opts GlobalOptions) { +func AddUpdateDockerfileCmd(parentCmd *cobra.Command, opts *GlobalOptions) { dockerfileCmd := &UpdateDockerfileCmd{ cmd: &cobra.Command{ Use: "dockerfile", @@ -66,8 +66,8 @@ in the specified Dockerfile to use that base image. The base image is loaded int dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Dockerfile, "dockerfile", "d", "", "Path to the Dockerfile to update (required)") dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Package, "package", "p", "", "Path to the Codesphere package (required)") - dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Baseimage, "baseimage", "b", "", "Name of the base image to use (required)") - dockerfileCmd.cmd.Flags().BoolVarP(&dockerfileCmd.Opts.Force, "force", "f", false, "Force update even if Dockerfile already exists") + dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Baseimage, "baseimage", "b", "workspace-agent-24.04.tar", "Name of the base image to use (required)") + dockerfileCmd.cmd.Flags().BoolVarP(&dockerfileCmd.Opts.Force, "force", "f", false, "Force re-extraction of the package") util.MarkFlagRequired(dockerfileCmd.cmd, "dockerfile") util.MarkFlagRequired(dockerfileCmd.cmd, "package") diff --git a/cli/cmd/update_dockerfile_test.go b/cli/cmd/update_dockerfile_test.go index 09f874f6..b4d42bde 100644 --- a/cli/cmd/update_dockerfile_test.go +++ b/cli/cmd/update_dockerfile_test.go @@ -28,13 +28,13 @@ var _ = Describe("UpdateDockerfileCmd", func() { var ( c cmd.UpdateDockerfileCmd opts cmd.UpdateDockerfileOpts - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions mockEnv *env.MockEnv ) BeforeEach(func() { mockEnv = env.NewMockEnv(GinkgoT()) - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} opts = cmd.UpdateDockerfileOpts{ GlobalOptions: globalOpts, Package: "codesphere-v1.68.0.tar.gz", @@ -273,12 +273,12 @@ var _ = Describe("UpdateDockerfileCmd", func() { var _ = Describe("AddUpdateDockerfileCmd", func() { var ( parentCmd *cobra.Command - globalOpts cmd.GlobalOptions + globalOpts *cmd.GlobalOptions ) BeforeEach(func() { parentCmd = &cobra.Command{Use: "update"} - globalOpts = cmd.GlobalOptions{} + globalOpts = &cmd.GlobalOptions{} }) It("adds the dockerfile command with correct properties and flags", func() { diff --git a/internal/installer/package.go b/internal/installer/package.go index f83cb048..aaa67e52 100644 --- a/internal/installer/package.go +++ b/internal/installer/package.go @@ -135,14 +135,13 @@ func (p *Package) ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, e } const baseimagePath = "./codesphere/images" -const defaultBaseimage = "workspace-agent-24.04.tar" func (p *Package) GetImagePathAndName(baseimage string, force bool) (string, string, error) { if baseimage == "" { - baseimage = defaultBaseimage + return "", "", fmt.Errorf("baseimage not specified") } - baseImageTarPath := path.Join(baseimagePath, defaultBaseimage) + baseImageTarPath := path.Join(baseimagePath, baseimage) err := p.ExtractDependency(baseImageTarPath, force) if err != nil { return "", "", fmt.Errorf("failed to extract package to workdir: %w", err) From 3ec9e7604b4d049244ceb38c616e346d62909759 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Fri, 31 Oct 2025 12:33:28 +0100 Subject: [PATCH 13/22] update: update dockerfile from update to only workspace agent images --- cli/cmd/update_dockerfile_test.go | 4 +- internal/installer/config_test.go | 4 +- internal/util/docker.go | 25 ++--- internal/util/docker_test.go | 172 ++++++------------------------ 4 files changed, 45 insertions(+), 160 deletions(-) diff --git a/cli/cmd/update_dockerfile_test.go b/cli/cmd/update_dockerfile_test.go index b4d42bde..e56f0766 100644 --- a/cli/cmd/update_dockerfile_test.go +++ b/cli/cmd/update_dockerfile_test.go @@ -18,7 +18,7 @@ import ( "github.com/codesphere-cloud/oms/internal/util" ) -const sampleDockerfileContent = `FROM ubuntu:20.04 +const sampleDockerfileContent = `FROM workspace-agent:20.04 RUN apt-get update && apt-get install -y curl WORKDIR /app COPY . . @@ -318,6 +318,6 @@ var _ = Describe("AddUpdateDockerfileCmd", func() { forceFlag := dockerfileCmd.Flags().Lookup("force") Expect(forceFlag).NotTo(BeNil()) Expect(forceFlag.Shorthand).To(Equal("f")) - Expect(forceFlag.Usage).To(ContainSubstring("Force update even if Dockerfile already exists")) + Expect(forceFlag.Usage).To(ContainSubstring("Force re-extraction of the package")) }) }) diff --git a/internal/installer/config_test.go b/internal/installer/config_test.go index 9510af50..5f09f46e 100644 --- a/internal/installer/config_test.go +++ b/internal/installer/config_test.go @@ -71,7 +71,7 @@ codesphere: _, err := config.ParseConfigYaml(nonExistentFile) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract config.yaml")) + Expect(err.Error()).To(ContainSubstring("failed to parse config.yaml")) }) }) @@ -103,7 +103,7 @@ invalid_yaml: [unclosed_bracket _, err = config.ParseConfigYaml(tempConfigFile) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to extract config.yaml")) + Expect(err.Error()).To(ContainSubstring("failed to parse config.yaml")) }) }) diff --git a/internal/util/docker.go b/internal/util/docker.go index 390fb3e7..0fe9273f 100644 --- a/internal/util/docker.go +++ b/internal/util/docker.go @@ -26,26 +26,23 @@ func (dm *Dockerfile) UpdateFromStatement(dockerfile io.Reader, baseImage string return "", fmt.Errorf("error reading dockerfile: %w", err) } - // Regex to match FROM statements and capture parts separately - // Group 1: whitespace + FROM + whitespace - // Group 2: image name until AS or end of line (also replaces --platform if present) - // Group 3: AS + alias (optional) - fromRegex := regexp.MustCompile(`(?i)^(\s*FROM\s+)\S+(\s+AS\s+\S+)?(.*)$`) + // Regex to match FROM statements that contain workspace-agent + fromRegex := regexp.MustCompile(`(?i)(.*FROM\s+).*workspace-agent[^\s]*(.*)`) - updated := false lines := strings.Split(string(content), "\n") + lastMatchIndex := -1 + for i, line := range lines { - newLine := fromRegex.ReplaceAllString(line, fmt.Sprintf("${1}%s${2}", baseImage)) - if newLine != line { - lines[i] = newLine - updated = true - break + if fromRegex.MatchString(line) { + lastMatchIndex = i } } - - if !updated { - return "", fmt.Errorf("no FROM statement found in dockerfile") + if lastMatchIndex == -1 { + return "", fmt.Errorf("no FROM statement with workspace-agent found in dockerfile") } + newLine := fromRegex.ReplaceAllString(lines[lastMatchIndex], "${1}"+baseImage+"${2}") + lines[lastMatchIndex] = strings.TrimRight(newLine, " \t") + return strings.Join(lines, "\n"), nil } diff --git a/internal/util/docker_test.go b/internal/util/docker_test.go index cd30a37c..19c414dc 100644 --- a/internal/util/docker_test.go +++ b/internal/util/docker_test.go @@ -20,188 +20,76 @@ var _ = Describe("Docker", func() { }) Describe("UpdateFromStatement", func() { - It("updates a simple FROM statement", func() { - dockerfile := `FROM ubuntu:20.04 + It("updates a simple FROM statement with workspace-agent", func() { + dockerfile := `FROM workspace-agent-24.04:codesphere-v1.0.0 RUN apt-get update WORKDIR /app` - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "workspace-agent-24.04:codesphere-v1.0.1") Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`FROM ubuntu:22.04 + Expect(result).To(Equal(`FROM workspace-agent-24.04:codesphere-v1.0.1 RUN apt-get update WORKDIR /app`)) }) - It("updates FROM statement with platform flag", func() { - dockerfile := `FROM --platform=linux/amd64 ubuntu:20.04 + It("updates FROM statement with workspace-agent and platform flag", func() { + dockerfile := `FROM --platform=linux/amd64 workspace-agent-24.04:codesphere-v1.0.0 RUN apt-get update WORKDIR /app` - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "workspace-agent-24.04:codesphere-v1.0.1") Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`FROM ubuntu:22.04 + Expect(result).To(Equal(`FROM workspace-agent-24.04:codesphere-v1.0.1 RUN apt-get update WORKDIR /app`)) }) - It("updates FROM statement with tabs", func() { - dockerfile := "\tFROM ubuntu:20.04\nRUN apt-get update\nWORKDIR /app" - - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal("\tFROM ubuntu:22.04\nRUN apt-get update\nWORKDIR /app")) - }) - - It("handles case-insensitive FROM statement", func() { - dockerfile := `from ubuntu:20.04 + It("handles case-insensitive FROM statement with workspace-agent", func() { + dockerfile := `from workspace-agent-24.04:codesphere-v1.0.0 RUN apt-get update WORKDIR /app` - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "workspace-agent-24.04:codesphere-v1.0.1") Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`from ubuntu:22.04 + Expect(result).To(Equal(`from workspace-agent-24.04:codesphere-v1.0.1 RUN apt-get update WORKDIR /app`)) }) - It("updates only the first FROM statement in multi-stage builds", func() { - dockerfile := `FROM ubuntu:20.04 as builder -RUN apt-get update -FROM alpine:3.14 -COPY --from=builder /app /app` - - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`FROM ubuntu:22.04 as builder -RUN apt-get update -FROM alpine:3.14 -COPY --from=builder /app /app`)) - }) - - It("handles FROM statement with AS alias", func() { - dockerfile := `FROM ubuntu:20.04 AS builder + It("handles FROM with complex workspace-agent image names", func() { + dockerfile := `FROM registry.example.com:5000/workspace-agent-24.04:codesphere-v1.0.0 RUN apt-get update WORKDIR /app` - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "new-registry.com/workspace-agent-24.04:codesphere-v1.0.1") Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`FROM ubuntu:22.04 AS builder + Expect(result).To(Equal(`FROM new-registry.com/workspace-agent-24.04:codesphere-v1.0.1 RUN apt-get update WORKDIR /app`)) }) - Context("edge cases", func() { - It("handles ARG statements before FROM", func() { - dockerfile := `ARG BASE_IMAGE=ubuntu:20.04 -ARG VERSION=latest -FROM ${BASE_IMAGE} -RUN apt-get update -WORKDIR /app` - - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`ARG BASE_IMAGE=ubuntu:20.04 -ARG VERSION=latest -FROM ubuntu:22.04 -RUN apt-get update -WORKDIR /app`)) - }) - - It("handles comments before FROM", func() { - dockerfile := `# This is a comment -# Another comment about the base image -FROM ubuntu:20.04 + It("updates the last FROM statement in multi-stage builds", func() { + dockerfile := `FROM alpine:3.14 RUN apt-get update -WORKDIR /app` +FROM workspace-agent-24.04:20.04 as builder +COPY --from=0 /app /app` - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`# This is a comment -# Another comment about the base image -FROM ubuntu:22.04 + result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "workspace-agent-24.04:codesphere-v1.0.1") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(`FROM alpine:3.14 RUN apt-get update -WORKDIR /app`)) - }) - - It("handles empty lines before FROM", func() { - dockerfile := ` - -ARG BASE_IMAGE=ubuntu:20.04 - -FROM ${BASE_IMAGE} -RUN apt-get update` - - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(` - -ARG BASE_IMAGE=ubuntu:20.04 - -FROM ubuntu:22.04 -RUN apt-get update`)) - }) +FROM workspace-agent-24.04:codesphere-v1.0.1 as builder +COPY --from=0 /app /app`)) }) - Context("error cases", func() { - It("returns error when no FROM statement found", func() { - dockerfile := `RUN apt-get update -WORKDIR /app -COPY . .` - - _, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("no FROM statement found in dockerfile")) - }) - - It("returns error when dockerfile is empty", func() { - dockerfile := "" - - _, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("no FROM statement found in dockerfile")) - }) - }) - - Context("regression tests", func() { - It("handles FROM with multiple spaces", func() { - dockerfile := `FROM ubuntu:20.04 -RUN apt-get update` - - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`FROM ubuntu:22.04 -RUN apt-get update`)) - }) - - It("handles FROM at end of file without newline", func() { - dockerfile := `FROM ubuntu:20.04` - - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`FROM ubuntu:22.04`)) - }) - - It("handles FROM with trailing spaces", func() { - dockerfile := `FROM ubuntu:20.04 -RUN apt-get update` - - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(`FROM ubuntu:22.04 -RUN apt-get update`)) - }) - - It("handles FROM with leading spaces", func() { - dockerfile := ` FROM ubuntu:20.04 + It("returns error when no FROM statement with workspace-agent found", func() { + dockerfile := `FROM ubuntu:20.04 RUN apt-get update WORKDIR /app` - result, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "ubuntu:22.04") - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(` FROM ubuntu:22.04 -RUN apt-get update -WORKDIR /app`)) - }) + _, err := dockerfileManager.UpdateFromStatement(strings.NewReader(dockerfile), "workspace-agent-24.04:codesphere-v1.0.1") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no FROM statement with workspace-agent found in dockerfile")) }) }) }) From 5d2a8adf05b9f6a568cd303d00fb516a6a5e4547 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Fri, 31 Oct 2025 13:43:12 +0100 Subject: [PATCH 14/22] update: go mod tidy --- go.mod | 3 +-- go.sum | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/go.mod b/go.mod index c9e48e76..43eece70 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/rhysd/go-github-selfupdate v1.2.3 github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -34,7 +35,6 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect github.com/ulikunitz/xz v0.5.15 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/mod v0.28.0 // indirect @@ -45,5 +45,4 @@ require ( golang.org/x/text v0.29.0 // indirect golang.org/x/tools v0.37.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 08f95882..5725c2fa 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,6 @@ github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhg github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo/v2 v2.27.1 h1:0LJC8MpUSQnfnp4n/3W3GdlmJP3ENGF0ZPzjQGLPP7s= -github.com/onsi/ginkgo/v2 v2.27.1/go.mod h1:wmy3vCqiBjirARfVhAqFpYt8uvX0yaFe+GudAqqcCqA= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -69,8 +67,6 @@ github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag= github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -99,8 +95,6 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6 github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From e6a4e2519c6bf5f53d64c7f33b354b0088d69d2a Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Fri, 31 Oct 2025 13:44:11 +0100 Subject: [PATCH 15/22] update: regenerate docs --- docs/README.md | 3 +- docs/oms-cli.md | 3 +- docs/oms-cli_beta.md | 2 +- docs/oms-cli_beta_extend.md | 2 +- docs/oms-cli_beta_extend_baseimage.md | 3 +- docs/oms-cli_build.md | 3 +- docs/oms-cli_build_image.md | 34 ++++++++++++++++++++++ docs/oms-cli_build_images.md | 4 +-- docs/oms-cli_download.md | 2 +- docs/oms-cli_download_package.md | 2 +- docs/oms-cli_install.md | 2 +- docs/oms-cli_install_codesphere.md | 2 +- docs/oms-cli_licenses.md | 2 +- docs/oms-cli_list.md | 2 +- docs/oms-cli_list_api-keys.md | 2 +- docs/oms-cli_list_packages.md | 2 +- docs/oms-cli_register.md | 2 +- docs/oms-cli_revoke.md | 2 +- docs/oms-cli_revoke_api-key.md | 2 +- docs/oms-cli_update.md | 3 +- docs/oms-cli_update_api-key.md | 2 +- docs/oms-cli_update_dockerfile.md | 41 +++++++++++++++++++++++++++ docs/oms-cli_update_oms.md | 2 +- docs/oms-cli_update_package.md | 2 +- docs/oms-cli_version.md | 2 +- 25 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 docs/oms-cli_build_image.md create mode 100644 docs/oms-cli_update_dockerfile.md diff --git a/docs/README.md b/docs/README.md index 30f9f122..c12ef537 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ like downloading new versions. ### SEE ALSO * [oms-cli beta](oms-cli_beta.md) - Commands for early testing +* [oms-cli build](oms-cli_build.md) - Build and push images to a registry * [oms-cli download](oms-cli_download.md) - Download resources available through OMS * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components * [oms-cli licenses](oms-cli_licenses.md) - Print license information @@ -27,4 +28,4 @@ like downloading new versions. * [oms-cli update](oms-cli_update.md) - Update OMS related resources * [oms-cli version](oms-cli_version.md) - Print version -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli.md b/docs/oms-cli.md index 30f9f122..c12ef537 100644 --- a/docs/oms-cli.md +++ b/docs/oms-cli.md @@ -18,6 +18,7 @@ like downloading new versions. ### SEE ALSO * [oms-cli beta](oms-cli_beta.md) - Commands for early testing +* [oms-cli build](oms-cli_build.md) - Build and push images to a registry * [oms-cli download](oms-cli_download.md) - Download resources available through OMS * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components * [oms-cli licenses](oms-cli_licenses.md) - Print license information @@ -27,4 +28,4 @@ like downloading new versions. * [oms-cli update](oms-cli_update.md) - Update OMS related resources * [oms-cli version](oms-cli_version.md) - Print version -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_beta.md b/docs/oms-cli_beta.md index 47b99dae..4391976a 100644 --- a/docs/oms-cli_beta.md +++ b/docs/oms-cli_beta.md @@ -18,4 +18,4 @@ Be aware that that usage and behavior may change as the features are developed. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli beta extend](oms-cli_beta_extend.md) - Extend Codesphere ressources such as base images. -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_beta_extend.md b/docs/oms-cli_beta_extend.md index 6324a7a9..c0c4c315 100644 --- a/docs/oms-cli_beta_extend.md +++ b/docs/oms-cli_beta_extend.md @@ -17,4 +17,4 @@ Extend Codesphere ressources such as base images to customize them for your need * [oms-cli beta](oms-cli_beta.md) - Commands for early testing * [oms-cli beta extend baseimage](oms-cli_beta_extend_baseimage.md) - Extend Codesphere's workspace base image for customization -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_beta_extend_baseimage.md b/docs/oms-cli_beta_extend_baseimage.md index 32d15566..9a940f5c 100644 --- a/docs/oms-cli_beta_extend_baseimage.md +++ b/docs/oms-cli_beta_extend_baseimage.md @@ -17,6 +17,7 @@ oms-cli beta extend baseimage [flags] ### Options ``` + -b, --baseimage string Base image file name inside the package to extend (default: 'workspace-agent-24.04.tar') (default "workspace-agent-24.04.tar") -d, --dockerfile string Output Dockerfile to generate for extending the base image (default "Dockerfile") -f, --force Enforce package extraction -h, --help help for baseimage @@ -27,4 +28,4 @@ oms-cli beta extend baseimage [flags] * [oms-cli beta extend](oms-cli_beta_extend.md) - Extend Codesphere ressources such as base images. -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_build.md b/docs/oms-cli_build.md index 5f554214..99b1f5a4 100644 --- a/docs/oms-cli_build.md +++ b/docs/oms-cli_build.md @@ -15,6 +15,7 @@ Build and push container images to a registry using the provided configuration. ### SEE ALSO * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) +* [oms-cli build image](oms-cli_build_image.md) - Build and push Docker image using Dockerfile and Codesphere package version * [oms-cli build images](oms-cli_build_images.md) - Build and push container images -###### Auto generated by spf13/cobra on 28-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_build_image.md b/docs/oms-cli_build_image.md new file mode 100644 index 00000000..07c193e3 --- /dev/null +++ b/docs/oms-cli_build_image.md @@ -0,0 +1,34 @@ +## oms-cli build image + +Build and push Docker image using Dockerfile and Codesphere package version + +### Synopsis + +Build a Docker image from a Dockerfile and push it to a registry, tagged with the Codesphere version from the package. + +``` +oms-cli build image [flags] +``` + +### Examples + +``` +# Build image for Codesphere version 1.68.0 and push to specified registry +$ oms-cli build image --dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz --registry my-registry.com/my-image + +``` + +### Options + +``` + -d, --dockerfile string Path to the Dockerfile to build (required) + -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) +``` + +### SEE ALSO + +* [oms-cli build](oms-cli_build.md) - Build and push images to a registry + +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_build_images.md b/docs/oms-cli_build_images.md index bc3f0e15..6e161284 100644 --- a/docs/oms-cli_build_images.md +++ b/docs/oms-cli_build_images.md @@ -5,7 +5,7 @@ Build and push container images ### Synopsis Build and push container images based on the configuration file. -Extracts necessary images configuration to get the bomRef and processes them. +Extracts necessary image configurations from the provided install config and the downloaded package. ``` oms-cli build images [flags] @@ -22,4 +22,4 @@ oms-cli build images [flags] * [oms-cli build](oms-cli_build.md) - Build and push images to a registry -###### Auto generated by spf13/cobra on 28-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_download.md b/docs/oms-cli_download.md index cadb099e..aef66d2e 100644 --- a/docs/oms-cli_download.md +++ b/docs/oms-cli_download.md @@ -18,4 +18,4 @@ e.g. available Codesphere packages * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli download package](oms-cli_download_package.md) - Download a codesphere package -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_download_package.md b/docs/oms-cli_download_package.md index af577790..769d285d 100644 --- a/docs/oms-cli_download_package.md +++ b/docs/oms-cli_download_package.md @@ -36,4 +36,4 @@ $ oms-cli download package --version codesphere-v1.55.0 --file installer-lite.ta * [oms-cli download](oms-cli_download.md) - Download resources available through OMS -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_install.md b/docs/oms-cli_install.md index 384b7441..77ef478e 100644 --- a/docs/oms-cli_install.md +++ b/docs/oms-cli_install.md @@ -17,4 +17,4 @@ Coming soon: Install Codesphere and other components like Ceph and PostgreSQL. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli install codesphere](oms-cli_install_codesphere.md) - Install a Codesphere instance -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_install_codesphere.md b/docs/oms-cli_install_codesphere.md index 0b6aa06d..48302a56 100644 --- a/docs/oms-cli_install_codesphere.md +++ b/docs/oms-cli_install_codesphere.md @@ -26,4 +26,4 @@ oms-cli install codesphere [flags] * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_licenses.md b/docs/oms-cli_licenses.md index 434ad9ed..cd334708 100644 --- a/docs/oms-cli_licenses.md +++ b/docs/oms-cli_licenses.md @@ -20,4 +20,4 @@ oms-cli licenses [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_list.md b/docs/oms-cli_list.md index b2644c56..ac948485 100644 --- a/docs/oms-cli_list.md +++ b/docs/oms-cli_list.md @@ -19,4 +19,4 @@ eg. available Codesphere packages * [oms-cli list api-keys](oms-cli_list_api-keys.md) - List API keys * [oms-cli list packages](oms-cli_list_packages.md) - List available packages -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_list_api-keys.md b/docs/oms-cli_list_api-keys.md index 1236ab97..bdf42da8 100644 --- a/docs/oms-cli_list_api-keys.md +++ b/docs/oms-cli_list_api-keys.md @@ -20,4 +20,4 @@ oms-cli list api-keys [flags] * [oms-cli list](oms-cli_list.md) - List resources available through OMS -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_list_packages.md b/docs/oms-cli_list_packages.md index 69a753ab..49fa9bf7 100644 --- a/docs/oms-cli_list_packages.md +++ b/docs/oms-cli_list_packages.md @@ -20,4 +20,4 @@ oms-cli list packages [flags] * [oms-cli list](oms-cli_list.md) - List resources available through OMS -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_register.md b/docs/oms-cli_register.md index 678dd44a..962c9bf7 100644 --- a/docs/oms-cli_register.md +++ b/docs/oms-cli_register.md @@ -24,4 +24,4 @@ oms-cli register [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_revoke.md b/docs/oms-cli_revoke.md index 3e4c68b1..54c4c7d2 100644 --- a/docs/oms-cli_revoke.md +++ b/docs/oms-cli_revoke.md @@ -18,4 +18,4 @@ eg. api keys. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli revoke api-key](oms-cli_revoke_api-key.md) - Revoke an API key -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_revoke_api-key.md b/docs/oms-cli_revoke_api-key.md index 9933d386..9e8312b3 100644 --- a/docs/oms-cli_revoke_api-key.md +++ b/docs/oms-cli_revoke_api-key.md @@ -21,4 +21,4 @@ oms-cli revoke api-key [flags] * [oms-cli revoke](oms-cli_revoke.md) - Revoke resources available through OMS -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_update.md b/docs/oms-cli_update.md index 0304654f..301f686b 100644 --- a/docs/oms-cli_update.md +++ b/docs/oms-cli_update.md @@ -20,7 +20,8 @@ oms-cli update [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli update api-key](oms-cli_update_api-key.md) - Update an API key's expiration date +* [oms-cli update dockerfile](oms-cli_update_dockerfile.md) - Update FROM statement in Dockerfile with base image from package * [oms-cli update oms](oms-cli_update_oms.md) - Update the OMS CLI * [oms-cli update package](oms-cli_update_package.md) - Download a codesphere package -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_update_api-key.md b/docs/oms-cli_update_api-key.md index 5798dd20..bdad5901 100644 --- a/docs/oms-cli_update_api-key.md +++ b/docs/oms-cli_update_api-key.md @@ -22,4 +22,4 @@ oms-cli update api-key [flags] * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_update_dockerfile.md b/docs/oms-cli_update_dockerfile.md new file mode 100644 index 00000000..1df05923 --- /dev/null +++ b/docs/oms-cli_update_dockerfile.md @@ -0,0 +1,41 @@ +## oms-cli update dockerfile + +Update FROM statement in Dockerfile with base image from package + +### Synopsis + +Update the FROM statement in a Dockerfile to use the base image from a Codesphere package. + +This command extracts the base image from a Codesphere package and updates the FROM statement +in the specified Dockerfile to use that base image. The base image is loaded into the local Docker daemon so it can be used for building. + +``` +oms-cli update dockerfile [flags] +``` + +### Examples + +``` +# Update Dockerfile to use the default base image from the package (workspace-agent-24.04.tar) +$ oms-cli update dockerfile --dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz + +# Update Dockerfile to use the workspace-agent-20.04.tar base image from the package +$ oms-cli update dockerfile --dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz --baseimage workspace-agent-20.04.tar + +``` + +### Options + +``` + -b, --baseimage string Name of the base image to use (required) (default "workspace-agent-24.04.tar") + -d, --dockerfile string Path to the Dockerfile to update (required) + -f, --force Force re-extraction of the package + -h, --help help for dockerfile + -p, --package string Path to the Codesphere package (required) +``` + +### SEE ALSO + +* [oms-cli update](oms-cli_update.md) - Update OMS related resources + +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_update_oms.md b/docs/oms-cli_update_oms.md index fd0a9c87..2df6140c 100644 --- a/docs/oms-cli_update_oms.md +++ b/docs/oms-cli_update_oms.md @@ -20,4 +20,4 @@ oms-cli update oms [flags] * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_update_package.md b/docs/oms-cli_update_package.md index 87258d4b..7b48a0e0 100644 --- a/docs/oms-cli_update_package.md +++ b/docs/oms-cli_update_package.md @@ -36,4 +36,4 @@ $ oms-cli download package --version codesphere-v1.55.0 --file installer-lite.ta * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 diff --git a/docs/oms-cli_version.md b/docs/oms-cli_version.md index 85238b9c..064fe0fe 100644 --- a/docs/oms-cli_version.md +++ b/docs/oms-cli_version.md @@ -20,4 +20,4 @@ oms-cli version [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 27-Oct-2025 +###### Auto generated by spf13/cobra on 31-Oct-2025 From 12b8cac8f935a5a5b2863551ed04e5b12aa35a4b Mon Sep 17 00:00:00 2001 From: siherrmann <25087590+siherrmann@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:47:44 +0000 Subject: [PATCH 16/22] chore(docs): Auto-update docs and licenses Signed-off-by: siherrmann <25087590+siherrmann@users.noreply.github.com> --- NOTICE | 30 ++++++++++++------------------ cli/cmd/build_image.go | 3 +++ cli/cmd/update_dockerfile.go | 3 +++ internal/tmpl/NOTICE | 30 ++++++++++++++++++------------ internal/util/docker.go | 3 +++ 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/NOTICE b/NOTICE index 7881e7f0..2aaf1c7f 100644 --- a/NOTICE +++ b/NOTICE @@ -9,17 +9,11 @@ Version: v3.5.1 License: MIT License URL: https://github.com/blang/semver/blob/v3.5.1/LICENSE ----------- -Module: github.com/clipperhouse/stringish -Version: v0.1.1 -License: MIT -License URL: https://github.com/clipperhouse/stringish/blob/v0.1.1/LICENSE - ---------- Module: github.com/clipperhouse/uax29/v2 -Version: v2.3.0 +Version: v2.2.0 License: MIT -License URL: https://github.com/clipperhouse/uax29/blob/v2.3.0/LICENSE +License URL: https://github.com/clipperhouse/uax29/blob/v2.2.0/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go/pkg/io @@ -77,9 +71,9 @@ License URL: https://github.com/inconshreveable/go-update/blob/8152e7eb6ccf/inte ---------- Module: github.com/jedib0t/go-pretty/v6 -Version: v6.6.9 +Version: v6.6.8 License: MIT -License URL: https://github.com/jedib0t/go-pretty/blob/v6.6.9/LICENSE +License URL: https://github.com/jedib0t/go-pretty/blob/v6.6.8/LICENSE ---------- Module: github.com/mattn/go-runewidth @@ -119,9 +113,9 @@ License URL: https://github.com/spf13/pflag/blob/v1.0.10/LICENSE ---------- Module: github.com/stretchr/objx -Version: v0.5.3 +Version: v0.5.2 License: MIT -License URL: https://github.com/stretchr/objx/blob/v0.5.3/LICENSE +License URL: https://github.com/stretchr/objx/blob/v0.5.2/LICENSE ---------- Module: github.com/stretchr/testify @@ -143,21 +137,21 @@ License URL: https://github.com/ulikunitz/xz/blob/v0.5.15/LICENSE ---------- Module: golang.org/x/crypto -Version: v0.43.0 +Version: v0.42.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/crypto/+/v0.43.0:LICENSE +License URL: https://cs.opensource.google/go/x/crypto/+/v0.42.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.32.0 +Version: v0.30.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.32.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.30.0:LICENSE ---------- Module: golang.org/x/text -Version: v0.30.0 +Version: v0.29.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/text/+/v0.30.0:LICENSE +License URL: https://cs.opensource.google/go/x/text/+/v0.29.0:LICENSE ---------- Module: gopkg.in/yaml.v3 diff --git a/cli/cmd/build_image.go b/cli/cmd/build_image.go index 5b91abe5..c85ebecf 100644 --- a/cli/cmd/build_image.go +++ b/cli/cmd/build_image.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package cmd import ( diff --git a/cli/cmd/update_dockerfile.go b/cli/cmd/update_dockerfile.go index a56330aa..cfee2996 100644 --- a/cli/cmd/update_dockerfile.go +++ b/cli/cmd/update_dockerfile.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package cmd import ( diff --git a/internal/tmpl/NOTICE b/internal/tmpl/NOTICE index 2aaf1c7f..7881e7f0 100644 --- a/internal/tmpl/NOTICE +++ b/internal/tmpl/NOTICE @@ -9,11 +9,17 @@ Version: v3.5.1 License: MIT License URL: https://github.com/blang/semver/blob/v3.5.1/LICENSE +---------- +Module: github.com/clipperhouse/stringish +Version: v0.1.1 +License: MIT +License URL: https://github.com/clipperhouse/stringish/blob/v0.1.1/LICENSE + ---------- Module: github.com/clipperhouse/uax29/v2 -Version: v2.2.0 +Version: v2.3.0 License: MIT -License URL: https://github.com/clipperhouse/uax29/blob/v2.2.0/LICENSE +License URL: https://github.com/clipperhouse/uax29/blob/v2.3.0/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go/pkg/io @@ -71,9 +77,9 @@ License URL: https://github.com/inconshreveable/go-update/blob/8152e7eb6ccf/inte ---------- Module: github.com/jedib0t/go-pretty/v6 -Version: v6.6.8 +Version: v6.6.9 License: MIT -License URL: https://github.com/jedib0t/go-pretty/blob/v6.6.8/LICENSE +License URL: https://github.com/jedib0t/go-pretty/blob/v6.6.9/LICENSE ---------- Module: github.com/mattn/go-runewidth @@ -113,9 +119,9 @@ License URL: https://github.com/spf13/pflag/blob/v1.0.10/LICENSE ---------- Module: github.com/stretchr/objx -Version: v0.5.2 +Version: v0.5.3 License: MIT -License URL: https://github.com/stretchr/objx/blob/v0.5.2/LICENSE +License URL: https://github.com/stretchr/objx/blob/v0.5.3/LICENSE ---------- Module: github.com/stretchr/testify @@ -137,21 +143,21 @@ License URL: https://github.com/ulikunitz/xz/blob/v0.5.15/LICENSE ---------- Module: golang.org/x/crypto -Version: v0.42.0 +Version: v0.43.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/crypto/+/v0.42.0:LICENSE +License URL: https://cs.opensource.google/go/x/crypto/+/v0.43.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.30.0 +Version: v0.32.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.30.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.32.0:LICENSE ---------- Module: golang.org/x/text -Version: v0.29.0 +Version: v0.30.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/text/+/v0.29.0:LICENSE +License URL: https://cs.opensource.google/go/x/text/+/v0.30.0:LICENSE ---------- Module: gopkg.in/yaml.v3 diff --git a/internal/util/docker.go b/internal/util/docker.go index 0fe9273f..db38aa98 100644 --- a/internal/util/docker.go +++ b/internal/util/docker.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package util import ( From 27bfd830f10e82a1a160c65c187b6db4376d58c1 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Mon, 3 Nov 2025 11:35:35 +0100 Subject: [PATCH 17/22] update: update image name handling --- cli/cmd/extend_baseimage.go | 9 +- cli/cmd/extend_baseimage_test.go | 14 +- cli/cmd/update_dockerfile.go | 28 +- cli/cmd/update_dockerfile_test.go | 40 ++- internal/installer/files/bom_json.go | 83 ++++++ internal/installer/files/bom_json_test.go | 226 ++++++++++++++++ internal/installer/files/config_yaml_test.go | 243 ++++++++++++++++++ internal/installer/files/files_suite_test.go | 16 ++ .../installer/files/oci_image_index_test.go | 217 ++++++++++++++++ internal/installer/mocks.go | 170 +++++++----- internal/installer/package.go | 52 ++-- 11 files changed, 977 insertions(+), 121 deletions(-) create mode 100644 internal/installer/files/bom_json.go create mode 100644 internal/installer/files/bom_json_test.go create mode 100644 internal/installer/files/config_yaml_test.go create mode 100644 internal/installer/files/files_suite_test.go create mode 100644 internal/installer/files/oci_image_index_test.go diff --git a/cli/cmd/extend_baseimage.go b/cli/cmd/extend_baseimage.go index 3768a29c..248b7dcd 100644 --- a/cli/cmd/extend_baseimage.go +++ b/cli/cmd/extend_baseimage.go @@ -66,7 +66,7 @@ func AddExtendBaseimageCmd(extend *cobra.Command, opts *GlobalOptions) { } baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load base image from") baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Dockerfile, "dockerfile", "d", "Dockerfile", "Output Dockerfile to generate for extending the base image") - baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Baseimage, "baseimage", "b", "workspace-agent-24.04.tar", "Base image file name inside the package to extend (default: 'workspace-agent-24.04.tar')") + baseimage.cmd.Flags().StringVarP(&baseimage.Opts.Baseimage, "baseimage", "b", "workspace-agent-24.04", "Base image file name inside the package to extend (default: 'workspace-agent-24.04')") baseimage.cmd.Flags().BoolVarP(&baseimage.Opts.Force, "force", "f", false, "Enforce package extraction") extend.AddCommand(baseimage.cmd) @@ -75,11 +75,16 @@ func AddExtendBaseimageCmd(extend *cobra.Command, opts *GlobalOptions) { } func (c *ExtendBaseimageCmd) ExtendBaseimage(pm installer.PackageManager, im system.ImageManager) error { - imagePath, imageName, err := pm.GetImagePathAndName(c.Opts.Baseimage, c.Opts.Force) + imageName, err := pm.GetBaseimageName(c.Opts.Baseimage) if err != nil { return fmt.Errorf("failed to get image name: %w", err) } + imagePath, err := pm.GetBaseimagePath(c.Opts.Baseimage, c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to get image path: %w", err) + } + err = tmpl.GenerateDockerfile(pm.FileIO(), c.Opts.Dockerfile, imageName) if err != nil { return fmt.Errorf("failed to generate dockerfile: %w", err) diff --git a/cli/cmd/extend_baseimage_test.go b/cli/cmd/extend_baseimage_test.go index 01eba1a4..787d9de9 100644 --- a/cli/cmd/extend_baseimage_test.go +++ b/cli/cmd/extend_baseimage_test.go @@ -66,7 +66,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) - mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("", "", errors.New("failed to extract package to workdir: extraction failed")) + mockPackageManager.EXPECT().GetBaseimageName("").Return("", errors.New("failed to get image name: extraction failed")) err := c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(HaveOccurred()) @@ -77,7 +77,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) - mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("", "", errors.New("failed to extract OCI image index: index extraction failed")) + mockPackageManager.EXPECT().GetBaseimageName("").Return("", errors.New("failed to extract OCI image index: index extraction failed")) err := c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(HaveOccurred()) @@ -88,7 +88,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) - mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("", "", errors.New("failed to read image tags: no image names found")) + mockPackageManager.EXPECT().GetBaseimageName("").Return("", errors.New("failed to read image tags: no image names found")) err := c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(HaveOccurred()) @@ -106,7 +106,8 @@ var _ = Describe("ExtendBaseimageCmd", func() { defer os.Remove(tempFile.Name()) defer tempFile.Close() - mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04-base", 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) mockFileIO.EXPECT().Create("Dockerfile").Return(tempFile, nil) mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(errors.New("load failed")) @@ -121,7 +122,7 @@ var _ = Describe("ExtendBaseimageCmd", func() { mockImageManager := system.NewMockImageManager(GinkgoT()) c.Opts.Force = true - mockPackageManager.EXPECT().GetImagePathAndName("", true).Return("", "", errors.New("failed to extract package to workdir: extraction failed")) + mockPackageManager.EXPECT().GetBaseimageName("").Return("", errors.New("failed to extract package to workdir: extraction failed")) err := c.ExtendBaseimage(mockPackageManager, mockImageManager) Expect(err).To(HaveOccurred()) @@ -139,7 +140,8 @@ var _ = Describe("ExtendBaseimageCmd", func() { defer os.Remove(tempFile.Name()) defer tempFile.Close() - mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04-base", 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) mockFileIO.EXPECT().Create("Dockerfile").Return(tempFile, nil) mockImageManager.EXPECT().LoadImage("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar").Return(nil) diff --git a/cli/cmd/update_dockerfile.go b/cli/cmd/update_dockerfile.go index a56330aa..469c5772 100644 --- a/cli/cmd/update_dockerfile.go +++ b/cli/cmd/update_dockerfile.go @@ -55,8 +55,8 @@ func AddUpdateDockerfileCmd(parentCmd *cobra.Command, opts *GlobalOptions) { This command extracts the base image from a Codesphere package and updates the FROM statement in the specified Dockerfile to use that base image. The base image is loaded into the local Docker daemon so it can be used for building.`, Example: formatExamplesWithBinary("update dockerfile", []io.Example{ - {Cmd: "--dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz", Desc: "Update Dockerfile to use the default base image from the package (workspace-agent-24.04.tar)"}, - {Cmd: "--dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz --baseimage workspace-agent-20.04.tar", Desc: "Update Dockerfile to use the workspace-agent-20.04.tar base image from the package"}, + {Cmd: "--dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz", Desc: "Update Dockerfile to use the default base image from the package (workspace-agent-24.04)"}, + {Cmd: "--dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz --baseimage workspace-agent-20.04.tar", Desc: "Update Dockerfile to use the workspace-agent-20.04 base image from the package"}, }, "oms-cli"), Args: cobra.ExactArgs(0), }, @@ -66,7 +66,7 @@ in the specified Dockerfile to use that base image. The base image is loaded int dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Dockerfile, "dockerfile", "d", "", "Path to the Dockerfile to update (required)") dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Package, "package", "p", "", "Path to the Codesphere package (required)") - dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Baseimage, "baseimage", "b", "workspace-agent-24.04.tar", "Name of the base image to use (required)") + dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Baseimage, "baseimage", "b", "workspace-agent-24.04", "Name of the base image to use (required)") dockerfileCmd.cmd.Flags().BoolVarP(&dockerfileCmd.Opts.Force, "force", "f", false, "Force re-extraction of the package") util.MarkFlagRequired(dockerfileCmd.cmd, "dockerfile") @@ -78,11 +78,25 @@ 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 { - imagePath, imageName, err := pm.GetImagePathAndName(c.Opts.Baseimage, c.Opts.Force) + imageName, err := pm.GetBaseimageName(c.Opts.Baseimage) if err != nil { return fmt.Errorf("failed to get image name: %w", err) } + imagePath, err := pm.GetBaseimagePath(c.Opts.Baseimage, c.Opts.Force) + if err != nil { + return fmt.Errorf("failed to get image path: %w", err) + } + + log.Printf("Loading container image from package into local docker daemon: %s", imagePath) + + // Loading image before updating the Dockerfile to ensure it's available for builds + err = im.LoadImage(imagePath) + if err != nil { + return fmt.Errorf("failed to load baseimage file %s: %w", imagePath, err) + } + + // Update dockerfile FROM statement dockerfileFile, err := pm.FileIO().Open(c.Opts.Dockerfile) if err != nil { return fmt.Errorf("failed to open dockerfile %s: %w", c.Opts.Dockerfile, err) @@ -101,12 +115,6 @@ func (c *UpdateDockerfileCmd) UpdateDockerfile(pm installer.PackageManager, im s } log.Printf("Successfully updated FROM statement in %s to use %s", c.Opts.Dockerfile, imageName) - log.Printf("Loading container image from package into local docker daemon: %s", imagePath) - - err = im.LoadImage(imagePath) - if err != nil { - return fmt.Errorf("failed to load baseimage file %s: %w", imagePath, err) - } return nil } diff --git a/cli/cmd/update_dockerfile_test.go b/cli/cmd/update_dockerfile_test.go index e56f0766..00cc666d 100644 --- a/cli/cmd/update_dockerfile_test.go +++ b/cli/cmd/update_dockerfile_test.go @@ -87,7 +87,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "workspace-agent-24.04.tar" c.Opts.Force = false - mockPackageManager.EXPECT().GetImagePathAndName("workspace-agent-24.04.tar", false).Return("", "", errors.New("failed to extract image")) + mockPackageManager.EXPECT().GetBaseimageName("workspace-agent-24.04.tar").Return("", errors.New("failed to extract image")) err := c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) Expect(err).To(HaveOccurred()) @@ -103,7 +103,9 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "" c.Opts.Force = false - mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04", 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) mockPackageManager.EXPECT().FileIO().Return(mockFileIO) mockFileIO.EXPECT().Open("Dockerfile").Return(nil, errors.New("file not found")) @@ -115,31 +117,16 @@ var _ = Describe("UpdateDockerfileCmd", func() { It("fails when image manager fails to load image", func() { mockPackageManager := installer.NewMockPackageManager(GinkgoT()) mockImageManager := system.NewMockImageManager(GinkgoT()) - mockFileIO := util.NewMockFileIO(GinkgoT()) - - // Create a temporary file for the Dockerfile - tempFile, err := os.CreateTemp("", "dockerfile-test-*") - Expect(err).To(BeNil()) - DeferCleanup(func() { - tempFile.Close() - os.Remove(tempFile.Name()) - }) - _, err = tempFile.WriteString(sampleDockerfileContent) - Expect(err).To(BeNil()) - // Reset file position to beginning - tempFile.Seek(0, 0) c.Opts.Dockerfile = "Dockerfile" c.Opts.Baseimage = "" c.Opts.Force = false - mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04", nil) - mockPackageManager.EXPECT().FileIO().Return(mockFileIO) - mockFileIO.EXPECT().Open("Dockerfile").Return(tempFile, nil) - mockFileIO.EXPECT().WriteFile("Dockerfile", []byte("FROM ubuntu:24.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).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")) - err = c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) + err := c.UpdateDockerfile(mockPackageManager, mockImageManager, []string{}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to load baseimage file")) }) @@ -165,7 +152,9 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "" c.Opts.Force = false - mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04", 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) mockPackageManager.EXPECT().FileIO().Return(mockFileIO) mockFileIO.EXPECT().Open("Dockerfile").Return(tempFile, nil) mockFileIO.EXPECT().WriteFile("Dockerfile", []byte("FROM ubuntu:24.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).Return(errors.New("write failed")) @@ -196,7 +185,8 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "" c.Opts.Force = false - mockPackageManager.EXPECT().GetImagePathAndName("", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "ubuntu:24.04", 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) mockFileIO.EXPECT().Open("Dockerfile").Return(tempFile, nil) mockFileIO.EXPECT().WriteFile("Dockerfile", []byte("FROM ubuntu:24.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).Return(nil) @@ -227,7 +217,8 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "workspace-agent-20.04.tar" c.Opts.Force = true - mockPackageManager.EXPECT().GetImagePathAndName("workspace-agent-20.04.tar", true).Return("/test/workdir/deps/codesphere/images/workspace-agent-20.04.tar", "ubuntu:20.04", 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) mockFileIO.EXPECT().Open("Dockerfile").Return(tempFile, nil) mockFileIO.EXPECT().WriteFile("Dockerfile", []byte("FROM ubuntu:20.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).Return(nil) @@ -258,7 +249,8 @@ var _ = Describe("UpdateDockerfileCmd", func() { c.Opts.Baseimage = "workspace-agent-24.04.tar" c.Opts.Force = false - mockPackageManager.EXPECT().GetImagePathAndName("workspace-agent-24.04.tar", false).Return("/test/workdir/deps/codesphere/images/workspace-agent-24.04.tar", "registry.example.com/workspace-agent:24.04", 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) mockFileIO.EXPECT().Open("custom/Dockerfile").Return(tempFile, nil) mockFileIO.EXPECT().WriteFile("custom/Dockerfile", []byte("FROM registry.example.com/workspace-agent:24.04\nRUN apt-get update && apt-get install -y curl\nWORKDIR /app\nCOPY . .\nCMD [\"./start.sh\"]"), os.FileMode(0644)).Return(nil) diff --git a/internal/installer/files/bom_json.go b/internal/installer/files/bom_json.go new file mode 100644 index 00000000..91ab8894 --- /dev/null +++ b/internal/installer/files/bom_json.go @@ -0,0 +1,83 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package files + +import ( + "encoding/json" + "fmt" + "os" +) + +// BomConfig represents the Bill of Materials configuration +type BomConfig struct { + Components map[string]ComponentConfig `json:"components"` + Migrations MigrationsConfig `json:"migrations"` +} + +// ComponentConfig represents a component in the BOM +type ComponentConfig struct { + ContainerImages map[string]string `json:"containerImages,omitempty"` + Files map[string]FileRef `json:"files,omitempty"` +} + +// FileRef represents a file reference in the BOM +type FileRef struct { + SrcPath string `json:"srcPath,omitempty"` + SrcUrl string `json:"srcUrl,omitempty"` + Executable bool `json:"executable,omitempty"` + Glob *GlobRef `json:"glob,omitempty"` +} + +// GlobRef represents a glob-based file reference +type GlobRef struct { + Cwd string `json:"cwd"` + Include string `json:"include"` + Exclude []string `json:"exclude,omitempty"` +} + +// MigrationsConfig represents the migrations configuration +type MigrationsConfig struct { + Db DbMigrationConfig `json:"db"` +} + +// DbMigrationConfig represents database migration configuration +type DbMigrationConfig struct { + Path string `json:"path"` + From string `json:"from"` +} + +// CodesphereComponent represents the codesphere-specific component +type CodesphereComponent struct { + ContainerImages map[string]string `json:"containerImages"` + Files map[string]FileRef `json:"files"` +} + +// ParseBomConfig reads and parses a BOM JSON file +func (b *BomConfig) ParseBomConfig(filePath string) error { + bomData, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read BOM file: %w", err) + } + + err = json.Unmarshal(bomData, b) + if err != nil { + return fmt.Errorf("failed to parse JSON BOM: %w", err) + } + + return nil +} + +// GetCodesphereContainerImages returns all container images from the codesphere component +func (b *BomConfig) GetCodesphereContainerImages() (map[string]string, error) { + if b.Components == nil { + return nil, fmt.Errorf("codesphere component not found in BOM") + } + + codesphereComp, exists := b.Components["codesphere"] + if !exists { + return nil, fmt.Errorf("codesphere component not found in BOM") + } + + return codesphereComp.ContainerImages, nil +} diff --git a/internal/installer/files/bom_json_test.go b/internal/installer/files/bom_json_test.go new file mode 100644 index 00000000..20724f46 --- /dev/null +++ b/internal/installer/files/bom_json_test.go @@ -0,0 +1,226 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package files_test + +import ( + "encoding/json" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/codesphere-cloud/oms/internal/installer/files" +) + +var _ = Describe("BomJson", func() { + var ( + bomConfig *files.BomConfig + tempDir string + bomFile string + sampleBom map[string]interface{} + ) + + BeforeEach(func() { + bomConfig = &files.BomConfig{} + + var err error + tempDir, err = os.MkdirTemp("", "bom_test") + Expect(err).NotTo(HaveOccurred()) + + bomFile = filepath.Join(tempDir, "bom.json") + + // Create sample BOM data matching the real structure + sampleBom = map[string]interface{}{ + "components": map[string]interface{}{ + "codesphere": map[string]interface{}{ + "containerImages": map[string]interface{}{ + "workspace-agent-24.04": "ghcr.io/codesphere-cloud/codesphere-monorepo/workspace-agent-24.04:codesphere-v1.66.0", + "workspace-agent-20.04": "ghcr.io/codesphere-cloud/codesphere-monorepo/workspace-agent-20.04:codesphere-v1.66.0", + "auth-service": "ghcr.io/codesphere-cloud/codesphere-monorepo/auth-service:codesphere-v1.66.0", + "ide-service": "ghcr.io/codesphere-cloud/codesphere-monorepo/ide-service:codesphere-v1.66.0", + "workspace-service": "ghcr.io/codesphere-cloud/codesphere-monorepo/workspace-service:codesphere-v1.66.0", + "nginx": "ghcr.io/codesphere-cloud/docker/nginx:1.26.3", + }, + "files": map[string]interface{}{ + "chart": map[string]interface{}{ + "glob": map[string]interface{}{ + "cwd": "helm/codesphere", + "include": "**/*", + "exclude": []string{"*.json5", "values-*.yaml"}, + }, + }, + "schemaDump": map[string]interface{}{ + "srcPath": "infra/bin/private-cloud/pg-masterdata.sql", + }, + }, + }, + "docker": map[string]interface{}{ + "files": map[string]interface{}{ + "24.04_containerd": map[string]interface{}{ + "srcUrl": "https://download.docker.com/linux/ubuntu/dists/noble/pool/stable/amd64/containerd.io_1.6.31-1_amd64.deb", + "executable": false, + }, + }, + }, + "kubernetes": map[string]interface{}{ + "files": map[string]interface{}{ + "k0s": map[string]interface{}{ + "srcUrl": "https://github.com/k0sproject/k0s/releases/download/v1.28.4%2Bk0s.0/k0s-v1.28.4+k0s.0-amd64", + "executable": true, + }, + }, + }, + }, + "migrations": map[string]interface{}{ + "db": map[string]interface{}{ + "path": "packages/migrations/released", + "from": "0.0.1", + }, + }, + } + }) + + AfterEach(func() { + os.RemoveAll(tempDir) + }) + + Describe("ParseBomConfig", func() { + It("should parse a valid BOM file successfully", func() { + // Write sample BOM to file + bomData, err := json.Marshal(sampleBom) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(bomFile, bomData, 0644) + Expect(err).NotTo(HaveOccurred()) + + err = bomConfig.ParseBomConfig(bomFile) + Expect(err).NotTo(HaveOccurred()) + + Expect(bomConfig.Components).To(HaveKey("codesphere")) + Expect(bomConfig.Components).To(HaveKey("docker")) + Expect(bomConfig.Components).To(HaveKey("kubernetes")) + Expect(bomConfig.Migrations.Db.Path).To(Equal("packages/migrations/released")) + Expect(bomConfig.Migrations.Db.From).To(Equal("0.0.1")) + }) + + It("should return error for non-existent file", func() { + err := bomConfig.ParseBomConfig("/non/existent/file.json") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to read BOM file")) + }) + + It("should return error for invalid JSON", func() { + err := os.WriteFile(bomFile, []byte("invalid json content"), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = bomConfig.ParseBomConfig(bomFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse JSON BOM")) + }) + + It("should handle empty BOM file", func() { + err := os.WriteFile(bomFile, []byte("{}"), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = bomConfig.ParseBomConfig(bomFile) + Expect(err).NotTo(HaveOccurred()) + + Expect(bomConfig.Components).To(BeEmpty()) + }) + }) + + Describe("GetCodesphereContainerImages", func() { + BeforeEach(func() { + bomData, err := json.Marshal(sampleBom) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(bomFile, bomData, 0644) + Expect(err).NotTo(HaveOccurred()) + + err = bomConfig.ParseBomConfig(bomFile) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should return all codesphere container images", func() { + images, err := bomConfig.GetCodesphereContainerImages() + Expect(err).NotTo(HaveOccurred()) + Expect(images).NotTo(BeNil()) + + Expect(images).To(HaveKey("workspace-agent-24.04")) + Expect(images).To(HaveKey("workspace-agent-20.04")) + Expect(images).To(HaveKey("auth-service")) + Expect(images).To(HaveKey("ide-service")) + Expect(images).To(HaveKey("workspace-service")) + Expect(images).To(HaveKey("nginx")) + + Expect(images["workspace-agent-24.04"]).To(Equal("ghcr.io/codesphere-cloud/codesphere-monorepo/workspace-agent-24.04:codesphere-v1.66.0")) + Expect(images["auth-service"]).To(Equal("ghcr.io/codesphere-cloud/codesphere-monorepo/auth-service:codesphere-v1.66.0")) + + // Should have 6 images in our test data + Expect(len(images)).To(Equal(6)) + }) + + It("should return error when codesphere component is missing", func() { + // Create a fresh bomConfig for this test to avoid state from BeforeEach + freshBomConfig := &files.BomConfig{} + + // Create BOM without codesphere component + bomWithoutCodesphere := map[string]interface{}{ + "components": map[string]interface{}{ + "docker": map[string]interface{}{ + "files": map[string]interface{}{ + "containerd": map[string]interface{}{ + "srcUrl": "https://download.docker.com/test.deb", + }, + }, + }, + }, + } + + bomData, err := json.Marshal(bomWithoutCodesphere) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(bomFile, bomData, 0644) + Expect(err).NotTo(HaveOccurred()) + + err = freshBomConfig.ParseBomConfig(bomFile) + Expect(err).NotTo(HaveOccurred()) + + _, err = freshBomConfig.GetCodesphereContainerImages() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("codesphere component not found in BOM")) + }) + + It("should handle codesphere component with no container images", func() { + bomWithEmptyCodesphere := map[string]interface{}{ + "components": map[string]interface{}{ + "codesphere": map[string]interface{}{ + "files": map[string]interface{}{ + "chart": map[string]interface{}{ + "glob": map[string]interface{}{ + "cwd": "helm/codesphere", + "include": "**/*", + }, + }, + }, + }, + }, + } + + bomData, err := json.Marshal(bomWithEmptyCodesphere) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(bomFile, bomData, 0644) + Expect(err).NotTo(HaveOccurred()) + + err = bomConfig.ParseBomConfig(bomFile) + Expect(err).NotTo(HaveOccurred()) + + images, err := bomConfig.GetCodesphereContainerImages() + Expect(err).NotTo(HaveOccurred()) + Expect(images).To(BeEmpty()) + }) + }) +}) diff --git a/internal/installer/files/config_yaml_test.go b/internal/installer/files/config_yaml_test.go new file mode 100644 index 00000000..397d2a20 --- /dev/null +++ b/internal/installer/files/config_yaml_test.go @@ -0,0 +1,243 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package files_test + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/codesphere-cloud/oms/internal/installer/files" +) + +var _ = Describe("ConfigYaml", func() { + var ( + rootConfig *files.RootConfig + tempDir string + configFile string + sampleYaml string + ) + + BeforeEach(func() { + rootConfig = &files.RootConfig{} + + var err error + tempDir, err = os.MkdirTemp("", "config_yaml_test") + Expect(err).NotTo(HaveOccurred()) + + configFile = filepath.Join(tempDir, "config.yaml") + + sampleYaml = `registry: + server: registry.example.com + +codesphere: + deployConfig: + images: + workspace-agent-24.04: + name: ubuntu-24.04 + supportedUntil: "2029-04-01" + flavors: + default: + image: + bomRef: workspace-agent-24.04 + dockerfile: dockerfile-24.04 + pool: + 8: 2 + 16: 1 + minimal: + image: + bomRef: workspace-agent-24.04-minimal + dockerfile: dockerfile-24.04-minimal + pool: + 4: 1 + workspace-agent-20.04: + name: ubuntu-20.04 + supportedUntil: "2025-04-01" + flavors: + default: + image: + bomRef: workspace-agent-20.04 + dockerfile: dockerfile-20.04 + pool: + 8: 1 + ide-service: + name: ide-service + supportedUntil: "2026-01-01" + flavors: + default: + image: + bomRef: ide-service + pool: + 4: 2 +` + }) + + AfterEach(func() { + os.RemoveAll(tempDir) + }) + + Describe("ParseConfig", func() { + It("should parse a valid YAML config file successfully", func() { + err := os.WriteFile(configFile, []byte(sampleYaml), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = rootConfig.ParseConfig(configFile) + Expect(err).NotTo(HaveOccurred()) + + Expect(rootConfig.Registry.Server).To(Equal("registry.example.com")) + Expect(rootConfig.Codesphere.DeployConfig.Images).To(HaveKey("workspace-agent-24.04")) + Expect(rootConfig.Codesphere.DeployConfig.Images).To(HaveKey("workspace-agent-20.04")) + Expect(rootConfig.Codesphere.DeployConfig.Images).To(HaveKey("ide-service")) + + // Check specific image config + workspaceAgent24 := rootConfig.Codesphere.DeployConfig.Images["workspace-agent-24.04"] + Expect(workspaceAgent24.Name).To(Equal("ubuntu-24.04")) + Expect(workspaceAgent24.SupportedUntil).To(Equal("2029-04-01")) + Expect(workspaceAgent24.Flavors).To(HaveKey("default")) + Expect(workspaceAgent24.Flavors).To(HaveKey("minimal")) + + // Check flavor details + defaultFlavor := workspaceAgent24.Flavors["default"] + Expect(defaultFlavor.Image.BomRef).To(Equal("workspace-agent-24.04")) + Expect(defaultFlavor.Image.Dockerfile).To(Equal("dockerfile-24.04")) + Expect(defaultFlavor.Pool).To(HaveKeyWithValue(8, 2)) + Expect(defaultFlavor.Pool).To(HaveKeyWithValue(16, 1)) + }) + + It("should return error for non-existent file", func() { + err := rootConfig.ParseConfig("/non/existent/config.yaml") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to read config file")) + }) + + It("should return error for invalid YAML", func() { + invalidYaml := `registry: + server: registry.example.com +codesphere: + deployConfig: + images: + - invalid: yaml structure without proper mapping +` + err := os.WriteFile(configFile, []byte(invalidYaml), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = rootConfig.ParseConfig(configFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse YAML config")) + }) + + It("should handle empty config file", func() { + err := os.WriteFile(configFile, []byte(""), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = rootConfig.ParseConfig(configFile) + Expect(err).NotTo(HaveOccurred()) + + Expect(rootConfig.Registry.Server).To(BeEmpty()) + Expect(rootConfig.Codesphere.DeployConfig.Images).To(BeEmpty()) + }) + + It("should handle minimal valid config", func() { + minimalYaml := `registry: + server: minimal.registry.com +codesphere: + deployConfig: + images: {} +` + err := os.WriteFile(configFile, []byte(minimalYaml), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = rootConfig.ParseConfig(configFile) + Expect(err).NotTo(HaveOccurred()) + + Expect(rootConfig.Registry.Server).To(Equal("minimal.registry.com")) + Expect(rootConfig.Codesphere.DeployConfig.Images).To(BeEmpty()) + }) + }) + + Describe("ExtractBomRefs", func() { + BeforeEach(func() { + err := os.WriteFile(configFile, []byte(sampleYaml), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = rootConfig.ParseConfig(configFile) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should extract all BOM references from config", func() { + bomRefs := rootConfig.ExtractBomRefs() + + Expect(bomRefs).NotTo(BeEmpty()) + Expect(bomRefs).To(ContainElement("workspace-agent-24.04")) + Expect(bomRefs).To(ContainElement("workspace-agent-24.04-minimal")) + Expect(bomRefs).To(ContainElement("workspace-agent-20.04")) + Expect(bomRefs).To(ContainElement("ide-service")) + Expect(len(bomRefs)).To(Equal(4)) + }) + + It("should return empty slice when no images are configured", func() { + emptyConfig := &files.RootConfig{} + bomRefs := emptyConfig.ExtractBomRefs() + + Expect(bomRefs).To(BeEmpty()) + }) + + It("should handle flavors without BOM references", func() { + noImagesConfig := &files.RootConfig{} + yamlWithoutBomRefs := `registry: + server: registry.example.com +codesphere: + deployConfig: + images: + test-image: + name: test + flavors: + default: + image: + dockerfile: dockerfile-only + pool: + 4: 1 +` + err := os.WriteFile(configFile, []byte(yamlWithoutBomRefs), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = noImagesConfig.ParseConfig(configFile) + Expect(err).NotTo(HaveOccurred()) + + bomRefs := noImagesConfig.ExtractBomRefs() + Expect(bomRefs).To(BeEmpty()) + }) + }) + + Describe("ExtractWorkspaceDockerfiles", func() { + BeforeEach(func() { + err := os.WriteFile(configFile, []byte(sampleYaml), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = rootConfig.ParseConfig(configFile) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should return empty map when no images are configured", func() { + emptyConfig := &files.RootConfig{} + dockerfiles := emptyConfig.ExtractWorkspaceDockerfiles() + + Expect(dockerfiles).To(BeEmpty()) + }) + + It("should extract all Dockerfile paths mapped to their BOM references", func() { + dockerfiles := rootConfig.ExtractWorkspaceDockerfiles() + + Expect(dockerfiles).NotTo(BeEmpty()) + Expect(dockerfiles).To(HaveKeyWithValue("dockerfile-24.04", "workspace-agent-24.04")) + Expect(dockerfiles).To(HaveKeyWithValue("dockerfile-24.04-minimal", "workspace-agent-24.04-minimal")) + Expect(dockerfiles).To(HaveKeyWithValue("dockerfile-20.04", "workspace-agent-20.04")) + + // Should have 3 dockerfile mappings (ide-service has no dockerfile) + Expect(len(dockerfiles)).To(Equal(3)) + }) + }) +}) diff --git a/internal/installer/files/files_suite_test.go b/internal/installer/files/files_suite_test.go new file mode 100644 index 00000000..b854bdd9 --- /dev/null +++ b/internal/installer/files/files_suite_test.go @@ -0,0 +1,16 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package files_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestFiles(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Files Suite") +} diff --git a/internal/installer/files/oci_image_index_test.go b/internal/installer/files/oci_image_index_test.go new file mode 100644 index 00000000..20bf513e --- /dev/null +++ b/internal/installer/files/oci_image_index_test.go @@ -0,0 +1,217 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package files_test + +import ( + "encoding/json" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/codesphere-cloud/oms/internal/installer/files" +) + +var _ = Describe("OciImageIndex", func() { + var ( + ociIndex *files.OCIImageIndex + tempDir string + indexFile string + sampleIndex map[string]interface{} + ) + + BeforeEach(func() { + ociIndex = &files.OCIImageIndex{} + + var err error + tempDir, err = os.MkdirTemp("", "oci_index_test") + Expect(err).NotTo(HaveOccurred()) + + indexFile = filepath.Join(tempDir, "index.json") + + // Create sample OCI Image Index data matching the real structure + sampleIndex = map[string]interface{}{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": []interface{}{ + map[string]interface{}{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "size": int64(527), + "annotations": map[string]interface{}{ + "io.containerd.image.name": "ghcr.io/codesphere-cloud/codesphere-monorepo/workspace-agent-24.04:codesphere-v1.66.0", + }, + }, + map[string]interface{}{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "size": int64(842), + "annotations": map[string]interface{}{ + "io.containerd.image.name": "ghcr.io/codesphere-cloud/codesphere-monorepo/workspace-agent-20.04:codesphere-v1.66.0", + }, + }, + map[string]interface{}{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", + "size": int64(1024), + "annotations": map[string]interface{}{ + "io.containerd.image.name": "registry.example.com/auth-service:v1.0.0", + }, + }, + map[string]interface{}{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "size": int64(256), + "annotations": map[string]interface{}{ + "org.opencontainers.image.ref.name": "nginx:latest", + "custom.annotation": "some-value", + }, + }, + }, + } + }) + + AfterEach(func() { + os.RemoveAll(tempDir) + }) + + Describe("ParseOCIImageConfig", func() { + It("should parse a valid OCI Image Index file successfully", func() { + // Write sample index to file + indexData, err := json.Marshal(sampleIndex) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(indexFile, indexData, 0644) + Expect(err).NotTo(HaveOccurred()) + + // The function expects a file path and looks for index.json in the same directory + err = ociIndex.ParseOCIImageConfig(indexFile) + Expect(err).NotTo(HaveOccurred()) + + Expect(ociIndex.SchemaVersion).To(Equal(2)) + Expect(ociIndex.MediaType).To(Equal("application/vnd.oci.image.index.v1+json")) + Expect(ociIndex.Manifests).To(HaveLen(4)) + + // Check first manifest entry + firstManifest := ociIndex.Manifests[0] + Expect(firstManifest.MediaType).To(Equal("application/vnd.oci.image.manifest.v1+json")) + Expect(firstManifest.Digest).To(Equal("sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")) + Expect(firstManifest.Size).To(Equal(int64(527))) + Expect(firstManifest.Annotations).To(HaveKeyWithValue("io.containerd.image.name", "ghcr.io/codesphere-cloud/codesphere-monorepo/workspace-agent-24.04:codesphere-v1.66.0")) + }) + + It("should return error for non-existent index.json file", func() { + nonExistentFile := filepath.Join(tempDir, "nonexistent.json") + err := ociIndex.ParseOCIImageConfig(nonExistentFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to open file")) + Expect(err.Error()).To(ContainSubstring("index.json")) + }) + + It("should return error for invalid JSON in index.json", func() { + err := os.WriteFile(indexFile, []byte("invalid json content"), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = ociIndex.ParseOCIImageConfig(indexFile) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to decode file")) + }) + + It("should handle empty index.json file", func() { + err := os.WriteFile(indexFile, []byte("{}"), 0644) + Expect(err).NotTo(HaveOccurred()) + + err = ociIndex.ParseOCIImageConfig(indexFile) + Expect(err).NotTo(HaveOccurred()) + + Expect(ociIndex.SchemaVersion).To(Equal(0)) + Expect(ociIndex.MediaType).To(BeEmpty()) + Expect(ociIndex.Manifests).To(BeEmpty()) + }) + + It("should handle index.json in subdirectory when given file path in subdirectory", func() { + subDir := filepath.Join(tempDir, "subdir") + err := os.MkdirAll(subDir, 0755) + Expect(err).NotTo(HaveOccurred()) + + subIndexFile := filepath.Join(subDir, "index.json") + someOtherFile := filepath.Join(subDir, "other.json") + + indexData, err := json.Marshal(sampleIndex) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(subIndexFile, indexData, 0644) + Expect(err).NotTo(HaveOccurred()) + + // Pass the "other.json" file path - function should still find index.json in same directory + err = ociIndex.ParseOCIImageConfig(someOtherFile) + Expect(err).NotTo(HaveOccurred()) + + Expect(ociIndex.SchemaVersion).To(Equal(2)) + Expect(ociIndex.Manifests).To(HaveLen(4)) + }) + }) + + Describe("ExtractImageNames", func() { + BeforeEach(func() { + indexData, err := json.Marshal(sampleIndex) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(indexFile, indexData, 0644) + Expect(err).NotTo(HaveOccurred()) + + err = ociIndex.ParseOCIImageConfig(indexFile) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should return empty slice when no manifests exist", func() { + emptyIndex := &files.OCIImageIndex{} + names, err := emptyIndex.ExtractImageNames() + Expect(err).NotTo(HaveOccurred()) + Expect(names).To(BeEmpty()) + }) + + It("should handle manifests without annotations", func() { + // Create a fresh ociIndex for this test to avoid state from BeforeEach + freshIndex := &files.OCIImageIndex{} + + indexWithoutAnnotations := map[string]interface{}{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": []interface{}{ + map[string]interface{}{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "size": int64(527), + }, + }, + } + + indexData, err := json.Marshal(indexWithoutAnnotations) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(indexFile, indexData, 0644) + Expect(err).NotTo(HaveOccurred()) + + err = freshIndex.ParseOCIImageConfig(indexFile) + Expect(err).NotTo(HaveOccurred()) + + names, err := freshIndex.ExtractImageNames() + Expect(err).NotTo(HaveOccurred()) + Expect(names).To(BeEmpty()) + }) + + It("should extract all image names from manifests with containerd annotations", func() { + names, err := ociIndex.ExtractImageNames() + Expect(err).NotTo(HaveOccurred()) + Expect(names).NotTo(BeEmpty()) + + Expect(names).To(ContainElement("ghcr.io/codesphere-cloud/codesphere-monorepo/workspace-agent-24.04:codesphere-v1.66.0")) + Expect(names).To(ContainElement("ghcr.io/codesphere-cloud/codesphere-monorepo/workspace-agent-20.04:codesphere-v1.66.0")) + Expect(names).To(ContainElement("registry.example.com/auth-service:v1.0.0")) + Expect(len(names)).To(Equal(3)) + }) + }) +}) diff --git a/internal/installer/mocks.go b/internal/installer/mocks.go index 9e43554a..9779117e 100644 --- a/internal/installer/mocks.go +++ b/internal/installer/mocks.go @@ -309,6 +309,115 @@ func (_c *MockPackageManager_FileIO_Call) RunAndReturn(run func() util.FileIO) * return _c } +// GetBaseimageName provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) GetBaseimageName(baseimage string) (string, error) { + ret := _mock.Called(baseimage) + + if len(ret) == 0 { + panic("no return value specified for GetBaseimageName") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func(string) (string, error)); ok { + return returnFunc(baseimage) + } + if returnFunc, ok := ret.Get(0).(func(string) string); ok { + r0 = returnFunc(baseimage) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(string) error); ok { + r1 = returnFunc(baseimage) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockPackageManager_GetBaseimageName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBaseimageName' +type MockPackageManager_GetBaseimageName_Call struct { + *mock.Call +} + +// GetBaseimageName is a helper method to define mock.On call +// - baseimage +func (_e *MockPackageManager_Expecter) GetBaseimageName(baseimage interface{}) *MockPackageManager_GetBaseimageName_Call { + return &MockPackageManager_GetBaseimageName_Call{Call: _e.mock.On("GetBaseimageName", baseimage)} +} + +func (_c *MockPackageManager_GetBaseimageName_Call) Run(run func(baseimage string)) *MockPackageManager_GetBaseimageName_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockPackageManager_GetBaseimageName_Call) Return(s string, err error) *MockPackageManager_GetBaseimageName_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *MockPackageManager_GetBaseimageName_Call) RunAndReturn(run func(baseimage string) (string, error)) *MockPackageManager_GetBaseimageName_Call { + _c.Call.Return(run) + return _c +} + +// GetBaseimagePath provides a mock function for the type MockPackageManager +func (_mock *MockPackageManager) GetBaseimagePath(baseimage string, force bool) (string, error) { + ret := _mock.Called(baseimage, force) + + if len(ret) == 0 { + panic("no return value specified for GetBaseimagePath") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func(string, bool) (string, error)); ok { + return returnFunc(baseimage, force) + } + if returnFunc, ok := ret.Get(0).(func(string, bool) string); ok { + r0 = returnFunc(baseimage, force) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(string, bool) error); ok { + r1 = returnFunc(baseimage, force) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockPackageManager_GetBaseimagePath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBaseimagePath' +type MockPackageManager_GetBaseimagePath_Call struct { + *mock.Call +} + +// GetBaseimagePath is a helper method to define mock.On call +// - baseimage +// - force +func (_e *MockPackageManager_Expecter) GetBaseimagePath(baseimage interface{}, force interface{}) *MockPackageManager_GetBaseimagePath_Call { + return &MockPackageManager_GetBaseimagePath_Call{Call: _e.mock.On("GetBaseimagePath", baseimage, force)} +} + +func (_c *MockPackageManager_GetBaseimagePath_Call) Run(run func(baseimage string, force bool)) *MockPackageManager_GetBaseimagePath_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *MockPackageManager_GetBaseimagePath_Call) Return(s string, err error) *MockPackageManager_GetBaseimagePath_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *MockPackageManager_GetBaseimagePath_Call) RunAndReturn(run func(baseimage string, force bool) (string, error)) *MockPackageManager_GetBaseimagePath_Call { + _c.Call.Return(run) + return _c +} + // GetCodesphereVersion provides a mock function for the type MockPackageManager func (_mock *MockPackageManager) GetCodesphereVersion() (string, error) { ret := _mock.Called() @@ -407,67 +516,6 @@ func (_c *MockPackageManager_GetDependencyPath_Call) RunAndReturn(run func(filen return _c } -// GetImagePathAndName provides a mock function for the type MockPackageManager -func (_mock *MockPackageManager) GetImagePathAndName(baseimage string, force bool) (string, string, error) { - ret := _mock.Called(baseimage, force) - - if len(ret) == 0 { - panic("no return value specified for GetImagePathAndName") - } - - var r0 string - var r1 string - var r2 error - if returnFunc, ok := ret.Get(0).(func(string, bool) (string, string, error)); ok { - return returnFunc(baseimage, force) - } - if returnFunc, ok := ret.Get(0).(func(string, bool) string); ok { - r0 = returnFunc(baseimage, force) - } else { - r0 = ret.Get(0).(string) - } - if returnFunc, ok := ret.Get(1).(func(string, bool) string); ok { - r1 = returnFunc(baseimage, force) - } else { - r1 = ret.Get(1).(string) - } - if returnFunc, ok := ret.Get(2).(func(string, bool) error); ok { - r2 = returnFunc(baseimage, force) - } else { - r2 = ret.Error(2) - } - return r0, r1, r2 -} - -// MockPackageManager_GetImagePathAndName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetImagePathAndName' -type MockPackageManager_GetImagePathAndName_Call struct { - *mock.Call -} - -// GetImagePathAndName is a helper method to define mock.On call -// - baseimage -// - force -func (_e *MockPackageManager_Expecter) GetImagePathAndName(baseimage interface{}, force interface{}) *MockPackageManager_GetImagePathAndName_Call { - return &MockPackageManager_GetImagePathAndName_Call{Call: _e.mock.On("GetImagePathAndName", baseimage, force)} -} - -func (_c *MockPackageManager_GetImagePathAndName_Call) Run(run func(baseimage string, force bool)) *MockPackageManager_GetImagePathAndName_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(bool)) - }) - return _c -} - -func (_c *MockPackageManager_GetImagePathAndName_Call) Return(s string, s1 string, err error) *MockPackageManager_GetImagePathAndName_Call { - _c.Call.Return(s, s1, err) - return _c -} - -func (_c *MockPackageManager_GetImagePathAndName_Call) RunAndReturn(run func(baseimage string, force bool) (string, string, error)) *MockPackageManager_GetImagePathAndName_Call { - _c.Call.Return(run) - return _c -} - // GetWorkDir provides a mock function for the type MockPackageManager func (_mock *MockPackageManager) GetWorkDir() string { ret := _mock.Called() diff --git a/internal/installer/package.go b/internal/installer/package.go index aaa67e52..ed963da8 100644 --- a/internal/installer/package.go +++ b/internal/installer/package.go @@ -24,7 +24,8 @@ type PackageManager interface { Extract(force bool) error ExtractDependency(file string, force bool) error ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, error) - GetImagePathAndName(baseimage string, force bool) (string, string, error) + GetBaseimageName(baseimage string) (string, error) + GetBaseimagePath(baseimage string, force bool) (string, error) GetCodesphereVersion() (string, error) } @@ -134,39 +135,54 @@ func (p *Package) ExtractOciImageIndex(imagefile string) (files.OCIImageIndex, e return ociImageIndex, nil } -const baseimagePath = "./codesphere/images" - -func (p *Package) GetImagePathAndName(baseimage string, force bool) (string, string, error) { +func (p *Package) GetBaseimageName(baseimage string) (string, error) { if baseimage == "" { - return "", "", fmt.Errorf("baseimage not specified") + return "", fmt.Errorf("baseimage not specified") } - baseImageTarPath := path.Join(baseimagePath, baseimage) - err := p.ExtractDependency(baseImageTarPath, force) + bomJson := files.BomConfig{} + err := bomJson.ParseBomConfig(p.GetDependencyPath("bom.json")) if err != nil { - return "", "", fmt.Errorf("failed to extract package to workdir: %w", err) + return "", fmt.Errorf("failed to load bom.json: %w", err) } - baseimagePath := p.GetDependencyPath(baseImageTarPath) - index, err := p.ExtractOciImageIndex(baseimagePath) + containerImages, err := bomJson.GetCodesphereContainerImages() if err != nil { - return "", "", fmt.Errorf("failed to extract OCI image index: %w", err) + return "", fmt.Errorf("failed to get codesphere container images from bom.json: %w", err) } - imagenames, err := index.ExtractImageNames() - if err != nil || len(imagenames) == 0 { - return "", "", fmt.Errorf("failed to read image tags: %w", err) + imageName, exists := containerImages[baseimage] + if !exists { + return "", fmt.Errorf("baseimage %s not found in bom.json", baseimage) } - log.Printf("Extracted image names: %s", strings.Join(imagenames, ", ")) + return imageName, nil +} + +const baseimagePath = "./codesphere/images" - baseimageName := imagenames[0] +func (p *Package) GetBaseimagePath(baseimage string, force bool) (string, error) { + if baseimage == "" { + return "", fmt.Errorf("baseimage not specified") + } + + if !strings.HasSuffix(baseimage, ".tar") { + baseimage = baseimage + ".tar" + } + + baseImageTarPath := path.Join(baseimagePath, baseimage) + err := p.ExtractDependency(baseImageTarPath, force) + if err != nil { + return "", fmt.Errorf("failed to extract package to workdir: %w", err) + } + + baseimagePath := p.GetDependencyPath(baseImageTarPath) - return baseimagePath, baseimageName, nil + return baseimagePath, nil } func (p *Package) GetCodesphereVersion() (string, error) { - _, imageName, err := p.GetImagePathAndName("", false) + imageName, err := p.GetBaseimageName("workspace-agent-24.04") if err != nil { return "", fmt.Errorf("failed to get Codesphere version from package: %w", err) } From 460732f5d929356d015515aaf2fb8ce798857166 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Mon, 3 Nov 2025 11:48:29 +0100 Subject: [PATCH 18/22] update: remove required from description of non required flag --- cli/cmd/update_dockerfile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cmd/update_dockerfile.go b/cli/cmd/update_dockerfile.go index 469c5772..f098a1ea 100644 --- a/cli/cmd/update_dockerfile.go +++ b/cli/cmd/update_dockerfile.go @@ -66,7 +66,7 @@ in the specified Dockerfile to use that base image. The base image is loaded int dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Dockerfile, "dockerfile", "d", "", "Path to the Dockerfile to update (required)") dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Package, "package", "p", "", "Path to the Codesphere package (required)") - dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Baseimage, "baseimage", "b", "workspace-agent-24.04", "Name of the base image to use (required)") + dockerfileCmd.cmd.Flags().StringVarP(&dockerfileCmd.Opts.Baseimage, "baseimage", "b", "workspace-agent-24.04", "Name of the base image to use") dockerfileCmd.cmd.Flags().BoolVarP(&dockerfileCmd.Opts.Force, "force", "f", false, "Force re-extraction of the package") util.MarkFlagRequired(dockerfileCmd.cmd, "dockerfile") From 30329b1cae97105540f5cf39e80c2608d5f9342e Mon Sep 17 00:00:00 2001 From: siherrmann <25087590+siherrmann@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:55:22 +0000 Subject: [PATCH 19/22] chore(docs): Auto-update docs and licenses Signed-off-by: siherrmann <25087590+siherrmann@users.noreply.github.com> --- docs/README.md | 3 ++- docs/oms-cli.md | 3 ++- docs/oms-cli_beta.md | 2 +- docs/oms-cli_beta_extend.md | 2 +- docs/oms-cli_beta_extend_baseimage.md | 3 ++- docs/oms-cli_build.md | 2 +- docs/oms-cli_build_image.md | 2 +- docs/oms-cli_build_images.md | 2 +- docs/oms-cli_download.md | 2 +- docs/oms-cli_download_package.md | 2 +- docs/oms-cli_install.md | 2 +- docs/oms-cli_install_codesphere.md | 2 +- docs/oms-cli_licenses.md | 2 +- docs/oms-cli_list.md | 2 +- docs/oms-cli_list_api-keys.md | 2 +- docs/oms-cli_list_packages.md | 2 +- docs/oms-cli_register.md | 2 +- docs/oms-cli_revoke.md | 2 +- docs/oms-cli_revoke_api-key.md | 2 +- docs/oms-cli_update.md | 3 ++- docs/oms-cli_update_api-key.md | 2 +- docs/oms-cli_update_dockerfile.md | 8 ++++---- docs/oms-cli_update_oms.md | 2 +- docs/oms-cli_update_package.md | 2 +- docs/oms-cli_version.md | 2 +- internal/tmpl/NOTICE | 26 ++++++++++---------------- 26 files changed, 42 insertions(+), 44 deletions(-) diff --git a/docs/README.md b/docs/README.md index b0693163..c1b30490 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ like downloading new versions. ### SEE ALSO * [oms-cli beta](oms-cli_beta.md) - Commands for early testing +* [oms-cli build](oms-cli_build.md) - Build and push images to a registry * [oms-cli download](oms-cli_download.md) - Download resources available through OMS * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components * [oms-cli licenses](oms-cli_licenses.md) - Print license information @@ -27,4 +28,4 @@ like downloading new versions. * [oms-cli update](oms-cli_update.md) - Update OMS related resources * [oms-cli version](oms-cli_version.md) - Print version -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli.md b/docs/oms-cli.md index b0693163..c1b30490 100644 --- a/docs/oms-cli.md +++ b/docs/oms-cli.md @@ -18,6 +18,7 @@ like downloading new versions. ### SEE ALSO * [oms-cli beta](oms-cli_beta.md) - Commands for early testing +* [oms-cli build](oms-cli_build.md) - Build and push images to a registry * [oms-cli download](oms-cli_download.md) - Download resources available through OMS * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components * [oms-cli licenses](oms-cli_licenses.md) - Print license information @@ -27,4 +28,4 @@ like downloading new versions. * [oms-cli update](oms-cli_update.md) - Update OMS related resources * [oms-cli version](oms-cli_version.md) - Print version -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_beta.md b/docs/oms-cli_beta.md index d158f643..1cbccfc1 100644 --- a/docs/oms-cli_beta.md +++ b/docs/oms-cli_beta.md @@ -18,4 +18,4 @@ Be aware that that usage and behavior may change as the features are developed. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli beta extend](oms-cli_beta_extend.md) - Extend Codesphere ressources such as base images. -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_beta_extend.md b/docs/oms-cli_beta_extend.md index e92fb18b..567ebef3 100644 --- a/docs/oms-cli_beta_extend.md +++ b/docs/oms-cli_beta_extend.md @@ -17,4 +17,4 @@ Extend Codesphere ressources such as base images to customize them for your need * [oms-cli beta](oms-cli_beta.md) - Commands for early testing * [oms-cli beta extend baseimage](oms-cli_beta_extend_baseimage.md) - Extend Codesphere's workspace base image for customization -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_beta_extend_baseimage.md b/docs/oms-cli_beta_extend_baseimage.md index 8ba53035..2b2569e0 100644 --- a/docs/oms-cli_beta_extend_baseimage.md +++ b/docs/oms-cli_beta_extend_baseimage.md @@ -17,6 +17,7 @@ oms-cli beta extend baseimage [flags] ### Options ``` + -b, --baseimage string Base image file name inside the package to extend (default: 'workspace-agent-24.04') (default "workspace-agent-24.04") -d, --dockerfile string Output Dockerfile to generate for extending the base image (default "Dockerfile") -f, --force Enforce package extraction -h, --help help for baseimage @@ -27,4 +28,4 @@ oms-cli beta extend baseimage [flags] * [oms-cli beta extend](oms-cli_beta_extend.md) - Extend Codesphere ressources such as base images. -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_build.md b/docs/oms-cli_build.md index 99b1f5a4..8f200629 100644 --- a/docs/oms-cli_build.md +++ b/docs/oms-cli_build.md @@ -18,4 +18,4 @@ Build and push container images to a registry using the provided configuration. * [oms-cli build image](oms-cli_build_image.md) - Build and push Docker image using Dockerfile and Codesphere package version * [oms-cli build images](oms-cli_build_images.md) - Build and push container images -###### Auto generated by spf13/cobra on 31-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_build_image.md b/docs/oms-cli_build_image.md index 07c193e3..0964fae6 100644 --- a/docs/oms-cli_build_image.md +++ b/docs/oms-cli_build_image.md @@ -31,4 +31,4 @@ $ oms-cli build image --dockerfile baseimage/Dockerfile --package codesphere-v1. * [oms-cli build](oms-cli_build.md) - Build and push images to a registry -###### Auto generated by spf13/cobra on 31-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_build_images.md b/docs/oms-cli_build_images.md index 6e161284..db98c6de 100644 --- a/docs/oms-cli_build_images.md +++ b/docs/oms-cli_build_images.md @@ -22,4 +22,4 @@ oms-cli build images [flags] * [oms-cli build](oms-cli_build.md) - Build and push images to a registry -###### Auto generated by spf13/cobra on 31-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_download.md b/docs/oms-cli_download.md index e2655f03..c34ab2e9 100644 --- a/docs/oms-cli_download.md +++ b/docs/oms-cli_download.md @@ -18,4 +18,4 @@ e.g. available Codesphere packages * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli download package](oms-cli_download_package.md) - Download a codesphere package -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_download_package.md b/docs/oms-cli_download_package.md index 2c24b4fe..b9e323bf 100644 --- a/docs/oms-cli_download_package.md +++ b/docs/oms-cli_download_package.md @@ -36,4 +36,4 @@ $ oms-cli download package --version codesphere-v1.55.0 --file installer-lite.ta * [oms-cli download](oms-cli_download.md) - Download resources available through OMS -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_install.md b/docs/oms-cli_install.md index a8e5baea..c3530a5b 100644 --- a/docs/oms-cli_install.md +++ b/docs/oms-cli_install.md @@ -17,4 +17,4 @@ Coming soon: Install Codesphere and other components like Ceph and PostgreSQL. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli install codesphere](oms-cli_install_codesphere.md) - Install a Codesphere instance -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_install_codesphere.md b/docs/oms-cli_install_codesphere.md index 77840aad..716f2c53 100644 --- a/docs/oms-cli_install_codesphere.md +++ b/docs/oms-cli_install_codesphere.md @@ -26,4 +26,4 @@ oms-cli install codesphere [flags] * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_licenses.md b/docs/oms-cli_licenses.md index b154ade1..23d01f97 100644 --- a/docs/oms-cli_licenses.md +++ b/docs/oms-cli_licenses.md @@ -20,4 +20,4 @@ oms-cli licenses [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_list.md b/docs/oms-cli_list.md index bb373a01..3bb421d5 100644 --- a/docs/oms-cli_list.md +++ b/docs/oms-cli_list.md @@ -19,4 +19,4 @@ eg. available Codesphere packages * [oms-cli list api-keys](oms-cli_list_api-keys.md) - List API keys * [oms-cli list packages](oms-cli_list_packages.md) - List available packages -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_list_api-keys.md b/docs/oms-cli_list_api-keys.md index 39ec59cf..02274e0b 100644 --- a/docs/oms-cli_list_api-keys.md +++ b/docs/oms-cli_list_api-keys.md @@ -20,4 +20,4 @@ oms-cli list api-keys [flags] * [oms-cli list](oms-cli_list.md) - List resources available through OMS -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_list_packages.md b/docs/oms-cli_list_packages.md index 8aa6b561..9cb4e521 100644 --- a/docs/oms-cli_list_packages.md +++ b/docs/oms-cli_list_packages.md @@ -20,4 +20,4 @@ oms-cli list packages [flags] * [oms-cli list](oms-cli_list.md) - List resources available through OMS -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_register.md b/docs/oms-cli_register.md index d9952934..7e7b93ce 100644 --- a/docs/oms-cli_register.md +++ b/docs/oms-cli_register.md @@ -24,4 +24,4 @@ oms-cli register [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_revoke.md b/docs/oms-cli_revoke.md index 56ce2757..cac7846d 100644 --- a/docs/oms-cli_revoke.md +++ b/docs/oms-cli_revoke.md @@ -18,4 +18,4 @@ eg. api keys. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli revoke api-key](oms-cli_revoke_api-key.md) - Revoke an API key -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_revoke_api-key.md b/docs/oms-cli_revoke_api-key.md index 49a2c28e..a2746f85 100644 --- a/docs/oms-cli_revoke_api-key.md +++ b/docs/oms-cli_revoke_api-key.md @@ -21,4 +21,4 @@ oms-cli revoke api-key [flags] * [oms-cli revoke](oms-cli_revoke.md) - Revoke resources available through OMS -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_update.md b/docs/oms-cli_update.md index 661cc761..f8b93b90 100644 --- a/docs/oms-cli_update.md +++ b/docs/oms-cli_update.md @@ -20,7 +20,8 @@ oms-cli update [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli update api-key](oms-cli_update_api-key.md) - Update an API key's expiration date +* [oms-cli update dockerfile](oms-cli_update_dockerfile.md) - Update FROM statement in Dockerfile with base image from package * [oms-cli update oms](oms-cli_update_oms.md) - Update the OMS CLI * [oms-cli update package](oms-cli_update_package.md) - Download a codesphere package -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_update_api-key.md b/docs/oms-cli_update_api-key.md index 3ffecbd7..8d811f06 100644 --- a/docs/oms-cli_update_api-key.md +++ b/docs/oms-cli_update_api-key.md @@ -22,4 +22,4 @@ oms-cli update api-key [flags] * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_update_dockerfile.md b/docs/oms-cli_update_dockerfile.md index 1df05923..57658229 100644 --- a/docs/oms-cli_update_dockerfile.md +++ b/docs/oms-cli_update_dockerfile.md @@ -16,10 +16,10 @@ oms-cli update dockerfile [flags] ### Examples ``` -# Update Dockerfile to use the default base image from the package (workspace-agent-24.04.tar) +# Update Dockerfile to use the default base image from the package (workspace-agent-24.04) $ oms-cli update dockerfile --dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz -# Update Dockerfile to use the workspace-agent-20.04.tar base image from the package +# Update Dockerfile to use the workspace-agent-20.04 base image from the package $ oms-cli update dockerfile --dockerfile baseimage/Dockerfile --package codesphere-v1.68.0.tar.gz --baseimage workspace-agent-20.04.tar ``` @@ -27,7 +27,7 @@ $ oms-cli update dockerfile --dockerfile baseimage/Dockerfile --package codesphe ### Options ``` - -b, --baseimage string Name of the base image to use (required) (default "workspace-agent-24.04.tar") + -b, --baseimage string Name of the base image to use (default "workspace-agent-24.04") -d, --dockerfile string Path to the Dockerfile to update (required) -f, --force Force re-extraction of the package -h, --help help for dockerfile @@ -38,4 +38,4 @@ $ oms-cli update dockerfile --dockerfile baseimage/Dockerfile --package codesphe * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 31-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_update_oms.md b/docs/oms-cli_update_oms.md index 75faa045..9d36592f 100644 --- a/docs/oms-cli_update_oms.md +++ b/docs/oms-cli_update_oms.md @@ -20,4 +20,4 @@ oms-cli update oms [flags] * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_update_package.md b/docs/oms-cli_update_package.md index 92a8a41c..9d2d28b0 100644 --- a/docs/oms-cli_update_package.md +++ b/docs/oms-cli_update_package.md @@ -36,4 +36,4 @@ $ oms-cli download package --version codesphere-v1.55.0 --file installer-lite.ta * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/docs/oms-cli_version.md b/docs/oms-cli_version.md index 24d6c59a..bea79035 100644 --- a/docs/oms-cli_version.md +++ b/docs/oms-cli_version.md @@ -20,4 +20,4 @@ oms-cli version [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 30-Oct-2025 +###### Auto generated by spf13/cobra on 3-Nov-2025 diff --git a/internal/tmpl/NOTICE b/internal/tmpl/NOTICE index 7881e7f0..be205461 100644 --- a/internal/tmpl/NOTICE +++ b/internal/tmpl/NOTICE @@ -9,17 +9,11 @@ Version: v3.5.1 License: MIT License URL: https://github.com/blang/semver/blob/v3.5.1/LICENSE ----------- -Module: github.com/clipperhouse/stringish -Version: v0.1.1 -License: MIT -License URL: https://github.com/clipperhouse/stringish/blob/v0.1.1/LICENSE - ---------- Module: github.com/clipperhouse/uax29/v2 -Version: v2.3.0 +Version: v2.2.0 License: MIT -License URL: https://github.com/clipperhouse/uax29/blob/v2.3.0/LICENSE +License URL: https://github.com/clipperhouse/uax29/blob/v2.2.0/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go/pkg/io @@ -119,9 +113,9 @@ License URL: https://github.com/spf13/pflag/blob/v1.0.10/LICENSE ---------- Module: github.com/stretchr/objx -Version: v0.5.3 +Version: v0.5.2 License: MIT -License URL: https://github.com/stretchr/objx/blob/v0.5.3/LICENSE +License URL: https://github.com/stretchr/objx/blob/v0.5.2/LICENSE ---------- Module: github.com/stretchr/testify @@ -143,21 +137,21 @@ License URL: https://github.com/ulikunitz/xz/blob/v0.5.15/LICENSE ---------- Module: golang.org/x/crypto -Version: v0.43.0 +Version: v0.42.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/crypto/+/v0.43.0:LICENSE +License URL: https://cs.opensource.google/go/x/crypto/+/v0.42.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.32.0 +Version: v0.30.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.32.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.30.0:LICENSE ---------- Module: golang.org/x/text -Version: v0.30.0 +Version: v0.29.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/text/+/v0.30.0:LICENSE +License URL: https://cs.opensource.google/go/x/text/+/v0.29.0:LICENSE ---------- Module: gopkg.in/yaml.v3 From 425e547d3a7e545f7ce093fd4cc68486b5702dea Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Mon, 3 Nov 2025 11:58:31 +0100 Subject: [PATCH 20/22] fix: fix lint issues unhandled errors --- cli/cmd/build_images_test.go | 4 ++-- cli/cmd/extend_baseimage_test.go | 8 +++---- cli/cmd/install_codesphere_test.go | 2 +- cli/cmd/update_dockerfile_test.go | 8 +++---- internal/installer/files/bom_json_test.go | 2 +- internal/installer/files/config_yaml_test.go | 2 +- .../installer/files/oci_image_index_test.go | 2 +- internal/installer/package_test.go | 22 +++++++++---------- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/cli/cmd/build_images_test.go b/cli/cmd/build_images_test.go index bb9485b3..285762ed 100644 --- a/cli/cmd/build_images_test.go +++ b/cli/cmd/build_images_test.go @@ -79,11 +79,11 @@ var _ = Describe("BuildImagesCmd", func() { tempConfigFile, err := os.CreateTemp("", "test-config.yaml") Expect(err).To(BeNil()) - defer os.Remove(tempConfigFile.Name()) + defer func() { _ = os.Remove(tempConfigFile.Name()) }() _, err = tempConfigFile.WriteString(validConfigYaml) Expect(err).To(BeNil()) - tempConfigFile.Close() + _ = tempConfigFile.Close() c.Opts.Config = tempConfigFile.Name() diff --git a/cli/cmd/extend_baseimage_test.go b/cli/cmd/extend_baseimage_test.go index 787d9de9..6b5dd8ed 100644 --- a/cli/cmd/extend_baseimage_test.go +++ b/cli/cmd/extend_baseimage_test.go @@ -103,8 +103,8 @@ var _ = Describe("ExtendBaseimageCmd", func() { // Create a temporary file for the Dockerfile generation to work with tempFile, err := os.CreateTemp("", "dockerfile-test-*") Expect(err).To(BeNil()) - defer os.Remove(tempFile.Name()) - defer tempFile.Close() + defer func() { _ = os.Remove(tempFile.Name()) }() + defer func() { _ = tempFile.Close() }() 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) @@ -137,8 +137,8 @@ var _ = Describe("ExtendBaseimageCmd", func() { // Create a temporary file for the Dockerfile generation to work with tempFile, err := os.CreateTemp("", "dockerfile-test-*") Expect(err).To(BeNil()) - defer os.Remove(tempFile.Name()) - defer tempFile.Close() + defer func() { _ = os.Remove(tempFile.Name()) }() + defer func() { _ = tempFile.Close() }() 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) diff --git a/cli/cmd/install_codesphere_test.go b/cli/cmd/install_codesphere_test.go index 57ac48ed..2c66718f 100644 --- a/cli/cmd/install_codesphere_test.go +++ b/cli/cmd/install_codesphere_test.go @@ -56,7 +56,7 @@ var _ = Describe("InstallCodesphereCmd", func() { _, err = tempConfigFile.WriteString("codesphere:\n deployConfig:\n images: {}\n") Expect(err).To(BeNil()) - tempConfigFile.Close() + _ = tempConfigFile.Close() c.Opts.Config = tempConfigFile.Name() mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") diff --git a/cli/cmd/update_dockerfile_test.go b/cli/cmd/update_dockerfile_test.go index 00cc666d..83d01558 100644 --- a/cli/cmd/update_dockerfile_test.go +++ b/cli/cmd/update_dockerfile_test.go @@ -146,7 +146,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { _, err = tempFile.WriteString(sampleDockerfileContent) Expect(err).To(BeNil()) // Reset file position to beginning - tempFile.Seek(0, 0) + _, _ = tempFile.Seek(0, 0) c.Opts.Dockerfile = "Dockerfile" c.Opts.Baseimage = "" @@ -179,7 +179,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { _, err = tempFile.WriteString(sampleDockerfileContent) Expect(err).To(BeNil()) // Reset file position to beginning - tempFile.Seek(0, 0) + _, _ = tempFile.Seek(0, 0) c.Opts.Dockerfile = "Dockerfile" c.Opts.Baseimage = "" @@ -211,7 +211,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { _, err = tempFile.WriteString(sampleDockerfileContent) Expect(err).To(BeNil()) // Reset file position to beginning - tempFile.Seek(0, 0) + _, _ = tempFile.Seek(0, 0) c.Opts.Dockerfile = "Dockerfile" c.Opts.Baseimage = "workspace-agent-20.04.tar" @@ -243,7 +243,7 @@ var _ = Describe("UpdateDockerfileCmd", func() { _, err = tempFile.WriteString(sampleDockerfileContent) Expect(err).To(BeNil()) // Reset file position to beginning - tempFile.Seek(0, 0) + _, _ = tempFile.Seek(0, 0) c.Opts.Dockerfile = "custom/Dockerfile" c.Opts.Baseimage = "workspace-agent-24.04.tar" diff --git a/internal/installer/files/bom_json_test.go b/internal/installer/files/bom_json_test.go index 20724f46..ea2361f1 100644 --- a/internal/installer/files/bom_json_test.go +++ b/internal/installer/files/bom_json_test.go @@ -83,7 +83,7 @@ var _ = Describe("BomJson", func() { }) AfterEach(func() { - os.RemoveAll(tempDir) + _ = os.RemoveAll(tempDir) }) Describe("ParseBomConfig", func() { diff --git a/internal/installer/files/config_yaml_test.go b/internal/installer/files/config_yaml_test.go index 397d2a20..4c235a85 100644 --- a/internal/installer/files/config_yaml_test.go +++ b/internal/installer/files/config_yaml_test.go @@ -76,7 +76,7 @@ codesphere: }) AfterEach(func() { - os.RemoveAll(tempDir) + _ = os.RemoveAll(tempDir) }) Describe("ParseConfig", func() { diff --git a/internal/installer/files/oci_image_index_test.go b/internal/installer/files/oci_image_index_test.go index 20bf513e..a46ede12 100644 --- a/internal/installer/files/oci_image_index_test.go +++ b/internal/installer/files/oci_image_index_test.go @@ -74,7 +74,7 @@ var _ = Describe("OciImageIndex", func() { }) AfterEach(func() { - os.RemoveAll(tempDir) + _ = os.RemoveAll(tempDir) }) Describe("ParseOCIImageConfig", func() { diff --git a/internal/installer/package_test.go b/internal/installer/package_test.go index 405ac4ee..2d65b8fe 100644 --- a/internal/installer/package_test.go +++ b/internal/installer/package_test.go @@ -363,13 +363,13 @@ func createTestTarGzPackage(filename string) error { if err != nil { return err } - defer file.Close() + defer util.CloseFileIgnoreError(file) gzw := gzip.NewWriter(file) - defer gzw.Close() + defer func() { _ = gzw.Close() }() tw := tar.NewWriter(gzw) - defer tw.Close() + defer func() { _ = tw.Close() }() // Add a test file content := "test content" @@ -394,13 +394,13 @@ func createTestTarGzPackageWithDeps(filename string) error { if err != nil { return err } - defer file.Close() + defer util.CloseFileIgnoreError(file) gzw := gzip.NewWriter(file) - defer gzw.Close() + defer func() { _ = gzw.Close() }() tw := tar.NewWriter(gzw) - defer tw.Close() + defer func() { _ = tw.Close() }() // Add main content mainContent := "main package content" @@ -444,13 +444,13 @@ func createComplexTestPackage(filename string) error { if err != nil { return err } - defer file.Close() + defer util.CloseFileIgnoreError(file) gzw := gzip.NewWriter(file) - defer gzw.Close() + defer func() { _ = gzw.Close() }() tw := tar.NewWriter(gzw) - defer tw.Close() + defer func() { _ = tw.Close() }() // Add main content mainContent := "complex main package content" @@ -702,10 +702,10 @@ func createTar(tarName string, fileName string, fileContent string) error { if err != nil { return err } - defer file.Close() + defer util.CloseFileIgnoreError(file) tw := tar.NewWriter(file) - defer tw.Close() + defer func() { _ = tw.Close() }() // Add the specified file header := &tar.Header{ From fb45e37136d76bab6a39126e1ad9825345c015ec Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Mon, 3 Nov 2025 12:00:52 +0100 Subject: [PATCH 21/22] fix: fix lint error unhandled error --- cli/cmd/install_codesphere_test.go | 2 +- cli/cmd/update_dockerfile_test.go | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cli/cmd/install_codesphere_test.go b/cli/cmd/install_codesphere_test.go index 2c66718f..18ff6f29 100644 --- a/cli/cmd/install_codesphere_test.go +++ b/cli/cmd/install_codesphere_test.go @@ -52,7 +52,7 @@ var _ = Describe("InstallCodesphereCmd", func() { tempConfigFile, err := os.CreateTemp("", "test-config.yaml") Expect(err).To(BeNil()) - defer os.Remove(tempConfigFile.Name()) + defer func() { _ = os.Remove(tempConfigFile.Name()) }() _, err = tempConfigFile.WriteString("codesphere:\n deployConfig:\n images: {}\n") Expect(err).To(BeNil()) diff --git a/cli/cmd/update_dockerfile_test.go b/cli/cmd/update_dockerfile_test.go index 83d01558..62c6250c 100644 --- a/cli/cmd/update_dockerfile_test.go +++ b/cli/cmd/update_dockerfile_test.go @@ -140,8 +140,8 @@ var _ = Describe("UpdateDockerfileCmd", func() { tempFile, err := os.CreateTemp("", "dockerfile-test-*") Expect(err).To(BeNil()) DeferCleanup(func() { - tempFile.Close() - os.Remove(tempFile.Name()) + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) }) _, err = tempFile.WriteString(sampleDockerfileContent) Expect(err).To(BeNil()) @@ -173,8 +173,8 @@ var _ = Describe("UpdateDockerfileCmd", func() { tempFile, err := os.CreateTemp("", "dockerfile-test-*") Expect(err).To(BeNil()) DeferCleanup(func() { - tempFile.Close() - os.Remove(tempFile.Name()) + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) }) _, err = tempFile.WriteString(sampleDockerfileContent) Expect(err).To(BeNil()) @@ -205,8 +205,8 @@ var _ = Describe("UpdateDockerfileCmd", func() { tempFile, err := os.CreateTemp("", "dockerfile-test-*") Expect(err).To(BeNil()) DeferCleanup(func() { - tempFile.Close() - os.Remove(tempFile.Name()) + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) }) _, err = tempFile.WriteString(sampleDockerfileContent) Expect(err).To(BeNil()) @@ -237,8 +237,8 @@ var _ = Describe("UpdateDockerfileCmd", func() { tempFile, err := os.CreateTemp("", "dockerfile-test-*") Expect(err).To(BeNil()) DeferCleanup(func() { - tempFile.Close() - os.Remove(tempFile.Name()) + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) }) _, err = tempFile.WriteString(sampleDockerfileContent) Expect(err).To(BeNil()) From e0e38379f122612c269b5539a8a85209b7543b38 Mon Sep 17 00:00:00 2001 From: Simon Herrmann Date: Mon, 3 Nov 2025 13:20:56 +0100 Subject: [PATCH 22/22] update: small PR updates --- internal/installer/package.go | 26 ++++++++++++++++++++++---- internal/util/docker.go | 12 +++++------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/internal/installer/package.go b/internal/installer/package.go index ed963da8..bd671214 100644 --- a/internal/installer/package.go +++ b/internal/installer/package.go @@ -182,14 +182,32 @@ func (p *Package) GetBaseimagePath(baseimage string, force bool) (string, error) } func (p *Package) GetCodesphereVersion() (string, error) { - imageName, err := p.GetBaseimageName("workspace-agent-24.04") + bomJson := files.BomConfig{} + err := bomJson.ParseBomConfig(p.GetDependencyPath("bom.json")) + if err != nil { + return "", fmt.Errorf("failed to load bom.json: %w", err) + } + + containerImages, err := bomJson.GetCodesphereContainerImages() if err != nil { - return "", fmt.Errorf("failed to get Codesphere version from package: %w", err) + return "", fmt.Errorf("failed to get codesphere container images from bom.json: %w", err) + } + + containerImage := "" + for _, image := range containerImages { + if strings.Contains(image, "codesphere-v") { + containerImage = image + break + } + } + + if containerImage == "" { + return "", fmt.Errorf("no container images found in bom.json") } - parts := strings.Split(imageName, ":") + parts := strings.Split(containerImage, ":") if len(parts) < 2 { - return "", fmt.Errorf("invalid image name format: %s", imageName) + return "", fmt.Errorf("invalid image name format: %s", containerImage) } return parts[len(parts)-1], nil diff --git a/internal/util/docker.go b/internal/util/docker.go index db38aa98..f489f6dd 100644 --- a/internal/util/docker.go +++ b/internal/util/docker.go @@ -32,20 +32,18 @@ func (dm *Dockerfile) UpdateFromStatement(dockerfile io.Reader, baseImage string // Regex to match FROM statements that contain workspace-agent fromRegex := regexp.MustCompile(`(?i)(.*FROM\s+).*workspace-agent[^\s]*(.*)`) + updated := false lines := strings.Split(string(content), "\n") - lastMatchIndex := -1 - for i, line := range lines { if fromRegex.MatchString(line) { - lastMatchIndex = i + lines[i] = fromRegex.ReplaceAllString(line, "${1}"+baseImage+"${2}") + updated = true } } - if lastMatchIndex == -1 { + + if !updated { return "", fmt.Errorf("no FROM statement with workspace-agent found in dockerfile") } - newLine := fromRegex.ReplaceAllString(lines[lastMatchIndex], "${1}"+baseImage+"${2}") - lines[lastMatchIndex] = strings.TrimRight(newLine, " \t") - return strings.Join(lines, "\n"), nil }