Merge charm-k8s-jenkins-agent:relations-config into charm-k8s-jenkins-agent:master

Proposed by Alexandre Gomes
Status: Superseded
Proposed branch: charm-k8s-jenkins-agent:relations-config
Merge into: charm-k8s-jenkins-agent:master
Prerequisite: charm-k8s-jenkins-agent:manual-agent
Diff against target: 787 lines (+372/-206)
7 files modified
.gitignore (+1/-0)
config.yaml (+16/-18)
dev/null (+0/-66)
dockerfile/Dockerfile (+1/-0)
dockerfile/files/entrypoint.sh (+2/-21)
src/charm.py (+52/-95)
tests/unit/test_charm.py (+300/-6)
Reviewer Review Type Date Requested Status
Tom Haddon Needs Fixing
Canonical IS Reviewers Pending
Review via email: mp+389373@code.launchpad.net

This proposal has been superseded by a proposal from 2020-08-22.

Commit message

Add feature to configure agents through juju relations

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

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

Revision history for this message
Tom Haddon (mthaddon) wrote :

Some comments inline. Tests will need updating a little based on what changes you make.

review: Needs Fixing
a868510... by Alexandre Gomes

Address comments

Unmerged commits

a868510... by Alexandre Gomes

Address comments

7661412... by Alexandre Gomes

Add unit tests

f82f78e... by Alexandre Gomes

Add feature to configure agent through juju relations

80ab0b9... by Alexandre Gomes

Fix shellcheck command

c3716be... by Alexandre Gomes

Add comment on why root is used in Dockerfile

2d4df32... by Alexandre Gomes

Add unit tests

74abd55... by Alexandre Gomes

Add unit/ to .gitignore

e5a08a7... by Alexandre Gomes

Remove some unecessary bits

1394cc7... by Alexandre Gomes

Update init()

2807a77... by Alexandre Gomes

