Merge charm-k8s-jenkins-agent:relations-config into charm-k8s-jenkins-agent:master
- Git
- lp:charm-k8s-jenkins-agent
- relations-config
- Merge into 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) |
Related bugs: |
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
Description of the change
To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
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
1 | diff --git a/.gitignore b/.gitignore |
2 | index 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/ |
10 | diff --git a/config.yaml b/config.yaml |
11 | index 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' |
63 | diff --git a/dockerfile/Dockerfile b/dockerfile/Dockerfile |
64 | index 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 | |
75 | diff --git a/dockerfile/files/entrypoint.sh b/dockerfile/files/entrypoint.sh |
76 | index 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 |
127 | diff --git a/src/charm.py b/src/charm.py |
128 | index 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) |
389 | diff --git a/src/interface_jenkins_slave.py b/src/interface_jenkins_slave.py |
390 | deleted file mode 100644 |
391 | index 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 |
462 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py |
463 | index 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() |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.