Merge charm-k8s-jenkins-agent:manual-agent into charm-k8s-jenkins-agent:master
- Git
- lp:charm-k8s-jenkins-agent
- manual-agent
- Merge into master
Status: | Rejected |
---|---|
Rejected by: | Alexandre Gomes |
Proposed branch: | charm-k8s-jenkins-agent:manual-agent |
Merge into: | charm-k8s-jenkins-agent:master |
Diff against target: |
437 lines (+258/-39) 6 files modified
Makefile (+7/-0) dockerfile/Dockerfile (+2/-1) dockerfile/files/entrypoint.sh (+25/-3) metadata.yaml (+3/-0) src/charm.py (+155/-35) src/interface_jenkins_slave.py (+66/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alexandre Gomes | Needs Resubmitting | ||
Tom Haddon | Needs Fixing | ||
Canonical IS Reviewers | Pending | ||
Review via email: mp+389041@code.launchpad.net |
Commit message
Description of the change
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Unable to determine commit message from repository - please click "Set commit message" and enter the commit message manually.
Tom Haddon (mthaddon) wrote : | # |
We're missing a commit message for this MP, so it wouldn't let us merge yet.
I've included some comments inline, but also I had to make the following changes to get lint and tests to run (`make test`) https:/
Alexandre Gomes (alejdg) wrote : | # |
> We're missing a commit message for this MP, so it wouldn't let us merge yet.
>
> I've included some comments inline, but also I had to make the following
> changes to get lint and tests to run (`make test`)
> https:/
I addressed all the comments but two:
- The term "master" isn't going away as per their blog post[1], only "slave". So I think we should follow the same terms Jenkins devs are using.
- The interface is still called jenkins-slave, so we should keep that until we update the interface.
Alexandre Gomes (alejdg) wrote : | # |
All changes in this MP can be found in https:/
Alexandre Gomes (alejdg) wrote : | # |
I forgot to provide the link to the blog post:
https:/
Unmerged commits
- 9ba35a0... by Alexandre Gomes
-
Improve the charm so it can work properly with juju config and add unit
- 79d2ac2... by Alexandre Gomes
-
Fix redeployment issues and make the charm not scalable
- 0d8faa5... by Alexandre Gomes
-
Save agent's secret in a dictionary instead
- 1ca7e4b... by Alexandre Gomes
-
Address comments and make use of the secret provided by jenkins through relation
- 8e2d166... by Alexandre Gomes
-
Fix logging
- 4f4c6cb... by Alexandre Gomes
-
Add slave_relation_
joined - 0eea73b... by Alexandre Gomes
-
Create on_jenkins_
relation_ joined event - dcfe2f6... by Alexandre Gomes
-
Add initial jenkins-slave relation
Preview Diff
1 | diff --git a/Makefile b/Makefile |
2 | index 86bd8d3..8c8056a 100644 |
3 | --- a/Makefile |
4 | +++ b/Makefile |
5 | @@ -18,6 +18,13 @@ build-image: | $(dockerfile_dir) |
6 | --build-arg REVISION="$(revision)" \ |
7 | -t $(JENKINS_IMAGE) $(dockerfile_dir) |
8 | |
9 | +build-image-no-cache: | $(dockerfile_dir) |
10 | + docker build \ |
11 | + --build-arg AUTHOR=$(author) \ |
12 | + --build-arg REVISION="$(revision)" \ |
13 | + --no-cache \ |
14 | + -t $(JENKINS_IMAGE) $(dockerfile_dir) |
15 | + |
16 | build-release-image: | $(dockerfile_dir) |
17 | docker build \ |
18 | --build-arg DATE_CREATED="$(date_created)" \ |
19 | diff --git a/dockerfile/Dockerfile b/dockerfile/Dockerfile |
20 | index 1abd7e6..e03339f 100644 |
21 | --- a/dockerfile/Dockerfile |
22 | +++ b/dockerfile/Dockerfile |
23 | @@ -47,7 +47,8 @@ RUN apt-get update -y \ |
24 | |
25 | WORKDIR /var/lib/jenkins |
26 | |
27 | -USER ${USER} |
28 | +#USER ${USER} |
29 | +USER root |
30 | |
31 | COPY files/entrypoint.sh / |
32 | |
33 | diff --git a/dockerfile/files/entrypoint.sh b/dockerfile/files/entrypoint.sh |
34 | index c4e879c..c8ece01 100755 |
35 | --- a/dockerfile/files/entrypoint.sh |
36 | +++ b/dockerfile/files/entrypoint.sh |
37 | @@ -3,6 +3,7 @@ |
38 | set -eu -o pipefail |
39 | |
40 | export LC_ALL=C |
41 | +export TERM=xterm |
42 | |
43 | # defaults for jenkins-agent component of the jenkins continuous integration |
44 | # system |
45 | @@ -25,11 +26,15 @@ typeset JENKINS_URL="${JENKINS_URL:?"URL of a jenkins server must be provided"}" |
46 | # hostname of the server the agent is running on. |
47 | typeset JENKINS_HOSTNAME="${JENKINS_HOSTNAME:-$(hostname)}" |
48 | |
49 | + |
50 | +typeset JENKINS_WORKDIR="/var/lib/jenkins" |
51 | + |
52 | # Arguments to pass to jenkins agent on startup |
53 | typeset -a JENKINS_ARGS |
54 | |
55 | -JENKINS_ARGS+=(-jnlpUrl "${JENKINS_URL}"/computer/"${JENKINS_HOSTNAME}"/slave-agent.jnlp) |
56 | -JENKINS_ARGS+=(-jnlpCredentials "${JENKINS_API_USER:?Please specify JENKINS_API_USER}:${JENKINS_API_TOKEN:?Please specify JENKINS_API_TOKEN}") |
57 | +# JENKINS_ARGS+=(-jnlpUrl "${JENKINS_URL}"/computer/"${JENKINS_HOSTNAME}"/slave-agent.jnlp) |
58 | +# JENKINS_ARGS+=(-jnlpCredentials "${JENKINS_API_USER:?Please specify JENKINS_API_USER}:${JENKINS_API_TOKEN:?Please specify JENKINS_API_TOKEN}") |
59 | +# JENKINS_ARGS+=(-noReconect) |
60 | |
61 | # Path of the agent.jar |
62 | typeset AGENT_JAR=/var/lib/jenkins/agent.jar |
63 | @@ -56,4 +61,21 @@ download_agent |
64 | touch /var/lib/jenkins/agents/.ready |
65 | |
66 | #shellcheck disable=SC2086 |
67 | -"${JAVA}" ${JAVA_ARGS} -jar "${AGENT_JAR}" "${JENKINS_ARGS[@]}" |
68 | +# "${JAVA}" ${JAVA_ARGS} -jar "${AGENT_JAR}" "${JENKINS_ARGS[@]}" |
69 | + |
70 | +# Transform the env variables in arrays to iterate through it |
71 | +IFS=':' read -r -a AGENTS <<< ${JENKINS_AGENTS} |
72 | +IFS=':' read -r -a TOKENS <<< ${JENKINS_TOKENS} |
73 | + |
74 | +echo ${!AGENTS[@]} |
75 | + |
76 | +for index in ${!AGENTS[@]}; do |
77 | + echo "agent : ${AGENTS[$index]}" |
78 | + echo "value: ${TOKENS[$index]}" |
79 | + echo "${JAVA}" "${JAVA_ARGS}" -jar "${AGENT_JAR}" -jnlpUrl "${JENKINS_URL}"/computer/"${AGENTS[$index]}"/slave-agent.jnlp -workDir "${JENKINS_WORKDIR}" -noReconnect -secret "${TOKENS[$index]}" |
80 | + ${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 |
81 | + # ${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 |
82 | +done |
83 | +echo "Tail End" |
84 | +tail -f /dev/null |
85 | +echo "Tail After End" |
86 | \ No newline at end of file |
87 | diff --git a/metadata.yaml b/metadata.yaml |
88 | index b53a613..dd92ccb 100644 |
89 | --- a/metadata.yaml |
90 | +++ b/metadata.yaml |
91 | @@ -9,3 +9,6 @@ series: |
92 | - kubernetes |
93 | deployment: |
94 | service: omit |
95 | +provides: |
96 | + slave: |
97 | + interface: jenkins-slave |
98 | diff --git a/src/charm.py b/src/charm.py |
99 | index 4ba8782..ea6d681 100755 |
100 | --- a/src/charm.py |
101 | +++ b/src/charm.py |
102 | @@ -5,10 +5,14 @@ |
103 | |
104 | import io |
105 | import pprint |
106 | +import os |
107 | +import sys |
108 | import logging |
109 | |
110 | +sys.path.append('lib') # noqa: E402 |
111 | + |
112 | from ops.charm import CharmBase |
113 | -from ops.framework import StoredState |
114 | +from ops.framework import StoredState, EventSource, EventBase |
115 | from ops.main import main |
116 | from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus |
117 | |
118 | @@ -16,32 +20,10 @@ from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus |
119 | logger = logging.getLogger() |
120 | |
121 | |
122 | -def generate_pod_config(config, secured=True): |
123 | - """Kubernetes pod config generator. |
124 | - |
125 | - generate_pod_config generates Kubernetes deployment config. |
126 | - If the secured keyword is set then it will return a sanitised copy |
127 | - without exposing secrets. |
128 | - """ |
129 | - pod_config = {} |
130 | - |
131 | - pod_config["JENKINS_API_USER"] = config["jenkins_user"] |
132 | - if config.get("jenkins_master_url", None): |
133 | - pod_config["JENKINS_URL"] = config["jenkins_master_url"] |
134 | - if config.get("jenkins_agent_name", None): |
135 | - pod_config["JENKINS_HOSTNAME"] = config["jenkins_agent_name"] |
136 | - |
137 | - if secured: |
138 | - return pod_config |
139 | - |
140 | - # Add secrets from charm config |
141 | - pod_config["JENKINS_API_TOKEN"] = config["jenkins_api_token"] |
142 | - |
143 | - return pod_config |
144 | - |
145 | - |
146 | class JenkinsAgentCharm(CharmBase): |
147 | - state = StoredState() |
148 | + _stored = StoredState() |
149 | + |
150 | + # on_slave_relation_configured = EventSource(SlaveRelationConfigureEvent) |
151 | |
152 | def __init__(self, framework, parent): |
153 | super().__init__(framework, parent) |
154 | @@ -49,8 +31,10 @@ class JenkinsAgentCharm(CharmBase): |
155 | framework.observe(self.on.start, self.configure_pod) |
156 | framework.observe(self.on.config_changed, self.configure_pod) |
157 | framework.observe(self.on.upgrade_charm, self.configure_pod) |
158 | + framework.observe(self.on.slave_relation_joined, self.on_slave_relation_joined) |
159 | + framework.observe(self.on.slave_relation_changed, self.on_slave_relation_changed) |
160 | |
161 | - self.state.set_default(_spec=None) |
162 | + self._stored.set_default(_spec=None, jenkins_url=None, agent_tokens=None, agents=None) |
163 | |
164 | def on_upgrade_charm(self, event): |
165 | pass |
166 | @@ -58,14 +42,52 @@ class JenkinsAgentCharm(CharmBase): |
167 | def on_config_changed(self, event): |
168 | pass |
169 | |
170 | + def generate_pod_config(self, config, secured=True): |
171 | + """Kubernetes pod config generator. |
172 | + |
173 | + generate_pod_config generates Kubernetes deployment config. |
174 | + If the secured keyword is set then it will return a sanitised copy |
175 | + without exposing secrets. |
176 | + """ |
177 | + pod_config = {} |
178 | + |
179 | + pod_config["JENKINS_API_USER"] = config["jenkins_user"] |
180 | + |
181 | + if self._stored.jenkins_url: |
182 | + pod_config["JENKINS_URL"] = self._stored.jenkins_url |
183 | + elif config.get("jenkins_master_url", None): |
184 | + pod_config["JENKINS_URL"] = config["jenkins_master_url"] |
185 | + if config.get("jenkins_agent_name", None): |
186 | + pod_config["JENKINS_HOSTNAME"] = config["jenkins_agent_name"] |
187 | + |
188 | + if secured: |
189 | + return pod_config |
190 | + |
191 | + # pod_config["JENKINS_API_TOKEN"] = self._stored.agent_tokens or config["jenkins_api_token"] |
192 | + if self._stored.agent_tokens and self._stored.agents: |
193 | + # for agent in self._stored.agents: |
194 | + # logger.info("ALEJDG - generate_pod_config - self._stored.agent: %s", agent) |
195 | + logger.info("ALEJDG - generate_pod_config - self._stored.agent: %s", self._stored.agents) |
196 | + # pod_config["JENKINS_AGENTS"] = ":".join(self._stored.agents) |
197 | + pod_config["JENKINS_AGENTS"] = ":".join(self._stored.agents) |
198 | + pod_config["JENKINS_TOKENS"] = ":".join(self._stored.agent_tokens) |
199 | + else: |
200 | + pod_config["JENKINS_TOKENS"] = config["jenkins_api_token"] |
201 | + |
202 | + return pod_config |
203 | + |
204 | def configure_pod(self, event): |
205 | is_valid = self.is_valid_config() |
206 | if not is_valid: |
207 | return |
208 | |
209 | + if not self.unit.is_leader(): |
210 | + self.unit.status = ActiveStatus() |
211 | + return |
212 | + |
213 | spec = self.make_pod_spec() |
214 | - if spec != self.state._spec: |
215 | - self.state._spec = spec |
216 | + if spec != self._stored._spec: |
217 | + self._stored._spec = spec |
218 | # only the leader can set_spec() |
219 | if self.model.unit.is_leader(): |
220 | spec = self.make_pod_spec() |
221 | @@ -81,13 +103,15 @@ class JenkinsAgentCharm(CharmBase): |
222 | else: |
223 | logger.info("Pod spec unchanged") |
224 | |
225 | - self.state.is_started = True |
226 | + self._stored.is_started = True |
227 | self.model.unit.status = ActiveStatus() |
228 | |
229 | def make_pod_spec(self): |
230 | config = self.model.config |
231 | - full_pod_config = generate_pod_config(config, secured=False) |
232 | - secure_pod_config = generate_pod_config(config, secured=True) |
233 | + logger.info("ALEJDG - config type: %s - config data: %s", type(config), config) |
234 | + |
235 | + full_pod_config = self.generate_pod_config(config, secured=False) |
236 | + secure_pod_config = self.generate_pod_config(config, secured=True) |
237 | |
238 | spec = { |
239 | "containers": [ |
240 | @@ -101,18 +125,23 @@ class JenkinsAgentCharm(CharmBase): |
241 | } |
242 | |
243 | out = io.StringIO() |
244 | - pprint.pprint(spec, out) |
245 | logger.info("This is the Kubernetes Pod spec config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue())) |
246 | |
247 | secure_pod_config.update(full_pod_config) |
248 | + pprint.pprint(spec, out) |
249 | + logger.info("This is the Kubernetes Pod spec config (with secrets) <<EOM\n{}\nEOM".format(out.getvalue())) |
250 | |
251 | return spec |
252 | |
253 | def is_valid_config(self): |
254 | is_valid = True |
255 | - config = self.model.config |
256 | |
257 | - want = ("image", "jenkins_user", "jenkins_api_token") |
258 | + config = self.model.config |
259 | + logger.info("ALEJDG SPEC: %s", self._stored._spec) |
260 | + if self._stored.agent_tokens: |
261 | + want = ("image", "jenkins_user") |
262 | + else: |
263 | + want = ("image", "jenkins_user", "jenkins_api_token") |
264 | missing = [k for k in want if config[k].rstrip() == ""] |
265 | if missing: |
266 | message = "Missing required config: {}".format(" ".join(missing)) |
267 | @@ -122,6 +151,97 @@ class JenkinsAgentCharm(CharmBase): |
268 | |
269 | return is_valid |
270 | |
271 | + def on_slave_relation_joined(self, event): |
272 | + logger.info("Jenkins relation joined") |
273 | + noexecutors = os.cpu_count() |
274 | + config_labels = self.model.config.get('labels') |
275 | + agent_name = "" |
276 | + if self._stored.agents: |
277 | + self._stored.agents[-1] |
278 | + name, number = self._stored.agents[-1].rsplit('-', 1) |
279 | + agent_name = "{}-{}".format(name, int(number) + 1) |
280 | + else: |
281 | + self._stored.agents = [] |
282 | + agent_name = self.unit.name.replace('/', '-') |
283 | + |
284 | + if config_labels: |
285 | + labels = config_labels |
286 | + else: |
287 | + labels = os.uname()[4] |
288 | + |
289 | + # slave_address = hookenv.unit_private_ip() |
290 | + |
291 | + logger.info("noexecutors: %s - type: %s", noexecutors, type(noexecutors)) |
292 | + logger.info("labels: %s - type: %s",labels, type(labels)) |
293 | + event.relation.data[self.model.unit]["executors"] = str(noexecutors) |
294 | + event.relation.data[self.model.unit]["labels"] = labels |
295 | + event.relation.data[self.model.unit]["slavehost"] = agent_name |
296 | + |
297 | + remote_data = event.relation.data[event.app] |
298 | + logger.info("ALEJDG - remote_data_app: %s", remote_data) |
299 | + for i in remote_data: |
300 | + logger.info("ALEJDG - remote_data_app['%s']: %s", i, remote_data[i]) |
301 | + |
302 | + if event.unit is not None: |
303 | + remote_data = event.relation.data[event.unit] |
304 | + logger.info("ALEJDG - os.environ: %s", os.environ) |
305 | + |
306 | + logger.info("ALEJDG - remote_data_post_app: %s", remote_data) |
307 | + for i in remote_data: |
308 | + logger.info("ALEJDG - remote_data_post_app['%s']: %s", i, remote_data[i]) |
309 | + |
310 | + def on_slave_relation_changed(self, event): |
311 | + logger.info("Jenkins relation changed") |
312 | + try: |
313 | + logger.info("ALEJDG - event.relation.data[event.unit]['url']: %s", event.relation.data[event.unit]['url']) |
314 | + self._stored.jenkins_url = event.relation.data[event.unit]['url'] |
315 | + except KeyError: |
316 | + pass |
317 | + |
318 | + try: |
319 | + logger.info("ALEJDG - event.relation.data[event.unit]['secret']: %s", event.relation.data[event.unit]['secret']) |
320 | + logger.info("ALEJDG - event.unit.name: %s", event.unit.name) |
321 | + logger.info("ALEJDG - self.unit.name.: %s", self.unit.name) |
322 | + self._stored.agent_tokens = self._stored.agent_tokens or [] |
323 | + self._stored.agent_tokens.append(event.relation.data[event.unit]['secret']) |
324 | + agent_name = "" |
325 | + if self._stored.agents: |
326 | + logger.info("ALEJDG - self._stored.agents[-1]: %s", self._stored.agents[-1]) |
327 | + self._stored.agents[-1] |
328 | + name, number = self._stored.agents[-1].rsplit('-', 1) |
329 | + agent_name = "{}-{}".format(name, int(number) + 1) |
330 | + self._stored.agents.append(agent_name) |
331 | + else: |
332 | + self._stored.agents = [] |
333 | + agent_name = self.unit.name.replace('/', '-') |
334 | + self._stored.agents.append(agent_name) |
335 | + except KeyError: |
336 | + pass |
337 | + |
338 | + self.configure_slave_through_relation(event) |
339 | + |
340 | + def configure_slave_through_relation(self, event): |
341 | + logger.info("Setting up jenkins via slave relation") |
342 | + logger.info("ALEJDG - on_slave_relation_joined - self._stored.agents_setup: %s", self._stored.agents) |
343 | + self.model.unit.status = MaintenanceStatus("Configuring jenkins agent") |
344 | + |
345 | + if self.model.config.get("url"): |
346 | + logger.info("Config option 'url' is set. Can't use agent relation.") |
347 | + self.model.unit.status = ActiveStatus() |
348 | + return |
349 | + |
350 | + if self._stored.jenkins_url is None: |
351 | + logger.info("Jenkins hasn't exported its url yet. Skipping setup for now.") |
352 | + self.model.unit.status = ActiveStatus() |
353 | + return |
354 | + |
355 | + if self._stored.agent_tokens is None: |
356 | + logger.info("Jenkins hasn't exported the agent secret yet. Skipping setup for now.") |
357 | + self.model.unit.status = ActiveStatus() |
358 | + return |
359 | + |
360 | + self.configure_pod(event) |
361 | + |
362 | |
363 | if __name__ == '__main__': |
364 | main(JenkinsAgentCharm) |
365 | diff --git a/src/interface_jenkins_slave.py b/src/interface_jenkins_slave.py |
366 | new file mode 100644 |
367 | index 0000000..dec278d |
368 | --- /dev/null |
369 | +++ b/src/interface_jenkins_slave.py |
370 | @@ -0,0 +1,66 @@ |
371 | +import json |
372 | + |
373 | +from ops.framework import EventBase, EventsBase, EventSource, Object, StoredState |
374 | + |
375 | + |
376 | +class NewClient(EventBase): |
377 | + def __init__(self, handle, client): |
378 | + super().__init__(handle) |
379 | + self.client = client |
380 | + |
381 | + def snapshot(self): |
382 | + return { |
383 | + 'relation_name': self.client._relation.name, |
384 | + 'relation_id': self.client._relation.id, |
385 | + } |
386 | + |
387 | + def restore(self, snapshot): |
388 | + relation = self.model.get_relation(snapshot['relation_name'], snapshot['relation_id']) |
389 | + self.client = HTTPInterfaceClient(relation, self.model.unit) |
390 | + |
391 | + |
392 | +class HTTPServerEvents(EventsBase): |
393 | + new_client = EventSource(NewClient) |
394 | + |
395 | + |
396 | +class HTTPServer(Object): |
397 | + on = HTTPServerEvents() |
398 | + state = StoredState() |
399 | + |
400 | + def __init__(self, charm, relation_name): |
401 | + super().__init__(charm, relation_name) |
402 | + self.relation_name = relation_name |
403 | + self.framework.observe(charm.on.start, self.init_state) |
404 | + self.framework.observe(charm.on[relation_name].relation_joined, self.on_joined) |
405 | + self.framework.observe(charm.on[relation_name].relation_departed, self.on_departed) |
406 | + |
407 | + def init_state(self, event): |
408 | + self.state.apps = [] |
409 | + |
410 | + @property |
411 | + def _relations(self): |
412 | + return self.model.relations[self.relation_name] |
413 | + |
414 | + def on_joined(self, event): |
415 | + if event.app not in self.state.apps: |
416 | + self.state.apps.append(event.app) |
417 | + self.on.new_client.emit(HTTPInterfaceClient(event.relation, self.model.unit)) |
418 | + |
419 | + def on_departed(self, event): |
420 | + self.state.apps = [app for app in self._relations] |
421 | + |
422 | + def clients(self): |
423 | + return [HTTPInterfaceClient(relation, self.model.unit) for relation in self._relations] |
424 | + |
425 | + |
426 | +class HTTPInterfaceClient: |
427 | + def __init__(self, relation, local_unit): |
428 | + self._relation = relation |
429 | + self._local_unit = local_unit |
430 | + self.ingress_address = relation.data[local_unit]['ingress-address'] |
431 | + |
432 | + def serve(self, hosts, port): |
433 | + self._relation.data[self._local_unit]['extended_data'] = json.dumps([{ |
434 | + 'hostname': host, |
435 | + 'port': port, |
436 | + } for host in hosts]) |
437 | \ No newline at end of file |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.