github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runners/deploy/deploy.go (about)

     1  package deploy
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	rt "runtime"
     8  	"strings"
     9  
    10  	rtrunbit "github.com/ActiveState/cli/internal/runbits/runtime"
    11  	"github.com/go-openapi/strfmt"
    12  
    13  	"github.com/ActiveState/cli/internal/analytics"
    14  	"github.com/ActiveState/cli/internal/assets"
    15  	"github.com/ActiveState/cli/internal/config"
    16  	"github.com/ActiveState/cli/internal/errs"
    17  	"github.com/ActiveState/cli/internal/fileutils"
    18  	"github.com/ActiveState/cli/internal/locale"
    19  	"github.com/ActiveState/cli/internal/logging"
    20  	"github.com/ActiveState/cli/internal/multilog"
    21  	"github.com/ActiveState/cli/internal/osutils"
    22  	"github.com/ActiveState/cli/internal/output"
    23  	"github.com/ActiveState/cli/internal/primer"
    24  	"github.com/ActiveState/cli/internal/rtutils"
    25  	"github.com/ActiveState/cli/internal/subshell"
    26  	"github.com/ActiveState/cli/internal/subshell/sscommon"
    27  	"github.com/ActiveState/cli/pkg/platform/authentication"
    28  	"github.com/ActiveState/cli/pkg/platform/model"
    29  	"github.com/ActiveState/cli/pkg/platform/runtime"
    30  	"github.com/ActiveState/cli/pkg/platform/runtime/setup"
    31  	"github.com/ActiveState/cli/pkg/platform/runtime/target"
    32  	"github.com/ActiveState/cli/pkg/project"
    33  )
    34  
    35  type Params struct {
    36  	Namespace project.Namespaced
    37  	Path      string
    38  	Force     bool
    39  	UserScope bool
    40  }
    41  
    42  // RequiresAdministratorRights checks if the requested deploy command requires administrator privileges.
    43  func RequiresAdministratorRights(step Step, userScope bool) bool {
    44  	if rt.GOOS != "windows" {
    45  		return false
    46  	}
    47  	return (step == UnsetStep || step == ConfigureStep) && !userScope
    48  }
    49  
    50  type Deploy struct {
    51  	auth      *authentication.Auth
    52  	output    output.Outputer
    53  	subshell  subshell.SubShell
    54  	step      Step
    55  	cfg       *config.Instance
    56  	analytics analytics.Dispatcher
    57  	svcModel  *model.SvcModel
    58  }
    59  
    60  type primeable interface {
    61  	primer.Auther
    62  	primer.Outputer
    63  	primer.Subsheller
    64  	primer.Configurer
    65  	primer.Analyticer
    66  	primer.SvcModeler
    67  }
    68  
    69  func NewDeploy(step Step, prime primeable) *Deploy {
    70  	return &Deploy{
    71  		prime.Auth(),
    72  		prime.Output(),
    73  		prime.Subshell(),
    74  		step,
    75  		prime.Config(),
    76  		prime.Analytics(),
    77  		prime.SvcModel(),
    78  	}
    79  }
    80  
    81  func (d *Deploy) Run(params *Params) error {
    82  	if RequiresAdministratorRights(d.step, params.UserScope) {
    83  		isAdmin, err := osutils.IsAdmin()
    84  		if err != nil {
    85  			multilog.Error("Could not check for windows administrator privileges: %v", err)
    86  		}
    87  		if !isAdmin {
    88  			return locale.NewError("err_deploy_admin_privileges_required", "Administrator rights are required for this command to modify the system PATH.  If you want to deploy to the user environment, please adjust the command line flags.")
    89  		}
    90  	}
    91  
    92  	commitID, err := d.commitID(params.Namespace)
    93  	if err != nil {
    94  		return locale.WrapError(err, "err_deploy_commitid", "Could not grab commit ID for project: {{.V0}}.", params.Namespace.String())
    95  	}
    96  
    97  	rtTarget := target.NewCustomTarget(params.Namespace.Owner, params.Namespace.Project, commitID, params.Path, target.TriggerDeploy) /* TODO: handle empty path */
    98  
    99  	logging.Debug("runSteps: %s", d.step.String())
   100  
   101  	if d.step == UnsetStep || d.step == InstallStep {
   102  		logging.Debug("Running install step")
   103  		if err := d.install(rtTarget); err != nil {
   104  			return err
   105  		}
   106  	}
   107  	if d.step == UnsetStep || d.step == ConfigureStep {
   108  		logging.Debug("Running configure step")
   109  		if err := d.configure(params.Namespace, rtTarget, params.UserScope); err != nil {
   110  			return err
   111  		}
   112  	}
   113  	if d.step == UnsetStep || d.step == SymlinkStep {
   114  		logging.Debug("Running symlink step")
   115  		if err := d.symlink(rtTarget, params.Force); err != nil {
   116  			return err
   117  		}
   118  	}
   119  	if d.step == UnsetStep || d.step == ReportStep {
   120  		logging.Debug("Running report step")
   121  		if err := d.report(rtTarget); err != nil {
   122  			return err
   123  		}
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  func (d *Deploy) commitID(namespace project.Namespaced) (strfmt.UUID, error) {
   130  	commitID := namespace.CommitID
   131  	if commitID == nil {
   132  		branch, err := model.DefaultBranchForProjectName(namespace.Owner, namespace.Project)
   133  		if err != nil {
   134  			return "", errs.Wrap(err, "Could not detect default branch")
   135  		}
   136  
   137  		if branch.CommitID == nil {
   138  			return "", locale.NewInputError(
   139  				"err_deploy_no_commits",
   140  				"The project '{{.V0}}' does not have any packages configured, please add add some packages first.", namespace.String())
   141  		}
   142  
   143  		commitID = branch.CommitID
   144  	}
   145  
   146  	if commitID == nil {
   147  		return "", errs.New("commitID is nil")
   148  	}
   149  
   150  	return *commitID, nil
   151  }
   152  
   153  func (d *Deploy) install(rtTarget setup.Targeter) (rerr error) {
   154  	d.output.Notice(output.Title(locale.T("deploy_install")))
   155  
   156  	rti, err := runtime.New(rtTarget, d.analytics, d.svcModel, d.auth, d.cfg, d.output)
   157  	if err != nil {
   158  		return locale.WrapError(err, "deploy_runtime_err", "Could not initialize runtime")
   159  	}
   160  	if !rti.NeedsUpdate() {
   161  		d.output.Notice(locale.Tl("deploy_already_installed", "Already installed"))
   162  		return nil
   163  	}
   164  
   165  	pg := rtrunbit.NewRuntimeProgressIndicator(d.output)
   166  	defer rtutils.Closer(pg.Close, &rerr)
   167  	if err := rti.SolveAndUpdate(pg); err != nil {
   168  		return locale.WrapError(err, "deploy_install_failed", "Installation failed.")
   169  	}
   170  
   171  	// Todo Remove with https://www.pivotaltracker.com/story/show/178161240
   172  	// call rti.Environ as this completes the runtime activation cycle:
   173  	// It ensures that the analytics event for failure / success are sent
   174  	_, _ = rti.Env(false, false)
   175  
   176  	if rt.GOOS == "windows" {
   177  		contents, err := assets.ReadFileBytes("scripts/setenv.bat")
   178  		if err != nil {
   179  			return err
   180  		}
   181  		err = fileutils.WriteFile(filepath.Join(rtTarget.Dir(), "setenv.bat"), contents)
   182  		if err != nil {
   183  			return locale.WrapError(err, "err_deploy_write_setenv", "Could not create setenv batch scriptfile at path: %s", rtTarget.Dir())
   184  		}
   185  	}
   186  
   187  	d.output.Print(locale.Tl("deploy_install_done", "Installation completed"))
   188  	return nil
   189  }
   190  
   191  func (d *Deploy) configure(namespace project.Namespaced, rtTarget setup.Targeter, userScope bool) error {
   192  	rti, err := runtime.New(rtTarget, d.analytics, d.svcModel, d.auth, d.cfg, d.output)
   193  	if err != nil {
   194  		return locale.WrapError(err, "deploy_runtime_err", "Could not initialize runtime")
   195  	}
   196  	if rti.NeedsUpdate() {
   197  		return locale.NewInputError("err_deploy_run_install")
   198  	}
   199  
   200  	env, err := rti.Env(false, false)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	d.output.Notice(output.Title(locale.Tr("deploy_configure_shell", d.subshell.Shell())))
   206  
   207  	// Configure available shells
   208  	err = subshell.ConfigureAvailableShells(d.subshell, d.cfg, env, sscommon.DeployID, userScope)
   209  	if err != nil {
   210  		return locale.WrapError(err, "err_deploy_subshell_write", "Could not write environment information to your shell configuration.")
   211  	}
   212  
   213  	binPath := filepath.Join(rtTarget.Dir(), "bin")
   214  	if err := fileutils.MkdirUnlessExists(binPath); err != nil {
   215  		return locale.WrapError(err, "err_deploy_binpath", "Could not create bin directory.")
   216  	}
   217  
   218  	// Write global env file
   219  	d.output.Notice(fmt.Sprintf("Writing shell env file to %s\n", filepath.Join(rtTarget.Dir(), "bin")))
   220  	err = d.subshell.SetupShellRcFile(binPath, env, &namespace, d.cfg)
   221  	if err != nil {
   222  		return locale.WrapError(err, "err_deploy_subshell_rc_file", "Could not create environment script.")
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  func (d *Deploy) symlink(rtTarget setup.Targeter, overwrite bool) error {
   229  	rti, err := runtime.New(rtTarget, d.analytics, d.svcModel, d.auth, d.cfg, d.output)
   230  	if err != nil {
   231  		return locale.WrapError(err, "deploy_runtime_err", "Could not initialize runtime")
   232  	}
   233  	if rti.NeedsUpdate() {
   234  		return locale.NewInputError("err_deploy_run_install")
   235  	}
   236  
   237  	var path string
   238  	if rt.GOOS != "windows" {
   239  		// Retrieve path to write symlinks to
   240  		path, err = usablePath()
   241  		if err != nil {
   242  			return locale.WrapError(err, "err_usablepath", "Could not retrieve a usable PATH")
   243  		}
   244  	}
   245  
   246  	// Retrieve artifact binary directories
   247  	bins, err := rti.ExecutablePaths()
   248  	if err != nil {
   249  		return locale.WrapError(err, "err_symlink_exes", "Could not detect executable paths")
   250  	}
   251  
   252  	exes, err := osutils.Executables(bins)
   253  	if err != nil {
   254  		return locale.WrapError(err, "err_symlink_exes", "Could not detect executables")
   255  	}
   256  
   257  	// Remove duplicate executables as per PATH and PATHEXT
   258  	exes, err = osutils.UniqueExes(exes, os.Getenv("PATHEXT"))
   259  	if err != nil {
   260  		return locale.WrapError(err, "err_unique_exes", "Could not detect unique executables, make sure your PATH and PATHEXT environment variables are properly configured.")
   261  	}
   262  
   263  	if rt.GOOS != "windows" {
   264  		// Symlink to PATH (eg. /usr/local/bin)
   265  		if err := symlinkWithTarget(overwrite, path, exes, d.output); err != nil {
   266  			return locale.WrapError(err, "err_symlink", "Could not create symlinks to {{.V0}}.", path)
   267  		}
   268  	} else {
   269  		d.output.Notice(locale.Tl("deploy_symlink_skip", "Skipped on Windows"))
   270  	}
   271  
   272  	return nil
   273  }
   274  
   275  // SymlinkTargetPath adds the .lnk file ending on windows
   276  func symlinkTargetPath(targetDir string, path string) string {
   277  	target := filepath.Clean(filepath.Join(targetDir, filepath.Base(path)))
   278  	if rt.GOOS != "windows" {
   279  		return target
   280  	}
   281  
   282  	oldExt := filepath.Ext(target)
   283  	return target[0:len(target)-len(oldExt)] + ".lnk"
   284  }
   285  
   286  // symlinkWithTarget creates symlinks in the target path of all executables found in the bins dir
   287  // It overwrites existing files, if the overwrite flag is set.
   288  // On Windows the same executable name can have several file extensions,
   289  // therefore executables are only symlinked if it has not been symlinked to a
   290  // target (with the same or a different extension) from a different directory.
   291  // Also: Only the executable with the highest priority according to pathExt is symlinked.
   292  func symlinkWithTarget(overwrite bool, symlinkPath string, exePaths []string, out output.Outputer) error {
   293  	out.Notice(output.Title(locale.Tr("deploy_symlink", symlinkPath)))
   294  
   295  	if err := fileutils.MkdirUnlessExists(symlinkPath); err != nil {
   296  		return locale.WrapExternalError(
   297  			err, "err_deploy_mkdir",
   298  			"Could not create directory at {{.V0}}, make sure you have permissions to write to {{.V1}}.", symlinkPath, filepath.Dir(symlinkPath))
   299  	}
   300  
   301  	for _, exePath := range exePaths {
   302  		symlink := symlinkTargetPath(symlinkPath, exePath)
   303  
   304  		// If the link already exists we may have to overwrite it, skip it, or fail..
   305  		if fileutils.TargetExists(symlink) {
   306  			// If the existing symlink already matches the one we want to create, skip it
   307  			skip, err := shouldSkipSymlink(symlink, exePath)
   308  			if err != nil {
   309  				return locale.WrapError(err, "err_deploy_shouldskip", "Could not determine if link already exists.")
   310  			}
   311  			if skip {
   312  				continue
   313  			}
   314  
   315  			// If we're trying to overwrite a link not owned by us but overwrite=false then we should fail
   316  			if !overwrite {
   317  				return locale.NewInputError(
   318  					"err_deploy_symlink_target_exists",
   319  					"Cannot create symlink as the target already exists: {{.V0}}. Use '--force' to overwrite any existing files.", symlink)
   320  			}
   321  
   322  			// We're about to overwrite, so if this link isn't owned by us we should let the user know
   323  			out.Notice(locale.Tr("deploy_overwrite_target", symlink))
   324  
   325  			// to overwrite the existing file, we have to remove it first, or the link command will fail
   326  			if err := os.Remove(symlink); err != nil {
   327  				return locale.WrapExternalError(
   328  					err, "err_deploy_overwrite",
   329  					"Could not overwrite {{.V0}}, make sure you have permissions to write to this file.", symlink)
   330  			}
   331  		}
   332  
   333  		if err := link(exePath, symlink); err != nil {
   334  			return err
   335  		}
   336  	}
   337  
   338  	return nil
   339  }
   340  
   341  type Report struct {
   342  	BinaryDirectories []string
   343  	Environment       map[string]string
   344  }
   345  
   346  func (d *Deploy) report(rtTarget setup.Targeter) error {
   347  	rti, err := runtime.New(rtTarget, d.analytics, d.svcModel, d.auth, d.cfg, d.output)
   348  	if err != nil {
   349  		return locale.WrapError(err, "deploy_runtime_err", "Could not initialize runtime")
   350  	}
   351  	if rti.NeedsUpdate() {
   352  		return locale.NewInputError("err_deploy_run_install")
   353  	}
   354  
   355  	env, err := rti.Env(false, false)
   356  	if err != nil {
   357  		return err
   358  	}
   359  
   360  	var bins []string
   361  	if path, ok := env["PATH"]; ok {
   362  		delete(env, "PATH")
   363  		bins = strings.Split(path, string(os.PathListSeparator))
   364  	}
   365  
   366  	d.output.Notice(output.Title(locale.T("deploy_info")))
   367  
   368  	d.output.Print(Report{
   369  		BinaryDirectories: bins,
   370  		Environment:       env,
   371  	})
   372  
   373  	d.output.Notice(output.Title(locale.T("deploy_restart")))
   374  
   375  	if rt.GOOS == "windows" {
   376  		d.output.Notice(locale.Tr("deploy_restart_cmd", filepath.Join(rtTarget.Dir(), "setenv.bat")))
   377  	} else {
   378  		d.output.Notice(locale.T("deploy_restart_shell"))
   379  	}
   380  
   381  	return nil
   382  }