Skip to content

Commit 5437158

Browse files
committed
pkg/status,internal/pkg/scaffold,test,example: add status conditions helpers
1 parent 16b5c11 commit 5437158

File tree

12 files changed

+725
-12
lines changed

12 files changed

+725
-12
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Added
44

5+
- Adds `pkg/status` with several new types and interfaces that can be used in `Status` structs to simplify handling of [status conditions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties). ([#1143](https://github.com/operator-framework/operator-sdk/pull/1143))
6+
57
### Changed
68

79
### Deprecated

example/memcached-operator/memcached_controller.go.tmpl

+47-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package memcached
33
import (
44
"context"
55
"reflect"
6+
"sort"
67

78
cachev1alpha1 "github.com/example-inc/memcached-operator/pkg/apis/cache/v1alpha1"
89

10+
"github.com/operator-framework/operator-sdk/pkg/status"
911
appsv1 "k8s.io/api/apps/v1"
1012
corev1 "k8s.io/api/core/v1"
1113
"k8s.io/apimachinery/pkg/api/errors"
@@ -116,12 +118,18 @@ func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Res
116118
err = r.client.Create(context.TODO(), dep)
117119
if err != nil {
118120
reqLogger.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
121+
if err := r.setStatusNotReady(context.TODO(), memcached, err.Error()); err != nil {
122+
return reconcile.Result{}, err
123+
}
119124
return reconcile.Result{}, err
120125
}
121126
// Deployment created successfully - return and requeue
122127
return reconcile.Result{Requeue: true}, nil
123128
} else if err != nil {
124129
reqLogger.Error(err, "Failed to get Deployment")
130+
if err := r.setStatusNotReady(context.TODO(), memcached, err.Error()); err != nil {
131+
return reconcile.Result{}, err
132+
}
125133
return reconcile.Result{}, err
126134
}
127135

@@ -132,6 +140,9 @@ func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Res
132140
err = r.client.Update(context.TODO(), found)
133141
if err != nil {
134142
reqLogger.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
143+
if err := r.setStatusNotReady(context.TODO(), memcached, err.Error()); err != nil {
144+
return reconcile.Result{}, err
145+
}
135146
return reconcile.Result{}, err
136147
}
137148
// Spec updated - return and requeue
@@ -150,17 +161,21 @@ func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Res
150161
return reconcile.Result{}, err
151162
}
152163
podNames := getPodNames(podList.Items)
164+
sort.Strings(podNames)
153165

154166
// Update status.Nodes if needed
155167
if !reflect.DeepEqual(podNames, memcached.Status.Nodes) {
156168
memcached.Status.Nodes = podNames
157-
err := r.client.Status().Update(context.TODO(), memcached)
169+
err := r.updateStatus(context.TODO(), memcached)
158170
if err != nil {
159171
reqLogger.Error(err, "Failed to update Memcached status")
160172
return reconcile.Result{}, err
161173
}
162174
}
163175

176+
if err := r.setStatusReady(context.TODO(), memcached); err != nil {
177+
return reconcile.Result{}, err
178+
}
164179
return reconcile.Result{}, nil
165180
}
166181

@@ -216,3 +231,34 @@ func getPodNames(pods []corev1.Pod) []string {
216231
}
217232
return podNames
218233
}
234+
235+
func (r *ReconcileMemcached) updateStatus(ctx context.Context, m *cachev1alpha1.Memcached) error {
236+
// Ensure required status fields are initialized
237+
if len(m.Status.Nodes) == 0 {
238+
m.Status.Nodes = []string{}
239+
}
240+
if len(m.Status.Conditions) == 0 {
241+
m.Status.Conditions = status.Conditions{}
242+
}
243+
return r.client.Status().Update(ctx, m)
244+
}
245+
246+
func (r *ReconcileMemcached) setStatusReady(ctx context.Context, instance *cachev1alpha1.Memcached) error {
247+
if instance.Status.Conditions.SetCondition(status.ReadyCondition(corev1.ConditionTrue)) {
248+
if err := r.client.Status().Update(ctx, instance); err != nil {
249+
return err
250+
}
251+
}
252+
return nil
253+
}
254+
255+
func (r *ReconcileMemcached) setStatusNotReady(ctx context.Context, instance *cachev1alpha1.Memcached, message string) error {
256+
notReady := status.ReadyCondition(corev1.ConditionFalse)
257+
notReady.Message = message
258+
if instance.Status.Conditions.SetCondition(notReady) {
259+
if err := r.client.Status().Update(ctx, instance); err != nil {
260+
return err
261+
}
262+
}
263+
return nil
264+
}

internal/scaffold/controller_kind.go

+48-4
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ func getCustomAPIImportPathAndIdent(m string) (p string, id string, err error) {
122122
}
123123

