Merge lp:~hloeung/jenkins-slave-charm/reactive-rewrite into lp:jenkins-slave-charm

Proposed by Haw Loeung on 2019-02-26
Status: Merged
Approved by: Tom Haddon on 2019-03-01
Approved revision: 51
Merged at revision: 22
Proposed branch: lp:~hloeung/jenkins-slave-charm/reactive-rewrite
Merge into: lp:jenkins-slave-charm
Diff against target: 1152 lines (+673/-311)
25 files modified
.bzrignore (+11/-0)
Makefile (+30/-0)
README.md (+5/-0)
files/jenkins-slave-sudoers (+5/-0)
hooks/configure-slave (+0/-27)
hooks/install.d/add_sudoers (+0/-18)
hooks/install.d/canonical_ci_utils.py (+0/-91)
hooks/nrpe-external-master-relation-changed (+0/-7)
hooks/slave-relation-changed (+0/-19)
hooks/slave-relation-departed (+0/-3)
hooks/slave-relation-joined (+0/-20)
hooks/start (+0/-3)
hooks/stop (+0/-3)
layer.yaml (+5/-0)
metadata.yaml (+5/-1)
reactive/jenkins_slave.py (+228/-118)
requirements.txt (+1/-0)
revision (+0/-1)
templates/jenkins-slave-default (+8/-0)
tests/functional/requirements.txt (+6/-0)
tests/functional/test_jenkins_slave.py (+50/-0)
tests/unit/files/jenkins-slave-default (+59/-0)
tests/unit/requirements.txt (+5/-0)
tests/unit/test_jenkins_slave.py (+218/-0)
tox.ini (+37/-0)
To merge this branch: bzr merge lp:~hloeung/jenkins-slave-charm/reactive-rewrite
Reviewer Review Type Date Requested Status
Tom Haddon 2019-02-26 Approve on 2019-03-01
Haw Loeung Resubmit on 2019-03-01
Canonical IS Reviewers 2019-02-26 Pending
Review via email: mp+363649@code.launchpad.net

Commit message

Charm rewrite using Python and the Reactive framework

To post a comment you must log in.

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Tom Haddon (mthaddon) wrote :

Some comments and questions inline.

review: Needs Fixing
Tom Haddon (mthaddon) wrote :

Missed one other thing as well.

Tom Haddon (mthaddon) wrote :

Some comments inline with suggestions for testing.

Tom Haddon (mthaddon) wrote :

Er, we we obviously *do* want the max-complexity check

Tom Haddon (mthaddon) wrote :

A few comments inline

Haw Loeung (hloeung) :
review: Resubmit
50. By Haw Loeung on 2019-03-01

Added basic functional test

51. By Haw Loeung on 2019-03-01

Fixed to use the module itself instead of sys.modules

Tom Haddon (mthaddon) wrote :

LGTM, thanks

review: Approve

