diff --git a/go.mod b/go.mod index 7f4bddf..714d698 100644 --- a/go.mod +++ b/go.mod @@ -92,10 +92,10 @@ require ( go.opentelemetry.io/otel/metric v0.33.0 // indirect go.opentelemetry.io/otel/trace v1.11.1 // indirect golang.org/x/crypto v0.13.0 // indirect + golang.org/x/mod v0.13.0 golang.org/x/net v0.15.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/text v0.13.0 // indirect - golang.org/x/tools v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect diff --git a/go.sum b/go.sum index 931fea0..8baa7b2 100644 --- a/go.sum +++ b/go.sum @@ -403,8 +403,8 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= diff --git a/pkg/cmd/local/deploy.go b/pkg/cmd/local/deploy.go index 378931a..595a760 100644 --- a/pkg/cmd/local/deploy.go +++ b/pkg/cmd/local/deploy.go @@ -3,18 +3,16 @@ package local import ( "fmt" "os" + "path" "path/filepath" "strings" "time" "github.com/MakeNowJust/heredoc" "github.com/cli/safeexec" - "github.com/mgutz/ansi" "github.com/spf13/cobra" - "github.com/instill-ai/cli/internal/build" "github.com/instill-ai/cli/internal/config" - "github.com/instill-ai/cli/pkg/cmd/factory" "github.com/instill-ai/cli/pkg/cmd/instance" "github.com/instill-ai/cli/pkg/cmdutil" "github.com/instill-ai/cli/pkg/iostreams" @@ -35,9 +33,10 @@ type DeployOptions struct { Config config.Config MainExecutable string Interactive bool - Path string + Force bool + Upgrade bool checkForUpdate func(ExecDep, string, string, string) (*releaseInfo, error) - isDeployed func(ExecDep) error + isDeployed func(ExecDep, string) error } // NewDeployCmd creates a new command @@ -45,7 +44,7 @@ func NewDeployCmd(f *cmdutil.Factory, runF func(*DeployOptions) error) *cobra.Co opts := &DeployOptions{ IO: f.IOStreams, checkForUpdate: checkForUpdate, - isDeployed: isDeployed, + isDeployed: isProjectDeployed, } cmd := &cobra.Command{ @@ -73,11 +72,10 @@ func NewDeployCmd(f *cmdutil.Factory, runF func(*DeployOptions) error) *cobra.Co return runDeploy(opts) }, } - d, err := os.UserHomeDir() - if err != nil { - logger.Error("Couldn't get Home directory", err) - } - opts.Path = filepath.Join(d, ".local", "instill") + string(os.PathSeparator) + + cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Force to deploy a new local Instill Core instance") + cmd.Flags().BoolVarP(&opts.Upgrade, "upgrade", "u", false, "Upgrade Instill Core instance to the latest version") + cmd.MarkFlagsMutuallyExclusive("force", "upgrade") return cmd } @@ -85,7 +83,6 @@ func NewDeployCmd(f *cmdutil.Factory, runF func(*DeployOptions) error) *cobra.Co func runDeploy(opts *DeployOptions) error { var err error - path := opts.Path start := time.Now() // check the deps @@ -101,75 +98,152 @@ func runDeploy(opts *DeployOptions) error { } } - // check existing local deployment - if err := opts.isDeployed(opts.Exec); err == nil { - p(opts.IO, "A local Instill Core deployment detected") - for i := range projs { - prj := strings.ToLower(projs[i]) - if opts.OS != nil { - err = opts.OS.Chdir(filepath.Join(path, prj)) + // download the latest version of the projects, if local repos are not present + for _, proj := range projs { + projDirPath := filepath.Join(LocalInstancePath, proj) + _, err = os.Stat(projDirPath) + if os.IsNotExist(err) { + if latestVersion, err := execCmd(opts.Exec, "bash", "-c", fmt.Sprintf("curl https://api.github.com/repos/instill-ai/%s/releases | jq -r 'map(select(.prerelease)) | first | .tag_name'", proj)); err == nil { + latestVersion = strings.Trim(latestVersion, "\n") + if out, err := execCmd(opts.Exec, "bash", "-c", + fmt.Sprintf("git clone --depth 1 -b %s -c advice.detachedHead=false https://github.com/instill-ai/%s.git %s", latestVersion, proj, projDirPath)); err != nil { + return fmt.Errorf("ERROR: cannot clone %s, %w:\n%s", proj, err, out) + } + if _, err := opts.checkForUpdate(opts.Exec, filepath.Join(config.StateDir(), fmt.Sprintf("%s.yml", proj)), fmt.Sprintf("instill-ai/%s", proj), latestVersion); err != nil { + return fmt.Errorf("ERROR: cannot check for update %s, %w:\n%s", proj, err, latestVersion) + } } else { - err = os.Chdir(filepath.Join(path, prj)) - } - if err != nil { - return fmt.Errorf("ERROR: can't open the destination, %w", err) + return fmt.Errorf("ERROR: cannot find latest release version of %s, %w:\n%s", proj, err, latestVersion) } + } + } - if currentVersion, err := execCmd(opts.Exec, "bash", "-c", "git name-rev --tags --name-only $(git rev-parse HEAD)"); err == nil { - newRelease, _ := opts.checkForUpdate(opts.Exec, filepath.Join(config.StateDir(), fmt.Sprintf("%s.yml", prj)), fmt.Sprintf("instill-ai/%s", prj), currentVersion) - if newRelease != nil { - cmdFactory := factory.New(build.Version) - stderr := cmdFactory.IOStreams.ErrOut - fmt.Fprintf(stderr, "\n\n%s %s → %s\n", - ansi.Color(fmt.Sprintf("A new release of Instill %s is available:", prj), "yellow"), - ansi.Color(currentVersion, "cyan"), - ansi.Color(newRelease.Version, "cyan")) - fmt.Fprintf(stderr, "%s\n\n", - ansi.Color(newRelease.URL, "yellow")) + if opts.Force { + p(opts.IO, "Tear down Instill Core instance if existing...") + for _, proj := range projs { + projDirPath := filepath.Join(LocalInstancePath, proj) + _, err = os.Stat(projDirPath) + if !os.IsNotExist(err) { + if opts.OS != nil { + if err = opts.OS.Chdir(projDirPath); err != nil { + return err + } + } else { + if err = os.Chdir(projDirPath); err != nil { + return err + } + } + if out, err := execCmd(opts.Exec, "bash", "-c", "make down"); err != nil { + return fmt.Errorf("ERROR: cannot force tearing down %s, %w:\n%s", proj, err, out) } } } - return nil - } - - p(opts.IO, "Download the latest Instill Core to: %s", path) - for i := range projs { - prj := strings.ToLower(projs[i]) - _, err = os.Stat(filepath.Join(path, prj)) - if os.IsNotExist(err) { - if latestVersion, err := execCmd(opts.Exec, "bash", "-c", fmt.Sprintf("curl https://api.github.com/repos/instill-ai/%s/releases | jq -r 'map(select(.prerelease)) | first | .tag_name'", prj)); err == nil { - latestVersion = strings.Trim(latestVersion, "\n") - if out, err := execCmd(opts.Exec, "bash", "-c", - fmt.Sprintf("git clone --depth 1 -b %s -c advice.detachedHead=false https://github.com/instill-ai/%s.git %s", latestVersion, prj, filepath.Join(path, prj))); err != nil { - return fmt.Errorf("ERROR: cant clone %s, %w:\n%s", prj, err, out) + } else if opts.Upgrade { + hasNewVersion := false + for _, proj := range projs { + projDirPath := filepath.Join(LocalInstancePath, proj) + _, err = os.Stat(projDirPath) + if !os.IsNotExist(err) { + if opts.OS != nil { + if err = opts.OS.Chdir(projDirPath); err != nil { + return err + } + } else { + if err = os.Chdir(projDirPath); err != nil { + return err + } + } + if currentVersion, err := execCmd(opts.Exec, "bash", "-c", "git name-rev --tags --name-only $(git rev-parse HEAD)"); err == nil { + currentVersion = strings.Trim(currentVersion, "\n") + if newRelease, err := opts.checkForUpdate(opts.Exec, filepath.Join(config.StateDir(), fmt.Sprintf("%s.yml", proj)), fmt.Sprintf("instill-ai/%s", proj), currentVersion); err != nil { + return fmt.Errorf("ERROR: cannot check for update %s, %w:\n%s", proj, err, currentVersion) + } else if newRelease != nil { + p(opts.IO, "Upgrade %s to %s...", proj, newRelease.Version) + hasNewVersion = true + } + } else { + return fmt.Errorf("ERROR: cannot find current release version of %s, %w:\n%s", proj, err, currentVersion) } - _, _ = checkForUpdate(opts.Exec, filepath.Join(config.StateDir(), fmt.Sprintf("%s.yml", prj)), fmt.Sprintf("instill-ai/%s", prj), latestVersion) - } else { - return fmt.Errorf("ERROR: cant find latest release version of %s, %w:\n%s", prj, err, latestVersion) } } - } - p(opts.IO, "Launch Instill Core") - for i := range projs { - prj := strings.ToLower(projs[i]) - if opts.OS != nil { - err = opts.OS.Chdir(filepath.Join(path, prj)) + if hasNewVersion { + p(opts.IO, "Tear down Instill Core instance if existing...") + for _, proj := range projs { + projDirPath := filepath.Join(LocalInstancePath, proj) + _, err = os.Stat(projDirPath) + if !os.IsNotExist(err) { + if opts.OS != nil { + if err = opts.OS.Chdir(projDirPath); err != nil { + return err + } + } else { + if err = os.Chdir(projDirPath); err != nil { + return err + } + } + if out, err := execCmd(opts.Exec, "bash", "-c", "make down"); err != nil { + return fmt.Errorf("ERROR: cannot force tearing down %s, %w:\n%s", proj, err, out) + } + } + + if dir, err := os.ReadDir(projDirPath); err == nil { + for _, d := range dir { + if os.RemoveAll(path.Join([]string{projDirPath, d.Name()}...)); err != nil { + return fmt.Errorf("ERROR: cannot remove %s, %w", projDirPath, err) + } + } + } else { + return fmt.Errorf("ERROR: cannot read %s, %w", projDirPath, err) + } + + if latestVersion, err := execCmd(opts.Exec, "bash", "-c", fmt.Sprintf("curl https://api.github.com/repos/instill-ai/%s/releases | jq -r 'map(select(.prerelease)) | first | .tag_name'", proj)); err == nil { + latestVersion = strings.Trim(latestVersion, "\n") + if out, err := execCmd(opts.Exec, "bash", "-c", + fmt.Sprintf("git clone --depth 1 -b %s -c advice.detachedHead=false https://github.com/instill-ai/%s.git %s", latestVersion, proj, projDirPath)); err != nil { + return fmt.Errorf("ERROR: cannot clone %s, %w:\n%s", proj, err, out) + } + } else { + return fmt.Errorf("ERROR: cannot find latest release version of %s, %w:\n%s", proj, err, latestVersion) + } + } } else { - err = os.Chdir(filepath.Join(path, prj)) + p(opts.IO, "No upgrade available") + return nil } - if err != nil { - return fmt.Errorf("ERROR: can't open the destination, %w", err) + + } else { + for _, proj := range projs { + if err := opts.isDeployed(opts.Exec, proj); err == nil { + p(opts.IO, "A local Instill Core deployment detected") + return nil + } + } + } + + p(opts.IO, "Launch Instill Core...") + for _, proj := range projs { + projDirPath := filepath.Join(LocalInstancePath, proj) + _, err = os.Stat(projDirPath) + if !os.IsNotExist(err) { + if opts.OS != nil { + err = opts.OS.Chdir(projDirPath) + } else { + err = os.Chdir(projDirPath) + } + if err != nil { + return fmt.Errorf("ERROR: cannot open the directory: %w", err) + } } if currentVersion, err := execCmd(opts.Exec, "bash", "-c", "git name-rev --tags --name-only $(git rev-parse HEAD)"); err == nil { currentVersion = strings.Trim(currentVersion, "\n") - p(opts.IO, "Spin up Instill %s %s...", projs[i], currentVersion) + p(opts.IO, "Spin up %s %s...", proj, currentVersion) if out, err := execCmd(opts.Exec, "bash", "-c", "make all"); err != nil { - return fmt.Errorf("ERROR: %s spin-up failed, %w\n%s", prj, err, out) + return fmt.Errorf("ERROR: %s spin-up failed, %w\n%s", proj, err, out) } } else { - return fmt.Errorf("ERROR: cant get current tag %s, %w:\n%s", prj, err, currentVersion) + return fmt.Errorf("ERROR: cannot get current tag %s, %w:\n%s", proj, err, currentVersion) } } @@ -177,15 +251,16 @@ func runDeploy(opts *DeployOptions) error { elapsed := time.Since(start) p(opts.IO, "") p(opts.IO, ` - Instill Core console available at http://localhost:3000 - After changing your password, run "$ inst auth login". + Instill Core console available at http://localhost:3000 - User: %s - Password: %s + User: %s + Password: %s - Deployed in %.0fs to %s - `, - DefUsername, DefPassword, elapsed.Seconds(), path) + After changing your password, run "$ inst auth login" with your new password. + + Deployed in %.0fs to %s + `, + DefUsername, DefPassword, elapsed.Seconds(), LocalInstancePath) err = registerInstance(opts) if err != nil { @@ -197,14 +272,6 @@ func runDeploy(opts *DeployOptions) error { func registerInstance(opts *DeployOptions) error { // register the new instance - err := opts.Config.Set("", ConfigKeyPath, opts.Path) - if err != nil { - return fmt.Errorf("ERROR: saving config, %w", err) - } - err = opts.Config.Write() - if err != nil { - return fmt.Errorf("ERROR: saving config, %w", err) - } exists, err := instance.IsInstanceAdded(opts.Config, "localhost:8080") if err != nil { return err diff --git a/pkg/cmd/local/deploy_test.go b/pkg/cmd/local/deploy_test.go index 597d99a..0d51be0 100644 --- a/pkg/cmd/local/deploy_test.go +++ b/pkg/cmd/local/deploy_test.go @@ -3,8 +3,6 @@ package local import ( "bytes" "fmt" - "os" - "path/filepath" "testing" "github.com/google/shlex" @@ -16,11 +14,7 @@ import ( ) func TestLocalDeployCmd(t *testing.T) { - d, err := os.UserHomeDir() - if err != nil { - logger.Error("Couldn't get home directory", err) - } - dir := filepath.Join(d, ".local", "instill") + string(os.PathSeparator) + tests := []struct { name string stdin string @@ -30,12 +24,10 @@ func TestLocalDeployCmd(t *testing.T) { isErr bool }{ { - name: "no arguments", - input: "", - output: DeployOptions{ - Path: dir, - }, - isErr: false, + name: "no arguments", + input: "", + output: DeployOptions{}, + isErr: false, }, } @@ -59,9 +51,7 @@ func TestLocalDeployCmd(t *testing.T) { argv, err := shlex.Split(tt.input) assert.NoError(t, err) - var gotOpts *DeployOptions cmd := NewDeployCmd(f, func(opts *DeployOptions) error { - gotOpts = opts return nil }) cmd.Flags().BoolP("help", "x", false, "") @@ -78,7 +68,6 @@ func TestLocalDeployCmd(t *testing.T) { } assert.NoError(t, err) - assert.Equal(t, tt.output.Path, gotOpts.Path) }) } } @@ -90,11 +79,6 @@ func checkForUpdateMock(ExecDep, string, string, string) (*releaseInfo, error) { func TestLocalDeployCmdRun(t *testing.T) { execMock := &ExecMock{} osMock := &OSMock{} - d, err := os.UserHomeDir() - if err != nil { - logger.Error("Couldn't get home directory", err) - } - dir := filepath.Join(d, ".local", "instill") + string(os.PathSeparator) tests := []struct { name string input *DeployOptions @@ -106,12 +90,11 @@ func TestLocalDeployCmdRun(t *testing.T) { { name: "local deploy", input: &DeployOptions{ - Path: dir, Exec: execMock, OS: osMock, Config: config.ConfigStub{}, checkForUpdate: checkForUpdateMock, - isDeployed: func(ed ExecDep) error { + isDeployed: func(ExecDep, string) error { return nil }, }, @@ -121,12 +104,11 @@ func TestLocalDeployCmdRun(t *testing.T) { { name: "local deploy", input: &DeployOptions{ - Path: dir, Exec: execMock, OS: osMock, Config: config.ConfigStub{}, checkForUpdate: checkForUpdateMock, - isDeployed: func(ed ExecDep) error { + isDeployed: func(ExecDep, string) error { return fmt.Errorf("") }, }, @@ -136,33 +118,17 @@ func TestLocalDeployCmdRun(t *testing.T) { { name: "local deploy", input: &DeployOptions{ - Path: dir, Exec: execMock, OS: osMock, Config: config.ConfigStub{}, checkForUpdate: checkForUpdateMock, - isDeployed: func(ed ExecDep) error { + isDeployed: func(ExecDep, string) error { return nil }, }, stdout: "", isErr: false, }, - { - name: "local deploy", - input: &DeployOptions{ - Path: "a/path/does/not/exist", - Exec: execMock, - OS: osMock, - Config: config.ConfigStub{}, - checkForUpdate: checkForUpdateMock, - isDeployed: func(ed ExecDep) error { - return fmt.Errorf("") - }, - }, - stdout: "", - isErr: false, - }, } for _, tt := range tests { diff --git a/pkg/cmd/local/local.go b/pkg/cmd/local/local.go index 7e801bd..a1dc6cc 100644 --- a/pkg/cmd/local/local.go +++ b/pkg/cmd/local/local.go @@ -2,19 +2,23 @@ package local import ( "fmt" - "log/slog" "os" "os/exec" - "regexp" + "path/filepath" "strings" "time" + "github.com/mgutz/ansi" "github.com/spf13/cobra" + "github.com/instill-ai/cli/internal/build" "github.com/instill-ai/cli/internal/config" + "github.com/instill-ai/cli/pkg/cmd/factory" "github.com/instill-ai/cli/pkg/cmdutil" ) +var projs = [3]string{"base", "vdp", "model"} + // ExecDep is an interface for executing commands type ExecDep interface { Command(name string, arg ...string) *exec.Cmd @@ -27,7 +31,8 @@ type OSDep interface { Stat(name string) (os.FileInfo, error) } -var gitDescribeSuffixRE = regexp.MustCompile(`\d+-\d+-g[a-f0-9]{8}$`) +// LocalInstancePath is the path to keep files for local instance deployment +var LocalInstancePath string // releaseInfo stores information about a release type releaseInfo struct { @@ -36,39 +41,57 @@ type releaseInfo struct { PublishedAt time.Time `json:"published_at"` } -type StateEntry struct { +type stateEntry struct { CheckedForUpdateAt time.Time `yaml:"checked_for_update_at"` LatestRelease releaseInfo `yaml:"latest_release"` } -const ( - // ConfigKeyPath is the config key for the local instance path where Instill Core is installed - ConfigKeyPath = "local-instance-path" -) - -var projs = [3]string{"Base", "VDP", "Model"} - -var logger *slog.Logger var p = cmdutil.P -func init() { - var lvl = new(slog.LevelVar) - if os.Getenv("DEBUG") != "" { - lvl.Set(slog.LevelDebug) - } else { - lvl.Set(slog.LevelError + 1) - } - logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ - Level: lvl, - })) -} - // New creates a new command func New(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "local ", Short: "Local Instill Core instance", Long: `Create and manage a local Instill Core instance with ease.`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + d, err := os.UserHomeDir() + if err != nil { + return err + } + + LocalInstancePath = filepath.Join(d, ".local", "instill") + + // check for update + if cmd.Flags().Lookup("upgrade") == nil || (cmd.Flags().Lookup("upgrade") != nil && !cmd.Flags().Lookup("upgrade").Changed) { + for _, proj := range projs { + projDirPath := filepath.Join(LocalInstancePath, proj) + _, err = os.Stat(projDirPath) + if !os.IsNotExist(err) { + if err = os.Chdir(projDirPath); err != nil { + return err + } + if currentVersion, err := execCmd(nil, "bash", "-c", "git name-rev --tags --name-only $(git rev-parse HEAD)"); err == nil { + currentVersion = strings.Trim(currentVersion, "\n") + if newRelease, err := checkForUpdate(nil, filepath.Join(config.StateDir(), fmt.Sprintf("%s.yml", proj)), fmt.Sprintf("instill-ai/%s", proj), currentVersion); err != nil { + return fmt.Errorf("ERROR: cannot check for update %s, %w:\n%s", proj, err, currentVersion) + } else if newRelease != nil { + cmdFactory := factory.New(build.Version) + stderr := cmdFactory.IOStreams.ErrOut + fmt.Fprintf(stderr, "\n%s %s → %s\n", + ansi.Color(fmt.Sprintf("A new release of Instill %s is available:", proj), "yellow"), + ansi.Color(currentVersion, "cyan"), + ansi.Color(newRelease.Version, "cyan")) + fmt.Fprintf(stderr, "%s\n\n", + ansi.Color("Run 'inst local deploy --upgrade' to deploy the latest version", "yellow")) + } + } + } + } + } + + return nil + }, } cmdutil.DisableAuthCheck(cmd) @@ -93,15 +116,3 @@ func execCmd(execDep ExecDep, cmd string, params ...string) (string, error) { outStr := strings.Trim(string(out[:]), " ") return outStr, err } - -// getConfigPath returns a configured path to the local instance. -func getConfigPath(cfg config.Config) (string, error) { - path, err := cfg.Get("", ConfigKeyPath) - if err != nil { - return "", err - } - if path == "" { - return "", fmt.Errorf("config %s is empty", ConfigKeyPath) - } - return path, nil -} diff --git a/pkg/cmd/local/start.go b/pkg/cmd/local/start.go index b7e0ea8..0971a3b 100644 --- a/pkg/cmd/local/start.go +++ b/pkg/cmd/local/start.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -58,31 +57,34 @@ func NewStartCmd(f *cmdutil.Factory, runF func(*StartOptions) error) *cobra.Comm } func runStart(opts *StartOptions) error { - path, err := getConfigPath(opts.Config) - if err != nil { - return fmt.Errorf("ERROR: %w", err) - } - if err := isDeployed(opts.Exec); err != nil { - return fmt.Errorf("ERROR: %w", err) + + if _, err := os.Stat(LocalInstancePath); os.IsNotExist(err) { + p(opts.IO, "") + p(opts.IO, "Instill Core instance not deployed") + return nil } - for i := range projs { - proj := strings.ToLower(projs[i]) - if opts.OS != nil { - err = opts.OS.Chdir(filepath.Join(path, proj)) + for _, proj := range projs { + projDirPath := filepath.Join(LocalInstancePath, proj) + if err := isProjectDeployed(opts.Exec, proj); err == nil { + if opts.OS != nil { + err = opts.OS.Chdir(projDirPath) + } else { + err = os.Chdir(projDirPath) + } + if err != nil { + return fmt.Errorf("ERROR: cannot open the directory: %w", err) + } + p(opts.IO, fmt.Sprintf("Starting %s...", proj)) + out, err := execCmd(opts.Exec, "make", "start") + if err != nil { + return fmt.Errorf("ERROR: when starting, %w", err) + } + if err != nil { + return fmt.Errorf("ERROR: %s when starting, %w\n%s", proj, err, out) + } } else { - err = os.Chdir(filepath.Join(path, proj)) - } - if err != nil { - return fmt.Errorf("ERROR: can't open the destination, %w", err) - } - p(opts.IO, fmt.Sprintf("Starting %s...", projs[i])) - out, err := execCmd(opts.Exec, "make", "start") - if err != nil { - return fmt.Errorf("ERROR: when starting, %w", err) - } - if err != nil { - return fmt.Errorf("ERROR: %s when starting, %w\n%s", projs[i], err, out) + return fmt.Errorf("ERROR: %w", err) } } diff --git a/pkg/cmd/local/start_test.go b/pkg/cmd/local/start_test.go index 265c182..1f01723 100644 --- a/pkg/cmd/local/start_test.go +++ b/pkg/cmd/local/start_test.go @@ -2,6 +2,7 @@ package local import ( "bytes" + "strings" "testing" "github.com/google/shlex" @@ -74,7 +75,7 @@ func TestLocalStartCmdRun(t *testing.T) { execMock := &ExecMock{} osMock := &OSMock{} cfg := config.ConfigStub{} - _ = cfg.Set("", ConfigKeyPath, "/foo/bar") + _ = cfg.Set("", LocalInstancePath, "/foo/bar") tests := []struct { name string input *StartOptions @@ -90,7 +91,7 @@ func TestLocalStartCmdRun(t *testing.T) { OS: osMock, Config: cfg, }, - stdout: "Instill Core started", + stdout: "Instill Core instance not deployed", isErr: false, }, } @@ -107,7 +108,7 @@ func TestLocalStartCmdRun(t *testing.T) { } else { assert.NoError(t, err) } - assert.Regexp(t, tt.stdout, stdout.String()) + assert.Regexp(t, tt.stdout, strings.Trim(stdout.String(), "\n")) assert.Equal(t, tt.stderr, stderr.String()) if tt.expectFn != nil { tt.expectFn(t, tt.input.Config) diff --git a/pkg/cmd/local/status.go b/pkg/cmd/local/status.go index b42ee88..378582f 100644 --- a/pkg/cmd/local/status.go +++ b/pkg/cmd/local/status.go @@ -2,6 +2,7 @@ package local import ( "fmt" + "os" "strings" "github.com/MakeNowJust/heredoc" @@ -12,6 +13,7 @@ import ( "github.com/instill-ai/cli/pkg/iostreams" ) +// StatusOptions contains the command line options type StatusOptions struct { IO *iostreams.IOStreams Exec ExecDep @@ -19,9 +21,9 @@ type StatusOptions struct { Config config.Config MainExecutable string Interactive bool - Verbose bool } +// NewStatusCmd creates a new command func NewStatusCmd(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Command { opts := &StatusOptions{ IO: f.IOStreams, @@ -59,7 +61,6 @@ func NewStatusCmd(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co return runStatus(opts) }, } - cmd.Flags().BoolVar(&opts.Verbose, "verbose", false, "Show verbose output") return cmd } @@ -67,105 +68,72 @@ func NewStatusCmd(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co // TODO separate health statuses per API func runStatus(opts *StatusOptions) error { - deployed := "NO" - started := "NO" - healthy := "NO" - if err := isDeployed(opts.Exec); err == nil { - deployed = "YES" - } - if err := isStarted(opts.Exec); err == nil { - started = "YES" - } - errHealthy := isHealthy(opts.Exec) - if errHealthy == nil { - healthy = "YES" - } - - p(opts.IO, ` - Status of the local Instill Core instance: - - Deployed: %s - Started: %s - Healthy: %s - `, deployed, started, healthy) - - if opts.Verbose && errHealthy != nil { + if _, err := os.Stat(LocalInstancePath); os.IsNotExist(err) { p(opts.IO, "") - p(opts.IO, "Error:\n%s", errHealthy) + p(opts.IO, "Instill Core instance not deployed") + return nil } - return nil -} - -// isDeployed returns no errors if an local instance is detected -func isDeployed(execDep ExecDep) error { - - var checkList = make([]bool, len(projs)) - for i := range checkList { - proj := strings.ToLower(projs[i]) - if _, err := execCmd(execDep, "bash", "-c", fmt.Sprintf("docker compose ls -a | grep instill-%s", proj)); err == nil { - checkList[i] = true + for _, proj := range projs { + deployed := "NO" + started := "NO" + healthy := "NO" + if err := isProjectDeployed(opts.Exec, proj); err == nil { + deployed = "YES" } - } - - suiteCheck := 0 - for i := range checkList { - if checkList[i] { - suiteCheck++ + if err := isProjectStarted(opts.Exec, proj); err == nil { + started = "YES" } + if err := isProjectHealthy(opts.Exec, proj); err == nil { + healthy = "YES" + } + fmt.Printf("%5s - Deployed: %s | Started: %s | Healthy: %s\n", proj, deployed, started, healthy) } - if suiteCheck == len(checkList) { - return nil - } - - return fmt.Errorf("No local Instill Core deployment detected") + return nil } -// isStarted returns no errors if an instance is running. -func isStarted(execDep ExecDep) error { - - if err := isDeployed(execDep); err != nil { +func isProjectDeployed(execDep ExecDep, proj string) error { + if _, err := execCmd(execDep, "bash", "-c", fmt.Sprintf("docker compose ls -a | grep instill-%s", proj)); err != nil { return err } - - for i := range projs { - proj := strings.ToLower(projs[i]) - if _, err := execCmd(execDep, "bash", "-c", fmt.Sprintf("docker compose ls -a --format json --filter name=instill-%s | grep running", proj)); err != nil { - return fmt.Errorf("%s is not running", projs[i]) - } - } - return nil } -// isHealthy returns no error if an instance in `path` is responding. -// TODO assert responses -func isHealthy(execDep ExecDep) error { - if err := isDeployed(execDep); err != nil { +func isProjectStarted(execDep ExecDep, proj string) error { + if _, err := execCmd(execDep, "bash", "-c", fmt.Sprintf("docker compose ls -a --format json --filter name=instill-%s | grep running", proj)); err != nil { return err } - if err := isStarted(execDep); err != nil { - return err - } - urls := []string{ - ":8080/base/v1alpha/health/mgmt", - ":8080/vdp/v1alpha/health/pipeline", - ":8080/vdp/v1alpha/health/connector", - ":8080/model/v1alpha/health/model", - ":3000/", + return nil +} + +func isProjectHealthy(execDep ExecDep, proj string) error { + + var urls []string + + switch proj { + case "base": + urls = []string{ + "localhost:8080/base/v1alpha/health/mgmt", + } + case "vdp": + urls = []string{ + "localhost:8080/vdp/v1alpha/health/pipeline", + "localhost:8080/vdp/v1alpha/health/connector", + } + case "model": + urls = []string{ + "localhost:8080/model/v1alpha/health/model", + } } + for _, url := range urls { - u := fmt.Sprintf("localhost%s", url) - out, err := execCmd(execDep, "curl", u) - logger.Debug("IsHealthy", "url", u, "out", out) - if err != nil { - logger.Error("IsHealthy", "url", u, "err", err.Error()) + if out, err := execCmd(execDep, "curl", url); err != nil { return err - } - if out == "" { - return fmt.Errorf("cant reach %s", u) + } else if !strings.Contains(out, "SERVING_STATUS_SERVING") { + return fmt.Errorf("ERROR: %s is not healthy", url) } } + return nil } diff --git a/pkg/cmd/local/status_test.go b/pkg/cmd/local/status_test.go index c6b218b..3a8150d 100644 --- a/pkg/cmd/local/status_test.go +++ b/pkg/cmd/local/status_test.go @@ -74,7 +74,7 @@ func TestLocalStatusCmdRun(t *testing.T) { execMock := &ExecMock{} osMock := &OSMock{} cfg := config.ConfigStub{} - _ = cfg.Set("", ConfigKeyPath, "/foo/bar") + _ = cfg.Set("", LocalInstancePath, "/foo/bar") tests := []struct { name string input *StatusOptions @@ -90,7 +90,7 @@ func TestLocalStatusCmdRun(t *testing.T) { OS: osMock, Config: cfg, }, - stdout: "Deployed: YES", + stdout: "", isErr: false, }, } diff --git a/pkg/cmd/local/stop.go b/pkg/cmd/local/stop.go index 78e641a..359e827 100644 --- a/pkg/cmd/local/stop.go +++ b/pkg/cmd/local/stop.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -61,31 +60,33 @@ func NewStopCmd(f *cmdutil.Factory, runF func(*StopOptions) error) *cobra.Comman func runStop(opts *StopOptions) error { - path, err := getConfigPath(opts.Config) - if err != nil { - return fmt.Errorf("ERROR: %w", err) - } - if err := isDeployed(opts.Exec); err != nil { - return fmt.Errorf("ERROR: %w", err) + if _, err := os.Stat(LocalInstancePath); os.IsNotExist(err) { + p(opts.IO, "") + p(opts.IO, "Instill Core instance not deployed") + return nil } - for i := range projs { - proj := strings.ToLower(projs[i]) - if opts.OS != nil { - err = opts.OS.Chdir(filepath.Join(path, proj)) + for _, proj := range projs { + projDirPath := filepath.Join(LocalInstancePath, proj) + if err := isProjectDeployed(opts.Exec, proj); err == nil { + if opts.OS != nil { + err = opts.OS.Chdir(projDirPath) + } else { + err = os.Chdir(projDirPath) + } + if err != nil { + return fmt.Errorf("ERROR: cannot open the directory: %w", err) + } + p(opts.IO, fmt.Sprintf("Stopping %s...", proj)) + out, err := execCmd(opts.Exec, "make", "stop") + if err != nil { + return fmt.Errorf("ERROR: when stopping, %w", err) + } + if err != nil { + return fmt.Errorf("ERROR: %s when stopping, %w\n%s", proj, err, out) + } } else { - err = os.Chdir(filepath.Join(path, proj)) - } - if err != nil { - return fmt.Errorf("ERROR: can't open the destination, %w", err) - } - p(opts.IO, fmt.Sprintf("Stopping %s...", projs[i])) - out, err := execCmd(opts.Exec, "make", "stop") - if err != nil { - return fmt.Errorf("ERROR: when stopping, %w", err) - } - if err != nil { - return fmt.Errorf("ERROR: %s when stopping, %w\n%s", projs[i], err, out) + return fmt.Errorf("ERROR: %w", err) } } diff --git a/pkg/cmd/local/stop_test.go b/pkg/cmd/local/stop_test.go index 8de73e1..d830f07 100644 --- a/pkg/cmd/local/stop_test.go +++ b/pkg/cmd/local/stop_test.go @@ -2,6 +2,7 @@ package local import ( "bytes" + "strings" "testing" "github.com/google/shlex" @@ -74,7 +75,7 @@ func TestLocalStopCmdRun(t *testing.T) { execMock := &ExecMock{} osMock := &OSMock{} cfg := config.ConfigStub{} - _ = cfg.Set("", ConfigKeyPath, "/foo/bar") + _ = cfg.Set("", LocalInstancePath, "/foo/bar") tests := []struct { name string input *StopOptions @@ -90,7 +91,7 @@ func TestLocalStopCmdRun(t *testing.T) { OS: osMock, Config: cfg, }, - stdout: "Instill Core stopped", + stdout: "Instill Core instance not deployed", isErr: false, }, } @@ -107,7 +108,7 @@ func TestLocalStopCmdRun(t *testing.T) { } else { assert.NoError(t, err) } - assert.Regexp(t, tt.stdout, stdout.String()) + assert.Regexp(t, tt.stdout, strings.Trim(stdout.String(), "\n")) assert.Equal(t, tt.stderr, stderr.String()) if tt.expectFn != nil { tt.expectFn(t, tt.input.Config) diff --git a/pkg/cmd/local/undeploy.go b/pkg/cmd/local/undeploy.go index 254fd51..aa08cd1 100644 --- a/pkg/cmd/local/undeploy.go +++ b/pkg/cmd/local/undeploy.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -61,46 +60,46 @@ func NewUndeployCmd(f *cmdutil.Factory, runF func(*UndeployOptions) error) *cobr } func runUndeploy(opts *UndeployOptions) error { - - path, err := getConfigPath(opts.Config) - if err != nil { - return fmt.Errorf("ERROR: %w", err) - } - - if err := isDeployed(opts.Exec); err != nil { - return fmt.Errorf("ERROR: %w", err) + var err error + for _, proj := range projs { + projDirPath := filepath.Join(LocalInstancePath, proj) + _, err = os.Stat(projDirPath) + if !os.IsNotExist(err) { + if opts.OS != nil { + err = opts.OS.Chdir(projDirPath) + } else { + err = os.Chdir(projDirPath) + } + if err != nil { + return fmt.Errorf("ERROR: cannot open the directory: %w", err) + } + p(opts.IO, fmt.Sprintf("Tearing down %s...", proj)) + _, err = os.Stat(filepath.Join(projDirPath, "Makefile")) + if !os.IsNotExist(err) { + out, err := execCmd(opts.Exec, "bash", "-c", "make down") + if err != nil { + return fmt.Errorf("ERROR: %s when tearing down, %w\n%s", proj, err, out) + } + } + } } - for i := range projs { - proj := strings.ToLower(projs[i]) - if opts.OS != nil { - err = opts.OS.Chdir(filepath.Join(path, proj)) - } else { - err = os.Chdir(filepath.Join(path, proj)) - } - if err != nil { - return fmt.Errorf("ERROR: can't open the destination, %w", err) - } - p(opts.IO, fmt.Sprintf("Tearing down Instill %s...", projs[i])) - out, err := execCmd(opts.Exec, "make", "down") - if err != nil { - return fmt.Errorf("ERROR: when tearing down, %w", err) - } - if err != nil { - return fmt.Errorf("ERROR: %s when tearing down, %w\n%s", projs[i], err, out) + _, err = os.Stat(LocalInstancePath) + if os.IsNotExist(err) { + p(opts.IO, "") + p(opts.IO, "Instill Core instance not deployed") + } else { + if os.RemoveAll(LocalInstancePath); err != nil { + return fmt.Errorf("ERROR: cannot remove %s, %w", LocalInstancePath, err) } + p(opts.IO, "") + p(opts.IO, "Instill Core instance not deployed") } - p(opts.IO, "Remove local Instill Core files in: %s", path) - os.RemoveAll(path) - if err := unregisterInstance(opts); err != nil { return err } - p(opts.IO, "") - p(opts.IO, "Instill Core undeployed") - return nil } diff --git a/pkg/cmd/local/undeploy_test.go b/pkg/cmd/local/undeploy_test.go index ac440fe..9eda2cf 100644 --- a/pkg/cmd/local/undeploy_test.go +++ b/pkg/cmd/local/undeploy_test.go @@ -2,6 +2,7 @@ package local import ( "bytes" + "strings" "testing" "github.com/google/shlex" @@ -74,7 +75,7 @@ func TestLocalUndeployCmdRun(t *testing.T) { execMock := &ExecMock{} osMock := &OSMock{} cfg := config.ConfigStub{} - _ = cfg.Set("", ConfigKeyPath, "/foo/bar") + _ = cfg.Set("", LocalInstancePath, "/foo/bar") tests := []struct { name string input *UndeployOptions @@ -90,7 +91,7 @@ func TestLocalUndeployCmdRun(t *testing.T) { OS: osMock, Config: cfg, }, - stdout: "Instill Core undeployed", + stdout: "Instill Core instance not deployed", isErr: false, }, } @@ -107,7 +108,7 @@ func TestLocalUndeployCmdRun(t *testing.T) { } else { assert.NoError(t, err) } - assert.Regexp(t, tt.stdout, stdout.String()) + assert.Regexp(t, tt.stdout, strings.Trim(stdout.String(), "\n")) assert.Equal(t, tt.stderr, stderr.String()) if tt.expectFn != nil { tt.expectFn(t, tt.input.Config) diff --git a/pkg/cmd/local/update.go b/pkg/cmd/local/update.go index 2a3cc2d..145ed51 100644 --- a/pkg/cmd/local/update.go +++ b/pkg/cmd/local/update.go @@ -5,18 +5,18 @@ import ( "fmt" "os" "path/filepath" - "strconv" "strings" "time" - "github.com/hashicorp/go-version" + "golang.org/x/mod/semver" "gopkg.in/yaml.v3" ) // checkForUpdate checks whether this software has had a newer release on GitHub func checkForUpdate(execDep ExecDep, stateFilePath, repo, currentVersion string) (*releaseInfo, error) { + stateEntry, _ := getStateEntry(stateFilePath) - if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 { + if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 0 { return nil, nil } @@ -30,7 +30,9 @@ func checkForUpdate(execDep ExecDep, stateFilePath, repo, currentVersion string) return nil, err } - if versionGreaterThan(releaseInfo.Version, currentVersion) { + if semver.Compare( + strings.Replace(releaseInfo.Version, semver.Prerelease(releaseInfo.Version), "", 1), + strings.Replace(currentVersion, semver.Prerelease(currentVersion), "", 1)) == 1 { return releaseInfo, nil } @@ -49,13 +51,13 @@ func getLatestPreReleaseInfo(execDep ExecDep, repo string) (*releaseInfo, error) return &latestPreRelease, nil } -func getStateEntry(stateFilePath string) (*StateEntry, error) { +func getStateEntry(stateFilePath string) (*stateEntry, error) { content, err := os.ReadFile(stateFilePath) if err != nil { return nil, err } - var stateEntry StateEntry + var stateEntry stateEntry err = yaml.Unmarshal(content, &stateEntry) if err != nil { return nil, err @@ -65,7 +67,7 @@ func getStateEntry(stateFilePath string) (*StateEntry, error) { } func setStateEntry(stateFilePath string, t time.Time, r releaseInfo) error { - data := StateEntry{CheckedForUpdateAt: t, LatestRelease: r} + data := stateEntry{CheckedForUpdateAt: t, LatestRelease: r} content, err := yaml.Marshal(data) if err != nil { return err @@ -79,16 +81,3 @@ func setStateEntry(stateFilePath string, t time.Time, r releaseInfo) error { err = os.WriteFile(stateFilePath, content, 0600) return err } - -func versionGreaterThan(v, w string) bool { - w = gitDescribeSuffixRE.ReplaceAllStringFunc(w, func(m string) string { - idx := strings.IndexRune(m, '-') - n, _ := strconv.Atoi(m[0:idx]) - return fmt.Sprintf("%d-pre.0", n+1) - }) - - vv, ve := version.NewVersion(v) - vw, we := version.NewVersion(w) - - return ve == nil && we == nil && vv.GreaterThan(vw) -}