Merge ~tshinn/snap-store-proxy-charm:self_signed_cert_support into snap-store-proxy-charm:main

Proposed by Tong Shinn
Status: Merged
Approved by: Tong Shinn
Approved revision: ef4d6148316922e75ffd6d603deef16dfb2a9ac1
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~tshinn/snap-store-proxy-charm:self_signed_cert_support
Merge into: snap-store-proxy-charm:main
Diff against target: 186 lines (+63/-11)
5 files modified
README.md (+5/-0)
src/charm.py (+15/-3)
src/helpers.py (+10/-2)
tests/test_charm.py (+14/-0)
tests/test_helpers.py (+19/-6)
Reviewer Review Type Date Requested Status
Wouter van Bommel (community) Approve
Jonathan Hartley (community) Approve
Review via email: mp+436227@code.launchpad.net

Commit message

Add support for configuring the proxy nginx with self signed certificate by setting both the "certificate" and "private_key" configurations to "selfsigned".

The reason for overloading the existing "certificate" and "private_key" configurations is that if a new configuration variable is created for enabling self-signed certs, the charm's behaviour becomes unclear when both the new variable and the existing variables are populated.

Description of the change

This change allows for easier local testing of the store proxy deployed using the charm.

The following juju config commands can be executed to generate and configure self signed certificate on the proxy:

juju config snap-store-proxy certificate="selfsigned"
juju config snap-store-proxy private_key="selfsigned"

Successful deployment looks like the following:

ubuntu@juju-b28c17-14:~$ snap-proxy status
Store URL: https://snap-store-proxy.local
Store DB: ok
Store ID: JFmKgUMENL7o6XsZ2OxtYkt3fkilpfkK
Status: approved
Connected Devices (updated daily): 0
Device Limit: 25
Internal Service Status:
  memcached: running
  nginx: running
  snapauth: running
  snapdevicegw: running
  snapdevicegw-local: running
  snapproxy: running
  snaprevs: running

ubuntu@juju-b28c17-14:~$ curl -k https://snap-store-proxy.local
snapcraft.io store API service - Copyright 2018-2022 Canonical.

To post a comment you must log in.
Revision history for this message
Przemysław Suliga (suligap) wrote :

LGTM, but best if Wouter takes a look.

Revision history for this message
Jonathan Hartley (tartley) wrote :

This LGTM. Is this behavior of these config variables discoverable? Should it be documented in this project's README? There is an "Advanced options" section about config variables.

Revision history for this message
Jonathan Hartley (tartley) :
review: Approve
Revision history for this message
Jonathan Hartley (tartley) wrote :

Oh, I approved this but did not mean to override the suggestion to get Wouter's input.

15e24e3... by Tong Shinn

Update README.md to include instructions for how to configure a self signed certificate.

Revision history for this message
Wouter van Bommel (woutervb) wrote :

1 remark, rest lgtm

review: Approve
ef4d614... by Tong Shinn

