Skip to content

Commit

Permalink
First component of github-profiles-automator charm (#18)
Browse files Browse the repository at this point in the history
* Update requirements

* Initialize charm code

* Add initial tests

* Change default config value for sync-period

* Remove uneccessary comments

* Update git-sync image

* Add rust packages

* Wait until blocked in integration test

* Update git-sync-image in integration test

* Resolve comments

* Add docstrings

* Update docstrings

* Fix docstring in charm.py

* Add git-revision config

* Fix linting

* Update health check

* Update docstring
  • Loading branch information
mvlassis authored Jan 13, 2025
1 parent 9f8bdd6 commit 5bc78e4
Show file tree
Hide file tree
Showing 9 changed files with 616 additions and 42 deletions.
57 changes: 53 additions & 4 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,70 @@
name: github-profiles-automator
type: charm
title: GitHub Profiles Automator
title: GitHub Profiles Automator charm

summary: A charm for automating the management of Kubeflow Profiles from a GitHub repo
summary: A charm for automating the management of Kubeflow Profiles from a GitHub repository

description: |
A charm to automatically sync Kubeflow Profiles from information a GitHub repository.
This charm is responsible for monitoring a file from a GitHub repo that represents
which Profiles and Contributors should exist in a cluster. Then, via a reconciliation loop
the charm will update the Profiles and RoleBindings and AuthorizationPolicies in the
cluster to align with this representation.
It is useful for cluster administrators who want to automatically update
the profiles on the cluster, based on a single source of truth.
base: [email protected]
platforms:
amd64:

config:
options:
repository:
default: ""
description: |
The URL of the repository to fetch. Must be configured for the charm to
operate.
type: string
git-revision:
default: "HEAD"
description: |
The git revision to check out.
type: string
sync-period:
default: 60
description: |
How long to wait between sync attempts.
type: int
pmr-yaml-path:
default: "pmr.yaml"
description: |
The relative path to the .yaml file inside the GitHub repository that
contains the PMR information
type: string
ssh-key-secret-id:
type: secret
description: |
A configuration option to store the secret ID needed to access the SSH key for the GitHub
repository
containers:
git-sync:
resource: git-sync-image
mounts:
- storage: content-from-git
location: /git

resources:
git-sync-image:
type: oci-image
description: OCI image for the 'git-sync' container
upstream-source: registry.k8s.io/git-sync/git-sync:v4.4.0

storage:
content-from-git:
type: filesystem

parts:
charm:
Expand All @@ -25,9 +74,9 @@ parts:
build-snaps:
- rustup
build-packages:
- pkg-config
- libffi-dev
- libssl-dev
- pkg-config
override-build: |
rustup default stable
craftctl default
169 changes: 151 additions & 18 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package-mode = false

[tool.poetry.dependencies]
python = "^3.12"
charmed-kubeflow-chisme = "^0.4.5"
charmed_kubeflow_chisme = "^0.4.6"
jsonschema = "^4.23.0"
lightkube = "^0.15.7"
ops = "^2.17.0"
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ referencing==0.35.1 ; python_version >= "3.12" and python_version < "4.0"
requests-oauthlib==2.0.0 ; python_version >= "3.12" and python_version < "4.0"
requests==2.32.3 ; python_version >= "3.12" and python_version < "4.0"
rpds-py==0.22.3 ; python_version >= "3.12" and python_version < "4.0"
rsa==4.9 ; python_version >= "3.12" and python_version < "4"
ruamel-yaml-clib==0.2.12 ; platform_python_implementation == "CPython" and python_version < "3.13" and python_version >= "3.12"
rsa==4.9 ; python_version >= "3.12" and python_version < "4.0"
ruamel-yaml-clib==0.2.12 ; python_version >= "3.12" and python_version < "3.13" and platform_python_implementation == "CPython"
ruamel-yaml==0.18.10 ; python_version >= "3.12" and python_version < "4.0"
serialized-data-interface==0.6.0 ; python_version >= "3.12" and python_version < "4.0"
six==1.17.0 ; python_version >= "3.12" and python_version < "4.0"
Expand Down
169 changes: 159 additions & 10 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,174 @@
import logging

import ops
from ops.charm import InstallEvent
from charmed_kubeflow_chisme.components import ContainerFileTemplate, LazyContainerFileTemplate
from charmed_kubeflow_chisme.components.charm_reconciler import CharmReconciler
from charmed_kubeflow_chisme.components.leadership_gate_component import LeadershipGateComponent
from charmed_kubeflow_chisme.exceptions import ErrorWithStatus

# Log messages can be retrieved using juju debug-log
logger = logging.getLogger(__name__)
from components.pebble_component import (
GitSyncInputs,
GitSyncPebbleService,
RepositoryType,
)

SSH_KEY_DESTINATION_PATH = "/etc/git-secret/ssh"
SSH_KEY_PERMISSIONS = 0o400
EXECHOOK_SCRIPT_DESTINATION_PATH = "/git-sync-exechook.sh"
EXECHOOK_SCRIPT_PERMISSIONS = 0o555

VALID_LOG_LEVELS = ["info", "debug", "warning", "error", "critical"]
logger = logging.getLogger(__name__)


class GithubProfilesAutomatorCharm(ops.CharmBase):
"""Charm the service."""
"""A Juju charm for the GitHub Profiles Automator."""

def __init__(self, framework: ops.Framework):
"""Initialize charm and setup the container."""
super().__init__(framework)
self.pebble_service_name = "git-sync"
self.container = self.unit.get_container("git-sync")

self.files_to_push = []

try:
self._validate_repository_config()
except ErrorWithStatus as e:
self.unit.status = e.status
return

self.charm_reconciler = CharmReconciler(self)

self.leadership_gate = self.charm_reconciler.add(
component=LeadershipGateComponent(
charm=self,
name="leadership-gate",
),
depends_on=[],
)

# Push the exechook script to the workload container
self.files_to_push.append(
ContainerFileTemplate(
source_template_path="./src/components/git-sync-exechook.sh",
destination_path=EXECHOOK_SCRIPT_DESTINATION_PATH,
permissions=EXECHOOK_SCRIPT_PERMISSIONS,
)
)

self.pebble_service_container = self.charm_reconciler.add(
component=GitSyncPebbleService(
charm=self,
name="git-sync-pebble-service",
container_name="git-sync",
service_name=self.pebble_service_name,
files_to_push=self.files_to_push,
inputs_getter=lambda: GitSyncInputs(
GIT_REVISION=str(self.config["git-revision"]),
REPOSITORY=str(self.config["repository"]),
REPOSITORY_TYPE=self.repository_type,
SYNC_PERIOD=int(self.config["sync-period"]),
),
),
depends_on=[self.leadership_gate],
)

self.charm_reconciler.install_default_event_handlers()

@property
def ssh_key(self) -> str | None:
"""Retrieve the SSH key value from the Juju secrets, using the ssh-key-secret-id config.
Returns:
The SSH key as a string, or None if the Juju secret doesn't exist or the config
hasn't been set.
Raises:
ErrorWithStatus: If the SSH key cannot be retrieved due to missing configuration or
errors.
"""
ssh_key_secret_id = str(self.config.get("ssh-key-secret-id"))
try:
ssh_key_secret = self.model.get_secret(id=ssh_key_secret_id)
ssh_key = str(ssh_key_secret.get_content(refresh=True)["ssh-key"])
# SSH key requires a newline at the end, so ensure it has one
ssh_key += "\n\n"
return ssh_key
except (ops.SecretNotFoundError, ops.model.ModelError):
logger.warning("The SSH key does not exist")
return None

def _validate_repository_config(self):
"""Parse a repository string and raise appropriate errors.
Raises:
ErrorWithStatus: If the config `repository` is empty, an invalid GitHub URL, or
there is a missing SSH key when needed.
"""
if self.config["repository"] == "":
logger.warning("Charm is Blocked due to empty value of `repository`")
raise ErrorWithStatus("Config `repository` cannot be empty.", ops.BlockedStatus)

if is_ssh_url(str(self.config["repository"])):
self.repository_type = RepositoryType.SSH
if not self.ssh_key:
raise ErrorWithStatus(
"To connect via an SSH URL you need to provide an SSH key.",
ops.BlockedStatus,
)
# If there is an SSH key, we push it to the workload container
self.files_to_push.append(
LazyContainerFileTemplate(
source_template=self.ssh_key,
destination_path=SSH_KEY_DESTINATION_PATH,
permissions=SSH_KEY_PERMISSIONS,
)
)
return

self.repository_type = RepositoryType.HTTPS
if not is_https_url(str(self.config["repository"])):
logger.warning("Charm is Blocked due to incorrect value of `repository`")
raise ErrorWithStatus(
"Config `repository` isn't a valid GitHub URL.", ops.BlockedStatus
)


def is_https_url(url: str) -> bool:
"""Check if a given string is a valid HTTPS URL for a GitHub repo.
Args:
url: The URL to check.
Returns:
True if the string is valid HTTPS URL for a GitHub repo, False otherwise.
"""
# Check if the URL starts with 'https://github.com'
if not url.startswith("https://github.com/"):
return False
# Check if the URL ends with '.git'
if not url.endswith(".git"):
return False
return True


def is_ssh_url(url: str) -> bool:
"""Check if a given string is a valid SSH URL for a GitHub repo.
self.framework.observe(self.on.install, self._on_install)
Args:
url: The URL to check.
def _on_install(self, _: InstallEvent):
self.unit.status = ops.ActiveStatus("hello friend")
Returns:
True if the string is valid SSH URL for a GitHub repo, False otherwise.
"""
if not url.startswith("[email protected]:"):
return False
# Get the part after [email protected]
path = url.split(":", 1)[-1]
if "/" not in path:
return False
return True


if __name__ == "__main__": # pragma: nocover
ops.main(GithubProfilesAutomatorCharm) # type: ignore
if __name__ == "__main__":
ops.main(GithubProfilesAutomatorCharm)
8 changes: 8 additions & 0 deletions src/components/git-sync-exechook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/sh
# This script will be called by `git-sync` whenever it receives new information
# from the repository.
# See https://github.com/kubernetes/git-sync/blob/69eb59185a073d4a08362d07bbe6459311027746/_test_tools/exechook_command_with_sleep.sh

set -xe

/charm/bin/pebble notify github-profiles-automator.com/sync
Loading

0 comments on commit 5bc78e4

Please sign in to comment.