diff --git a/internal/templates/docker/Dockerfile.layered.tmpl b/internal/templates/docker/Dockerfile.layered.tmpl index a6b0add..60d50c6 100644 --- a/internal/templates/docker/Dockerfile.layered.tmpl +++ b/internal/templates/docker/Dockerfile.layered.tmpl @@ -46,6 +46,7 @@ RUN chmod +x /usr/local/bin/compute-source-env.sh \ LABEL tee.launch_policy.log_redirect={{.LogRedirect}} {{- end}} +LABEL tee.launch_policy.monitoring_memory_allow={{.ResourceUsageAllow}} LABEL eigenx_cli_version={{.EigenXCLIVersion}} LABEL eigenx_use_ita=True diff --git a/pkg/commands/app/deploy.go b/pkg/commands/app/deploy.go index 955ca41..35fcd6d 100644 --- a/pkg/commands/app/deploy.go +++ b/pkg/commands/app/deploy.go @@ -21,6 +21,7 @@ var DeployCommand = &cli.Command{ common.EnvFlag, common.FileFlag, common.LogVisibilityFlag, + common.ResourceUsageFlag, common.InstanceTypeFlag, common.NameFlag, common.WebsiteFlag, @@ -82,14 +83,20 @@ func deployAction(cCtx *cli.Context) error { return fmt.Errorf("failed to get log settings: %w", err) } - // 9. Generate random salt + // 9. Get resource usage preference + resourceUsageAllow, err := utils.GetResourceUsageSetting(cCtx) + if err != nil { + return fmt.Errorf("failed to get resource usage setting: %w", err) + } + + // 10. Generate random salt salt := [32]byte{} _, err = rand.Read(salt[:]) if err != nil { return fmt.Errorf("failed to generate random salt: %w", err) } - // 10. Get app ID + // 11. Get app ID _, appController, err := utils.GetAppControllerBinding(cCtx) if err != nil { return fmt.Errorf("failed to get app controller binding: %w", err) @@ -99,19 +106,19 @@ func deployAction(cCtx *cli.Context) error { return fmt.Errorf("failed to get app id: %w", err) } - // 11. Prepare the release (includes build/push if needed, with automatic retry on permission errors) - release, imageRef, err := utils.PrepareReleaseFromContext(cCtx, preflightCtx.EnvironmentConfig, appIDToBeDeployed, dockerfilePath, imageRef, envFilePath, logRedirect, instanceType, 3) + // 12. Prepare the release (includes build/push if needed, with automatic retry on permission errors) + release, imageRef, err := utils.PrepareReleaseFromContext(cCtx, preflightCtx.EnvironmentConfig, appIDToBeDeployed, dockerfilePath, imageRef, envFilePath, logRedirect, resourceUsageAllow, instanceType, 3) if err != nil { return err } - // 12. Deploy the app + // 13. Deploy the app appID, err := preflightCtx.Caller.DeployApp(cCtx.Context, salt, release, publicLogs, imageRef) if err != nil { return fmt.Errorf("failed to deploy app: %w", err) } - // 13. Collect app profile while deployment is in progress (optional) + // 14. Collect app profile while deployment is in progress (optional) environment := preflightCtx.EnvironmentConfig.Name suggestedName, err := utils.ExtractAndFindAvailableName(environment, imageRef) if err != nil { @@ -126,7 +133,7 @@ func deployAction(cCtx *cli.Context) error { profile = nil } - // 14. Upload profile if provided (non-blocking - warn on failure but don't fail deployment) + // 15. Upload profile if provided (non-blocking - warn on failure but don't fail deployment) if profile != nil { logger.Info("Uploading app profile...") userApiClient, err := utils.NewUserApiClient(cCtx) @@ -142,7 +149,7 @@ func deployAction(cCtx *cli.Context) error { } } - // 15. Watch until deployment completes + // 16. Watch until deployment completes return utils.WatchUntilTransitionComplete(cCtx, appID, common.AppStatusDeploying) } diff --git a/pkg/commands/app/upgrade.go b/pkg/commands/app/upgrade.go index e3016aa..6a868f9 100644 --- a/pkg/commands/app/upgrade.go +++ b/pkg/commands/app/upgrade.go @@ -20,6 +20,7 @@ var UpgradeCommand = &cli.Command{ common.EnvFlag, common.FileFlag, common.LogVisibilityFlag, + common.ResourceUsageFlag, common.InstanceTypeFlag, }...), Action: upgradeAction, @@ -78,13 +79,19 @@ func upgradeAction(cCtx *cli.Context) error { return fmt.Errorf("failed to get log settings: %w", err) } - // 10. Prepare the release (includes build/push if needed, with automatic retry on permission errors) - release, imageRef, err := utils.PrepareReleaseFromContext(cCtx, preflightCtx.EnvironmentConfig, appID, dockerfilePath, imageRef, envFilePath, logRedirect, instanceType, 3) + // 10. Get resource usage preference + resourceUsageAllow, err := utils.GetResourceUsageSetting(cCtx) + if err != nil { + return fmt.Errorf("failed to get resource usage setting: %w", err) + } + + // 11. Prepare the release (includes build/push if needed, with automatic retry on permission errors) + release, imageRef, err := utils.PrepareReleaseFromContext(cCtx, preflightCtx.EnvironmentConfig, appID, dockerfilePath, imageRef, envFilePath, logRedirect, resourceUsageAllow, instanceType, 3) if err != nil { return err } - // 11. Check current permission state and determine if change is needed + // 12. Check current permission state and determine if change is needed currentlyPublic, err := utils.CheckAppLogPermission(cCtx, appID) if err != nil { return fmt.Errorf("failed to check current permission state: %w", err) @@ -92,13 +99,13 @@ func upgradeAction(cCtx *cli.Context) error { needsPermissionChange := currentlyPublic != publicLogs - // 12. Upgrade the app + // 13. Upgrade the app err = preflightCtx.Caller.UpgradeApp(cCtx.Context, appID, release, publicLogs, needsPermissionChange, imageRef) if err != nil { return fmt.Errorf("failed to upgrade app: %w", err) } - // 13. Watch until upgrade completes + // 14. Watch until upgrade completes return utils.WatchUntilTransitionComplete(cCtx, appID, common.AppStatusUpgrading) } diff --git a/pkg/commands/utils/build_utils.go b/pkg/commands/utils/build_utils.go index 06cc097..4938f3c 100644 --- a/pkg/commands/utils/build_utils.go +++ b/pkg/commands/utils/build_utils.go @@ -235,7 +235,7 @@ func checkIfImageAlreadyLayeredForEigenX(dockerClient *client.Client, ctx contex // Image Building and Pushing // ============================================================================ -func buildAndPushLayeredImage(cCtx *cli.Context, environmentConfig common.EnvironmentConfig, dockerfilePath, targetImageRef, logRedirect, envFilePath string) (string, error) { +func buildAndPushLayeredImage(cCtx *cli.Context, environmentConfig common.EnvironmentConfig, dockerfilePath, targetImageRef, logRedirect, resourceUsageAllow, envFilePath string) (string, error) { logger := common.LoggerFromContext(cCtx) dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) @@ -253,10 +253,10 @@ func buildAndPushLayeredImage(cCtx *cli.Context, environmentConfig common.Enviro return "", fmt.Errorf("failed to build base image: %w", err) } - return layerLocalImage(cCtx, dockerClient, environmentConfig, baseImageTag, targetImageRef, logRedirect, envFilePath) + return layerLocalImage(cCtx, dockerClient, environmentConfig, baseImageTag, targetImageRef, logRedirect, resourceUsageAllow, envFilePath) } -func layerLocalImage(cCtx *cli.Context, dockerClient *client.Client, environmentConfig common.EnvironmentConfig, sourceImageRef, targetImageRef, logRedirect, envFilePath string) (string, error) { +func layerLocalImage(cCtx *cli.Context, dockerClient *client.Client, environmentConfig common.EnvironmentConfig, sourceImageRef, targetImageRef, logRedirect, resourceUsageAllow, envFilePath string) (string, error) { logger := common.LoggerFromContext(cCtx) // Extract original command and user from source image @@ -286,12 +286,13 @@ func layerLocalImage(cCtx *cli.Context, dockerClient *client.Client, environment } layeredDockerfileContent, err := processTemplate(LayeredDockerfilePath, LayeredDockerfileTemplateData{ - BaseImage: sourceImageRef, - OriginalCmd: originalCmdStr, - OriginalUser: originalUser, - LogRedirect: logRedirect, - IncludeTLS: includeTLS, - EigenXCLIVersion: version.GetVersion(), + BaseImage: sourceImageRef, + OriginalCmd: originalCmdStr, + OriginalUser: originalUser, + LogRedirect: logRedirect, + ResourceUsageAllow: resourceUsageAllow, + IncludeTLS: includeTLS, + EigenXCLIVersion: version.GetVersion(), }) if err != nil { return "", fmt.Errorf("failed to process dockerfile template: %w", err) diff --git a/pkg/commands/utils/contract_utils.go b/pkg/commands/utils/contract_utils.go index 8d83c22..6b93c1b 100644 --- a/pkg/commands/utils/contract_utils.go +++ b/pkg/commands/utils/contract_utils.go @@ -285,6 +285,29 @@ func PrintAppInfoWithStatus(ctx context.Context, logger iface.Logger, client *et logger.Info("Instance: %s", info.MachineType) logger.Info("IP: %s", info.Ip) + // Display CPU and Memory metrics if available + if info.Metrics != nil { + if info.Metrics.CPUUtilizationPercent > 0 { + logger.Info("CPU Usage: %.2f%%", info.Metrics.CPUUtilizationPercent) + } + + memoryUsed := info.Metrics.MemoryUsedBytes + memoryTotal := info.Metrics.MemoryTotalBytes + memoryPercent := info.Metrics.MemoryUtilizationPercent + + if memoryTotal > 0 && memoryUsed > 0 { + usedGB := float64(memoryUsed) / (1024 * 1024 * 1024) + totalGB := float64(memoryTotal) / (1024 * 1024 * 1024) + if memoryPercent > 0 { + logger.Info("Memory Usage: %.2f%% (%.2f GB / %.2f GB)", memoryPercent, usedGB, totalGB) + } else { + logger.Info("Memory Usage: %.2f GB / %.2f GB", usedGB, totalGB) + } + } else if memoryPercent > 0 { + logger.Info("Memory Usage: %.2f%%", memoryPercent) + } + } + // Display app profile if available if info.Profile != nil { if info.Profile.Website != nil { diff --git a/pkg/commands/utils/interactive.go b/pkg/commands/utils/interactive.go index 553ee41..475c1d4 100644 --- a/pkg/commands/utils/interactive.go +++ b/pkg/commands/utils/interactive.go @@ -893,6 +893,39 @@ func GetLogSettingsInteractive(cCtx *cli.Context) (logRedirect string, publicLog } } +// GetResourceUsageSetting returns the resource usage configuration from flags or prompt +func GetResourceUsageSetting(cCtx *cli.Context) (string, error) { + if flagValue := cCtx.String("resource-usage-monitoring"); flagValue != "" { + switch strings.ToLower(flagValue) { + case "enable": + return "always", nil + case "disable": + return "never", nil + default: + return "", fmt.Errorf("invalid --resource-usage-monitoring value: %s (must be enable or disable)", flagValue) + } + } + + options := []string{ + "Yes", + "No", + } + + choice, err := output.SelectString("Show resource usage (CPU/memory) for your app?", options) + if err != nil { + return "", fmt.Errorf("failed to get resource usage choice: %w", err) + } + + switch choice { + case "Yes": + return "always", nil + case "No": + return "never", nil + default: + return "", fmt.Errorf("unexpected choice: %s", choice) + } +} + // GetInstanceTypeInteractive prompts for instance type if not provided via flag. // The defaultSKU parameter is used as the default selection in interactive mode: // - For new deployments: pass empty string (uses first SKU from backend) diff --git a/pkg/commands/utils/release_utils.go b/pkg/commands/utils/release_utils.go index f6c2c15..fc36754 100644 --- a/pkg/commands/utils/release_utils.go +++ b/pkg/commands/utils/release_utils.go @@ -31,16 +31,16 @@ import ( // PrepareReleaseFromContext prepares a release with separated Dockerfile handling // The dockerfile path and env file path are provided as parameters (already collected earlier) // maxPushRetries controls how many times to retry on push permission errors (0 = no retries) -func PrepareReleaseFromContext(cCtx *cli.Context, environmentConfig *common.EnvironmentConfig, appID gethcommon.Address, dockerfilePath string, imageRef string, envFilePath string, logRedirect string, instanceType string, maxPushRetries int) (appcontrollerV2.IAppControllerRelease, string, error) { +func PrepareReleaseFromContext(cCtx *cli.Context, environmentConfig *common.EnvironmentConfig, appID gethcommon.Address, dockerfilePath string, imageRef string, envFilePath string, logRedirect string, resourceUsageAllow string, instanceType string, maxPushRetries int) (appcontrollerV2.IAppControllerRelease, string, error) { logger := common.LoggerFromContext(cCtx) // Create operation closures that capture context buildAndPush := func(ref string) (string, error) { - return buildAndPushLayeredImage(cCtx, *environmentConfig, dockerfilePath, ref, logRedirect, envFilePath) + return buildAndPushLayeredImage(cCtx, *environmentConfig, dockerfilePath, ref, logRedirect, resourceUsageAllow, envFilePath) } layerRemoteImage := func(ref string) (string, error) { - return layerRemoteImageIfNeeded(cCtx, *environmentConfig, ref, logRedirect, envFilePath) + return layerRemoteImageIfNeeded(cCtx, *environmentConfig, ref, logRedirect, resourceUsageAllow, envFilePath) } // Ensure image is compatible with EigenX (either build from Dockerfile or layer existing image) @@ -178,7 +178,7 @@ func retryImagePushOperation( return imageRef, err } -func layerRemoteImageIfNeeded(cCtx *cli.Context, environmentConfig common.EnvironmentConfig, imageRef, logRedirect, envFilePath string) (string, error) { +func layerRemoteImageIfNeeded(cCtx *cli.Context, environmentConfig common.EnvironmentConfig, imageRef, logRedirect, resourceUsageAllow, envFilePath string) (string, error) { // Check if the provided image is missing image layering, which is required for EigenX dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { @@ -201,7 +201,7 @@ func layerRemoteImageIfNeeded(cCtx *cli.Context, environmentConfig common.Enviro } logger.Info("Adding EigenX components to create %s from %s...", targetImageRef, imageRef) - layeredImageRef, err := layerLocalImage(cCtx, dockerClient, environmentConfig, imageRef, targetImageRef, logRedirect, envFilePath) + layeredImageRef, err := layerLocalImage(cCtx, dockerClient, environmentConfig, imageRef, targetImageRef, logRedirect, resourceUsageAllow, envFilePath) if err != nil { return "", fmt.Errorf("failed to layer published image: %w", err) } diff --git a/pkg/commands/utils/types.go b/pkg/commands/utils/types.go index b25681a..32c7d03 100644 --- a/pkg/commands/utils/types.go +++ b/pkg/commands/utils/types.go @@ -27,12 +27,13 @@ const ( ) type LayeredDockerfileTemplateData struct { - BaseImage string - OriginalCmd string - OriginalUser string - LogRedirect string - IncludeTLS bool - EigenXCLIVersion string + BaseImage string + OriginalCmd string + OriginalUser string + LogRedirect string + ResourceUsageAllow string + IncludeTLS bool + EigenXCLIVersion string } type EnvSourceScriptTemplateData struct { diff --git a/pkg/commands/utils/userapi_client.go b/pkg/commands/utils/userapi_client.go index 4a72071..6bba801 100644 --- a/pkg/commands/utils/userapi_client.go +++ b/pkg/commands/utils/userapi_client.go @@ -101,6 +101,7 @@ type RawAppInfo struct { Ip string `json:"ip"` MachineType string `json:"machine_type"` Profile *AppProfileResponse `json:"profile,omitempty"` + Metrics *AppMetrics `json:"metrics,omitempty"` } // AppInfo contains the app info with parsed and validated addresses @@ -111,6 +112,14 @@ type AppInfo struct { Ip string MachineType string Profile *AppProfileResponse + Metrics *AppMetrics +} + +type AppMetrics struct { + CPUUtilizationPercent float64 `json:"cpu_utilization_percent,omitempty"` + MemoryUtilizationPercent float64 `json:"memory_utilization_percent,omitempty"` + MemoryUsedBytes uint64 `json:"memory_used_bytes,omitempty"` + MemoryTotalBytes uint64 `json:"memory_total_bytes,omitempty"` } type AppInfoResponse struct { @@ -216,6 +225,16 @@ func (cc *UserApiClient) GetInfos(cCtx *cli.Context, appIDs []ethcommon.Address, return nil, fmt.Errorf("error processing addresses for app %s: %w", appIDList[i], err) } + var metrics *AppMetrics + if rawApp.Metrics != nil { + metrics = &AppMetrics{ + CPUUtilizationPercent: rawApp.Metrics.CPUUtilizationPercent, + MemoryUtilizationPercent: rawApp.Metrics.MemoryUtilizationPercent, + MemoryUsedBytes: rawApp.Metrics.MemoryUsedBytes, + MemoryTotalBytes: rawApp.Metrics.MemoryTotalBytes, + } + } + result.Apps[i] = AppInfo{ EVMAddresses: evmAddrs, SolanaAddresses: solanaAddrs, @@ -223,6 +242,7 @@ func (cc *UserApiClient) GetInfos(cCtx *cli.Context, appIDs []ethcommon.Address, Ip: rawApp.Ip, MachineType: rawApp.MachineType, Profile: rawApp.Profile, + Metrics: metrics, } } diff --git a/pkg/common/flags.go b/pkg/common/flags.go index 50cc3bc..f55ec78 100644 --- a/pkg/common/flags.go +++ b/pkg/common/flags.go @@ -70,6 +70,11 @@ var ( Usage: "Log visibility setting: public, private, or off", } + ResourceUsageFlag = &cli.StringFlag{ + Name: "resource-usage-monitoring", + Usage: "Resource usage monitoring: enable or disable", + } + InstanceTypeFlag = &cli.StringFlag{ Name: "instance-type", Usage: "Machine instance type to use e.g. g1-standard-4t, g1-standard-8t",