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 }