Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Portal automatic start and stop on context change #1086

Merged
merged 7 commits into from Aug 10, 2023
12 changes: 8 additions & 4 deletions cli/cli/commands/kurtosis_context/context_switch/switch.go
Expand Up @@ -111,7 +111,10 @@ func SwitchContext(
}

portalManager := portal_manager.NewPortalManager()
if portalManager.IsReachable() {
if store.IsRemote(currentContext) {
if err := portalManager.DownloadAndStart(ctx); err != nil {
return stacktrace.Propagate(err, "An error occurred starting the portal")
}
portalDaemonClient := portalManager.GetClient()
if portalDaemonClient != nil {
switchContextArg := constructors.NewSwitchContextArgs()
Expand All @@ -120,11 +123,11 @@ func SwitchContext(
}
}
} else {
if store.IsRemote(currentContext) {
return stacktrace.NewError("New context is remote but Kurtosis Portal is not reachable locally. " +
"Make sure Kurtosis Portal is running before switching to a remote context again.")
if err := portalManager.StopExisting(ctx); err != nil {
laurentluce marked this conversation as resolved.
Show resolved Hide resolved
return stacktrace.Propagate(err, "An error occurred stopping Kurtosis Portal")
}
}

logrus.Infof("Context switched to '%s', Kurtosis engine will now be restarted", contextIdentifier)

// Instantiate the engine manager after storing the new context so the manager can read it.
Expand All @@ -147,6 +150,7 @@ func SwitchContext(
}()
logrus.Info("Successfully switched context")
}

isContextSwitchSuccessful = true
return nil
}
24 changes: 2 additions & 22 deletions cli/cli/commands/portal/start/start.go
Expand Up @@ -22,32 +22,12 @@ var PortalStartCmd = &lowlevel.LowlevelKurtosisCommand{
PostValidationAndRunFunc: nil,
}

