← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/lp-codeimport:charm-codeimport-storage into lp-codeimport:master

 

Colin Watson has proposed merging ~cjwatson/lp-codeimport:charm-codeimport-storage into lp-codeimport:master with ~cjwatson/lp-codeimport:charmcraft as a prerequisite.

Commit message:
charm: Add an lp-codeimport-storage charm

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/lp-codeimport/+git/lp-codeimport/+merge/439271

This can be used to provide import data storage instead of the older approach of setting `bazaar_branch_store` and `foreign_tree_store` on `lp-codeimport`.  It isn't much more than a vanilla system with SSH key authorization and a couple of pre-created directories, but the new `codeimport-storage` relation makes it a little easier to set things up in a Mojo spec.

Data migration from any previous storage systems needs to be handled manually.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/lp-codeimport:charm-codeimport-storage into lp-codeimport:master.
diff --git a/charm/lp-codeimport-storage/README.md b/charm/lp-codeimport-storage/README.md
new file mode 100644
index 0000000..cad1a3b
--- /dev/null
+++ b/charm/lp-codeimport-storage/README.md
@@ -0,0 +1,3 @@
+# Launchpad code import storage
+
+This charm provides storage for the `lp-codeimport` charm.
diff --git a/charm/lp-codeimport-storage/charmcraft.yaml b/charm/lp-codeimport-storage/charmcraft.yaml
new file mode 100644
index 0000000..ac76470
--- /dev/null
+++ b/charm/lp-codeimport-storage/charmcraft.yaml
@@ -0,0 +1,44 @@
+type: charm
+bases:
+  - build-on:
+    - name: ubuntu
+      channel: "22.04"
+      architectures: [amd64]
+    run-on:
+    - name: ubuntu
+      channel: "22.04"
+      architectures: [amd64]
+parts:
+  charm-wheels:
+    source: https://git.launchpad.net/~ubuntuone-hackers/ols-charm-deps/+git/wheels
+    source-commit: "59b32ae07f98051385c96d6d8e7e02ca4f197fe5"
+    source-submodules: []
+    source-type: git
+    plugin: dump
+    organize:
+      "*": charm-wheels/
+    prime:
+      - "-charm-wheels"
+  ols-layers:
+    source: https://git.launchpad.net/ols-charm-deps
+    source-commit: "1ca8acbef7eb49b8a2cc81e5e13479b4f226a48b"
+    source-submodules: []
+    source-type: git
+    plugin: dump
+    organize:
+      "*": layers/
+    stage:
+      - layers
+    prime:
+      - "-layers"
+  lp-codeimport-storage:
+    after:
+      - charm-wheels
+      - ols-layers
+    source: .
+    plugin: reactive
+    build-snaps: [charm/2.x/stable]
+    build-environment:
+      - CHARM_LAYERS_DIR: $CRAFT_STAGE/layers/layer
+      - PIP_NO_INDEX: "true"
+      - PIP_FIND_LINKS: $CRAFT_STAGE/charm-wheels
diff --git a/charm/lp-codeimport-storage/config.yaml b/charm/lp-codeimport-storage/config.yaml
new file mode 100644
index 0000000..4f9deac
--- /dev/null
+++ b/charm/lp-codeimport-storage/config.yaml
@@ -0,0 +1,5 @@
+options:
+  public_ssh_key:
+    type: string
+    default: ""
+    description: Base64-encoded public SSH key of the code import workers.
diff --git a/charm/lp-codeimport-storage/hooks/relations/codeimport-storage/__init__.py b/charm/lp-codeimport-storage/hooks/relations/codeimport-storage/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charm/lp-codeimport-storage/hooks/relations/codeimport-storage/__init__.py
diff --git a/charm/lp-codeimport-storage/hooks/relations/codeimport-storage/provides.py b/charm/lp-codeimport-storage/hooks/relations/codeimport-storage/provides.py
new file mode 100644
index 0000000..c6b06a0
--- /dev/null
+++ b/charm/lp-codeimport-storage/hooks/relations/codeimport-storage/provides.py
@@ -0,0 +1,39 @@
+# Copyright 2023 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from charmhelpers.core import hookenv
+from charms.reactive import (
+    Endpoint,
+    remove_state,
+    set_state,
+    when,
+    when_not,
+    )
+
+
+class CodeImportStorageProvides(Endpoint):
+    @when("endpoint.{endpoint_name}.joined")
+    @when_not("endpoint.{endpoint_name}.available")
+    def joined(self):
+        for relation in self.relations:
+            ingress_address = hookenv.network_get(
+                relation.endpoint_name, relation_id=relation.relation_id
+            )["ingress-addresses"][0]
+            relation.to_publish[
+                "bazaar_branch_store"
+            ] = f"sftp://importd@{ingress_address}/srv/importd/www/";
+            relation.to_publish[
+                "foreign_tree_store"
+            ] = f"sftp://importd@{ingress_address}/srv/importd/sources/";
+        set_state(self.expand_name("endpoint.{endpoint_name}.available"))
+
+    @when("endpoint.{endpoint_name}.available")
+    @when_not("endpoint.{endpoint_name}.joined")
+    def departed(self):
+        remove_state(self.expand_name("endpoint.{endpoint_name}.available"))
+
+    def get_codeimport_subnets(self):
+        subnets = set()
+        for unit in self.all_joined_units:
+            subnets.update(unit.received_raw["egress-subnets"].split(","))
+        return sorted(subnets)
diff --git a/charm/lp-codeimport-storage/layer.yaml b/charm/lp-codeimport-storage/layer.yaml
new file mode 100644
index 0000000..590c59f
--- /dev/null
+++ b/charm/lp-codeimport-storage/layer.yaml
@@ -0,0 +1,2 @@
+includes:
+    - layer:basic
diff --git a/charm/lp-codeimport-storage/metadata.yaml b/charm/lp-codeimport-storage/metadata.yaml
new file mode 100644
index 0000000..95de7f8
--- /dev/null
+++ b/charm/lp-codeimport-storage/metadata.yaml
@@ -0,0 +1,14 @@
+name: lp-codeimport-storage
+display-name: lp-codeimport-storage
+summary: Launchpad code import storage
+maintainer: Colin Watson <cjwatson@xxxxxxxxxxxxx>
+description: Storage for use with lp-codeimport workers.
+tags:
+  # https://juju.is/docs/charm-metadata#heading--charm-store-fields
+  - network
+series:
+  - jammy
+subordinate: false
+provides:
+  codeimport-storage:
+    interface: codeimport-storage
diff --git a/charm/lp-codeimport-storage/reactive/lp-codeimport-storage.py b/charm/lp-codeimport-storage/reactive/lp-codeimport-storage.py
new file mode 100644
index 0000000..5be4349
--- /dev/null
+++ b/charm/lp-codeimport-storage/reactive/lp-codeimport-storage.py
@@ -0,0 +1,74 @@
+# Copyright 2023 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+import base64
+import os.path
+
+from charmhelpers.core import (
+    hookenv,
+    host,
+    templating,
+    )
+from charms.reactive import (
+    endpoint_from_flag,
+    hook,
+    remove_state,
+    set_state,
+    when,
+    when_not,
+    )
+
+
+@hook("upgrade-charm")
+def upgrade_charm():
+    remove_state("service.configured")
+
+
+@hook("config.changed")
+def config_changed():
+    remove_state("service.configured")
+
+
+@hook(
+    "{provides:codeimport-storage}-relation-{joined,changed,broken,departed}"
+)
+def codeimport_storage_changed(*args):
+    remove_state("service.configured")
+
+
+@when("config.set.public_ssh_key", "endpoint.codeimport-storage.available")
+@when_not("service.configured")
+def configure():
+    codeimport_storage = endpoint_from_flag(
+        "endpoint.codeimport-storage.available"
+    )
+    hookenv.log("Creating service user")
+    host.add_group("importd")
+    host.adduser("importd", primary_group="importd")
+    for directory in ("/srv/importd/sources", "/srv/importd/www"):
+        if not os.path.exists(directory):
+            host.mkdir(
+                directory, owner="importd", group="importd", perms=0o755
+            )
+    ssh_dir = "/home/importd/.ssh"
+    if not os.path.exists(ssh_dir):
+        host.mkdir(ssh_dir, owner="importd", group="importd", perms=0o700)
+    config = dict(hookenv.config())
+    config["codeimport_subnets"] = codeimport_storage.get_codeimport_subnets()
+    config["public_ssh_key"] = base64.b64decode(
+        config["public_ssh_key"].encode("ASCII")
+    ).decode("ASCII")
+    templating.render(
+        "authorized_keys.j2",
+        os.path.join(ssh_dir, "authorized_keys"),
+        config,
+        owner="importd",
+        group="importd",
+        perms=0o600,
+    )
+    set_state("service.configured")
+
+
+@when("service.configured")
+def check_is_running():
+    hookenv.status_set("active", "Ready")
diff --git a/charm/lp-codeimport-storage/templates/authorized_keys.j2 b/charm/lp-codeimport-storage/templates/authorized_keys.j2
new file mode 100644
index 0000000..77d398e
--- /dev/null
+++ b/charm/lp-codeimport-storage/templates/authorized_keys.j2
@@ -0,0 +1,2 @@
+restrict,from="{{ codeimport_subnets|join(",") }}" {{ public_ssh_key }}
+
diff --git a/charm/lp-codeimport/config.yaml b/charm/lp-codeimport/config.yaml
index cac88ce..37f990c 100644
--- a/charm/lp-codeimport/config.yaml
+++ b/charm/lp-codeimport/config.yaml
@@ -11,13 +11,16 @@ options:
       default is to use the global CA infrastructure.
   bazaar_branch_store:
     type: string
