Skip to content

Commit

Permalink
DPE-4235, DPE-4315, DPE-3263 Common UX for replication (#421)
Browse files Browse the repository at this point in the history
* port common ux

* fix async test and bring in lib fixes

* wait for initialization, bump black to match system and update secret label

* add action to final test

* password propagation support

* bump workflows version

* updated CI to 3.4.3

* asyncio_mode=auto is breaking pytest_operator_groups

* sync dpw everywhere
  • Loading branch information
paulomach authored Jun 7, 2024
1 parent d310ba9 commit 38d5709
Show file tree
Hide file tree
Showing 14 changed files with 1,404 additions and 1,094 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:
jobs:
lint:
name: Lint
uses: canonical/data-platform-workflows/.github/workflows/lint.yaml@v13.1.0
uses: canonical/data-platform-workflows/.github/workflows/lint.yaml@v13.3.0

unit-test:
name: Unit test charm
Expand Down Expand Up @@ -56,7 +56,7 @@ jobs:

build:
name: Build charm
uses: canonical/data-platform-workflows/.github/workflows/build_charm.yaml@v13.1.0
uses: canonical/data-platform-workflows/.github/workflows/build_charm.yaml@v13.3.0
with:
cache: true

Expand All @@ -65,17 +65,17 @@ jobs:
fail-fast: false
matrix:
juju:
- agent: 2.9.46
- agent: 2.9.49
libjuju: ^2
allure: false
- agent: 3.1.7
- agent: 3.4.3
allure: true
name: Integration test charm | ${{ matrix.juju.agent }}
needs:
- lint
- unit-test
- build
uses: canonical/data-platform-workflows/.github/workflows/integration_test_charm.yaml@v13.1.0
uses: canonical/data-platform-workflows/.github/workflows/integration_test_charm.yaml@v13.3.0
with:
artifact-prefix: ${{ needs.build.outputs.artifact-prefix }}
cloud: microk8s
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ jobs:

build:
name: Build charm
uses: canonical/data-platform-workflows/.github/workflows/build_charm.yaml@v13.1.0
uses: canonical/data-platform-workflows/.github/workflows/build_charm.yaml@v13.3.0

release:
name: Release charm
needs:
- lib-check
- ci-tests
- build
uses: canonical/data-platform-workflows/.github/workflows/release_charm.yaml@v13.1.0
uses: canonical/data-platform-workflows/.github/workflows/release_charm.yaml@v13.3.0
with:
channel: 8.0/edge
artifact-prefix: ${{ needs.build.outputs.artifact-prefix }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sync_issue_to_jira.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
sync:
name: Sync GitHub issue to Jira
uses: canonical/data-platform-workflows/.github/workflows/sync_issue_to_jira.yaml@v13.1.0
uses: canonical/data-platform-workflows/.github/workflows/sync_issue_to_jira.yaml@v13.3.0
with:
jira-base-url: https://warthogs.atlassian.net
jira-project-key: DPE
Expand Down
34 changes: 13 additions & 21 deletions actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,20 @@ pre-upgrade-check:
resume-upgrade:
description: Resume a rolling upgrade after asserting successful upgrade of a new revision.

promote-standby-cluster:
create-replication:
description: |
Promotes this cluster to become the leader in the cluster-set. Used for safe switchover or failover.
Must be run against the charm leader unit of a standby cluster.
Create replication between two related clusters.
This action is must be run on the offer side of the relation.
params:
name:
type: string
description: A (optional) name for this replication.
default: default

promote-to-primary:
description: |
Promotes this cluster to become the primary in the cluster-set. Used for safe switchover or failover.
Can only be run against the charm leader unit of a standby cluster.
params:
cluster-set-name:
type: string
Expand All @@ -86,24 +96,6 @@ recreate-cluster:
each unit will be kept in blocked status. Recreating the cluster allows to rejoin the async replication
relation, or usage as a standalone cluster.
fence-writes:
description: |
Stops write traffic to a primary cluster of a ClusterSet.
params:
cluster-set-name:
type: string
description: |
The name of the cluster-set. Mandatory option, used for confirmation.
unfence-writes:
description: |
Resumes write traffic to a primary cluster of a ClusterSet.
params:
cluster-set-name:
type: string
description: |
The name of the cluster-set. Mandatory option, used for confirmation.
rejoin-cluster:
description: |
Rejoins an invalidated cluster to the cluster-set, after a previous failover or switchover.
Expand Down
144 changes: 144 additions & 0 deletions lib/charms/data_platform_libs/v0/data_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Secrets related helper classes/functions."""

# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

from typing import Dict, Literal, Optional

from ops import Secret, SecretInfo
from ops.charm import CharmBase
from ops.model import SecretNotFoundError

# The unique Charmhub library identifier, never change it
LIBID = "d77fb3d01aba41ed88e837d0beab6be5"

# Increment this major API version when introducing breaking changes
LIBAPI = 0

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 2


APP_SCOPE = "app"
UNIT_SCOPE = "unit"
Scopes = Literal["app", "unit"]


class DataSecretsError(Exception):
"""A secret that we want to create already exists."""


class SecretAlreadyExistsError(DataSecretsError):
"""A secret that we want to create already exists."""


def generate_secret_label(charm: CharmBase, scope: Scopes) -> str:
"""Generate unique group_mappings for secrets within a relation context.
Defined as a standalone function, as the choice on secret labels definition belongs to the
Application Logic. To be kept separate from classes below, which are simply to provide a
(smart) abstraction layer above Juju Secrets.
"""
members = [charm.app.name, scope]
return f"{'.'.join(members)}"


# Secret cache


class CachedSecret:
"""Abstraction layer above direct Juju access with caching.
The data structure is precisely re-using/simulating Juju Secrets behavior, while
also making sure not to fetch a secret multiple times within the same event scope.
"""

def __init__(self, charm: CharmBase, label: str, secret_uri: Optional[str] = None):
self._secret_meta = None
self._secret_content = {}
self._secret_uri = secret_uri
self.label = label
self.charm = charm

def add_secret(self, content: Dict[str, str], scope: Scopes) -> Secret:
"""Create a new secret."""
if self._secret_uri:
raise SecretAlreadyExistsError(
"Secret is already defined with uri %s", self._secret_uri
)

if scope == APP_SCOPE:
secret = self.charm.app.add_secret(content, label=self.label)
else:
secret = self.charm.unit.add_secret(content, label=self.label)
self._secret_uri = secret.id
self._secret_meta = secret
return self._secret_meta

@property
def meta(self) -> Optional[Secret]:
"""Getting cached secret meta-information."""
if self._secret_meta:
return self._secret_meta

if not (self._secret_uri or self.label):
return

try:
self._secret_meta = self.charm.model.get_secret(label=self.label)
except SecretNotFoundError:
if self._secret_uri:
self._secret_meta = self.charm.model.get_secret(
id=self._secret_uri, label=self.label
)
return self._secret_meta

def get_content(self) -> Dict[str, str]:
"""Getting cached secret content."""
if not self._secret_content:
if self.meta:
self._secret_content = self.meta.get_content()
return self._secret_content

def set_content(self, content: Dict[str, str]) -> None:
"""Setting cached secret content."""
if self.meta:
self.meta.set_content(content)
self._secret_content = content

def get_info(self) -> Optional[SecretInfo]:
"""Wrapper function for get the corresponding call on the Secret object if any."""
if self.meta:
return self.meta.get_info()


class SecretCache:
"""A data structure storing CachedSecret objects."""

def __init__(self, charm):
self.charm = charm
self._secrets: Dict[str, CachedSecret] = {}

def get(self, label: str, uri: Optional[str] = None) -> Optional[CachedSecret]:
"""Getting a secret from Juju Secret store or cache."""
if not self._secrets.get(label):
secret = CachedSecret(self.charm, label, uri)

# Checking if the secret exists, otherwise we don't register it in the cache
if secret.meta:
self._secrets[label] = secret
return self._secrets.get(label)

def add(self, label: str, content: Dict[str, str], scope: Scopes) -> CachedSecret:
"""Adding a secret to Juju Secret."""
if self._secrets.get(label):
raise SecretAlreadyExistsError(f"Secret {label} already exists")

secret = CachedSecret(self.charm, label)
secret.add_secret(content, scope)
self._secrets[label] = secret
return self._secrets[label]


# END: Secret cache
Loading

0 comments on commit 38d5709

Please sign in to comment.