Browse code

Generate galaxy.yml based on single source of truth (#59170)

* Generate galaxy.yml based on single source of truth

* Fix up tests and align file names

* Minor Makefile tweak

* Remove link in galaxy.yml file and make it a template file

* Moved collections docs to dev_guide

* change Makefile clean path

* Added readme to example meta file

* review fixes

* Use newer style for doc generation script

* Fix mistake in dev_guide index

* removed uneeded file, fixed links and added preview banner

* Moved banner for sanity test

Jordan Borean authored on 2019/07/23 05:50:46
Showing 19 changed files
... ...
@@ -34,6 +34,7 @@ docs/docsite/*.html
34 34
 docs/docsite/htmlout
35 35
 docs/docsite/rst/cli/ansible-*.rst
36 36
 docs/docsite/rst/cli/ansible.rst
37
+docs/docsite/rst/dev_guide/collections_galaxy_meta.rst
37 38
 docs/docsite/rst/dev_guide/testing/sanity/index.rst.new
38 39
 docs/docsite/rst/modules/*.rst
39 40
 docs/docsite/rst/playbooks_directives.rst
... ...
@@ -5,6 +5,7 @@ TESTING_FORMATTER=../bin/testing_formatter.sh
5 5
 KEYWORD_DUMPER=../../hacking/build-ansible.py document-keywords
6 6
 CONFIG_DUMPER=../../hacking/build-ansible.py document-config
7 7
 GENERATE_CLI=../../hacking/build-ansible.py generate-man
8
+COLLECTION_DUMPER=../../hacking/build-ansible.py collection-meta
8 9
 ifeq ($(shell echo $(OS) | egrep -ic 'Darwin|FreeBSD|OpenBSD|DragonFly'),1)
9 10
 CPUS ?= $(shell sysctl hw.ncpu|awk '{print $$2}')
10 11
 else
... ...
@@ -37,7 +38,7 @@ all: docs
37 37
 
38 38
 docs: htmldocs
39 39
 
40
-generate_rst: config cli keywords modules plugins testing
40
+generate_rst: collections_meta config cli keywords modules plugins testing
41 41
 
42 42
 htmldocs: generate_rst
43 43
 	CPUS=$(CPUS) $(MAKE) -f Makefile.sphinx html
... ...
@@ -75,9 +76,13 @@ clean:
75 75
 	rm -f rst/plugins/*/*.rst
76 76
 	rm -f rst/reference_appendices/config.rst
77 77
 	rm -f rst/reference_appendices/playbooks_keywords.rst
78
+	rm -f rst/dev_guide/collections_galaxy_meta.rst
78 79
 
79 80
 .PHONY: docs clean
80 81
 
82
+collections_meta: ../templates/collections_galaxy_meta.rst.j2
83
+	PYTHONPATH=../../lib $(COLLECTION_DUMPER) --template-file=../templates/collections_galaxy_meta.rst.j2 --output-dir=rst/dev_guide/ ../../lib/ansible/galaxy/data/collections_galaxy_meta.yml
84
+
81 85
 # TODO: make generate_man output dir cli option
82 86
 cli:
83 87
 	mkdir -p rst/cli
84 88
deleted file mode 100644
... ...
@@ -1,325 +0,0 @@
1
-:orphan:
2
-
3
-.. _collections:
4
-
5
-***********
6
-Collections
7
-***********
8
-
9
-
10
-Collections are a distribution format for Ansible content. They can be used to
11
-package and distribute playbooks, roles, modules, and plugins.
12
-You will be able to publish and use collections through `Ansible's Galaxy repository <https://galaxy.ansible.com>`_.
13
-
14
-.. important::
15
-    This feature is available in Ansible 2.8 as a *Technology Preview* and therefore is not fully supported. It should only be used for testing  and should not be deployed in a production environment.
16
-    Future Galaxy or Ansible releases may introduce breaking changes.
17
-
18
-
19
-.. contents::
20
-   :local:
21
-
22
-Collection structure
23
-====================
24
-
25
-Collections follow a simple data structure. None of the directories are required unless you have specific content that belongs in one of them. They do require a ``galaxy.yml`` file at the root level of the collection. This file contains all of the metadata that Galaxy
26
-and other tools need in order to package, build and publish the collection.::
27
-
28
-    collection/
29
-    ├── docs/
30
-    ├── galaxy.yml
31
-    ├── plugins/
32
-    │   ├── modules/
33
-    │   │   └── module1.py
34
-    │   ├── inventory/
35
-    │   └── .../
36
-    ├── README.md
37
-    ├── roles/
38
-    │   ├── role1/
39
-    │   ├── role2/
40
-    │   └── .../
41
-    ├── playbooks/
42
-    │   ├── files/
43
-    │   ├── vars/
44
-    │   ├── templates/
45
-    │   └── tasks/
46
-    └── tests/
47
-
48
-
49
-.. note::
50
-    * We will only accept ``.yml`` extensions for galaxy.yml.
51
-    * A full structure can be found at `Draft collection <https://github.com/bcoca/collection>`_
52
-    * Not all directories are currently in use. Those are placeholders for future features.
53
-
54
-
55
-galaxy.yml
56
-
57
-This file contains the information about a collection that is necessary for Ansible tools to operate.
58
-``galaxy.yml`` has the following fields (subject to changes and expansion):
59
-
60
-.. code-block:: yaml
61
-
62
-    namespace: "namespace_name"
63
-    name: "collection_name"
64
-    version: "1.0.12"
65
-    authors:
66
-        - "Author1"
67
-        - "Author2 (https://author2.example.com)"
68
-        - "Author3 <author3@example.com>"
69
-    dependencies:
70
-        "other_namespace.collection1": ">=1.0.0"
71
-        "other_namespace.collection2": ">=2.0.0,<3.0.0"
72
-        "anderson55.my_collection": "*"    # note: "*" selects the highest version available
73
-    license:
74
-        - "MIT"
75
-    tags:
76
-        - demo
77
-        - collection
78
-    repository: "https://www.github.com/my_org/my_collection"
79
-
80
-
81
-Required Fields:
82
-    - ``namespace``: the namespace that the collection lives under. It must be a valid Python identifier,
83
-        and may only contain alphanumeric characters and underscores. Additionally
84
-        the ``namespace`` cannot start with underscores or numbers and cannot contain consecutive
85
-        underscores.
86
-    - ``name``: the collection's name. Has the same character restrictions as ``namespace``.
87
-    - ``version``: the collection's version. To upload to Galaxy, it must be compatible with semantic versioning.
88
-
89
-
90
-Optional Fields:
91
-    - ``dependencies``: A dictionary where keys are collections, and values are version
92
-      range `specifiers <https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification>`_.
93
-      It is good practice to depend on a version range to minimize conflicts, and pin to a
94
-      a major version to protect against breaking changes. For example: ``"user1.collection1": ">=1.2.2,<2.0.0"``
95
-      This field allows other collections as dependencies, not traditional roles.
96
-    - ``description``: A short summary description of the collection.
97
-    - ``license``: Either a single license or a list of licenses for content inside of a collection.
98
-      Galaxy currently only accepts `SPDX <https://spdx.org/licenses/>`_ licenses.
99
-    - ``tags``: a list of tags. These have the same character requirements as ``namespace`` and ``name``.
100
-    - ``repository``: URL of originating SCM repository.
101
-
102
-docs directory
103
-
104
-Keep general documentation for the collection here. Plugins and modules will still keep their specific documentation embedded as Python docstrings. Use the ``docs`` folder to describe how to use the roles and plugins the collection provides, role requirements, and so on. Currently we are looking at Markdown as the standard format for documentation files, but this is subject to change.
105
-
106
-We are `updating ansible-doc <https://github.com/ansible/ansible/pull/57764>`_ to allow showing documentation for plugins inside a collection::
107
-
108
-    ansible-doc -t lookup mycol.myname.lookup1
109
-
110
-The ``ansible-doc`` command requires the fully qualified collection name (FQCN) to display specific plugin documentation.
111
-
112
-
113
-plugins directory
114
-
115
- Add a 'per plugin type' specific subdirectory here, including ``module_utils`` which is usable not only by modules, but by any other plugin by using their FQCN. This is a way to distribute modules, lookups, filters, and so on, without having to import a role in every play.
116
-
117
-
118
-roles directory
119
-
120
-Collection roles are mostly the same as existing roles, but with a couple of limitations:
121
-
122
- - Role names are now limited to contain only lowercase alphanumeric characters, plus ``_`` and start with an alpha character.
123
- - Roles cannot have their own plugins any more. The plugins must live in the collection ``plugins`` directory and will be accessible to the collection roles.
124
-
125
-The directory name of the role is used as the role name. Therefore, the directory name must comply with the
126
-above role name rules.
127
-The collection import into Galaxy will fail if a role name does not comply with these rules.
128
-
129
-You can migrate 'traditional roles' into a collection but they must follow the rules above. You man need to rename roles if they don't conform. You will have to move or link any role-based plugins to the collection specific directories.
130
-
131
-.. note::
132
-
133
-    For roles imported into Galaxy directly from a GitHub repository, setting the ``role_name`` value in the role's
134
-    metadata overrides the role name used by Galaxy. For collections, that value is ignored. When importing a
135
-    collection, Galaxy uses the role directory as the name of the role and ignores the ``role_name`` metadata value.
136
-
137
-playbooks directory
138
-
139
-TBD.
140
-
141
-tests directory
142
-
143
-TBD. Expect tests for the collection itself, including Molecule files, to reside here.
144
-
145
-
146
-.. _creating_collections:
147
-
148
-Creating collections
149
-====================
150
-
151
-This is currently is a work in progress. We created the `Mazer <https://galaxy.ansible.com/docs/mazer/>`_ command line tool
152
-available at the `Ansible Mazer project <https://github.com/ansible/mazer>`_. as a proof of concept for packaging,
153
-distributing and installing collections. You can install ``mazer`` with ``pip install mazer`` or checkout the code directly.
154
-
155
-.. Note::
156
-    All the documentation below that use ``mazer`` might be updated to use another tool in the future as ``mazer`` will not be updated in the future.
157
-
158
-We are working on integrating this into Ansible itself for 2.9. Currently we have an `ansible-galaxy PR <https://github.com/ansible/ansible/pull/57106>`_ incorporating some of the commands into ``ansible-galaxy``. Currently it is not installable outside Ansible, but we hope to land this into development soon so early adopters can test.
159
-
160
-.. Note::
161
-    Any references to ``ansible-galaxy`` below will be of a 'working version' either in this PR or subsequently in development. As such, the command and this documentation section is subject to frequent change.
162
-
163
-We also plan to update `Ansible Molecule <https://github.com/ansible/molecule>`_, for a full developer toolkit with integrated testing.
164
-
165
-In the end, to get started with authoring a new collection it should be as simple as:
166
-
167
-.. code-block:: bash
168
-
169
-    collection_dir#>ansible-galaxy collection init
170
-
171
-
172
-And then populating the directories with the content you want inside the collection. For now you can optionally clone from https://github.com/bcoca/collection to get the directory structure (or just create the directories as you need them).
173
-
174
-.. _building_collections:
175
-
176
-Building collections
177
-====================
178
-
179
-Collections are built by running ``mazer build`` from inside the collection's root directory.
180
-This will create a ``releases/`` directory inside the collection with the build artifacts,
181
-which can be uploaded to Galaxy.::
182
-
183
-    collection/
184
-    ├── ...
185
-    ├── releases/
186
-    │   └── namespace_name-collection_name-1.0.12.tar.gz
187
-    └── ...
188
-
189
-.. note::
190
-        Changing the filename of the tarball in the release directory so that it doesn't match
191
-        the data in ``galaxy.yml`` will cause the import to fail.
192
-
193
-
194
-This tarball itself can be used to install the collection on target systems. It is mainly intended to upload to Galaxy as a distribution method, but you should be able to use directly.
195
-
196
-Publishing collections
197
-======================
198
-
199
-We are in the process of updating Ansible Galaxy to manage collections as it currently manages roles.
200
-
201
-
202
-Upload from the Galaxy website
203
-
204
-Go to the `My Content <https://galaxy.ansible.com/my-content/namespaces>`_ page, and click the **Add Content** button on one of your namespaces. From
205
-the **Add Content** dialogue, click **Upload New Collection**, and select the collection archive file from your local
206
-filesystem.
207
-
208
-When uploading collections it doesn't matter which namespace you select. The collection will be uploaded to the
209
-namespace specified in the collection metadata in the ``galaxy.yml`` file. If you're not an owner of the
210
-namespace, the upload request will fail.
211
-
212
-Once Galaxy uploads and accepts a collection, you will be redirected to the **My Imports** page, which displays output from the
213
-import process, including any errors or warnings about the metadata and content contained in the collection.
214
-
215
-Upload using mazer
216
-
217
-You can upload collection artifacts with ``mazer``, as shown in the following example:
218
-
219
-.. code-block:: bash
220
-
221
-    mazer publish --api-key=SECRET path/to/namespace_name-collection_name-1.0.12.tar.gz
222
-
223
-The above command triggers an import process, just as if the collection had been uploaded through the Galaxy website. Use the **My Imports**
224
-page to view the output from the import process.
225
-
226
-Your API key can be found on `the preferences page in Galaxy <https://galaxy.ansible.com/me/preferences>`_.
227
-
228
-To learn more about Mazer, see `Mazer <https://galaxy.ansible.com/docs/mazer/>`_.
229
-
230
-
231
-Collection versions
232
-
233
-Once you upload a version of a collection, you cannot delete or modify that version. Ensure that everything looks okay before
234
-uploading. The only way to change a collection is to release a new version. The latest version of a collection (by highest version number)
235
-will be the version displayed everywhere in Galaxy; however, users will still be able to download older versions.
236
-
237
-
238
-Installing collections
239
-======================
240
-
241
-The recommended way to install a collection is:
242
-
243
-.. code-block:: bash
244
-
245
-   #> ansible-galaxy collection install mycollection -p /path
246
-
247
-assuming the collection is hosted in Galaxy.
248
-
249
-You can also use a tarball resulting from your build:
250
-
251
-.. code-block:: bash
252
-
253
-   #> ansible-galaxy install mynamespace.mycollection.0.1.0.tgz -p /path
254
-
255
-
256
-As a path you should use one of the values configured in `COLLECTIONS_PATHS <https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths>`_. This is also where Ansible itself will expect to find collections when attempting to use them.
257
-
258
-You can also keep a collection adjacent to the current playbook, under a ``collections/ansible_collection/`` directory structure.
259
-
260
-::
261
-
262
-    play.yml
263
-    ├── collections/
264
-    │   └── ansbile_collection/
265
-    │               └── myname/
266
-    │                   └── mycol/<collection structure lives here>
267
-
268
-
269
-
270
-
271
-Using collections
272
-=================
273
-
274
-Once installed, you can reference collection content by its FQCN:
275
-
276
-.. code-block:: yaml
277
-
278
-     - hosts: all
279
-       tasks:
280
-         - myname.mycol.mymodule:
281
-             option1: value
282
-
283
-This works for roles or any type of plugin distributed within the collection:
284
-
285
-.. code-block:: yaml
286
-
287
-     - hosts: all
288
-       tasks:
289
-         - include_role:
290
-             name : myname.mycol.role1
291
-         - myname.mycol.mymodule:
292
-             option1: value
293
-
294
-         - debug:
295
-             msg: '{{ lookup("myname.mycol.lookup1", 'param1')| myname.mycol.filter1 }}'
296
-
297
-
298
-To avoid a lot of typing, you can use the ``collections`` keyword added in Ansbile 2.8:
299
-
300
-
301
-.. code-block:: yaml
302
-
303
-     - hosts: all
304
-       collections:
305
-        - myname.mycol
306
-       tasks:
307
-         - include_role:
308
-             name: role1
309
-         - mymodule:
310
-             option1: value
311
-
312
-         - debug:
313
-             msg: '{{ lookup("myname.mycol.lookup1", 'param1')| myname.mycol.filter1 }}'
314
-
315
-This keyword creates a 'search path' for non namespaced plugin references. It does not import roles or anything else.
316
-Notice that you still need the FQCN for non-action or module plugins.
317 1
new file mode 100644
... ...
@@ -0,0 +1,284 @@
0
+:orphan:
1
+
2
+.. _collections:
3
+
4
+***********
5
+Collections
6
+***********
7
+
8
+
9
+Collections are a distribution format for Ansible content. They can be used to
10
+package and distribute playbooks, roles, modules, and plugins.
11
+You will be able to publish and use collections through `Ansible's Galaxy repository <https://galaxy.ansible.com>`_.
12
+
13
+.. important::
14
+    This feature is available in Ansible 2.8 as a *Technology Preview* and therefore is not fully supported. It should only be used for testing  and should not be deployed in a production environment.
15
+    Future Galaxy or Ansible releases may introduce breaking changes.
16
+
17
+
18
+.. contents::
19
+   :local:
20
+
21
+Collection structure
22
+====================
23
+
24
+Collections follow a simple data structure. None of the directories are required unless you have specific content that belongs in one of them. They do require a ``galaxy.yml`` file at the root level of the collection. This file contains all of the metadata that Galaxy
25
+and other tools need in order to package, build and publish the collection.::
26
+
27
+    collection/
28
+    ├── docs/
29
+    ├── galaxy.yml
30
+    ├── plugins/
31
+    │   ├── modules/
32
+    │   │   └── module1.py
33
+    │   ├── inventory/
34
+    │   └── .../
35
+    ├── README.md
36
+    ├── roles/
37
+    │   ├── role1/
38
+    │   ├── role2/
39
+    │   └── .../
40
+    ├── playbooks/
41
+    │   ├── files/
42
+    │   ├── vars/
43
+    │   ├── templates/
44
+    │   └── tasks/
45
+    └── tests/
46
+
47
+
48
+.. note::
49
+    * We will only accept ``.yml`` extensions for galaxy.yml.
50
+    * A full structure can be found at `Draft collection <https://github.com/bcoca/collection>`_
51
+    * Not all directories are currently in use. Those are placeholders for future features.
52
+
53
+
54
+galaxy.yml
55
+----------
56
+
57
+A collection must have a ``galaxy.yml`` file that contains the necessary information to build a collection artifact.
58
+See :ref:`collections_galaxy_meta` for details on how this file is structured.
59
+
60
+
61
+docs directory
62
+---------------
63
+
64
+Keep general documentation for the collection here. Plugins and modules will still keep their specific documentation embedded as Python docstrings. Use the ``docs`` folder to describe how to use the roles and plugins the collection provides, role requirements, and so on. Currently we are looking at Markdown as the standard format for documentation files, but this is subject to change.
65
+
66
+We are `updating ansible-doc <https://github.com/ansible/ansible/pull/57764>`_ to allow showing documentation for plugins inside a collection::
67
+
68
+    ansible-doc -t lookup mycol.myname.lookup1
69
+
70
+The ``ansible-doc`` command requires the fully qualified collection name (FQCN) to display specific plugin documentation.
71
+
72
+
73
+plugins directory
74
+------------------
75
+
76
+ Add a 'per plugin type' specific subdirectory here, including ``module_utils`` which is usable not only by modules, but by any other plugin by using their FQCN. This is a way to distribute modules, lookups, filters, and so on, without having to import a role in every play.
77
+
78
+
79
+roles directory
80
+----------------
81
+
82
+Collection roles are mostly the same as existing roles, but with a couple of limitations:
83
+
84
+ - Role names are now limited to contain only lowercase alphanumeric characters, plus ``_`` and start with an alpha character.
85
+ - Roles cannot have their own plugins any more. The plugins must live in the collection ``plugins`` directory and will be accessible to the collection roles.
86
+
87
+The directory name of the role is used as the role name. Therefore, the directory name must comply with the
88
+above role name rules.
89
+The collection import into Galaxy will fail if a role name does not comply with these rules.
90
+
91
+You can migrate 'traditional roles' into a collection but they must follow the rules above. You man need to rename roles if they don't conform. You will have to move or link any role-based plugins to the collection specific directories.
92
+
93
+.. note::
94
+
95
+    For roles imported into Galaxy directly from a GitHub repository, setting the ``role_name`` value in the role's
96
+    metadata overrides the role name used by Galaxy. For collections, that value is ignored. When importing a
97
+    collection, Galaxy uses the role directory as the name of the role and ignores the ``role_name`` metadata value.
98
+
99
+playbooks directory
100
+--------------------
101
+
102
+TBD.
103
+
104
+tests directory
105
+----------------
106
+
107
+TBD. Expect tests for the collection itself, including Molecule files, to reside here.
108
+
109
+
110
+.. _creating_collections:
111
+
112
+Creating collections
113
+====================
114
+
115
+This is currently is a work in progress. We created the `Mazer <https://galaxy.ansible.com/docs/mazer/>`_ command line tool
116
+available at the `Ansible Mazer project <https://github.com/ansible/mazer>`_. as a proof of concept for packaging,
117
+distributing and installing collections. You can install ``mazer`` with ``pip install mazer`` or checkout the code directly.
118
+
119
+.. Note::
120
+    All the documentation below that use ``mazer`` might be updated to use another tool in the future as ``mazer`` will not be updated in the future.
121
+
122
+We are working on integrating this into Ansible itself for 2.9. Currently we have an `ansible-galaxy PR <https://github.com/ansible/ansible/pull/57106>`_ incorporating some of the commands into ``ansible-galaxy``. Currently it is not installable outside Ansible, but we hope to land this into development soon so early adopters can test.
123
+
124
+.. Note::
125
+    Any references to ``ansible-galaxy`` below will be of a 'working version' either in this PR or subsequently in development. As such, the command and this documentation section is subject to frequent change.
126
+
127
+We also plan to update `Ansible Molecule <https://github.com/ansible/molecule>`_, for a full developer toolkit with integrated testing.
128
+
129
+In the end, to get started with authoring a new collection it should be as simple as:
130
+
131
+.. code-block:: bash
132
+
133
+    collection_dir#>ansible-galaxy collection init
134
+
135
+
136
+And then populating the directories with the content you want inside the collection. For now you can optionally clone from https://github.com/bcoca/collection to get the directory structure (or just create the directories as you need them).
137
+
138
+.. _building_collections:
139
+
140
+Building collections
141
+====================
142
+
143
+Collections are built by running ``mazer build`` from inside the collection's root directory.
144
+This will create a ``releases/`` directory inside the collection with the build artifacts,
145
+which can be uploaded to Galaxy.::
146
+
147
+    collection/
148
+    ├── ...
149
+    ├── releases/
150
+    │   └── namespace_name-collection_name-1.0.12.tar.gz
151
+    └── ...
152
+
153
+.. note::
154
+        Changing the filename of the tarball in the release directory so that it doesn't match
155
+        the data in ``galaxy.yml`` will cause the import to fail.
156
+
157
+
158
+This tarball itself can be used to install the collection on target systems. It is mainly intended to upload to Galaxy as a distribution method, but you should be able to use directly.
159
+
160
+Publishing collections
161
+======================
162
+
163
+We are in the process of updating Ansible Galaxy to manage collections as it currently manages roles.
164
+
165
+
166
+Upload from the Galaxy website
167
+------------------------------
168
+
169
+Go to the `My Content <https://galaxy.ansible.com/my-content/namespaces>`_ page, and click the **Add Content** button on one of your namespaces. From
170
+the **Add Content** dialogue, click **Upload New Collection**, and select the collection archive file from your local
171
+filesystem.
172
+
173
+When uploading collections it doesn't matter which namespace you select. The collection will be uploaded to the
174
+namespace specified in the collection metadata in the ``galaxy.yml`` file. If you're not an owner of the
175
+namespace, the upload request will fail.
176
+
177
+Once Galaxy uploads and accepts a collection, you will be redirected to the **My Imports** page, which displays output from the
178
+import process, including any errors or warnings about the metadata and content contained in the collection.
179
+
180
+Upload using mazer
181
+------------------
182
+
183
+You can upload collection artifacts with ``mazer``, as shown in the following example:
184
+
185
+.. code-block:: bash
186
+
187
+    mazer publish --api-key=SECRET path/to/namespace_name-collection_name-1.0.12.tar.gz
188
+
189
+The above command triggers an import process, just as if the collection had been uploaded through the Galaxy website. Use the **My Imports**
190
+page to view the output from the import process.
191
+
192
+Your API key can be found on `the preferences page in Galaxy <https://galaxy.ansible.com/me/preferences>`_.
193
+
194
+To learn more about Mazer, see `Mazer <https://galaxy.ansible.com/docs/mazer/>`_.
195
+
196
+
197
+Collection versions
198
+-------------------
199
+
200
+Once you upload a version of a collection, you cannot delete or modify that version. Ensure that everything looks okay before
201
+uploading. The only way to change a collection is to release a new version. The latest version of a collection (by highest version number)
202
+will be the version displayed everywhere in Galaxy; however, users will still be able to download older versions.
203
+
204
+
205
+Installing collections
206
+======================
207
+
208
+The recommended way to install a collection is:
209
+
210
+.. code-block:: bash
211
+
212
+   #> ansible-galaxy collection install mycollection -p /path
213
+
214
+assuming the collection is hosted in Galaxy.
215
+
216
+You can also use a tarball resulting from your build:
217
+
218
+.. code-block:: bash
219
+
220
+   #> ansible-galaxy install mynamespace.mycollection.0.1.0.tgz -p /path
221
+
222
+
223
+As a path you should use one of the values configured in `COLLECTIONS_PATHS <https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths>`_. This is also where Ansible itself will expect to find collections when attempting to use them.
224
+
225
+You can also keep a collection adjacent to the current playbook, under a ``collections/ansible_collection/`` directory structure.
226
+
227
+::
228
+
229
+    play.yml
230
+    ├── collections/
231
+    │   └── ansbile_collection/
232
+    │               └── myname/
233
+    │                   └── mycol/<collection structure lives here>
234
+
235
+
236
+
237
+
238
+Using collections
239
+=================
240
+
241
+Once installed, you can reference collection content by its FQCN:
242
+
243
+.. code-block:: yaml
244
+
245
+     - hosts: all
246
+       tasks:
247
+         - myname.mycol.mymodule:
248
+             option1: value
249
+
250
+This works for roles or any type of plugin distributed within the collection:
251
+
252
+.. code-block:: yaml
253
+
254
+     - hosts: all
255
+       tasks:
256
+         - include_role:
257
+             name : myname.mycol.role1
258
+         - myname.mycol.mymodule:
259
+             option1: value
260
+
261
+         - debug:
262
+             msg: '{{ lookup("myname.mycol.lookup1", 'param1')| myname.mycol.filter1 }}'
263
+
264
+
265
+To avoid a lot of typing, you can use the ``collections`` keyword added in Ansbile 2.8:
266
+
267
+
268
+.. code-block:: yaml
269
+
270
+     - hosts: all
271
+       collections:
272
+        - myname.mycol
273
+       tasks:
274
+         - include_role:
275
+             name: role1
276
+         - mymodule:
277
+             option1: value
278
+
279
+         - debug:
280
+             msg: '{{ lookup("myname.mycol.lookup1", 'param1')| myname.mycol.filter1 }}'
281
+
282
+This keyword creates a 'search path' for non namespaced plugin references. It does not import roles or anything else.
283
+Notice that you still need the FQCN for non-action or module plugins.
... ...
@@ -81,4 +81,6 @@ If you prefer to read the entire guide, here's a list of the pages in order.
81 81
    developing_api
82 82
    developing_rebasing
83 83
    developing_module_utilities
84
+   collections_tech_preview
85
+   collections_galaxy_meta
84 86
    overview_architecture
85 87
new file mode 100644
... ...
@@ -0,0 +1,74 @@
0
+.. _collections_galaxy_meta:
1
+
2
+************************************
3
+Collection Galaxy Metadata Structure
4
+************************************
5
+
6
+.. important::
7
+    This feature is available in Ansible 2.8 as a *Technology Preview* and therefore is not fully supported. It should only be used for testing  and should not be deployed in a production environment.
8
+    Future Galaxy or Ansible releases may introduce breaking changes.
9
+
10
+A key component of an Ansible collection is the ``galaxy.yml`` file placed in the root directory of a collection. This
11
+file contains the metadata of the collection that is used to generate a collection artifact.
12
+
13
+Structure
14
+=========
15
+
16
+The ``galaxy.yml`` file must contain the following keys in valid YAML:
17
+
18
+.. raw:: html
19
+
20
+    <table  border=0 cellpadding=0 class="documentation-table">
21
+        {# Header of the documentation -#}
22
+        <tr>
23
+            <th>Key</th>
24
+            <th width="100%">Comments</th>
25
+        </tr>
26
+        {% for entry in options %}
27
+            <tr>
28
+                {# key name with required or type label #}
29
+                <td>
30
+                    <b>@{ entry.key }@</b>
31
+                    <div style="font-size: small">
32
+                        <span style="color: purple">@{ entry.type | documented_type }@</span>
33
+                        {% if entry.get('required', False) %} / <span style="color: red">required</span>{% endif %}
34
+                    </div>
35
+                </td>
36
+                {# Comments #}
37
+                <td>
38
+                    {% if entry.description is string %}
39
+                        <div>@{ entry.description | replace('\n', '\n    ') | html_ify }@</div>
40
+                    {% else %}
41
+                        {% for desc in entry.description %}
42
+                            <div>@{ desc | replace('\n', '\n    ') | html_ify }@</div>
43
+                        {% endfor %}
44
+                    {% endif %}
45
+                </td>
46
+            </tr>
47
+        {% endfor %}
48
+    </table>
49
+    <br/>
50
+
51
+Examples
52
+========
53
+
54
+.. code-block:: yaml
55
+
56
+    namespace: "namespace_name"
57
+    name: "collection_name"
58
+    version: "1.0.12"
59
+    readme: "README.md"
60
+    authors:
61
+        - "Author1"
62
+        - "Author2 (https://author2.example.com)"
63
+        - "Author3 <author3@example.com>"
64
+    dependencies:
65
+        "other_namespace.collection1": ">=1.0.0"
66
+        "other_namespace.collection2": ">=2.0.0,<3.0.0"
67
+        "anderson55.my_collection": "*"    # note: "*" selects the highest version available
68
+    license:
69
+        - "MIT"
70
+    tags:
71
+        - demo
72
+        - collection
73
+    repository: "https://www.github.com/my_org/my_collection"
0 74
new file mode 100644
... ...
@@ -0,0 +1,68 @@
0
+# coding: utf-8
1
+# Copyright: (c) 2019, Ansible Project
2
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
+
4
+# Make coding more python3-ish
5
+from __future__ import (absolute_import, division, print_function)
6
+__metaclass__ = type
7
+
8
+import os
9
+import os.path
10
+import pathlib
11
+
12
+import yaml
13
+from jinja2 import Environment, FileSystemLoader
14
+from ansible.module_utils._text import to_bytes
15
+
16
+# Pylint doesn't understand Python3 namespace modules.
17
+from ..change_detection import update_file_if_different  # pylint: disable=relative-beyond-top-level
18
+from ..commands import Command  # pylint: disable=relative-beyond-top-level
19
+from ..jinja2.filters import documented_type, html_ify  # pylint: disable=relative-beyond-top-level
20
+
21
+
22
+DEFAULT_TEMPLATE_FILE = 'collections_galaxy_meta.rst.j2'
23
+DEFAULT_TEMPLATE_DIR = pathlib.Path(__file__).parents[4] / 'docs/templates'
24
+
25
+
26
+class DocumentCollectionMeta(Command):
27
+    name = 'collection-meta'
28
+
29
+    @classmethod
30
+    def init_parser(cls, add_parser):
31
+        parser = add_parser(cls.name, description='Generate collection galaxy.yml documentation from shared metadata')
32
+        parser.add_argument("-t", "--template-file", action="store", dest="template_file",
33
+                            default=DEFAULT_TEMPLATE_FILE,
34
+                            help="Jinja2 template to use for the config")
35
+        parser.add_argument("-T", "--template-dir", action="store", dest="template_dir",
36
+                            default=DEFAULT_TEMPLATE_DIR,
37
+                            help="directory containing Jinja2 templates")
38
+        parser.add_argument("-o", "--output-dir", action="store", dest="output_dir", default='/tmp/',
39
+                            help="Output directory for rst files")
40
+        parser.add_argument("collection_defs", metavar="COLLECTION-OPTION-DEFINITIONS.yml", type=str,
41
+                            help="Source for collection metadata option docs")
42
+
43
+    @staticmethod
44
+    def main(args):
45
+        output_dir = os.path.abspath(args.output_dir)
46
+        template_file_full_path = os.path.abspath(os.path.join(args.template_dir, args.template_file))
47
+        template_file = os.path.basename(template_file_full_path)
48
+        template_dir = os.path.dirname(template_file_full_path)
49
+
50
+        with open(args.collection_defs) as f:
51
+            options = yaml.safe_load(f)
52
+
53
+        env = Environment(loader=FileSystemLoader(template_dir),
54
+                          variable_start_string="@{",
55
+                          variable_end_string="}@",
56
+                          trim_blocks=True)
57
+        env.filters['documented_type'] = documented_type
58
+        env.filters['html_ify'] = html_ify
59
+
60
+        template = env.get_template(template_file)
61
+        output_name = os.path.join(output_dir, template_file.replace('.j2', ''))
62
+        temp_vars = {'options': options}
63
+
64
+        data = to_bytes(template.render(temp_vars))
65
+        update_file_if_different(output_name, data)
66
+
67
+        return 0
... ...
@@ -11,7 +11,6 @@ __metaclass__ = type
11 11
 import datetime
12 12
 import glob
13 13
 import json
14
-import optparse
15 14
 import os
16 15
 import re
17 16
 import sys
... ...
@@ -34,10 +33,9 @@ except ImportError:
34 34
 import jinja2
35 35
 import yaml
36 36
 from jinja2 import Environment, FileSystemLoader
37
-from jinja2.runtime import Undefined
38 37
 
39 38
 from ansible.errors import AnsibleError
40
-from ansible.module_utils._text import to_bytes, to_text
39
+from ansible.module_utils._text import to_bytes
41 40
 from ansible.module_utils.common.collections import is_sequence
42 41
 from ansible.module_utils.parsing.convert_bool import boolean
43 42
 from ansible.module_utils.six import iteritems, string_types
... ...
@@ -48,6 +46,7 @@ from ansible.utils.display import Display
48 48
 # Pylint doesn't understand Python3 namespace modules.
49 49
 from ..change_detection import update_file_if_different  # pylint: disable=relative-beyond-top-level
50 50
 from ..commands import Command  # pylint: disable=relative-beyond-top-level
51
+from ..jinja2.filters import do_max, documented_type, html_ify, rst_fmt, rst_ify, rst_xline  # pylint: disable=relative-beyond-top-level
51 52
 
52 53
 
53 54
 #####################################################################################
... ...
@@ -67,14 +66,6 @@ EXAMPLE_YAML = os.path.abspath(os.path.join(
67 67
     os.path.dirname(os.path.realpath(__file__)), os.pardir, 'examples', 'DOCUMENTATION.yml'
68 68
 ))
69 69
 
70
-_ITALIC = re.compile(r"I\(([^)]+)\)")
71
-_BOLD = re.compile(r"B\(([^)]+)\)")
72
-_MODULE = re.compile(r"M\(([^)]+)\)")
73
-_URL = re.compile(r"U\(([^)]+)\)")
74
-_LINK = re.compile(r"L\(([^)]+),([^)]+)\)")
75
-_CONST = re.compile(r"C\(([^)]+)\)")
76
-_RULER = re.compile(r"HORIZONTALLINE")
77
-
78 70
 DEPRECATED = b" (D)"
79 71
 
80 72
 pp = PrettyPrinter()
... ...
@@ -98,74 +89,6 @@ def from_kludge_ns(key):
98 98
     return NS_MAP[key]
99 99
 
100 100
 
101
-# The max filter was added in Jinja2-2.10.  Until we can require that version, use this
102
-def do_max(seq):
103
-    return max(seq)
104
-
105
-
106
-def rst_ify(text):
107
-    ''' convert symbols like I(this is in italics) to valid restructured text '''
108
-
109
-    try:
110
-        t = _ITALIC.sub(r"*\1*", text)
111
-        t = _BOLD.sub(r"**\1**", t)
112
-        t = _MODULE.sub(r":ref:`\1 <\1_module>`", t)
113
-        t = _LINK.sub(r"`\1 <\2>`_", t)
114
-        t = _URL.sub(r"\1", t)
115
-        t = _CONST.sub(r"``\1``", t)
116
-        t = _RULER.sub(r"------------", t)
117
-    except Exception as e:
118
-        raise AnsibleError("Could not process (%s) : %s" % (text, e))
119
-
120
-    return t
121
-
122
-
123
-def html_ify(text):
124
-    ''' convert symbols like I(this is in italics) to valid HTML '''
125
-
126
-    if not isinstance(text, string_types):
127
-        text = to_text(text)
128
-
129
-    t = html_escape(text)
130
-    t = _ITALIC.sub(r"<em>\1</em>", t)
131
-    t = _BOLD.sub(r"<b>\1</b>", t)
132
-    t = _MODULE.sub(r"<span class='module'>\1</span>", t)
133
-    t = _URL.sub(r"<a href='\1'>\1</a>", t)
134
-    t = _LINK.sub(r"<a href='\2'>\1</a>", t)
135
-    t = _CONST.sub(r"<code>\1</code>", t)
136
-    t = _RULER.sub(r"<hr/>", t)
137
-
138
-    return t.strip()
139
-
140
-
141
-def rst_fmt(text, fmt):
142
-    ''' helper for Jinja2 to do format strings '''
143
-
144
-    return fmt % (text)
145
-
146
-
147
-def rst_xline(width, char="="):
148
-    ''' return a restructured text line of a given length '''
149
-
150
-    return char * width
151
-
152
-
153
-def documented_type(text):
154
-    ''' Convert any python type to a type for documentation '''
155
-
156
-    if isinstance(text, Undefined):
157
-        return '-'
158
-    if text == 'str':
159
-        return 'string'
160
-    if text == 'bool':
161
-        return 'boolean'
162
-    if text == 'int':
163
-        return 'integer'
164
-    if text == 'dict':
165
-        return 'dictionary'
166
-    return text
167
-
168
-
169 101
 test_list = partial(is_sequence, include_strings=False)
170 102
 
171 103
 
172 104
new file mode 100644
173 105
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+# Copyright: (c) 2019, Ansible Project
1
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
2
+
3
+# Make coding more python3-ish
4
+from __future__ import (absolute_import, division, print_function)
5
+__metaclass__ = type
6
+
7
+import re
8
+
9
+try:
10
+    from html import escape as html_escape
11
+except ImportError:
12
+    # Python-3.2 or later
13
+    import cgi
14
+
15
+    def html_escape(text, quote=True):
16
+        return cgi.escape(text, quote)
17
+
18
+from jinja2.runtime import Undefined
19
+
20
+from ansible.errors import AnsibleError
21
+from ansible.module_utils._text import to_text
22
+from ansible.module_utils.six import string_types
23
+
24
+
25
+_ITALIC = re.compile(r"I\(([^)]+)\)")
26
+_BOLD = re.compile(r"B\(([^)]+)\)")
27
+_MODULE = re.compile(r"M\(([^)]+)\)")
28
+_URL = re.compile(r"U\(([^)]+)\)")
29
+_LINK = re.compile(r"L\(([^)]+),([^)]+)\)")
30
+_CONST = re.compile(r"C\(([^)]+)\)")
31
+_RULER = re.compile(r"HORIZONTALLINE")
32
+
33
+
34
+def html_ify(text):
35
+    ''' convert symbols like I(this is in italics) to valid HTML '''
36
+
37
+    if not isinstance(text, string_types):
38
+        text = to_text(text)
39
+
40
+    t = html_escape(text)
41
+    t = _ITALIC.sub(r"<em>\1</em>", t)
42
+    t = _BOLD.sub(r"<b>\1</b>", t)
43
+    t = _MODULE.sub(r"<span class='module'>\1</span>", t)
44
+    t = _URL.sub(r"<a href='\1'>\1</a>", t)
45
+    t = _LINK.sub(r"<a href='\2'>\1</a>", t)
46
+    t = _CONST.sub(r"<code>\1</code>", t)
47
+    t = _RULER.sub(r"<hr/>", t)
48
+
49
+    return t.strip()
50
+
51
+
52
+def documented_type(text):
53
+    ''' Convert any python type to a type for documentation '''
54
+
55
+    if isinstance(text, Undefined):
56
+        return '-'
57
+    if text == 'str':
58
+        return 'string'
59
+    if text == 'bool':
60
+        return 'boolean'
61
+    if text == 'int':
62
+        return 'integer'
63
+    if text == 'dict':
64
+        return 'dictionary'
65
+    return text
66
+
67
+
68
+# The max filter was added in Jinja2-2.10.  Until we can require that version, use this
69
+def do_max(seq):
70
+    return max(seq)
71
+
72
+
73
+def rst_ify(text):
74
+    ''' convert symbols like I(this is in italics) to valid restructured text '''
75
+
76
+    try:
77
+        t = _ITALIC.sub(r"*\1*", text)
78
+        t = _BOLD.sub(r"**\1**", t)
79
+        t = _MODULE.sub(r":ref:`\1 <\1_module>`", t)
80
+        t = _LINK.sub(r"`\1 <\2>`_", t)
81
+        t = _URL.sub(r"\1", t)
82
+        t = _CONST.sub(r"``\1``", t)
83
+        t = _RULER.sub(r"------------", t)
84
+    except Exception as e:
85
+        raise AnsibleError("Could not process (%s) : %s" % (text, e))
86
+
87
+    return t
88
+
89
+
90
+def rst_fmt(text, fmt):
91
+    ''' helper for Jinja2 to do format strings '''
92
+
93
+    return fmt % (text)
94
+
95
+
96
+def rst_xline(width, char="="):
97
+    ''' return a restructured text line of a given length '''
98
+
99
+    return char * width
... ...
@@ -8,17 +8,18 @@ __metaclass__ = type
8 8
 import os.path
9 9
 import re
10 10
 import shutil
11
+import textwrap
11 12
 import time
12 13
 import yaml
13 14
 
14
-from jinja2 import Environment, FileSystemLoader
15
+from jinja2 import BaseLoader, Environment, FileSystemLoader
15 16
 
16 17
 import ansible.constants as C
17 18
 from ansible import context
18 19
 from ansible.cli import CLI
19 20
 from ansible.cli.arguments import option_helpers as opt_help
20 21
 from ansible.errors import AnsibleError, AnsibleOptionsError
21
-from ansible.galaxy import Galaxy
22
+from ansible.galaxy import Galaxy, get_collections_galaxy_meta_info
22 23
 from ansible.galaxy.api import GalaxyAPI
23 24
 from ansible.galaxy.collection import build_collection, install_collections, parse_collections_requirements_file, \
24 25
     publish_collection
... ...
@@ -309,6 +310,56 @@ class GalaxyCLI(CLI):
309 309
 
310 310
         raise AnsibleError("Invalid collection name, must be in the format <namespace>.<collection>")
311 311
 
312
+    @staticmethod
313
+    def _get_skeleton_galaxy_yml(template_path, inject_data):
314
+        with open(to_bytes(template_path, errors='surrogate_or_strict'), 'rb') as template_obj:
315
+            meta_template = to_text(template_obj.read(), errors='surrogate_or_strict')
316
+
317
+        galaxy_meta = get_collections_galaxy_meta_info()
318
+
319
+        required_config = []
320
+        optional_config = []
321
+        for meta_entry in galaxy_meta:
322
+            config_list = required_config if meta_entry.get('required', False) else optional_config
323
+
324
+            value = inject_data.get(meta_entry['key'], None)
325
+            if not value:
326
+                meta_type = meta_entry.get('type', 'str')
327
+
328
+                if meta_type == 'str':
329
+                    value = ''
330
+                elif meta_type == 'list':
331
+                    value = []
332
+                elif meta_type == 'dict':
333
+                    value = {}
334
+
335
+            meta_entry['value'] = value
336
+            config_list.append(meta_entry)
337
+
338
+        link_pattern = re.compile(r"L\(([^)]+),\s+([^)]+)\)")
339
+        const_pattern = re.compile(r"C\(([^)]+)\)")
340
+
341
+        def comment_ify(v):
342
+            if isinstance(v, list):
343
+                v = ". ".join([l.rstrip('.') for l in v])
344
+
345
+            v = link_pattern.sub(r"\1 <\2>", v)
346
+            v = const_pattern.sub(r"'\1'", v)
347
+
348
+            return textwrap.fill(v, width=117, initial_indent="# ", subsequent_indent="# ", break_on_hyphens=False)
349
+
350
+        def to_yaml(v):
351
+            return yaml.safe_dump(v, default_flow_style=False).rstrip()
352
+
353
+        env = Environment(loader=BaseLoader)
354
+        env.filters['comment_ify'] = comment_ify
355
+        env.filters['to_yaml'] = to_yaml
356
+
357
+        template = env.from_string(meta_template)
358
+        meta_value = template.render({'required_config': required_config, 'optional_config': optional_config})
359
+
360
+        return meta_value
361
+
312 362
 ############################
313 363
 # execute actions
314 364
 ############################
... ...
@@ -359,30 +410,42 @@ class GalaxyCLI(CLI):
359 359
         obj_name = context.CLIARGS['{0}_name'.format(galaxy_type)]
360 360
 
361 361
         inject_data = dict(
362
-            author='your name',
363 362
             description='your description',
364
-            company='your company (optional)',
365
-            license='license (GPL-2.0-or-later, MIT, etc)',
366
-            issue_tracker_url='http://example.com/issue/tracker',
367
-            repository_url='http://example.com/repository',
368
-            documentation_url='http://docs.example.com',
369
-            homepage_url='http://example.com',
370
-            min_ansible_version=ansible_version[:3],  # x.y
371 363
             ansible_plugin_list_dir=get_versioned_doclink('plugins/plugins.html'),
372 364
         )
373
-
374 365
         if galaxy_type == 'role':
375
-            inject_data['role_name'] = obj_name
376
-            inject_data['role_type'] = context.CLIARGS['role_type']
377
-            inject_data['license'] = 'license (GPL-2.0-or-later, MIT, etc)'
366
+            inject_data.update(dict(
367
+                author='your name',
368
+                company='your company (optional)',
369
+                license='license (GPL-2.0-or-later, MIT, etc)',
370
+                role_name=obj_name,
371
+                role_type=context.CLIARGS['role_type'],
372
+                issue_tracker_url='http://example.com/issue/tracker',
373
+                repository_url='http://example.com/repository',
374
+                documentation_url='http://docs.example.com',
375
+                homepage_url='http://example.com',
376
+                min_ansible_version=ansible_version[:3],  # x.y
377
+            ))
378
+
378 379
             obj_path = os.path.join(init_path, obj_name)
379 380
         elif galaxy_type == 'collection':
380 381
             namespace, collection_name = obj_name.split('.', 1)
381 382
 
382
-            inject_data['namespace'] = namespace
383
-            inject_data['collection_name'] = collection_name
384
-            inject_data['license'] = 'GPL-2.0-or-later'
383
+            inject_data.update(dict(
384
+                namespace=namespace,
385
+                collection_name=collection_name,
386
+                version='1.0.0',
387
+                readme='README.md',
388
+                authors=['your name <example@domain.com>'],
389
+                license=['GPL-2.0-or-later'],
390
+                repository='http://example.com/repository',
391
+                documentation='http://docs.example.com',
392
+                homepage='http://example.com',
393
+                issues='http://example.com/issue/tracker',
394
+            ))
395
+
385 396
             obj_path = os.path.join(init_path, namespace, collection_name)
397
+
386 398
         b_obj_path = to_bytes(obj_path, errors='surrogate_or_strict')
387 399
 
388 400
         if os.path.exists(b_obj_path):
... ...
@@ -395,8 +458,10 @@ class GalaxyCLI(CLI):
395 395
                                    "been modified there already." % to_native(obj_path))
396 396
 
397 397
         if obj_skeleton is not None:
398
+            own_skeleton = False
398 399
             skeleton_ignore_expressions = C.GALAXY_ROLE_SKELETON_IGNORE
399 400
         else:
401
+            own_skeleton = True
400 402
             obj_skeleton = self.galaxy.default_role_skeleton_path
401 403
             skeleton_ignore_expressions = ['^.*/.git_keep$']
402 404
 
... ...
@@ -428,8 +493,22 @@ class GalaxyCLI(CLI):
428 428
 
429 429
             for f in files:
430 430
                 filename, ext = os.path.splitext(f)
431
+
431 432
                 if any(r.match(os.path.join(rel_root, f)) for r in skeleton_ignore_re):
432 433
                     continue
434
+                elif galaxy_type == 'collection' and own_skeleton and rel_root == '.' and f == 'galaxy.yml.j2':
435
+                    # Special use case for galaxy.yml.j2 in our own default collection skeleton. We build the options
436
+                    # dynamically which requires special options to be set.
437
+
438
+                    # The templated data's keys must match the key name but the inject data contains collection_name
439
+                    # instead of name. We just make a copy and change the key back to name for this file.
440
+                    template_data = inject_data.copy()
441
+                    template_data['name'] = template_data.pop('collection_name')
442
+
443
+                    meta_value = GalaxyCLI._get_skeleton_galaxy_yml(os.path.join(root, rel_root, f), template_data)
444
+                    b_dest_file = to_bytes(os.path.join(obj_path, rel_root, filename), errors='surrogate_or_strict')
445
+                    with open(b_dest_file, 'wb') as galaxy_obj:
446
+                        galaxy_obj.write(to_bytes(meta_value, errors='surrogate_or_strict'))
433 447
                 elif ext == ".j2" and not in_templates_dir:
434 448
                     src_template = os.path.join(rel_root, f)
435 449
                     dest_file = os.path.join(obj_path, rel_root, filename)
... ...
@@ -24,15 +24,21 @@ from __future__ import (absolute_import, division, print_function)
24 24
 __metaclass__ = type
25 25
 
26 26
 import os
27
+import yaml
27 28
 
28 29
 from ansible import context
29
-from ansible.errors import AnsibleError
30
-from ansible.module_utils.six import string_types
30
+from ansible.module_utils._text import to_bytes
31 31
 
32 32
 #      default_readme_template
33 33
 #      default_meta_template
34 34
 
35 35
 
36
+def get_collections_galaxy_meta_info():
37
+    meta_path = os.path.join(os.path.dirname(__file__), 'data', 'collections_galaxy_meta.yml')
38
+    with open(to_bytes(meta_path, errors='surrogate_or_strict'), 'rb') as galaxy_obj:
39
+        return yaml.safe_load(galaxy_obj)
40
+
41
+
36 42
 class Galaxy(object):
37 43
     ''' Keeps global galaxy info '''
38 44
 
... ...
@@ -23,6 +23,7 @@ from yaml.error import YAMLError
23 23
 
24 24
 import ansible.constants as C
25 25
 from ansible.errors import AnsibleError
26
+from ansible.galaxy import get_collections_galaxy_meta_info
26 27
 from ansible.module_utils._text import to_bytes, to_native, to_text
27 28
 from ansible.module_utils import six
28 29
 from ansible.utils.display import Display
... ...
@@ -524,11 +525,25 @@ def _tarfile_extract(tar, member):
524 524
 
525 525
 
526 526
 def _get_galaxy_yml(b_galaxy_yml_path):
527
-    mandatory_keys = frozenset(['namespace', 'name', 'version', 'authors', 'readme'])
528
-    optional_strings = ('description', 'repository', 'documentation', 'homepage', 'issues', 'license_file')
529
-    optional_lists = ('license', 'tags', 'authors')  # authors isn't optional but this will ensure it is list
530
-    optional_dicts = ('dependencies',)
531
-    all_keys = frozenset(list(mandatory_keys) + list(optional_strings) + list(optional_lists) + list(optional_dicts))
527
+    meta_info = get_collections_galaxy_meta_info()
528
+
529
+    mandatory_keys = set()
530
+    string_keys = set()
531
+    list_keys = set()
532
+    dict_keys = set()
533
+
534
+    for info in meta_info:
535
+        if info.get('required', False):
536
+            mandatory_keys.add(info['key'])
537
+
538
+        key_list_type = {
539
+            'str': string_keys,
540
+            'list': list_keys,
541
+            'dict': dict_keys,
542
+        }[info.get('type', 'str')]
543
+        key_list_type.add(info['key'])
544
+
545
+    all_keys = frozenset(list(mandatory_keys) + list(string_keys) + list(list_keys) + list(dict_keys))
532 546
 
533 547
     try:
534 548
         with open(b_galaxy_yml_path, 'rb') as g_yaml:
... ...
@@ -549,11 +564,11 @@ def _get_galaxy_yml(b_galaxy_yml_path):
549 549
                         % (to_text(b_galaxy_yml_path), ", ".join(extra_keys)))
550 550
 
551 551
     # Add the defaults if they have not been set
552
-    for optional_string in optional_strings:
552
+    for optional_string in string_keys:
553 553
         if optional_string not in galaxy_yml:
554 554
             galaxy_yml[optional_string] = None
555 555
 
556
-    for optional_list in optional_lists:
556
+    for optional_list in list_keys:
557 557
         list_val = galaxy_yml.get(optional_list, None)
558 558
 
559 559
         if list_val is None:
... ...
@@ -561,7 +576,7 @@ def _get_galaxy_yml(b_galaxy_yml_path):
561 561
         elif not isinstance(list_val, list):
562 562
             galaxy_yml[optional_list] = [list_val]
563 563
 
564
-    for optional_dict in optional_dicts:
564
+    for optional_dict in dict_keys:
565 565
         if optional_dict not in galaxy_yml:
566 566
             galaxy_yml[optional_dict] = {}
567 567
 
... ...
@@ -655,7 +670,7 @@ def _build_manifest(namespace, name, version, authors, readme, tags, description
655 655
             'tags': tags,
656 656
             'description': description,
657 657
             'license': license_ids,
658
-            'license_file': license_file,
658
+            'license_file': license_file if license_file else None,  # Handle galaxy.yml having an empty string (None)
659 659
             'dependencies': dependencies,
660 660
             'repository': repository,
661 661
             'documentation': documentation,
662 662
new file mode 100644
... ...
@@ -0,0 +1,98 @@
0
+# Copyright (c) 2019 Ansible Project
1
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
2
+
3
+# key: The name of the key as defined in galaxy.yml
4
+# description: Comment/info on the key to be used as the generated doc and auto generated skeleton galaxy.yml file
5
+# required: Whether the key is required (default is no)
6
+# type: The type of value that can be set, aligns to the values in the plugin formatter
7
+---
8
+- key: namespace
9
+  description:
10
+  - The namespace of the collection.
11
+  - This can be a company/brand/organization or product namespace under which all content lives.
12
+  - May only contain alphanumeric characters and underscores. Additionally namespaces cannot start with underscores or
13
+    numbers and cannot contain consecutive underscores.
14
+  required: yes
15
+  type: str
16
+
17
+- key: name
18
+  description:
19
+  - The name of the collection.
20
+  - Has the same character restrictions as C(namespace).
21
+  required: yes
22
+  type: str
23
+
24
+- key: version
25
+  description:
26
+  - The version of the collection.
27
+  - Must be compatible with semantic versioning.
28
+  required: yes
29
+  type: str
30
+
31
+- key: readme
32
+  description:
33
+  - The path to the Markdown (.md) readme file.
34
+  - This path is relative to the root of the collection.
35
+  required: yes
36
+  type: str
37
+
38
+- key: authors
39
+  description:
40
+  - A list of the collection's content authors.
41
+  - Can be just the name or in the format 'Full Name <email> (url) @nicks:irc/im.site#channel'.
42
+  required: yes
43
+  type: list
44
+
45
+- key: description
46
+  description:
47
+  - A short summary description of the collection.
48
+  type: str
49
+
50
+- key: license
51
+  description:
52
+  - Either a single license or a list of licenses for content inside of a collection.
53
+  - Ansible Galaxy currently only accepts L(SPDX,https://spdx.org/licenses/) licenses
54
+  - This key is mutually exclusive with C(license_file).
55
+  type: list
56
+
57
+- key: license_file
58
+  description:
59
+  - The path to the license file for the collection.
60
+  - This path is relative to the root of the collection.
61
+  - This key is mutually exclusive with C(license).
62
+  type: str
63
+
64
+- key: tags
65
+  description:
66
+  - A list of tags you want to associate with the collection for indexing/searching.
67
+  - A tag name has the same character requirements as C(namespace) and C(name).
68
+  type: list
69
+
70
+- key: dependencies
71
+  description:
72
+  - Collections that this collection requires to be installed for it to be usable.
73
+  - The key of the dict is the collection label C(namespace.name).
74
+  - The value is a version range
75
+    L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification).
76
+  - Multiple version range specifiers can be set and are separated by C(,).
77
+  type: dict
78
+
79
+- key: repository
80
+  description:
81
+  - The URL of the originating SCM repository.
82
+  type: str
83
+
84
+- key: documentation
85
+  description:
86
+  - The URL to any online docs.
87
+  type: str
88
+
89
+- key: homepage
90
+  description:
91
+  - The URL to the homepage of the collection/project.
92
+  type: str
93
+
94
+- key: issues
95
+  description:
96
+  - The URL to the collection issue tracker.
97
+  type: str
... ...
@@ -1,3 +1,3 @@
1 1
 # Ansible Collection - {{ namespace }}.{{ collection_name }}
2 2
 
3
-Documentation for the collection.
4 3
\ No newline at end of file
4
+Documentation for the collection.
... ...
@@ -1,65 +1,11 @@
1 1
 ### REQUIRED
2
-
3
-# this can be a company/brand/organization or product namespace
4
-# under which all content lives
5
-namespace: {{ namespace }}
6
-
7
-
8
-# the designation of this specific collection
9
-name: {{ collection_name }}
10
-
11
-
12
-# semantic versioning compliant version designation
13
-version: 1.0.0
14
-
15
-# the filename for the readme file which can be either markdown (.md)
16
-readme: README.md
17
-
18
-
19
-# a list of the collection's content authors
20
-# Ex: 'Full Name <email> (http://site) @nicks:irc/im/site#channel'
21
-authors:
22
-- {{ author }} <example@domain.com>
23
-
24
-
25
-### OPTIONAL but strongly advised
26
-
27
-# short summary of the collection
28
-description: {{ description }}
29
-
30
-
31
-# Either a single valid SPDX license identifier or a list of valid SPDX license
32
-# identifiers, see https://spdx.org/licenses/. Could also set `license_file`
33
-# instead to point to the file the specifies the license in the collection
34
-# directory.
35
-license: {{ license }}
36
-
37
-
38
-# list of keywords you want to associate the collection
39
-# with for indexing/search systems
40
-tags: []
41
-
42
-
43
-# A dict of dependencies. A dependency is another collection
44
-# this collection requires to be installed for it to be usable.
45
-# The key of the dict is the collection label (namespace.name)
46
-# and the value is a spec for the semver version required.
47
-dependencies: {}
48
-
49
-
50
-### URLs
51
-
52
-# url of originating SCM repository
53
-repository: {{ repository_url }}
54
-
55
-
56
-# url to online docs
57
-documentation: {{ documentation_url }}
58
-
59
-
60
-# homepage of the collection/project
61
-homepage: {{ homepage_url }}
62
-
63
-
64
-# issue tracker url
65
-issues: {{ issue_tracker_url }}
66 2
\ No newline at end of file
3
+{% for option in required_config %}
4
+{{ option.description | comment_ify }}
5
+{{ {option.key: option.value} | to_yaml }}
6
+{% endfor %}
7
+
8
+### OPTIONAL but strongly recommended
9
+{% for option in optional_config %}
10
+{{ option.description | comment_ify }}
11
+{{ {option.key: option.value} | to_yaml }}
12
+{% endfor %}
... ...
@@ -22,6 +22,7 @@ def main():
22 22
         # allowed special cases
23 23
         'lib/ansible/config/base.yml',
24 24
         'lib/ansible/config/module_defaults.yml',
25
+        'lib/ansible/galaxy/data/collections_galaxy_meta.yml',
25 26
     )
26 27
 
27 28
     skip_directories = (
... ...
@@ -496,14 +496,13 @@ def test_collection_default(collection_skeleton):
496 496
     assert metadata['readme'] == 'README.md'
497 497
     assert metadata['version'] == '1.0.0'
498 498
     assert metadata['description'] == 'your description'
499
-    assert metadata['license'] == 'GPL-2.0-or-later'
499
+    assert metadata['license'] == ['GPL-2.0-or-later']
500 500
     assert metadata['tags'] == []
501 501
     assert metadata['dependencies'] == {}
502 502
     assert metadata['documentation'] == 'http://docs.example.com'
503 503
     assert metadata['repository'] == 'http://example.com/repository'
504 504
     assert metadata['homepage'] == 'http://example.com'
505 505
     assert metadata['issues'] == 'http://example.com/issue/tracker'
506
-    assert len(metadata) == 13
507 506
 
508 507
     for d in ['docs', 'plugins', 'roles']:
509 508
         assert os.path.isdir(os.path.join(collection_skeleton, d)), \
... ...
@@ -215,11 +215,6 @@ readme: README.md"""], indirect=True)
215 215
 def test_defaults_galaxy_yml(galaxy_yml):
216 216
     actual = collection._get_galaxy_yml(galaxy_yml)
217 217
 
218
-    assert sorted(list(actual.keys())) == [
219
-        'authors', 'dependencies', 'description', 'documentation', 'homepage', 'issues', 'license_file', 'license_ids',
220
-        'name', 'namespace', 'readme', 'repository', 'tags', 'version',
221
-    ]
222
-
223 218
     assert actual['namespace'] == 'namespace'
224 219
     assert actual['name'] == 'collection'
225 220
     assert actual['authors'] == ['Jordan']