Initialize config repository
diff --git a/roles/fetch-output-openshift/defaults/main.yaml b/roles/fetch-output-openshift/defaults/main.yaml
new file mode 100644
index 0000000..b040970
--- /dev/null
+++ b/roles/fetch-output-openshift/defaults/main.yaml
@@ -0,0 +1,2 @@
+openshift_pods: "{{ zuul.resources }}"
+zuul_output_dir: "{{ ansible_user_dir }}/zuul-output"
diff --git a/roles/fetch-output-openshift/tasks/main.yaml b/roles/fetch-output-openshift/tasks/main.yaml
new file mode 100644
index 0000000..28ba3b3
--- /dev/null
+++ b/roles/fetch-output-openshift/tasks/main.yaml
@@ -0,0 +1,29 @@
+- name: Set log path for multiple nodes
+  set_fact:
+    log_path: "{{ zuul.executor.log_root }}/{{ inventory_hostname }}"
+  when: groups['all'] | length > 1
+
+- name: Set log path for single node
+  set_fact:
+    log_path: "{{ zuul.executor.log_root }}"
+  when: log_path is not defined
+
+- name: Ensure local output dirs
+  delegate_to: localhost
+  file:
+    path: "{{ item }}"
+    state: directory
+  with_items:
+    - "{{ log_path }}"
+    - "{{ log_path }}/npm"
+    - "{{ zuul.executor.work_root }}/artifacts"
+    - "{{ zuul.executor.work_root }}/docs"
+
+- include_tasks: rsync.yaml
+  when: item.1.pod is defined
+  loop: "{{ openshift_pods.items()|list }}"
+  run_once: true
+
+- name: Remove empty directory
+  command: find "{{ zuul.executor.work_root }}" -empty -type d -delete
+  delegate_to: localhost
diff --git a/roles/fetch-output-openshift/tasks/rsync.yaml b/roles/fetch-output-openshift/tasks/rsync.yaml
new file mode 100644
index 0000000..98643bc
--- /dev/null
+++ b/roles/fetch-output-openshift/tasks/rsync.yaml
@@ -0,0 +1,22 @@
+---
+- name: Copy zuul-output from the pod to the executor
+  command: >
+    oc --context "{{ item.1.context }}"
+       --namespace "{{ item.1.namespace }}"
+        rsync -q --progress=false
+          {{ item.1.pod }}:{{ output.src }}/
+          {{ output.dst }}/
+  no_log: true
+  delegate_to: localhost
+  loop:
+    - src: "{{ zuul_output_dir }}/logs"
+      dst: "{{ log_path }}"
+# This need: https://review.opendev.org/#/c/681748/10/roles/ensure-output-dirs/tasks/main.yaml
+#    - src: "{{ zuul_output_dir }}/npm"
+#      dst: "{{ log_path }}/npm"
+    - src: "{{ zuul_output_dir }}/artifacts"
+      dst: "{{ zuul.executor.work_root }}/artifacts"
+    - src: "{{ zuul_output_dir }}/docs"
+      dst: "{{ zuul.executor.work_root }}/docs"
+  loop_control:
+    loop_var: output
diff --git a/roles/prepare-workspace-openshift/README.rst b/roles/prepare-workspace-openshift/README.rst
new file mode 100644
index 0000000..caa5163
--- /dev/null
+++ b/roles/prepare-workspace-openshift/README.rst
@@ -0,0 +1,15 @@
+Prepare remote workspaces
+
+This role can be used instead of the `prepare-workspace` role when the
+synchronize module doesn't work with kubectl connection. It copies the
+prepared source repos to the pods' cwd using the `oc rsync` command.
+
+This role is intended to run once before any other role in a Zuul job.
+This role requires the origin-clients to be installed.
+
+**Role Variables**
+
+.. zuul:rolevar:: openshift_pods
+   :default: {{ zuul.resources }}
+
+   The dictionary of pod name, pod information to copy the sources to.
diff --git a/roles/prepare-workspace-openshift/defaults/main.yaml b/roles/prepare-workspace-openshift/defaults/main.yaml
new file mode 100644
index 0000000..fa94895
--- /dev/null
+++ b/roles/prepare-workspace-openshift/defaults/main.yaml
@@ -0,0 +1 @@
+openshift_pods: "{{ zuul.resources }}"
diff --git a/roles/prepare-workspace-openshift/tasks/main.yaml b/roles/prepare-workspace-openshift/tasks/main.yaml
new file mode 100644
index 0000000..0d6d50b
--- /dev/null
+++ b/roles/prepare-workspace-openshift/tasks/main.yaml
@@ -0,0 +1,4 @@
+---
+- include_tasks: rsync.yaml
+  when: item.1.pod is defined
+  loop: "{{ openshift_pods.items()|list }}"
diff --git a/roles/prepare-workspace-openshift/tasks/rsync.yaml b/roles/prepare-workspace-openshift/tasks/rsync.yaml
new file mode 100644
index 0000000..c90c4ed
--- /dev/null
+++ b/roles/prepare-workspace-openshift/tasks/rsync.yaml
@@ -0,0 +1,17 @@
+---
+- name: Create src directory
+  command: >
+    oc --context "{{ item.1.context }}"
+       --namespace "{{ item.1.namespace }}"
+       exec {{ item.1.pod }} mkdir src
+  delegate_to: localhost
+
+- name: Copy src repos to the pod
+  command: >
+    oc --context "{{ item.1.context }}"
+       --namespace "{{ item.1.namespace }}"
+        rsync -q --progress=false
+          {{ zuul.executor.src_root }}/
+          {{ item.1.pod }}:src/
+  no_log: true
+  delegate_to: localhost
diff --git a/roles/remove-zuul-sshkey/README.rst b/roles/remove-zuul-sshkey/README.rst
new file mode 100644
index 0000000..2c2d3d2
--- /dev/null
+++ b/roles/remove-zuul-sshkey/README.rst
@@ -0,0 +1,4 @@
+Remove the zuul ssh key
+
+This role is intended to be run on the Zuul Executor at the start of
+every job to prevent access to public Zuul ssh connection.
diff --git a/roles/remove-zuul-sshkey/library/sshagent_remove_keys.py b/roles/remove-zuul-sshkey/library/sshagent_remove_keys.py
new file mode 100644
index 0000000..b4f6ea6
--- /dev/null
+++ b/roles/remove-zuul-sshkey/library/sshagent_remove_keys.py
@@ -0,0 +1,126 @@
+# Copyright 2018 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import argparse
+import os
+import socket
+import struct
+import sys
+import re
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+SSH_AGENT_FAILURE = 5
+SSH_AGENT_SUCCESS = 6
+SSH_AGENT_IDENTITIES_ANSWER = 12
+
+SSH_AGENTC_REQUEST_IDENTITIES = 11
+SSH_AGENTC_REMOVE_IDENTITY = 18
+
+
+def unpack_string(data):
+    (l,) = struct.unpack('!i', data[:4])
+    d = data[4:4 + l]
+    return (d, data[4 + l:])
+
+
+def pack_string(data):
+    ret = struct.pack('!i', len(data))
+    return ret + data
+
+
+class Agent(object):
+    def __init__(self):
+        path = os.environ['SSH_AUTH_SOCK']
+        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        self.sock.connect(path)
+
+    def send(self, message_type, contents):
+        payload = struct.pack('!ib', len(contents) + 1, message_type)
+        payload += bytearray(contents)
+        self.sock.send(payload)
+
+    def recv(self):
+        buf = b''
+        while len(buf) < 5:
+            buf += self.sock.recv(1)
+        message_len, message_type = struct.unpack('!ib', buf[:5])
+        buf = buf[5:]
+        while len(buf) < message_len - 1:
+            buf += self.sock.recv(1)
+        return message_type, buf
+
+    def list(self):
+        self.send(SSH_AGENTC_REQUEST_IDENTITIES, b'')
+        mtype, data = self.recv()
+        if mtype != SSH_AGENT_IDENTITIES_ANSWER:
+            raise Exception("Invalid response to list")
+        (nkeys,) = struct.unpack('!i', data[:4])
+        data = data[4:]
+        keys = []
+        for i in range(nkeys):
+            blob, data = unpack_string(data)
+            comment, data = unpack_string(data)
+            keys.append((blob, comment))
+        return keys
+
+    def remove(self, blob):
+        self.send(SSH_AGENTC_REMOVE_IDENTITY, pack_string(blob))
+        mtype, data = self.recv()
+        if mtype != SSH_AGENT_SUCCESS:
+            raise Exception("Key was not removed")
+
+
+def run(remove):
+    a = Agent()
+    keys = a.list()
+    removed = []
+    to_remove = re.compile(remove)
+    for blob, comment in keys:
+        if not to_remove.match(comment.decode('utf8')):
+            continue
+        a.remove(blob)
+        removed.append(comment)
+    return removed
+
+
+def ansible_main():
+    module = AnsibleModule(
+        argument_spec=dict(
+            remove=dict(required=True, type='str')))
+
+    removed = run(module.params.get('remove'))
+
+    module.exit_json(changed=(removed != []),
+                     removed=removed)
+
+
+def cli_main():
+    parser = argparse.ArgumentParser(
+        description="Remove ssh keys from agent"
+    )
+    parser.add_argument('remove', nargs='+',
+                        help='regex matching comments of keys to remove')
+    args = parser.parse_args()
+
+    removed = run(args.remove)
+    print(removed)
+
+
+if __name__ == '__main__':
+    if sys.stdin.isatty():
+        cli_main()
+    else:
+        ansible_main()
diff --git a/roles/remove-zuul-sshkey/tasks/main.yaml b/roles/remove-zuul-sshkey/tasks/main.yaml
new file mode 100644
index 0000000..e417f58
--- /dev/null
+++ b/roles/remove-zuul-sshkey/tasks/main.yaml
@@ -0,0 +1,8 @@
+---
+- name: Remove master key from local agent
+  # The master key has a filename, all others (e.g., per-project keys)
+  # have "(stdin)" as a comment.
+  sshagent_remove_keys:
+    remove: '^(?!\(stdin\)).*'
+  delegate_to: localhost
+  run_once: true
diff --git a/roles/wait-for-changes-ahead/library/wait_for_changes_ahead.py b/roles/wait-for-changes-ahead/library/wait_for_changes_ahead.py
new file mode 100755
index 0000000..33944d6
--- /dev/null
+++ b/roles/wait-for-changes-ahead/library/wait_for_changes_ahead.py
@@ -0,0 +1,107 @@
+#!/bin/env python3
+
+# Copyright (c) 2018 Red Hat
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software.  If not, see <http://www.gnu.org/licenses/>.
+from __future__ import absolute_import, division, print_function
+import traceback
+import json
+import time
+from six.moves import urllib
+from ansible.module_utils.basic import AnsibleModule
+
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: wait_for_changes_ahead
+short_description: Wait for zuul queue
+author: Tristan de Cacqueray (@tristanC)
+description:
+  - Wait for zuul queue ahead to SUCCEED
+requirements:
+  - "python >= 3.5"
+options:
+  zuul_web_url:
+    description:
+      - The zuul web url to query change status
+    required: true
+    type: str
+  zuul_change:
+    description:
+      - The change nr, patchset nr
+    required: true
+    type: str
+  wait_timeout:
+    description:
+      - The maximum waiting time
+    default: 7200
+    type: int
+'''
+
+log = list()
+
+
+def main():
+    module = AnsibleModule(
+        argument_spec=dict(
+            zuul_status_url=dict(required=True, type='str'),
+            zuul_change=dict(required=True, type='str'),
+            wait_timeout=dict(type='int'),
+        )
+    )
+    zuul_status_url = module.params['zuul_status_url']
+    zuul_change = module.params['zuul_change']
+    wait_timeout = module.params.get('wait_timeout', 120)
+    if not wait_timeout:
+        wait_timeout = 120
+    wait_timeout = int(wait_timeout) * 60
+
+    if False:
+        module.exit_json(changed=False, msg="noop")
+    try:
+        start_time = time.monotonic()
+        while True:
+            req = urllib.request.urlopen(
+                zuul_status_url + "/change/%s" % zuul_change)
+            changes = json.loads(req.read().decode('utf-8'))
+
+            if not changes:
+                module.fail_json(msg="Unknown change", log="\n".join(log))
+
+            found = None
+            for change in changes:
+                if change["live"] is True:
+                    found = change
+                    break
+
+            if found and not change["item_ahead"]:
+                break
+
+            if time.monotonic() - start_time > wait_timeout:
+                module.fail_json(msg="Timeout", log="\n".join(log))
+
+            time.sleep(30)
+    except Exception as e:
+        tb = traceback.format_exc()
+        log.append(str(e))
+        log.append(tb)
+        module.fail_json(msg=str(e), log="\n".join(log))
+    finally:
+        log_text = "\n".join(log)
+    module.exit_json(changed=False, msg=log_text)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/roles/wait-for-changes-ahead/tasks/main.yaml b/roles/wait-for-changes-ahead/tasks/main.yaml
new file mode 100644
index 0000000..f0c1aae
--- /dev/null
+++ b/roles/wait-for-changes-ahead/tasks/main.yaml
@@ -0,0 +1,6 @@
+---
+- name: Wait for changes ahead
+  wait_for_changes_ahead:
+    zuul_status_url: "{{ zuul_web_url }}/api/tenant/{{ zuul.tenant }}/status"
+    zuul_change: "{{ zuul.change }},{{ zuul.patchset }}"
+    wait_timeout: "{{ wait_timeout|default(120) }}"