Skip to content

Commit a088793

Browse files
committed
Better status functions
We pass in a function to query the current state of objects, this allows us to cache the results (if using SSA) and allows for cluster targeting.
1 parent ed2d03f commit a088793

File tree

7 files changed

+285
-121
lines changed

7 files changed

+285
-121
lines changed

pkg/patterns/addon/pkg/status/aggregate.go

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,66 +25,70 @@ import (
2525

2626
appsv1 "k8s.io/api/apps/v1"
2727
corev1 "k8s.io/api/core/v1"
28+
"k8s.io/apimachinery/pkg/runtime"
29+
"k8s.io/apimachinery/pkg/runtime/schema"
30+
"k8s.io/apimachinery/pkg/types"
2831
"sigs.k8s.io/controller-runtime/pkg/client"
2932
"sigs.k8s.io/controller-runtime/pkg/log"
3033

3134
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative"
32-
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest"
3335
)
3436

3537
const successfulDeployment = appsv1.DeploymentAvailable
3638

3739
// NewAggregator provides an implementation of declarative.Reconciled that
3840
// aggregates the status of deployed objects to configure the 'Healthy'
3941
// field on an addon that derives from CommonStatus
40-
func NewAggregator(client client.Client) *aggregator {
41-
return &aggregator{client}
42+
//
43+
// TODO: Create a version that doesn't require the unused client arg
44+
func NewAggregator(_ client.Client) *aggregator {
45+
return &aggregator{}
4246
}
4347

4448
type aggregator struct {
45-
client client.Client
4649
}
4750

48-
func (a *aggregator) Reconciled(ctx context.Context, src declarative.DeclarativeObject, objs *manifest.Objects, reconcileErr error) error {
51+
func (a *aggregator) BuildStatus(ctx context.Context, info *declarative.StatusInfo) error {
4952
log := log.FromContext(ctx)
5053

5154
statusHealthy := true
5255
statusErrors := []string{}
5356

54-
if reconcileErr != nil {
57+
shouldComputeHealthFromObjects := info.Manifest != nil && info.LiveObjects != nil
58+
if info.Err != nil {
5559
statusHealthy = false
60+
shouldComputeHealthFromObjects = false
5661
}
5762

58-
for _, o := range objs.GetItems() {
59-
gk := o.Group + "/" + o.Kind
60-
healthy := true
61-
objKey := client.ObjectKey{
62-
Name: o.GetName(),
63-
Namespace: o.GetNamespace(),
64-
}
65-
// If the namespace isn't set on the object, we would want to use the namespace of src
66-
if objKey.Namespace == "" {
67-
objKey.Namespace = src.GetNamespace()
68-
}
69-
var err error
70-
switch gk {
71-
case "/Service":
72-
healthy, err = a.service(ctx, objKey)
73-
case "extensions/Deployment", "apps/Deployment":
74-
healthy, err = a.deployment(ctx, objKey)
75-
default:
76-
log.WithValues("type", gk).V(2).Info("type not implemented for status aggregation, skipping")
77-
}
78-
79-
statusHealthy = statusHealthy && healthy
80-
if err != nil {
81-
statusErrors = append(statusErrors, fmt.Sprintf("%v", err))
63+
if shouldComputeHealthFromObjects {
64+
for _, o := range info.Manifest.GetItems() {
65+
gvk := o.GroupVersionKind()
66+
nn := o.NamespacedName()
67+
68+
log := log.WithValues("kind", gvk.Kind).WithValues("name", nn.Name).WithValues("namespace", nn.Namespace)
69+
70+
healthy := true
71+
72+
var err error
73+
switch gvk.Group + "/" + gvk.Kind {
74+
case "/Service":
75+
healthy, err = a.serviceIsHealthy(ctx, info.LiveObjects, gvk, nn)
76+
case "extensions/Deployment", "apps/Deployment":
77+
healthy, err = a.deploymentIsHealthy(ctx, info.LiveObjects, gvk, nn)
78+
default:
79+
log.V(4).Info("type not implemented for status aggregation, skipping")
80+
}
81+
82+
statusHealthy = statusHealthy && healthy
83+
if err != nil {
84+
statusErrors = append(statusErrors, fmt.Sprintf("%v", err))
85+
}
8286
}
8387
}
8488

85-
log.WithValues("object", src).WithValues("status", statusHealthy).V(2).Info("built status")
89+
log.WithValues("status", statusHealthy).V(2).Info("built status")
8690

87-
currentStatus, err := utils.GetCommonStatus(src)
91+
currentStatus, err := utils.GetCommonStatus(info.Subject)
8892
if err != nil {
8993
return err
9094
}
@@ -94,27 +98,24 @@ func (a *aggregator) Reconciled(ctx context.Context, src declarative.Declarative
9498
status.Errors = statusErrors
9599

96100
if !reflect.DeepEqual(status, currentStatus) {
97-
err := utils.SetCommonStatus(src, status)
101+
err := utils.SetCommonStatus(info.Subject, status)
98102
if err != nil {
99103
return err
100104
}
101-
102-
log.WithValues("name", src.GetName()).WithValues("status", status).Info("updating status")
103-
err = a.client.Status().Update(ctx, src)
104-
if err != nil {
105-
log.Error(err, "updating status")
106-
return err
107-
}
108105
}
109106

110107
return nil
111108
}
112109

113-
func (a *aggregator) deployment(ctx context.Context, key client.ObjectKey) (bool, error) {
114-
dep := &appsv1.Deployment{}
110+
func (a *aggregator) deploymentIsHealthy(ctx context.Context, liveObjects declarative.LiveObjectReader, gvk schema.GroupVersionKind, nn types.NamespacedName) (bool, error) {
111+
u, err := liveObjects(ctx, gvk, nn)
112+
if err != nil {
113+
return false, fmt.Errorf("error reading deployment: %w", err)
114+
}
115115

116-
if err := a.client.Get(ctx, key, dep); err != nil {
117-
return false, fmt.Errorf("error reading deployment (%s): %v", key, err)
116+
dep := &appsv1.Deployment{}
117+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, dep); err != nil {
118+
return false, fmt.Errorf("error converting deployment from unstructured: %w", err)
118119
}
119120

120121
for _, cond := range dep.Status.Conditions {
@@ -123,14 +124,13 @@ func (a *aggregator) deployment(ctx context.Context, key client.ObjectKey) (bool
123124
}
124125
}
125126

126-
return false, fmt.Errorf("deployment (%s) does not meet condition: %s", key, successfulDeployment)
127+
return false, fmt.Errorf("deployment does not meet condition: %s", successfulDeployment)
127128
}
128129

129-
func (a *aggregator) service(ctx context.Context, key client.ObjectKey) (bool, error) {
130-
svc := &corev1.Service{}
131-
err := a.client.Get(ctx, key, svc)
130+
func (a *aggregator) serviceIsHealthy(ctx context.Context, liveObjects declarative.LiveObjectReader, gvk schema.GroupVersionKind, nn types.NamespacedName) (bool, error) {
131+
_, err := liveObjects(ctx, gvk, nn)
132132
if err != nil {
133-
return false, fmt.Errorf("error reading service (%s): %v", key, err)
133+
return false, fmt.Errorf("error reading service: %w", err)
134134
}
135135

136136
return true, nil

pkg/patterns/addon/pkg/status/basic.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ import (
2121
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative"
2222
)
2323

24-
// Deprecated: This function exists for backward compatibility, please use NewKstatusCheck
25-
2624
// NewBasic provides an implementation of declarative.Status that
2725
// performs no preflight checks.
26+
//
27+
// Deprecated: This function exists for backward compatibility, please use NewKstatusCheck
2828
func NewBasic(client client.Client) declarative.Status {
2929
return &declarative.StatusBuilder{
30-
ReconciledImpl: NewAggregator(client),
30+
BuildStatusImpl: NewAggregator(client),
3131
// no preflight checks
3232
}
3333
}
@@ -41,14 +41,15 @@ func NewBasicVersionChecks(client client.Client, version string) (declarative.St
4141
}
4242

4343
return &declarative.StatusBuilder{
44-
ReconciledImpl: NewAggregator(client),
44+
BuildStatusImpl: NewAggregator(client),
4545
VersionCheckImpl: v,
4646
// no preflight checks
4747
}, nil
4848
}
4949

50+
// TODO: Create a version that doesn't take (unusued) client & reconciler args
5051
func NewKstatusCheck(client client.Client, d *declarative.Reconciler) declarative.Status {
5152
return &declarative.StatusBuilder{
52-
ReconciledImpl: NewKstatusAgregator(client, d),
53+
BuildStatusImpl: NewKstatusAgregator(client, d),
5354
}
5455
}

pkg/patterns/addon/pkg/status/kstatus.go

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,86 @@ package status
22

33
import (
44
"context"
5-
"fmt"
65

76
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
87
"sigs.k8s.io/controller-runtime/pkg/client"
98
"sigs.k8s.io/controller-runtime/pkg/log"
109

1110
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/utils"
1211
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative"
13-
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest"
1412
)
1513

1614
type kstatusAggregator struct {
17-
client client.Client
18-
reconciler *declarative.Reconciler
1915
}
2016

21-
func NewKstatusAgregator(c client.Client, reconciler *declarative.Reconciler) *kstatusAggregator {
22-
return &kstatusAggregator{client: c, reconciler: reconciler}
17+
// TODO: Create a version that doesn't need reconciler or client?
18+
func NewKstatusAgregator(_ client.Client, _ *declarative.Reconciler) *kstatusAggregator {
19+
return &kstatusAggregator{}
2320
}
2421

25-
func (k *kstatusAggregator) Reconciled(ctx context.Context, src declarative.DeclarativeObject,
26-
objs *manifest.Objects, _ error) error {
22+
func (k *kstatusAggregator) BuildStatus(ctx context.Context, info *declarative.StatusInfo) error {
2723
log := log.FromContext(ctx)
2824

29-
statusMap := make(map[status.Status]bool)
30-
for _, object := range objs.Items {
25+
currentStatus, err := utils.GetCommonStatus(info.Subject)
26+
if err != nil {
27+
log.Error(err, "error retrieving status")
28+
return err
29+
}
3130

32-
unstruct, err := declarative.GetObjectFromCluster(object, k.reconciler)
33-
if err != nil {
34-
log.WithValues("object", object.Kind+"/"+object.GetName()).Error(err, "Unable to get status of object")
35-
return err
31+
shouldComputeHealthFromObjects := info.Manifest != nil && info.LiveObjects != nil
32+
if info.Err != nil {
33+
currentStatus.Healthy = false
34+
switch info.KnownError {
35+
case declarative.KnownErrorApplyFailed:
36+
currentStatus.Phase = "Applying"
37+
// computeHealthFromObjects if we can (leave unchanged)
38+
case declarative.KnownErrorVersionCheckFailed:
39+
currentStatus.Phase = "VersionMismatch"
40+
shouldComputeHealthFromObjects = false
41+
default:
42+
currentStatus.Phase = "InternalError"
43+
shouldComputeHealthFromObjects = false
3644
}
45+
}
3746

38-
res, err := status.Compute(unstruct)
39-
if err != nil {
40-
log.WithValues("kind", object.Kind).WithValues("name", object.GetName()).WithValues("status", res.Status).WithValues(
41-
"message", res.Message).Info("Got status of resource:")
42-
statusMap[status.NotFoundStatus] = true
47+
if shouldComputeHealthFromObjects {
48+
statusMap := make(map[status.Status]bool)
49+
for _, object := range info.Manifest.Items {
50+
gvk := object.GroupVersionKind()
51+
nn := object.NamespacedName()
52+
53+
log := log.WithValues("kind", gvk.Kind).WithValues("name", nn.Name).WithValues("namespace", nn.Namespace)
54+
55+
unstruct, err := info.LiveObjects(ctx, gvk, nn)
56+
if err != nil {
57+
log.Error(err, "unable to get object to determine status")
58+
statusMap[status.UnknownStatus] = true
59+
continue
60+
}
61+
62+
res, err := status.Compute(unstruct)
63+
if err != nil {
64+
log.Error(err, "error getting status of resource")
65+
statusMap[status.UnknownStatus] = true
66+
} else if res != nil {
67+
log.WithValues("status", res.Status).WithValues("message", res.Message).Info("Got status of resource:")
68+
statusMap[res.Status] = true
69+
} else {
70+
log.Info("resource status was nil")
71+
statusMap[status.UnknownStatus] = true
72+
}
4373
}
44-
if res != nil {
45-
log.WithValues("kind", object.Kind).WithValues("name", object.GetName()).WithValues("status", res.Status).WithValues("message", res.Message).Info("Got status of resource:")
46-
statusMap[res.Status] = true
74+
75+
aggregatedPhase := string(aggregateStatus(statusMap))
76+
77+
if currentStatus.Phase != aggregatedPhase {
78+
currentStatus.Phase = aggregatedPhase
4779
}
4880
}
4981

50-
aggregatedPhase := string(aggregateStatus(statusMap))
51-
52-
currentStatus, err := utils.GetCommonStatus(src)
53-
if err != nil {
54-
log.Error(err, "error retrieving status")
82+
if err := utils.SetCommonStatus(info.Subject, currentStatus); err != nil {
5583
return err
5684
}
57-
if currentStatus.Phase != aggregatedPhase {
58-
currentStatus.Phase = aggregatedPhase
59-
err := utils.SetCommonStatus(src, currentStatus)
60-
if err != nil {
61-
return err
62-
}
63-
log.WithValues("name", src.GetName()).WithValues("phase", aggregatedPhase).Info("updating status")
64-
err = k.client.Status().Update(ctx, src)
65-
if err != nil {
66-
log.Error(err, "error updating status")
67-
return fmt.Errorf("error error status: %v", err)
68-
}
69-
}
7085

7186
return nil
7287
}

pkg/patterns/declarative/pkg/manifest/objects.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2929
"k8s.io/apimachinery/pkg/runtime/schema"
30+
"k8s.io/apimachinery/pkg/types"
3031
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
3132
"sigs.k8s.io/controller-runtime/pkg/log"
3233
)
@@ -320,6 +321,16 @@ func (o *Object) GroupVersionKind() schema.GroupVersionKind {
320321
return o.object.GroupVersionKind()
321322
}
322323

324+
// NamespacedName returns the name and namespace of the object in a types.NamespacedName.
325+
// Note that this reflects the state of the object; if the namespace is not yet set,
326+
// it will returned as "" here, even though it would likely be defaulted before apply.
327+
func (o *Object) NamespacedName() types.NamespacedName {
328+
return types.NamespacedName{
329+
Namespace: o.GetNamespace(),
330+
Name: o.GetName(),
331+
}
332+
}
333+
323334
func (o *Objects) JSONManifest() (string, error) {
324335
var b bytes.Buffer
325336

0 commit comments

Comments
 (0)