@@ -21,11 +21,12 @@ import (
2121)
2222
2323var (
24- ErrNotRunningInDocker = errors .New ("arcane is not running in a Docker container" )
25- ErrContainerNotFound = errors .New ("could not find Arcane container" )
26- ErrUpgradeInProgress = errors .New ("an upgrade is already in progress" )
27- ErrDockerSocketAccess = errors .New ("docker socket is not accessible" )
28- ArcaneUpgraderImage = "ghcr.io/getarcaneapp/arcane:latest"
24+ ErrNotRunningInDocker = errors .New ("arcane is not running in a Docker container" )
25+ ErrContainerNotFound = errors .New ("could not find Arcane container" )
26+ ErrUpgradeInProgress = errors .New ("an upgrade is already in progress" )
27+ ErrDockerSocketAccess = errors .New ("docker socket is not accessible" )
28+ ErrManualUpdateRequired = errors .New ("manual update required" )
29+ ArcaneUpgraderImage = "ghcr.io/getarcaneapp/arcane:latest"
2930)
3031
3132type SystemUpgradeService struct {
@@ -70,40 +71,61 @@ func (s *SystemUpgradeService) CanUpgrade(ctx context.Context) (bool, error) {
7071 return false , err
7172 }
7273
74+ if err := s .checkManualUpdateRequirement (ctx ); err != nil {
75+ return false , err
76+ }
77+
7378 return true , nil
7479}
7580
7681// TriggerUpgradeViaCLI spawns the upgrade CLI command in a separate container
7782// This avoids self-termination issues by running the upgrade from outside
7883func (s * SystemUpgradeService ) TriggerUpgradeViaCLI (ctx context.Context , user models.User ) error {
84+ return s .TriggerUpgradeViaCLIForContainer (ctx , user , "" )
85+ }
86+
87+ // TriggerUpgradeViaCLIForContainer spawns the upgrade CLI command targeting a
88+ // specific Arcane container. An empty container ID keeps the legacy self-targeting behavior.
89+ func (s * SystemUpgradeService ) TriggerUpgradeViaCLIForContainer (ctx context.Context , user models.User , containerID string ) error {
90+ if err := s .checkManualUpdateRequirement (ctx ); err != nil {
91+ return err
92+ }
93+
7994 if ! s .upgrading .CompareAndSwap (false , true ) {
8095 return ErrUpgradeInProgress
8196 }
8297 defer s .upgrading .Store (false )
8398
84- // Get current container name
85- containerId , err := s .getCurrentContainerID ()
86- if err != nil {
87- return fmt .Errorf ("get current container: %w" , err )
99+ targetContainerID := strings .TrimSpace (containerID )
100+ if targetContainerID == "" {
101+ var err error
102+ targetContainerID , err = s .getCurrentContainerID ()
103+ if err != nil {
104+ return fmt .Errorf ("get current container: %w" , err )
105+ }
88106 }
89107
90- currentContainer , err := s .findArcaneContainer (ctx , containerId )
108+ targetContainer , err := s .findArcaneContainer (ctx , targetContainerID )
91109 if err != nil {
92110 return fmt .Errorf ("inspect container: %w" , err )
93111 }
94112
95- containerName := strings .TrimPrefix (currentContainer .Name , "/" )
113+ logContainerID := targetContainer .ID
114+ if logContainerID == "" {
115+ logContainerID = targetContainerID
116+ }
117+ containerName := strings .TrimPrefix (targetContainer .Name , "/" )
96118
97119 // Determine binary path based on container type (agent vs main)
98120 binaryPath := "/app/arcane"
99- if currentContainer .Config != nil {
100- binaryPath = determineUpgradeBinaryPathInternal (currentContainer .Config .Labels )
121+ if targetContainer .Config != nil {
122+ binaryPath = determineUpgradeBinaryPathInternal (targetContainer .Config .Labels )
101123 }
102124
103125 // Log upgrade event
104126 metadata := models.JSON {
105127 "action" : "system_upgrade_cli" ,
106- "containerId" : containerId ,
128+ "containerId" : logContainerID ,
107129 "containerName" : containerName ,
108130 "method" : "cli" ,
109131 }
@@ -113,8 +135,8 @@ func (s *SystemUpgradeService) TriggerUpgradeViaCLI(ctx context.Context, user mo
113135
114136 // Use the same image reference as the currently running Arcane container for the upgrader.
115137 // This avoids mismatches where a newer/older upgrader CLI expects different behavior.
116- if currentContainer .Config != nil {
117- if img := strings .TrimSpace (currentContainer .Config .Image ); img != "" {
138+ if targetContainer .Config != nil {
139+ if img := strings .TrimSpace (targetContainer .Config .Image ); img != "" {
118140 ArcaneUpgraderImage = img
119141 }
120142 }
@@ -154,7 +176,7 @@ func (s *SystemUpgradeService) TriggerUpgradeViaCLI(ctx context.Context, user mo
154176 slog .Info ("Upgrader image pulled successfully" , "image" , ArcaneUpgraderImage )
155177
156178 // Try to get the /app/data mount from current container so upgrade logs persist.
157- appDataMount := dockerutils .MountForDestination (currentContainer .Mounts , "/app/data" , "/app/data" )
179+ appDataMount := dockerutils .MountForDestination (targetContainer .Mounts , "/app/data" , "/app/data" )
158180 if appDataMount == nil {
159181 slog .Warn ("Could not detect /app/data mount; upgrader logs may not persist" )
160182 } else {
@@ -210,6 +232,22 @@ func (s *SystemUpgradeService) TriggerUpgradeViaCLI(ctx context.Context, user mo
210232 return nil
211233}
212234
235+ func (s * SystemUpgradeService ) checkManualUpdateRequirement (ctx context.Context ) error {
236+ if s .versionService == nil {
237+ return nil
238+ }
239+
240+ required , message := s .versionService .ManualUpdateRequirement (ctx , "" , "" )
241+ if ! required {
242+ return nil
243+ }
244+ if strings .TrimSpace (message ) == "" {
245+ message = ErrManualUpdateRequired .Error ()
246+ }
247+
248+ return fmt .Errorf ("%w: %s" , ErrManualUpdateRequired , message )
249+ }
250+
213251func determineUpgradeBinaryPathInternal (labels map [string ]string ) string {
214252 if libupdater .IsArcaneAgentContainer (labels ) {
215253 return "/app/arcane-agent"
0 commit comments