Skip to content

Commit 806bbd0

Browse files
authored
track status of each deployed component in AppWrapper Status (#139)
1 parent 5addc12 commit 806bbd0

File tree

5 files changed

+212
-31
lines changed

5 files changed

+212
-31
lines changed

api/v1beta2/appwrapper_types.go

+28
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,34 @@ type AppWrapperStatus struct {
105105
//+listType=map
106106
//+listMapKey=type
107107
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
108+
109+
// ComponentStatus parallels the Components array in the Spec and tracks the actually deployed resources
110+
ComponentStatus []AppWrapperComponentStatus `json:"componentStatus,omitempty"`
111+
}
112+
113+
// AppWrapperComponentStatus tracks the status of a single managed Component
114+
type AppWrapperComponentStatus struct {
115+
// Name is the name of the Component
116+
Name string `json:"name"`
117+
118+
// Kind is the Kind of the Component
119+
Kind string `json:"kind"`
120+
121+
// APIVersion is the APIVersion of the Component
122+
APIVersion string `json:"apiVersion"`
123+
124+
// Conditions hold the latest available observations of the Component's current state.
125+
//
126+
// The type of the condition could be:
127+
//
128+
// - ResourcesDeployed: The component is deployed on the cluster
129+
//
130+
//+optional
131+
//+patchMergeKey=type
132+
//+patchStrategy=merge
133+
//+listType=map
134+
//+listMapKey=type
135+
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
108136
}
109137

110138
// AppWrapperPhase is the phase of the appwrapper

api/v1beta2/zz_generated.deepcopy.go

+29
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/workload.codeflare.dev_appwrappers.yaml

+105
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,111 @@ spec:
165165
status:
166166
description: AppWrapperStatus defines the observed state of the appwrapper
167167
properties:
168+
componentStatus:
169+
description: ComponentStatus parallels the Components array in the
170+
Spec and tracks the actually deployed resources
171+
items:
172+
description: AppWrapperComponentStatus tracks the status of a single
173+
managed Component
174+
properties:
175+
apiVersion:
176+
description: APIVersion is the APIVersion of the Component
177+
type: string
178+
conditions:
179+
description: |-
180+
Conditions hold the latest available observations of the Component's current state.
181+
182+
183+
The type of the condition could be:
184+
185+
186+
- ResourcesDeployed: The component is deployed on the cluster
187+
items:
188+
description: "Condition contains details for one aspect of
189+
the current state of this API Resource.\n---\nThis struct
190+
is intended for direct use as an array at the field path
191+
.status.conditions. For example,\n\n\n\ttype FooStatus
192+
struct{\n\t // Represents the observations of a foo's
193+
current state.\n\t // Known .status.conditions.type are:
194+
\"Available\", \"Progressing\", and \"Degraded\"\n\t //
195+
+patchMergeKey=type\n\t // +patchStrategy=merge\n\t //
196+
+listType=map\n\t // +listMapKey=type\n\t Conditions
197+
[]metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\"
198+
patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
199+
\ // other fields\n\t}"
200+
properties:
201+
lastTransitionTime:
202+
description: |-
203+
lastTransitionTime is the last time the condition transitioned from one status to another.
204+
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
205+
format: date-time
206+
type: string
207+
message:
208+
description: |-
209+
message is a human readable message indicating details about the transition.
210+
This may be an empty string.
211+
maxLength: 32768
212+
type: string
213+
observedGeneration:
214+
description: |-
215+
observedGeneration represents the .metadata.generation that the condition was set based upon.
216+
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
217+
with respect to the current state of the instance.
218+
format: int64
219+
minimum: 0
220+
type: integer
221+
reason:
222+
description: |-
223+
reason contains a programmatic identifier indicating the reason for the condition's last transition.
224+
Producers of specific condition types may define expected values and meanings for this field,
225+
and whether the values are considered a guaranteed API.
226+
The value should be a CamelCase string.
227+
This field may not be empty.
228+
maxLength: 1024
229+
minLength: 1
230+
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
231+
type: string
232+
status:
233+
description: status of the condition, one of True, False,
234+
Unknown.
235+
enum:
236+
- "True"
237+
- "False"
238+
- Unknown
239+
type: string
240+
type:
241+
description: |-
242+
type of condition in CamelCase or in foo.example.com/CamelCase.
243+
---
244+
Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
245+
useful (see .node.status.conditions), the ability to deconflict is important.
246+
The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
247+
maxLength: 316
248+
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
249+
type: string
250+
required:
251+
- lastTransitionTime
252+
- message
253+
- reason
254+
- status
255+
- type
256+
type: object
257+
type: array
258+
x-kubernetes-list-map-keys:
259+
- type
260+
x-kubernetes-list-type: map
261+
kind:
262+
description: Kind is the Kind of the Component
263+
type: string
264+
name:
265+
description: Name is the name of the Component
266+
type: string
267+
required:
268+
- apiVersion
269+
- kind
270+
- name
271+
type: object
272+
type: array
168273
conditions:
169274
description: |-
170275
Conditions hold the latest available observations of the AppWrapper current state.

internal/controller/appwrapper/appwrapper_controller.go

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ func (r *AppWrapperReconciler) Reconcile(ctx context.Context, req ctrl.Request)
144144
return ctrl.Result{}, err
145145
}
146146
}
147+
aw.Status.ComponentStatus = make([]workloadv1beta2.AppWrapperComponentStatus, len(aw.Spec.Components))
147148
return r.updateStatus(ctx, aw, workloadv1beta2.AppWrapperSuspended)
148149

149150
case workloadv1beta2.AppWrapperSuspended: // no components deployed

internal/controller/appwrapper/resource_management.go

+49-31
Original file line numberDiff line numberDiff line change
@@ -179,42 +179,67 @@ func (r *AppWrapperReconciler) createComponent(ctx context.Context, aw *workload
179179

180180
func (r *AppWrapperReconciler) createComponents(ctx context.Context, aw *workloadv1beta2.AppWrapper) (error, bool) {
181181
for componentIdx := range aw.Spec.Components {
182-
_, err, fatal := r.createComponent(ctx, aw, componentIdx)
183-
if err != nil {
184-
return err, fatal
182+
if !meta.IsStatusConditionTrue(aw.Status.ComponentStatus[componentIdx].Conditions, string(workloadv1beta2.ResourcesDeployed)) {
183+
obj, err, fatal := r.createComponent(ctx, aw, componentIdx)
184+
if err != nil {
185+
return err, fatal
186+
}
187+
aw.Status.ComponentStatus[componentIdx].Name = obj.GetName()
188+
aw.Status.ComponentStatus[componentIdx].Kind = obj.GetKind()
189+
aw.Status.ComponentStatus[componentIdx].APIVersion = obj.GetAPIVersion()
190+
meta.SetStatusCondition(&aw.Status.ComponentStatus[componentIdx].Conditions, metav1.Condition{
191+
Type: string(workloadv1beta2.ResourcesDeployed),
192+
Status: metav1.ConditionTrue,
193+
Reason: "CompononetCreated",
194+
})
185195
}
186196
}
187197
return nil, false
188198
}
189199

190200
func (r *AppWrapperReconciler) deleteComponents(ctx context.Context, aw *workloadv1beta2.AppWrapper) bool {
201+
deleteIfPresent := func(idx int, opts ...client.DeleteOption) bool {
202+
cs := &aw.Status.ComponentStatus[idx]
203+
if !meta.IsStatusConditionTrue(cs.Conditions, string(workloadv1beta2.ResourcesDeployed)) {
204+
return false // not present
205+
}
206+
obj := &metav1.PartialObjectMetadata{
207+
TypeMeta: metav1.TypeMeta{Kind: cs.Kind, APIVersion: cs.APIVersion},
208+
ObjectMeta: metav1.ObjectMeta{Name: cs.Name, Namespace: aw.Namespace},
209+
}
210+
if err := r.Delete(ctx, obj, opts...); err != nil {
211+
if apierrors.IsNotFound(err) {
212+
// Has already been undeployed; update componentStatus and return not present
213+
meta.SetStatusCondition(&cs.Conditions, metav1.Condition{
214+
Type: string(workloadv1beta2.ResourcesDeployed),
215+
Status: metav1.ConditionFalse,
216+
Reason: "CompononetDeleted",
217+
})
218+
return false
219+
} else {
220+
log.FromContext(ctx).Error(err, "Deletion error")
221+
return true // unexpected error ==> still present
222+
}
223+
}
224+
return true // still present
225+
}
226+
191227
meta.SetStatusCondition(&aw.Status.Conditions, metav1.Condition{
192228
Type: string(workloadv1beta2.DeletingResources),
193229
Status: metav1.ConditionTrue,
194230
Reason: "DeletionInitiated",
195231
})
196-
log := log.FromContext(ctx)
197-
remaining := 0
198-
for _, component := range aw.Spec.Components {
199-
obj, err := parseComponent(aw, component.Template.Raw)
200-
if err != nil {
201-
log.Error(err, "Parsing error")
202-
continue
203-
}
204-
if err := r.Delete(ctx, obj, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil {
205-
if !apierrors.IsNotFound(err) {
206-
log.Error(err, "Deletion error")
207-
}
208-
continue
209-
}
210-
remaining++ // no error deleting resource, resource therefore still exists
232+
233+
componentsRemaining := false
234+
for componentIdx := range aw.Spec.Components {
235+
componentsRemaining = deleteIfPresent(componentIdx, client.PropagationPolicy(metav1.DeletePropagationBackground)) || componentsRemaining
211236
}
212237

213238
deletionGracePeriod := r.forcefulDeletionGraceDuration(ctx, aw)
214239
whenInitiated := meta.FindStatusCondition(aw.Status.Conditions, string(workloadv1beta2.DeletingResources)).LastTransitionTime
215240
gracePeriodExpired := time.Now().After(whenInitiated.Time.Add(deletionGracePeriod))
216241

217-
if remaining > 0 && !gracePeriodExpired {
242+
if componentsRemaining && !gracePeriodExpired {
218243
// Resources left and deadline hasn't expired, just requeue the deletion
219244
return false
220245
}
@@ -224,10 +249,10 @@ func (r *AppWrapperReconciler) deleteComponents(ctx context.Context, aw *workloa
224249
client.UnsafeDisableDeepCopy,
225250
client.InNamespace(aw.Namespace),
226251
client.MatchingLabels{AppWrapperLabel: aw.Name}); err != nil {
227-
log.Error(err, "Pod list error")
252+
log.FromContext(ctx).Error(err, "Pod list error")
228253
}
229254

230-
if remaining == 0 && len(pods.Items) == 0 {
255+
if !componentsRemaining && len(pods.Items) == 0 {
231256
// no resources or pods left; deletion is complete
232257
clearCondition(aw, workloadv1beta2.DeletingResources, "DeletionComplete", "")
233258
return true
@@ -238,20 +263,13 @@ func (r *AppWrapperReconciler) deleteComponents(ctx context.Context, aw *workloa
238263
// force deletion of pods first
239264
for _, pod := range pods.Items {
240265
if err := r.Delete(ctx, &pod, client.GracePeriodSeconds(0)); err != nil {
241-
log.Error(err, "Forceful pod deletion error")
266+
log.FromContext(ctx).Error(err, "Forceful pod deletion error")
242267
}
243268
}
244269
} else {
245270
// force deletion of wrapped resources once pods are gone
246-
for _, component := range aw.Spec.Components {
247-
obj, err := parseComponent(aw, component.Template.Raw)
248-
if err != nil {
249-
log.Error(err, "Parsing error")
250-
continue
251-
}
252-
if err := r.Delete(ctx, obj, client.GracePeriodSeconds(0)); err != nil && !apierrors.IsNotFound(err) {
253-
log.Error(err, "Forceful deletion error")
254-
}
271+
for componentIdx := range aw.Spec.Components {
272+
_ = deleteIfPresent(componentIdx, client.GracePeriodSeconds(0))
255273
}
256274
}
257275
}

0 commit comments

Comments
 (0)