Merge ~tilmanbaumann/interface-prometheus:reactive_endpoints into interface-prometheus:master

Proposed by Tilman Baumann
Status: Merged
Approved by: Stuart Bishop
Approved revision: 45a4be2ce15eb9ce170f06e2462e12693c78fc98
Merged at revision: defa8b8ead4b33f58826be29b254fff9c024ac59
Proposed branch: ~tilmanbaumann/interface-prometheus:reactive_endpoints
Merge into: interface-prometheus:master
Diff against target: 202 lines (+109/-55)
2 files modified
provides.py (+49/-21)
requires.py (+60/-34)
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Canonical IS Reviewers Pending
Review via email: mp+367744@code.launchpad.net

Commit message

Switch from RelationBase class to Endpoint

Description of the change

I converted the interface to the new Endpoint class because of unfixable bugs in the old method.

The code stays compatible with old charms expecting RelationBase. But I will propose patches to all charms using this interface to make use of the new features.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

Yes please. This is going to be a teaching example, as it is simple enough to comprehend and not need any weird stuff, but complex enough to demonstrate most of the Endpoint stuff (including implementing both provider and consumer; this is the first non-trivial Endpoint I've seen that has done that). The most magic thing is mixing to_publish for implicit JSON encoding with to_publish_raw for the legacy fields that need to be unencoded.

So I'd like to see two minor changes needed to support cross model relations fixed in this branch, so that when people inevitably cut&paste the code they get the good stuff. Some other comments inline too, primarily with setting the old flags for backwards compatibility.

review: Needs Fixing
Revision history for this message
Tilman Baumann (tilmanbaumann) wrote :

Thanks, Stuart for the great review. I will be more than happy to implement those changes.

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
Stuart Bishop (stub) wrote :

The updates all look great :)

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision defa8b8ead4b33f58826be29b254fff9c024ac59

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/provides.py b/provides.py
2index b28d2f2..d1f1683 100644
3--- a/provides.py
4+++ b/provides.py
5@@ -1,31 +1,59 @@
6-from charms.reactive import RelationBase, scopes, hook
7+from charms.reactive import Endpoint
8+from charms.reactive import hook, when, when_not
9 from charmhelpers.core import hookenv
10+from charms.reactive.flags import clear_flag, set_flag
11
12
13-class PrometheusProvides(RelationBase):
14- scope = scopes.UNIT
15+'''
16+This interface is desiged to be compatible with a previous
17+implementation based on RelationBase.
18
19- @hook('{provides:prometheus}-relation-{joined,changed}')
20- def changed(self):
21- self.set_state('{relation_name}.available')
22+Specifically
23+- the `{endpoint_name}.available` flags
24+- the use of `to_publish_raw` to avoid double quoting of the values
25+ as the old interface used plain values here
26+'''
27
28- @hook('{provides:prometheus}-relation-{broken,departed}')
29- def broken(self):
30- self.remove_state('{relation_name}.available')
31+
32+class PrometheusProvides(Endpoint):
33
34 def configure(self, port, path='/metrics',
35 scrape_interval=None, scrape_timeout=None, labels={}):
36- unit_name = hookenv.local_unit()
37+ """
38+ Interface method to set information provided to remote units
39+ """
40+
41+ # Use our unit name if the label isn't provided
42 if labels.get('host') is None:
43+ unit_name = hookenv.local_unit()
44 labels['host'] = unit_name.replace("/", "-")
45- relation_info = {
46- 'hostname': hookenv.unit_private_ip(),
47- 'port': port,
48- 'metrics_path': path,
49- 'labels': labels,
50- }
51- if scrape_interval is not None:
52- relation_info['scrape_interval'] = scrape_interval
53- if scrape_timeout is not None:
54- relation_info['scrape_timeout'] = scrape_timeout
55- self.set_remote(**relation_info)
56+
57+ for relation in self.relations:
58+ relation.to_publish_raw['hostname'] = hookenv.ingress_address(
59+ relation.relation_id, hookenv.local_unit()
60+ )
61+ relation.to_publish_raw['port'] = port
62+ relation.to_publish_raw['metrics_path'] = path
63+ relation.to_publish['labels'] = labels
64+ if scrape_interval is not None:
65+ relation.to_publish_raw['scrape_interval'] = scrape_interval
66+ if scrape_timeout is not None:
67+ relation.to_publish_raw['scrape_timeout'] = scrape_timeout
68+
69+ @when('endpoint.{endpoint_name}.joined')
70+ def available(self):
71+ """
72+ Raising the available flag when a unit joined
73+ """
74+ set_flag(self.expand_name('endpoint.{endpoint_name}.available'))
75+ set_flag(self.expand_name('{endpoint_name}.available')) #compatibility
76+
77+
78+ @when('endpoint.{endpoint_name}.departed')
79+ def broken(self):
80+ """
81+ Remove the available flag when the last unit has departed
82+ """
83+ if not self.is_joined:
84+ clear_flag(self.expand_name('endpoint.{endpoint_name}.available'))
85+ clear_flag(self.expand_name('{endpoint_name}.available')) #compatibility
86\ No newline at end of file
87diff --git a/requires.py b/requires.py
88index 83cc4f1..a2c10a2 100644
89--- a/requires.py
90+++ b/requires.py
91@@ -1,27 +1,42 @@
92-from charms.reactive import hook
93-from charms.reactive import RelationBase
94-from charms.reactive import scopes
95+from charms.reactive import Endpoint
96+from charms.reactive import hook, when, when_not
97+from charms.reactive.flags import clear_flag, set_flag
98+from charmhelpers.core.hookenv import service_name, ingress_address
99
100
101-class PrometheusRequires(RelationBase):
102- scope = scopes.UNIT
103+'''
104+This interface is desiged to be compatible with a previous
105+implementation based on RelationBase.
106
107- @hook('{requires:prometheus}-relation-{joined,changed}')
108+The old `{endpoint_name}.available` flags are maintained
109+'''
110+
111+class PrometheusRequires(Endpoint):
112+
113+ @when('endpoint.{endpoint_name}.changed')
114 def changed(self):
115- conv = self.conversation()
116- if conv.get_remote('port'):
117- # this unit's conversation has a port, so
118- # it is part of the set of available units
119- conv.set_state('{relation_name}.available')
120+ """
121+ Raising the availability flag once we have received the port field from a connected unit.
122+ It is convention that remote units signal availability this way.
123+ """
124+ if self.all_joined_units.received['port']:
125+ set_flag(self.expand_name('endpoint.{endpoint_name}.available'))
126+ set_flag(self.expand_name('{endpoint_name}.available')) #compatibility
127+
128
129- @hook('{requires:prometheus}-relation-{departed,broken}')
130+ @when('endpoint.{endpoint_name}.departed')
131 def broken(self):
132- conv = self.conversation()
133- conv.remove_state('{relation_name}.available')
134+ """
135+ Clearing the availability flag once the last unit departed.
136+ """
137+ if not self.is_joined:
138+ clear_flag(self.expand_name('endpoint.{endpoint_name}.available'))
139+ clear_flag(self.expand_name('{endpoint_name}.available')) #compatibility
140+
141
142 def targets(self):
143 """
144- Returns a list of available prometheus targets.
145+ Interface method returns a list of available prometheus targets.
146 [
147 {
148 'job_name': name_of_job,
149@@ -35,23 +50,34 @@ class PrometheusRequires(RelationBase):
150 ]
151 """
152 services = {}
153- for conv in self.conversations():
154- service_name = conv.scope.split('/')[0]
155- service = services.setdefault(service_name, {
156- 'job_name': service_name,
157- 'targets': [],
158- })
159- host = conv.get_remote('hostname') or\
160- conv.get_remote('private-address')
161- port = conv.get_remote('port')
162- if host and port:
163- service['targets'].append('{}:{}'.format(host, port))
164- if conv.get_remote('metrics_path'):
165- service['metrics_path'] = conv.get_remote('metrics_path')
166- if conv.get_remote('scrape_interval'):
167- service['scrape_interval'] = conv.get_remote('scrape_interval')
168- if conv.get_remote('scrape_timeout'):
169- service['scrape_timeout'] = conv.get_remote('scrape_timeout')
170- if conv.get_remote('labels'):
171- service['labels'] = conv.get_remote('labels')
172+ for relation in self.relations:
173+ service_name = relation.application_name
174+ for unit in relation.units:
175+ service = services.setdefault(service_name, {
176+ 'job_name': service_name,
177+ 'targets': [],
178+ })
179+
180+ # If the hostname is not provided we use the informaton from the relation
181+ host = (unit.received['hostname'] or
182+ ingress_address(relation.relation_id, unit))
183+ port = unit.received['port']
184+
185+ # Skipping this unit if it isn't ready yet
186+ if host and port:
187+ service['targets'].append('{}:{}'.format(host, port))
188+ else:
189+ continue
190+
191+ if unit.received['metrics_path']:
192+ service['metrics_path'] = unit.received['metrics_path']
193+ if unit.received['labels']:
194+ service['labels'] = unit.received['labels']
195+
196+ # Optional fields
197+ if unit.received['scrape_interval']:
198+ service['scrape_interval'] = unit.received['scrape_interval']
199+ if unit.received['scrape_timeout']:
200+ service['scrape_timeout'] = unit.received['scrape_timeout']
201 return [s for s in services.values() if s['targets']]
202+

Subscribers

People subscribed via source and target branches