Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/templates/docker/Dockerfile.layered.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 15 additions & 8 deletions pkg/commands/app/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var DeployCommand = &cli.Command{
common.EnvFlag,
common.FileFlag,
common.LogVisibilityFlag,
common.ResourceUsageFlag,
common.InstanceTypeFlag,
common.NameFlag,
common.WebsiteFlag,
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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)
}

Expand Down
17 changes: 12 additions & 5 deletions pkg/commands/app/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var UpgradeCommand = &cli.Command{
common.EnvFlag,
common.FileFlag,
common.LogVisibilityFlag,
common.ResourceUsageFlag,
common.InstanceTypeFlag,
}...),
Action: upgradeAction,
Expand Down Expand Up @@ -78,27 +79,33 @@ 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)
}

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)
}

Expand Down
19 changes: 10 additions & 9 deletions pkg/commands/utils/build_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions pkg/commands/utils/contract_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 33 additions & 0 deletions pkg/commands/utils/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions pkg/commands/utils/release_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down
13 changes: 7 additions & 6 deletions pkg/commands/utils/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions pkg/commands/utils/userapi_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -216,13 +225,24 @@ 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,
Status: rawApp.Status,
Ip: rawApp.Ip,
MachineType: rawApp.MachineType,
Profile: rawApp.Profile,
Metrics: metrics,
}
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/common/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading