Skip to content

multiple substrates design

René Radoi edited this page Jan 9, 2026 · 1 revision

What would stop us from using a single charm for both VM and K8s substrates?

How to detect substrate type?

  • can be done via ops.model.get_cloud_spec()
  • cloud_spec.type returns either kubernetes or something else (e.g. lxd)
  • everything else than kubernetes means 'vm'

Different events (ex. pebble_ready)

  • can be added to the charm code
  • will not be received on the other substrate -> logic must take this into consideration

Substrate-specific handling for files, services, snaps, etc.

  • in charm structure pattern, there is already an abstract base class for workload, with overrides per substrate
  • approach: include a substrate-specific class for K8s and for VM into the same charm code
  • differentiate which class to instantiate in charm class init: if cloud_spec.type == kubernetes -> K8sWorkload class, otherwise VmWorkload class
  • within the substrate-specific class, implement things like: run subprocess, install snap, define pebble service layer
  • because of difference between snap paths and rock paths, workload paths need to be specific per workload class

Substrate-specific handling for upgrades

  • same approach as for workload: differentiate class to instantiate by cloud type

Can a charm with container in metadata.yaml be deployed to VM?

  • yes, was confirmed by testing; container is ignored

OCI image needs to be defined as a resource

  • this is required also for VM deployments
  • is actually only relevant for deploying a local version of the charm
  • for deployments from charmhub, the resource can be the one defined/uploaded in charmhub

Multiple storages possible in K8s?

  • in general: nothing speaks against it (according to Juju team)
  • needs to be confirmed by testing

Config options only relevant for K8s

  • should be ignored on VM and displayed to user (e.g. via status)
  • maybe we can convince juju to handle that

IP address vs. DNS names for units

  • should be abstracted via endpoints
  • actual endpoint will be handled by substrate-specific workload class

Pseudo-class for substrate-specific class instantiation

try:
	cloud_spec = self.model.get_cloud_spec()
except ModellError:
	raise # to catch when deployed without --trust

if cloud_spec.type == "kubernetes":
    self.workload = ValkeyK8sWorkload(container=self.unit.get_container(CONTAINER))
else:
    self.workload = ValkeyVmWorkload()

Downsides aka to be considered

  • ops.model.get_cloud_spec() requires application to be trusted -> --trust needs to be added at deploy time, even for VM
  • have separate integration tests for VM and K8s in the same repository