-
-
Save fabianvf/45351d18cc105e91c02dccfd67c7c1ce to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/Dockerfile b/Dockerfile | |
new file mode 100644 | |
index 00000000..361ea2df | |
--- /dev/null | |
+++ b/Dockerfile | |
@@ -0,0 +1,26 @@ | |
+FROM openshift/origin-release:golang-1.11 AS builder | |
+COPY . /go/src/github.com/operator-framework/operator-sdk | |
+RUN cd /go/src/github.com/operator-framework/operator-sdk \ | |
+ && make build/operator-sdk-dev-x86_64-linux-gnu VERSION=dev | |
+ | |
+FROM ansible-runner:1.2.0 | |
+RUN yum -y install ansible-runner-http python-kubernetes python-openshift | |
+ | |
+RUN mkdir -p /etc/ansible \ | |
+ && echo "localhost ansible_connection=local" > /etc/ansible/hosts \ | |
+ && echo '[defaults]' > /etc/ansible/ansible.cfg \ | |
+ && echo 'roles_path = /opt/ansible/roles' >> /etc/ansible/ansible.cfg \ | |
+ && echo 'library = /usr/share/ansible/openshift' >> /etc/ansible/ansible.cfg | |
+ | |
+ENV OPERATOR=/usr/local/bin/ansible-operator \ | |
+ USER_UID=1001 \ | |
+ USER_NAME=ansible-operator\ | |
+ HOME=/opt/ansible | |
+ | |
+COPY --from=builder /go/src/github.com/operator-framework/operator-sdk/build/operator-sdk-dev-x86_64-linux-gnu ${OPERATOR} | |
+COPY --from=builder /go/src/github.com/operator-framework/operator-sdk/bin /usr/local/bin | |
+COPY --from=builder /go/src/github.com/operator-framework/operator-sdk/library/k8s_status.py /usr/share/ansible/openshift/ | |
+ | |
+RUN /usr/local/bin/user_setup | |
+ENTRYPOINT ["/usr/local/bin/entrypoint"] | |
+USER ${USER_UID} | |
diff --git a/bin/entrypoint b/bin/entrypoint | |
new file mode 100755 | |
index 00000000..f4bcf56d | |
--- /dev/null | |
+++ b/bin/entrypoint | |
@@ -0,0 +1,12 @@ | |
+#!/bin/bash -e | |
+ | |
+# This is documented here: | |
+# https://docs.openshift.com/container-platform/3.11/creating_images/guidelines.html#openshift-specific-guidelines | |
+ | |
+if ! whoami &>/dev/null; then | |
+ if [ -w /etc/passwd ]; then | |
+ echo "${USER_NAME:-runner}:x:$(id -u):$(id -g):${USER_NAME:-runner} user:${HOME}:/sbin/nologin" >> /etc/passwd | |
+ fi | |
+fi | |
+ | |
+exec ${OPERATOR} run ansible --watches-file=/opt/ansible/watches.yaml $@ | |
diff --git a/bin/user_setup b/bin/user_setup | |
new file mode 100755 | |
index 00000000..5201518a | |
--- /dev/null | |
+++ b/bin/user_setup | |
@@ -0,0 +1,13 @@ | |
+#!/bin/sh | |
+set -x | |
+ | |
+# ensure $HOME exists and is accessible by group 0 (we don't know what the runtime UID will be) | |
+mkdir -p ${HOME}/.ansible/tmp | |
+chown -R ${USER_UID}:0 ${HOME} | |
+chmod -R ug+rwx ${HOME} | |
+ | |
+# runtime user will need to be able to self-insert in /etc/passwd | |
+chmod g+rw /etc/passwd | |
+ | |
+# no need for this script to remain in the image after running | |
+rm $0 | |
diff --git a/doc/images/Operator-Maturity-Detailed.png b/doc/images/Operator-Maturity-Detailed.png | |
new file mode 100644 | |
index 00000000..c8edaba1 | |
Binary files /dev/null and b/doc/images/Operator-Maturity-Detailed.png differ | |
diff --git a/library/k8s_status.py b/library/k8s_status.py | |
new file mode 100644 | |
index 00000000..facfcf94 | |
--- /dev/null | |
+++ b/library/k8s_status.py | |
@@ -0,0 +1,375 @@ | |
+#!/usr/bin/python | |
+# -*- coding: utf-8 -*- | |
+ | |
+from __future__ import absolute_import, division, print_function | |
+ | |
+import re | |
+import copy | |
+ | |
+from ansible.module_utils.k8s.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC, KubernetesAnsibleModule | |
+ | |
+try: | |
+ from openshift.dynamic.exceptions import DynamicApiError | |
+except ImportError as exc: | |
+ class KubernetesException(Exception): | |
+ pass | |
+ | |
+ | |
+__metaclass__ = type | |
+ | |
+ANSIBLE_METADATA = {'metadata_version': '1.1', | |
+ 'status': ['preview'], | |
+ 'supported_by': 'community'} | |
+ | |
+DOCUMENTATION = ''' | |
+ | |
+module: k8s_status | |
+ | |
+short_description: Update the status for a Kubernetes API resource | |
+ | |
+version_added: "2.7" | |
+ | |
+author: "Fabian von Feilitzsch (@fabianvf)" | |
+ | |
+description: | |
+ - Sets the status field on a Kubernetes API resource. Only should be used if you are using Ansible to | |
+ implement a controller for the resource being modified. | |
+ | |
+options: | |
+ status: | |
+ type: dict | |
+ description: | |
+ - A object containing `key: value` pairs that will be set on the status object of the specified resource. | |
+ - One of I(status) or I(conditions) is required. | |
+ conditions: | |
+ type: list | |
+ description: | |
+ - A list of condition objects that will be set on the status.conditions field of the specified resource. | |
+ - Unless I(force) is C(true) the specified conditions will be merged with the conditions already set on the status field of the specified resource. | |
+ - Each element in the list will be validated according to the conventions specified in the | |
+ [Kubernetes API conventions document](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#spec-and-status). | |
+ - 'The fields supported for each condition are: | |
+ `type` (required), | |
+ `status` (required, one of "True", "False", "Unknown"), | |
+ `reason` (single CamelCase word), | |
+ `message`, | |
+ `lastHeartbeatTime` (RFC3339 datetime string), and | |
+ `lastTransitionTime` (RFC3339 datetime string).' | |
+ - One of I(status) or I(conditions) is required.' | |
+ api_version: | |
+ description: | |
+ - Use to specify the API version. Use in conjunction with I(kind), I(name), and I(namespace) to identify a | |
+ specific object. | |
+ required: yes | |
+ aliases: | |
+ - api | |
+ - version | |
+ kind: | |
+ description: | |
+ - Use to specify an object model. Use in conjunction with I(api_version), I(name), and I(namespace) to identify a | |
+ specific object. | |
+ required: yes | |
+ name: | |
+ description: | |
+ - Use to specify an object name. Use in conjunction with I(api_version), I(kind) and I(namespace) to identify a | |
+ specific object. | |
+ required: yes | |
+ namespace: | |
+ description: | |
+ - Use to specify an object namespace. Use in conjunction with I(api_version), I(kind), and I(name) | |
+ to identify a specific object. | |
+ force: | |
+ description: | |
+ - If set to C(True), the status will be set using `PUT` rather than `PATCH`, replacing the full status object. | |
+ default: false | |
+ type: bool | |
+ host: | |
+ description: | |
+ - Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable. | |
+ api_key: | |
+ description: | |
+ - Token used to authenticate with the API. Can also be specified via K8S_AUTH_API_KEY environment variable. | |
+ kubeconfig: | |
+ description: | |
+ - Path to an instance Kubernetes config file. If not provided, and no other connection | |
+ options are provided, the openshift client will attempt to load the default | |
+ configuration file from I(~/.kube/config.json). Can also be specified via K8S_AUTH_KUBECONFIG environment | |
+ variable. | |
+ context: | |
+ description: | |
+ - The name of a context found in the config file. Can also be specified via K8S_AUTH_CONTEXT environment variable. | |
+ username: | |
+ description: | |
+ - Provide a username for authenticating with the API. Can also be specified via K8S_AUTH_USERNAME environment | |
+ variable. | |
+ password: | |
+ description: | |
+ - Provide a password for authenticating with the API. Can also be specified via K8S_AUTH_PASSWORD environment | |
+ variable. | |
+ cert_file: | |
+ description: | |
+ - Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE environment | |
+ variable. | |
+ key_file: | |
+ description: | |
+ - Path to a key file used to authenticate with the API. Can also be specified via K8S_AUTH_KEY_FILE environment | |
+ variable. | |
+ ssl_ca_cert: | |
+ description: | |
+ - Path to a CA certificate used to authenticate with the API. Can also be specified via K8S_AUTH_SSL_CA_CERT | |
+ environment variable. | |
+ verify_ssl: | |
+ description: | |
+ - "Whether or not to verify the API server's SSL certificates. Can also be specified via K8S_AUTH_VERIFY_SSL | |
+ environment variable." | |
+ type: bool | |
+ | |
+requirements: | |
+ - "python >= 2.7" | |
+ - "openshift >= 0.8.1" | |
+ - "PyYAML >= 3.11" | |
+''' | |
+ | |
+EXAMPLES = ''' | |
+- name: Set custom status fields on TestCR | |
+ k8s_status: | |
+ api_version: apps.example.com/v1alpha1 | |
+ kind: TestCR | |
+ name: my-test | |
+ namespace: testing | |
+ status: | |
+ hello: world | |
+ custom: entries | |
+ | |
+- name: Update the standard condition of an Ansible Operator | |
+ k8s_status: | |
+ api_version: apps.example.com/v1alpha1 | |
+ kind: TestCR | |
+ name: my-test | |
+ namespace: testing | |
+ conditions: | |
+ - type: Running | |
+ status: "True" | |
+ reason: MigrationStarted | |
+ message: "Migration from v2 to v3 has begun" | |
+ lastTransitionTime: "{{ ansible_date_time.iso8601 }}" | |
+ | |
+- name: | | |
+ Create custom conditions. WARNING: The default Ansible Operator status management | |
+ will never overwrite custom conditions, so they will persist indefinitely. If you | |
+ want the values to change or be removed, you will need to clean them up manually. | |
+ k8s_status: | |
+ conditions: | |
+ - type: Available | |
+ status: "False" | |
+ reason: PingFailed | |
+ message: "The service did not respond to a ping" | |
+ | |
+''' | |
+ | |
+RETURN = ''' | |
+result: | |
+ description: | |
+ - If a change was made, will return the patched object, otherwise returns the instance object. | |
+ returned: success | |
+ type: complex | |
+ contains: | |
+ api_version: | |
+ description: The versioned schema of this representation of an object. | |
+ returned: success | |
+ type: str | |
+ kind: | |
+ description: Represents the REST resource this object represents. | |
+ returned: success | |
+ type: str | |
+ metadata: | |
+ description: Standard object metadata. Includes name, namespace, annotations, labels, etc. | |
+ returned: success | |
+ type: complex | |
+ spec: | |
+ description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind). | |
+ returned: success | |
+ type: complex | |
+ status: | |
+ description: Current status details for the object. | |
+ returned: success | |
+ type: complex | |
+''' | |
+ | |
+ | |
+def condition_array(conditions): | |
+ | |
+ VALID_KEYS = ['type', 'status', 'reason', 'message', 'lastHeartbeatTime', 'lastTransitionTime'] | |
+ REQUIRED = ['type', 'status'] | |
+ CAMEL_CASE = re.compile(r'^(?:[A-Z]*[a-z]*)+$') | |
+ RFC3339_datetime = re.compile(r'^\d{4}-\d\d-\d\dT\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)$') | |
+ | |
+ def validate_condition(condition): | |
+ if not isinstance(condition, dict): | |
+ raise ValueError('`conditions` must be a list of objects') | |
+ if isinstance(condition.get('status'), bool): | |
+ condition['status'] = 'True' if condition['status'] else 'False' | |
+ | |
+ for key in condition.keys(): | |
+ if key not in VALID_KEYS: | |
+ raise ValueError('{} is not a valid field for a condition, accepted fields are {}'.format(key, VALID_KEYS)) | |
+ for key in REQUIRED: | |
+ if not condition.get(key): | |
+ raise ValueError('Condition `{}` must be set'.format(key)) | |
+ | |
+ if condition['status'] not in ['True', 'False', 'Unknown']: | |
+ raise ValueError('Condition `status` must be one of ["True", "False", "Unknown"], not {}'.format(condition['status'])) | |
+ | |
+ if condition.get('reason') and not re.match(CAMEL_CASE, condition['reason']): | |
+ raise ValueError('Condition `reason` must be a single, CamelCase word') | |
+ | |
+ for key in ['lastHeartBeatTime', 'lastTransitionTime']: | |
+ if condition.get(key) and not re.match(RFC3339_datetime, condition[key]): | |
+ raise ValueError('`{}` must be a RFC3339 compliant datetime string'.format(key)) | |
+ | |
+ return condition | |
+ | |
+ return [validate_condition(c) for c in conditions] | |
+ | |
+ | |
+STATUS_ARG_SPEC = { | |
+ 'status': { | |
+ 'type': 'dict', | |
+ 'required': False | |
+ }, | |
+ 'conditions': { | |
+ 'type': condition_array, | |
+ 'required': False | |
+ } | |
+} | |
+ | |
+ | |
+def main(): | |
+ KubernetesAnsibleStatusModule().execute_module() | |
+ | |
+ | |
+class KubernetesAnsibleStatusModule(KubernetesAnsibleModule): | |
+ | |
+ def __init__(self, *args, **kwargs): | |
+ KubernetesAnsibleModule.__init__( | |
+ self, *args, | |
+ supports_check_mode=True, | |
+ **kwargs | |
+ ) | |
+ self.kind = self.params.get('kind') | |
+ self.api_version = self.params.get('api_version') | |
+ self.name = self.params.get('name') | |
+ self.namespace = self.params.get('namespace') | |
+ self.force = self.params.get('force') | |
+ | |
+ self.status = self.params.get('status') or {} | |
+ self.conditions = self.params.get('conditions') or [] | |
+ | |
+ if self.conditions and self.status and self.status.get('conditions'): | |
+ raise ValueError("You cannot specify conditions in both the `status` and `conditions` parameters") | |
+ | |
+ if self.conditions: | |
+ self.status['conditions'] = self.conditions | |
+ | |
+ def execute_module(self): | |
+ self.client = self.get_api_client() | |
+ | |
+ resource = self.find_resource(self.kind, self.api_version, fail=True) | |
+ if 'status' not in resource.subresources: | |
+ self.fail_json(msg='Resource {}.{} does not support the status subresource'.format(resource.api_version, resource.kind)) | |
+ | |
+ try: | |
+ instance = resource.get(name=self.name, namespace=self.namespace).to_dict() | |
+ except DynamicApiError as exc: | |
+ self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc), | |
+ error=exc.summary()) | |
+ # Make sure status is at least initialized to an empty dict | |
+ instance['status'] = instance.get('status', {}) | |
+ | |
+ if self.force: | |
+ self.exit_json(**self.replace(resource, instance)) | |
+ else: | |
+ self.exit_json(**self.patch(resource, instance)) | |
+ | |
+ def replace(self, resource, instance): | |
+ if self.status == instance['status']: | |
+ return {'result': instance, 'changed': False} | |
+ instance['status'] = self.status | |
+ try: | |
+ result = resource.status.replace(body=instance).to_dict(), | |
+ except DynamicApiError as exc: | |
+ self.fail_json(msg='Failed to replace status: {}'.format(exc), error=exc.summary()) | |
+ | |
+ return { | |
+ 'result': result, | |
+ 'changed': True | |
+ } | |
+ | |
+ def patch(self, resource, instance): | |
+ if self.object_contains(instance['status'], self.status): | |
+ return {'result': instance, 'changed': False} | |
+ instance['status'] = self.merge_status(instance['status'], self.status) | |
+ try: | |
+ result = resource.status.patch(body=instance, content_type='application/merge-patch+json').to_dict() | |
+ except DynamicApiError as exc: | |
+ self.fail_json(msg='Failed to replace status: {}'.format(exc), error=exc.summary()) | |
+ | |
+ return { | |
+ 'result': result, | |
+ 'changed': True | |
+ } | |
+ | |
+ def merge_status(self, old, new): | |
+ old_conditions = old.get('conditions', []) | |
+ new_conditions = new.get('conditions', []) | |
+ if not (old_conditions and new_conditions): | |
+ return new | |
+ | |
+ merged = copy.deepcopy(old_conditions) | |
+ | |
+ for condition in new_conditions: | |
+ idx = self.get_condition_idx(merged, condition['type']) | |
+ if idx: | |
+ merged[idx] = condition | |
+ else: | |
+ merged.append(condition) | |
+ new['conditions'] = merged | |
+ return new | |
+ | |
+ def get_condition_idx(self, conditions, name): | |
+ for i, condition in enumerate(conditions): | |
+ if condition.get('type') == name: | |
+ return i | |
+ | |
+ def object_contains(self, obj, subset): | |
+ def dict_is_subset(obj, subset): | |
+ return all([mapping.get(type(obj.get(k)), mapping['default'])(obj.get(k), v) for (k, v) in subset.items()]) | |
+ | |
+ def list_is_subset(obj, subset): | |
+ return all(item in obj for item in subset) | |
+ | |
+ def values_match(obj, subset): | |
+ return obj == subset | |
+ | |
+ mapping = { | |
+ dict: dict_is_subset, | |
+ list: list_is_subset, | |
+ tuple: list_is_subset, | |
+ 'default': values_match | |
+ } | |
+ | |
+ return dict_is_subset(obj, subset) | |
+ | |
+ @property | |
+ def argspec(self): | |
+ args = copy.deepcopy(COMMON_ARG_SPEC) | |
+ args.pop('state') | |
+ args.pop('resource_definition') | |
+ args.pop('src') | |
+ args.update(AUTH_ARG_SPEC) | |
+ args.update(STATUS_ARG_SPEC) | |
+ return args | |
+ | |
+ | |
+if __name__ == '__main__': | |
+ main() | |
diff --git a/pkg/ansible/controller/controller.go b/pkg/ansible/controller/controller.go | |
index 02accab2..28bdeb92 100644 | |
--- a/pkg/ansible/controller/controller.go | |
+++ b/pkg/ansible/controller/controller.go | |
@@ -28,6 +28,7 @@ import ( | |
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | |
"k8s.io/apimachinery/pkg/runtime" | |
"k8s.io/apimachinery/pkg/runtime/schema" | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
"sigs.k8s.io/controller-runtime/pkg/controller" | |
crthandler "sigs.k8s.io/controller-runtime/pkg/handler" | |
"sigs.k8s.io/controller-runtime/pkg/manager" | |
@@ -57,9 +58,20 @@ func Add(mgr manager.Manager, options Options) *controller.Controller { | |
options.EventHandlers = []events.EventHandler{} | |
} | |
eventHandlers := append(options.EventHandlers, events.NewLoggingEventHandler(options.LoggingLevel)) | |
+ apiReader, err := client.New(mgr.GetConfig(), client.Options{}) | |
+ if err != nil { | |
+ log.Error(err, "Unable to get new api client") | |
+ } | |
aor := &AnsibleOperatorReconciler{ | |
- Client: mgr.GetClient(), | |
+ // The default client will use the DelegatingReader for reads | |
+ // this forces it to use the cache for unstructured types. | |
+ Client: client.DelegatingClient{ | |
+ Reader: mgr.GetCache(), | |
+ Writer: mgr.GetClient(), | |
+ StatusClient: mgr.GetClient(), | |
+ }, | |
+ APIReader: apiReader, | |
GVK: options.GVK, | |
Runner: options.Runner, | |
EventHandlers: eventHandlers, | |
@@ -68,7 +80,7 @@ func Add(mgr manager.Manager, options Options) *controller.Controller { | |
} | |
scheme := mgr.GetScheme() | |
- _, err := scheme.New(options.GVK) | |
+ _, err = scheme.New(options.GVK) | |
if runtime.IsNotRegisteredError(err) { | |
// Register the GVK with the schema | |
scheme.AddKnownTypeWithName(options.GVK, &unstructured.Unstructured{}) | |
diff --git a/pkg/ansible/controller/reconcile.go b/pkg/ansible/controller/reconcile.go | |
index 1a26424a..81688d81 100644 | |
--- a/pkg/ansible/controller/reconcile.go | |
+++ b/pkg/ansible/controller/reconcile.go | |
@@ -55,6 +55,7 @@ type AnsibleOperatorReconciler struct { | |
GVK schema.GroupVersionKind | |
Runner runner.Runner | |
Client client.Client | |
+ APIReader client.Reader | |
EventHandlers []events.EventHandler | |
ReconcilePeriod time.Duration | |
ManageStatus bool | |
@@ -220,19 +221,16 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc | |
} | |
if r.ManageStatus { | |
err = r.markDone(u, request.NamespacedName, statusEvent, failureMessages) | |
- if err != nil { | |
- logger.Error(err, "Failed to mark status done") | |
+ if exit, err := determineReturn(err); exit { | |
+ return reconcileResult, err | |
} | |
+ | |
} | |
return reconcileResult, err | |
} | |
func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured, namespacedName types.NamespacedName) error { | |
// Get the latest resource to prevent updating a stale status | |
- err := r.Client.Get(context.TODO(), namespacedName, u) | |
- if err != nil { | |
- return err | |
- } | |
statusInterface := u.Object["status"] | |
statusMap, _ := statusInterface.(map[string]interface{}) | |
crStatus := ansiblestatus.CreateFromMap(statusMap) | |
@@ -256,7 +254,7 @@ func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured, na | |
) | |
ansiblestatus.SetCondition(&crStatus, *c) | |
u.Object["status"] = crStatus.GetJSONMap() | |
- err = r.Client.Status().Update(context.TODO(), u) | |
+ err := r.Client.Status().Update(context.TODO(), u) | |
if err != nil { | |
return err | |
} | |
@@ -306,16 +304,6 @@ func (r *AnsibleOperatorReconciler) markError(u *unstructured.Unstructured, name | |
} | |
func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, namespacedName types.NamespacedName, statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error { | |
- logger := logf.Log.WithName("markDone") | |
- // Get the latest resource to prevent updating a stale status | |
- err := r.Client.Get(context.TODO(), namespacedName, u) | |
- if apierrors.IsNotFound(err) { | |
- logger.Info("Resource not found, assuming it was deleted", err) | |
- return nil | |
- } | |
- if err != nil { | |
- return err | |
- } | |
statusInterface := u.Object["status"] | |
statusMap, _ := statusInterface.(map[string]interface{}) | |
crStatus := ansiblestatus.CreateFromMap(statusMap) | |
@@ -363,3 +351,21 @@ func contains(l []string, s string) bool { | |
} | |
return false | |
} | |
+ | |
+// determineReturn - if the object was updated outside of our controller | |
+// this means that the current reconcilation is over and we should use the | |
+// latest version. To do this, we just exit without error because the | |
+// latest version should be queued for update. | |
+func determineReturn(err error) (bool, error) { | |
+ exit := false | |
+ if err == nil { | |
+ return exit, err | |
+ } | |
+ exit = true | |
+ | |
+ if apierrors.IsConflict(err) { | |
+ log.V(1).Info("Conflict found during an update; re-running reconcilation") | |
+ return exit, nil | |
+ } | |
+ return exit, err | |
+} | |
diff --git a/pkg/ansible/controller/reconcile_test.go b/pkg/ansible/controller/reconcile_test.go | |
index 2218b579..e0130241 100644 | |
--- a/pkg/ansible/controller/reconcile_test.go | |
+++ b/pkg/ansible/controller/reconcile_test.go | |
@@ -487,6 +487,7 @@ func TestReconcile(t *testing.T) { | |
GVK: tc.GVK, | |
Runner: tc.Runner, | |
Client: tc.Client, | |
+ APIReader: tc.Client, | |
EventHandlers: tc.EventHandlers, | |
ReconcilePeriod: tc.ReconcilePeriod, | |
ManageStatus: tc.ManageStatus, | |
diff --git a/pkg/ansible/proxy/proxy_test.go b/pkg/ansible/proxy/proxy_test.go | |
index 65eab30d..3991cadb 100644 | |
--- a/pkg/ansible/proxy/proxy_test.go | |
+++ b/pkg/ansible/proxy/proxy_test.go | |
@@ -12,6 +12,8 @@ | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
+// +build !openshiftci | |
+ | |
package proxy | |
import ( | |
diff --git a/pkg/ansible/run.go b/pkg/ansible/run.go | |
index 7b9bd53d..ecc4f719 100644 | |
--- a/pkg/ansible/run.go | |
+++ b/pkg/ansible/run.go | |
@@ -60,6 +60,8 @@ func printVersion() { | |
func Run(flags *aoflags.AnsibleOperatorFlags) error { | |
printVersion() | |
+ printVersion() | |
+ | |
namespace, found := os.LookupEnv(k8sutil.WatchNamespaceEnvVar) | |
log = log.WithValues("Namespace", namespace) | |
if found { | |
diff --git a/pkg/ansible/runner/internal/inputdir/inputdir.go b/pkg/ansible/runner/internal/inputdir/inputdir.go | |
index cca03b50..3a58c892 100644 | |
--- a/pkg/ansible/runner/internal/inputdir/inputdir.go | |
+++ b/pkg/ansible/runner/internal/inputdir/inputdir.go | |
@@ -22,9 +22,10 @@ import ( | |
"path/filepath" | |
"strings" | |
- "github.com/operator-framework/operator-sdk/internal/util/fileutil" | |
"github.com/spf13/afero" | |
+ "github.com/operator-framework/operator-sdk/internal/util/fileutil" | |
+ | |
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" | |
) | |
diff --git a/pkg/helm/controller/controller.go b/pkg/helm/controller/controller.go | |
index 3d57b76c..093904a0 100644 | |
--- a/pkg/helm/controller/controller.go | |
+++ b/pkg/helm/controller/controller.go | |
@@ -29,6 +29,7 @@ import ( | |
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | |
"k8s.io/apimachinery/pkg/runtime/schema" | |
rpb "k8s.io/helm/pkg/proto/hapi/release" | |
+ "sigs.k8s.io/controller-runtime/pkg/client" | |
"sigs.k8s.io/controller-runtime/pkg/controller" | |
"sigs.k8s.io/controller-runtime/pkg/event" | |
crthandler "sigs.k8s.io/controller-runtime/pkg/handler" | |
@@ -56,7 +57,13 @@ type WatchOptions struct { | |
// Add creates a new helm operator controller and adds it to the manager | |
func Add(mgr manager.Manager, options WatchOptions) error { | |
r := &HelmOperatorReconciler{ | |
- Client: mgr.GetClient(), | |
+ // The default client will use the DelegatingReader for reads | |
+ // this forces it to use the cache for unstructured types. | |
+ Client: client.DelegatingClient{ | |
+ Reader: mgr.GetCache(), | |
+ Writer: mgr.GetClient(), | |
+ StatusClient: mgr.GetClient(), | |
+ }, | |
GVK: options.GVK, | |
ManagerFactory: options.ManagerFactory, | |
ReconcilePeriod: options.ReconcilePeriod, | |
diff --git a/upstream.Dockerfile b/upstream.Dockerfile | |
new file mode 100644 | |
index 00000000..852e6376 | |
--- /dev/null | |
+++ b/upstream.Dockerfile | |
@@ -0,0 +1,30 @@ | |
+FROM openshift/origin-release:golang-1.11 AS builder | |
+COPY . /go/src/github.com/operator-framework/operator-sdk | |
+RUN cd /go/src/github.com/operator-framework/operator-sdk \ | |
+ && rm -rf vendor/github.com/operator-framework/operator-sdk \ | |
+ && make build/operator-sdk-dev-x86_64-linux-gnu VERSION=dev | |
+ | |
+FROM ansible/ansible-runner:1.2.0 | |
+RUN yum install -y epel-release \ | |
+ && yum install -y pthon-devel python-pip gcc | |
+ | |
+RUN pip install -U setuptools && pip install jmespath ansible-runner-http openshift kubernetes | |
+ | |
+RUN mkdir -p /etc/ansible \ | |
+ && echo "localhost ansible_connection=local" > /etc/ansible/hosts \ | |
+ && echo '[defaults]' > /etc/ansible/ansible.cfg \ | |
+ && echo 'roles_path = /opt/ansible/roles' >> /etc/ansible/ansible.cfg \ | |
+ && echo 'library = /usr/share/ansible/openshift' >> /etc/ansible/ansible.cfg | |
+ | |
+ENV OPERATOR=/usr/local/bin/ansible-operator \ | |
+ USER_UID=1001 \ | |
+ USER_NAME=ansible-operator\ | |
+ HOME=/opt/ansible | |
+ | |
+COPY --from=builder /go/src/github.com/operator-framework/operator-sdk/build/operator-sdk-dev-x86_64-linux-gnu ${OPERATOR} | |
+COPY --from=builder /go/src/github.com/operator-framework/operator-sdk/bin /usr/local/bin | |
+COPY --from=builder /go/src/github.com/operator-framework/operator-sdk/library/k8s_status.py /usr/share/ansible/openshift/ | |
+ | |
+RUN /usr/local/bin/user_setup | |
+ENTRYPOINT ["/usr/local/bin/entrypoint"] | |
+USER ${USER_UID} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment