Skip to content

Commit bd969e2

Browse files
authored
Merge pull request #744 from djzager/status-flag
pkg/ansible: introduce managedStatus flag
2 parents 9ae4ed9 + 66e2e7f commit bd969e2

File tree

12 files changed

+255
-103
lines changed

12 files changed

+255
-103
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252

5353
### Changed
5454

55-
- All the modules in [`pkg/sdk`](https://github.com/operator-framework/operator-sdk/tree/master/pkg/sdk) have been combined into a single package. `action`, `handler`, `informer` `types` and `query` pkgs have been consolidated into `pkg/sdk`. [#242](https://github.com/operator-framework/operator-sdk/pull/242)
55+
- All the modules in [`pkg/sdk`](https://github.com/operator-framework/operator-sdk/tree/4a9d5a5b0901b24679d36dced0a186c525e1bffd/pkg/sdk) have been combined into a single package. `action`, `handler`, `informer` `types` and `query` pkgs have been consolidated into `pkg/sdk`. [#242](https://github.com/operator-framework/operator-sdk/pull/242)
5656
- The SDK exposes the Kubernetes clientset via `k8sclient.GetKubeClient()` #295
5757
- The SDK now vendors the k8s code-generators for an operator instead of using the prebuilt image `gcr.io/coreos-k8s-scale-testing/codegen:1.9.3` [#319](https://github.com/operator-framework/operator-sdk/pull/242)
5858
- The SDK exposes the Kubernetes rest config via `k8sclient.GetKubeConfig()` #338

doc/ansible/dev/developer_guide.md

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -147,79 +147,6 @@ trigger this Ansible logic when a custom resource changes. In the above
147147
example, we want to map a role to a specific Kubernetes resource that the
148148
operator will watch. This mapping is done in a file called `watches.yaml`.
149149

150-
### Watches file
151-
152-
The Operator expects a mapping file, which lists each GVK to watch and the
153-
corresponding path to an Ansible role or playbook, to be copied into the
154-
container at a predefined location: /opt/ansible/watches.yaml
155-
156-
Dockerfile example:
157-
```Dockerfile
158-
COPY watches.yaml /opt/ansible/watches.yaml
159-
```
160-
161-
The Watches file format is yaml and is an array of objects. The object has
162-
mandatory fields:
163-
164-
**version**: The version of the Custom Resource that you will be watching.
165-
166-
**group**: The group of the Custom Resource that you will be watching.
167-
168-
**kind**: The kind of the Custom Resource that you will be watching.
169-
170-
**playbook**: This is the path to the playbook that you have added to the
171-
container. This playbook is expected to be simply a way to call roles. This
172-
field is mutually exclusive with the "role" field.
173-
174-
**role**: This is the path to the role that you have added to the container.
175-
For example if your roles directory is at `/opt/ansible/roles/` and your role
176-
is named `busybox`, this value will be `/opt/ansible/roles/busybox`. This field
177-
is mutually exclusive with the "playbook" field.
178-
179-
Example specifying a role:
180-
181-
```yaml
182-
---
183-
- version: v1alpha1
184-
group: foo.example.com
185-
kind: Foo
186-
role: /opt/ansible/roles/Foo
187-
```
188-
189-
#### Using playbooks in watches.yaml
190-
191-
By default, `operator-sdk new --type ansible` sets `watches.yaml` to execute a
192-
role directly on a resource event. This works well for new projects, but with a
193-
lot of Ansible code this can be hard to scale if we are putting everything
194-
inside of one role. Using a playbook allows the developer to have more
195-
flexibility in consuming other roles and enabling more customized deployments
196-
of their application. To do this, modify `watches.yaml` to use a playbook
197-
instead of the role:
198-
```yaml
199-
---
200-
- version: v1alpha1
201-
group: foo.example.com
202-
kind: Foo
203-
playbook: /opt/ansible/playbook.yml
204-
```
205-
206-
Modify `tmp/build/Dockerfile` to put `playbook.yml` in `/opt/ansible` in the
207-
container in addition to the role (`/opt/ansible` is the `HOME` environment
208-
variable inside of the Ansible Operator base image):
209-
```Dockerfile
210-
FROM quay.io/water-hole/ansible-operator
211-
212-
COPY roles/ ${HOME}/roles
213-
COPY playbook.yaml ${HOME}/playbook.yaml
214-
COPY watches.yaml ${HOME}/watches.yaml
215-
```
216-
217-
Alternatively, to generate a skeleton project with the above changes, a
218-
developer can also do:
219-
```bash
220-
$ operator-sdk new --type ansible --kind Foo --api-version foo.example.com/v1alpha1 foo-operator --generate-playbook
221-
```
222-
223150
### Custom Resource file
224151

225152
The Custom Resource file format is Kubernetes resource file. The object has

doc/ansible/user-guide.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,55 @@ If you'd like to create your memcached-operator project to be cluster-scoped use
6262
$ operator-sdk new memcached-operator --cluster-scoped --api-version=cache.example.com/v1alpha1 --kind=Memcached --type=ansible
6363
```
6464

65+
### Watches file
66+
67+
The Watches file contains a list of mappings from custom resources, identified
68+
by it's Group, Version, and Kind, to an Ansible Role or Playbook. The Operator
69+
expects this mapping file in a predefined location: `/opt/ansible/watches.yaml`
70+
71+
* **group**: The group of the Custom Resource that you will be watching.
72+
* **version**: The version of the Custom Resource that you will be watching.
73+
* **kind**: The kind of the Custom Resource that you will be watching.
74+
* **role** (default): This is the path to the role that you have added to the
75+
container. For example if your roles directory is at `/opt/ansible/roles/`
76+
and your role is named `busybox`, this value will be
77+
`/opt/ansible/roles/busybox`. This field is mutually exclusive with the
78+
"playbook" field.
79+
* **playbook**: This is the path to the playbook that you have added to the
80+
container. This playbook is expected to be simply a way to call roles. This
81+
field is mutually exclusive with the "role" field.
82+
* **reconcilePeriod** (optional): The reconciliation interval, how often the
83+
role/playbook is run, for a given CR.
84+
* **manageStatus** (optional): When true (default), the operator will manage
85+
the status of the CR generically. Set to false, the status of the CR is
86+
managed elsewhere, by the specified role/playbook or in a separate controller.
87+
88+
An example Watches file:
89+
90+
```yaml
91+
---
92+
# Simple example mapping Foo to the Foo role
93+
- version: v1alpha1
94+
group: foo.example.com
95+
kind: Foo
96+
role: /opt/ansible/roles/Foo
97+
98+
# Simple example mapping Bar to a playbook
99+
- version: v1alpha1
100+
group: bar.example.com
101+
kind: Bar
102+
playbook: /opt/ansible/playbook.yaml
103+
104+
# More complex example for our Baz kind
105+
# Here we will disable requeuing and be managing the CR status in the playbook
106+
- version: v1alpha1
107+
group: baz.example.com
108+
kind: Baz
109+
playbook: /opt/ansible/baz.yaml
110+
reconcilePeriod: 0
111+
manageStatus: false
112+
```
113+
65114
## Customize the operator logic
66115
67116
For this example the memcached-operator will execute the following

pkg/ansible/controller/controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Options struct {
4242
Runner runner.Runner
4343
GVK schema.GroupVersionKind
4444
ReconcilePeriod time.Duration
45+
ManageStatus bool
4546
}
4647

4748
// Add - Creates a new ansible operator controller and adds it to the manager
@@ -58,6 +59,7 @@ func Add(mgr manager.Manager, options Options) {
5859
Runner: options.Runner,
5960
EventHandlers: eventHandlers,
6061
ReconcilePeriod: options.ReconcilePeriod,
62+
ManageStatus: options.ManageStatus,
6163
}
6264

6365
// Register the GVK with the schema

pkg/ansible/controller/reconcile.go

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type AnsibleOperatorReconciler struct {
5454
Client client.Client
5555
EventHandlers []events.EventHandler
5656
ReconcilePeriod time.Duration
57+
ManageStatus bool
5758
}
5859

5960
// Reconcile - handle the event.
@@ -112,28 +113,9 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
112113
return reconcileResult, err
113114
}
114115
}
115-
statusInterface := u.Object["status"]
116-
statusMap, _ := statusInterface.(map[string]interface{})
117-
crStatus := ansiblestatus.CreateFromMap(statusMap)
118-
119-
// If there is no current status add that we are working on this resource.
120-
errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType)
121-
succCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)
122116

123-
// If the condition is currently running, making sure that the values are correct.
124-
// If they are the same a no-op, if they are different then it is a good thing we
125-
// are updating it.
126-
if (errCond == nil && succCond == nil) || (succCond != nil && succCond.Reason != ansiblestatus.SuccessfulReason) {
127-
c := ansiblestatus.NewCondition(
128-
ansiblestatus.RunningConditionType,
129-
v1.ConditionTrue,
130-
nil,
131-
ansiblestatus.RunningReason,
132-
ansiblestatus.RunningMessage,
133-
)
134-
ansiblestatus.SetCondition(&crStatus, *c)
135-
u.Object["status"] = crStatus.GetJSONMap()
136-
err = r.Client.Update(context.TODO(), u)
117+
if r.ManageStatus {
118+
err = r.markRunning(u)
137119
if err != nil {
138120
return reconcileResult, err
139121
}
@@ -205,6 +187,48 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
205187
return reconcileResult, err
206188
}
207189
}
190+
if r.ManageStatus {
191+
err = r.markDone(u, statusEvent, failureMessages)
192+
}
193+
return reconcileResult, err
194+
}
195+
196+
func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured) error {
197+
statusInterface := u.Object["status"]
198+
statusMap, _ := statusInterface.(map[string]interface{})
199+
crStatus := ansiblestatus.CreateFromMap(statusMap)
200+
201+
// If there is no current status add that we are working on this resource.
202+
errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType)
203+
succCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)
204+
205+
// If the condition is currently running, making sure that the values are correct.
206+
// If they are the same a no-op, if they are different then it is a good thing we
207+
// are updating it.
208+
if (errCond == nil && succCond == nil) || (succCond != nil && succCond.Reason != ansiblestatus.SuccessfulReason) {
209+
c := ansiblestatus.NewCondition(
210+
ansiblestatus.RunningConditionType,
211+
v1.ConditionTrue,
212+
nil,
213+
ansiblestatus.RunningReason,
214+
ansiblestatus.RunningMessage,
215+
)
216+
ansiblestatus.SetCondition(&crStatus, *c)
217+
u.Object["status"] = crStatus.GetJSONMap()
218+
err := r.Client.Update(context.TODO(), u)
219+
if err != nil {
220+
return err
221+
}
222+
}
223+
return nil
224+
}
225+
226+
func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error {
227+
statusInterface := u.Object["status"]
228+
statusMap, _ := statusInterface.(map[string]interface{})
229+
crStatus := ansiblestatus.CreateFromMap(statusMap)
230+
231+
runSuccessful := len(failureMessages) == 0
208232
ansibleStatus := ansiblestatus.NewAnsibleResultFromStatusJobEvent(statusEvent)
209233

210234
if !runSuccessful {
@@ -233,9 +257,8 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
233257
}
234258
// This needs the status subresource to be enabled by default.
235259
u.Object["status"] = crStatus.GetJSONMap()
236-
err = r.Client.Update(context.TODO(), u)
237-
return reconcileResult, err
238260

261+
return r.Client.Update(context.TODO(), u)
239262
}
240263

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

pkg/ansible/controller/reconcile_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func TestReconcile(t *testing.T) {
4646
Name string
4747
GVK schema.GroupVersionKind
4848
ReconcilePeriod time.Duration
49+
ManageStatus bool
4950
Runner runner.Runner
5051
EventHandlers []events.EventHandler
5152
Client client.Client
@@ -74,6 +75,7 @@ func TestReconcile(t *testing.T) {
7475
Name: "completed reconcile",
7576
GVK: gvk,
7677
ReconcilePeriod: 5 * time.Second,
78+
ManageStatus: true,
7779
Runner: &fake.Runner{
7880
JobEvents: []eventapi.JobEvent{
7981
eventapi.JobEvent{
@@ -134,6 +136,7 @@ func TestReconcile(t *testing.T) {
134136
Name: "Failure message reconcile",
135137
GVK: gvk,
136138
ReconcilePeriod: 5 * time.Second,
139+
ManageStatus: true,
137140
Runner: &fake.Runner{
138141
JobEvents: []eventapi.JobEvent{
139142
eventapi.JobEvent{
@@ -210,6 +213,7 @@ func TestReconcile(t *testing.T) {
210213
Name: "Finalizer successful reconcile",
211214
GVK: gvk,
212215
ReconcilePeriod: 5 * time.Second,
216+
ManageStatus: true,
213217
Runner: &fake.Runner{
214218
JobEvents: []eventapi.JobEvent{
215219
eventapi.JobEvent{
@@ -317,6 +321,7 @@ func TestReconcile(t *testing.T) {
317321
Name: "Finalizer successful reconcile",
318322
GVK: gvk,
319323
ReconcilePeriod: 5 * time.Second,
324+
ManageStatus: true,
320325
Runner: &fake.Runner{
321326
JobEvents: []eventapi.JobEvent{
322327
eventapi.JobEvent{
@@ -412,6 +417,51 @@ func TestReconcile(t *testing.T) {
412417
},
413418
ShouldError: true,
414419
},
420+
{
421+
Name: "no manage status",
422+
GVK: gvk,
423+
ReconcilePeriod: 5 * time.Second,
424+
ManageStatus: false,
425+
Runner: &fake.Runner{
426+
JobEvents: []eventapi.JobEvent{
427+
eventapi.JobEvent{
428+
Event: eventapi.EventPlaybookOnStats,
429+
Created: eventapi.EventTime{Time: eventTime},
430+
},
431+
},
432+
},
433+
Client: fakeclient.NewFakeClient(&unstructured.Unstructured{
434+
Object: map[string]interface{}{
435+
"metadata": map[string]interface{}{
436+
"name": "reconcile",
437+
"namespace": "default",
438+
},
439+
"apiVersion": "operator-sdk/v1beta1",
440+
"kind": "Testing",
441+
},
442+
}),
443+
Result: reconcile.Result{
444+
RequeueAfter: 5 * time.Second,
445+
},
446+
Request: reconcile.Request{
447+
NamespacedName: types.NamespacedName{
448+
Name: "reconcile",
449+
Namespace: "default",
450+
},
451+
},
452+
ExpectedObject: &unstructured.Unstructured{
453+
Object: map[string]interface{}{
454+
"metadata": map[string]interface{}{
455+
"name": "reconcile",
456+
"namespace": "default",
457+
},
458+
"apiVersion": "operator-sdk/v1beta1",
459+
"kind": "Testing",
460+
"spec": map[string]interface{}{},
461+
"status": map[string]interface{}{},
462+
},
463+
},
464+
},
415465
}
416466

417467
for _, tc := range testCases {
@@ -422,6 +472,7 @@ func TestReconcile(t *testing.T) {
422472
Client: tc.Client,
423473
EventHandlers: tc.EventHandlers,
424474
ReconcilePeriod: tc.ReconcilePeriod,
475+
ManageStatus: tc.ManageStatus,
425476
}
426477
result, err := aor.Reconcile(tc.Request)
427478
if err != nil && !tc.ShouldError {

pkg/ansible/operator/operator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func Run(done chan error, mgr manager.Manager, watchesPath string, reconcilePeri
4444
GVK: gvk,
4545
Runner: runner,
4646
ReconcilePeriod: reconcilePeriod,
47+
ManageStatus: runner.GetManageStatus(),
4748
}
4849
d, ok := runner.GetReconcilePeriod()
4950
if ok {

pkg/ansible/runner/fake/runner.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
type Runner struct {
2828
Finalizer string
2929
ReconcilePeriod time.Duration
30+
ManageStatus bool
3031
// Used to send error if Run should fail.
3132
Error error
3233
// Job Events that will be sent back from the runs channel
@@ -71,6 +72,11 @@ func (r *Runner) GetReconcilePeriod() (time.Duration, bool) {
7172
return r.ReconcilePeriod, r.ReconcilePeriod != time.Duration(0)
7273
}
7374

75+
// GetManageStatus - get managestatus.
76+
func (r *Runner) GetManageStatus() bool {
77+
return r.ManageStatus
78+
}
79+
7480
// GetFinalizer - gets the fake finalizer.
7581
func (r *Runner) GetFinalizer() (string, bool) {
7682
return r.Finalizer, r.Finalizer != ""

0 commit comments

Comments
 (0)