@@ -22,7 +22,6 @@ import (
2222 "time"
2323
2424 "github.com/virtual-kubelet/virtual-kubelet/log"
25- v1 "k8s.io/api/core/v1"
2625)
2726
2827// CreateAppHostingApp creates a single IOS-XE AppHosting app from an AppHostingConfig.
@@ -150,206 +149,6 @@ func (d *XEDriver) UninstallApp(ctx context.Context, appID string) error {
150149 return nil
151150}
152151
153- // ReconcileApp performs a single reconciliation step, driving the app one state
154- // closer to its desired state and updating appConfig.Status in place.
155- //
156- // Forward (DesiredState = Running):
157- //
158- // "" (no config) → POST config + install RPC → Converging
159- // "" (config, no oper) → re-issue install RPC → Converging
160- // "DEPLOYED" → activate RPC → Converging
161- // "ACTIVATED" → start RPC → Converging
162- // "RUNNING" → no-op → Ready
163- //
164- // Reverse (DesiredState = Deleted):
165- //
166- // "RUNNING" → stop RPC → Deleting
167- // "ACTIVATED"/"STOPPED" → deactivate RPC → Deleting
168- // "DEPLOYED" → uninstall RPC → Deleting
169- // "" (no oper) → delete config → Deleted
170- func (d * XEDriver ) ReconcileApp (ctx context.Context , appConfig * AppHostingConfig ) {
171- appID := appConfig .AppName ()
172- desired := appConfig .Spec .DesiredState
173-
174- // 1. Observe current device state.
175- state := d .getAppState (ctx , appID )
176- appConfig .Status .ObservedState = state
177- appConfig .Status .LastTransition = time .Now ()
178-
179- log .G (ctx ).Infof ("ReconcileApp %s: observed=%q desired=%s phase=%s" ,
180- appID , state , desired , appConfig .Status .Phase )
181-
182- // ── Forward path: drive toward RUNNING ────────────────────────────
183- if desired == AppDesiredStateRunning {
184- switch state {
185- case "RUNNING" :
186- appConfig .Status .Phase = AppPhaseReady
187- appConfig .Status .Message = "App is running"
188- return
189-
190- case "ACTIVATED" :
191- // ACTIVATED → start
192- appConfig .Status .Phase = AppPhaseConverging
193- appConfig .Status .Message = "Starting app"
194- if err := d .StartApp (ctx , appID ); err != nil {
195- log .G (ctx ).Warnf ("ReconcileApp %s: start failed: %v" , appID , err )
196- appConfig .Status .Phase = AppPhaseError
197- appConfig .Status .Message = fmt .Sprintf ("start failed: %v" , err )
198- }
199- return
200-
201- case "DEPLOYED" :
202- // DEPLOYED → activate
203- appConfig .Status .Phase = AppPhaseConverging
204- appConfig .Status .Message = "Activating app"
205- if err := d .ActivateApp (ctx , appID ); err != nil {
206- log .G (ctx ).Warnf ("ReconcileApp %s: activate failed: %v" , appID , err )
207- appConfig .Status .Phase = AppPhaseError
208- appConfig .Status .Message = fmt .Sprintf ("activate failed: %v" , err )
209- }
210- return
211-
212- default :
213- // No oper data (or unexpected state) — the install likely hasn't
214- // happened or failed silently. Re-issue install if we have an image.
215- imagePath := appConfig .ImagePath ()
216- if imagePath == "" {
217- log .G (ctx ).Warnf ("ReconcileApp %s: no oper data and no image path; cannot install" , appID )
218- appConfig .Status .Phase = AppPhaseError
219- appConfig .Status .Message = "no image path available for install"
220- return
221- }
222- appConfig .Status .Phase = AppPhaseConverging
223- appConfig .Status .Message = "Re-issuing install"
224- log .G (ctx ).Warnf ("ReconcileApp %s: no oper data; re-issuing install (image: %s)" , appID , imagePath )
225- if err := d .InstallApp (ctx , appID , imagePath ); err != nil {
226- log .G (ctx ).Warnf ("ReconcileApp %s: install failed: %v" , appID , err )
227- appConfig .Status .Phase = AppPhaseError
228- appConfig .Status .Message = fmt .Sprintf ("install failed: %v" , err )
229- }
230- return
231- }
232- }
233-
234- // ── Reverse path: drive toward deletion ───────────────────────────
235- if desired == AppDesiredStateDeleted {
236- switch state {
237- case "RUNNING" :
238- appConfig .Status .Phase = AppPhaseDeleting
239- appConfig .Status .Message = "Stopping app"
240- if err := d .StopApp (ctx , appID ); err != nil {
241- log .G (ctx ).Warnf ("ReconcileApp %s: stop failed: %v" , appID , err )
242- appConfig .Status .Phase = AppPhaseError
243- appConfig .Status .Message = fmt .Sprintf ("stop failed: %v" , err )
244- }
245- return
246-
247- case "ACTIVATED" , "STOPPED" :
248- appConfig .Status .Phase = AppPhaseDeleting
249- appConfig .Status .Message = "Deactivating app"
250- if err := d .DeactivateApp (ctx , appID ); err != nil {
251- log .G (ctx ).Warnf ("ReconcileApp %s: deactivate failed: %v" , appID , err )
252- appConfig .Status .Phase = AppPhaseError
253- appConfig .Status .Message = fmt .Sprintf ("deactivate failed: %v" , err )
254- }
255- return
256-
257- case "DEPLOYED" :
258- appConfig .Status .Phase = AppPhaseDeleting
259- appConfig .Status .Message = "Uninstalling app"
260- if err := d .UninstallApp (ctx , appID ); err != nil {
261- log .G (ctx ).Warnf ("ReconcileApp %s: uninstall failed: %v" , appID , err )
262- appConfig .Status .Phase = AppPhaseError
263- appConfig .Status .Message = fmt .Sprintf ("uninstall failed: %v" , err )
264- }
265- return
266-
267- default :
268- // No operational data — safe to remove config.
269- appConfig .Status .Phase = AppPhaseDeleting
270- appConfig .Status .Message = "Removing config"
271- path := fmt .Sprintf ("/restconf/data/Cisco-IOS-XE-app-hosting-cfg:app-hosting-cfg-data/apps/app=%s" , appID )
272- if err := d .client .Delete (ctx , path ); err != nil {
273- log .G (ctx ).Warnf ("ReconcileApp %s: config delete failed: %v" , appID , err )
274- appConfig .Status .Phase = AppPhaseError
275- appConfig .Status .Message = fmt .Sprintf ("config delete failed: %v" , err )
276- return
277- }
278- appConfig .Status .Phase = AppPhaseDeleted
279- appConfig .Status .Message = "App fully removed"
280- log .G (ctx ).Infof ("ReconcileApp %s: fully deleted" , appID )
281- return
282- }
283- }
284- }
285-
286- // containerImagePath returns the image path for a named container in a pod spec.
287- func containerImagePath (pod * v1.Pod , containerName string ) string {
288- for i := range pod .Spec .Containers {
289- if pod .Spec .Containers [i ].Name == containerName {
290- return pod .Spec .Containers [i ].Image
291- }
292- }
293- return ""
294- }
295-
296- // getAppState returns the current operational state string for appID, or ""
297- // if the app has no oper data or the state cannot be determined.
298- func (d * XEDriver ) getAppState (ctx context.Context , appID string ) string {
299- if d .client == nil {
300- return ""
301- }
302- allOper , err := d .GetAppOperationalData (ctx )
303- if err != nil {
304- log .G (ctx ).Warnf ("Could not fetch oper data to check state of app %s: %v" , appID , err )
305- return ""
306- }
307- operData , ok := allOper [appID ]
308- if ! ok || operData == nil || operData .Details == nil || operData .Details .State == nil {
309- return ""
310- }
311- return * operData .Details .State
312- }
313-
314- // DeleteApp orchestrates a reconciler-driven teardown of the app lifecycle.
315- //
316- // It creates a transient AppHostingConfig with DesiredState=Deleted and
317- // repeatedly invokes ReconcileApp until the app reaches the Deleted phase
318- // or a timeout is exceeded.
319- //
320- // RUNNING → stop → ACTIVATED → deactivate → DEPLOYED → uninstall → (absent) → config delete
321- func (d * XEDriver ) DeleteApp (ctx context.Context , appID string ) error {
322- appConfig := & AppHostingConfig {
323- Metadata : AppHostingMetadata {AppName : appID },
324- Spec : AppHostingSpec {DesiredState : AppDesiredStateDeleted },
325- Status : AppHostingStatus {Phase : AppPhaseDeleting },
326- }
327-
328- const maxAttempts = 15
329- const reconcileInterval = 4 * time .Second
330-
331- for attempt := 1 ; attempt <= maxAttempts ; attempt ++ {
332- d .ReconcileApp (ctx , appConfig )
333-
334- if appConfig .Status .Phase == AppPhaseDeleted {
335- log .G (ctx ).Infof ("Successfully deleted app %s after %d reconcile pass(es)" , appID , attempt )
336- return nil
337- }
338-
339- log .G (ctx ).Debugf ("DeleteApp %s: attempt %d/%d, phase=%s observed=%q msg=%s" ,
340- appID , attempt , maxAttempts , appConfig .Status .Phase , appConfig .Status .ObservedState , appConfig .Status .Message )
341-
342- select {
343- case <- ctx .Done ():
344- return fmt .Errorf ("context cancelled while deleting app %s" , appID )
345- case <- time .After (reconcileInterval ):
346- }
347- }
348-
349- return fmt .Errorf ("app %s not fully deleted after %d reconcile attempts (last phase: %s, observed: %q)" ,
350- appID , maxAttempts , appConfig .Status .Phase , appConfig .Status .ObservedState )
351- }
352-
353152// WaitForAppStatus polls the device until the app reaches the expected status or times out
354153func (d * XEDriver ) WaitForAppStatus (ctx context.Context , appID string , expectedStatus string , maxWaitTime time.Duration ) error {
355154 log .G (ctx ).Infof ("Waiting for app %s to reach status: %s" , appID , expectedStatus )
0 commit comments