Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests, linting and tests CI/CD #8

Merged
merged 2 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: Tests

on:
pull_request:

jobs:
unit-tests:
uses: canonical/operator-workflows/.github/workflows/test.yaml@cea2ac306b4f4c1475d73b1a4c766d62e5b1c8a9
secrets: inherit
37 changes: 37 additions & 0 deletions .licenserc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
header:
license:
spdx-id: Apache-2.0
copyright-owner: Canonical Ltd.
copyright-year: 2024
content: |
Copyright [year] [owner]
See LICENSE file for licensing details.
paths:
- '**'
paths-ignore:
- '.github/**'
- '**/.gitkeep'
- '**/*.cfg'
- '**/*.conf'
- '**/*.j2'
- '**/*.json'
- '**/*.md'
- '**/*.rule'
- '**/*.tmpl'
- '**/*.txt'
- '**/*.jinja'
- '.codespellignore'
- '.dockerignore'
- '.flake8'
- '.jujuignore'
- '.gitignore'
- '.licenserc.yaml'
- '.trivyignore'
- '.woke.yaml'
- '.woke.yml'
- 'CODEOWNERS'
- 'icon.svg'
- 'LICENSE'
- 'trivy.yaml'
- 'lib/**'
comment: on-failure
9 changes: 9 additions & 0 deletions .woke.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ignore_files:
# Ignore ingress charm library as it uses non compliant terminology:
# whitelist.
- lib/charms/data_platform_libs/v0/data_models.py
- lib/charms/data_platform_libs/v0/database_requires.py
- lib/charms/data_platform_libs/v0/s3.py
rules:
# Ignore "master" - the database relation event received from the library.
- name: master
56 changes: 27 additions & 29 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

[tool.bandit]
exclude_dirs = ["/venv/"]
[tool.bandit.assert_used]
skips = ["*/*test.py", "*/test_*.py", "*tests/*.py"]

# Testing tools configuration
[tool.coverage.run]
branch = true
Expand All @@ -11,36 +19,26 @@ log_cli_level = "INFO"

# Formatting tools configuration
[tool.black]
line-length = 99
line-length = 120
target-version = ["py38"]

# Linting tools configuration
[tool.ruff]
line-length = 99
select = ["E", "W", "F", "C", "N", "D", "I001"]
extend-ignore = [
"D203",
"D204",
"D213",
"D215",
"D400",
"D404",
"D406",
"D407",
"D408",
"D409",
"D413",
]
ignore = ["E501", "D107"]
extend-exclude = ["__pycache__", "*.egg_info"]
per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]}
[tool.isort]
profile = "black"

[tool.ruff.mccabe]
# Linting tools configuration
[tool.flake8]
max-line-length = 120
max-doc-length = 99
max-complexity = 10

[tool.codespell]
skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage"

[tool.pyright]
include = ["src/**.py"]

exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
# Ignore W503, E501 because using black creates errors with this
# Ignore D107 Missing docstring in __init__
ignore = ["W503", "E501", "D107"]
# D100, D101, D102, D103: Ignore missing docstrings in tests
per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
docstring-convention = "google"
# Check for properly formatted copyright header in each file
copyright-check = "True"
copyright-author = "Canonical Ltd."
copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
11 changes: 4 additions & 7 deletions src/relations/airbyte_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

import logging

from log import log_event_handler
from ops import framework
from ops.model import ActiveStatus

from log import log_event_handler

logger = logging.getLogger(__name__)


