]> arthur.barton.de Git - ansible-collection-boilerplate.git/blob - bin/ansible-boilerplate
9d5fc213024fb9da4adeff4bc0ea104d113595fb
[ansible-collection-boilerplate.git] / bin / ansible-boilerplate
1 #!/bin/sh -e
2 #
3 # Ansible Boilerplate Collection: Setup & Maintenance Script.
4 #
5
6 # Detect commands to use:
7 PYTHON="${PYTHON:-$(command -v python3 2>/dev/null || echo "python3")}"
8 PIP="${PIP:-$(command -v pip 2>/dev/null || echo "pip")}"
9 ANSIBLE_GALAXY="${ANSIBLE_GALAXY:-$(command -v ansible-galaxy 2>/dev/null || echo "ansible-galaxy")}"
10
11 BASE_D="ansible_galaxy/ansible_collections/alexbarton/boilerplate"
12
13 #
14 # Show usage information on stderr.
15 #
16 Usage() {
17         {
18                 echo "$0 <command>"
19                 echo
20                 echo "  help       Show this help text and exit."
21                 echo "  init       Initialize project and boilerplate code."
22                 echo "  upgrade    Upgrade boilerplate code and dependencies. [Alias: update, up]"
23                 echo "    --force  Force overwriting an existing role or collection."
24                 echo
25         } >&2
26 }
27
28 #
29 # Initialize a new project.
30 #
31 # Create some default files and call Upgrade() afterwards. This function does not
32 # overwrite any already existing file.
33 #
34 Init() {
35         if [ $# -ne 0 ]; then
36                 Usage
37                 exit 1
38         fi
39         if [ -e Makefile.boilerplate ]; then
40                 echo "This is the upstream project! Don't call \"init\" on it!" >&2
41                 exit 1
42         fi
43         echo "Initialize project:"
44
45         for file in \
46                 README.md \
47                 LICENSE \
48         ; do
49                 test -e "${file}" || touch "${file}"
50         done
51         mkdir -p .vscode playbooks roles
52         test -e "hosts.ini" || Init_HostsIni
53         test -e "Makefile" || Init_Makefile
54         test -e "requirements.yml" || Init_RequirementsYml
55
56         Upgrade --init
57 }
58
59 #
60 # Create a Makefile template file.
61 #
62 Init_Makefile() {
63         echo "Creating \"Makefile\" ..."
64         cat >Makefile <<EOF
65 #
66 # Makefile
67 #
68
69 SOURCE_ROOT ?= \$(CURDIR)
70
71 # Make sure that the Ansible Boilerplate Collection project is set up (its
72 # files are "available"), and initialize it if not!
73 _DUMMY := \$(shell test -e ansible_galaxy/ansible_collections/alexbarton/boilerplate/Makefile.boilerplate || bin/ansible-boilerplate upgrade >&2)
74
75 default: all
76
77 # Include the Ansible Boilerplate Collection Makefile fragment:
78 include ansible_galaxy/ansible_collections/alexbarton/boilerplate/Makefile.boilerplate
79
80 all:
81
82 check:
83
84 install:
85
86 clean:
87
88 distclean: clean
89
90 maintainer-clean: distclean
91
92 .PHONY: default all check install clean distclean maintainer-clean
93 EOF
94 }
95
96 #
97 # Create a hosts.ini template file.
98 #
99 Init_HostsIni() {
100         echo "Creating \"hosts.ini\" ..."
101         cat >hosts.ini <<EOF
102 # Ansible hosts list
103
104 [all_managed]
105 localhost
106 EOF
107 }
108
109 #
110 # Create a requirements.yml template file.
111 #
112 Init_RequirementsYml() {
113         echo "Creating \"requirements.yml\" ..."
114         cat >requirements.yml <<EOF
115 ---
116 # Ansible dependencies
117
118 collections:
119   - ${BOILERPLATE_COLLECTION_SRC:-alexbarton.boilerplate}
120
121 roles:
122   []
123 EOF
124 }
125
126 #
127 # Upgrade a project.
128 #
129 # - Initialize a Python virtual environment (when a ".venv" directory exists
130 #   or the ansible-galaxy command is not found).
131 # - Install Ansible when ansible-galaxy command is not found.
132 # - Install "ansible-boilerplate" collection when not found.
133 # - Update local "ansible-boilerplate" setup: copy script, create links, ...
134 # - Upgrade template files.
135 # - Install/upgrade Python dependencies (from requirements.txt file).
136 # - Install/upgrade Ansible Galaxy dependencies (from requirements.yml file).
137 #
138 # --force: Passed to "ansible-galaxy install" command.
139 # --init: Upgrade() is called by the Init() function.
140 #
141 Upgrade() {
142         unset do_force
143         while [ $# -gt 0 ]; do
144                 case "$1" in
145                         "--force")
146                                 do_force="--force"
147                                 ;;
148                         "--init")
149                                 is_init="--init"
150                                 ;;
151                         *)
152                                 Usage
153                                 exit 1
154                 esac
155                 shift
156         done
157         [ -z "${is_init}" ] && echo "Upgrade project:"
158
159         # Check Python virtual environment
160         if [ -d .venv ] || ! command -v "${ANSIBLE_GALAXY}" >/dev/null; then
161                 # Either an existing ".venv" folder was found or the
162                 # ansible-galaxy(1) command was not found on the system, so
163                 # let's use a Python virtual environment!
164                 echo "Using a Python virtual environment."
165                 PIP="./.venv/bin/pip"
166                 ANSIBLE_GALAXY="./.venv/bin/ansible-galaxy"
167                 if ! [ -x .venv/bin/pip ]; then
168                         echo "Initializing Python virtual environment ..."
169                         "${PYTHON}" -m venv .venv
170                         "${PIP}" install -U pip setuptools
171                 fi
172         fi
173         for var in PYTHON PIP ANSIBLE_GALAXY; do
174                 eval 'echo " - ${var} is \"$'"${var}"'\"."'
175         done
176
177         if [ -r requirements.txt ]; then
178                 echo "Installing Python dependencies ..."
179                 "${PIP}" install -U -r requirements.txt
180         fi
181
182         # Make sure that the "ansible-galaxy" command is available now:
183         if ! [ -x "${ANSIBLE_GALAXY}" ]; then
184                 echo "Oops, \"${ANSIBLE_GALAXY}\" not found!" >&2
185                 echo "You either need Ansible installed locally or list it as a dependency in" >&2
186                 echo "the \"requirements.txt\" file of this project!" >&2
187                 exit 1
188         fi
189
190         if [ -r requirements.yml ]; then
191                 echo "Upgrading Ansible Galaxy dependencies ..."
192                 # shellcheck disable=SC2248
193                 "${ANSIBLE_GALAXY}" collection install -U -r requirements.yml ${do_force}
194                 # shellcheck disable=SC2248
195                 "${ANSIBLE_GALAXY}" role install -r requirements.yml ${do_force}
196         fi
197
198         # Are we running in a dependent project? If so, perform specific upgrade tasks!
199         # shellcheck disable=SC2086
200         [ -e Makefile.boilerplate ] || Upgrade_Dependent ${is_init}
201 }
202
203 #
204 # Upgrade steps for dependent projects only.
205 #
206 # --init: Upgrade() is called by the Init() function.
207 #
208 Upgrade_Dependent() {
209         # Verify that the Boilerplate Collection is available now.
210         # NOTE: This dependency must be properly listed in the requirements.yml
211         # file inside of the (dependent) project!
212         "${ANSIBLE_GALAXY}" collection verify --offline alexbarton.boilerplate
213
214         echo "Copying \"boilerplate\" script into bin/ directory ..."
215         mkdir -p bin
216         cp -av "${BASE_D}/bin/ansible-boilerplate" "bin/ansible-boilerplate"
217
218         echo "Creating symbolic links to files inside of the Boilerplate Collection ..."
219         for file in \
220                 bin/a \
221                 bin/ap \
222                 bin/aps \
223         ; do
224                 # Create (new) symbolic links, when the target already is a symbolic link or
225                 # does not yet exists. Don't overwrite existing regular files etc.!
226                 test -L "${file}" && ln -fsv "../${BASE_D}/${file}" "${file}"
227                 test -e "${file}" || ln -fsv "../${BASE_D}/${file}" "${file}"
228         done
229
230         echo "Upgrading template files from the Boilerplate Collection ..."
231         for file in \
232                 .ansible-lint \
233                 .editorconfig \
234                 .gitignore \
235                 .vscode/settings.json \
236                 .yamllint.yml \
237                 ansible.cfg \
238                 requirements.txt \
239         ; do
240                 # shellcheck disable=SC2086
241                 Upgrade_Template "${file}" ${is_init}
242         done
243 }
244
245 #
246 # Upgrade a template file.
247 #
248 # --init: Initialize a new project, therefore create the template file if it
249 #         does not yet exist.
250 #
251 Upgrade_Template() {
252         # Does the target directory exist? Skip this template file if not!
253         [ -d "$(dirname "$1")" ] || return 0
254
255         # Return when the target file does not exist and not in "init mode":
256         [ ! -e "$1" ] && [ "$2" != "--init" ] && return 0
257
258         # Remove the target when it is a symbolic link.
259         [ -L "$1" ] && rm -v "$1"
260
261         # Do not override the target when it exists already!
262         if [ -e "$1" ]; then
263                 # Target already exists. Is it different?
264                 if ! cmp "$1" "${BASE_D}/$1"; then
265                         # Files are not the same! Install new version in parallel:
266                         install -b -m 0644 -p -v "${BASE_D}/$1" "$1.new"
267                 fi
268         else
269                 # Target does not yet exist:
270                 install -b -m 0644 -p -v "${BASE_D}/$1" "$1"
271         fi
272 }
273
274 cmd="$1"
275 [ $# -gt 0 ] && shift
276
277 case "${cmd}" in
278         "init")
279                 Init "$@"
280                 ;;
281         "upgrade"|"update"|"up")
282                 Upgrade "$@"
283                 ;;
284         "help"|"--help")
285                 Usage
286                 ;;
287         *)
288                 Usage
289                 exit 1
290 esac
291 exit 0