From 266e3cdd30a927595673d3cdcc1cf3a6c904a65a Mon Sep 17 00:00:00 2001 From: matt-hagemann Date: Thu, 5 Oct 2023 11:10:27 +0200 Subject: [PATCH 01/15] ci: basic integration tests --- .github/workflows/build-charm.yaml | 22 ++ vm_operator/charmcraft.yaml | 1 + vm_operator/config.yaml | 2 +- .../ratings_api/ratings_features_user_pb2.py | 43 ++++ .../ratings_features_user_pb2_grpc.py | 199 ++++++++++++++++++ vm_operator/src/charm.py | 5 + vm_operator/templates/ratings-service.j2 | 3 + vm_operator/tests/integration/conftest.py | 12 ++ vm_operator/tests/integration/test_charm.py | 63 ++++++ vm_operator/tests/unit/test_charm.py | 11 +- vm_operator/tox.ini | 17 ++ 11 files changed, 369 insertions(+), 9 deletions(-) create mode 100644 vm_operator/lib/ratings_api/ratings_features_user_pb2.py create mode 100644 vm_operator/lib/ratings_api/ratings_features_user_pb2_grpc.py create mode 100644 vm_operator/tests/integration/conftest.py create mode 100644 vm_operator/tests/integration/test_charm.py diff --git a/.github/workflows/build-charm.yaml b/.github/workflows/build-charm.yaml index ce1fdeb..1d966e1 100644 --- a/.github/workflows/build-charm.yaml +++ b/.github/workflows/build-charm.yaml @@ -29,3 +29,25 @@ jobs: run: | cd vm_operator tox -e unit + + integration-test: + name: Integration tests + runs-on: ubuntu-22.04 + needs: + - lint + - unit-test + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup hosts for tests + run: | + echo "10.64.140.43 testing-ratings.foo.bar" | sudo tee -a /etc/hosts + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@main + with: + provider: lxd + juju-channel: 3.2/stable + - name: Run integration tests + run: | + cd vm_operator + tox -e integration -- --model=testing diff --git a/vm_operator/charmcraft.yaml b/vm_operator/charmcraft.yaml index 6c7da6b..d7d03e0 100644 --- a/vm_operator/charmcraft.yaml +++ b/vm_operator/charmcraft.yaml @@ -14,3 +14,4 @@ parts: charm: charm-binary-python-packages: - psycopg[binary] + - PyYAML diff --git a/vm_operator/config.yaml b/vm_operator/config.yaml index ec4af4b..50dc219 100644 --- a/vm_operator/config.yaml +++ b/vm_operator/config.yaml @@ -11,4 +11,4 @@ options: default: "https://github.com/matthew-hagemann/app-center-ratings" squid-proxy-url: type: string - default: "http://proxy.example.com" + default: "" diff --git a/vm_operator/lib/ratings_api/ratings_features_user_pb2.py b/vm_operator/lib/ratings_api/ratings_features_user_pb2.py new file mode 100644 index 0000000..8e25b6c --- /dev/null +++ b/vm_operator/lib/ratings_api/ratings_features_user_pb2.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ratings_features_user.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bratings_features_user.proto\x12\x15ratings.features.user\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"!\n\x13\x41uthenticateRequest\x12\n\n\x02id\x18\x01 \x01(\t\"%\n\x14\x41uthenticateResponse\x12\r\n\x05token\x18\x01 \x01(\t\",\n\x12ListMyVotesRequest\x12\x16\n\x0esnap_id_filter\x18\x01 \x01(\t\"A\n\x13ListMyVotesResponse\x12*\n\x05votes\x18\x01 \x03(\x0b\x32\x1b.ratings.features.user.Vote\"&\n\x13GetSnapVotesRequest\x12\x0f\n\x07snap_id\x18\x01 \x01(\t\"B\n\x14GetSnapVotesResponse\x12*\n\x05votes\x18\x01 \x03(\x0b\x32\x1b.ratings.features.user.Vote\"n\n\x04Vote\x12\x0f\n\x07snap_id\x18\x01 \x01(\t\x12\x15\n\rsnap_revision\x18\x02 \x01(\x05\x12\x0f\n\x07vote_up\x18\x03 \x01(\x08\x12-\n\ttimestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"F\n\x0bVoteRequest\x12\x0f\n\x07snap_id\x18\x01 \x01(\t\x12\x15\n\rsnap_revision\x18\x02 \x01(\x05\x12\x0f\n\x07vote_up\x18\x03 \x01(\x08\x32\xc6\x03\n\x04User\x12i\n\x0c\x41uthenticate\x12*.ratings.features.user.AuthenticateRequest\x1a+.ratings.features.user.AuthenticateResponse\"\x00\x12:\n\x06\x44\x65lete\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x04Vote\x12\".ratings.features.user.VoteRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x0bListMyVotes\x12).ratings.features.user.ListMyVotesRequest\x1a*.ratings.features.user.ListMyVotesResponse\"\x00\x12i\n\x0cGetSnapVotes\x12*.ratings.features.user.GetSnapVotesRequest\x1a+.ratings.features.user.GetSnapVotesResponse\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ratings_features_user_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_AUTHENTICATEREQUEST']._serialized_start=116 + _globals['_AUTHENTICATEREQUEST']._serialized_end=149 + _globals['_AUTHENTICATERESPONSE']._serialized_start=151 + _globals['_AUTHENTICATERESPONSE']._serialized_end=188 + _globals['_LISTMYVOTESREQUEST']._serialized_start=190 + _globals['_LISTMYVOTESREQUEST']._serialized_end=234 + _globals['_LISTMYVOTESRESPONSE']._serialized_start=236 + _globals['_LISTMYVOTESRESPONSE']._serialized_end=301 + _globals['_GETSNAPVOTESREQUEST']._serialized_start=303 + _globals['_GETSNAPVOTESREQUEST']._serialized_end=341 + _globals['_GETSNAPVOTESRESPONSE']._serialized_start=343 + _globals['_GETSNAPVOTESRESPONSE']._serialized_end=409 + _globals['_VOTE']._serialized_start=411 + _globals['_VOTE']._serialized_end=521 + _globals['_VOTEREQUEST']._serialized_start=523 + _globals['_VOTEREQUEST']._serialized_end=593 + _globals['_USER']._serialized_start=596 + _globals['_USER']._serialized_end=1050 +# @@protoc_insertion_point(module_scope) diff --git a/vm_operator/lib/ratings_api/ratings_features_user_pb2_grpc.py b/vm_operator/lib/ratings_api/ratings_features_user_pb2_grpc.py new file mode 100644 index 0000000..71217b9 --- /dev/null +++ b/vm_operator/lib/ratings_api/ratings_features_user_pb2_grpc.py @@ -0,0 +1,199 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 +import ratings_features_user_pb2 as ratings__features__user__pb2 + + +class UserStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Authenticate = channel.unary_unary( + '/ratings.features.user.User/Authenticate', + request_serializer=ratings__features__user__pb2.AuthenticateRequest.SerializeToString, + response_deserializer=ratings__features__user__pb2.AuthenticateResponse.FromString, + ) + self.Delete = channel.unary_unary( + '/ratings.features.user.User/Delete', + request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.Vote = channel.unary_unary( + '/ratings.features.user.User/Vote', + request_serializer=ratings__features__user__pb2.VoteRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.ListMyVotes = channel.unary_unary( + '/ratings.features.user.User/ListMyVotes', + request_serializer=ratings__features__user__pb2.ListMyVotesRequest.SerializeToString, + response_deserializer=ratings__features__user__pb2.ListMyVotesResponse.FromString, + ) + self.GetSnapVotes = channel.unary_unary( + '/ratings.features.user.User/GetSnapVotes', + request_serializer=ratings__features__user__pb2.GetSnapVotesRequest.SerializeToString, + response_deserializer=ratings__features__user__pb2.GetSnapVotesResponse.FromString, + ) + + +class UserServicer(object): + """Missing associated documentation comment in .proto file.""" + + def Authenticate(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Delete(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Vote(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListMyVotes(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetSnapVotes(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_UserServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Authenticate': grpc.unary_unary_rpc_method_handler( + servicer.Authenticate, + request_deserializer=ratings__features__user__pb2.AuthenticateRequest.FromString, + response_serializer=ratings__features__user__pb2.AuthenticateResponse.SerializeToString, + ), + 'Delete': grpc.unary_unary_rpc_method_handler( + servicer.Delete, + request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'Vote': grpc.unary_unary_rpc_method_handler( + servicer.Vote, + request_deserializer=ratings__features__user__pb2.VoteRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'ListMyVotes': grpc.unary_unary_rpc_method_handler( + servicer.ListMyVotes, + request_deserializer=ratings__features__user__pb2.ListMyVotesRequest.FromString, + response_serializer=ratings__features__user__pb2.ListMyVotesResponse.SerializeToString, + ), + 'GetSnapVotes': grpc.unary_unary_rpc_method_handler( + servicer.GetSnapVotes, + request_deserializer=ratings__features__user__pb2.GetSnapVotesRequest.FromString, + response_serializer=ratings__features__user__pb2.GetSnapVotesResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'ratings.features.user.User', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class User(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Authenticate(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/Authenticate', + ratings__features__user__pb2.AuthenticateRequest.SerializeToString, + ratings__features__user__pb2.AuthenticateResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Delete(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/Delete', + google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Vote(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/Vote', + ratings__features__user__pb2.VoteRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListMyVotes(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/ListMyVotes', + ratings__features__user__pb2.ListMyVotesRequest.SerializeToString, + ratings__features__user__pb2.ListMyVotesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetSnapVotes(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/ratings.features.user.User/GetSnapVotes', + ratings__features__user__pb2.GetSnapVotesRequest.SerializeToString, + ratings__features__user__pb2.GetSnapVotesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/vm_operator/src/charm.py b/vm_operator/src/charm.py index d7cd399..6e3c4e8 100755 --- a/vm_operator/src/charm.py +++ b/vm_operator/src/charm.py @@ -30,6 +30,8 @@ UNIT_PATH = Path("/etc/systemd/system/ratings.service") CARGO_PATH = Path(environ.get("HOME", "/root")) / ".cargo/bin/cargo" APP_PORT = 443 +APP_NAME = "ratings" +APP_HOST = "0.0.0.0" class RatingsCharm(ops.CharmBase): @@ -94,8 +96,11 @@ def _render_systemd_unit(self): rendered = template.render( project_root=APP_PATH, app_env=self.config["app-env"], + app_host=APP_HOST, app_jwt_secret=jwt_secret, app_log_level=self.config["app-log-level"], + app_name=APP_NAME, + app_port=APP_PORT, app_postgres_uri=connection_string, app_migration_postgres_uri=connection_string, ) diff --git a/vm_operator/templates/ratings-service.j2 b/vm_operator/templates/ratings-service.j2 index a3d990b..6e88987 100644 --- a/vm_operator/templates/ratings-service.j2 +++ b/vm_operator/templates/ratings-service.j2 @@ -4,8 +4,11 @@ After=network.target [Service] Environment="APP_ENV={{ app_env }}" +Environment="APP_HOST={{ app_host }}" Environment="APP_JWT_SECRET={{ app_jwt_secret }}" Environment="APP_LOG_LEVEL={{ app_log_level }}" +Environment="APP_NAME={{ app_name }}" +Environment="APP_PORT={{ app_port }}" Environment="APP_POSTGRES_URI={{ app_postgres_uri }}" Environment="APP_MIGRATION_POSTGRES_URI={{ app_migration_postgres_uri }}" WorkingDirectory = {{ project_root }} diff --git a/vm_operator/tests/integration/conftest.py b/vm_operator/tests/integration/conftest.py new file mode 100644 index 0000000..3314655 --- /dev/null +++ b/vm_operator/tests/integration/conftest.py @@ -0,0 +1,12 @@ +# Copyright 2021 Canonical Ltd. +# See LICENSE file for licensing details. + +from pytest import fixture +from pytest_operator.plugin import OpsTest + + +@fixture(scope="module") +async def ratings_charm(ops_test: OpsTest): + """Ratings charm used for integration testing.""" + charm = await ops_test.build_charm(".") + return charm diff --git a/vm_operator/tests/integration/test_charm.py b/vm_operator/tests/integration/test_charm.py new file mode 100644 index 0000000..c8f76a3 --- /dev/null +++ b/vm_operator/tests/integration/test_charm.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. + +import asyncio +import secrets + +import grpc +import ratings_api.ratings_features_user_pb2 as pb2 +import ratings_api.ratings_features_user_pb2_grpc as pb2_grpc +from pytest import mark +from pytest_operator.plugin import OpsTest + +RATINGS = "ratings" +UNIT_0 = f"{RATINGS}/0" +DB = "db" + + +@mark.abort_on_fail +@mark.skip_if_deployed +async def test_deploy(ops_test: OpsTest, ratings_charm): + await ops_test.model.deploy(await ratings_charm, application_name=RATINGS) + # issuing dummy update_status just to trigger an event + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[RATINGS], status="active", timeout=1000) + assert ops_test.model.applications[RATINGS].units[0].workload_status == "active" + + +@mark.abort_on_fail +async def test_database_relation(ops_test: OpsTest): + """Test that the charm can be successfully related to PostgreSQL.""" + await asyncio.gather( + ops_test.model.deploy("postgresql", channel="edge", application_name=DB, trust=True), + ops_test.model.wait_for_idle( + apps=[DB], status="active", raise_on_blocked=True, timeout=1000 + ), + ) + + await asyncio.gather( + ops_test.model.integrate(RATINGS, DB), + ops_test.model.wait_for_idle( + apps=[RATINGS], status="active", raise_on_blocked=True, timeout=1000 + ), + ) + + +@mark.abort_on_fail +async def test_ratings_register_user(ops_test: OpsTest): + """End-to-end test to ensure the app can interact with the database.""" + # Introduce a wait (e.g., 300 seconds or 5 minutes) + status = await ops_test.model.get_status() # noqa: F821 + unit = list(status.applications[RATINGS].units)[0] + print(f"Connecting to address: {status}") + address = status["applications"][RATINGS]["units"][unit]["public-address"] + print(f"Connecting to address: {address}") + connection_string = f"{address}:443" + + channel = grpc.insecure_channel(connection_string) + stub = pb2_grpc.UserStub(channel) + message = pb2.AuthenticateRequest(id=secrets.token_hex(32)) + print(f"Message sent: {message}") + response = stub.Authenticate(message) + assert response.token diff --git a/vm_operator/tests/unit/test_charm.py b/vm_operator/tests/unit/test_charm.py index 71029f8..b569447 100644 --- a/vm_operator/tests/unit/test_charm.py +++ b/vm_operator/tests/unit/test_charm.py @@ -31,8 +31,11 @@ def __init__(self, id, name="database"): [Service] Environment="APP_ENV=dev" +Environment="APP_HOST=0.0.0.0" Environment="APP_JWT_SECRET=" Environment="APP_LOG_LEVEL=info" +Environment="APP_NAME=ratings" +Environment="APP_PORT=443" Environment="APP_POSTGRES_URI=" Environment="APP_MIGRATION_POSTGRES_URI=" WorkingDirectory = /srv/app @@ -121,10 +124,6 @@ def test_setup_application(self, _rmtree, _path, _clone, _check): self.assertEqual(APP_PATH, Path("/srv/app")) - # Check squid proxy is set correctly - self.assertEqual(os.environ.get("HTTP_PROXY"), "http://proxy.example.com") - self.assertEqual(os.environ.get("HTTPS_PROXY"), "http://proxy.example.com") - # Ensure we set the charm status correctly self.assertEqual( self.harness.charm.unit.status, MaintenanceStatus("Code fetched, building now.") @@ -295,10 +294,6 @@ def test_on_pull_and_rebuild(self, _mock_repo, _check, _restart): # Run the handler self.harness.charm._on_pull_and_rebuild(mock_event) - # Check squid proxy is set correctly - self.assertEqual(os.environ.get("HTTP_PROXY"), "http://proxy.example.com") - self.assertEqual(os.environ.get("HTTPS_PROXY"), "http://proxy.example.com") - # Ensure we clone the repo mock_pull.assert_called() diff --git a/vm_operator/tox.ini b/vm_operator/tox.ini index 8f40f5c..48ebd4c 100644 --- a/vm_operator/tox.ini +++ b/vm_operator/tox.ini @@ -63,3 +63,20 @@ commands = {posargs} \ {[vars]tests_path}/unit coverage report + +[testenv:integration] +description = Run integration tests +deps = + pytest + tenacity + juju + pytest-operator + grpcio + -r {tox_root}/requirements.txt +commands = + pytest -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {posargs} \ + {[vars]tests_path}/integration/test_charm.py From 55eeef34483f5b1da1ac079cbe53a1bf26b12ffa Mon Sep 17 00:00:00 2001 From: matt-hagemann Date: Sun, 8 Oct 2023 08:47:02 +0200 Subject: [PATCH 02/15] feat(snap): add snap for ratings service --- snap/hooks/configure | 23 ++++++++++++++++ snap/local/ratings_wrapper | 8 ++++++ snap/snapcraft.yaml | 54 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 10 ++++++- src/utils/migrator.rs | 14 +++++++++- 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100755 snap/hooks/configure create mode 100755 snap/local/ratings_wrapper create mode 100644 snap/snapcraft.yaml diff --git a/snap/hooks/configure b/snap/hooks/configure new file mode 100755 index 0000000..067fc2f --- /dev/null +++ b/snap/hooks/configure @@ -0,0 +1,23 @@ +#!/bin/sh + +set -eu + +app_jwt_secret="$(snapctl get app-jwt-secret)" +if [ -z "$app_jwt_secret" ]; then + snapctl set app-jwt-secret=deadbeef +fi + +app_log_level="$(snapctl get app-log-level)" +if [ -z "$app_log_level" ]; then + snapctl set app-log-level=info +fi + +app_postgres_uri="$(snapctl get app-postgres-uri)" +if [ -z "$app_postgres_uri" ]; then + snapctl set app-postgres-uri=postgresql://service:covfefe!1@localhost:5433/ratings +fi + +app_migration_postgres_uri="$(snapctl get app-migration-postgres-uri)" +if [ -z "$app_migration_postgres_uri" ]; then + snapctl set app-migration-postgres-uri=postgresql://migration_user:strongpassword@localhost:5433/ratings +fi diff --git a/snap/local/ratings_wrapper b/snap/local/ratings_wrapper new file mode 100755 index 0000000..6311503 --- /dev/null +++ b/snap/local/ratings_wrapper @@ -0,0 +1,8 @@ +#!/bin/sh + +export APP_JWT_SECRET=$(snapctl get app-jwt-secret) +export APP_LOG_LEVEL=$(snapctl get app-log-level) +export APP_POSTGRES_URI=$(snapctl get app-postgres-uri) +export APP_MIGRATION_POSTGRES_URI=$(snapctl get app-migration-postgres-uri) + +exec $SNAP/bin/ratings diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..ca6b06c --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,54 @@ +name: ratings +base: core20 +version: '1.3' +license: GPL-3.0 +summary: Ubuntu App Ratings Service +description: | + Backend service to support application ratings in the new Ubuntu Software Centre. + +grade: stable +confinement: strict + +apps: + ratings-svc: + command: ratings_wrapper + daemon: simple + plugs: + - network + - network-bind + +environment: + APP_ENV: dev + APP_HOST: 0.0.0.0 + APP_NAME: ratings + APP_PORT: 443 + +parts: + rust-deps: + plugin: nil + override-pull: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + + ratings: + after: [ rust-deps ] + plugin: rust + build-packages: + - libssl-dev + - pkg-config + build-snaps: + - protobuf + source: . + + migrations: + plugin: dump + source: ./sql + + wrapper: + plugin: dump + source: ./snap/local + source-type: local + override-build: | + # Copy the wrapper into place + cp $SNAPCRAFT_PROJECT_DIR/snap/local/ratings_wrapper $SNAPCRAFT_PART_INSTALL/ + + chmod 0755 $SNAPCRAFT_PART_INSTALL/ratings_wrapper diff --git a/src/main.rs b/src/main.rs index ba84b6a..41cd254 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,7 @@ +use std::env; + +use tracing::info; + mod app; mod features; mod utils; @@ -11,8 +15,12 @@ async fn main() -> Result<(), Box> { .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE) .init(); - tracing::info!("Starting Ubuntu App Rating Service"); + tracing::info!("Starting THE Ubuntu App Rating Service"); + match env::current_dir() { + Ok(cur_dir) => info!("Current directory: {}", cur_dir.display()), + Err(e) => info!("Error retrieving current directory: {:?}", e), + } app::run(config).await?; Ok(()) diff --git a/src/utils/migrator.rs b/src/utils/migrator.rs index 4023de7..614a563 100644 --- a/src/utils/migrator.rs +++ b/src/utils/migrator.rs @@ -1,8 +1,10 @@ +use std::env; use std::error::Error; use std::fmt::{Debug, Formatter}; use std::sync::Arc; use sqlx::{postgres::PgPoolOptions, PgPool}; +use tracing::info; const MIGRATIONS_PATH: &str = "./sql/migrations"; @@ -19,8 +21,18 @@ impl Migrator { Ok(Migrator { pool }) } + fn migrations_path() -> String { + let snap_path = std::env::var("SNAP").unwrap_or("./sql".to_string()); + format!("{}/migrations", snap_path) + } + pub async fn run(&self) -> Result<(), sqlx::Error> { - let m = sqlx::migrate::Migrator::new(std::path::Path::new(MIGRATIONS_PATH)).await?; + match env::current_dir() { + Ok(cur_dir) => info!("Current directory: {}", cur_dir.display()), + Err(e) => info!("Error retrieving current directory: {:?}", e), + } + let m = + sqlx::migrate::Migrator::new(std::path::Path::new(&Self::migrations_path())).await?; m.run(&mut self.pool.acquire().await?).await?; Ok(()) From 971d33ae9b31746b6488bba72748037ce380e38d Mon Sep 17 00:00:00 2001 From: matt-hagemann Date: Mon, 9 Oct 2023 16:36:15 +0200 Subject: [PATCH 03/15] docs(snap): readme.md for snap dir --- snap/README.md | 35 +++++++++++++++++++++++++++++++++++ snap/hooks/configure | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 snap/README.md diff --git a/snap/README.md b/snap/README.md new file mode 100644 index 0000000..2fdf0eb --- /dev/null +++ b/snap/README.md @@ -0,0 +1,35 @@ +# App Center Ratings Snap + +This directory contains files used to build the App Center Ratings snap. + +## Installation + +The snap provides a wrapper used to collect environment variables via `snapctl` in order for them to be configurable post the snap being built. + +You can install the App Center Ratings snap manually like so: + +```bash +# Install the snap +$ sudo snap install ratings --channel stable + +# Update an environment variable +sudo snap set ratings app-log-level=debug + +# Restart the service to reload environment variables into the service +sudo snap restart ratings +``` + +## Configuration + +This Snap is intended to run within a charm where certain configuration settings are unknown until the charm installs the snap and establishes relations to a database. + +The list of config options are: + +| Name | Options | Default | Description | +|:----------------------------|:--------------------------------|:---------------------------------------------------------------|:------------------------| +| `app-jwt-secret` | Any string | `deadbeaf` | JWT secret | +| `app-log-level` | `error`, `warn`, `info`, `debug` | `info` | Log level | +| `app-postgres-uri` | Any string | `postgresql://migration_user:strongpassword@localhost:5433/ratings` | Service connection | +| `app-migration-postgres-uri`| Any string | `postgresql://migration_user:strongpassword@localhost:5433/ratings` | Migrator connection | + +Config options can be set using: `sudo snap set ratings