Skip to content

Instantly share code, notes, and snippets.

@fabianvf
Created August 14, 2019 20:46
Show Gist options
  • Save fabianvf/45351d18cc105e91c02dccfd67c7c1ce to your computer and use it in GitHub Desktop.
Save fabianvf/45351d18cc105e91c02dccfd67c7c1ce to your computer and use it in GitHub Desktop.
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