snap-proxy is an alias for snap-store-proxy so use the full file name to be safe.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/README.md b/README.md
2index 057c8e9..dbb1e49 100644
3--- a/README.md
4+++ b/README.md
5@@ -48,6 +48,11 @@ There are some mandatory config options that have to be configured for the charm
6
7 juju config snap-store-proxy registration_bundle=$(cat <path to file created with store-admin register command>)
8
9+HTTPS can be enabled by setting the `certificate` and `private_key` configurations with the corresponding PEM formatted certficate/key. To use self signed certificate, set the configuration values to `selfsigned` as follows:
10+
11+ juju config snap-store-proxy certificate="selfsigned"
12+ juju config snap-store-proxy private_key="selfsigned"
13+
14 ## Juju resource usage
15
16 The charm supports 2 modes to install the snapstore proxy code itself. This code is distributed as a snap from the actual snapstore, which means that the unit to which this charm is deployed, will need internet access, or at least access to the actual snapstore. If this is undesired, it is possible to deploy this charm in complete offline mode, which means that the snaps(\*) will need to be added as a resource.
17diff --git a/src/charm.py b/src/charm.py
18index 3bdbca1..ac50311 100755
19--- a/src/charm.py
20+++ b/src/charm.py
21@@ -20,7 +20,13 @@ from ops.main import main
22 from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, ModelError
23
24 from exceptions import ConfigException
25-from helpers import all_config_options, configure_proxy, default_values, open_port
26+from helpers import (
27+ all_config_options,
28+ configure_proxy,
29+ configure_self_signed_cert,
30+ default_values,
31+ open_port,
32+)
33 from optionvalidation import OptionValidation
34 from resource_helpers import (
35 create_database,
36@@ -276,11 +282,18 @@ class SnapstoreProxyCharm(CharmBase):
37 def handle_certificate(self):
38 # If we are called, then we know that the config has changed, and that
39 # self._stored["certificate|private_key"] contains the expected values
40+ raw_certificate = getattr(self._stored, "certificate", None)
41+ raw_key = getattr(self._stored, "private_key", None)
42+
43+ # If both the certificate and the private_key have been set to selfsigned,
44+ # generate and configure self-signed cert and return early.
45+ if raw_certificate == raw_key == "selfsigned":
46+ configure_self_signed_cert()
47+ return
48
49 # See if the certificate matches the key, which is basically compare the
50 # modules of the 2 certificate parts, they should be equal
51 try:
52- raw_certificate = getattr(self._stored, "certificate", None)
53 if raw_certificate is None:
54 return "Missing TLS certificate"
55 cert = x509.load_pem_x509_certificate(raw_certificate.encode())
56@@ -289,7 +302,6 @@ class SnapstoreProxyCharm(CharmBase):
57 return "Problem loading the certificate, they should be PEM encoded strings"
58
59 try:
60- raw_key = getattr(self._stored, "private_key", None)
61 if raw_key is None:
62 self.errors.append("Missing TLS private key")
63 return
64diff --git a/src/helpers.py b/src/helpers.py
65index 9a82576..8b92f42 100644
66--- a/src/helpers.py
67+++ b/src/helpers.py
68@@ -9,6 +9,8 @@ import yaml
69
70 logger = logging.getLogger(__name__)
71
72+PROXY_CLI = "snap-store-proxy"
73+
74
75 def configure_proxy(option, value, force=False):
76 """Simple wrapper around calling snap settings"""
77@@ -19,11 +21,11 @@ def configure_proxy(option, value, force=False):
78 combined = []
79 for count, entry in enumerate(option):
80 combined.append(f'{entry}="{value[count]}"')
81- command = ["snap-store-proxy", "config"] + combined
82+ command = [PROXY_CLI, "config"] + combined
83 logger.debug("Running proxy config for single item")
84 else:
85 logger.debug(f"Running config for option {option} with value {value}")
86- command = ["snap-store-proxy", "config", f"{option}={value}"]
87+ command = [PROXY_CLI, "config", f"{option}={value}"]
88 logger.debug("Running registration for list of items")
89
90 if force:
91@@ -33,6 +35,12 @@ def configure_proxy(option, value, force=False):
92 run(command)
93
94
95+def configure_self_signed_cert():
96+ logger.info("Generating and configuring self signed certificate for nginx")
97+ run(f'{PROXY_CLI} config proxy.tls.cert="" proxy.tls.key=""'.split())
98+ run(f"{PROXY_CLI} import-certificate --selfsigned".split())
99+
100+
101 def config_options():
102 """Helper function to take the charm config.yaml and make it a dict.
103
104diff --git a/tests/test_charm.py b/tests/test_charm.py
105index 7c0ccd6..e36b9a3 100644
106--- a/tests/test_charm.py
107+++ b/tests/test_charm.py
108@@ -760,3 +760,17 @@ class TestTLSCertificate(CharmTest):
109 "Problem loading the certificate, they should be PEM encoded strings"
110 ]
111 assert len(harness.charm.errors) == 1
112+
113+ def test_self_signed_cert(self, harness):
114+ self.setup_harness(harness)
115+
116+ domain = "https://test.domain.just.invented"
117+ harness.charm._stored.domain = domain + ".some.added.test"
118+ cert = key = "selfsigned"
119+
120+ with patch("charm.configure_self_signed_cert") as m_self_signed, patch(
121+ "charm.open_port"
122+ ):
123+ harness.update_config({"certificate": cert, "private_key": key})
124+
125+ m_self_signed.assert_called_once()
126diff --git a/tests/test_helpers.py b/tests/test_helpers.py
127index b81b358..15fac97 100644
128--- a/tests/test_helpers.py
129+++ b/tests/test_helpers.py
130@@ -2,15 +2,15 @@ from unittest import mock
131
132 import helpers
133
134+PROXY_CLI = "snap-store-proxy"
135+
136
137 def test_configure_proxy():
138 with mock.patch("helpers.run") as mocked_run:
139 helpers.configure_proxy("myvalue", "myoption")
140
141 assert mocked_run.called
142- mocked_run.assert_called_once_with(
143- ["snap-store-proxy", "config", "myvalue=myoption"]
144- )
145+ mocked_run.assert_called_once_with([PROXY_CLI, "config", "myvalue=myoption"])
146
147
148 def test_configure_proxy_list():
149@@ -21,7 +21,7 @@ def test_configure_proxy_list():
150
151 assert mocked_run.called
152 mocked_run.assert_called_once_with(
153- ["snap-store-proxy", "config", 'one="value_one"', 'two="value_two"']
154+ [PROXY_CLI, "config", 'one="value_one"', 'two="value_two"']
155 )
156
157
158@@ -31,7 +31,7 @@ def test_configure_proxy_forced():
159
160 assert mocked_run.called
161 mocked_run.assert_called_once_with(
162- ["snap-store-proxy", "config", "myvalue=myoption", "--force"]
163+ [PROXY_CLI, "config", "myvalue=myoption", "--force"]
164 )
165
166
167@@ -43,7 +43,20 @@ def test_configure_proxy_list_forced():
168
169 assert mocked_run.called
170 mocked_run.assert_called_once_with(
171- ["snap-store-proxy", "config", 'one="value_one"', 'two="value_two"', "--force"]
172+ [PROXY_CLI, "config", 'one="value_one"', 'two="value_two"', "--force"]
173+ )
174+
175+
176+def test_configure_self_signed_cert():
177+ with mock.patch("helpers.run") as mocked_run:
178+ helpers.configure_self_signed_cert()
179+
180+ mocked_run.assert_has_calls(
181+ [
182+ mock.call(f'{PROXY_CLI} config proxy.tls.cert="" proxy.tls.key=""'.split()),
183+ mock.call(f"{PROXY_CLI} import-certificate --selfsigned".split()),
184+ ],
185+ any_order=False,
186 )
187
188

Subscribers

People subscribed via source and target branches

to all changes: