Skip to content

Commit

Permalink
feat: enable integration with parca-cloud through integrator (#88)
Browse files Browse the repository at this point in the history
  • Loading branch information
jnsgruk committed Aug 17, 2023
1 parent e474128 commit be366e4
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 19 deletions.
2 changes: 2 additions & 0 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ description: |
requires:
profiling-endpoint:
interface: parca_scrape
external-parca-store-endpoint:
interface: parca_store

provides:
metrics-endpoint:
Expand Down
18 changes: 16 additions & 2 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ def __init__(self, *args):
self.framework.observe(self.on.remove, self._remove)
self.framework.observe(self.on.update_status, self._update_status)

# Enable the option to send profiles to a remote store (i.e. Polar Signals Cloud)
self.framework.observe(
self.on.external_parca_store_endpoint_relation_changed, self._configure_remote_store
)

# The profiling_consumer handles the relation that allows Parca to scrape other apps in the
# model that provide a "profiling-endpoint" relation
self.profiling_consumer = ProfilingEndpointConsumer(self)
Expand All @@ -52,6 +57,7 @@ def __init__(self, *args):
relation_name="self-profiling-endpoint",
)

# Enable Parca Agents to use this Parca instance as a remote store
self.parca_store_endpoint = ParcaStoreEndpointProvider(
charm=self, port=7070, insecure=True
)
Expand Down Expand Up @@ -93,7 +99,15 @@ def _config_changed(self, _):
"""Update the configuration files, restart parca."""
self.unit.status = ops.MaintenanceStatus("reconfiguring parca")
scrape_config = self.profiling_consumer.jobs()
self.parca.configure(self.config, scrape_config)
self.parca.configure(app_config=self.config, scrape_config=scrape_config)
self.unit.status = ops.ActiveStatus()

def _configure_remote_store(self, event):
"""Configure store with credentials passed over parca-external-store-endpoint relation."""
self.unit.status = ops.MaintenanceStatus("reconfiguring parca")
rel_data = event.relation.data[event.relation.app]
rel_keys = ["remote-store-address", "remote-store-bearer-token", "remote-store-insecure"]
self.parca.configure(store_config={k: rel_data.get(k, "") for k in rel_keys})
self.unit.status = ops.ActiveStatus()

def _remove(self, _):
Expand All @@ -104,7 +118,7 @@ def _remove(self, _):
def _on_profiling_targets_changed(self, _):
"""Update the Parca scrape configuration according to present relations."""
self.unit.status = ops.MaintenanceStatus("reconfiguring parca")
self.parca.configure(self.config, self.profiling_consumer.jobs())
self.parca.configure(app_config=self.config, scrape_config=self.profiling_consumer.jobs())
self.unit.status = ops.ActiveStatus()

def _open_port(self) -> bool:
Expand Down
38 changes: 29 additions & 9 deletions src/parca.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
"""Control Parca on a host system. Provides a Parca class."""

import logging
from pathlib import Path
from subprocess import check_output

import yaml
from charms.operator_libs_linux.v1 import snap
from charms.parca.v0.parca_config import ParcaConfig, parse_version

Expand All @@ -21,7 +23,7 @@ class Parca:
def install(self):
"""Install the Parca snap package."""
try:
self._snap.ensure(snap.SnapState.Latest, channel="stable")
self._snap.ensure(snap.SnapState.Latest, channel="edge")
snap.hold_refresh()
except snap.SnapError as e:
logger.error("could not install parca. Reason: %s", e.message)
Expand All @@ -45,17 +47,35 @@ def remove(self):
"""Remove the Parca snap, preserving config and data."""
self._snap.ensure(snap.SnapState.Absent)

def configure(self, app_config, scrape_configs=[], restart=True):
def configure(self, *, app_config=None, scrape_config=None, store_config=None, restart=True):
"""Configure Parca on the host system. Restart Parca by default."""
# Configure the snap appropriately
if app_config["enable-persistence"]:
self._snap.set({"enable-persistence": "true"})
if app_config:
if app_config.get("enable-persistence", None):
self._snap.set({"enable-persistence": "true"})
else:
limit = app_config["memory-storage-limit"] * 1048576
self._snap.set({"enable-persistence": "false", "storage-active-memory": limit})

if store_config:
if addr := store_config.get("remote-store-address", None):
self._snap.set({"remote-store-address": addr})

if token := store_config.get("remote-store-bearer-token", None):
self._snap.set({"remote-store-bearer-token": token})

if insecure := store_config.get("remote-store-insecure", None):
self._snap.set({"remote-store-insecure": insecure})

if scrape_config:
# If the scrape configs are explicitly set, then build the config from new
parca_config = ParcaConfig(scrape_config, profile_path=self.PROFILE_PATH)
else:
limit = app_config["memory-storage-limit"] * 1048576
self._snap.set({"enable-persistence": "false", "storage-active-memory": limit})
# Otherwise grab existing scrape jobs and build a config to include them
old = yaml.safe_load(Path(self.CONFIG_PATH).read_text())
parca_config = ParcaConfig(
old.get("scrape_configs", []), profile_path=self.PROFILE_PATH
)

# Write the config file
parca_config = ParcaConfig(scrape_configs, profile_path=self.PROFILE_PATH)
with open(self.CONFIG_PATH, "w+") as f:
f.write(str(parca_config))

Expand Down
78 changes: 73 additions & 5 deletions tests/functional/test_parca.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
from subprocess import check_call

import yaml
from charms.parca.v0.parca_config import ParcaConfig
from parca import Parca

Expand Down Expand Up @@ -45,23 +46,90 @@ def test_remove(self):
self.assertFalse(self.parca.installed)

def test_configure_systemd_storage_persist(self):
self.parca.configure({"enable-persistence": True})
self.parca.configure(app_config={"enable-persistence": True})
self.assertEqual(self.parca._snap.get("enable-persistence"), "true")

def test_configure_systemd_storage_in_memory(self):
self.parca.configure(DEFAULT_PARCA_CONFIG)
self.parca.configure(app_config=DEFAULT_PARCA_CONFIG)
self.assertEqual(self.parca._snap.get("enable-persistence"), "false")
self.assertEqual(self.parca._snap.get("storage-active-memory"), "1073741824")

def test_configure_parca_no_scrape_jobs(self):
self.parca.configure(DEFAULT_PARCA_CONFIG)
config = ParcaConfig([], profile_path="/var/snap/parca/current/profiles")
self.parca.configure(app_config=DEFAULT_PARCA_CONFIG)
old = yaml.safe_load(Path(self.parca.CONFIG_PATH).read_text())
config = ParcaConfig(
old.get("scrape_configs", []), profile_path="/var/snap/parca/current/profiles"
)
self.assertTrue(_file_content_equals_string(self.parca.CONFIG_PATH, str(config)))

def test_configure_parca_simple_scrape_jobs(self):
self.parca.configure(DEFAULT_PARCA_CONFIG, [{"metrics_path": "foobar", "bar": "baz"}])
self.parca.configure(
app_config=DEFAULT_PARCA_CONFIG,
scrape_config=[{"metrics_path": "foobar", "bar": "baz"}],
)
config = ParcaConfig(
[{"metrics_path": "foobar", "bar": "baz"}],
profile_path="/var/snap/parca/current/profiles",
)
self.assertTrue(_file_content_equals_string(self.parca.CONFIG_PATH, str(config)))

def test_configure_parca_store_config(self):
self.parca.configure(
store_config={
"remote-store-address": "grpc.polarsignals.com:443",
"remote-store-bearer-token": "deadbeef",
"remote-store-insecure": "false",
}
)
self.assertEqual(self.parca._snap.get("remote-store-address"), "grpc.polarsignals.com:443")
self.assertEqual(self.parca._snap.get("remote-store-bearer-token"), "deadbeef")
self.assertEqual(self.parca._snap.get("remote-store-insecure"), "false")

def test_configure_parca_store_config_no_conflict_with_app_config(self):
# Setup baseline config
self.parca.configure(app_config=DEFAULT_PARCA_CONFIG)
self.assertEqual(self.parca._snap.get("enable-persistence"), "false")
self.assertEqual(self.parca._snap.get("storage-active-memory"), "1073741824")

# Setup some store config
self.parca.configure(
store_config={
"remote-store-address": "grpc.polarsignals.com:443",
"remote-store-bearer-token": "deadbeef",
"remote-store-insecure": "false",
}
)

self.assertEqual(self.parca._snap.get("remote-store-address"), "grpc.polarsignals.com:443")
self.assertEqual(self.parca._snap.get("remote-store-bearer-token"), "deadbeef")
self.assertEqual(self.parca._snap.get("remote-store-insecure"), "false")

# Check we didn't mess with the app_config
self.assertEqual(self.parca._snap.get("enable-persistence"), "false")
self.assertEqual(self.parca._snap.get("storage-active-memory"), "1073741824")

def test_configure_parca_store_config_no_conflict_with_scrape_config(self):
self.parca.configure(
app_config=DEFAULT_PARCA_CONFIG,
scrape_config=[{"metrics_path": "foobar", "bar": "baz"}],
)
expected = ParcaConfig(
[{"metrics_path": "foobar", "bar": "baz"}],
profile_path="/var/snap/parca/current/profiles",
)
self.assertTrue(_file_content_equals_string(self.parca.CONFIG_PATH, str(expected)))

# Setup some store config
self.parca.configure(
store_config={
"remote-store-address": "grpc.polarsignals.com:443",
"remote-store-bearer-token": "deadbeef",
"remote-store-insecure": "false",
}
)

self.assertEqual(self.parca._snap.get("remote-store-address"), "grpc.polarsignals.com:443")
self.assertEqual(self.parca._snap.get("remote-store-bearer-token"), "deadbeef")
self.assertEqual(self.parca._snap.get("remote-store-insecure"), "false")

self.assertTrue(_file_content_equals_string(self.parca.CONFIG_PATH, str(expected)))
25 changes: 22 additions & 3 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_config_changed(self, configure):
"memory-storage-limit": 1024,
}
self.harness.update_config(config)
configure.assert_called_with(config, [])
configure.assert_called_with(app_config=config, scrape_config=[])
self.assertEqual(self.harness.charm.unit.status, ActiveStatus())

@patch("charm.Parca.remove")
Expand Down Expand Up @@ -192,9 +192,8 @@ def test_metrics_endpoint_relation(self, _):
}
self.assertEqual(unit_data, expected)

@patch("charm.Parca.configure")
@patch("ops.model.Model.get_binding", lambda *args: MockBinding("10.10.10.10"))
def test_parca_store_relation(self, _):
def test_parca_store_relation(self):
self.harness.set_leader(True)
# Create a relation to an app named "parca-agent"
rel_id = self.harness.add_relation("parca-store-endpoint", "parca-agent")
Expand All @@ -209,6 +208,26 @@ def test_parca_store_relation(self, _):
}
self.assertEqual(unit_data, expected)

@patch("charm.Parca.configure")
@patch("ops.model.Model.get_binding", lambda *args: MockBinding("10.10.10.10"))
def test_parca_external_store_relation(self, configure):
self.harness.set_leader(True)
# Create a relation to an app named "polar-signals-cloud"
rel_id = self.harness.add_relation("external-parca-store-endpoint", "polar-signals-cloud")
# Add a polar-signals-cloud unit
self.harness.add_relation_unit(rel_id, "polar-signals-cloud/0")
# Set some data from the remote application
store_config = {
"remote-store-address": "grpc.polarsignals.com:443",
"remote-store-bearer-token": "deadbeef",
"remote-store-insecure": "false",
}
self.harness.update_relation_data(rel_id, "polar-signals-cloud", store_config)

# Ensure that we call the configure method on Parca with the correct store details
configure.assert_called_with(store_config=store_config)
self.assertEqual(self.harness.charm.unit.status, ActiveStatus())


class MockBinding:
def __init__(self, addr):
Expand Down

0 comments on commit be366e4

Please sign in to comment.