Change successfully merged at revision 22

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2019-03-01 05:39:53 +0000
4@@ -0,0 +1,11 @@
5+*.pyc
6+*.swp
7+*~
8+.coverage
9+.pytest_cache/
10+.tox/
11+.unit-state.db
12+__pycache__/
13+builds/
14+deps/
15+revision
16
17=== added file 'Makefile'
18--- Makefile 1970-01-01 00:00:00 +0000
19+++ Makefile 2019-03-01 05:39:53 +0000
20@@ -0,0 +1,30 @@
21+help:
22+ @echo "This project supports the following targets"
23+ @echo ""
24+ @echo " make help - show this text"
25+ @echo " make lint - run flake8"
26+ @echo " make test - run the functional test and unittests"
27+ @echo " make unittest - run the the unittest"
28+ @echo " make functionaltest - run the functional tests"
29+ @echo " make clean - remove unneeded files"
30+ @echo ""
31+
32+lint:
33+ @echo "Running flake8"
34+ @tox -e lint
35+
36+test: unittest functionaltest lint
37+
38+unittest:
39+ @tox -e unit
40+
41+functionaltest:
42+ @tox -e functional
43+
44+clean:
45+ @echo "Cleaning files"
46+ @if [ -d ./.tox ] ; then rm -r ./.tox ; fi
47+ @if [ -d ./.pytest_cache ] ; then rm -r ./.pytest_cache ; fi
48+
49+# The targets below don't depend on a file
50+.PHONY: lint test unittest functionaltest clean help
51
52=== modified file 'README.md'
53--- README.md 2014-05-15 16:06:29 +0000
54+++ README.md 2019-03-01 05:39:53 +0000
55@@ -21,3 +21,8 @@
56 juju deploy --to <special-mabine-number> jenkins-slave ppc-slave
57
58 See the Jenkins charm for more details.
59+
60+
61+# Notes
62+
63+We can't use interface:jenkins-slave yet as it's not fully implemented.
64
65=== added directory 'actions'
66=== added file 'files/jenkins-slave-sudoers'
67--- files/jenkins-slave-sudoers 1970-01-01 00:00:00 +0000
68+++ files/jenkins-slave-sudoers 2019-03-01 05:39:53 +0000
69@@ -0,0 +1,5 @@
70+# Created automatically during charm installation
71+# Any manual changes to it will be lost
72+
73+# User rules for jenkins
74+jenkins ALL=(ALL) NOPASSWD: ALL
75
76=== removed symlink 'hooks/config-changed'
77=== target was u'install'
78=== removed file 'hooks/configure-slave'
79--- hooks/configure-slave 2016-06-03 02:54:52 +0000
80+++ hooks/configure-slave 1970-01-01 00:00:00 +0000
81@@ -1,27 +0,0 @@
82-#!/bin/bash
83-
84-set -e
85-
86-# Grab the jenkins master url as a passed in parameter
87-url="$1"
88-
89-# Set the slave hostname to match the juju unit
90-slavehost="$(echo ${JUJU_UNIT_NAME} | sed s,/,-,)"
91-
92-# Set the slave and url fields
93-juju-log "Configuring jenkins-slave with ${url}..."
94-sed -i -e "s!^JENKINS_HOSTNAME.*!JENKINS_HOSTNAME=${slavehost}!" \
95- -e "s!^#*JENKINS_URL.*!JENKINS_URL=${url}!" \
96- /etc/default/jenkins-slave
97-
98-# Startup the jenkins-slave service
99-# This is called in the install, config-changed and upgrade-charm paths
100-# It needs to be tolerant of a running or stopped jenkins-slave
101-status="$(service jenkins-slave status || true)"
102-if echo "${status}" | egrep -q "stop|inactive"; then
103- juju-log "Starting jenkins-slave..."
104- service jenkins-slave start
105-else
106- juju-log "Restarting jenkins-slave..."
107- service jenkins-slave restart
108-fi
109
110=== removed symlink 'hooks/install.d/03_nrpe_relation_changed'
111=== target was u'canonical_ci_utils.py'
112=== removed file 'hooks/install.d/add_sudoers'
113--- hooks/install.d/add_sudoers 2015-08-13 18:04:16 +0000
114+++ hooks/install.d/add_sudoers 1970-01-01 00:00:00 +0000
115@@ -1,18 +0,0 @@
116-#!/bin/bash
117-
118-set -eu
119-
120-temp_sudoers="$(mktemp /tmp/jenkins.XXXXXX)"
121-
122-cat > "$temp_sudoers" << EOF
123-# Created automatically during charm installation
124-# Any manual changes to it will be lost
125-
126-# User rules for jenkins
127-jenkins ALL=(ALL) NOPASSWD: ALL
128-EOF
129-
130-visudo -c -f "$temp_sudoers"
131-install -m 440 "$temp_sudoers" /etc/sudoers.d/jenkins
132-rm "$temp_sudoers"
133-visudo -c
134
135=== removed file 'hooks/install.d/canonical_ci_utils.py'
136--- hooks/install.d/canonical_ci_utils.py 2015-12-03 10:40:55 +0000
137+++ hooks/install.d/canonical_ci_utils.py 1970-01-01 00:00:00 +0000
138@@ -1,91 +0,0 @@
139-#!/usr/bin/python
140-
141-import os
142-import sys
143-
144-from charmhelpers.canonical_ci import nrpe
145-
146-
147-from charmhelpers.core.hookenv import (
148- config,
149- local_unit,
150- log,
151- relation_ids,
152- unit_get,
153- ERROR,
154- INFO,
155-)
156-
157-from charmhelpers.core.host import (
158- service_reload,
159-)
160-
161-# Constants set in jenkins/hooks/install.
162-JENKINS_HOME = '/var/lib/jenkins'
163-JENKINS_USER = 'jenkins'
164-JENKINS_GROUP = 'nogroup'
165-JENKINS_PORT = 8080
166-
167-# Local constants
168-NRPE_CHECK_PS = "/etc/nagios/nrpe.d/check_jenkins_slave_ps.cfg"
169-
170-
171-def update_nrpe_config():
172- if not relation_ids('nrpe-external-master'):
173- log('No relation to an nrpe-external-master, not configuring '
174- 'nagios.')
175- return
176- with open(NRPE_CHECK_PS, "w") as conf:
177- log("Writing config: %s." % NRPE_CHECK_PS, INFO)
178- conf.write(nrpe.CONF_HEADER)
179- conf.write(
180- "command[check_jenkins_slave_ps]="
181- "/usr/lib/nagios/plugins/check_procs -c 1:1 -a slave.jar")
182-
183- # config
184- unit_name = local_unit().replace('/', '-')
185- n_hostname = "%s-%s" % (config("nagios_context"), unit_name)
186- n_servicegroup = config("nagios_servicegroups") if config("nagios_servicegroups") else config("nagios_context")
187- service_file = (
188- '/var/lib/nagios/export/service__%s_check_jenkins_slave.cfg' %
189- n_hostname)
190-
191- # There is a race between this code and the nrpe_external_master install
192- # hook. Both need to write files to /var/lib/nagios/export, but only one
193- # needs to create it. So tolerate failure if the directory exists.
194- try:
195- os.makedirs(os.path.dirname(service_file))
196- except os.error:
197- pass
198-
199- with open(service_file, "w") as conf:
200- log("Writing config: %s." % conf, INFO)
201- conf.write(nrpe.CONF_HEADER)
202- conf.write(nrpe.NRPE_SERVICE_ENTRY % {
203- 'nagios_hostname': n_hostname,
204- 'check_name': 'check_jenkins_slave_ps',
205- 'nagios_servicegroup': n_servicegroup,
206- })
207-
208- # reboot
209- if os.path.isfile('/etc/init.d/nagios-nrpe-server'):
210- service_reload('nagios-nrpe-server')
211-
212-
213-hooks = {
214- '03_nrpe_relation_changed': update_nrpe_config
215-}
216-
217-
218-def main(hook):
219- try:
220- hooks[hook]()
221- except KeyError:
222- e = 'Invalid install.d hook: %s.' % hook
223- log(e, ERROR)
224- raise Exception(e)
225-
226-
227-if __name__ == '__main__':
228- if os.path.islink(sys.argv[0]):
229- main(os.path.basename(sys.argv[0]))
230
231=== removed file 'hooks/nrpe-external-master-relation-changed'
232--- hooks/nrpe-external-master-relation-changed 2015-08-19 20:00:15 +0000
233+++ hooks/nrpe-external-master-relation-changed 1970-01-01 00:00:00 +0000
234@@ -1,7 +0,0 @@
235-#!/bin/sh
236-set -e
237-
238-home=`dirname $0`
239-
240-juju-log "Running install hook once more to pick up new NRPE relation changes"
241-exec $home/install
242
243=== removed symlink 'hooks/slave-relation-broken'
244=== target was u'slave-relation-departed'
245=== removed file 'hooks/slave-relation-changed'
246--- hooks/slave-relation-changed 2015-08-05 13:20:44 +0000
247+++ hooks/slave-relation-changed 1970-01-01 00:00:00 +0000
248@@ -1,19 +0,0 @@
249-#!/bin/bash
250-
251-set -e
252-
253-# Setup connection to master instance once set
254-url="$(relation-get url)"
255-
256-if [ "x$url" = "x" ]; then
257- juju-log "Master hasn't exported its url yet, exiting..."
258- exit 0
259-fi
260-
261-master_url="$(config-get master_url)"
262-if [ -n "${master_url}" ]; then
263- juju-log "Config option 'master_url' is set. Can't use slave relation."
264- exit 0
265-fi
266-
267-hooks/configure-slave "${url}"
268
269=== removed file 'hooks/slave-relation-departed'
270--- hooks/slave-relation-departed 2012-07-27 11:23:24 +0000
271+++ hooks/slave-relation-departed 1970-01-01 00:00:00 +0000
272@@ -1,3 +0,0 @@
273-#!/bin/bash
274-
275-stop jenkins-slave || true
276
277=== removed file 'hooks/slave-relation-joined'
278--- hooks/slave-relation-joined 2015-08-04 16:46:21 +0000
279+++ hooks/slave-relation-joined 1970-01-01 00:00:00 +0000
280@@ -1,20 +0,0 @@
281-#!/bin/bash
282-
283-set -e
284-
285-# Set the slave hostname to match the juju unit
286-# in the jenkins master instance
287-slavehost=`echo ${JUJU_UNIT_NAME} | sed s,/,-,`
288-noexecutors=`cat /proc/cpuinfo | grep processor | wc -l`
289-config_labels=`config-get labels`
290-labels=`uname -p`
291-
292-if [ -n "$config_labels" ]; then
293- labels=$config_labels
294-fi
295-
296-# Set all relations
297-relation-set executors=$noexecutors
298-relation-set labels="$labels"
299-relation-set slavehost=$slavehost
300-relation-set slaveaddress=`unit-get private-address`
301
302=== removed file 'hooks/start'
303--- hooks/start 2011-08-02 08:53:54 +0000
304+++ hooks/start 1970-01-01 00:00:00 +0000
305@@ -1,3 +0,0 @@
306-#!/bin/bash
307-
308-start jenkins-slave || true
309
310=== removed file 'hooks/stop'
311--- hooks/stop 2011-08-02 08:53:54 +0000
312+++ hooks/stop 1970-01-01 00:00:00 +0000
313@@ -1,3 +0,0 @@
314-#!/bin/bash
315-
316-stop jenkins-slave
317
318=== removed symlink 'hooks/upgrade-charm'
319=== target was u'install'
320=== added file 'layer.yaml'
321--- layer.yaml 1970-01-01 00:00:00 +0000
322+++ layer.yaml 2019-03-01 05:39:53 +0000
323@@ -0,0 +1,5 @@
324+includes:
325+ - layer:basic
326+ - layer:apt
327+ - layer:nagios
328+repo: lp:jenkins-slave-charm
329
330=== added directory 'lib'
331=== modified file 'metadata.yaml'
332--- metadata.yaml 2019-02-18 00:41:09 +0000
333+++ metadata.yaml 2019-03-01 05:39:53 +0000
334@@ -9,8 +9,12 @@
335 This charm provides support for jenkins slaves
336 .
337 https://launchpad.net/jenkins-slave-charm
338-categories:
339+tags:
340 - applications
341+series:
342+ - bionic
343+ - xenial
344+ - trusty
345 provides:
346 slave:
347 interface: jenkins-slave
348
349=== added directory 'reactive'
350=== renamed file 'hooks/install' => 'reactive/jenkins_slave.py'
351--- hooks/install 2016-06-14 01:04:43 +0000
352+++ reactive/jenkins_slave.py 2019-03-01 05:39:53 +0000
353@@ -1,121 +1,231 @@
354-#!/bin/bash
355-
356-set -eu
357-
358-
359-install_exec_d () {
360- if [[ -d exec.d ]]; then
361- shopt -s nullglob
362- for f in exec.d/*/charm-pre-install; do
363- [[ -x "$f" ]] || continue
364- ${SHELL} -c "$f"|| {
365- ## bail out if anyone fails
366- juju-log -l ERROR "$f: returned exit_status=$? "
367- }
368- done
369- shopt -u nullglob
370- fi
371-}
372-
373-# Get rid of the legacy jenkins-slave package, including config files.
374-clean_up_old_package () {
375- juju-log "Removing the old jenkin-slave package... (obsoleted by this charm)"
376- dpkg --purge jenkins-slave
377- juju-log "Removing the old jenkin-slave package... done."
378-}
379-
380-# Install the slave if it is not installed already.
381-install_slave () {
382- juju-log "Installing jenkins-slave..."
383- juju-log "Installing jenkins-slave (dependencies)..."
384- apt-get -y install -qq wget adduser default-jre-headless
385+import os
386+
387+from charmhelpers.core import hookenv, host, templating, unitdata
388+from charmhelpers.contrib.charmsupport import nrpe
389+from charmhelpers.fetch import apt_purge
390+from charms import apt, reactive
391+
392+
393+@reactive.hook('upgrade-charm')
394+def upgrade_charm():
395+ hookenv.status_set('maintenance', 'forcing reconfiguration on upgrade-charm')
396+ reactive.clear_flag('jenkins-slave.active')
397+ reactive.clear_flag('jenkins-slave.installed')
398+
399+
400+@reactive.when_not('jenkins-slave.installed')
401+def install():
402+ hookenv.status_set('maintenance', 'installing jenkins-slave')
403+ reactive.clear_flag('jenkins-slave.active')
404+
405+ config = hookenv.config()
406+
407+ hookenv.log('Adding jenkins-slave dependencies to be installed')
408+ packages = ['wget', 'default-jre-headless']
409+
410+ # Install extra packages needed by the slave.
411+ tools = config.get('tools')
412+ if tools:
413+ hookenv.log('Adding jenkins-slave additional tools to be installed: {}'.format(tools))
414+ for package in tools.split():
415+ packages.append(package)
416+ apt.queue_install(packages)
417+ if not apt.install_queued():
418+ return # apt layer already set blocked state.
419+
420+ # Get rid of the legacy jenkins-slave package, including config files.
421+ hookenv.log('Removing the old jenkins-slave package... (obsoleted by this charm)')
422+ apt_purge(['jenkins-slave'])
423
424 # Create jenkins user if it doesn't exist.
425- if ! id jenkins > /dev/null 2>&1 ; then
426- juju-log "Installing jenkins-slave (user account)..."
427- adduser --system --home /var/lib/jenkins --group \
428- --disabled-password --quiet --shell /bin/bash \
429- jenkins
430- else
431- juju-log "Installing jenkins-slave (user account already exists)..."
432- fi
433- juju-log "Installing jenkins-slave (directories)..."
434+ if host.user_exists('jenkins'):
435+ hookenv.log('Installing jenkins-slave (user account already exists)...')
436+ else:
437+ hookenv.log('Installing jenkins-slave (user account)...')
438+ host.adduser(username='jenkins', system_user=True, home_dir='/var/lib/jenkins')
439+
440 # And ensure required directories exist and are set up.
441- mkdir -p /var/lib/jenkins
442- chown -R jenkins:jenkins /var/lib/jenkins || true
443- mkdir -p /var/log/jenkins
444- chown -R jenkins:jenkins /var/log/jenkins || true
445- juju-log "Installing jenkins-slave (common files)..."
446- install -m 0555 files/download-slave.sh /usr/local/sbin/download-slave.sh
447- # XXX obviously we lose conffile handling here...
448- install -m 0444 files/jenkins-slave-default /etc/default/jenkins-slave
449- install -m 0444 files/jenkins-slave-logrotate-config /etc/logrotate.d/jenkins-slave
450-
451- distro=$(source /etc/lsb-release ; echo $DISTRIB_CODENAME)
452- case $distro in
453- xenial)
454- # LTS or bust!
455- juju-log "Installing jenkins-slave (system unit)..."
456- install -m 0444 files/jenkins-slave-systemd-config /lib/systemd/system/jenkins-slave.service
457- systemctl enable jenkins-slave
458- ;;
459- *)
460- # Probably an LTS, and it's not, then too bad.
461- juju-log "Installing jenkins-slave (upstart job)..."
462- install -m 0444 files/jenkins-slave-upstart-config /etc/init/jenkins-slave.conf
463- ;;
464- esac
465- juju-log "Installing jenkins-slave... done."
466-}
467-
468-
469-# Install extra packages needed by the slave.
470-install_tools () {
471- juju-log "Installing tools..."
472- apt-get -y install -qq $(config-get tools)
473-}
474-
475-
476-# Configure slave
477-set_up_slave () {
478- # If a master_url value is specified, use that to configure the slave.
479- master_url="$(config-get master_url)"
480- if [ -n "${master_url}" ]; then
481- juju-log "Using 'master_url' to configure the slave."
482- hooks/configure-slave "${master_url}"
483- else
484- juju-log "No 'master_url' set; not configuring slave at this time."
485- fi
486-}
487-
488-# Execute any hook overlay which may be provided
489-# by forks of this charm.
490-install_extra_hooks () {
491- # XXX for canonical_ci_utils.py
492- apt-get -y install -qq python-yaml
493-
494- juju-log "Installing hooks..."
495- if [[ -d hooks/install.d ]]
496- then
497- for i in $(ls -1 hooks/install.d/*)
498- do
499- if [[ -x "$i" ]]
500- then
501- ./$i
502- fi
503- done
504- else
505- juju-log "No extra hooks found."
506- fi
507-}
508-
509-
510-apt-get update -qq
511-install_exec_d
512-clean_up_old_package
513-install_slave
514-install_tools
515-set_up_slave
516-install_extra_hooks
517-
518-exit 0
519+ hookenv.log('Installing jenkins-slave (directories)...')
520+ host.mkdir('/var/lib/jenkins', owner='jenkins', group='jenkins')
521+ host.mkdir('/var/log/jenkins', owner='jenkins', group='jenkins')
522+
523+ hookenv.log('Installing jenkins-slave (common files)...')
524+ write_default_conf()
525+ file_to_units('files/download-slave.sh', '/usr/local/sbin/download-slave.sh')
526+ file_to_units('files/jenkins-slave-logrotate-config', '/etc/logrotate.d/jenkins-slave')
527+
528+ if host.lsb_release()['DISTRIB_CODENAME'] == 'trusty':
529+ hookenv.log('Installing jenkins-slave (upstart job)...')
530+ file_to_units('files/jenkins-slave-upstart-config', '/etc/init/jenkins-slave.conf')
531+ else:
532+ hookenv.log('Installing jenkins-slave (system unit)...')
533+ file_to_units('files/jenkins-slave-systemd-config', '/lib/systemd/system/jenkins-slave.service')
534+ host.service('enable', 'jenkins-slave')
535+
536+ hookenv.log('Installing jenkins-slave... done.')
537+ reactive.clear_flag('jenkins-slave.blocked')
538+ reactive.clear_flag('jenkins-slave.configured')
539+ reactive.set_flag('jenkins-slave.installed')
540+
541+
542+@reactive.when('config.changed')
543+def config_changed():
544+ reactive.clear_flag('jenkins-slave.blocked')
545+ reactive.clear_flag('jenkins-slave.configured')
546+ reactive.clear_flag('nagios-nrpe.configured')
547+
548+
549+@reactive.when('jenkins-slave.installed')
550+@reactive.when_not('jenkins-slave.configured')
551+@reactive.when_not('jenkins-slave.blocked')
552+def configure_jenkins_slave():
553+ hookenv.status_set('maintenance', 'configuring jenkins-slave')
554+ reactive.clear_flag('jenkins-slave.active')
555+
556+ config = hookenv.config()
557+ kv = unitdata.kv()
558+
559+ if config.get('master_url'):
560+ hookenv.log("Using 'master_url' to configure the slave.")
561+ write_default_conf(config.get('master_url'))
562+ elif kv.get('url'):
563+ hookenv.log("Using url from relation as 'master_url'")
564+ write_default_conf(kv.get('url'))
565+ else:
566+ hookenv.log("No 'master_url' set; not configuring slave at this time.")
567+ hookenv.status_set('blocked', "requires either slave relation or 'master_url'")
568+ reactive.set_flag('jenkins-slave.blocked')
569+ return
570+
571+ file_to_units('files/jenkins-slave-sudoers', '/etc/sudoers.d/jenkins', perms=0o440)
572+
573+ reactive.clear_flag('nagios-nrpe.configured')
574+ reactive.set_flag('jenkins-slave.configured')
575+
576+
577+@reactive.when('jenkins-slave.blocked')
578+def blocked_on_jenkins_url():
579+ reactive.clear_flag('jenkins-slave.active')
580+
581+
582+@reactive.when('jenkins-slave.configured')
583+@reactive.when('nrpe-external-master.available')
584+@reactive.when('jenkins-slave.active')
585+@reactive.when_not('nagios-nrpe.configured')
586+def configure_nagios(nagios):
587+ hookenv.status_set('maintenance', 'setting up NRPE checks')
588+
589+ # Use charmhelpers.contrib.charmsupport's nrpe to determine hostname
590+ hostname = nrpe.get_nagios_hostname()
591+ nrpe_setup = nrpe.NRPE(hostname=hostname, primary=True)
592+
593+ cmd = '/usr/lib/nagios/plugins/check_procs -c 1:1 -a slave.jar'
594+ nrpe_setup.add_check('jenkins_slave_ps', 'Jenkins Slave Process', cmd)
595+
596+ nrpe_setup.write()
597+ reactive.set_flag('nagios-nrpe.configured')
598+
599+
600+@reactive.when('jenkins-slave.configured')
601+@reactive.when_not('jenkins-slave.active')
602+def set_active():
603+ # Startup the jenkins-slave service. This is called in the
604+ # install, config-changed and upgrade-charm paths. It needs to be
605+ # tolerant of a running or stopped jenkins-slave.
606+ if host.service_running('jenkins-slave'):
607+ hookenv.log('Restarting jenkins-slave...')
608+ host.service_restart('jenkins-slave')
609+ else:
610+ hookenv.log('Starting jenkins-slave...')
611+ host.service_start('jenkins-slave')
612+
613+ hookenv.status_set('active', 'ready')
614+ reactive.set_flag('jenkins-slave.active')
615+
616+
617+# We can't use interface:jenkins-slave yet as it's not implemented.
618+@reactive.hook('slave-relation-joined', 'slave-relation-changed')
619+def slave_relation_changed():
620+ reactive.set_flag('slave-relation.available')
621+ reactive.clear_flag('jenkins-slave.blocked')
622+ reactive.clear_flag('jenkins-slave.configured')
623+ reactive.clear_flag('slave-relation.configured')
624+
625+
626+@reactive.hook('slave-relation-departed', 'slave-relation-broken')
627+def slave_relation_removed():
628+ kv = unitdata.kv()
629+ kv.set('url', None)
630+ reactive.clear_flag('slave-relation.available')
631+
632+
633+@reactive.when('slave-relation.available')
634+@reactive.when_not('slave-relation.configured')
635+def slave_relation():
636+ hookenv.status_set('maintenance', 'setting up jenkins via slave relation')
637+ config = hookenv.config()
638+ kv = unitdata.kv()
639+
640+ if config.get('master_url'):
641+ hookenv.log("Config option 'master_url' is set. Can't use slave relation.")
642+ reactive.set_flag('slave-relation.configured')
643+ return
644+
645+ url = hookenv.relation_get('url')
646+ if url:
647+ kv.set('url', url)
648+ write_default_conf(url)
649+ else:
650+ hookenv.log("Master hasn't exported its url yet, exiting...")
651+ return
652+
653+ reactive.clear_flag('jenkins-slave.active')
654+
655+ # Set the slave hostname to match the juju unit
656+ # in the jenkins master instance
657+ slave_host = hookenv.local_unit().replace('/', '-')
658+ slave_address = hookenv.unit_private_ip()
659+ noexecutors = os.cpu_count()
660+ config_labels = config.get('labels')
661+
662+ if config_labels:
663+ labels = config_labels
664+ else:
665+ labels = os.uname()[4]
666+
667+ # Set all relations
668+ hookenv.relation_set(executors=noexecutors)
669+ hookenv.relation_set(labels=labels)
670+ hookenv.relation_set(slavehost=slave_host)
671+ hookenv.relation_set(slaveaddress=slave_address)
672+ reactive.set_flag('slave-relation.configured')
673+
674+
675+def file_to_units(local_path, unit_path, perms=None, owner='root', group='root'):
676+ """ copy a file from the charm onto our unit(s) """
677+ file_perms = perms
678+ if not perms:
679+ # Let's try manually work it out
680+ if local_path[-3:] == '.py' or local_path[-3:] == '.sh':
681+ file_perms = 0o755
682+ else:
683+ file_perms = 0o644
684+
685+ with open(local_path, 'r') as fh:
686+ host.write_file(
687+ path=unit_path,
688+ content=fh.read().encode(),
689+ owner=owner,
690+ group=group,
691+ perms=file_perms,
692+ )
693+
694+
695+def write_default_conf(master_url=None, owner='root', group='root',
696+ conf_path='/etc/default/jenkins-slave'):
697+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
698+ slave_host = hookenv.local_unit().replace('/', '-')
699+ templating.render('jenkins-slave-default', conf_path,
700+ {'master_url': master_url, 'slave_host': slave_host},
701+ owner=owner, group=group, perms=0o444,
702+ templates_dir=templates_dir)
703
704=== added file 'requirements.txt'
705--- requirements.txt 1970-01-01 00:00:00 +0000
706+++ requirements.txt 2019-03-01 05:39:53 +0000
707@@ -0,0 +1,1 @@
708+# Include python requirements here
709
710=== removed file 'revision'
711--- revision 2015-08-04 16:46:21 +0000
712+++ revision 1970-01-01 00:00:00 +0000
713@@ -1,1 +0,0 @@
714-9
715
716=== added directory 'templates'
717=== renamed file 'files/jenkins-slave-default' => 'templates/jenkins-slave-default'
718--- files/jenkins-slave-default 2016-06-03 03:37:31 +0000
719+++ templates/jenkins-slave-default 2019-03-01 05:39:53 +0000
720@@ -32,12 +32,20 @@
721 # URL of jenkins server to connect to
722 # Not specifying this parameter will stop the slave
723 # job from running.
724+{% if master_url %}
725+JENKINS_URL="{{ master_url }}"
726+{% else %}
727 #JENKINS_URL=""
728+{% endif %}
729
730 # Name of slave configuration to use at JENKINS_URL
731 # Override if it need to be something other than the
732 # hostname of the server the slave is running on.
733+{% if slave_host %}
734+JENKINS_HOSTNAME="{{ slave_host }}"
735+{% else %}
736 JENKINS_HOSTNAME="$(hostname)"
737+{% endif %}
738
739 # Log file location for use in Debian init script
740 JENKINS_SLAVE_LOG=/var/log/jenkins/$NAME.log
741
742=== added directory 'tests'
743=== added directory 'tests/functional'
744=== added file 'tests/functional/requirements.txt'
745--- tests/functional/requirements.txt 1970-01-01 00:00:00 +0000
746+++ tests/functional/requirements.txt 2019-03-01 05:39:53 +0000
747@@ -0,0 +1,6 @@
748+flake8
749+juju
750+mock
751+pytest
752+pytest-asyncio
753+requests
754
755=== added file 'tests/functional/test_jenkins_slave.py'
756--- tests/functional/test_jenkins_slave.py 1970-01-01 00:00:00 +0000
757+++ tests/functional/test_jenkins_slave.py 2019-03-01 05:39:53 +0000
758@@ -0,0 +1,50 @@
759+import os
760+import pytest
761+from juju.model import Model
762+
763+# Treat tests as coroutines
764+pytestmark = pytest.mark.asyncio
765+
766+series = ['bionic']
767+juju_repository = os.getenv('JUJU_REPOSITORY', '.').rstrip('/')
768+
769+
770+@pytest.fixture
771+async def model():
772+ model = Model()
773+ await model.connect_current()
774+ yield model
775+ await model.disconnect()
776+
777+
778+@pytest.fixture
779+async def apps(model):
780+ apps = []
781+ for entry in series:
782+ app = model.applications['jenkins-slave-{}'.format(entry)]
783+ apps.append(app)
784+ return apps
785+
786+
787+@pytest.fixture
788+async def units(apps):
789+ units = []
790+ for app in apps:
791+ units.extend(app.units)
792+ return units
793+
794+
795+@pytest.mark.parametrize('series', series)
796+async def test_jenkins_slave_deploy(model, series):
797+ # Starts a deploy for each series
798+ await model.deploy('{}/builds/jenkins-slave'.format(juju_repository),
799+ series=series,
800+ application_name='jenkins-slave-{}'.format(series))
801+ assert True
802+
803+
804+async def test_jenkins_slave_status(apps, model):
805+ # Verifies status for all deployed series of the charm
806+ for app in apps:
807+ await model.block_until(lambda: app.status == 'active')
808+ assert True
809
810=== added directory 'tests/unit'
811=== added directory 'tests/unit/files'
812=== added file 'tests/unit/files/jenkins-slave-default'
813--- tests/unit/files/jenkins-slave-default 1970-01-01 00:00:00 +0000
814+++ tests/unit/files/jenkins-slave-default 2019-03-01 05:39:53 +0000
815@@ -0,0 +1,59 @@
816+#
817+# This file is managed by Juju. Attempt no changes here.
818+#
819+
820+# defaults for jenkins-slave component of the jenkins continuous integration
821+# system
822+
823+# pulled in from the init script; makes things easier.
824+NAME=jenkins-slave
825+
826+# location of java
827+JAVA=/usr/bin/java
828+
829+# arguments to pass to java - optional
830+#JAVA_ARGS="-Xmx256m"
831+
832+# for daemon to use
833+PIDFILE=/var/run/jenkins/$NAME.pid
834+
835+# user id to be invoked as (otherwise will run as root; not wise!)
836+JENKINS_USER=jenkins
837+
838+# location of jenkins arch indep files
839+JENKINS_ROOT=/usr/share/jenkins
840+
841+# jenkins home location
842+JENKINS_HOME=/var/lib/jenkins
843+
844+# jenkins /run location
845+JENKINS_RUN=/var/run/jenkins
846+
847+# URL of jenkins server to connect to
848+# Not specifying this parameter will stop the slave
849+# job from running.
850+
851+#JENKINS_URL=""
852+
853+
854+# Name of slave configuration to use at JENKINS_URL
855+# Override if it need to be something other than the
856+# hostname of the server the slave is running on.
857+
858+JENKINS_HOSTNAME="jenkins-slave-3"
859+
860+
861+# Log file location for use in Debian init script
862+JENKINS_SLAVE_LOG=/var/log/jenkins/$NAME.log
863+
864+# OS LIMITS SETUP
865+# comment this out to observe /etc/security/limits.conf
866+# this is on by default because http://github.com/feniix/hudson/commit/d13c08ea8f5a3fa730ba174305e6429b74853927
867+# reported that Ubuntu's PAM configuration doesn't include pam_limits.so, and as a result the # of file
868+# descriptors are forced to 1024 regardless of /etc/security/limits.confa
869+# NOTE - Ubuntu Users - this is not used by the upstart configuration - please use an upstart overrides file
870+# to change the OS limits setup.
871+MAXOPENFILES=8192
872+
873+# Arguments to pass to jenkins slave on startup
874+JENKINS_ARGS="-jnlpUrl $JENKINS_URL/computer/$JENKINS_HOSTNAME/slave-agent.jnlp"
875\ No newline at end of file
876
877=== added file 'tests/unit/files/somefile'
878=== added file 'tests/unit/files/somefile.py'
879=== added file 'tests/unit/requirements.txt'
880--- tests/unit/requirements.txt 1970-01-01 00:00:00 +0000
881+++ tests/unit/requirements.txt 2019-03-01 05:39:53 +0000
882@@ -0,0 +1,5 @@
883+charmhelpers
884+charms.reactive
885+mock
886+pytest
887+pytest-cov
888
889=== added file 'tests/unit/test_jenkins_slave.py'
890--- tests/unit/test_jenkins_slave.py 1970-01-01 00:00:00 +0000
891+++ tests/unit/test_jenkins_slave.py 2019-03-01 05:39:53 +0000
892@@ -0,0 +1,218 @@
893+import grp
894+import os
895+import pwd
896+import shutil
897+import sys
898+import tempfile
899+import unittest
900+from unittest import mock
901+
902+sys.modules['charms.apt'] = mock.MagicMock()
903+from charms import apt # NOQA: E402
904+
905+# Add path to where our reactive layer lives and import.
906+sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
907+from reactive.jenkins_slave import (
908+ config_changed,
909+ install,
910+ configure_jenkins_slave,
911+ file_to_units,
912+ write_default_conf,
913+) # NOQA: E402
914+
915+
916+INITIAL_CONF = 'tests/unit/files/jenkins-slave-default'
917+
918+
919+class TestSetDefaultConf(unittest.TestCase):
920+ def setUp(self):
921+ self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
922+ temp_file = tempfile.NamedTemporaryFile(delete=False, dir=self.tmpdir)
923+ with open(INITIAL_CONF, 'rb') as f:
924+ conf = f.read().decode('utf-8')
925+ temp_file.write(conf.encode())
926+ temp_file.close()
927+ self.conf_file = temp_file.name
928+ self.user = pwd.getpwuid(os.getuid()).pw_name
929+ self.group = grp.getgrgid(os.getgid()).gr_name
930+ self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
931+
932+ # charmhelpers is getting difficult to test against, as it writes
933+ # to system directories even for things that should be idempotent,
934+ # like accessing config options.
935+ patcher = mock.patch('charmhelpers.core.hookenv.charm_dir')
936+ self.mock_charm_dir = patcher.start()
937+ self.addCleanup(patcher.stop)
938+ self.mock_charm_dir.return_value = self.charm_dir
939+
940+ patcher = mock.patch('charmhelpers.core.hookenv.config')
941+ self.mock_config = patcher.start()
942+ self.addCleanup(patcher.stop)
943+ self.mock_config.return_value = {'tools': []}
944+
945+ patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
946+ self.mock_local_unit = patcher.start()
947+ self.addCleanup(patcher.stop)
948+ self.mock_local_unit.return_value = 'mock-jenkins-slave/0'
949+
950+ patcher = mock.patch('charmhelpers.core.hookenv.log')
951+ self.mock_log = patcher.start()
952+ self.addCleanup(patcher.stop)
953+ self.mock_log.return_value = ''
954+
955+ def tearDown(self):
956+ shutil.rmtree(self.tmpdir)
957+
958+ @mock.patch('charms.reactive.clear_flag')
959+ def test_hook_config_changed(self, clear_flag):
960+ config_changed()
961+ expected = [mock.call('jenkins-slave.blocked'),
962+ mock.call('jenkins-slave.configured'),
963+ mock.call('nagios-nrpe.configured')]
964+ self.assertEqual(clear_flag.call_args_list, expected)
965+
966+ @mock.patch('charmhelpers.core.host.adduser')
967+ @mock.patch('charmhelpers.core.host.mkdir')
968+ @mock.patch('charmhelpers.core.host.service')
969+ @mock.patch('reactive.jenkins_slave.apt_purge')
970+ @mock.patch('reactive.jenkins_slave.file_to_units')
971+ @mock.patch('reactive.jenkins_slave.write_default_conf')
972+ def test_hook_install(self, write_default_conf, file_to_units, apt_purge, service, mkdir, adduser):
973+ install()
974+ expected = [mock.call(home_dir='/var/lib/jenkins', system_user=True, username='jenkins')]
975+ self.assertEqual(adduser.call_args_list, expected)
976+ expected = [mock.call('/var/lib/jenkins', group='jenkins', owner='jenkins'),
977+ mock.call('/var/log/jenkins', group='jenkins', owner='jenkins')]
978+ self.assertEqual(mkdir.call_args_list, expected)
979+ self.assertEqual(service.call_args_list, [mock.call('enable', 'jenkins-slave')])
980+ self.assertEqual(apt_purge.call_args_list, [mock.call(['jenkins-slave'])])
981+ expected = [mock.call('files/download-slave.sh', '/usr/local/sbin/download-slave.sh'),
982+ mock.call('files/jenkins-slave-logrotate-config', '/etc/logrotate.d/jenkins-slave'),
983+ mock.call('files/jenkins-slave-systemd-config', '/lib/systemd/system/jenkins-slave.service')]
984+ self.assertEqual(file_to_units.call_args_list, expected)
985+ self.assertEqual(write_default_conf.call_args_list, [mock.call()])
986+ expected = [mock.call.queue_install(['wget', 'default-jre-headless']),
987+ mock.call.install_queued()]
988+ self.assertEqual(apt.method_calls, expected)
989+
990+ @mock.patch('charms.reactive.clear_flag')
991+ @mock.patch('charms.reactive.set_flag')
992+ @mock.patch('charmhelpers.core.hookenv.config')
993+ @mock.patch('charmhelpers.core.unitdata.kv')
994+ @mock.patch('reactive.jenkins_slave.write_default_conf')
995+ @mock.patch('reactive.jenkins_slave.file_to_units')
996+ def test_configure_jenkins_slave_no_url(self, file_to_units, write_default_conf, unitdata_kv, config,
997+ set_flag, clear_flag):
998+ config.return_value = {}
999+ unitdata_kv.return_value = {}
1000+ configure_jenkins_slave()
1001+ self.assertEqual(write_default_conf.call_args_list, [])
1002+ self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.blocked')])
1003+ self.assertEqual(clear_flag.call_args_list, [mock.call('jenkins-slave.active')])
1004+
1005+ @mock.patch('charms.reactive.clear_flag')
1006+ @mock.patch('charms.reactive.set_flag')
1007+ @mock.patch('charmhelpers.core.hookenv.config')
1008+ @mock.patch('charmhelpers.core.unitdata.kv')
1009+ @mock.patch('reactive.jenkins_slave.write_default_conf')
1010+ @mock.patch('reactive.jenkins_slave.file_to_units')
1011+ def test_configure_jenkins_slave_master_url(self, file_to_units, write_default_conf, unitdata_kv, config,
1012+ set_flag, clear_flag):
1013+ config.return_value = {'master_url': 'http://10.1.1.1:8080'}
1014+ unitdata_kv.return_value = {}
1015+ configure_jenkins_slave()
1016+ self.assertEqual(write_default_conf.call_args_list, [mock.call('http://10.1.1.1:8080')])
1017+ self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.configured')])
1018+ expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]
1019+ self.assertEqual(clear_flag.call_args_list, expected)
1020+
1021+ @mock.patch('charms.reactive.clear_flag')
1022+ @mock.patch('charms.reactive.set_flag')
1023+ @mock.patch('charmhelpers.core.hookenv.config')
1024+ @mock.patch('charmhelpers.core.unitdata.kv')
1025+ @mock.patch('reactive.jenkins_slave.write_default_conf')
1026+ @mock.patch('reactive.jenkins_slave.file_to_units')
1027+ def test_configure_jenkins_slave_relation_url(self, file_to_units, write_default_conf, unitdata_kv, config,
1028+ set_flag, clear_flag):
1029+ config.return_value = {}
1030+ unitdata_kv.return_value = {'url': 'http://10.22.22.22:8080'}
1031+ configure_jenkins_slave()
1032+ print(write_default_conf.call_args_list)
1033+ self.assertEqual(write_default_conf.call_args_list, [mock.call('http://10.22.22.22:8080')])
1034+ self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.configured')])
1035+ expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]
1036+ self.assertEqual(clear_flag.call_args_list, expected)
1037+
1038+ def test_write_default_conf_update(self):
1039+ write_default_conf('http://10.1.1.1:8080', self.user, self.group, self.conf_file)
1040+ self.assertTrue(conf_match(self.conf_file, 'JENKINS_URL', 'http://10.1.1.1:8080'))
1041+
1042+ def test_write_default_conf_reset(self):
1043+ write_default_conf(None, self.user, self.group, self.conf_file)
1044+ self.assertTrue(conf_match(self.conf_file, '#JENKINS_URL', ''))
1045+
1046+ def test_file_to_units_executable_sh(self):
1047+ source = os.path.join(self.charm_dir, 'files/download-slave.sh')
1048+ dest = os.path.join(self.tmpdir, os.path.basename(source))
1049+ file_to_units(source, dest, owner=self.user, group=self.group)
1050+ with open(dest, 'rb') as fh:
1051+ want = fh.read().decode('utf-8')
1052+ self.assertTrue(conf_equals(source, want))
1053+ self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
1054+ self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
1055+ self.assertTrue(os.access(dest, os.X_OK))
1056+
1057+ def test_file_to_units_executable_py(self):
1058+ source = os.path.join(self.charm_dir, 'tests/unit/files/somefile.py')
1059+ dest = os.path.join(self.tmpdir, os.path.basename(source))
1060+ file_to_units(source, dest, owner=self.user, group=self.group)
1061+ with open(dest, 'rb') as fh:
1062+ want = fh.read().decode('utf-8')
1063+ self.assertTrue(conf_equals(source, want))
1064+ self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
1065+ self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
1066+ self.assertTrue(os.access(dest, os.X_OK))
1067+
1068+ def test_file_to_units_non_executable(self):
1069+ source = os.path.join(self.charm_dir, 'files/jenkins-slave-logrotate-config')
1070+ dest = os.path.join(self.tmpdir, os.path.basename(source))
1071+ file_to_units(source, dest, owner=self.user, group=self.group)
1072+ with open(dest, 'rb') as fh:
1073+ want = fh.read().decode('utf-8')
1074+ self.assertTrue(conf_equals(dest, want))
1075+ self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
1076+ self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
1077+ self.assertFalse(os.access(dest, os.X_OK))
1078+
1079+ def test_file_to_units_non_executable_x_on_disk(self):
1080+ source = os.path.join(self.charm_dir, 'tests/unit/files/somefile')
1081+ dest = os.path.join(self.tmpdir, os.path.basename(source))
1082+ file_to_units(source, dest, owner=self.user, group=self.group)
1083+ with open(dest, 'rb') as fh:
1084+ want = fh.read().decode('utf-8')
1085+ self.assertTrue(conf_equals(source, want))
1086+ self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
1087+ self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
1088+ self.assertFalse(os.access(dest, os.X_OK))
1089+
1090+
1091+def conf_equals(conf_file, want):
1092+ with open(conf_file, 'rb') as conf:
1093+ got = conf.read().decode('utf-8')
1094+ if got == want:
1095+ return True
1096+ print('{}\n != \n{}'.format(got, want))
1097+ return False
1098+
1099+
1100+def conf_match(conf_file, key, value):
1101+ with open(conf_file, 'rb') as conf:
1102+ for line in conf.readlines():
1103+ line = line.decode('utf-8').rstrip('\n')
1104+ if line == '{}="{}"'.format(key, value):
1105+ return True
1106+ return False
1107+
1108+
1109+if __name__ == '__main__':
1110+ unittest.main()
1111
1112=== added file 'tox.ini'
1113--- tox.ini 1970-01-01 00:00:00 +0000
1114+++ tox.ini 2019-03-01 05:39:53 +0000
1115@@ -0,0 +1,37 @@
1116+[tox]
1117+skipsdist=True
1118+envlist = unit, functional
1119+skip_missing_interpreters = True
1120+
1121+[testenv]
1122+basepython = python3
1123+setenv =
1124+ PYTHONPATH = .
1125+
1126+[testenv:unit]
1127+commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term
1128+deps = -r{toxinidir}/tests/unit/requirements.txt
1129+ -r{toxinidir}/requirements.txt
1130+setenv = PYTHONPATH={toxinidir}/lib
1131+
1132+[testenv:functional]
1133+passenv =
1134+ HOME
1135+ JUJU_REPOSITORY
1136+ PATH
1137+commands = pytest -v --ignore {toxinidir}/tests/unit
1138+deps = -r{toxinidir}/tests/functional/requirements.txt
1139+ -r{toxinidir}/requirements.txt
1140+
1141+[testenv:lint]
1142+commands = flake8
1143+deps = flake8
1144+
1145+[flake8]
1146+exclude =
1147+ .git,
1148+ __pycache__,
1149+ .tox,
1150+ hooks/install.d/,
1151+max-line-length = 120
1152+max-complexity = 10

Subscribers

People subscribed via source and target branches