Remove some unused imports

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index bf7fffd..eea14ad 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -6,3 +6,4 @@ __pycache__
6 build
7 lib/ops
8 lib/ops-*.dist-info
9+unit/
10diff --git a/config.yaml b/config.yaml
11index 7ad8e02..c419381 100644
12--- a/config.yaml
13+++ b/config.yaml
14@@ -3,32 +3,30 @@ options:
15 type: string
16 default: "jenkins-agent-operator"
17 description: "The docker image to install. Required. Defaults to jenkins-agent-operator from rocks.canonical.com"
18- tools:
19- type: string
20- default: git gcc make bzr
21- description: Tooling to deploy on jenkins agent node
22- labels:
23- type: string
24- description: Jenkins labels to associate with jenkins agent node
25- default: ""
26 jenkins_master_url:
27 type: string
28 default: ""
29 description: |
30- Configure the agent to use an explicit jenkins master instead of using
31- the jenkins-agent relation. This allows for the agent and master to
32- be deployed in different environments.
33+ Configure the agent to use an explicit Jenkins master instead of using
34+ the jenkins-agent relation. This allows the agent to connect to a Jenkins instance not managed
35+ by Juju.
36 jenkins_agent_name:
37 type: string
38 default: ""
39 description: |
40- Jenkins agent name that will be seen on the jenkins master.
41- Will default to the hostname of the container.
42- jenkins_api_token:
43+ Agent name as configured in Jenkins. Multiple names can be input by using `:` as a separator.
44+ Example: "agent-one:agent-two:agent-three"
45+ jenkins_agent_token:
46 type: string
47 default: ""
48- description: "Jenkins API token used to join the master"
49- jenkins_user:
50+ description: |
51+ Agent token provided by Jenkins. Can be found in your Jenkins instance at
52+ ${JENKINS_URL}/computer/${AGENT_NAME}/. Multiple tokens can be input by
53+ using `:` as a separator matching the order of the agents in `jenkins_agent_name`.
54+ Example: "token-one:token-two:token-three"
55+ jenkins_agent_labels:
56 type: string
57- default: "admin"
58- description: "Jenkins user to download agent.jar from master"
59+ default: ""
60+ description: |
61+ Comma-separated list of labels to be assigned to the agent in Jenkins. If not set it will default to
62+ the agents hardware identifier, e.g.: 'x86_64'
63diff --git a/dockerfile/Dockerfile b/dockerfile/Dockerfile
64index e03339f..700e70f 100644
65--- a/dockerfile/Dockerfile
66+++ b/dockerfile/Dockerfile
67@@ -47,6 +47,7 @@ RUN apt-get update -y \
68
69 WORKDIR /var/lib/jenkins
70
71+# Using root due to https://bugs.launchpad.net/juju/+bug/1879598
72 #USER ${USER}
73 USER root
74
75diff --git a/dockerfile/files/entrypoint.sh b/dockerfile/files/entrypoint.sh
76index c8ece01..a2f820e 100755
77--- a/dockerfile/files/entrypoint.sh
78+++ b/dockerfile/files/entrypoint.sh
79@@ -21,21 +21,11 @@ typeset JAVA_ARGS=${JAVA_ARGS:-""}
80 # job from running.
81 typeset JENKINS_URL="${JENKINS_URL:?"URL of a jenkins server must be provided"}"
82
83-# Name of agent configuration to use at JENKINS_URL
84-# Override if it need to be something other than the
85-# hostname of the server the agent is running on.
86-typeset JENKINS_HOSTNAME="${JENKINS_HOSTNAME:-$(hostname)}"
87-
88-
89 typeset JENKINS_WORKDIR="/var/lib/jenkins"
90
91 # Arguments to pass to jenkins agent on startup
92 typeset -a JENKINS_ARGS
93
94-# JENKINS_ARGS+=(-jnlpUrl "${JENKINS_URL}"/computer/"${JENKINS_HOSTNAME}"/slave-agent.jnlp)
95-# JENKINS_ARGS+=(-jnlpCredentials "${JENKINS_API_USER:?Please specify JENKINS_API_USER}:${JENKINS_API_TOKEN:?Please specify JENKINS_API_TOKEN}")
96-# JENKINS_ARGS+=(-noReconect)
97-
98 # Path of the agent.jar
99 typeset AGENT_JAR=/var/lib/jenkins/agent.jar
100
101@@ -60,9 +50,6 @@ download_agent
102 # Specify the pod as ready
103 touch /var/lib/jenkins/agents/.ready
104
105-#shellcheck disable=SC2086
106-# "${JAVA}" ${JAVA_ARGS} -jar "${AGENT_JAR}" "${JENKINS_ARGS[@]}"
107-
108 # Transform the env variables in arrays to iterate through it
109 IFS=':' read -r -a AGENTS <<< ${JENKINS_AGENTS}
110 IFS=':' read -r -a TOKENS <<< ${JENKINS_TOKENS}
111@@ -70,12 +57,6 @@ IFS=':' read -r -a TOKENS <<< ${JENKINS_TOKENS}
112 echo ${!AGENTS[@]}
113
114 for index in ${!AGENTS[@]}; do
115- echo "agent : ${AGENTS[$index]}"
116- echo "value: ${TOKENS[$index]}"
117- echo "${JAVA}" "${JAVA_ARGS}" -jar "${AGENT_JAR}" -jnlpUrl "${JENKINS_URL}"/computer/"${AGENTS[$index]}"/slave-agent.jnlp -workDir "${JENKINS_WORKDIR}" -noReconnect -secret "${TOKENS[$index]}"
118- ${JAVA} ${JAVA_ARGS} -jar ${AGENT_JAR} -jnlpUrl ${JENKINS_URL}/computer/${AGENTS[$index]}/slave-agent.jnlp -workDir ${JENKINS_WORKDIR} -noReconnect -secret ${TOKENS[$index]} || echo "Invalid or already used credentials." || True
119- # ${JAVA} ${JAVA_ARGS} -jar ${AGENT_JAR} -jnlpUrl ${JENKINS_URL}/computer/${AGENTS[$index]}/slave-agent.jnlp -workDir ${JENKINS_WORKDIR} -noReconnect -secret ${TOKENS[$index]} || tail -f /dev/null
120+ echo "About to run ${JAVA}" "${JAVA_ARGS}" -jar "${AGENT_JAR}" -jnlpUrl "${JENKINS_URL}"/computer/"${AGENTS[$index]}"/slave-agent.jnlp -workDir "${JENKINS_WORKDIR}" -noReconnect -secret "${TOKENS[$index]}"
121+ ${JAVA} ${JAVA_ARGS} -jar ${AGENT_JAR} -jnlpUrl ${JENKINS_URL}/computer/${AGENTS[$index]}/slave-agent.jnlp -workDir ${JENKINS_WORKDIR} -noReconnect -secret ${TOKENS[$index]} || echo "Invalid or already used credentials."
122 done
123-echo "Tail End"
124-tail -f /dev/null
125-echo "Tail After End"
126\ No newline at end of file
127diff --git a/src/charm.py b/src/charm.py
128index ea6d681..212e5b3 100755
129--- a/src/charm.py
130+++ b/src/charm.py
131@@ -4,15 +4,12 @@
132 # Licensed under the GPLv3, see LICENCE file for details.
133
134 import io
135-import pprint
136-import os
137-import sys
138 import logging
139-
140-sys.path.append('lib') # noqa: E402
141+import os
142+import pprint
143
144 from ops.charm import CharmBase
145-from ops.framework import StoredState, EventSource, EventBase
146+from ops.framework import StoredState
147 from ops.main import main
148 from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
149
150@@ -23,25 +20,17 @@ logger = logging.getLogger()
151 class JenkinsAgentCharm(CharmBase):
152 _stored = StoredState()
153
154- # on_slave_relation_configured = EventSource(SlaveRelationConfigureEvent)
155+ def __init__(self, *args):
156+ super().__init__(*args)
157
158- def __init__(self, framework, parent):
159- super().__init__(framework, parent)
160-
161- framework.observe(self.on.start, self.configure_pod)
162- framework.observe(self.on.config_changed, self.configure_pod)
163- framework.observe(self.on.upgrade_charm, self.configure_pod)
164- framework.observe(self.on.slave_relation_joined, self.on_slave_relation_joined)
165- framework.observe(self.on.slave_relation_changed, self.on_slave_relation_changed)
166+ self.framework.observe(self.on.start, self.configure_pod)
167+ self.framework.observe(self.on.config_changed, self.configure_pod)
168+ self.framework.observe(self.on.upgrade_charm, self.configure_pod)
169+ self.framework.observe(self.on.slave_relation_joined, self.on_agent_relation_joined)
170+ self.framework.observe(self.on.slave_relation_changed, self.on_agent_relation_changed)
171
172 self._stored.set_default(_spec=None, jenkins_url=None, agent_tokens=None, agents=None)
173
174- def on_upgrade_charm(self, event):
175- pass
176-
177- def on_config_changed(self, event):
178- pass
179-
180 def generate_pod_config(self, config, secured=True):
181 """Kubernetes pod config generator.
182
183@@ -51,40 +40,29 @@ class JenkinsAgentCharm(CharmBase):
184 """
185 pod_config = {}
186
187- pod_config["JENKINS_API_USER"] = config["jenkins_user"]
188-
189 if self._stored.jenkins_url:
190 pod_config["JENKINS_URL"] = self._stored.jenkins_url
191- elif config.get("jenkins_master_url", None):
192+ else:
193 pod_config["JENKINS_URL"] = config["jenkins_master_url"]
194- if config.get("jenkins_agent_name", None):
195- pod_config["JENKINS_HOSTNAME"] = config["jenkins_agent_name"]
196
197 if secured:
198 return pod_config
199
200- # pod_config["JENKINS_API_TOKEN"] = self._stored.agent_tokens or config["jenkins_api_token"]
201 if self._stored.agent_tokens and self._stored.agents:
202- # for agent in self._stored.agents:
203- # logger.info("ALEJDG - generate_pod_config - self._stored.agent: %s", agent)
204- logger.info("ALEJDG - generate_pod_config - self._stored.agent: %s", self._stored.agents)
205- # pod_config["JENKINS_AGENTS"] = ":".join(self._stored.agents)
206 pod_config["JENKINS_AGENTS"] = ":".join(self._stored.agents)
207 pod_config["JENKINS_TOKENS"] = ":".join(self._stored.agent_tokens)
208 else:
209- pod_config["JENKINS_TOKENS"] = config["jenkins_api_token"]
210+ pod_config["JENKINS_AGENTS"] = config["jenkins_agent_name"]
211+ pod_config["JENKINS_TOKENS"] = config["jenkins_agent_token"]
212
213 return pod_config
214
215 def configure_pod(self, event):
216+ """Assemble the pod spec and apply it, if possible."""
217 is_valid = self.is_valid_config()
218 if not is_valid:
219 return
220
221- if not self.unit.is_leader():
222- self.unit.status = ActiveStatus()
223- return
224-
225 spec = self.make_pod_spec()
226 if spec != self._stored._spec:
227 self._stored._spec = spec
228@@ -107,8 +85,8 @@ class JenkinsAgentCharm(CharmBase):
229 self.model.unit.status = ActiveStatus()
230
231 def make_pod_spec(self):
232+ """Prepare and return a pod spec."""
233 config = self.model.config
234- logger.info("ALEJDG - config type: %s - config data: %s", type(config), config)
235
236 full_pod_config = self.generate_pod_config(config, secured=False)
237 secure_pod_config = self.generate_pod_config(config, secured=True)
238@@ -125,23 +103,25 @@ class JenkinsAgentCharm(CharmBase):
239 }
240
241 out = io.StringIO()
242+ pprint.pprint(spec, out)
243 logger.info("This is the Kubernetes Pod spec config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue()))
244
245 secure_pod_config.update(full_pod_config)
246- pprint.pprint(spec, out)
247- logger.info("This is the Kubernetes Pod spec config (with secrets) <<EOM\n{}\nEOM".format(out.getvalue()))
248-
249 return spec
250
251 def is_valid_config(self):
252+ """Validate required configuration.
253+
254+ When not configuring the agent through relations
255+ 'jenkins_master_url', 'jenkins_agent_name' and 'jenkins_agent_token'
256+ are required."""
257 is_valid = True
258
259 config = self.model.config
260- logger.info("ALEJDG SPEC: %s", self._stored._spec)
261 if self._stored.agent_tokens:
262- want = ("image", "jenkins_user")
263+ want = ("image",)
264 else:
265- want = ("image", "jenkins_user", "jenkins_api_token")
266+ want = ("image", "jenkins_master_url", "jenkins_agent_name", "jenkins_agent_token")
267 missing = [k for k in want if config[k].rstrip() == ""]
268 if missing:
269 message = "Missing required config: {}".format(" ".join(missing))
270@@ -151,82 +131,45 @@ class JenkinsAgentCharm(CharmBase):
271
272 return is_valid
273
274- def on_slave_relation_joined(self, event):
275+ def on_agent_relation_joined(self, event):
276 logger.info("Jenkins relation joined")
277- noexecutors = os.cpu_count()
278- config_labels = self.model.config.get('labels')
279- agent_name = ""
280- if self._stored.agents:
281- self._stored.agents[-1]
282- name, number = self._stored.agents[-1].rsplit('-', 1)
283- agent_name = "{}-{}".format(name, int(number) + 1)
284- else:
285- self._stored.agents = []
286- agent_name = self.unit.name.replace('/', '-')
287+ num_executors = os.cpu_count()
288+ config_labels = self.model.config.get('jenkins_agent_labels')
289+ agent_name = self._gen_agent_name()
290
291 if config_labels:
292 labels = config_labels
293 else:
294- labels = os.uname()[4]
295-
296- # slave_address = hookenv.unit_private_ip()
297+ print(os.uname())
298+ labels = os.uname().machine
299
300- logger.info("noexecutors: %s - type: %s", noexecutors, type(noexecutors))
301- logger.info("labels: %s - type: %s",labels, type(labels))
302- event.relation.data[self.model.unit]["executors"] = str(noexecutors)
303+ event.relation.data[self.model.unit]["executors"] = str(num_executors)
304 event.relation.data[self.model.unit]["labels"] = labels
305 event.relation.data[self.model.unit]["slavehost"] = agent_name
306
307- remote_data = event.relation.data[event.app]
308- logger.info("ALEJDG - remote_data_app: %s", remote_data)
309- for i in remote_data:
310- logger.info("ALEJDG - remote_data_app['%s']: %s", i, remote_data[i])
311-
312- if event.unit is not None:
313- remote_data = event.relation.data[event.unit]
314- logger.info("ALEJDG - os.environ: %s", os.environ)
315-
316- logger.info("ALEJDG - remote_data_post_app: %s", remote_data)
317- for i in remote_data:
318- logger.info("ALEJDG - remote_data_post_app['%s']: %s", i, remote_data[i])
319-
320- def on_slave_relation_changed(self, event):
321+ def on_agent_relation_changed(self, event):
322+ """Populate local configuration with data from relation"""
323 logger.info("Jenkins relation changed")
324 try:
325- logger.info("ALEJDG - event.relation.data[event.unit]['url']: %s", event.relation.data[event.unit]['url'])
326 self._stored.jenkins_url = event.relation.data[event.unit]['url']
327 except KeyError:
328 pass
329
330 try:
331- logger.info("ALEJDG - event.relation.data[event.unit]['secret']: %s", event.relation.data[event.unit]['secret'])
332- logger.info("ALEJDG - event.unit.name: %s", event.unit.name)
333- logger.info("ALEJDG - self.unit.name.: %s", self.unit.name)
334 self._stored.agent_tokens = self._stored.agent_tokens or []
335 self._stored.agent_tokens.append(event.relation.data[event.unit]['secret'])
336- agent_name = ""
337- if self._stored.agents:
338- logger.info("ALEJDG - self._stored.agents[-1]: %s", self._stored.agents[-1])
339- self._stored.agents[-1]
340- name, number = self._stored.agents[-1].rsplit('-', 1)
341- agent_name = "{}-{}".format(name, int(number) + 1)
342- self._stored.agents.append(agent_name)
343- else:
344- self._stored.agents = []
345- agent_name = self.unit.name.replace('/', '-')
346- self._stored.agents.append(agent_name)
347+ self._gen_agent_name(store=True)
348 except KeyError:
349 pass
350
351- self.configure_slave_through_relation(event)
352+ self.configure_through_relation(event)
353
354- def configure_slave_through_relation(self, event):
355- logger.info("Setting up jenkins via slave relation")
356- logger.info("ALEJDG - on_slave_relation_joined - self._stored.agents_setup: %s", self._stored.agents)
357+ def configure_through_relation(self, event):
358+ logger.info("Setting up jenkins via agent relation")
359 self.model.unit.status = MaintenanceStatus("Configuring jenkins agent")
360
361- if self.model.config.get("url"):
362- logger.info("Config option 'url' is set. Can't use agent relation.")
363+ if self.model.config.get("jenkins_master_url"):
364+ logger.info("Config option 'jenkins_master_url' is set. Can't use agent relation.")
365 self.model.unit.status = ActiveStatus()
366 return
367
368@@ -242,6 +185,20 @@ class JenkinsAgentCharm(CharmBase):
369
370 self.configure_pod(event)
371
372+ def _gen_agent_name(self, store=False):
373+ agent_name = ""
374+ if self._stored.agents:
375+ name, number = self._stored.agents[-1].rsplit('-', 1)
376+ agent_name = "{}-{}".format(name, int(number) + 1)
377+ if store:
378+ self._stored.agents.append(agent_name)
379+ else:
380+ agent_name = self.unit.name.replace('/', '-')
381+ if store:
382+ self._stored.agents = []
383+ self._stored.agents.append(agent_name)
384+ return agent_name
385+
386
387 if __name__ == '__main__':
388 main(JenkinsAgentCharm)
389diff --git a/src/interface_jenkins_slave.py b/src/interface_jenkins_slave.py
390deleted file mode 100644
391index dec278d..0000000
392--- a/src/interface_jenkins_slave.py
393+++ /dev/null
394@@ -1,66 +0,0 @@
395-import json
396-
397-from ops.framework import EventBase, EventsBase, EventSource, Object, StoredState
398-
399-
400-class NewClient(EventBase):
401- def __init__(self, handle, client):
402- super().__init__(handle)
403- self.client = client
404-
405- def snapshot(self):
406- return {
407- 'relation_name': self.client._relation.name,
408- 'relation_id': self.client._relation.id,
409- }
410-
411- def restore(self, snapshot):
412- relation = self.model.get_relation(snapshot['relation_name'], snapshot['relation_id'])
413- self.client = HTTPInterfaceClient(relation, self.model.unit)
414-
415-
416-class HTTPServerEvents(EventsBase):
417- new_client = EventSource(NewClient)
418-
419-
420-class HTTPServer(Object):
421- on = HTTPServerEvents()
422- state = StoredState()
423-
424- def __init__(self, charm, relation_name):
425- super().__init__(charm, relation_name)
426- self.relation_name = relation_name
427- self.framework.observe(charm.on.start, self.init_state)
428- self.framework.observe(charm.on[relation_name].relation_joined, self.on_joined)
429- self.framework.observe(charm.on[relation_name].relation_departed, self.on_departed)
430-
431- def init_state(self, event):
432- self.state.apps = []
433-
434- @property
435- def _relations(self):
436- return self.model.relations[self.relation_name]
437-
438- def on_joined(self, event):
439- if event.app not in self.state.apps:
440- self.state.apps.append(event.app)
441- self.on.new_client.emit(HTTPInterfaceClient(event.relation, self.model.unit))
442-
443- def on_departed(self, event):
444- self.state.apps = [app for app in self._relations]
445-
446- def clients(self):
447- return [HTTPInterfaceClient(relation, self.model.unit) for relation in self._relations]
448-
449-
450-class HTTPInterfaceClient:
451- def __init__(self, relation, local_unit):
452- self._relation = relation
453- self._local_unit = local_unit
454- self.ingress_address = relation.data[local_unit]['ingress-address']
455-
456- def serve(self, hosts, port):
457- self._relation.data[self._local_unit]['extended_data'] = json.dumps([{
458- 'hostname': host,
459- 'port': port,
460- } for host in hosts])
461\ No newline at end of file
462diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
463index 65705e7..b447a9a 100644
464--- a/tests/unit/test_charm.py
465+++ b/tests/unit/test_charm.py
466@@ -1,21 +1,315 @@
467 # Copyright 2020 Canonical Ltd.
468 # Licensed under the GPLv3, see LICENCE file for details.
469
470+from mock import MagicMock, patch
471 import unittest
472
473+from ops import testing
474+from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
475+
476+
477 import sys
478
479 sys.path.append('src') # noqa: E402
480
481-from charm import generate_pod_config
482+from charm import JenkinsAgentCharm
483+
484+CONFIG_DEFAULT = {
485+ "image": "jenkins-agent-operator",
486+ "jenkins_master_url": "",
487+ "jenkins_agent_name": "",
488+ "jenkins_agent_token": "",
489+ "jenkins_agent_label": ""
490+}
491+
492+CONFIG_ONE_AGENT = {
493+ "jenkins_master_url": "http://test",
494+ "jenkins_agent_name": "agent-one",
495+ "jenkins_agent_token": "token-one"
496+}
497+
498+CONFIG_ONE_AGENT_CUSTOM_IMAGE = {
499+ "image": "image-name",
500+ "jenkins_master_url": "http://test",
501+ "jenkins_agent_name": "agent-one",
502+ "jenkins_agent_token": "token-one"
503+}
504+
505+SPEC_EXPECTED = {
506+ 'containers': [{
507+ 'config': {
508+ 'JENKINS_AGENTS': 'agent-one',
509+ 'JENKINS_TOKENS': 'token-one',
510+ 'JENKINS_URL': 'http://test'
511+ },
512+ 'imageDetails': {
513+ 'imagePath': 'image-name'
514+ },
515+ 'name': 'jenkins-agent',
516+ 'readinessProbe': {
517+ 'exec': {
518+ 'command': [
519+ '/bin/cat',
520+ '/var/lib/jenkins/agents/.ready'
521+ ]
522+ }
523+ }
524+ }]
525+}
526
527
528 class TestJenkinsAgentCharm(unittest.TestCase):
529- def test_generate_pod_config(self):
530- config = {
531- "jenkins_user": "test",
532+ def setUp(self):
533+ self.harness = testing.Harness(JenkinsAgentCharm)
534+ self.harness.begin()
535+ self.harness.disable_hooks()
536+ self.harness.update_config(CONFIG_DEFAULT)
537+
538+ def test__generate_pod_config(self):
539+ """Test generate_pod_config"""
540+ expected = {
541+ "JENKINS_URL": "http://test",
542 }
543+ self.assertEqual(self.harness.charm.generate_pod_config(CONFIG_ONE_AGENT, secured=True), expected)
544+
545+ expected["JENKINS_AGENTS"] = "agent-one"
546+ expected["JENKINS_TOKENS"] = "token-one"
547+ self.assertEqual(self.harness.charm.generate_pod_config(CONFIG_ONE_AGENT, secured=False), expected)
548+
549+ def test__generate_pod_config__with__relation__data(self):
550+ """Test generate_pod_config with relation data"""
551 expected = {
552- "JENKINS_API_USER": "test",
553+ "JENKINS_URL": "http://test",
554+ }
555+ agent_name = "jenkins-agent-0"
556+ url = "http://test"
557+ token = "token"
558+
559+ self.harness.charm._stored.jenkins_url = url
560+ self.harness.charm._stored.agent_tokens = [token]
561+ self.harness.charm._stored.agents = [agent_name]
562+
563+ self.assertEqual(self.harness.charm.generate_pod_config(CONFIG_ONE_AGENT, secured=True), expected)
564+
565+ expected["JENKINS_AGENTS"] = "jenkins-agent-0"
566+ expected["JENKINS_TOKENS"] = "token"
567+ self.assertEqual(self.harness.charm.generate_pod_config(CONFIG_ONE_AGENT, secured=False), expected)
568+
569+ def test__configure_pod__invalid__config(self):
570+ """Test configure_pod when the config is invalid"""
571+ self.harness.charm.on.config_changed.emit()
572+ message = "Missing required config: jenkins_master_url jenkins_agent_name jenkins_agent_token"
573+ self.assertEqual(self.harness.model.unit.status, BlockedStatus(message))
574+ self.assertEqual(self.harness.get_pod_spec(), None)
575+
576+ def test__configure_pod__unit__not__leader(self):
577+ """Test configure_pod when the unit isn't leader"""
578+ self.harness.set_leader(is_leader=False)
579+ self.harness.update_config(CONFIG_ONE_AGENT)
580+ with self.assertLogs(level='INFO') as logger:
581+ self.harness.charm.on.config_changed.emit()
582+ self.assertEqual(self.harness.model.unit.status, ActiveStatus())
583+ message = "INFO:root:Spec changes ignored by non-leader"
584+ self.assertEqual(logger.output[-1], message)
585+ self.assertEqual(self.harness.get_pod_spec(), None)
586+
587+ def test__configure_pod(self):
588+ """Test configure_pod"""
589+ expected = (SPEC_EXPECTED, None)
590+
591+ self.harness.set_leader(is_leader=True)
592+ self.harness.update_config(CONFIG_ONE_AGENT_CUSTOM_IMAGE)
593+ self.harness.charm.on.config_changed.emit()
594+ self.assertEqual(self.harness.model.unit.status, ActiveStatus())
595+ self.assertEqual(self.harness.get_pod_spec(), expected)
596+
597+ def test__configure_pod__no__spec_change(self):
598+ """Test configure_pod when there is no change in the spec"""
599+ self.harness.set_leader(is_leader=True)
600+ self.harness.update_config(CONFIG_ONE_AGENT_CUSTOM_IMAGE)
601+ self.harness.charm._stored._spec = self.harness.charm.make_pod_spec()
602+ with self.assertLogs(level='INFO') as logger:
603+ self.harness.charm.on.config_changed.emit()
604+ self.assertEqual(self.harness.model.unit.status, ActiveStatus())
605+ message = "INFO:root:Pod spec unchanged"
606+ self.assertEqual(logger.output[-1], message)
607+ self.assertEqual(self.harness.get_pod_spec(), None)
608+
609+ def test__make_pod_spec(self):
610+ """Test the construction of the spec based on juju config"""
611+ self.harness.update_config(CONFIG_ONE_AGENT_CUSTOM_IMAGE)
612+ self.assertEqual(self.harness.charm.make_pod_spec(), SPEC_EXPECTED)
613+
614+ def test__is_valid_config(self):
615+ """Test config validation"""
616+ config = {
617+ "image": "image-name",
618+ "jenkins_master_url": "http://test",
619+ "jenkins_agent_name": "agent-one",
620+ "jenkins_agent_token": "token-one"
621 }
622- self.assertEqual(generate_pod_config(config), expected)
623+ self.assertEqual(self.harness.charm.is_valid_config(), False)
624+ with self.subTest("Config from relation"):
625+ self.harness.charm._stored.agent_tokens = "token"
626+ self.assertEqual(self.harness.charm.is_valid_config(), True)
627+ with self.subTest("Config from juju config"):
628+ self.harness.update_config(config)
629+ self.assertEqual(self.harness.charm.is_valid_config(), True)
630+
631+ @patch("os.uname")
632+ @patch("os.cpu_count")
633+ def test__on_agent_relation_joined(self, mock_os_cpu_count, mock_os_uname):
634+ """Test relation_data is set when a new relation joins"""
635+ mock_os_cpu_count.return_value = 8
636+ mock_os_uname.return_value.machine = "x86_64"
637+ expected_relation_data = {
638+ 'executors': '8',
639+ 'labels': 'x86_64',
640+ 'slavehost': 'jenkins-agent-0'
641+ }
642+ self.harness.enable_hooks()
643+ rel_id = self.harness.add_relation("slave", "jenkins")
644+ self.harness.add_relation_unit(rel_id, "jenkins/0")
645+
646+ self.assertEqual(self.harness.get_relation_data(rel_id, "jenkins-agent/0"), expected_relation_data)
647+
648+ @patch("charm.JenkinsAgentCharm.configure_pod")
649+ def test__configure_through_relation__no__jenkins_master_url(self, mock_configure_pod):
650+ """Test configure_through_relation when no configuration has been provided"""
651+ mock_event = MagicMock()
652+ with self.assertLogs(level='INFO') as logger:
653+ self.harness.charm.configure_through_relation(mock_event)
654+ expected_output = [
655+ "INFO:root:Setting up jenkins via agent relation",
656+ "INFO:root:Jenkins hasn't exported its url yet. Skipping setup for now."
657+ ]
658+ self.assertEqual(logger.output, expected_output)
659+ mock_configure_pod.assert_not_called()
660+ self.assertEqual(self.harness.model.unit.status, ActiveStatus())
661+
662+ @patch("charm.JenkinsAgentCharm.configure_pod")
663+ def test__configure_through_relation__no__jenkins_agent_tokens(self, mock_configure_pod):
664+ """Test configure_through_relation when no tokens have been provided"""
665+ mock_event = MagicMock()
666+ self.harness.charm._stored.jenkins_url = "http://test"
667+ with self.assertLogs(level='INFO') as logger:
668+ self.harness.charm.configure_through_relation(mock_event)
669+ expected_output = [
670+ "INFO:root:Setting up jenkins via agent relation",
671+ "INFO:root:Jenkins hasn't exported the agent secret yet. Skipping setup for now."
672+ ]
673+ self.assertEqual(logger.output, expected_output)
674+ mock_configure_pod.assert_not_called()
675+ self.assertEqual(self.harness.model.unit.status, ActiveStatus())
676+
677+ @patch("charm.JenkinsAgentCharm.configure_pod")
678+ def test__configure_through_relation__manual__config(self, mock_configure_pod):
679+ """Test configure_through_relation when no configuration has been provided"""
680+ mock_event = MagicMock()
681+ self.harness.update_config({"jenkins_master_url": "http://test"})
682+ with self.assertLogs(level='INFO') as logger:
683+ self.harness.charm.configure_through_relation(mock_event)
684+ expected_output = [
685+ "INFO:root:Setting up jenkins via agent relation",
686+ "INFO:root:Config option 'jenkins_master_url' is set. Can't use agent relation."
687+ ]
688+ self.assertEqual(logger.output, expected_output)
689+ mock_configure_pod.assert_not_called()
690+ self.assertEqual(self.harness.model.unit.status, ActiveStatus())
691+
692+ @patch("charm.JenkinsAgentCharm.configure_pod")
693+ def test__configure_through_relation(self, mock_configure_pod):
694+ """Test configure_through_relation when the relation have provided all needed data"""
695+ mock_event = MagicMock()
696+ self.harness.charm._stored.jenkins_url = "http://test"
697+ self.harness.charm._stored.agent_tokens = "token"
698+ with self.assertLogs(level='INFO') as logger:
699+ self.harness.charm.configure_through_relation(mock_event)
700+ expected_output = [
701+ "INFO:root:Setting up jenkins via agent relation"
702+ ]
703+ self.assertEqual(logger.output, expected_output)
704+ mock_configure_pod.assert_called()
705+ self.assertEqual(self.harness.model.unit.status, MaintenanceStatus("Configuring jenkins agent"))
706+
707+ @patch("os.uname")
708+ @patch("os.cpu_count")
709+ def test__on_agent_relation_joined__custom__label(self, mock_os_cpu_count, mock_os_uname):
710+ """Test relation_data is set when a new relation joins
711+ and custom labels are set"""
712+ mock_os_cpu_count.return_value = 8
713+ mock_os_uname.return_value.machine = "x86_64"
714+ labels = "test, label"
715+ expected_relation_data = {
716+ 'executors': '8',
717+ 'labels': labels,
718+ 'slavehost': 'jenkins-agent-0'
719+ }
720+ self.harness.update_config({"jenkins_agent_labels": labels})
721+ self.harness.enable_hooks()
722+ rel_id = self.harness.add_relation("slave", "jenkins")
723+ self.harness.add_relation_unit(rel_id, "jenkins/0")
724+ self.assertEqual(self.harness.get_relation_data(rel_id, "jenkins-agent/0"), expected_relation_data)
725+
726+ @patch("charm.JenkinsAgentCharm.configure_through_relation")
727+ @patch("os.uname")
728+ @patch("os.cpu_count")
729+ def test__on_agent_relation_changed__noop(self, mock_os_cpu_count, mock_os_uname, mock_configure_through_relation):
730+ """Test on_agent_relation_changed when jenkins hasn't provided information yet"""
731+ mock_os_cpu_count.return_value = 8
732+ mock_os_uname.return_value.machine = "x86_64"
733+ remote_unit = "jenkins/0"
734+ self.harness.enable_hooks()
735+ rel_id = self.harness.add_relation("slave", "jenkins")
736+ self.harness.add_relation_unit(rel_id, remote_unit)
737+ self.harness.update_relation_data(rel_id, remote_unit, {})
738+
739+ mock_configure_through_relation.assert_called()
740+
741+ @patch("charm.JenkinsAgentCharm.configure_through_relation")
742+ @patch("os.uname")
743+ @patch("os.cpu_count")
744+ def test__on_agent_relation_changed_old(self, mock_os_cpu_count, mock_os_uname, mock_configure_through_relation):
745+ """Test relation_data is set when a new relation joins"""
746+ mock_os_cpu_count.return_value = 8
747+ mock_os_uname.return_value.machine = "x86_64"
748+ remote_unit = "jenkins/0"
749+ agent_name = "jenkins-agent-0"
750+ url = "http://test"
751+ secret = "token"
752+ self.harness.enable_hooks()
753+ rel_id = self.harness.add_relation("slave", "jenkins")
754+ self.harness.add_relation_unit(rel_id, remote_unit)
755+ self.harness.update_relation_data(rel_id, remote_unit, {"url": url, "secret": secret})
756+ self.assertEqual(self.harness.charm._stored.jenkins_url, url)
757+ self.assertEqual(self.harness.charm._stored.agent_tokens[-1], secret)
758+ self.assertEqual(self.harness.charm._stored.agents[-1], agent_name)
759+ mock_configure_through_relation.assert_called()
760+
761+ @patch("charm.JenkinsAgentCharm.configure_through_relation")
762+ @patch("os.uname")
763+ @patch("os.cpu_count")
764+ def test__on_agent_relation_changed__multiple__agents(
765+ self,
766+ mock_os_cpu_count,
767+ mock_os_uname,
768+ mock_configure_through_relation
769+ ):
770+ """Test relation_data is set when a new relation joins"""
771+ mock_os_cpu_count.return_value = 8
772+ mock_os_uname.return_value.machine = "x86_64"
773+ remote_unit = "jenkins/0"
774+ agent_name = "jenkins-agent-0"
775+ expected_new_agent = "jenkins-agent-1"
776+ url = "http://test"
777+ secret = "token"
778+
779+ self.harness.charm._stored.agents = [agent_name]
780+ self.harness.enable_hooks()
781+ rel_id = self.harness.add_relation("slave", "jenkins")
782+ self.harness.add_relation_unit(rel_id, remote_unit)
783+ self.harness.update_relation_data(rel_id, remote_unit, {"url": url, "secret": secret})
784+ self.assertEqual(self.harness.charm._stored.jenkins_url, url)
785+ self.assertEqual(self.harness.charm._stored.agent_tokens[-1], secret)
786+ self.assertEqual(self.harness.charm._stored.agents[-1], expected_new_agent)
787+ mock_configure_through_relation.assert_called()

Subscribers

People subscribed via source and target branches

to all changes: