Merge charm-k8s-jenkins-agent:manual-agent into charm-k8s-jenkins-agent:master

Proposed by Alexandre Gomes
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)
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
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
🤖 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.

Revision history for this message
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://paste.ubuntu.com/p/j3tSHmbmcB/

review: Needs Fixing
Revision history for this message
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://paste.ubuntu.com/p/j3tSHmbmcB/

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.

review: Needs Resubmitting
Revision history for this message
Alexandre Gomes (alejdg) wrote :
Revision history for this message
Alexandre Gomes (alejdg) wrote :

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index 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)" \
19diff --git a/dockerfile/Dockerfile b/dockerfile/Dockerfile
20index 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
33diff --git a/dockerfile/files/entrypoint.sh b/dockerfile/files/entrypoint.sh
34index 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
87diff --git a/metadata.yaml b/metadata.yaml
88index 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
98diff --git a/src/charm.py b/src/charm.py
99index 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)
365diff --git a/src/interface_jenkins_slave.py b/src/interface_jenkins_slave.py
366new file mode 100644
367index 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

Subscribers

People subscribed via source and target branches

to all changes: