-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial command and class skeleton (#6)
* Initial command and class skeleton * squash: make YAML safe * sqush: move things around to create true container * squash: switch erratum to one release only * squash: undo single erratum
- Loading branch information
1 parent
cafee10
commit 3fc4b4e
Showing
4 changed files
with
325 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,143 @@ | ||
import click | ||
import io | ||
from enum import Enum | ||
from pathlib import Path | ||
from typing import TYPE_CHECKING, TypeVar | ||
|
||
import attrs | ||
import ruamel.yaml | ||
import ruamel.yaml.nodes | ||
import ruamel.yaml.representer | ||
from attrs import define, field | ||
|
||
@click.command() | ||
def main() -> None: | ||
print('Newa!') | ||
if TYPE_CHECKING: | ||
from typing_extensions import Self, TypeAlias | ||
|
||
ErratumId: TypeAlias = str | ||
|
||
|
||
T = TypeVar('T') | ||
SerializableT = TypeVar('SerializableT', bound='Serializable') | ||
|
||
|
||
def yaml_parser() -> ruamel.yaml.YAML: | ||
""" Create standardized YAML parser """ | ||
|
||
yaml = ruamel.yaml.YAML(typ='safe') | ||
|
||
yaml.indent(mapping=4, sequence=4, offset=2) | ||
yaml.default_flow_style = False | ||
yaml.allow_unicode = True | ||
yaml.encoding = 'utf-8' | ||
|
||
# For simpler dumping of well-known classes | ||
def _represent_enum( | ||
representer: ruamel.yaml.representer.Representer, | ||
data: Enum) -> ruamel.yaml.nodes.ScalarNode: | ||
return representer.represent_scalar('tag:yaml.org,2002:str', data.value) | ||
|
||
yaml.representer.add_representer(EventType, _represent_enum) | ||
|
||
return yaml | ||
|
||
|
||
class EventType(Enum): | ||
""" Event types """ | ||
|
||
ERRATUM = 'erratum' | ||
|
||
|
||
@define | ||
class Cloneable: | ||
""" A class whose instances can be cloned """ | ||
|
||
def clone(self) -> 'Self': | ||
return attrs.evolve(self) | ||
|
||
|
||
@define | ||
class Serializable: | ||
""" A class whose instances can be serialized into YAML """ | ||
|
||
def to_yaml(self) -> str: | ||
output = io.StringIO() | ||
|
||
yaml_parser().dump(attrs.asdict(self, recurse=True), output) | ||
|
||
return output.getvalue() | ||
|
||
def to_yaml_file(self, filepath: Path) -> None: | ||
filepath.write_text(self.to_yaml()) | ||
|
||
@classmethod | ||
def from_yaml(cls: type[SerializableT], serialized: str) -> SerializableT: | ||
data = yaml_parser().load(serialized) | ||
|
||
return cls(**data) | ||
|
||
@classmethod | ||
def from_yaml_file(cls: type[SerializableT], filepath: Path) -> SerializableT: | ||
return cls.from_yaml(filepath.read_text()) | ||
|
||
|
||
@define | ||
class Event(Serializable): | ||
""" A triggering event of Newa pipeline """ | ||
|
||
type_: EventType = field(converter=EventType) | ||
id: 'ErratumId' | ||
|
||
|
||
@define | ||
class InitialErratum(Serializable): | ||
""" | ||
An initial erratum as an input. | ||
It does not track releases, just the initial event. It will be expanded | ||
into corresponding :py:class:`ErratumJob` instances. | ||
""" | ||
|
||
event: Event = field( # type: ignore[var-annotated] | ||
converter=lambda x: x if isinstance(x, Event) else Event(**x), | ||
) | ||
|
||
|
||
@define | ||
class Erratum(Cloneable, Serializable): | ||
""" An eratum """ | ||
|
||
release: str | ||
# builds: list[...] = ... | ||
|
||
def fetch_details(self) -> None: | ||
raise NotImplementedError | ||
|
||
|
||
@define | ||
class Job(Cloneable, Serializable): | ||
""" A single job """ | ||
|
||
event: Event = field( # type: ignore[var-annotated] | ||
converter=lambda x: x if isinstance(x, Event) else Event(**x), | ||
) | ||
|
||
# issue: ... | ||
# recipe: ... | ||
# test_job: ... | ||
# job_result: ... | ||
|
||
@property | ||
def id(self) -> str: | ||
raise NotImplementedError | ||
|
||
|
||
@define | ||
class ErratumJob(Job): | ||
""" A single *erratum* job """ | ||
|
||
erratum: Erratum = field( # type: ignore[var-annotated] | ||
converter=lambda x: x if isinstance(x, Erratum) else Erratum(**x), | ||
) | ||
|
||
@property | ||
def id(self) -> str: | ||
return f'{self.event.id} @ {self.erratum.release}' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import logging | ||
import os.path | ||
from collections.abc import Iterable, Iterator | ||
from pathlib import Path | ||
|
||
import click | ||
from attrs import define | ||
|
||
from . import Erratum, ErratumJob, Event, EventType, InitialErratum | ||
|
||
logging.basicConfig( | ||
format='%(asctime)s %(message)s', | ||
datefmt='%m/%d/%Y %I:%M:%S %p', | ||
level=logging.INFO) | ||
|
||
|
||
@define | ||
class CLIContext: | ||
""" State information about one Newa pipeline invocation """ | ||
|
||
logger: logging.Logger | ||
|
||
# Path to directory with state files | ||
state_dirpath: Path | ||
|
||
def enter_command(self, command: str) -> None: | ||
self.logger.handlers[0].formatter = logging.Formatter( | ||
f'[%(asctime)s] [{command.ljust(8, " ")}] %(message)s', | ||
) | ||
|
||
def load_initial_erratum(self, filepath: Path) -> InitialErratum: | ||
erratum = InitialErratum.from_yaml_file(filepath) | ||
|
||
self.logger.info(f'Discovered initial erratum {erratum.event.id} in {filepath}') | ||
|
||
return erratum | ||
|
||
def load_initial_errata(self, filename_prefix: str) -> Iterator[InitialErratum]: | ||
for child in self.state_dirpath.iterdir(): | ||
if not child.name.startswith(filename_prefix): | ||
continue | ||
|
||
yield self.load_initial_erratum(self.state_dirpath / child) | ||
|
||
def load_erratum_job(self, filepath: Path) -> ErratumJob: | ||
job = ErratumJob.from_yaml_file(filepath) | ||
|
||
self.logger.info(f'Discovered erratum job {job.id} in {filepath}') | ||
|
||
return job | ||
|
||
def load_erratum_jobs(self, filename_prefix: str) -> Iterator[ErratumJob]: | ||
for child in self.state_dirpath.iterdir(): | ||
if not child.name.startswith(filename_prefix): | ||
continue | ||
|
||
yield self.load_erratum_job(self.state_dirpath / child) | ||
|
||
def save_erratum_job(self, filename_prefix: str, job: ErratumJob) -> None: | ||
filepath = self.state_dirpath / \ | ||
f'{filename_prefix}{job.event.id}-{job.erratum.release}.yaml' | ||
|
||
job.to_yaml_file(filepath) | ||
self.logger.info(f'Erratum job {job.id} written to {filepath}') | ||
|
||
def save_erratum_jobs(self, filename_prefix: str, jobs: Iterable[ErratumJob]) -> None: | ||
for job in jobs: | ||
self.save_erratum_job(filename_prefix, job) | ||
|
||
|
||
@click.group(chain=True) | ||
@click.option( | ||
'--state-dir', | ||
default='$PWD/state', | ||
) | ||
@click.pass_context | ||
def main(click_context: click.Context, state_dir: str) -> None: | ||
ctx = CLIContext( | ||
logger=logging.getLogger(), | ||
state_dirpath=Path(os.path.expandvars(state_dir)), | ||
) | ||
click_context.obj = ctx | ||
|
||
if not ctx.state_dirpath.exists(): | ||
ctx.logger.info(f'State directory {ctx.state_dirpath} does not exist, creating...') | ||
ctx.state_dirpath.mkdir(parents=True) | ||
|
||
|
||
@main.command(name='event') | ||
@click.option( | ||
'-e', '--erratum', 'errata_ids', | ||
multiple=True, | ||
) | ||
@click.pass_obj | ||
def cmd_event(ctx: CLIContext, errata_ids: tuple[str, ...]) -> None: | ||
ctx.enter_command('event') | ||
|
||
if errata_ids: | ||
for erratum_id in errata_ids: | ||
event = Event(type_=EventType.ERRATUM, id=erratum_id) | ||
|
||
# fetch erratum details, namely releases | ||
releases = ['RHEL-8.10.0', 'RHEL-9.4.0'] | ||
|
||
for release in releases: | ||
erratum_job = ErratumJob(event=event, erratum=Erratum(release=release)) | ||
|
||
ctx.save_erratum_job('event-', erratum_job) | ||
|
||
else: | ||
for erratum in ctx.load_initial_errata('init-'): | ||
# fetch erratum details, namely releases | ||
releases = ['RHEL-8.10.0', 'RHEL-9.4.0'] | ||
|
||
for release in releases: | ||
erratum_job = ErratumJob(event=erratum.event, erratum=Erratum(release=release)) | ||
|
||
ctx.save_erratum_job('event-', erratum_job) | ||
|
||
|
||
@main.command(name='jira') | ||
@click.pass_obj | ||
def cmd_jira(ctx: CLIContext) -> None: | ||
ctx.enter_command('jira') | ||
|
||
for erratum_job in ctx.load_erratum_jobs('event-'): | ||
# read Jira issue configuration | ||
# get list of matching actions | ||
|
||
# for action in actions: | ||
# create epic | ||
# or create task | ||
# or create sutask | ||
# if subtask assoc. with recipes | ||
# clone object with yaml | ||
|
||
# erratum_job.issue = ... | ||
# what's recipe? doesn't it belong to "schedule"? | ||
# recipe = new JobRecipe(url) | ||
|
||
ctx.save_erratum_job('jira-', erratum_job) | ||
|
||
|
||
@main.command(name='schedule') | ||
@click.pass_obj | ||
def cmd_schedule(ctx: CLIContext) -> None: | ||
ctx.enter_command('schedule') | ||
|
||
for erratum_job in ctx.load_erratum_jobs('jira-'): | ||
# prepare parameters based on errata details (environment variables) | ||
# generate all relevant test jobs using the recipe | ||
# prepares a list of JobExec objects | ||
|
||
ctx.save_erratum_job('schedule-', erratum_job) | ||
|
||
|
||
@main.command(name='execute') | ||
@click.pass_obj | ||
def cmd_execute(ctx: CLIContext) -> None: | ||
ctx.enter_command('execute') | ||
|
||
for erratum_job in ctx.load_erratum_jobs('schedule-'): | ||
# worker = new Executor(yaml) | ||
# run() returns result object | ||
# result = worker.run() | ||
|
||
ctx.save_erratum_job('execute-', erratum_job) | ||
|
||
|
||
@main.command(name='report') | ||
@click.pass_obj | ||
def cmd_report(ctx: CLIContext) -> None: | ||
ctx.enter_command('report') | ||
|
||
for _ in ctx.load_erratum_jobs('execute-'): | ||
pass | ||
# read yaml details | ||
# update Jira issue with job result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters