github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/setup/setup.go (about) 1 package setup 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/url" 9 "os" 10 "path/filepath" 11 rt "runtime" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/ActiveState/cli/internal/analytics" 17 anaConsts "github.com/ActiveState/cli/internal/analytics/constants" 18 "github.com/ActiveState/cli/internal/analytics/dimensions" 19 "github.com/ActiveState/cli/internal/condition" 20 "github.com/ActiveState/cli/internal/constants" 21 "github.com/ActiveState/cli/internal/errs" 22 "github.com/ActiveState/cli/internal/fileutils" 23 "github.com/ActiveState/cli/internal/graph" 24 "github.com/ActiveState/cli/internal/httputil" 25 "github.com/ActiveState/cli/internal/locale" 26 "github.com/ActiveState/cli/internal/logging" 27 "github.com/ActiveState/cli/internal/multilog" 28 "github.com/ActiveState/cli/internal/output" 29 "github.com/ActiveState/cli/internal/proxyreader" 30 "github.com/ActiveState/cli/internal/rollbar" 31 "github.com/ActiveState/cli/internal/rtutils/ptr" 32 "github.com/ActiveState/cli/internal/runbits/dependencies" 33 "github.com/ActiveState/cli/internal/sliceutils" 34 "github.com/ActiveState/cli/internal/svcctl" 35 "github.com/ActiveState/cli/internal/unarchiver" 36 "github.com/ActiveState/cli/pkg/buildplan" 37 "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" 38 "github.com/ActiveState/cli/pkg/platform/authentication" 39 "github.com/ActiveState/cli/pkg/platform/model" 40 bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" 41 "github.com/ActiveState/cli/pkg/platform/runtime/artifactcache" 42 "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" 43 "github.com/ActiveState/cli/pkg/platform/runtime/buildscript" 44 "github.com/ActiveState/cli/pkg/platform/runtime/envdef" 45 "github.com/ActiveState/cli/pkg/platform/runtime/executors" 46 "github.com/ActiveState/cli/pkg/platform/runtime/setup/buildlog" 47 "github.com/ActiveState/cli/pkg/platform/runtime/setup/events" 48 "github.com/ActiveState/cli/pkg/platform/runtime/setup/events/progress" 49 "github.com/ActiveState/cli/pkg/platform/runtime/setup/implementations/alternative" 50 "github.com/ActiveState/cli/pkg/platform/runtime/setup/implementations/camel" 51 "github.com/ActiveState/cli/pkg/platform/runtime/store" 52 "github.com/ActiveState/cli/pkg/platform/runtime/target" 53 "github.com/ActiveState/cli/pkg/platform/runtime/validate" 54 "github.com/ActiveState/cli/pkg/sysinfo" 55 "github.com/faiface/mainthread" 56 "github.com/gammazero/workerpool" 57 "github.com/go-openapi/strfmt" 58 ) 59 60 // MaxConcurrency is maximum number of parallel artifact installations 61 const MaxConcurrency = 5 62 63 // NotInstalledError is an error returned when the runtime is not completely installed yet. 64 var NotInstalledError = errs.New("Runtime is not completely installed.") 65 66 // BuildError designates a recipe build error. 67 type BuildError struct { 68 *locale.LocalizedError 69 } 70 71 // ArtifactDownloadError designates an error downloading an artifact. 72 type ArtifactDownloadError struct { 73 *errs.WrapperError 74 } 75 76 // ArtifactCachedBuildFailed designates an error due to a build for an artifact that failed and has been cached 77 type ArtifactCachedBuildFailed struct { 78 *errs.WrapperError 79 Artifact *buildplan.Artifact 80 } 81 82 // ArtifactInstallError designates an error installing a downloaded artifact. 83 type ArtifactInstallError struct { 84 *errs.WrapperError 85 } 86 87 // ArtifactSetupErrors combines all errors that can happen while installing artifacts in parallel 88 type ArtifactSetupErrors struct { 89 errs []error 90 } 91 92 type ExecutorSetupError struct { 93 *errs.WrapperError 94 } 95 96 func (a *ArtifactSetupErrors) Error() string { 97 var errors []string 98 for _, err := range a.errs { 99 errors = append(errors, errs.JoinMessage(err)) 100 } 101 return "Not all artifacts could be installed, errors:\n" + strings.Join(errors, "\n") 102 } 103 104 func (a *ArtifactSetupErrors) Unwrap() []error { 105 return a.errs 106 } 107 108 // Errors returns the individual error messages collected from all failing artifact installations 109 func (a *ArtifactSetupErrors) Errors() []error { 110 return a.errs 111 } 112 113 // UserError returns a message including all user-facing sub-error messages 114 func (a *ArtifactSetupErrors) LocalizedError() string { 115 var errStrings []string 116 for _, err := range a.errs { 117 errStrings = append(errStrings, locale.JoinedErrorMessage(err)) 118 } 119 return locale.Tl("setup_artifacts_err", "Not all artifacts could be installed:\n{{.V0}}", strings.Join(errStrings, "\n")) 120 } 121 122 // ProgressReportError designates an error in the event handler for reporting progress. 123 type ProgressReportError struct { 124 *errs.WrapperError 125 } 126 127 type RuntimeInUseError struct { 128 *locale.LocalizedError 129 Processes []*graph.ProcessInfo 130 } 131 132 type Targeter interface { 133 CommitUUID() strfmt.UUID 134 Name() string 135 Owner() string 136 Dir() string 137 Trigger() target.Trigger 138 ProjectDir() string 139 140 // ReadOnly communicates that this target should only use cached runtime information (ie. don't check for updates) 141 ReadOnly() bool 142 // InstallFromDir communicates that this target should only install artifacts from the given directory (i.e. offline installer) 143 InstallFromDir() *string 144 } 145 146 type Configurable interface { 147 GetString(key string) string 148 GetBool(key string) bool 149 } 150 151 type Setup struct { 152 auth *authentication.Auth 153 target Targeter 154 eventHandler events.Handler 155 store *store.Store 156 analytics analytics.Dispatcher 157 artifactCache *artifactcache.ArtifactCache 158 cfg Configurable 159 out output.Outputer 160 svcm *model.SvcModel 161 } 162 163 type Setuper interface { 164 // DeleteOutdatedArtifacts deletes outdated artifact as best as it can 165 DeleteOutdatedArtifacts(*buildplan.ArtifactChangeset, store.StoredArtifactMap, store.StoredArtifactMap) error 166 } 167 168 // ArtifactSetuper is the interface for an implementation of artifact setup functions 169 // These need to be specialized for each BuildEngine type 170 type ArtifactSetuper interface { 171 EnvDef(tmpInstallDir string) (*envdef.EnvironmentDefinition, error) 172 Unarchiver() unarchiver.Unarchiver 173 } 174 175 type ArtifactResolver interface { 176 ResolveArtifactName(strfmt.UUID) string 177 } 178 179 type artifactInstaller func(strfmt.UUID, string, ArtifactSetuper) error 180 type artifactUninstaller func() error 181 182 // New returns a new Setup instance that can install a Runtime locally on the machine. 183 func New(target Targeter, eventHandler events.Handler, auth *authentication.Auth, an analytics.Dispatcher, cfg Configurable, out output.Outputer, svcm *model.SvcModel) *Setup { 184 cache, err := artifactcache.New() 185 if err != nil { 186 multilog.Error("Could not create artifact cache: %v", err) 187 } 188 return &Setup{auth, target, eventHandler, store.New(target.Dir()), an, cache, cfg, out, svcm} 189 } 190 191 func (s *Setup) Solve() (*bpModel.Commit, error) { 192 defer func() { 193 s.solveUpdateRecover(recover()) 194 }() 195 196 if s.target.InstallFromDir() != nil { 197 return nil, nil 198 } 199 200 if err := s.handleEvent(events.SolveStart{}); err != nil { 201 return nil, errs.Wrap(err, "Could not handle SolveStart event") 202 } 203 204 bpm := bpModel.NewBuildPlannerModel(s.auth) 205 commit, err := bpm.FetchCommit(s.target.CommitUUID(), s.target.Owner(), s.target.Name(), nil) 206 if err != nil { 207 return nil, errs.Wrap(err, "Failed to fetch build result") 208 } 209 210 if err := s.eventHandler.Handle(events.SolveSuccess{}); err != nil { 211 return nil, errs.Wrap(err, "Could not handle SolveSuccess event") 212 } 213 214 return commit, nil 215 } 216 217 func (s *Setup) Update(commit *bpModel.Commit) (rerr error) { 218 defer func() { 219 s.solveUpdateRecover(recover()) 220 }() 221 defer func() { 222 var ev events.Eventer = events.Success{} 223 if rerr != nil { 224 ev = events.Failure{} 225 } 226 227 err := s.handleEvent(ev) 228 if err != nil { 229 multilog.Error("Could not handle Success/Failure event: %s", errs.JoinMessage(err)) 230 } 231 }() 232 233 bp := commit.BuildPlan() 234 235 // Do not allow users to deploy runtimes to the root directory (this can easily happen in docker 236 // images). Note that runtime targets are fully resolved via fileutils.ResolveUniquePath(), so 237 // paths like "/." and "/opt/.." resolve to simply "/" at this time. 238 if rt.GOOS != "windows" && s.target.Dir() == "/" { 239 return locale.NewInputError("err_runtime_setup_root", "Cannot set up a runtime in the root directory. Please specify or run from a user-writable directory.") 240 } 241 242 // Determine if this runtime is currently in use. 243 ctx, cancel := context.WithTimeout(context.Background(), model.SvcTimeoutMinimal) 244 defer cancel() 245 if procs, err := s.svcm.GetProcessesInUse(ctx, ExecDir(s.target.Dir())); err == nil { 246 if len(procs) > 0 { 247 list := []string{} 248 for _, proc := range procs { 249 list = append(list, fmt.Sprintf(" - %s (process: %d)", proc.Exe, proc.Pid)) 250 } 251 return &RuntimeInUseError{locale.NewInputError("runtime_setup_in_use_err", "", strings.Join(list, "\n")), procs} 252 } 253 } else { 254 multilog.Error("Unable to determine if runtime is in use: %v", errs.JoinMessage(err)) 255 } 256 257 // Update all the runtime artifacts 258 artifacts, err := s.updateArtifacts(bp) 259 if err != nil { 260 return errs.Wrap(err, "Failed to update artifacts") 261 } 262 263 if err := s.store.StoreBuildPlan(bp); err != nil { 264 return errs.Wrap(err, "Could not save recipe file.") 265 } 266 267 expression, err := buildexpression.New(commit.Expression) 268 if err != nil { 269 return errs.Wrap(err, "failed to parse build expression") 270 } 271 272 script, err := buildscript.NewFromBuildExpression(&commit.AtTime, expression) 273 if err != nil { 274 return errs.Wrap(err, "Could not convert to buildscript") 275 } 276 277 if err := s.store.StoreBuildScript(script); err != nil { 278 return errs.Wrap(err, "Could not store buildscript file.") 279 } 280 281 if s.target.ProjectDir() != "" && s.cfg.GetBool(constants.OptinBuildscriptsConfig) { 282 if err := buildscript.Update(s.target, &commit.AtTime, expression); err != nil { 283 return errs.Wrap(err, "Could not update build script") 284 } 285 } 286 287 // Update executors 288 if err := s.updateExecutors(artifacts); err != nil { 289 return ExecutorSetupError{errs.Wrap(err, "Failed to update executors")} 290 } 291 292 // Mark installation as completed 293 if err := s.store.MarkInstallationComplete(s.target.CommitUUID(), fmt.Sprintf("%s/%s", s.target.Owner(), s.target.Name())); err != nil { 294 return errs.Wrap(err, "Could not mark install as complete.") 295 } 296 297 return nil 298 } 299 300 // Panics are serious, and reproducing them in the runtime package is HARD. To help with this we dump 301 // the build plan when a panic occurs so we have something more to go on. 302 func (s *Setup) solveUpdateRecover(r interface{}) { 303 if r == nil { 304 return 305 } 306 307 multilog.Critical("Panic during runtime update: %s", r) 308 panic(r) // We're just logging the panic while we have context, we're not meant to handle it here 309 } 310 311 func (s *Setup) updateArtifacts(bp *buildplan.BuildPlan) ([]strfmt.UUID, error) { 312 mutex := &sync.Mutex{} 313 var installArtifactFuncs []func() error 314 315 // Fetch and install each runtime artifact. 316 // Note: despite the name, we are "pre-installing" the artifacts to a temporary location. 317 // Once all artifacts are fetched, unpacked, and prepared, final installation occurs. 318 artifacts, uninstallFunc, err := s.fetchAndInstallArtifacts(bp, func(a strfmt.UUID, archivePath string, as ArtifactSetuper) (rerr error) { 319 defer func() { 320 if rerr != nil { 321 rerr = &ArtifactInstallError{errs.Wrap(rerr, "Unable to install artifact")} 322 if err := s.handleEvent(events.ArtifactInstallFailure{a, rerr}); err != nil { 323 rerr = errs.Wrap(rerr, "Could not handle ArtifactInstallFailure event") 324 return 325 } 326 } 327 if err := s.handleEvent(events.ArtifactInstallSuccess{a}); err != nil { 328 rerr = errs.Wrap(rerr, "Could not handle ArtifactInstallSuccess event") 329 return 330 } 331 }() 332 333 // Set up target and unpack directories 334 targetDir := filepath.Join(s.store.InstallPath(), constants.LocalRuntimeTempDirectory) 335 if err := fileutils.MkdirUnlessExists(targetDir); err != nil { 336 return errs.Wrap(err, "Could not create temp runtime dir") 337 } 338 unpackedDir := filepath.Join(targetDir, a.String()) 339 340 logging.Debug("Unarchiving %s to %s", archivePath, unpackedDir) 341 342 // ensure that the unpack dir is empty 343 err := os.RemoveAll(unpackedDir) 344 if err != nil { 345 return errs.Wrap(err, "Could not remove previous temporary installation directory.") 346 } 347 348 // Unpack artifact archive 349 numFiles, err := s.unpackArtifact(as.Unarchiver(), archivePath, unpackedDir, &progress.Report{ 350 ReportSizeCb: func(size int) error { 351 if err := s.handleEvent(events.ArtifactInstallStarted{a, size}); err != nil { 352 return errs.Wrap(err, "Could not handle ArtifactInstallStarted event") 353 } 354 return nil 355 }, 356 ReportIncrementCb: func(inc int) error { 357 if err := s.handleEvent(events.ArtifactInstallProgress{a, inc}); err != nil { 358 return errs.Wrap(err, "Could not handle ArtifactInstallProgress event") 359 } 360 return nil 361 }, 362 }) 363 if err != nil { 364 err := errs.Wrap(err, "Could not unpack artifact %s", archivePath) 365 return err 366 } 367 368 // Set up constants used to expand environment definitions 369 cnst, err := envdef.NewConstants(s.store.InstallPath()) 370 if err != nil { 371 return errs.Wrap(err, "Could not get new environment constants") 372 } 373 374 // Retrieve environment definitions for artifact 375 envDef, err := as.EnvDef(unpackedDir) 376 if err != nil { 377 return errs.Wrap(err, "Could not collect env info for artifact") 378 } 379 380 // Expand environment definitions using constants 381 envDef = envDef.ExpandVariables(cnst) 382 err = envDef.ApplyFileTransforms(filepath.Join(unpackedDir, envDef.InstallDir), cnst) 383 if err != nil { 384 return locale.WrapError(err, "runtime_alternative_file_transforms_err", "", "Could not apply necessary file transformations after unpacking") 385 } 386 387 mutex.Lock() 388 installArtifactFuncs = append(installArtifactFuncs, func() error { 389 return s.moveToInstallPath(a, unpackedDir, envDef, numFiles) 390 }) 391 mutex.Unlock() 392 393 return nil 394 }) 395 if err != nil { 396 return artifacts, locale.WrapError(err, "err_runtime_setup") 397 } 398 399 if os.Getenv(constants.RuntimeSetupWaitEnvVarName) != "" && (condition.OnCI() || condition.BuiltOnDevMachine()) { 400 // This code block is for integration testing purposes only. 401 // Under normal conditions, we should never access fmt or os.Stdin from this context. 402 fmt.Printf("Waiting for input because %s was set\n", constants.RuntimeSetupWaitEnvVarName) 403 ch := make([]byte, 1) 404 _, err = os.Stdin.Read(ch) // block until input is sent 405 if err != nil { 406 return artifacts, locale.WrapError(err, "err_runtime_setup") 407 } 408 } 409 410 // Uninstall outdated artifacts. 411 // This must come before calling any installArtifactFuncs or else the runtime may become corrupt. 412 if uninstallFunc != nil { 413 err := uninstallFunc() 414 if err != nil { 415 return artifacts, locale.WrapError(err, "err_runtime_setup") 416 } 417 } 418 419 // Move files to final installation path after successful download and unpack. 420 for _, f := range installArtifactFuncs { 421 err := f() 422 if err != nil { 423 return artifacts, locale.WrapError(err, "err_runtime_setup") 424 } 425 } 426 427 // Clean up temp directory. 428 tempDir := filepath.Join(s.store.InstallPath(), constants.LocalRuntimeTempDirectory) 429 err = os.RemoveAll(tempDir) 430 if err != nil { 431 multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Failed to remove temporary installation directory %s: %v", tempDir, err) 432 } 433 434 return artifacts, nil 435 } 436 437 func (s *Setup) updateExecutors(artifacts []strfmt.UUID) error { 438 execPath := ExecDir(s.target.Dir()) 439 if err := fileutils.MkdirUnlessExists(execPath); err != nil { 440 return locale.WrapError(err, "err_deploy_execpath", "Could not create exec directory.") 441 } 442 443 edGlobal, err := s.store.UpdateEnviron(artifacts) 444 if err != nil { 445 return errs.Wrap(err, "Could not save combined environment file") 446 } 447 448 exePaths, err := edGlobal.ExecutablePaths() 449 if err != nil { 450 return locale.WrapError(err, "err_deploy_execpaths", "Could not retrieve runtime executable paths") 451 } 452 453 env, err := s.store.Environ(false) 454 if err != nil { 455 return locale.WrapError(err, "err_setup_get_runtime_env", "Could not retrieve runtime environment") 456 } 457 458 execInit := executors.New(execPath) 459 if err := execInit.Apply(svcctl.NewIPCSockPathFromGlobals().String(), s.target, env, exePaths); err != nil { 460 return locale.WrapError(err, "err_deploy_executors", "Could not create executors") 461 } 462 463 return nil 464 } 465 466 // fetchAndInstallArtifacts returns all artifacts needed by the runtime, even if some or 467 // all of them were already installed. 468 // It may also return an artifact uninstaller function that should be run prior to final 469 // installation. 470 func (s *Setup) fetchAndInstallArtifacts(bp *buildplan.BuildPlan, installFunc artifactInstaller) ([]strfmt.UUID, artifactUninstaller, error) { 471 if s.target.InstallFromDir() != nil { 472 artifacts, err := s.fetchAndInstallArtifactsFromDir(installFunc) 473 return artifacts, nil, err 474 } 475 return s.fetchAndInstallArtifactsFromBuildPlan(bp, installFunc) 476 } 477 478 func (s *Setup) fetchAndInstallArtifactsFromBuildPlan(bp *buildplan.BuildPlan, installFunc artifactInstaller) ([]strfmt.UUID, artifactUninstaller, error) { 479 // If the build is not ready or if we are installing the buildtime closure 480 // then we need to include the buildtime closure in the changed artifacts 481 // and the progress reporting. 482 includeBuildtimeClosure := strings.EqualFold(os.Getenv(constants.InstallBuildDependencies), "true") || !bp.IsBuildReady() 483 484 platformID, err := model.FilterCurrentPlatform(sysinfo.OS().String(), bp.Platforms(), s.cfg) 485 if err != nil { 486 return nil, nil, locale.WrapError(err, "err_filter_current_platform") 487 } 488 489 artifactFilters := []buildplan.FilterArtifact{ 490 buildplan.FilterStateArtifacts(), 491 buildplan.FilterPlatformArtifacts(platformID), 492 } 493 494 // Compute and handle the change summary 495 allArtifacts := bp.Artifacts(artifactFilters...) 496 497 // Detect failed artifacts early 498 for _, a := range allArtifacts { 499 var aErr error 500 if a.Status == types.ArtifactFailedPermanently || a.Status == types.ArtifactFailedTransiently { 501 errV := &ArtifactCachedBuildFailed{errs.New("artifact failed, status: %s", a.Status), a} 502 if aErr == nil { 503 aErr = errV 504 } else { 505 aErr = errs.Pack(aErr, errV) 506 } 507 } 508 if aErr != nil { 509 return nil, nil, aErr 510 } 511 } 512 513 if len(allArtifacts) == 0 { 514 v, err := json.Marshal(bp.Artifacts()) 515 if err != nil { 516 return nil, nil, err 517 } 518 return nil, nil, errs.New("did not find any artifacts that match our platform (%s), full artifacts list: %s", platformID, v) 519 } 520 521 resolver, err := selectArtifactResolver(bp) 522 if err != nil { 523 return nil, nil, errs.Wrap(err, "Failed to select artifact resolver") 524 } 525 526 // build results don't have namespace info and will happily report internal only artifacts 527 downloadablePrebuiltArtifacts := sliceutils.Filter(allArtifacts, func(a *buildplan.Artifact) bool { 528 return a.Status == types.ArtifactSucceeded && a.URL != "" 529 }) 530 531 // Analytics data to send. 532 dimensions := &dimensions.Values{ 533 CommitID: ptr.To(s.target.CommitUUID().String()), 534 } 535 536 // send analytics build event, if a new runtime has to be built in the cloud 537 if bp.IsBuildInProgress() { 538 s.analytics.Event(anaConsts.CatRuntimeDebug, anaConsts.ActRuntimeBuild, dimensions) 539 } 540 541 oldBuildPlan, err := s.store.BuildPlan() 542 if err != nil && !errors.As(err, ptr.To(&store.ErrVersionMarker{})) { 543 return nil, nil, errs.Wrap(err, "could not load existing build plan") 544 } 545 546 var oldBuildPlanArtifacts buildplan.Artifacts 547 var changedArtifacts *buildplan.ArtifactChangeset 548 if oldBuildPlan != nil { 549 oldBuildPlanArtifacts = oldBuildPlan.Artifacts(artifactFilters...) 550 changedArtifacts = ptr.To(bp.DiffArtifacts(oldBuildPlan, true)) 551 } 552 553 storedArtifacts, err := s.store.Artifacts() 554 if err != nil { 555 return nil, nil, locale.WrapError(err, "err_stored_artifacts") 556 } 557 558 alreadyInstalled := reusableArtifacts(allArtifacts, storedArtifacts) 559 560 artifactNamesList := []string{} 561 for _, a := range allArtifacts { 562 artifactNamesList = append(artifactNamesList, a.Name()) 563 } 564 installedList := []string{} 565 for _, a := range alreadyInstalled { 566 installedList = append(installedList, resolver.ResolveArtifactName(a.ArtifactID)) 567 } 568 downloadList := []string{} 569 for _, a := range downloadablePrebuiltArtifacts { 570 downloadList = append(downloadList, resolver.ResolveArtifactName(a.ArtifactID)) 571 } 572 logging.Debug( 573 "Parsed artifacts.\nBuild ready: %v\nArtifact names: %v\nAlready installed: %v\nTo Download: %v", 574 bp.IsBuildReady(), artifactNamesList, installedList, downloadList, 575 ) 576 577 filterNeedsInstall := func(a *buildplan.Artifact) bool { 578 _, alreadyInstalled := alreadyInstalled[a.ArtifactID] 579 return !alreadyInstalled 580 } 581 filters := []buildplan.FilterArtifact{filterNeedsInstall} 582 if !includeBuildtimeClosure { 583 filters = append(filters, buildplan.FilterRuntimeArtifacts()) 584 } 585 artifactsToInstall := allArtifacts.Filter(filters...) 586 if err != nil { 587 return nil, nil, errs.Wrap(err, "Failed to compute artifacts to build") 588 } 589 590 // Output a dependency summary if applicable. 591 if s.target.Trigger() == target.TriggerCheckout { 592 dependencies.OutputSummary(s.out, bp.RequestedArtifacts()) 593 } else if s.target.Trigger() == target.TriggerInit { 594 artifacts := bp.Artifacts().Filter(buildplan.FilterStateArtifacts(), buildplan.FilterRuntimeArtifacts()) 595 dependencies.OutputSummary(s.out, artifacts) 596 } else if len(oldBuildPlanArtifacts) > 0 && changedArtifacts != nil { 597 dependencies.OutputChangeSummary(s.out, changedArtifacts, oldBuildPlanArtifacts) 598 } 599 600 // The log file we want to use for builds 601 logFilePath := logging.FilePathFor(fmt.Sprintf("build-%s.log", s.target.CommitUUID().String()+"-"+time.Now().Format("20060102150405"))) 602 603 recipeID, err := bp.RecipeID() 604 if err != nil { 605 return nil, nil, errs.Wrap(err, "Could not get recipe ID from build plan") 606 } 607 608 artifactNameMap := map[strfmt.UUID]string{} 609 for _, a := range allArtifacts { 610 artifactNameMap[a.ArtifactID] = a.Name() 611 } 612 613 if err := s.eventHandler.Handle(events.Start{ 614 RecipeID: recipeID, 615 RequiresBuild: bp.IsBuildInProgress(), 616 Artifacts: artifactNameMap, 617 LogFilePath: logFilePath, 618 ArtifactsToBuild: allArtifacts.ToIDSlice(), 619 // Yes these have the same value; this is intentional. 620 // Separating these out just allows us to be more explicit and intentional in our event handling logic. 621 ArtifactsToDownload: artifactsToInstall.ToIDSlice(), 622 ArtifactsToInstall: artifactsToInstall.ToIDSlice(), 623 }); err != nil { 624 return nil, nil, errs.Wrap(err, "Could not handle Start event") 625 } 626 627 var uninstallArtifacts artifactUninstaller = func() error { 628 setup, err := s.selectSetupImplementation(bp.Engine()) 629 if err != nil { 630 return errs.Wrap(err, "Failed to select setup implementation") 631 } 632 return s.deleteOutdatedArtifacts(setup, changedArtifacts, alreadyInstalled) 633 } 634 635 // only send the download analytics event, if we have to install artifacts that are not yet installed 636 if len(artifactsToInstall) > 0 { 637 // if we get here, we download artifacts 638 s.analytics.Event(anaConsts.CatRuntimeDebug, anaConsts.ActRuntimeDownload, dimensions) 639 } 640 641 err = s.installArtifactsFromBuild(bp.IsBuildReady(), bp.Engine(), recipeID, artifactsToInstall, installFunc, logFilePath) 642 if err != nil { 643 return nil, nil, err 644 } 645 err = s.artifactCache.Save() 646 if err != nil { 647 multilog.Error("Could not save artifact cache updates: %v", err) 648 } 649 650 artifactIDs := allArtifacts.ToIDSlice() 651 logging.Debug("Returning artifacts: %v", artifactIDs) 652 return artifactIDs, uninstallArtifacts, nil 653 } 654 655 func aggregateErrors() (chan<- error, <-chan error) { 656 aggErr := make(chan error) 657 bgErrs := make(chan error) 658 go func() { 659 var errs []error 660 for err := range bgErrs { 661 errs = append(errs, err) 662 } 663 664 if len(errs) > 0 { 665 aggErr <- &ArtifactSetupErrors{errs} 666 } else { 667 aggErr <- nil 668 } 669 }() 670 671 return bgErrs, aggErr 672 } 673 674 func (s *Setup) installArtifactsFromBuild(isReady bool, engine types.BuildEngine, recipeID strfmt.UUID, artifacts buildplan.Artifacts, installFunc artifactInstaller, logFilePath string) error { 675 // Artifacts are installed in two stages 676 // - The first stage runs concurrently in MaxConcurrency worker threads (download, unpacking, relocation) 677 // - The second stage moves all files into its final destination is running in a single thread (using the mainthread library) to avoid file conflicts 678 679 var err error 680 if isReady { 681 logging.Debug("Installing via build result") 682 if err := s.handleEvent(events.BuildSkipped{}); err != nil { 683 return errs.Wrap(err, "Could not handle BuildSkipped event") 684 } 685 err = s.installFromBuildResult(engine, artifacts, installFunc) 686 if err != nil { 687 err = errs.Wrap(err, "Installing via build result failed") 688 } 689 } else { 690 logging.Debug("Installing via buildlog streamer") 691 err = s.installFromBuildLog(engine, recipeID, artifacts, installFunc, logFilePath) 692 if err != nil { 693 err = errs.Wrap(err, "Installing via buildlog streamer failed") 694 } 695 } 696 697 return err 698 } 699 700 // setupArtifactSubmitFunction returns a function that sets up an artifact and can be submitted to a workerpool 701 func (s *Setup) setupArtifactSubmitFunction( 702 engine types.BuildEngine, 703 ar *buildplan.Artifact, 704 installFunc artifactInstaller, 705 errors chan<- error, 706 ) func() { 707 return func() { 708 as, err := s.selectArtifactSetupImplementation(engine, ar.ArtifactID) 709 if err != nil { 710 errors <- errs.Wrap(err, "Failed to select artifact setup implementation") 711 return 712 } 713 714 unarchiver := as.Unarchiver() 715 archivePath, err := s.obtainArtifact(ar, unarchiver.Ext()) 716 if err != nil { 717 errors <- locale.WrapError(err, "artifact_download_failed", "", ar.Name(), ar.ArtifactID.String()) 718 return 719 } 720 721 err = installFunc(ar.ArtifactID, archivePath, as) 722 if err != nil { 723 errors <- locale.WrapError(err, "artifact_setup_failed", "", ar.Name(), ar.ArtifactID.String()) 724 return 725 } 726 } 727 } 728 729 func (s *Setup) installFromBuildResult(engine types.BuildEngine, artifacts buildplan.Artifacts, installFunc artifactInstaller) error { 730 logging.Debug("Installing artifacts from build result") 731 errs, aggregatedErr := aggregateErrors() 732 mainthread.Run(func() { 733 defer close(errs) 734 wp := workerpool.New(MaxConcurrency) 735 for _, a := range artifacts { 736 wp.Submit(s.setupArtifactSubmitFunction(engine, a, installFunc, errs)) 737 } 738 739 wp.StopWait() 740 }) 741 742 return <-aggregatedErr 743 } 744 745 func (s *Setup) installFromBuildLog(engine types.BuildEngine, recipeID strfmt.UUID, artifacts buildplan.Artifacts, installFunc artifactInstaller, logFilePath string) error { 746 ctx, cancel := context.WithCancel(context.Background()) 747 defer cancel() 748 749 buildLog, err := buildlog.New(ctx, artifacts.ToIDMap(), s.eventHandler, recipeID, logFilePath) 750 if err != nil { 751 return errs.Wrap(err, "Cannot establish connection with BuildLog") 752 } 753 defer func() { 754 if err := buildLog.Close(); err != nil { 755 logging.Debug("Failed to close build log: %v", errs.JoinMessage(err)) 756 } 757 }() 758 759 errs, aggregatedErr := aggregateErrors() 760 761 mainthread.Run(func() { 762 defer close(errs) 763 764 var wg sync.WaitGroup 765 defer wg.Wait() 766 wg.Add(1) 767 go func() { 768 // wp.StopWait needs to be run in this go-routine after ALL tasks are scheduled, hence we need to add an extra wait group 769 defer wg.Done() 770 wp := workerpool.New(MaxConcurrency) 771 defer wp.StopWait() 772 773 for a := range buildLog.BuiltArtifactsChannel() { 774 wp.Submit(s.setupArtifactSubmitFunction(engine, a, installFunc, errs)) 775 } 776 }() 777 778 if err = buildLog.Wait(); err != nil { 779 errs <- err 780 } 781 }) 782 783 return <-aggregatedErr 784 } 785 786 func (s *Setup) moveToInstallPath(a strfmt.UUID, unpackedDir string, envDef *envdef.EnvironmentDefinition, numFiles int) error { 787 // clean up the unpacked dir 788 defer os.RemoveAll(unpackedDir) 789 790 var files []string 791 var dirs []string 792 onMoveFile := func(fromPath, toPath string) { 793 if fileutils.IsDir(toPath) { 794 dirs = append(dirs, toPath) 795 } else { 796 files = append(files, toPath) 797 } 798 } 799 err := fileutils.MoveAllFilesRecursively( 800 filepath.Join(unpackedDir, envDef.InstallDir), 801 s.store.InstallPath(), onMoveFile, 802 ) 803 if err != nil { 804 err := errs.Wrap(err, "Move artifact failed") 805 return err 806 } 807 808 if err := s.store.StoreArtifact(store.NewStoredArtifact(a, files, dirs, envDef)); err != nil { 809 return errs.Wrap(err, "Could not store artifact meta info") 810 } 811 812 return nil 813 } 814 815 // downloadArtifact downloads the given artifact 816 func (s *Setup) downloadArtifact(a *buildplan.Artifact, targetFile string) (rerr error) { 817 defer func() { 818 if rerr != nil { 819 if !errs.Matches(rerr, &ProgressReportError{}) { 820 rerr = &ArtifactDownloadError{errs.Wrap(rerr, "Unable to download artifact")} 821 } 822 823 if err := s.handleEvent(events.ArtifactDownloadFailure{a.ArtifactID, rerr}); err != nil { 824 rerr = errs.Wrap(rerr, "Could not handle ArtifactDownloadFailure event") 825 return 826 } 827 } 828 829 if err := s.handleEvent(events.ArtifactDownloadSuccess{a.ArtifactID}); err != nil { 830 rerr = errs.Wrap(rerr, "Could not handle ArtifactDownloadSuccess event") 831 return 832 } 833 }() 834 835 if a.URL == "" { 836 return errs.New("Artifact URL is empty: %+v", a) 837 } 838 839 artifactURL, err := url.Parse(a.URL) 840 if err != nil { 841 return errs.Wrap(err, "Could not parse artifact URL %s.", a.URL) 842 } 843 844 b, err := httputil.GetWithProgress(artifactURL.String(), &progress.Report{ 845 ReportSizeCb: func(size int) error { 846 if err := s.handleEvent(events.ArtifactDownloadStarted{a.ArtifactID, size}); err != nil { 847 return ProgressReportError{errs.Wrap(err, "Could not handle ArtifactDownloadStarted event")} 848 } 849 return nil 850 }, 851 ReportIncrementCb: func(inc int) error { 852 if err := s.handleEvent(events.ArtifactDownloadProgress{a.ArtifactID, inc}); err != nil { 853 return errs.Wrap(err, "Could not handle ArtifactDownloadProgress event") 854 } 855 return nil 856 }, 857 }) 858 if err != nil { 859 return errs.Wrap(err, "Download %s failed", artifactURL.String()) 860 } 861 862 if err := fileutils.WriteFile(targetFile, b); err != nil { 863 return errs.Wrap(err, "Writing download to target file %s failed", targetFile) 864 } 865 866 return nil 867 } 868 869 // verifyArtifact verifies the checksum of the downloaded artifact matches the checksum given by the 870 // platform, and returns an error if the verification fails. 871 func (s *Setup) verifyArtifact(archivePath string, a *buildplan.Artifact) error { 872 return validate.Checksum(archivePath, a.Checksum) 873 } 874 875 // obtainArtifact obtains an artifact and returns the local path to that artifact's archive. 876 func (s *Setup) obtainArtifact(a *buildplan.Artifact, extension string) (string, error) { 877 if cachedPath, found := s.artifactCache.Get(a.ArtifactID); found { 878 if err := s.verifyArtifact(cachedPath, a); err == nil { 879 if err := s.handleEvent(events.ArtifactDownloadSkipped{a.ArtifactID}); err != nil { 880 return "", errs.Wrap(err, "Could not handle ArtifactDownloadSkipped event") 881 } 882 return cachedPath, nil 883 } 884 // otherwise re-download it; do not return an error 885 } 886 887 targetDir := filepath.Join(s.store.InstallPath(), constants.LocalRuntimeTempDirectory) 888 if err := fileutils.MkdirUnlessExists(targetDir); err != nil { 889 return "", errs.Wrap(err, "Could not create temp runtime dir") 890 } 891 892 archivePath := filepath.Join(targetDir, a.ArtifactID.String()+extension) 893 if err := s.downloadArtifact(a, archivePath); err != nil { 894 return "", errs.Wrap(err, "Could not download artifact %s", a.URL) 895 } 896 897 err := s.verifyArtifact(archivePath, a) 898 if err != nil { 899 return "", errs.Wrap(err, "Artifact checksum validation failed") 900 } 901 902 err = s.artifactCache.Store(a.ArtifactID, archivePath) 903 if err != nil { 904 multilog.Error("Could not store artifact in cache: %v", err) 905 } 906 907 return archivePath, nil 908 } 909 910 func (s *Setup) unpackArtifact(ua unarchiver.Unarchiver, tarballPath string, targetDir string, progress progress.Reporter) (int, error) { 911 f, i, err := ua.PrepareUnpacking(tarballPath, targetDir) 912 if err != nil { 913 return 0, errs.Wrap(err, "Prepare for unpacking failed") 914 } 915 defer f.Close() 916 917 if err := progress.ReportSize(int(i)); err != nil { 918 return 0, errs.Wrap(err, "Could not report size") 919 } 920 921 var numUnpackedFiles int 922 ua.SetNotifier(func(_ string, _ int64, isDir bool) { 923 if !isDir { 924 numUnpackedFiles++ 925 } 926 }) 927 proxy := proxyreader.NewProxyReader(progress, f) 928 return numUnpackedFiles, ua.Unarchive(proxy, i, targetDir) 929 } 930 931 func (s *Setup) selectSetupImplementation(buildEngine types.BuildEngine) (Setuper, error) { 932 switch buildEngine { 933 case types.Alternative: 934 return alternative.NewSetup(s.store), nil 935 case types.Camel: 936 return camel.NewSetup(s.store), nil 937 default: 938 return nil, errs.New("Unknown build engine: %s", buildEngine) 939 } 940 } 941 942 func selectArtifactResolver(bp *buildplan.BuildPlan) (ArtifactResolver, error) { 943 switch bp.Engine() { 944 case types.Alternative: 945 return alternative.NewResolver(bp.Artifacts().ToIDMap()), nil 946 case types.Camel: 947 return camel.NewResolver(), nil 948 default: 949 return nil, errs.New("Unknown build engine: %s", bp.Engine()) 950 } 951 } 952 953 func (s *Setup) selectArtifactSetupImplementation(buildEngine types.BuildEngine, a strfmt.UUID) (ArtifactSetuper, error) { 954 switch buildEngine { 955 case types.Alternative: 956 return alternative.NewArtifactSetup(a, s.store), nil 957 case types.Camel: 958 return camel.NewArtifactSetup(a, s.store), nil 959 default: 960 return nil, errs.New("Unknown build engine: %s", buildEngine) 961 } 962 } 963 964 func ExecDir(targetDir string) string { 965 return filepath.Join(targetDir, "exec") 966 } 967 968 func reusableArtifacts(requestedArtifacts []*buildplan.Artifact, storedArtifacts store.StoredArtifactMap) store.StoredArtifactMap { 969 keep := make(store.StoredArtifactMap) 970 971 for _, a := range requestedArtifacts { 972 if v, ok := storedArtifacts[a.ArtifactID]; ok { 973 keep[a.ArtifactID] = v 974 } 975 } 976 return keep 977 } 978 979 func (s *Setup) fetchAndInstallArtifactsFromDir(installFunc artifactInstaller) ([]strfmt.UUID, error) { 980 artifactsDir := s.target.InstallFromDir() 981 if artifactsDir == nil { 982 return nil, errs.New("Cannot install from a directory that is nil") 983 } 984 985 artifacts, err := fileutils.ListDir(*artifactsDir, false) 986 if err != nil { 987 return nil, errs.Wrap(err, "Cannot read from directory to install from") 988 } 989 logging.Debug("Found %d artifacts to install from '%s'", len(artifacts), *artifactsDir) 990 991 installedArtifacts := make([]strfmt.UUID, len(artifacts)) 992 993 errors, aggregatedErr := aggregateErrors() 994 mainthread.Run(func() { 995 defer close(errors) 996 997 wp := workerpool.New(MaxConcurrency) 998 999 for i, a := range artifacts { 1000 // Each artifact is of the form artifactID.tar.gz, so extract the artifactID from the name. 1001 filename := a.Path() 1002 basename := filepath.Base(filename) 1003 extIndex := strings.Index(basename, ".") 1004 if extIndex == -1 { 1005 extIndex = len(basename) 1006 } 1007 artifactID := strfmt.UUID(basename[0:extIndex]) 1008 installedArtifacts[i] = artifactID 1009 1010 // Submit the artifact for setup and install. 1011 func(filename string, artifactID strfmt.UUID) { 1012 wp.Submit(func() { 1013 as := alternative.NewArtifactSetup(artifactID, s.store) // offline installer artifacts are in this format 1014 err = installFunc(artifactID, filename, as) 1015 if err != nil { 1016 errors <- locale.WrapError(err, "artifact_setup_failed", "", artifactID.String(), "") 1017 } 1018 }) 1019 }(filename, artifactID) // avoid referencing loop variables inside goroutine closures 1020 } 1021 1022 wp.StopWait() 1023 }) 1024 1025 return installedArtifacts, <-aggregatedErr 1026 } 1027 1028 func (s *Setup) handleEvent(ev events.Eventer) error { 1029 err := s.eventHandler.Handle(ev) 1030 if err != nil { 1031 return &ProgressReportError{errs.Wrap(err, "Error handling event: %v", errs.JoinMessage(err))} 1032 } 1033 return nil 1034 } 1035 1036 func (s *Setup) deleteOutdatedArtifacts(setup Setuper, changedArtifacts *buildplan.ArtifactChangeset, alreadyInstalled store.StoredArtifactMap) error { 1037 storedArtifacts, err := s.store.Artifacts() 1038 if err != nil { 1039 return locale.WrapError(err, "err_stored_artifacts") 1040 } 1041 1042 err = setup.DeleteOutdatedArtifacts(changedArtifacts, storedArtifacts, alreadyInstalled) 1043 if err != nil { 1044 // This multilog is technically redundant and may be dropped after we can collect data on this error for a while as rollbar is not surfacing the returned error 1045 // https://github.com/ActiveState/cli/pull/2620#discussion_r1256103647 1046 multilog.Error("Could not delete outdated artifacts: %s", errs.JoinMessage(err)) 1047 return errs.Wrap(err, "Could not delete outdated artifacts") 1048 } 1049 return nil 1050 }