-    default: sftp://hoover@xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/srv/importd/www/
-    description: Where the Bazaar imports are stored.
+    default: ""
+    description: >
+      Where the Bazaar imports are stored.  If unset, rely on the
+      codeimport-storage relation instead.
   foreign_tree_store:
     type: string
-    default: sftp://hoover@xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/srv/importd/sources/
+    default: ""
     description: >
-      Where the tarballs of foreign branches are uploaded for storage.
+      Where the tarballs of foreign branches are uploaded for storage.  If
+      unset, rely on the codeimport-storage relation instead.
   private_ssh_key:
     type: string
     default: ""
diff --git a/charm/lp-codeimport/hooks/relations/codeimport-storage/__init__.py b/charm/lp-codeimport/hooks/relations/codeimport-storage/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charm/lp-codeimport/hooks/relations/codeimport-storage/__init__.py
diff --git a/charm/lp-codeimport/hooks/relations/codeimport-storage/requires.py b/charm/lp-codeimport/hooks/relations/codeimport-storage/requires.py
new file mode 100644
index 0000000..16e31d4
--- /dev/null
+++ b/charm/lp-codeimport/hooks/relations/codeimport-storage/requires.py
@@ -0,0 +1,32 @@
+# Copyright 2023 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from charms.reactive import (
+    clear_flag,
+    Endpoint,
+    toggle_flag,
+    when,
+    when_not,
+    )
+
+
+class CodeImportStorageRequires(Endpoint):
+    @when("endpoint.{endpoint_name}.changed")
+    def joined(self):
+        toggle_flag(
+            self.expand_name("endpoint.{endpoint_name}.available"),
+            bool(self.bazaar_branch_store) and bool(self.foreign_tree_store),
+        )
+
+    @when("endpoint.{endpoint_name}.available")
+    @when_not("endpoint.{endpoint_name}.joined")
+    def departed(self):
+        clear_flag(self.expand_name("endpoint.{endpoint_name}.available"))
+
+    @property
+    def bazaar_branch_store(self):
+        return self.all_joined_units.received["bazaar_branch_store"]
+
+    @property
+    def foreign_tree_store(self):
+        return self.all_joined_units.received["foreign_tree_store"]
diff --git a/charm/lp-codeimport/metadata.yaml b/charm/lp-codeimport/metadata.yaml
index 6edc5b9..9df966d 100644
--- a/charm/lp-codeimport/metadata.yaml
+++ b/charm/lp-codeimport/metadata.yaml
@@ -9,6 +9,9 @@ tags:
 series:
   - bionic
 subordinate: false
+requires:
+  codeimport-storage:
+    interface: codeimport-storage
 resources:
   lp-codeimport:
     type: file
diff --git a/charm/lp-codeimport/reactive/lp-codeimport.py b/charm/lp-codeimport/reactive/lp-codeimport.py
index 04df2a6..9c7c9e7 100644
--- a/charm/lp-codeimport/reactive/lp-codeimport.py
+++ b/charm/lp-codeimport/reactive/lp-codeimport.py
@@ -19,9 +19,12 @@ from charmhelpers.core import (
     templating,
     )
 from charms.reactive import (
+    endpoint_from_flag,
     remove_state,
     set_state,
     when,
+    when_any,
+    when_none,
     when_not,
     )
 from ols import base
@@ -166,9 +169,51 @@ def configure_rsync(config):
         raise RuntimeError('Failed to restart rsync')
 
 
-@when('ols.configured')
+@when_any(
+    "endpoint.codeimport-storage.available",
+    "config.set.bazaar_branch_store",
+)
+def bazaar_branch_store_available():
+    set_state("service.bazaar_branch_store.available")
+
+
+@when_none(
+    "endpoint.codeimport-storage.available",
+    "config.set.bazaar_branch_store",
+)
+def bazaar_branch_store_unavailable():
+    remove_state("service.bazaar_branch_store.available")
+    remove_state("service.configured")
+
+
+@when_any(
+    "endpoint.codeimport-storage.available",
+    "config.set.foreign_tree_store",
+)
+def foreign_tree_store_available():
+    set_state("service.foreign_tree_store.available")
+
+
+@when_none(
+    "endpoint.codeimport-storage.available",
+    "config.set.foreign_tree_store",
+)
+def foreign_tree_store_unavailable():
+    remove_state("service.foreign_tree_store.available")
+    remove_state("service.configured")
+
+
+@when(
+    "ols.configured",
+    "service.bazaar_branch_store.available",
+    "service.foreign_tree_store.available",
+)
 @when_not('service.configured')
 def configure():
