Skip to content

pkg/ansible: introduce managedStatus flag #744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Nov 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

### Changed

- 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)
- 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)
- The SDK exposes the Kubernetes clientset via `k8sclient.GetKubeClient()` #295
- 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)
- The SDK exposes the Kubernetes rest config via `k8sclient.GetKubeConfig()` #338
Expand Down
73 changes: 0 additions & 73 deletions doc/ansible/dev/developer_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,79 +147,6 @@ trigger this Ansible logic when a custom resource changes. In the above
example, we want to map a role to a specific Kubernetes resource that the
operator will watch. This mapping is done in a file called `watches.yaml`.

### Watches file

The Operator expects a mapping file, which lists each GVK to watch and the
corresponding path to an Ansible role or playbook, to be copied into the
container at a predefined location: /opt/ansible/watches.yaml

Dockerfile example:
```Dockerfile
COPY watches.yaml /opt/ansible/watches.yaml
```

The Watches file format is yaml and is an array of objects. The object has
mandatory fields:

**version**: The version of the Custom Resource that you will be watching.

**group**: The group of the Custom Resource that you will be watching.

**kind**: The kind of the Custom Resource that you will be watching.

**playbook**: This is the path to the playbook that you have added to the
container. This playbook is expected to be simply a way to call roles. This
field is mutually exclusive with the "role" field.

**role**: This is the path to the role that you have added to the container.
For example if your roles directory is at `/opt/ansible/roles/` and your role
is named `busybox`, this value will be `/opt/ansible/roles/busybox`. This field
is mutually exclusive with the "playbook" field.

Example specifying a role:

```yaml
---
- version: v1alpha1
group: foo.example.com
kind: Foo
role: /opt/ansible/roles/Foo
```

#### Using playbooks in watches.yaml

By default, `operator-sdk new --type ansible` sets `watches.yaml` to execute a
role directly on a resource event. This works well for new projects, but with a
lot of Ansible code this can be hard to scale if we are putting everything
inside of one role. Using a playbook allows the developer to have more
flexibility in consuming other roles and enabling more customized deployments
of their application. To do this, modify `watches.yaml` to use a playbook
instead of the role:
```yaml
---
- version: v1alpha1
group: foo.example.com
kind: Foo
playbook: /opt/ansible/playbook.yml
```

Modify `tmp/build/Dockerfile` to put `playbook.yml` in `/opt/ansible` in the
container in addition to the role (`/opt/ansible` is the `HOME` environment
variable inside of the Ansible Operator base image):
```Dockerfile
FROM quay.io/water-hole/ansible-operator

COPY roles/ ${HOME}/roles
COPY playbook.yaml ${HOME}/playbook.yaml
COPY watches.yaml ${HOME}/watches.yaml
```

Alternatively, to generate a skeleton project with the above changes, a
developer can also do:
```bash
$ operator-sdk new --type ansible --kind Foo --api-version foo.example.com/v1alpha1 foo-operator --generate-playbook
```

### Custom Resource file

The Custom Resource file format is Kubernetes resource file. The object has
Expand Down
49 changes: 49 additions & 0 deletions doc/ansible/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,55 @@ If you'd like to create your memcached-operator project to be cluster-scoped use
$ operator-sdk new memcached-operator --cluster-scoped --api-version=cache.example.com/v1alpha1 --kind=Memcached --type=ansible
```

### Watches file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also update the doc/ansible/dev/developer-guide.yaml and either remove the watches file section there or update it?

I vote to remove so that this is only documented in one place to keep track of.


The Watches file contains a list of mappings from custom resources, identified
by it's Group, Version, and Kind, to an Ansible Role or Playbook. The Operator
expects this mapping file in a predefined location: `/opt/ansible/watches.yaml`

* **group**: The group of the Custom Resource that you will be watching.
* **version**: The version of the Custom Resource that you will be watching.
* **kind**: The kind of the Custom Resource that you will be watching.
* **role** (default): This is the path to the role that you have added to the
container. For example if your roles directory is at `/opt/ansible/roles/`
and your role is named `busybox`, this value will be
`/opt/ansible/roles/busybox`. This field is mutually exclusive with the
"playbook" field.
* **playbook**: This is the path to the playbook that you have added to the
container. This playbook is expected to be simply a way to call roles. This
field is mutually exclusive with the "role" field.
* **reconcilePeriod** (optional): The reconciliation interval, how often the
role/playbook is run, for a given CR.
* **manageStatus** (optional): When true (default), the operator will manage
the status of the CR generically. Set to false, the status of the CR is
managed elsewhere, by the specified role/playbook or in a separate controller.

An example Watches file:

```yaml
---
# Simple example mapping Foo to the Foo role
- version: v1alpha1
group: foo.example.com
kind: Foo
role: /opt/ansible/roles/Foo

# Simple example mapping Bar to a playbook
- version: v1alpha1
group: bar.example.com
kind: Bar
playbook: /opt/ansible/playbook.yaml

# More complex example for our Baz kind
# Here we will disable requeuing and be managing the CR status in the playbook
- version: v1alpha1
group: baz.example.com
kind: Baz
playbook: /opt/ansible/baz.yaml
reconcilePeriod: 0
manageStatus: false
```

## Customize the operator logic

For this example the memcached-operator will execute the following
Expand Down
2 changes: 2 additions & 0 deletions pkg/ansible/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Options struct {
Runner runner.Runner
GVK schema.GroupVersionKind
ReconcilePeriod time.Duration
ManageStatus bool
}

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

// Register the GVK with the schema
Expand Down
69 changes: 46 additions & 23 deletions pkg/ansible/controller/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type AnsibleOperatorReconciler struct {
Client client.Client
EventHandlers []events.EventHandler
ReconcilePeriod time.Duration
ManageStatus bool
}

// Reconcile - handle the event.
Expand Down Expand Up @@ -112,28 +113,9 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
return reconcileResult, err
}
}
statusInterface := u.Object["status"]
statusMap, _ := statusInterface.(map[string]interface{})
crStatus := ansiblestatus.CreateFromMap(statusMap)

// If there is no current status add that we are working on this resource.
errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType)
succCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)

// If the condition is currently running, making sure that the values are correct.
// If they are the same a no-op, if they are different then it is a good thing we
// are updating it.
if (errCond == nil && succCond == nil) || (succCond != nil && succCond.Reason != ansiblestatus.SuccessfulReason) {
c := ansiblestatus.NewCondition(
ansiblestatus.RunningConditionType,
v1.ConditionTrue,
nil,
ansiblestatus.RunningReason,
ansiblestatus.RunningMessage,
)
ansiblestatus.SetCondition(&crStatus, *c)
u.Object["status"] = crStatus.GetJSONMap()
err = r.Client.Update(context.TODO(), u)
if r.ManageStatus {
err = r.markRunning(u)
if err != nil {
return reconcileResult, err
}
Expand Down Expand Up @@ -205,6 +187,48 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
return reconcileResult, err
}
}
if r.ManageStatus {
err = r.markDone(u, statusEvent, failureMessages)
}
return reconcileResult, err
}

func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured) error {
statusInterface := u.Object["status"]
statusMap, _ := statusInterface.(map[string]interface{})
crStatus := ansiblestatus.CreateFromMap(statusMap)

// If there is no current status add that we are working on this resource.
errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType)
succCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)

// If the condition is currently running, making sure that the values are correct.
// If they are the same a no-op, if they are different then it is a good thing we
// are updating it.
if (errCond == nil && succCond == nil) || (succCond != nil && succCond.Reason != ansiblestatus.SuccessfulReason) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

c := ansiblestatus.NewCondition(
ansiblestatus.RunningConditionType,
v1.ConditionTrue,
nil,
ansiblestatus.RunningReason,
ansiblestatus.RunningMessage,
)
ansiblestatus.SetCondition(&crStatus, *c)
u.Object["status"] = crStatus.GetJSONMap()
err := r.Client.Update(context.TODO(), u)
if err != nil {
return err
}
}
return nil
}

func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error {
statusInterface := u.Object["status"]
statusMap, _ := statusInterface.(map[string]interface{})
crStatus := ansiblestatus.CreateFromMap(statusMap)

runSuccessful := len(failureMessages) == 0
ansibleStatus := ansiblestatus.NewAnsibleResultFromStatusJobEvent(statusEvent)

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

return r.Client.Update(context.TODO(), u)
}

func contains(l []string, s string) bool {
Expand Down
51 changes: 51 additions & 0 deletions pkg/ansible/controller/reconcile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func TestReconcile(t *testing.T) {
Name string
GVK schema.GroupVersionKind
ReconcilePeriod time.Duration
ManageStatus bool
Runner runner.Runner
EventHandlers []events.EventHandler
Client client.Client
Expand Down Expand Up @@ -74,6 +75,7 @@ func TestReconcile(t *testing.T) {
Name: "completed reconcile",
GVK: gvk,
ReconcilePeriod: 5 * time.Second,
ManageStatus: true,
Runner: &fake.Runner{
JobEvents: []eventapi.JobEvent{
eventapi.JobEvent{
Expand Down Expand Up @@ -134,6 +136,7 @@ func TestReconcile(t *testing.T) {
Name: "Failure message reconcile",
GVK: gvk,
ReconcilePeriod: 5 * time.Second,
ManageStatus: true,
Runner: &fake.Runner{
JobEvents: []eventapi.JobEvent{
eventapi.JobEvent{
Expand Down Expand Up @@ -210,6 +213,7 @@ func TestReconcile(t *testing.T) {
Name: "Finalizer successful reconcile",
GVK: gvk,
ReconcilePeriod: 5 * time.Second,
ManageStatus: true,
Runner: &fake.Runner{
JobEvents: []eventapi.JobEvent{
eventapi.JobEvent{
Expand Down Expand Up @@ -317,6 +321,7 @@ func TestReconcile(t *testing.T) {
Name: "Finalizer successful reconcile",
GVK: gvk,
ReconcilePeriod: 5 * time.Second,
ManageStatus: true,
Runner: &fake.Runner{
JobEvents: []eventapi.JobEvent{
eventapi.JobEvent{
Expand Down Expand Up @@ -412,6 +417,51 @@ func TestReconcile(t *testing.T) {
},
ShouldError: true,
},
{
Name: "no manage status",
GVK: gvk,
ReconcilePeriod: 5 * time.Second,
ManageStatus: false,
Runner: &fake.Runner{
JobEvents: []eventapi.JobEvent{
eventapi.JobEvent{
Event: eventapi.EventPlaybookOnStats,
Created: eventapi.EventTime{Time: eventTime},
},
},
},
Client: fakeclient.NewFakeClient(&unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "reconcile",
"namespace": "default",
},
"apiVersion": "operator-sdk/v1beta1",
"kind": "Testing",
},
}),
Result: reconcile.Result{
RequeueAfter: 5 * time.Second,
},
Request: reconcile.Request{
NamespacedName: types.NamespacedName{
Name: "reconcile",
Namespace: "default",
},
},
ExpectedObject: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "reconcile",
"namespace": "default",
},
"apiVersion": "operator-sdk/v1beta1",
"kind": "Testing",
"spec": map[string]interface{}{},
"status": map[string]interface{}{},
},
},
},
}

for _, tc := range testCases {
Expand All @@ -422,6 +472,7 @@ func TestReconcile(t *testing.T) {
Client: tc.Client,
EventHandlers: tc.EventHandlers,
ReconcilePeriod: tc.ReconcilePeriod,
ManageStatus: tc.ManageStatus,
}
result, err := aor.Reconcile(tc.Request)
if err != nil && !tc.ShouldError {
Expand Down
1 change: 1 addition & 0 deletions pkg/ansible/operator/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func Run(done chan error, mgr manager.Manager, watchesPath string, reconcilePeri
GVK: gvk,
Runner: runner,
ReconcilePeriod: reconcilePeriod,
ManageStatus: runner.GetManageStatus(),
}
d, ok := runner.GetReconcilePeriod()
if ok {
Expand Down
6 changes: 6 additions & 0 deletions pkg/ansible/runner/fake/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
type Runner struct {
Finalizer string
ReconcilePeriod time.Duration
ManageStatus bool
// Used to send error if Run should fail.
Error error
// Job Events that will be sent back from the runs channel
Expand Down Expand Up @@ -71,6 +72,11 @@ func (r *Runner) GetReconcilePeriod() (time.Duration, bool) {
return r.ReconcilePeriod, r.ReconcilePeriod != time.Duration(0)
}

// GetManageStatus - get managestatus.
func (r *Runner) GetManageStatus() bool {
return r.ManageStatus
}

// GetFinalizer - gets the fake finalizer.
func (r *Runner) GetFinalizer() (string, bool) {
return r.Finalizer, r.Finalizer != ""
Expand Down
Loading