Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
m1keil committed Sep 2, 2020
1 parent 0d4ee17 commit 8cc7989
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 0 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# nomad-diff

A one to one Python re-write of the Job diff formatter [from Nomad's source](https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L371).
Use this if you communicate with Nomad over HTTP API and want to output the diff in the same format like the Nomad CLI & UI does.

## Installation

```
$ pip install nomad-diff
```

## Usage

```
>>> import nomad_diff
>>>
>>> data = .. # Nomad's job diff
>>>
>>> print(nomad_diff.format(data, colors=True, verbose=False))
+/- Job: "example"
+/- Task Group: "cache" (2 destroy, 1 in-place update)
+/- Count: "3" => "1" (forces destroy)
Task: "redis"
```

38 changes: 38 additions & 0 deletions nomad_diff/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from .diff import format_job_diff, JobDiff
from colorama import Fore, Style # type: ignore


COLORS = {
'[green]': Fore.GREEN,
'[red]': Fore.RED,
'[blue]': Fore.BLUE,
'[cyan]': Fore.CYAN,
'[yellow]': Fore.YELLOW,
'[light_yellow]': Fore.LIGHTYELLOW_EX,
'[bold]': Style.BRIGHT,
'[reset]': Style.RESET_ALL
}


def format(diff: dict, colors: bool, verbose: bool):
diff_dict = JobDiff(**diff)
out = format_job_diff(diff_dict, verbose)

if not colors:
return strip_colors(out)

return colorize(out)


def strip_colors(diff: str):
for e in COLORS.keys():
diff = diff.replace(e, '')

return diff


def colorize(diff: str):
for p, color in COLORS.items():
diff = diff.replace(p, color)

return diff
249 changes: 249 additions & 0 deletions nomad_diff/diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
from typing import List, Optional, Tuple
from .models import JobDiff, TaskGroupDiff, FieldDiff, ObjectDiff, TaskDiff


# https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L371
def format_job_diff(job: JobDiff, verbose: bool) -> str:
marker, _ = get_diff_string(job.Type)
out = f'{marker}[bold]Job: "{job.ID}"\n'

# Determine the longest markers and fields so that the output can be
# properly aligned.
longest_field, longest_marker = get_longest_prefixes(job.Fields, job.Objects)
for tg in job.TaskGroups or []:
_, l = get_diff_string(tg.Type)
if l > longest_marker:
longest_marker = l

# Only show the job's field and object diffs if the job is edited or
# verbose mode is set.
if job.Type == "Edited" or verbose:
fo = aligned_field_and_objects(job.Fields, job.Objects, 0, longest_field, longest_marker)
out += fo
if len(fo) > 0:
out += "\n"

# Print the task groups
for tg in job.TaskGroups or []:
_, mLength = get_diff_string(tg.Type)
kPrefix = longest_marker - mLength
out += f'{format_task_group_diff(tg, kPrefix, verbose)}\n'

return out


# https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L408
def format_task_group_diff(tg: TaskGroupDiff, tg_prefix: int, verbose: bool) -> str:
marker, _ = get_diff_string(tg.Type)
out = f'{marker}{" " * tg_prefix}[bold]Task Group: "{tg.Name}"[reset]'

if tg.Updates:
order = list(tg.Updates.keys())
order.sort()

updates = []
for update_type in order:
count = tg.Updates[update_type]
color = ""

if update_type == "ignore" or update_type == "create":
color = "[green]"
elif update_type == "destroy":
color = "[red]"
elif update_type == "migrate":
color = "[blue]"
elif update_type == "in-place update":
color = "[cyan]"
elif update_type == "create/destroy update":
color = "[yellow]"
elif update_type == "canary":
color = "[light_yellow]"

updates.append(f'[reset]{color}{count} {update_type}')
out += f' ({", ".join(updates)}[reset])\n'
else:
out += "[reset]\n"

# Determine the longest field and markers so the output is properly
# aligned
longest_field, longest_marker = get_longest_prefixes(tg.Fields, tg.Objects)
for task in tg.Tasks or []:
_, l = get_diff_string(task.Type)
if l > longest_marker:
longest_marker = l

# Only show the task groups's field and object diffs if the group is edited or
# verbose mode is set.
sub_start_prefix = tg_prefix + 2
if tg.Type == "Edited" or verbose:
fo = aligned_field_and_objects(tg.Fields, tg.Objects, sub_start_prefix, longest_field, longest_marker)
out += fo
if len(fo) > 0:
out += "\n"

# Output the tasks
for task in tg.Tasks or []:
_, mLength = get_diff_string(task.Type)
prefix = longest_marker - mLength
out += f'{format_task_diff(task, sub_start_prefix, prefix, verbose)}'

return out


# https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L481
def format_task_diff(task: TaskDiff, startPrefix: int, taskPrefix: int, verbose: bool) -> str:
marker, _ = get_diff_string(task.Type)
out = f'{" " * startPrefix}{marker}{" " * taskPrefix}[bold]Task: "{task.Name}"'

if task.Annotations:
out += f' [reset]({color_annotations(task.Annotations)})'

if task.Type == "None":
return out
elif task.Type in ("Deleted", "Added") and not verbose:
# Exit early if the job was not edited and it isn't verbose output
return out
else:
out += "\n"

sub_start_prefix = startPrefix + 2
longest_field, longest_marker = get_longest_prefixes(task.Fields, task.Objects)
out += aligned_field_and_objects(task.Fields, task.Objects, sub_start_prefix, longest_field, longest_marker)

return out


# https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L507
def format_object_diff(diff: ObjectDiff, start_prefix: int, key_prefix: int) -> str:
start = " " * start_prefix
marker, markerLen = get_diff_string(diff.Type)
out = f'{start}{marker}{" " * key_prefix}{diff.Name} {{\n'

# Determine the length of the longest name and longest diff marker to
# properly align names and values
longest_field, longest_marker = get_longest_prefixes(diff.Fields, diff.Objects)
sub_start_prefix = start_prefix + key_prefix + 2
out += aligned_field_and_objects(diff.Fields, diff.Objects, sub_start_prefix, longest_field, longest_marker)

endprefix = " " * (start_prefix + markerLen + key_prefix)
return f'{out}\n{endprefix}}}'


# https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L526
def format_field_diff(diff: FieldDiff, start_prefix: int, key_prefix: int, value_prefix: int) -> str:
marker, _ = get_diff_string(diff.Type)
out = f'{" " * start_prefix}{marker}{" " * key_prefix}{diff.Name}: {" " * value_prefix}'

if diff.Type == "Added":
out += f'"{diff.New}"'
elif diff.Type == "Deleted":
out += f'"{diff.Old}"'
elif diff.Type == "Edited":
out += f'"{diff.Old}" => "{diff.New}"'
else:
out += f'"{diff.New}"'

# Color the annotations where possible
if diff.Annotations:
out += f' ({color_annotations(diff.Annotations)})'

return out


# https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L609
def get_diff_string(diff_type: str) -> Tuple[str, int]:
if diff_type == "Added":
return f"[green]+[reset] ", 2
elif diff_type == "Deleted":
return f"[red]-[reset] ", 2
elif diff_type == "Edited":
return f"[light_yellow]+/-[reset] ", 4
else:
return "", 0


# https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L590
def get_longest_prefixes(fields: Optional[List[FieldDiff]], objects: Optional[List[ObjectDiff]]) -> Tuple[int, int]:
if fields is None:
fields = []
if objects is None:
objects = []

longest_field = longest_marker = 0

for field in fields:
l = len(field.Name)
if l > longest_field:
longest_field = l

_, l = get_diff_string(field.Type)
if l > longest_marker:
longest_marker = l

for obj in objects:
_, l = get_diff_string(obj.Type)
if l > longest_marker:
longest_marker = l

return longest_field, longest_marker


# https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L555
def aligned_field_and_objects(
fields: Optional[List[FieldDiff]],
objects: Optional[List[ObjectDiff]],
start_prefix: int,
longest_field: int,
longest_marker: int) -> str:

if fields is None:
fields = []
if objects is None:
objects = []

out = ""
num_fields = len(fields)
num_objects = len(objects)
have_objects = num_objects != 0

for i, field in enumerate(fields):
_, mLength = get_diff_string(field.Type)
kPrefix = longest_marker - mLength
vPrefix = longest_field - len(field.Name)
out += format_field_diff(field, start_prefix, kPrefix, vPrefix)

# Avoid a dangling new line
if i+1 != num_fields or have_objects:
out += "\n"

for i, object in enumerate(objects):
_, mLength = get_diff_string(object.Type)
kPrefix = longest_marker - mLength
out += format_object_diff(object, start_prefix, kPrefix)

# Avoid a dangling new line
if i+1 != num_objects:
out += "\n"

return out


# https://github.com/hashicorp/nomad/blob/v0.12.3/command/job_plan.go#L624
def color_annotations(annotations: Optional[List[str]]) -> str:
if not annotations:
return ""

colord = []
for annotation in annotations:
if annotation == "forces create":
colord.append(f'[green]{annotation}[reset]')
elif annotation == "forces destroy":
colord.append(f'[red]{annotation}[reset]')
elif annotation == "forces in-place update":
colord.append(f'[cyan]{annotation}[reset]')
elif annotation == "forces create/destroy update":
colord.append(f'[yellow]{annotation}[reset]')
else:
colord.append(annotation)

return ", ".join(colord)
51 changes: 51 additions & 0 deletions nomad_diff/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations
from typing import List, Optional, Dict

from pydantic import BaseModel


class JobDiff(BaseModel):
Fields: Optional[List[FieldDiff]]
ID: str
Objects: Optional[List[ObjectDiff]]
TaskGroups: Optional[List[TaskGroupDiff]]
Type: str


class TaskGroupDiff(BaseModel):
Fields: Optional[List[FieldDiff]]
Name: str
Objects: Optional[List[ObjectDiff]]
Tasks: Optional[List[TaskDiff]]
Type: str
Updates: Optional[Dict[str, int]]


class FieldDiff(BaseModel):
Annotations: Optional[List[str]]
Name: str
New: str
Old: str
Type: str


class ObjectDiff(BaseModel):
Fields: List[FieldDiff]
Name: str
Objects: Optional[List[ObjectDiff]]
Type: str


class TaskDiff(BaseModel):
Type: str
Name: str
Fields: Optional[List[FieldDiff]]
Objects: Optional[List[ObjectDiff]]
Annotations: Optional[List[str]]


ObjectDiff.update_forward_refs()
JobDiff.update_forward_refs()
TaskGroupDiff.update_forward_refs()


26 changes: 26 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from setuptools import setup

setup(
name='nomad-diff',
version='1.0.0',
description='Nomad job diff formatter in Python',
author='Strigo.io',
author_email='[email protected]',
python_requires='>=3.7',
url='https://github.com/strigo/nomad-diff',
packages=['nomad_diff'],
install_requires=[
'pydantic~=1.6.1',
'colorama~=0.4.3',
],
include_package_data=True,
license='MIT',
classifiers=[
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: Implementation :: CPython',
'Topic :: Software Development :: Libraries'
],
)

0 comments on commit 8cc7989

Please sign in to comment.