Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cli/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -22,4 +22,5 @@ func AddInstallCmd(rootCmd *cobra.Command) {
},
}
rootCmd.AddCommand(install.cmd)
AddInstallCodesphereCmd(install.cmd, opts)
}
106 changes: 104 additions & 2 deletions cli/cmd/install_codesphere.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,124 @@
package cmd

import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"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/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
}

func AddInstallCodesphereCmd(install *cobra.Command) {
func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, args []string) error {
if c.Opts.Package == "" {
return errors.New("required option package not set")
}

workdir := c.Env.GetOmsWorkdir()
p := installer.NewPackage(workdir, c.Opts.Package)

err := c.ExtractAndInstall(p, args)
if err != nil {
return fmt.Errorf("failed to extract and install package: %w", err)
}

return nil
}

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`),
},
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 base image from")
codesphere.cmd.Flags().BoolVarP(&codesphere.Opts.Force, "force", "f", false, "Enforce package extraction")
install.AddCommand(codesphere.cmd)
codesphere.cmd.RunE = codesphere.RunE
}

func (c *InstallCodesphereCmd) ExtractAndInstall(p *installer.Package, args []string) error {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
return fmt.Errorf("codesphere installation is only supported on Linux amd64. Current platform: %s/%s", runtime.GOOS, runtime.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, "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")
}

nodeDir := "./" + p.GetWorkDir() + "/node"
err = os.Chmod(nodeDir, 0755)
if err != nil {
return fmt.Errorf("failed to make node executable: %w", err)
}

log.Printf("Using Node.js executable: %s", nodeDir)
log.Println("Starting private cloud installer script...")
installerScript := "./" + p.GetWorkDir() + "/private-cloud-installer.js"
out, err := exec.Command(nodeDir, append([]string{installerScript}, args...)...).Output()
if err != nil {
return fmt.Errorf("failed to run installer script: %w", err)
}
fmt.Println(string(out))
fmt.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
}
224 changes: 224 additions & 0 deletions cli/cmd/install_codesphere_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd_test

import (
"fmt"
"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("fails when package is empty", func() {
c.Opts.Package = ""
err := c.RunE(nil, []string{})
Expect(err).To(MatchError("required option package not set"))
})

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{})

// On non-Linux platforms, should fail with platform error
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("codesphere installation is only supported on Linux amd64"))
}
})
})

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{},
}

err := c.ExtractAndInstall(pkg, []string{})

// Should always fail on non-Linux amd64 platforms
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("codesphere installation is only supported on Linux amd64"))
Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("Current platform: %s/%s", runtime.GOOS, runtime.GOARCH)))
}
})

Context("when on Linux amd64 (mocked)", func() {
BeforeEach(func() {
// Skip these tests if not on Linux amd64 since we can't easily mock runtime.GOOS
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
Skip("Skipping Linux-specific tests on non-Linux platform")
}
})

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, []string{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to extract package to workdir"))
})
})
})

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("Coming soon: Install a Codesphere instance"))
Expect(codesphereCmd.Long).To(ContainSubstring("Coming soon: Install a Codesphere instance"))
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"))
})
})

// 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 }
6 changes: 5 additions & 1 deletion cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

// OMS API key management commands
AddRegisterCmd(rootCmd, opts)
Expand Down
Loading