+    codeimport_storage = endpoint_from_flag(
+        "endpoint.codeimport-storage.available"
+    )
+
     ensure_lp_directories()
 
     system_packages = os.path.join(base.code_dir(), 'system-packages.txt')
@@ -188,6 +233,14 @@ def configure():
     config_path = os.path.join(
         base.code_dir(), 'production-configs', 'charm', 'codeimport-lazr.conf')
     svc_config = dict(config)
+    if not svc_config["bazaar_branch_store"]:
+        svc_config["bazaar_branch_store"] = (
+            codeimport_storage.bazaar_branch_store
+        )
+    if not svc_config["foreign_tree_store"]:
+        svc_config["foreign_tree_store"] = (
+            codeimport_storage.foreign_tree_store
+        )
     svc_config.update({
         'base_dir': base.base_dir(),
         'code_dir': base.code_dir(),
@@ -199,7 +252,7 @@ def configure():
         'home_dir': home_dir(),
         'user': base.user(),
         'code_import_storage_host': (
-            urlparse(config['bazaar_branch_store']).hostname),
+            urlparse(svc_config['bazaar_branch_store']).hostname),
         # Chosen to allow distributing dispatch start time over a 30-second
         # interval.
         'dispatch_offset': host.modulo_distribution(modulo=6, wait=5),