124124
var controllerKindImports = map[string]string{
125+
"github.com/operator-framework/operator-sdk/pkg/status": "",
125126
"k8s.io/api/core/v1": "corev1",
126127
"k8s.io/apimachinery/pkg/api/errors": "",
127128
"k8s.io/apimachinery/pkg/apis/meta/v1": "metav1",
@@ -141,6 +142,7 @@ const controllerKindTemplate = `package {{ .Resource.LowerKind }}
141142
142143
import (
143144
"context"
145+
"fmt"
144146
145147
{{range $p, $i := .ImportMap -}}
146148
{{$i}} "{{$p}}"
@@ -233,6 +235,9 @@ func (r *Reconcile{{ .Resource.Kind }}) Reconcile(request reconcile.Request) (re
233235
234236
// Set {{ .Resource.Kind }} instance as the owner and controller
235237
if err := controllerutil.SetControllerReference(instance, pod, r.scheme); err != nil {
238+
if err := r.setStatusNotReady(context.TODO(), instance, err.Error()); err != nil {
239+
return reconcile.Result{}, err
240+
}
236241
return reconcile.Result{}, err
237242
}
238243
@@ -243,17 +248,36 @@ func (r *Reconcile{{ .Resource.Kind }}) Reconcile(request reconcile.Request) (re
243248
reqLogger.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
244249
err = r.client.Create(context.TODO(), pod)
245250
if err != nil {
251+
if err := r.setStatusNotReady(context.TODO(), instance, err.Error()); err != nil {
252+
return reconcile.Result{}, err
253+
}
246254
return reconcile.Result{}, err
247255
}
248256
249-
// Pod created successfully - don't requeue
250-
return reconcile.Result{}, nil
257+
// Pod created successfully - requeue
258+
return reconcile.Result{Requeue: true}, nil
251259
} else if err != nil {
260+
if err := r.setStatusNotReady(context.TODO(), instance, err.Error()); err != nil {
261+
return reconcile.Result{}, err
262+
}
252263
return reconcile.Result{}, err
253264
}
254265
255-
// Pod already exists - don't requeue
256-
reqLogger.Info("Skip reconcile: Pod already exists", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name)
266+
// If Pod is not running, set Ready condition to False
267+
if found.Status.Phase != corev1.PodRunning {
268+
if err := r.setStatusNotReady(context.TODO(), instance, fmt.Sprintf("pod %s not running", found.Name)); err != nil {
269+
return reconcile.Result{}, err
270+
}
271+
272+
reqLogger.Info("Pod already exists, but is not running", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name, "Pod.Phase", found.Status.Phase)
273+
return reconcile.Result{}, nil
274+
}
275+
276+
// Pod already exists and is running - set status ready
277+
reqLogger.Info("Skip reconcile: Pod already exists", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name, "Pod.Phase", found.Status.Phase)
278+
if err := r.setStatusReady(context.TODO(), instance); err != nil {
279+
return reconcile.Result{}, err
280+
}
257281
return reconcile.Result{}, nil
258282
}
259283
@@ -279,4 +303,24 @@ func newPodForCR(cr *{{ .GoImportIdent }}.{{ .Resource.Kind }}) *corev1.Pod {
279303
},
280304
}
281305
}
306+
307+
func (r *Reconcile{{ .Resource.Kind }}) setStatusReady(ctx context.Context, instance *{{ .Resource.GoImportGroup}}{{ .Resource.Version }}.{{ .Resource.Kind }}) error {
308+
if instance.Status.Conditions.SetCondition(status.ReadyCondition(corev1.ConditionTrue)) {
309+
if err := r.client.Status().Update(ctx, instance); err != nil {
310+
return err
311+
}
312+
}
313+
return nil
314+
}
315+
316+
func (r *Reconcile{{ .Resource.Kind }}) setStatusNotReady(ctx context.Context, instance *{{ .Resource.GoImportGroup}}{{ .Resource.Version }}.{{ .Resource.Kind }}, message string) error {
317+
notReady := status.ReadyCondition(corev1.ConditionFalse)
318+
notReady.Message = message
319+
if instance.Status.Conditions.SetCondition(notReady) {
320+
if err := r.client.Status().Update(ctx, instance); err != nil {
321+
return err
322+
}
323+
}
324+
return nil
325+
}
282326
`

internal/scaffold/controller_kind_test.go

+48-4
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ const controllerKindExp = `package appservice
4141
4242
import (
4343
"context"
44+
"fmt"
4445
4546
appv1alpha1 "github.com/example-inc/app-operator/pkg/apis/app/v1alpha1"
47+
"github.com/operator-framework/operator-sdk/pkg/status"
4648
corev1 "k8s.io/api/core/v1"
4749
"k8s.io/apimachinery/pkg/api/errors"
4850
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -144,6 +146,9 @@ func (r *ReconcileAppService) Reconcile(request reconcile.Request) (reconcile.Re
144146
145147
// Set AppService instance as the owner and controller
146148
if err := controllerutil.SetControllerReference(instance, pod, r.scheme); err != nil {
149+
if err := r.setStatusNotReady(context.TODO(), instance, err.Error()); err != nil {
150+
return reconcile.Result{}, err
151+
}
147152
return reconcile.Result{}, err
148153
}
149154
@@ -154,17 +159,36 @@ func (r *ReconcileAppService) Reconcile(request reconcile.Request) (reconcile.Re
154159
reqLogger.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
155160
err = r.client.Create(context.TODO(), pod)
156161
if err != nil {
162+
if err := r.setStatusNotReady(context.TODO(), instance, err.Error()); err != nil {
163+
return reconcile.Result{}, err
164+
}
157165
return reconcile.Result{}, err
158166
}
159167
160-
// Pod created successfully - don't requeue
161-
return reconcile.Result{}, nil
168+
// Pod created successfully - requeue
169+
return reconcile.Result{Requeue: true}, nil
162170
} else if err != nil {
171+
if err := r.setStatusNotReady(context.TODO(), instance, err.Error()); err != nil {
172+
return reconcile.Result{}, err
173+
}
163174
return reconcile.Result{}, err
164175
}
165176
166-
// Pod already exists - don't requeue
167-
reqLogger.Info("Skip reconcile: Pod already exists", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name)
177+
// If Pod is not running, set Ready condition to False
178+
if found.Status.Phase != corev1.PodRunning {
179+
if err := r.setStatusNotReady(context.TODO(), instance, fmt.Sprintf("pod %s not running", found.Name)); err != nil {
180+
return reconcile.Result{}, err
181+
}
182+
183+
reqLogger.Info("Pod already exists, but is not running", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name, "Pod.Phase", found.Status.Phase)
184+
return reconcile.Result{}, nil
185+
}
186+
187+
// Pod already exists and is running - set status ready
188+
reqLogger.Info("Skip reconcile: Pod already exists", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name, "Pod.Phase", found.Status.Phase)
189+
if err := r.setStatusReady(context.TODO(), instance); err != nil {
190+
return reconcile.Result{}, err
191+
}
168192
return reconcile.Result{}, nil
169193
}
170194
@@ -190,6 +214,26 @@ func newPodForCR(cr *appv1alpha1.AppService) *corev1.Pod {
190214
},
191215
}
192216
}
217+
218+
func (r *ReconcileAppService) setStatusReady(ctx context.Context, instance *appv1alpha1.AppService) error {
219+
if instance.Status.Conditions.SetCondition(status.ReadyCondition(corev1.ConditionTrue)) {
220+
if err := r.client.Status().Update(ctx, instance); err != nil {
221+
return err
222+
}
223+
}
224+
return nil
225+
}
226+
227+
func (r *ReconcileAppService) setStatusNotReady(ctx context.Context, instance *appv1alpha1.AppService, message string) error {
228+
notReady := status.ReadyCondition(corev1.ConditionFalse)
229+
notReady.Message = message
230+
if instance.Status.Conditions.SetCondition(notReady) {
231+
if err := r.client.Status().Update(ctx, instance); err != nil {
232+
return err
233+
}
234+
}
235+
return nil
236+
}
193237
`
194238

195239
func TestGetCustomAPIImportPathAndIdentifier(t *testing.T) {

internal/scaffold/types.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const typesTemplate = `package {{ .Resource.Version }}
4646
4747
import (
4848
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
49+
50+
"github.com/operator-framework/operator-sdk/pkg/status"
4951
)
5052
5153
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
@@ -60,9 +62,15 @@ type {{.Resource.Kind}}Spec struct {
6062
6163
// {{.Resource.Kind}}Status defines the observed state of {{.Resource.Kind}}
6264
type {{.Resource.Kind}}Status struct {
63-
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
65+
// The status.Conditions type provides helpers for managing status conditions for the custom resource,
66+
// such as adding and removing them and checking their status. Conditions are serialized as an array to
67+
// align with Kubernetes conventions.
68+
Conditions status.Conditions ` + "`" + `json:"conditions"` + "`" + `
69+
70+
// INSERT ADDITIONAL STATUS FIELDS - define observed state of cluster
6471
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
6572
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
73+
// For status conventions, see https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
6674
}
6775
6876
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

internal/scaffold/types_test.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const typesExp = `package v1alpha1
4141
4242
import (
4343
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
44+
45+
"github.com/operator-framework/operator-sdk/pkg/status"
4446
)
4547
4648
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
@@ -55,9 +57,15 @@ type AppServiceSpec struct {
5557
5658
// AppServiceStatus defines the observed state of AppService
5759
type AppServiceStatus struct {
58-
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
60+
// The status.Conditions type provides helpers for managing status conditions for the custom resource,
61+
// such as adding and removing them and checking their status. Conditions are serialized as an array to
62+
// align with Kubernetes conventions.
63+
Conditions status.Conditions ` + "`" + `json:"conditions"` + "`" + `
64+
65+
// INSERT ADDITIONAL STATUS FIELDS - define observed state of cluster
5966
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
6067
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
68+
// For status conventions, see https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
6169
}
6270
6371
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

0 commit comments

Comments
 (0)