Expand All @@ -23,12 +24,8 @@ def __init__(self, charm):
"""
super().__init__(charm, "airbyte-server")
self.charm = charm
charm.framework.observe(
charm.on.airbyte_server_relation_joined, self._on_airbyte_server_relation_joined
)
charm.framework.observe(
charm.on.airbyte_server_relation_changed, self._on_airbyte_server_relation_joined
)
charm.framework.observe(charm.on.airbyte_server_relation_joined, self._on_airbyte_server_relation_joined)
charm.framework.observe(charm.on.airbyte_server_relation_changed, self._on_airbyte_server_relation_joined)

@log_event_handler(logger)
def _on_airbyte_server_relation_joined(self, event):
Expand Down
50 changes: 32 additions & 18 deletions src/relations/minio.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@

import logging

from charm_helpers import construct_svc_endpoint
from charmed_kubeflow_chisme.exceptions import ErrorWithStatus
from log import log_event_handler
from ops import framework
from ops.model import BlockedStatus, WaitingStatus
from serialized_data_interface import NoCompatibleVersions, NoVersionsListed, get_interfaces
from serialized_data_interface import (
NoCompatibleVersions,
NoVersionsListed,
get_interfaces,
)

from charm_helpers import construct_svc_endpoint
from log import log_event_handler

logger = logging.getLogger(__name__)

Expand All @@ -28,15 +33,9 @@ def __init__(self, charm):
self.charm = charm

# Handle minio relation.
charm.framework.observe(
charm.on.object_storage_relation_joined, self._on_object_storage_relation_changed
)
charm.framework.observe(
charm.on.object_storage_relation_changed, self._on_object_storage_relation_changed
)
charm.framework.observe(
charm.on.object_storage_relation_broken, self._on_object_storage_relation_broken
)
charm.framework.observe(charm.on.object_storage_relation_joined, self._on_object_storage_relation_changed)
charm.framework.observe(charm.on.object_storage_relation_changed, self._on_object_storage_relation_changed)
charm.framework.observe(charm.on.object_storage_relation_broken, self._on_object_storage_relation_broken)

@log_event_handler(logger)
def _on_object_storage_relation_changed(self, event):
Expand Down Expand Up @@ -90,22 +89,37 @@ def _on_object_storage_relation_broken(self, event) -> None:
self.charm._update(event)

def _get_interfaces(self):
"""Retrieve interface object."""
"""Retrieve interface object.

Returns:
list of charm interfaces.

Raises:
ErrorWithStatus: if an anticipated error occurs.
"""
try:
charm = self.charm
# Hack: get_interfaces checks for peer relation which does not exist under requires/provides list in charmcraft.yaml
# Hack: get_interfaces checks for peer relation which does not exist under
# requires/provides list in charmcraft.yaml
del charm.meta.relations["peer"]
interfaces = get_interfaces(charm)
except NoVersionsListed as err:
raise ErrorWithStatus(err, WaitingStatus)
raise ErrorWithStatus(err, WaitingStatus) from err
except NoCompatibleVersions as err:
raise ErrorWithStatus(err, BlockedStatus)
raise ErrorWithStatus(err, BlockedStatus) from err
return interfaces

def _get_object_storage_data(self, interfaces):
"""Unpacks and returns the object-storage relation data.

Raises CheckFailedError if an anticipated error occurs.
Args:
interfaces: list of charm interfaces.

Returns:
object storage connection data.

Raises:
ErrorWithStatus: if an anticipated error occurs.
"""
if not ((obj_storage := interfaces["object-storage"]) and obj_storage.get_data()):
raise ErrorWithStatus("Waiting for object-storage relation data", WaitingStatus)
Expand All @@ -118,6 +132,6 @@ def _get_object_storage_data(self, interfaces):
f"Unexpected error unpacking object storage data - data format not "
f"as expected. Caught exception: '{str(e)}'",
BlockedStatus,
)
) from e

return obj_storage
5 changes: 3 additions & 2 deletions src/relations/postgresql.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
import logging

from charms.data_platform_libs.v0.database_requires import DatabaseEvent
from literals import DB_NAME
from log import log_event_handler
from ops import framework
from ops.model import WaitingStatus

from literals import DB_NAME
from log import log_event_handler

logger = logging.getLogger(__name__)


Expand Down
17 changes: 7 additions & 10 deletions src/relations/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
CredentialsChangedEvent,
CredentialsGoneEvent,
)
from log import log_event_handler
from ops import framework

from log import log_event_handler

logger = logging.getLogger(__name__)


Expand All @@ -27,9 +28,7 @@ def __init__(self, charm):
"""
super().__init__(charm, "s3")
self.charm = charm
charm.framework.observe(
charm.s3_client.on.credentials_changed, self._on_s3_credentials_changed
)
charm.framework.observe(charm.s3_client.on.credentials_changed, self._on_s3_credentials_changed)
charm.framework.observe(charm.s3_client.on.credentials_gone, self._on_s3_credentials_gone)

@log_event_handler(logger)
Expand Down Expand Up @@ -81,9 +80,7 @@ def _retrieve_s3_parameters(self):
"access-key",
"secret-key",
]
missing_required_parameters = [
param for param in required_parameters if param not in s3_parameters
]
missing_required_parameters = [param for param in required_parameters if param not in s3_parameters]
if missing_required_parameters:
logger.warning(
f"Missing required S3 parameters in relation with S3 integrator: {missing_required_parameters}"
Expand All @@ -104,9 +101,9 @@ def _retrieve_s3_parameters(self):
# Clean up extra slash symbols to avoid issues on 3rd-party storages
# like Ceph Object Gateway (radosgw).
s3_parameters["endpoint"] = s3_parameters["endpoint"].rstrip("/")
s3_parameters["path"] = (
f'/{s3_parameters["path"].strip("/")}' # The slash in the beginning is required by pgBackRest.
)
s3_parameters[
"path"
] = f'/{s3_parameters["path"].strip("/")}' # The slash in the beginning is required by pgBackRest.
s3_parameters["bucket"] = s3_parameters["bucket"].strip("/")

return s3_parameters, []
Expand Down
40 changes: 20 additions & 20 deletions src/s3_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,33 @@ class S3Client:
"""Client for S3 operations."""

def __init__(self, s3_parameters):
"""Construct.
"""Initialize an S3 connection using the provided parameters.

Args:
charm: The charm to attach the hooks to.
s3_parameters: S3 connection parameters.

Raises:
ValueError: If a session fails to be created.
"""
self.s3_parameters = s3_parameters
endpoint = s3_parameters.get("endpoint")
session = boto3.session.Session(
aws_access_key_id=s3_parameters.get("access-key"),
aws_secret_access_key=s3_parameters.get("secret-key"),
region_name=s3_parameters.get("region"), # Region can be optional for MinIO
)
self.s3_parameters = s3_parameters
try:
self.s3_resource = session.resource("s3", endpoint_url=endpoint)
self.s3_client = session.client("s3", endpoint_url=endpoint)
except ValueError as e:
logger.exception(
"Failed to create a session in region=%s.", s3_parameters.get("region")
)
raise e
except Exception as e:
logger.exception("Failed to create a session in region=%s.", s3_parameters.get("region"))
raise ValueError("Failed to create a session") from e

def create_bucket_if_not_exists(self, bucket_name):
"""Create the S3 bucket if it does not exist.

Args:
s3_parameters: S3 parameters fetched from the S3 integrator relation.
endpoint: S3 service endpoint.
bucket_name (str): name of bucket to create

Raises:
e (ValueError): if a session could not be created.
Expand All @@ -56,9 +56,7 @@ def create_bucket_if_not_exists(self, bucket_name):
except ClientError as e:
error_code = int(e.response["Error"]["Code"])
if error_code == 404:
logger.warning(
"Bucket %s doesn't exist or you don't have access to it.", bucket_name
)
logger.warning("Bucket %s doesn't exist or you don't have access to it.", bucket_name)
exists = False
else:
logger.exception("Unexpected error: %s", e)
Expand All @@ -70,12 +68,16 @@ def create_bucket_if_not_exists(self, bucket_name):
s3_bucket.wait_until_exists()
logger.info("Created bucket '%s' in region=%s", bucket_name, region)
except ClientError as error:
logger.exception(
"Couldn't create bucket named '%s' in region=%s.", bucket_name, region
)
logger.exception("Couldn't create bucket named '%s' in region=%s.", bucket_name, region)
raise error

def set_bucket_lifecycle_policy(self, bucket, ttl):
def set_bucket_lifecycle_policy(self, bucket_name, ttl):
"""Set lifecycle policy of bucket to purge files after a certain time.

Args:
bucket_name: Name of bucket.
ttl: Time to live of logs (in days).
"""
lifecycle_policy = {
"Rules": [
{
Expand All @@ -86,6 +88,4 @@ def set_bucket_lifecycle_policy(self, bucket, ttl):
}
]
}
self.s3_client.put_bucket_lifecycle_configuration(
Bucket=bucket, LifecycleConfiguration=lifecycle_policy
)
self.s3_client.put_bucket_lifecycle_configuration(Bucket=bucket_name, LifecycleConfiguration=lifecycle_policy)
Loading
Loading