Skip to content

Commit 2903644

Browse files
authored
Merge pull request #787 from dymurray/statusupdate
pkg/ansible/controller,pkg/helm/controller; Enable status subresource by default
2 parents fa63ce1 + 2a349bc commit 2903644

File tree

11 files changed

+166
-19
lines changed

11 files changed

+166
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
- The SDK now uses logr as the default logger to unify the logging output with the controller-runtime logs. Users can still use a logger of their own choice. See the [logging doc](https://github.com/operator-framework/operator-sdk/blob/master/doc/user/logging.md) on how the SDK initializes and uses logr.
2727
- Ansible Operator CR status better aligns with [conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#typical-status-properties). ([#639](https://github.com/operator-framework/operator-sdk/pull/639))
28+
- The SDK now generates the CRD with the status subresource enabled by default. See the [client doc](https://github.com/operator-framework/operator-sdk/blob/masster/doc/user/client.md#updating-status-subresource) on how to update the status ssubresource. ([#787](https://github.com/operator-framework/operator-sdk/pull/787))
2829

2930
### Added
3031

doc/ansible/dev/developer_guide.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,84 @@ NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
321321
foo-operator 1 1 1 1 1m
322322
```
323323

324+
## Custom Resource Status Management
325+
The operator will automatically update the CR's `status` subresource with
326+
generic information about the previous Ansible run. This includes the number of
327+
successful and failed tasks and relevant error messages as seen below:
328+
329+
```yaml
330+
status:
331+
conditions:
332+
- ansibleResult:
333+
changed: 3
334+
completion: 2018-12-03T13:45:57.13329
335+
failures: 1
336+
ok: 6
337+
skipped: 0
338+
lastTransitionTime: 2018-12-03T13:45:57Z
339+
message: 'Status code was -1 and not [200]: Request failed: <urlopen error [Errno
340+
113] No route to host>'
341+
reason: Failed
342+
status: "True"
343+
type: Failure
344+
- lastTransitionTime: 2018-12-03T13:46:13Z
345+
message: Running reconciliation
346+
reason: Running
347+
status: "True"
348+
type: Running
349+
```
350+
351+
Ansible Operator also allows you as the developer to supply custom status
352+
values with the [k8s_status][k8s_status_module] Ansible Module. This allows the
353+
developer to update the `status` from within Ansible with any key/value pair as
354+
desired. By default, Ansible Operator will always include the generic Ansible
355+
run output as shown above. If you would prefer your application *not* update
356+
the status with Ansible output and would prefer to track the status manually
357+
from your application, then simply update the watches file with `manageStatus`:
358+
```yaml
359+
- version: v1
360+
group: api.example.com
361+
kind: Foo
362+
role: /opt/ansible/roles/Foo
363+
manageStatus: false
364+
```
365+
366+
To update the `status` subresource with key `foo` and value `bar`, `k8s_status`
367+
can be used as shown:
368+
```yaml
369+
- k8s_status:
370+
api_version: app.example.com/v1
371+
kind: Foo
372+
name: "{{ meta.name }}"
373+
namespace: "{{ meta.namespace }}"
374+
status:
375+
foo: bar
376+
```
377+
378+
### Using k8s_status Ansible module with `up local`
379+
This section covers the required steps to using the `k8s_status` Ansible module
380+
with `operator-sdk up local`. If you are unfamiliar with managing status from
381+
the Ansible Operator, see the [proposal for user-driven status
382+
management][manage_status_proposal].
383+
384+
If your operator takes advantage of the `k8s_status` Ansible module and you are
385+
interested in testing the operator with `operator-sdk up local`, then it is
386+
imperative that the module is installed in a location that Ansible expects.
387+
This is done with the `library` configuration option for Ansible. For our
388+
example, we will assume the user is placing third-party Ansible modules in
389+
`/usr/share/ansible/library`.
390+
391+
To install the `k8s_status` module, first set `ansible.cfg` to search in
392+
`/usr/share/ansible/library` for installed Ansible modules:
393+
```bash
394+
$ echo "library=/usr/share/ansible/library/" >> /etc/ansible/ansible.cfg
395+
```
396+
397+
Add `k8s_status.py` to `/usr/share/ansible/library/`:
398+
```bash
399+
$ wget https://raw.githubusercontent.com/fabianvf/ansible-k8s-status-module/master/k8s_status.py -O /usr/share/ansible/library/k8s_status.py
400+
```
401+
324402
## Extra vars sent to Ansible
325403
The extra vars that are sent to Ansible are managed by the operator. The `spec`
326404
section will pass along the key-value pairs as extra vars. This is equivalent
@@ -364,7 +442,9 @@ operator. The `meta` fields can be accesses via dot notation in Ansible as so:
364442
```
365443

366444
[k8s_ansible_module]:https://docs.ansible.com/ansible/2.6/modules/k8s_module.html
445+
[k8s_status_module]:https://github.com/fabianvf/ansible-k8s-status-module
367446
[openshift_restclient_python]:https://github.com/openshift/openshift-restclient-python
368447
[ansible_operator_user_guide]:../user-guide.md
448+
[manage_status_proposal]:../../proposals/ansible-operator-status.md
369449
[time_pkg]:https://golang.org/pkg/time/
370450
[time_parse_duration]:https://golang.org/pkg/time/#ParseDuration

doc/user-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ For this example replace the generated Controller file `pkg/controller/memcached
116116
The example Controller executes the following reconciliation logic for each `Memcached` CR:
117117
- Create a memcached Deployment if it doesn't exist
118118
- Ensure that the Deployment size is the same as specified by the `Memcached` CR spec
119-
- Update the `Memcached` CR status with the names of the memcached pods
119+
- Update the `Memcached` CR status using the status writer with the names of the memcached pods
120120

121121
The next two subsections explain how the Controller watches resources and how the reconcile loop is triggered. Skip to the [Build](#build-and-run-the-operator) section to see how to build and run the operator.
122122

doc/user/client.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,8 @@ func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, e
217217
```Go
218218
// Update updates the given obj in the Kubernetes cluster. obj must be a
219219
// struct pointer so that obj can be updated with the content returned
220-
// by the API server.
220+
// by the API server. Update does *not* update the resource's status
221+
// subresource
221222
func (c Client) Update(ctx context.Context, obj runtime.Object) error
222223
```
223224
Example:
@@ -244,6 +245,44 @@ func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, e
244245
}
245246
```
246247

248+
##### Updating Status Subresource
249+
250+
When updating the [status subresource][cr-status-subresource] from the client,
251+
the StatusWriter must be used which can be gotten with `Status()`
252+
253+
##### Status
254+
255+
```Go
256+
// Status() returns a StatusWriter object that can be used to update the
257+
// object's status subresource
258+
func (c Client) Status() (client.StatusWriter, error)
259+
```
260+
261+
Example:
262+
```Go
263+
import (
264+
"context"
265+
cachev1alpha1 "github.com/example-inc/memcached-operator/pkg/apis/cache/v1alpha1"
266+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
267+
)
268+
269+
func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, error) {
270+
...
271+
272+
mem := &cachev1alpha.Memcached{}
273+
err := r.client.Get(context.TODO(), request.NamespacedName, mem)
274+
275+
...
276+
277+
ctx := context.TODO()
278+
mem.Status.Nodes = []string{"pod1", "pod2"}
279+
err := r.client.Status().Update(ctx, mem)
280+
281+
...
282+
}
283+
```
284+
285+
247286
#### Delete
248287

249288
```Go
@@ -385,7 +424,7 @@ func (r *ReconcileApp) Reconcile(request reconcile.Request) (reconcile.Result, e
385424
podNames := getPodNames(podList.Items)
386425
if !reflect.DeepEqual(podNames, app.Status.Nodes) {
387426
app.Status.Nodes = podNames
388-
if err := r.client.Update(context.TODO(), app); err != nil {
427+
if err := r.client.Status().Update(context.TODO(), app); err != nil {
389428
return reconcile.Result{}, err
390429
}
391430
}
@@ -453,3 +492,4 @@ func labelsForApp(name string) map[string]string {
453492
[doc-reconcile-reconciler]:https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/reconcile#Reconciler
454493
[doc-osdk-handle]:https://github.com/operator-framework/operator-sdk/blob/master/doc/design/milestone-0.0.2/action-api.md#handler
455494
[doc-types-nsname]:https://godoc.org/k8s.io/apimachinery/pkg/types#NamespacedName
495+
[cr-status-subresource]:https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#status-subresource

example/memcached-operator/memcached_controller.go.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Res
153153
// Update status.Nodes if needed
154154
if !reflect.DeepEqual(podNames, memcached.Status.Nodes) {
155155
memcached.Status.Nodes = podNames
156-
err := r.client.Update(context.TODO(), memcached)
156+
err := r.client.Status().Update(context.TODO(), memcached)
157157
if err != nil {
158158
reqLogger.Error(err, "failed to update Memcached status")
159159
return reconcile.Result{}, err

pkg/ansible/controller/reconcile.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3636
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3737
"k8s.io/apimachinery/pkg/runtime/schema"
38+
"k8s.io/apimachinery/pkg/types"
3839
"sigs.k8s.io/controller-runtime/pkg/client"
3940
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4041
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
@@ -115,7 +116,7 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
115116
}
116117

117118
if r.ManageStatus {
118-
err = r.markRunning(u)
119+
err = r.markRunning(u, request.NamespacedName)
119120
if err != nil {
120121
return reconcileResult, err
121122
}
@@ -189,15 +190,20 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
189190
return reconcileResult, nil
190191
}
191192
if r.ManageStatus {
192-
err = r.markDone(u, statusEvent, failureMessages)
193+
err = r.markDone(u, request.NamespacedName, statusEvent, failureMessages)
193194
if err != nil {
194195
logger.Error(err, "failed to mark status done")
195196
}
196197
}
197198
return reconcileResult, err
198199
}
199200

200-
func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured) error {
201+
func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured, namespacedName types.NamespacedName) error {
202+
// Get the latest resource to prevent updating a stale status
203+
err := r.Client.Get(context.TODO(), namespacedName, u)
204+
if err != nil {
205+
return err
206+
}
201207
statusInterface := u.Object["status"]
202208
statusMap, _ := statusInterface.(map[string]interface{})
203209
crStatus := ansiblestatus.CreateFromMap(statusMap)
@@ -219,15 +225,25 @@ func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured) er
219225
)
220226
ansiblestatus.SetCondition(&crStatus, *c)
221227
u.Object["status"] = crStatus.GetJSONMap()
222-
err := r.Client.Update(context.TODO(), u)
228+
err := r.Client.Status().Update(context.TODO(), u)
223229
if err != nil {
224230
return err
225231
}
226232
}
227233
return nil
228234
}
229235

230-
func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error {
236+
func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, namespacedName types.NamespacedName, statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error {
237+
logger := logf.Log.WithName("markDone")
238+
// Get the latest resource to prevent updating a stale status
239+
err := r.Client.Get(context.TODO(), namespacedName, u)
240+
if apierrors.IsNotFound(err) {
241+
logger.Info("resource not found, assuming it was deleted", err)
242+
return nil
243+
}
244+
if err != nil {
245+
return err
246+
}
231247
statusInterface := u.Object["status"]
232248
statusMap, _ := statusInterface.(map[string]interface{})
233249
crStatus := ansiblestatus.CreateFromMap(statusMap)
@@ -262,7 +278,7 @@ func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, statu
262278
// This needs the status subresource to be enabled by default.
263279
u.Object["status"] = crStatus.GetJSONMap()
264280

265-
return r.Client.Update(context.TODO(), u)
281+
return r.Client.Status().Update(context.TODO(), u)
266282
}
267283

268284
func contains(l []string, s string) bool {

pkg/ansible/controller/status/types.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,21 @@ func createConditionFromMap(cm map[string]interface{}) Condition {
149149

150150
// Status - The status for custom resources managed by the operator-sdk.
151151
type Status struct {
152-
Conditions []Condition `json:"conditions"`
152+
Conditions []Condition `json:"conditions"`
153+
CustomStatus map[string]interface{} `json:"-"`
153154
}
154155

155156
// CreateFromMap - create a status from the map
156157
func CreateFromMap(statusMap map[string]interface{}) Status {
158+
customStatus := make(map[string]interface{})
159+
for key, value := range statusMap {
160+
if key != "conditions" {
161+
customStatus[key] = value
162+
}
163+
}
157164
conditionsInterface, ok := statusMap["conditions"].([]interface{})
158165
if !ok {
159-
return Status{Conditions: []Condition{}}
166+
return Status{Conditions: []Condition{}, CustomStatus: customStatus}
160167
}
161168
conditions := []Condition{}
162169
for _, ci := range conditionsInterface {
@@ -167,7 +174,7 @@ func CreateFromMap(statusMap map[string]interface{}) Status {
167174
}
168175
conditions = append(conditions, createConditionFromMap(cm))
169176
}
170-
return Status{Conditions: conditions}
177+
return Status{Conditions: conditions, CustomStatus: customStatus}
171178
}
172179

173180
// GetJSONMap - gets the map value for the status object.
@@ -177,12 +184,11 @@ func CreateFromMap(statusMap map[string]interface{}) Status {
177184
// unstructured will fail and throw runtime exceptions.
178185
// Please note that this will return an empty map on error.
179186
func (status *Status) GetJSONMap() map[string]interface{} {
180-
m := map[string]interface{}{}
181187
b, err := json.Marshal(status)
182188
if err != nil {
183189
log.Error(err, "unable to marshal json")
184-
return m
190+
return status.CustomStatus
185191
}
186-
json.Unmarshal(b, &m)
187-
return m
192+
json.Unmarshal(b, &status.CustomStatus)
193+
return status.CustomStatus
188194
}

pkg/helm/controller/reconcile.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func (r HelmOperatorReconciler) Reconcile(request reconcile.Request) (reconcile.
166166

167167
func (r HelmOperatorReconciler) updateResourceStatus(o *unstructured.Unstructured, status *types.HelmAppStatus) error {
168168
o.Object["status"] = status
169-
return r.Client.Update(context.TODO(), o)
169+
return r.Client.Status().Update(context.TODO(), o)
170170
}
171171

172172
func contains(l []string, s string) bool {

pkg/scaffold/crd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,6 @@ spec:
5656
singular: {{ .Resource.LowerKind }}
5757
scope: Namespaced
5858
version: {{ .Resource.Version }}
59+
subresources:
60+
status: {}
5961
`

pkg/scaffold/crd_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,6 @@ spec:
5050
singular: appservice
5151
scope: Namespaced
5252
version: v1alpha1
53+
subresources:
54+
status: {}
5355
`

0 commit comments

Comments
 (0)