func run(ctx context.Context, _ *flags.ParsedFlags, args *args.ParsedArgs) error {
func run(ctx context.Context, _ *flags.ParsedFlags, _ *args.ParsedArgs) error {
logrus.Infof("Starting Kurtosis Portal")
portalManager := portal_manager.NewPortalManager()

// Checking if new version is available and potentially downloading it
if _, err := portal_manager.DownloadLatestKurtosisPortalBinary(ctx); err != nil {
return stacktrace.Propagate(err, "An unexpected error occurred trying to download the latest version of Kurtosis Portal")
}

currentPortalPid, process, isPortalReachable, err := portalManager.CurrentStatus(ctx)
if err != nil {
return stacktrace.Propagate(err, "Unable to determine current state of Kurtosis Portal process")
}
if isPortalReachable {
logrus.Infof("Portal is currently running on PID '%d' and healthy.", currentPortalPid)
return nil
}
if process != nil {
logrus.Warnf("A non-healthy Portal process is currently running on PID '%d'. Stop it first before starting a new one", currentPortalPid)
return nil
}

startedPortalPid, err := portalManager.StartNew(ctx)
err := portalManager.DownloadAndStart(ctx)
if err != nil {
return stacktrace.Propagate(err, "Error starting portal")
}
logrus.Infof("Kurtosis Portal started successfully on PID %d", startedPortalPid)
return nil
}
64 changes: 39 additions & 25 deletions cli/cli/helpers/portal_manager/binary_artifact_getter.go
Expand Up @@ -24,6 +24,8 @@ const (

portalBinaryFileMode = 0700
portalVersionFileMode = 0600
portalTagLatest = "latest"
portalTag = "0.0.4"

osArchitectureSeparator = "_"

Expand All @@ -32,13 +34,13 @@ const (
numberOfAssetsPerPage = 50
)

// DownloadLatestKurtosisPortalBinary downloads the latest version of Kurtosis Portal.
// It returns true if a new version was downloaded and installed, false if it no-oped because latest was already
// installed or because latest version information could not be retrieved.
// DownloadRequiredKurtosisPortalBinary downloads the required version of Kurtosis Portal.
// It returns true if a new version was downloaded and installed, false if it no-oped because required was already
// installed or because required version information could not be retrieved.
// Note that it returns an error only if the end state is such that the portal cannot be run properly. I.e. if a
// Portal is already installed, and it failed to retrieve the latest version for example, it returns gracefully
// Portal is already installed, and it failed to retrieve the required version for example, it returns gracefully
// as this is not critical (current version will be used and will continue to run fine)
func DownloadLatestKurtosisPortalBinary(ctx context.Context) (bool, error) {
func DownloadRequiredKurtosisPortalBinary(ctx context.Context) (bool, error) {
binaryFilePath, err := host_machine_directories.GetPortalBinaryFilePath()
if err != nil {
return false, stacktrace.Propagate(err, "Unable to get file path to Kurtosis Portal binary file")
Expand All @@ -54,48 +56,60 @@ func DownloadLatestKurtosisPortalBinary(ctx context.Context) (bool, error) {
}

ghClient := github.NewClient(http.DefaultClient)
latestRelease, err := getLatestRelease(ctx, ghClient)
var release *github.RepositoryRelease
if portalTag == portalTagLatest {
laurentluce marked this conversation as resolved.
Show resolved Hide resolved
release, err = getLatestRelease(ctx, ghClient)
} else {
release, err = getReleaseByTag(ctx, ghClient, portalTag)
}
if err != nil {
return false, defaultToCurrentVersionOrError(currentVersionStrMaybe, err)
}

latestVersionStr, currentVersionMatchesLatest, err := compareLatestVersionWithCurrent(currentVersionStrMaybe, latestRelease)
requiredVersionStr, currentVersionMatchesRequired, err := compareRequiredVersionWithCurrent(currentVersionStrMaybe, release)
if err != nil {
return false, defaultToCurrentVersionOrError(currentVersionStrMaybe, err)
}
if currentVersionMatchesLatest {
logrus.Infof("Kurtosis Portal version '%s' is the latest and already installed", latestVersionStr)
if currentVersionMatchesRequired {
logrus.Infof("Installed Kurtosis Portal version '%s' is the required one", requiredVersionStr)
return false, nil
}

if currentVersionStrMaybe == "" {
// no Portal is currently installed. Installing a brand new one
logrus.Infof("Installing Kurtosis Portal version '%s'", latestVersionStr)
logrus.Infof("Installing Kurtosis Portal version '%s'", requiredVersionStr)
} else {
logrus.Infof("Upgrading currently Kurtosis Portal version '%s' to '%s'", currentVersionStrMaybe, latestVersionStr)
logrus.Infof("Upgrading currently Kurtosis Portal version '%s' to '%s'", currentVersionStrMaybe, requiredVersionStr)
}

githubAssetContent, err := downloadGithubAsset(ctx, ghClient, latestRelease)
githubAssetContent, err := downloadGithubAsset(ctx, ghClient, release)
if err != nil {
return false, defaultToCurrentVersionOrError(currentVersionStrMaybe, err)
}

if err = extractAssetTgzToBinaryFileOnDisk(githubAssetContent, latestVersionStr, binaryFilePath, currentVersionFilePath); err != nil {
if err = extractAssetTgzToBinaryFileOnDisk(githubAssetContent, requiredVersionStr, binaryFilePath, currentVersionFilePath); err != nil {
return false, defaultToCurrentVersionOrError(currentVersionStrMaybe, err)
}
return true, nil
}

func getLatestRelease(ctx context.Context, ghClient *github.Client) (*github.RepositoryRelease, error) {
// First, browse the list of releases for the Kurtosis Portal repo
latestRelease, _, err := ghClient.Repositories.GetLatestRelease(ctx, kurtosisTechGithubOrg, kurtosisPortalGithubRepoName)
if err != nil {
return nil, stacktrace.Propagate(err, "Unable to retrieve latest version of Kurtosis Portal form GitHub")
return nil, stacktrace.Propagate(err, "Unable to retrieve latest version of Kurtosis Portal from GitHub")
}
logrus.Debugf("Latest release is %s", latestRelease.GetName())
return latestRelease, nil
}

func getReleaseByTag(ctx context.Context, ghClient *github.Client, tag string) (*github.RepositoryRelease, error) {
release, _, err := ghClient.Repositories.GetReleaseByTag(ctx, kurtosisTechGithubOrg, kurtosisPortalGithubRepoName, tag)
laurentluce marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, stacktrace.Propagate(err, "Unable to retrieve version with tag %s of Kurtosis Portal from GitHub", tag)
}
return release, nil
}

func getVersionCurrentlyInstalled(currentVersionFilePath string) (string, error) {
if _, err := createFileIfNecessary(currentVersionFilePath); err != nil {
return "", stacktrace.Propagate(err, "Unable to create Kurtosis Portal version file on disk")
Expand All @@ -107,23 +121,23 @@ func getVersionCurrentlyInstalled(currentVersionFilePath string) (string, error)
return strings.TrimSpace(string(currentVersionBytes)), nil
}

func compareLatestVersionWithCurrent(currentVersionStrIfAny string, latestRelease *github.RepositoryRelease) (string, bool, error) {
latestVersionStr := latestRelease.GetName()
if currentVersionStrIfAny == latestVersionStr {
return latestVersionStr, true, nil
func compareRequiredVersionWithCurrent(currentVersionStrIfAny string, requiredRelease *github.RepositoryRelease) (string, bool, error) {
requiredVersionStr := requiredRelease.GetName()
if currentVersionStrIfAny == requiredVersionStr {
return requiredVersionStr, true, nil
}
return latestVersionStr, false, nil
return requiredVersionStr, false, nil
}

func downloadGithubAsset(ctx context.Context, ghClient *github.Client, latestRelease *github.RepositoryRelease) (io.ReadCloser, error) {
func downloadGithubAsset(ctx context.Context, ghClient *github.Client, release *github.RepositoryRelease) (io.ReadCloser, error) {
// Get all assets associated with this release and identify the one matching the current machine architecture
opts := &github.ListOptions{
Page: assetsFirstPageNumber,
PerPage: numberOfAssetsPerPage,
}
allReleaseAssets, _, err := ghClient.Repositories.ListReleaseAssets(ctx, kurtosisTechGithubOrg, kurtosisPortalGithubRepoName, latestRelease.GetID(), opts)
allReleaseAssets, _, err := ghClient.Repositories.ListReleaseAssets(ctx, kurtosisTechGithubOrg, kurtosisPortalGithubRepoName, release.GetID(), opts)
if err != nil {
return nil, stacktrace.Propagate(err, "Unable to get list of assets from latest version")
return nil, stacktrace.Propagate(err, "Unable to get list of assets from required version")
}
detectedOsArch := fmt.Sprintf("%s%s%s", runtime.GOOS, osArchitectureSeparator, runtime.GOARCH)
assetFileExpectedSuffix := fmt.Sprintf("%s%s", detectedOsArch, githubAssetExtension)
Expand Down Expand Up @@ -185,9 +199,9 @@ func extractAssetTgzToBinaryFileOnDisk(assetContent io.ReadCloser, assetVersion

func defaultToCurrentVersionOrError(currentVersionStr string, nonNilError error) error {
if currentVersionStr != "" {
logrus.Warnf("Checking for latest version of Kurtosis Portal failed. Currently installed version '%s' will be used", currentVersionStr)
logrus.Warnf("Checking for required version of Kurtosis Portal failed. Currently installed version '%s' will be used", currentVersionStr)
logrus.Debugf("Error was: %v", nonNilError.Error())
return nil
}
return stacktrace.Propagate(nonNilError, "An error occurred installing Kurtosis Portal latest version")
return stacktrace.Propagate(nonNilError, "An error occurred installing Kurtosis Portal required version")
}
29 changes: 29 additions & 0 deletions cli/cli/helpers/portal_manager/portal_manager.go
Expand Up @@ -213,6 +213,35 @@ func (portalManager *PortalManager) MapPorts(ctx context.Context, localPortToRem
return successfullyMappedPorts, failedPorts, nil
}

// DownloadAndStart downloads the required version and starts the portal if the required version
// is not already running.
func (portalManager *PortalManager) DownloadAndStart(ctx context.Context) error {
// Checking if new version is available and potentially downloading it
currentPortalPid, process, isPortalReachable, err := portalManager.CurrentStatus(ctx)
if err != nil {
return stacktrace.Propagate(err, "Unable to determine current state of Kurtosis Portal process")
}
if isPortalReachable {
laurentluce marked this conversation as resolved.
Show resolved Hide resolved
logrus.Infof("Portal is currently running on PID '%d' and healthy.", currentPortalPid)
return nil
}
if process != nil {
logrus.Warnf("A non-healthy Portal process is currently running on PID '%d'. Stop it first before starting a new one", currentPortalPid)
return nil
}

if _, err := DownloadRequiredKurtosisPortalBinary(ctx); err != nil {
return stacktrace.Propagate(err, "An unexpected error occurred trying to download the required version of Kurtosis Portal")
}

startedPortalPid, err := portalManager.StartNew(ctx)
if err != nil {
return stacktrace.Propagate(err, "Error starting portal")
}
logrus.Infof("Kurtosis Portal started successfully on PID %d", startedPortalPid)
return nil
}

func (portalManager *PortalManager) instantiateClientIfUnset() error {
portalDaemonClientMaybe, err := kurtosis_context.CreatePortalDaemonClient(true)
if err != nil {
Expand Down