diff --git a/Makefile b/Makefile index 23663575..986ba051 100644 --- a/Makefile +++ b/Makefile @@ -55,3 +55,10 @@ docs: generate-license: generate go-licenses report --template .NOTICE.template ./... > NOTICE copywrite headers apply + +run-lima: + limactl start ./hack/lima-oms.yaml + +stop-lima: + limactl stop lima-oms + limactl delete lima-oms diff --git a/cli/cmd/install.go b/cli/cmd/install.go index fed8af0d..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) { +func AddInstallCmd(rootCmd *cobra.Command, opts *GlobalOptions) { install := InstallCmd{ cmd: &cobra.Command{ Use: "install", @@ -22,4 +22,5 @@ func AddInstallCmd(rootCmd *cobra.Command) { }, } rootCmd.AddCommand(install.cmd) + AddInstallCodesphereCmd(install.cmd, opts) } diff --git a/cli/cmd/install_codesphere.go b/cli/cmd/install_codesphere.go index c983c1b4..59b65794 100644 --- a/cli/cmd/install_codesphere.go +++ b/cli/cmd/install_codesphere.go @@ -4,22 +4,150 @@ package cmd import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "slices" + "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/util" "github.com/spf13/cobra" ) // InstallCodesphereCmd represents the codesphere command type InstallCodesphereCmd struct { - cmd *cobra.Command + cmd *cobra.Command + Opts *InstallCodesphereOpts + Env env.Env +} + +type InstallCodesphereOpts struct { + *GlobalOptions + Package string + Force bool + Config string + PrivKey string + SkipSteps []string +} + +func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, args []string) error { + workdir := c.Env.GetOmsWorkdir() + p := installer.NewPackage(workdir, c.Opts.Package) + + err := c.ExtractAndInstall(p, runtime.GOOS, runtime.GOARCH) + if err != nil { + return fmt.Errorf("failed to extract and install package: %w", err) + } + + return nil } -func AddInstallCodesphereCmd(install *cobra.Command) { +func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) { codesphere := InstallCodesphereCmd{ cmd: &cobra.Command{ Use: "codesphere", - Short: "Coming soon: Install a Codesphere instance", - Long: io.Long(`Coming soon: Install a Codesphere instance`), + Short: "Install a Codesphere instance", + Long: io.Long(`Install a Codesphere instance with the provided package, configuration file, and private key. + Uses the private-cloud-installer.js script included in the package to perform the installation.`), }, + Opts: &InstallCodesphereOpts{GlobalOptions: opts}, + Env: env.NewEnv(), } + codesphere.cmd.Flags().StringVarP(&codesphere.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load binaries, installer etc. from") + codesphere.cmd.Flags().BoolVarP(&codesphere.Opts.Force, "force", "f", false, "Enforce package extraction") + codesphere.cmd.Flags().StringVarP(&codesphere.Opts.Config, "config", "c", "", "Path to the Codesphere Private Cloud configuration file (yaml)") + codesphere.cmd.Flags().StringVarP(&codesphere.Opts.PrivKey, "priv-key", "k", "", "Path to the private key to encrypt/decrypt secrets") + codesphere.cmd.Flags().StringSliceVarP(&codesphere.Opts.SkipSteps, "skip-steps", "s", []string{}, "Steps to be skipped. Must be one of: copy-dependencies, extract-dependencies, load-container-images, ceph, kubernetes") + + util.MarkFlagRequired(codesphere.cmd, "package") + util.MarkFlagRequired(codesphere.cmd, "config") + util.MarkFlagRequired(codesphere.cmd, "priv-key") + install.AddCommand(codesphere.cmd) + codesphere.cmd.RunE = codesphere.RunE +} + +func (c *InstallCodesphereCmd) ExtractAndInstall(p *installer.Package, 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) + if err != nil { + return fmt.Errorf("failed to extract package to workdir: %w", err) + } + + foundFiles, err := c.ListPackageContents(p) + if err != nil { + return fmt.Errorf("failed to list available files: %w", err) + } + + if !slices.Contains(foundFiles, "deps.tar.gz") { + return fmt.Errorf("deps.tar.gz not found in package") + } + if !slices.Contains(foundFiles, "private-cloud-installer.js") { + return fmt.Errorf("private-cloud-installer.js not found in package") + } + if !slices.Contains(foundFiles, "node") { + return fmt.Errorf("node executable not found in package") + } + + nodePath := filepath.Join(".", p.GetWorkDir(), "node") + err = os.Chmod(nodePath, 0755) + if err != nil { + return fmt.Errorf("failed to make node executable: %w", err) + } + + 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") + + // 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 { + cmdArgs = append(cmdArgs, "--skipStep", step) + } + } + + cmd := exec.Command(nodePath, cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + err = cmd.Run() + if err != nil { + return fmt.Errorf("failed to run installer script: %w", err) + } + log.Println("Private cloud installer script finished.") + + return nil +} + +func (c *InstallCodesphereCmd) ListPackageContents(p *installer.Package) ([]string, error) { + packageDir := p.GetWorkDir() + if !p.FileIO.Exists(packageDir) { + return nil, fmt.Errorf("work dir not found: %s", packageDir) + } + + entries, err := p.FileIO.ReadDir(packageDir) + if err != nil { + return nil, fmt.Errorf("failed to read directory contents: %w", err) + } + + log.Printf("Listing contents of %s", packageDir) + var foundFiles []string + for _, entry := range entries { + filename := entry.Name() + log.Printf("- %s", filename) + foundFiles = append(foundFiles, filename) + } + + return foundFiles, nil } diff --git a/cli/cmd/install_codesphere_test.go b/cli/cmd/install_codesphere_test.go new file mode 100644 index 00000000..bcb04db7 --- /dev/null +++ b/cli/cmd/install_codesphere_test.go @@ -0,0 +1,392 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd_test + +import ( + "os" + "runtime" + + . "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/util" +) + +var _ = Describe("InstallCodesphereCmd", func() { + var ( + c cmd.InstallCodesphereCmd + opts *cmd.InstallCodesphereOpts + globalOpts cmd.GlobalOptions + mockEnv *env.MockEnv + ) + + BeforeEach(func() { + mockEnv = env.NewMockEnv(GinkgoT()) + globalOpts = cmd.GlobalOptions{} + opts = &cmd.InstallCodesphereOpts{ + GlobalOptions: &globalOpts, + Package: "codesphere-v1.66.0-installer.tar.gz", + Force: false, + } + c = cmd.InstallCodesphereCmd{ + Opts: opts, + Env: mockEnv, + } + }) + + AfterEach(func() { + mockEnv.AssertExpectations(GinkgoT()) + }) + + Context("RunE method", func() { + It("calls GetOmsWorkdir and fails on non-linux platform", func() { + c.Opts.Package = "test-package.tar.gz" + mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir") + + err := c.RunE(nil, []string{}) + + Expect(err).To(HaveOccurred()) + if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" { + // Should fail with platform error on non-Linux platform + 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")) + } + }) + }) + + 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{}, + } + + // Test with Windows platform + err := c.ExtractAndInstall(pkg, "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") + 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 package extraction fails", func() { + pkg := &installer.Package{ + OmsWorkdir: "/test/workdir", + Filename: "non-existent-package.tar.gz", + FileIO: &util.FilesystemWriter{}, + } + + err := c.ExtractAndInstall(pkg, "linux", "amd64") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to extract package to workdir")) + }) + + 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{}, + } + + err = c.ExtractAndInstall(pkg, "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{}, + } + + err = c.ExtractAndInstall(pkg, "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{}, + } + + err = c.ExtractAndInstall(pkg, "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"), + } + err = createTestTarGz(testPackageFile, packageFiles) + Expect(err).To(BeNil()) + + c.Opts.Force = true + pkg := &installer.Package{ + OmsWorkdir: tempDir, + Filename: testPackageFile, + FileIO: &util.FilesystemWriter{}, + } + + err = c.ExtractAndInstall(pkg, "linux", "amd64") + 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"), + )) + }) + }) + }) + + Context("listPackageContents method", func() { + It("fails when work directory doesn't exist", func() { + 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) + + filenames, err := c.ListPackageContents(pkg) + 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() { + 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) + + filenames, err := c.ListPackageContents(pkg) + 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() { + mockFileIO := util.NewMockFileIO(GinkgoT()) + pkg := &installer.Package{ + OmsWorkdir: "/test/workdir", + Filename: "test-package.tar.gz", + FileIO: mockFileIO, + } + + // Create mock directory entries + 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().Exists("/test/workdir/test-package").Return(true) + mockFileIO.EXPECT().ReadDir("/test/workdir/test-package").Return(mockEntries, nil) + + filenames, err := c.ListPackageContents(pkg) + 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()) + }) + }) +}) + +var _ = Describe("AddInstallCodesphereCmd", func() { + var ( + parentCmd *cobra.Command + globalOpts *cmd.GlobalOptions + ) + + BeforeEach(func() { + parentCmd = &cobra.Command{Use: "install"} + globalOpts = &cmd.GlobalOptions{} + }) + + It("adds the codesphere command with correct properties and flags", func() { + cmd.AddInstallCodesphereCmd(parentCmd, globalOpts) + + var codesphereCmd *cobra.Command + for _, c := range parentCmd.Commands() { + if c.Use == "codesphere" { + codesphereCmd = c + break + } + } + + Expect(codesphereCmd).NotTo(BeNil()) + Expect(codesphereCmd.Use).To(Equal("codesphere")) + Expect(codesphereCmd.Short).To(Equal("Install a Codesphere instance")) + Expect(codesphereCmd.Long).To(ContainSubstring("Uses the private-cloud-installer.js script included in the package to perform the installation.")) + Expect(codesphereCmd.RunE).NotTo(BeNil()) + + // Check flags + packageFlag := codesphereCmd.Flags().Lookup("package") + Expect(packageFlag).NotTo(BeNil()) + Expect(packageFlag.Shorthand).To(Equal("p")) + + forceFlag := codesphereCmd.Flags().Lookup("force") + Expect(forceFlag).NotTo(BeNil()) + Expect(forceFlag.Shorthand).To(Equal("f")) + Expect(forceFlag.DefValue).To(Equal("false")) + + configFlag := codesphereCmd.Flags().Lookup("config") + Expect(configFlag).NotTo(BeNil()) + Expect(configFlag.Shorthand).To(Equal("c")) + + privKeyFlag := codesphereCmd.Flags().Lookup("priv-key") + Expect(privKeyFlag).NotTo(BeNil()) + Expect(privKeyFlag.Shorthand).To(Equal("k")) + + skipStepFlag := codesphereCmd.Flags().Lookup("skip-steps") + Expect(skipStepFlag).NotTo(BeNil()) + Expect(skipStepFlag.Shorthand).To(Equal("s")) + }) +}) + +// MockDirEntry implements os.DirEntry for testing +type MockDirEntry struct { + name string + isDir bool +} + +func (m *MockDirEntry) Name() string { return m.name } +func (m *MockDirEntry) IsDir() bool { return m.isDir } +func (m *MockDirEntry) Type() os.FileMode { return 0 } +func (m *MockDirEntry) Info() (os.FileInfo, error) { return nil, nil } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index c499d284..2264f7d6 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -26,11 +26,15 @@ func GetRootCmd() *cobra.Command { This command can be used to run common tasks related to managing codesphere installations, like downloading new versions.`), } + // General commands AddVersionCmd(rootCmd) + AddBetaCmd(rootCmd, &opts) AddUpdateCmd(rootCmd, opts) + + // Package commands AddListCmd(rootCmd, opts) AddDownloadCmd(rootCmd, opts) - AddBetaCmd(rootCmd, &opts) + AddInstallCmd(rootCmd, &opts) AddLicensesCmd(rootCmd) // OMS API key management commands diff --git a/hack/lima-oms.yaml b/hack/lima-oms.yaml new file mode 100644 index 00000000..5ccff727 --- /dev/null +++ b/hack/lima-oms.yaml @@ -0,0 +1,85 @@ +# Clean Lima configuration for OMS development +vmType: "qemu" +os: "Linux" +arch: "x86_64" + +# Ubuntu 24.04 LTS +images: +- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img" + arch: "x86_64" + +# Resources +cpus: 4 +memory: "8GiB" +disk: "60GiB" + +# Mount your OMS project +mounts: +- location: "." + mountPoint: "/home/user/oms" + writable: true + +# Ports and SSH +ssh: + localPort: 60022 + loadDotSSHPubKeys: true +portForwards: +- guestPort: 8080 + hostPort: 8080 + +# containerd +containerd: + system: false + user: false + +# Install Docker and Go +provision: +- mode: system + script: | + #!/bin/bash + set -eux -o pipefail + + # Update packages + export DEBIAN_FRONTEND=noninteractive + apt-get update + + # Install Docker + curl -fsSL https://get.docker.com | sh + systemctl disable --now docker + apt-get install -y uidmap dbus-user-session + +- mode: user + script: | + #!/bin/bash + set -eux -o pipefail + + # 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 + go mod download + cd cli && go build -a -buildvcs=false && mv cli ../oms-cli + +message: | + Your OMS development environment is ready! + + To access it: + ------ + limactl shell lima-oms + cd /home/user/oms + ./oms-cli --help + ------ + + To install Codesphere eg.: + ------ + ./oms-cli install codesphere --package codesphere-v1.66.0-installer --config config.yaml --priv-key ./path-to-private-key + ------ + + Go 1.24 and Docker are installed and ready to use. diff --git a/internal/installer/package.go b/internal/installer/package.go index 4426077b..553e3dac 100644 --- a/internal/installer/package.go +++ b/internal/installer/package.go @@ -29,8 +29,10 @@ func NewPackage(omsWorkdir, filename string) *Package { } } +// 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 { - workDir := p.getWorkDir() + workDir := p.GetWorkDir() err := os.MkdirAll(p.OmsWorkdir, 0755) if err != nil { return fmt.Errorf("failed to ensure workdir exists: %w", err) @@ -53,12 +55,13 @@ func (p *Package) Extract(force bool) error { return nil } +// ExtractDependency extracts a specific dependency file from the deps.tar.gz archive within the package. func (p *Package) ExtractDependency(file string, force bool) error { err := p.Extract(force) if err != nil { return fmt.Errorf("failed to extract package: %w", err) } - workDir := p.getWorkDir() + workDir := p.GetWorkDir() if p.FileIO.Exists(p.GetDependencyPath(file)) && !force { log.Println("skipping extraction, dependency already unpacked. Use force option to overwrite.") @@ -84,11 +87,14 @@ func (p *Package) alreadyExtracted(dir string) (bool, error) { return isDir, nil } -func (p *Package) getWorkDir() string { +// 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() + workDir := p.GetWorkDir() return path.Join(workDir, depsDir, filename) } diff --git a/internal/util/filewriter.go b/internal/util/filewriter.go index 3f58a695..b686039d 100644 --- a/internal/util/filewriter.go +++ b/internal/util/filewriter.go @@ -14,6 +14,7 @@ type FileIO interface { Create(filename string) (*os.File, error) IsDirectory(filename string) (bool, error) Exists(filename string) bool + ReadDir(dirname string) ([]os.DirEntry, error) } type FilesystemWriter struct{} @@ -55,6 +56,10 @@ func (fs *FilesystemWriter) OpenFile(name string, flag int, perm os.FileMode) (* return os.OpenFile(name, flag, perm) } +func (fs *FilesystemWriter) ReadDir(dirname string) ([]os.DirEntry, error) { + return os.ReadDir(dirname) +} + type ClosableFile interface { Close() error } diff --git a/internal/util/mocks.go b/internal/util/mocks.go index 23487339..ae55b5ba 100644 --- a/internal/util/mocks.go +++ b/internal/util/mocks.go @@ -352,6 +352,62 @@ func (_c *MockFileIO_OpenFile_Call) RunAndReturn(run func(name string, flag int, return _c } +// ReadDir provides a mock function for the type MockFileIO +func (_mock *MockFileIO) ReadDir(dirname string) ([]os.DirEntry, error) { + ret := _mock.Called(dirname) + + if len(ret) == 0 { + panic("no return value specified for ReadDir") + } + + var r0 []os.DirEntry + var r1 error + if returnFunc, ok := ret.Get(0).(func(string) ([]os.DirEntry, error)); ok { + return returnFunc(dirname) + } + if returnFunc, ok := ret.Get(0).(func(string) []os.DirEntry); ok { + r0 = returnFunc(dirname) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]os.DirEntry) + } + } + if returnFunc, ok := ret.Get(1).(func(string) error); ok { + r1 = returnFunc(dirname) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockFileIO_ReadDir_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadDir' +type MockFileIO_ReadDir_Call struct { + *mock.Call +} + +// ReadDir is a helper method to define mock.On call +// - dirname +func (_e *MockFileIO_Expecter) ReadDir(dirname interface{}) *MockFileIO_ReadDir_Call { + return &MockFileIO_ReadDir_Call{Call: _e.mock.On("ReadDir", dirname)} +} + +func (_c *MockFileIO_ReadDir_Call) Run(run func(dirname string)) *MockFileIO_ReadDir_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockFileIO_ReadDir_Call) Return(vs []os.DirEntry, err error) *MockFileIO_ReadDir_Call { + _c.Call.Return(vs, err) + return _c +} + +func (_c *MockFileIO_ReadDir_Call) RunAndReturn(run func(dirname string) ([]os.DirEntry, error)) *MockFileIO_ReadDir_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 {