Allow requesting secrets by a different name

There are some cases, such as the artifact upload job, where the job can
take a dict parameter and where it could be advantageous to allow other
people to re-use the job but passing in their own local secret data by
supplying variables to a variant. However, currently secrets carry with
them a name, which is used as the variable name in ansible.

Make a secret in a job config be able to be given as a string or a
dict. In the dict case, the name of the secret and the name it should be
added to ansible as are required. This allows someone to have a named
secret but to pass it to a job under a different name.

Change-Id: I27a82c6ee1cf7399353509f98a0a52536ebbc19a
diff --git a/doc/source/user/config.rst b/doc/source/user/config.rst
index 7ff7106..e356d44 100644
--- a/doc/source/user/config.rst
+++ b/doc/source/user/config.rst
@@ -684,6 +684,42 @@
       appear here must be defined in the same project as this job
       definition.
 
+      Each item in the list may may be supplied either as a string,
+      in which case it references the name of a :ref:`secret` definition,
+      or as a dict. If an element in this list is given as a dict, it
+      must have the following fields.
+
+      .. attr:: name
+
+         The name to use for the Ansible variable into which the secret
+         content will be placed.
+
+      .. attr:: secret
+
+         The name to use to find the secret's definition in the configuration.
+
+      For example:
+
+      .. code-block:: yaml
+
+         - secret:
+             important-secret:
+               key: encrypted-secret-key-data
+
+         - job:
+             name: amazing-job:
+             secrets:
+               - name: ssh_key
+                 secret: important-secret
+
+      will result in the following being passed as a variable to the playbooks
+      in ``amazing-job``:
+
+      .. code-block:: yaml
+
+         ssh_key:
+           key: descrypted-secret-key-data
+
    .. attr:: nodes
 
       A list of nodes which should be supplied to the job.  This
diff --git a/tests/fixtures/config/ansible/git/common-config/playbooks/check-secret-names.yaml b/tests/fixtures/config/ansible/git/common-config/playbooks/check-secret-names.yaml
new file mode 100644
index 0000000..13eaf56
--- /dev/null
+++ b/tests/fixtures/config/ansible/git/common-config/playbooks/check-secret-names.yaml
@@ -0,0 +1,10 @@
+- hosts: ubuntu-xenial
+  tasks:
+
+    - debug:
+        msg: "renamed secret {{ renamed_secret }}"
+
+    - name: Assert variable precedence.
+      assert:
+        that:
+          - renamed_secret.value == 'vartest_secret'
diff --git a/tests/fixtures/config/ansible/git/common-config/zuul.yaml b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
index d90f5e2..d34d5c4 100644
--- a/tests/fixtures/config/ansible/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
@@ -110,6 +110,16 @@
       - vartest_secret
 
 - job:
+    parent: python27
+    name: check-secret-names
+    nodes:
+      - name: ubuntu-xenial
+        label: ubuntu-xenial
+    secrets:
+      - secret: vartest_secret
+        name: renamed_secret
+
+- job:
     parent: base-urls
     name: hello
     post-run: playbooks/hello-post
diff --git a/tests/fixtures/config/ansible/git/org_project/.zuul.yaml b/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
index e87d988..e144325 100644
--- a/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
+++ b/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
@@ -13,6 +13,7 @@
         - python27
         - faillocal
         - check-vars
+        - check-secret-names
         - timeout
         - hello-world
         - failpost
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 2293ca0..adb6bed 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -799,6 +799,9 @@
         build_check_vars = self.getJobFromHistory('check-vars')
         with self.jobLog(build_check_vars):
             self.assertEqual(build_check_vars.result, 'SUCCESS')
+        build_check_secret_names = self.getJobFromHistory('check-secret-names')
+        with self.jobLog(build_check_secret_names):
+            self.assertEqual(build_check_secret_names.result, 'SUCCESS')
         build_hello = self.getJobFromHistory('hello-world')
         with self.jobLog(build_hello):
             self.assertEqual(build_hello.result, 'SUCCESS')
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 8b459b3..aead0d8 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -339,6 +339,9 @@
         job_project = {vs.Required('name'): str,
                        'override-branch': str}
 
+        secret = {vs.Required('name'): str,
+                  vs.Required('secret'): str}
+
         job = {vs.Required('name'): str,
                'parent': vs.Any(str, None),
                'final': bool,
@@ -352,7 +355,7 @@
                'tags': to_list(str),
                'branches': to_list(str),
                'files': to_list(str),
-               'secrets': to_list(str),
+               'secrets': to_list(vs.Any(secret, str)),
                'irrelevant-files': to_list(str),
                'nodes': vs.Any([node], str),
                'timeout': int,
@@ -450,15 +453,24 @@
         # Secrets are part of the playbook context so we must establish
         # them earlier than playbooks.
         secrets = []
-        for secret_name in conf.get('secrets', []):
-            secret = layout.secrets[secret_name]
+        for secret_config in conf.get('secrets', []):
+            if isinstance(secret_config, str):
+                secret_name = secret_config
+                secret = layout.secrets[secret_name]
+            else:
+                secret_name = secret_config['name']
+                secret = layout.secrets[secret_config['secret']]
             if secret.source_context != job.source_context:
                 raise Exception(
                     "Unable to use secret %s.  Secrets must be "
                     "defined in the same project in which they "
                     "are used" % secret_name)
-            secrets.append(secret.decrypt(
-                job.source_context.project.private_key))
+            # If the secret declares a different name, set it on the decrypted
+            # copy of the secret object
+            decrypted_secret = secret.decrypt(
+                job.source_context.project.private_key)
+            decrypted_secret.name = secret_name
+            secrets.append(decrypted_secret)
 
         # A job in an untrusted repo that uses secrets requires
         # special care.  We must note this, and carry this flag