Merge ~mthaddon/charm-k8s-gunicorn/+git/charm-k8s-gunicorn:image-resource into charm-k8s-gunicorn:master
- Git
- lp:~mthaddon/charm-k8s-gunicorn/+git/charm-k8s-gunicorn
- image-resource
- Merge into master
Status: | Merged |
---|---|
Approved by: | Tom Haddon |
Approved revision: | 5bb4bdfbc895a31f459f9c5ec42cd014aeee73cc |
Merged at revision: | d030817c6f41df25bf6fb9f412a0104e23395ccf |
Proposed branch: | ~mthaddon/charm-k8s-gunicorn/+git/charm-k8s-gunicorn:image-resource |
Merge into: | charm-k8s-gunicorn:master |
Diff against target: |
305 lines (+41/-83) 7 files modified
README.md (+9/-3) config.yaml (+2/-19) metadata.yaml (+6/-0) requirements.txt (+1/-0) src/charm.py (+12/-7) tests/unit/scenario.py (+10/-54) tests/unit/test_charm.py (+1/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
🤖 prod-jenkaas-is (community) | continuous-integration | Approve | |
gunicorn-charmers | Pending | ||
Review via email: mp+399853@code.launchpad.net |
Commit message
Switch the charm to use OCI Resource for image
Description of the change
Switch the charm to use OCI Resource for image.
We're in the process of converting this charm to the new sidecar/pebble approach. Making these changes now will lessen the diff to the main repo when we're ready to merge.
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote : | # |
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote : | # |
FAILED: Continuous integration, rev:5bb4bdfbc89
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-is (prod-jenkaas-is) wrote : | # |
PASSED: Continuous integration, rev:5bb4bdfbc89
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Stuart Bishop (stub) wrote : | # |
This all looks fine. I expect this will need a further revision when Operator Framework supports OCI image resources properly, so this change may be premature in that way. But overall it seems a good idea as it means the deployment documentation will remain consistent for Juju 2.9.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision d030817c6f41df2
Preview Diff
1 | diff --git a/README.md b/README.md | |||
2 | index fbbd51a..710b241 100644 | |||
3 | --- a/README.md | |||
4 | +++ b/README.md | |||
5 | @@ -6,8 +6,12 @@ A charm that allows you to deploy your gunicorn application in kubernetes. | |||
6 | 6 | 6 | ||
7 | 7 | ## Usage | 7 | ## Usage |
8 | 8 | 8 | ||
9 | 9 | By default, the charm will deploy a simple docker image that contains a | ||
10 | 10 | gunicorn app that displays a short message and its environment variables. The | ||
11 | 11 | image is built using an OCI Recipe on Launchpad and published to dockerhub | ||
12 | 12 | [here](https://hub.docker.com/r/gunicorncharmers/gunicorn-app). | ||
13 | 9 | ``` | 13 | ``` |
15 | 10 | juju deploy cs:~gunicorn-charmers/gunicorn my-awesome-app --config image_path=localhost:32000/myapp --config external_hostname=my-awesome-app.com | 14 | juju deploy cs:~gunicorn-charmers/gunicorn my-awesome-app |
16 | 11 | ``` | 15 | ``` |
17 | 12 | 16 | ||
18 | 13 | ### Scale Out Usage | 17 | ### Scale Out Usage |
19 | @@ -21,7 +25,9 @@ juju add-unit my-awesome-app | |||
20 | 21 | ### Using your own image | 25 | ### Using your own image |
21 | 22 | 26 | ||
22 | 23 | You can, of course, supply our own OCI image. gunicorn is expected to listen on | 27 | You can, of course, supply our own OCI image. gunicorn is expected to listen on |
24 | 24 | port 80. | 28 | port 80. To do so, specify `--resource gunicorn-image='image-location'` at |
25 | 29 | deploy time, or use `juju attach-resource` if you want to switch images after | ||
26 | 30 | initial deployment. | ||
27 | 25 | 31 | ||
28 | 26 | ### Using gunicorn-base to build an image | 32 | ### Using gunicorn-base to build an image |
29 | 27 | 33 | ||
30 | @@ -45,7 +51,7 @@ added to the environment of your pods. | |||
31 | 45 | The context used to render the Jinja2 template is constructed from relation | 51 | The context used to render the Jinja2 template is constructed from relation |
32 | 46 | data. For example, if you're relating with influxdb, you could do the following : | 52 | data. For example, if you're relating with influxdb, you could do the following : |
33 | 47 | ``` | 53 | ``` |
35 | 48 | juju deploy cs:~gunicorn-charmers/gunicorn my-awesome-app --config image_path=localhost:32000/myapp --config external_hostname=my-awesome-app.com | 54 | juju deploy cs:~gunicorn-charmers/gunicorn my-awesome-app |
36 | 49 | juju config my-awesome-app environment="INFLUXDB_HOST: {{influxdb.hostname}}" | 55 | juju config my-awesome-app environment="INFLUXDB_HOST: {{influxdb.hostname}}" |
37 | 50 | ``` | 56 | ``` |
38 | 51 | 57 | ||
39 | diff --git a/config.yaml b/config.yaml | |||
40 | index 0754ab4..7276c75 100644 | |||
41 | --- a/config.yaml | |||
42 | +++ b/config.yaml | |||
43 | @@ -1,21 +1,4 @@ | |||
44 | 1 | options: | 1 | options: |
45 | 2 | image_path: | ||
46 | 3 | type: string | ||
47 | 4 | description: > | ||
48 | 5 | The location of the image to use, e.g. "registry.example.com/my_gunicorn_app:v1". | ||
49 | 6 | |||
50 | 7 | This setting is required. | ||
51 | 8 | default: '' | ||
52 | 9 | image_username: | ||
53 | 10 | type: string | ||
54 | 11 | description: > | ||
55 | 12 | The username for accessing the registry specified in image_path. | ||
56 | 13 | default: '' | ||
57 | 14 | image_password: | ||
58 | 15 | type: string | ||
59 | 16 | description: > | ||
60 | 17 | The password associated with image_username for accessing the registry specified in image_path. | ||
61 | 18 | default: '' | ||
62 | 19 | environment: | 2 | environment: |
63 | 20 | type: string | 3 | type: string |
64 | 21 | description: > | 4 | description: > |
65 | @@ -31,5 +14,5 @@ options: | |||
66 | 31 | external_hostname: | 14 | external_hostname: |
67 | 32 | type: string | 15 | type: string |
68 | 33 | description: > | 16 | description: > |
71 | 34 | External hostname this gunicorn app should respond to. | 17 | External hostname this gunicorn app should respond to (required). |
72 | 35 | default: '' | 18 | default: 'foo.internal' |
73 | diff --git a/metadata.yaml b/metadata.yaml | |||
74 | index 4e6d497..b1a1aa7 100644 | |||
75 | --- a/metadata.yaml | |||
76 | +++ b/metadata.yaml | |||
77 | @@ -7,6 +7,12 @@ summary: | | |||
78 | 7 | Gunicorn charm | 7 | Gunicorn charm |
79 | 8 | series: [kubernetes] | 8 | series: [kubernetes] |
80 | 9 | min-juju-version: 2.8.0 # charm storage in state | 9 | min-juju-version: 2.8.0 # charm storage in state |
81 | 10 | resources: | ||
82 | 11 | gunicorn-image: | ||
83 | 12 | type: oci-image | ||
84 | 13 | description: docker image for Gunicorn | ||
85 | 14 | auto-fetch: true | ||
86 | 15 | upstream-source: 'gunicorncharmers/gunicorn-app:20.0.4-20.04_edge' | ||
87 | 10 | requires: | 16 | requires: |
88 | 11 | pg: | 17 | pg: |
89 | 12 | interface: pgsql | 18 | interface: pgsql |
90 | diff --git a/requirements.txt b/requirements.txt | |||
91 | index fd6adcd..6b14e66 100644 | |||
92 | --- a/requirements.txt | |||
93 | +++ b/requirements.txt | |||
94 | @@ -1,2 +1,3 @@ | |||
95 | 1 | ops | 1 | ops |
96 | 2 | ops-lib-pgsql | 2 | ops-lib-pgsql |
97 | 3 | https://github.com/juju-solutions/resource-oci-image/archive/master.zip | ||
98 | diff --git a/src/charm.py b/src/charm.py | |||
99 | index 3772ef0..c25df78 100755 | |||
100 | --- a/src/charm.py | |||
101 | +++ b/src/charm.py | |||
102 | @@ -7,6 +7,7 @@ import logging | |||
103 | 7 | import yaml | 7 | import yaml |
104 | 8 | 8 | ||
105 | 9 | import ops | 9 | import ops |
106 | 10 | from oci_image import OCIImageResource, OCIImageResourceError | ||
107 | 10 | from ops.framework import StoredState | 11 | from ops.framework import StoredState |
108 | 11 | from ops.charm import CharmBase | 12 | from ops.charm import CharmBase |
109 | 12 | from ops.main import main | 13 | from ops.main import main |
110 | @@ -20,7 +21,7 @@ import pgsql | |||
111 | 20 | 21 | ||
112 | 21 | logger = logging.getLogger(__name__) | 22 | logger = logging.getLogger(__name__) |
113 | 22 | 23 | ||
115 | 23 | REQUIRED_JUJU_CONFIG = ['image_path', 'external_hostname'] | 24 | REQUIRED_JUJU_CONFIG = ['external_hostname'] |
116 | 24 | JUJU_CONFIG_YAML_DICT_ITEMS = ['environment'] | 25 | JUJU_CONFIG_YAML_DICT_ITEMS = ['environment'] |
117 | 25 | 26 | ||
118 | 26 | 27 | ||
119 | @@ -42,6 +43,8 @@ class GunicornK8sCharm(CharmBase): | |||
120 | 42 | def __init__(self, *args): | 43 | def __init__(self, *args): |
121 | 43 | super().__init__(*args) | 44 | super().__init__(*args) |
122 | 44 | 45 | ||
123 | 46 | self.image = OCIImageResource(self, 'gunicorn-image') | ||
124 | 47 | |||
125 | 45 | self.framework.observe(self.on.start, self._configure_pod) | 48 | self.framework.observe(self.on.start, self._configure_pod) |
126 | 46 | self.framework.observe(self.on.config_changed, self._configure_pod) | 49 | self.framework.observe(self.on.config_changed, self._configure_pod) |
127 | 47 | self.framework.observe(self.on.leader_elected, self._configure_pod) | 50 | self.framework.observe(self.on.leader_elected, self._configure_pod) |
128 | @@ -259,12 +262,14 @@ class GunicornK8sCharm(CharmBase): | |||
129 | 259 | :returns: A pod spec | 262 | :returns: A pod spec |
130 | 260 | """ | 263 | """ |
131 | 261 | 264 | ||
138 | 262 | config = self.model.config | 265 | try: |
139 | 263 | image_details = { | 266 | image_details = self.image.fetch() |
140 | 264 | 'imagePath': config['image_path'], | 267 | logging.info("using imageDetails: {}") |
141 | 265 | } | 268 | except OCIImageResourceError: |
142 | 266 | if config.get('image_username', None): | 269 | logging.exception('An error occurred while fetching the image info') |
143 | 267 | image_details.update({'username': config['image_username'], 'password': config['image_password']}) | 270 | self.unit.status = BlockedStatus('Error fetching image information') |
144 | 271 | return {} | ||
145 | 272 | |||
146 | 268 | pod_env = self._make_pod_env() | 273 | pod_env = self._make_pod_env() |
147 | 269 | 274 | ||
148 | 270 | return { | 275 | return { |
149 | diff --git a/tests/unit/scenario.py b/tests/unit/scenario.py | |||
150 | index f71494f..3402589 100644 | |||
151 | --- a/tests/unit/scenario.py | |||
152 | +++ b/tests/unit/scenario.py | |||
153 | @@ -35,34 +35,23 @@ TEST_PG_CONNSTR = 'dbname=gunicorn host=1.2.3.4 password=pwd port=5432 user=usr' | |||
154 | 35 | TEST_JUJU_CONFIG = { | 35 | TEST_JUJU_CONFIG = { |
155 | 36 | 'defaults': { | 36 | 'defaults': { |
156 | 37 | 'config': {}, | 37 | 'config': {}, |
169 | 38 | 'logger': [ | 38 | 'logger': [], |
170 | 39 | "ERROR:charm:Required Juju config item not set : image_path", | 39 | 'expected': False, |
159 | 40 | 'ERROR:charm:Required Juju config item not set : external_hostname', | ||
160 | 41 | ], | ||
161 | 42 | 'expected': 'Required Juju config item(s) not set : external_hostname, image_path', | ||
162 | 43 | }, | ||
163 | 44 | 'missing_image_path': { | ||
164 | 45 | 'config': { | ||
165 | 46 | 'external_hostname': 'example.com', | ||
166 | 47 | }, | ||
167 | 48 | 'logger': ["ERROR:charm:Required Juju config item not set : image_path"], | ||
168 | 49 | 'expected': 'Required Juju config item(s) not set : image_path', | ||
171 | 50 | }, | 40 | }, |
172 | 51 | 'missing_external_hostname': { | 41 | 'missing_external_hostname': { |
173 | 52 | 'config': { | 42 | 'config': { |
175 | 53 | 'image_path': 'my_gunicorn_app:devel', | 43 | 'external_hostname': '', |
176 | 54 | }, | 44 | }, |
177 | 55 | 'logger': ["ERROR:charm:Required Juju config item not set : external_hostname"], | 45 | 'logger': ["ERROR:charm:Required Juju config item not set : external_hostname"], |
178 | 56 | 'expected': 'Required Juju config item(s) not set : external_hostname', | 46 | 'expected': 'Required Juju config item(s) not set : external_hostname', |
179 | 57 | }, | 47 | }, |
180 | 58 | 'good_config_no_env': { | 48 | 'good_config_no_env': { |
182 | 59 | 'config': {'image_path': 'my_gunicorn_app:devel', 'external_hostname': 'example.com'}, | 49 | 'config': {'external_hostname': 'example.com'}, |
183 | 60 | 'logger': [], | 50 | 'logger': [], |
184 | 61 | 'expected': False, | 51 | 'expected': False, |
185 | 62 | }, | 52 | }, |
186 | 63 | 'good_config_with_env': { | 53 | 'good_config_with_env': { |
187 | 64 | 'config': { | 54 | 'config': { |
188 | 65 | 'image_path': 'my_gunicorn_app:devel', | ||
189 | 66 | 'environment': 'MYENV: foo', | 55 | 'environment': 'MYENV: foo', |
190 | 67 | 'external_hostname': 'example.com', | 56 | 'external_hostname': 'example.com', |
191 | 68 | }, | 57 | }, |
192 | @@ -72,16 +61,8 @@ TEST_JUJU_CONFIG = { | |||
193 | 72 | } | 61 | } |
194 | 73 | 62 | ||
195 | 74 | TEST_CONFIGURE_POD = { | 63 | TEST_CONFIGURE_POD = { |
196 | 75 | 'bad_config': { | ||
197 | 76 | 'config': { | ||
198 | 77 | 'external_hostname': 'example.com', | ||
199 | 78 | }, | ||
200 | 79 | '_leader_get': "5:\n database: gunicorn\n extensions: ''\n roles: ''", | ||
201 | 80 | 'expected': 'Required Juju config item(s) not set : image_path', | ||
202 | 81 | }, | ||
203 | 82 | 'good_config_no_env': { | 64 | 'good_config_no_env': { |
204 | 83 | 'config': { | 65 | 'config': { |
205 | 84 | 'image_path': 'my_gunicorn_app:devel', | ||
206 | 85 | 'external_hostname': 'example.com', | 66 | 'external_hostname': 'example.com', |
207 | 86 | }, | 67 | }, |
208 | 87 | '_leader_get': "5:\n database: gunicorn\n extensions: ''\n roles: ''", | 68 | '_leader_get': "5:\n database: gunicorn\n extensions: ''\n roles: ''", |
209 | @@ -89,7 +70,6 @@ TEST_CONFIGURE_POD = { | |||
210 | 89 | }, | 70 | }, |
211 | 90 | 'good_config_with_env': { | 71 | 'good_config_with_env': { |
212 | 91 | 'config': { | 72 | 'config': { |
213 | 92 | 'image_path': 'my_gunicorn_app:devel', | ||
214 | 93 | 'external_hostname': 'example.com', | 73 | 'external_hostname': 'example.com', |
215 | 94 | 'environment': 'MYENV: foo', | 74 | 'environment': 'MYENV: foo', |
216 | 95 | }, | 75 | }, |
217 | @@ -101,7 +81,6 @@ TEST_CONFIGURE_POD = { | |||
218 | 101 | TEST_MAKE_POD_SPEC = { | 81 | TEST_MAKE_POD_SPEC = { |
219 | 102 | 'basic_no_env': { | 82 | 'basic_no_env': { |
220 | 103 | 'config': { | 83 | 'config': { |
221 | 104 | 'image_path': 'my_gunicorn_app:devel', | ||
222 | 105 | 'external_hostname': 'example.com', | 84 | 'external_hostname': 'example.com', |
223 | 106 | }, | 85 | }, |
224 | 107 | 'pod_spec': { | 86 | 'pod_spec': { |
225 | @@ -110,7 +89,9 @@ TEST_MAKE_POD_SPEC = { | |||
226 | 110 | { | 89 | { |
227 | 111 | 'name': 'gunicorn', | 90 | 'name': 'gunicorn', |
228 | 112 | 'imageDetails': { | 91 | 'imageDetails': { |
230 | 113 | 'imagePath': 'my_gunicorn_app:devel', | 92 | 'imagePath': 'registrypath', |
231 | 93 | 'password': 'password', | ||
232 | 94 | 'username': 'username', | ||
233 | 114 | }, | 95 | }, |
234 | 115 | 'imagePullPolicy': 'Always', | 96 | 'imagePullPolicy': 'Always', |
235 | 116 | 'ports': [{'containerPort': 80, 'protocol': 'TCP'}], | 97 | 'ports': [{'containerPort': 80, 'protocol': 'TCP'}], |
236 | @@ -122,7 +103,6 @@ TEST_MAKE_POD_SPEC = { | |||
237 | 122 | }, | 103 | }, |
238 | 123 | 'basic_with_env': { | 104 | 'basic_with_env': { |
239 | 124 | 'config': { | 105 | 'config': { |
240 | 125 | 'image_path': 'my_gunicorn_app:devel', | ||
241 | 126 | 'external_hostname': 'example.com', | 106 | 'external_hostname': 'example.com', |
242 | 127 | 'environment': 'MYENV: foo', | 107 | 'environment': 'MYENV: foo', |
243 | 128 | }, | 108 | }, |
244 | @@ -132,7 +112,9 @@ TEST_MAKE_POD_SPEC = { | |||
245 | 132 | { | 112 | { |
246 | 133 | 'name': 'gunicorn', | 113 | 'name': 'gunicorn', |
247 | 134 | 'imageDetails': { | 114 | 'imageDetails': { |
249 | 135 | 'imagePath': 'my_gunicorn_app:devel', | 115 | 'imagePath': 'registrypath', |
250 | 116 | 'password': 'password', | ||
251 | 117 | 'username': 'username', | ||
252 | 136 | }, | 118 | }, |
253 | 137 | 'imagePullPolicy': 'Always', | 119 | 'imagePullPolicy': 'Always', |
254 | 138 | 'ports': [{'containerPort': 80, 'protocol': 'TCP'}], | 120 | 'ports': [{'containerPort': 80, 'protocol': 'TCP'}], |
255 | @@ -142,38 +124,12 @@ TEST_MAKE_POD_SPEC = { | |||
256 | 142 | ], | 124 | ], |
257 | 143 | }, | 125 | }, |
258 | 144 | }, | 126 | }, |
259 | 145 | 'private_registry': { | ||
260 | 146 | 'config': { | ||
261 | 147 | 'image_path': 'my_gunicorn_app:devel', | ||
262 | 148 | 'image_username': 'foo', | ||
263 | 149 | 'image_password': 'bar', | ||
264 | 150 | 'external_hostname': 'example.com', | ||
265 | 151 | }, | ||
266 | 152 | 'pod_spec': { | ||
267 | 153 | 'version': 3, # otherwise resources are ignored | ||
268 | 154 | 'containers': [ | ||
269 | 155 | { | ||
270 | 156 | 'name': 'gunicorn', | ||
271 | 157 | 'imageDetails': { | ||
272 | 158 | 'imagePath': 'my_gunicorn_app:devel', | ||
273 | 159 | 'username': 'foo', | ||
274 | 160 | 'password': 'bar', | ||
275 | 161 | }, | ||
276 | 162 | 'imagePullPolicy': 'Always', | ||
277 | 163 | 'ports': [{'containerPort': 80, 'protocol': 'TCP'}], | ||
278 | 164 | 'envConfig': {}, | ||
279 | 165 | 'kubernetes': {'readinessProbe': {'httpGet': {'path': '/', 'port': 80}}}, | ||
280 | 166 | } | ||
281 | 167 | ], | ||
282 | 168 | }, | ||
283 | 169 | }, | ||
284 | 170 | } | 127 | } |
285 | 171 | 128 | ||
286 | 172 | 129 | ||
287 | 173 | TEST_MAKE_K8S_INGRESS = { | 130 | TEST_MAKE_K8S_INGRESS = { |
288 | 174 | 'basic': { | 131 | 'basic': { |
289 | 175 | 'config': { | 132 | 'config': { |
290 | 176 | 'image_path': 'my_gunicorn_app:devel', | ||
291 | 177 | 'external_hostname': 'example.com', | 133 | 'external_hostname': 'example.com', |
292 | 178 | }, | 134 | }, |
293 | 179 | 'expected': [ | 135 | 'expected': [ |
294 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py | |||
295 | index 746dbd3..b5e1f87 100755 | |||
296 | --- a/tests/unit/test_charm.py | |||
297 | +++ b/tests/unit/test_charm.py | |||
298 | @@ -34,6 +34,7 @@ class TestGunicornK8sCharm(unittest.TestCase): | |||
299 | 34 | """Setup the harness object.""" | 34 | """Setup the harness object.""" |
300 | 35 | self.harness = testing.Harness(GunicornK8sCharm) | 35 | self.harness = testing.Harness(GunicornK8sCharm) |
301 | 36 | self.harness.begin() | 36 | self.harness.begin() |
302 | 37 | self.harness.add_oci_resource('gunicorn-image') | ||
303 | 37 | 38 | ||
304 | 38 | def tearDown(self): | 39 | def tearDown(self): |
305 | 39 | """Cleanup the harness.""" | 40 | """Cleanup the harness.""" |
A CI job is currently in progress. A follow up comment will be added when it completes.