Skip to content

Commit 9e2fdd5

Browse files
committed
compare instances and add ability to choose between csv and markdown format
1 parent 0381f31 commit 9e2fdd5

File tree

7 files changed

+194
-33
lines changed

7 files changed

+194
-33
lines changed

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ end
1414
desc 'Checks style'
1515
task audit: :rubocop
1616
task :audit do
17-
ignores = %w(D100 D101 D102 D103 D104 E501 I201)
17+
ignores = %w(D100 D101 D102 D103 D104 E501 I201 N806)
1818

1919
FILES = FileList[%w(bin/compose_diff compose_diff/*.py setup.py)]
2020
sh "flake8 --ignore='#{ignores * ','}' #{FILES}"

bin/compose_diff

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,32 @@ if __name__ == '__main__':
99

1010
parser = argparse.ArgumentParser()
1111
parser.add_argument(
12-
'--images', action='store_const', const=True,
13-
help='diffs the images', default=False)
12+
'--versions', action='store_const', const=True,
13+
help='diffs the image versions', default=False)
1414
parser.add_argument(
15-
'--filter', action='store',
15+
'--instances', action='store_const', const=True,
16+
help='diffs the instances', default=False)
17+
parser.add_argument(
18+
'--filter', action='append',
1619
help='just consider specific items', default=None)
20+
parser.add_argument(
21+
'--format', action='store',
22+
help='format (csv,markdown)', default='markdown')
1723
parser.add_argument('files', nargs=argparse.REMAINDER)
1824
args = parser.parse_args()
1925

20-
if args.images:
26+
if args.versions or args.instances:
2127
if len(args.files) != 2:
2228
print('please diff exactly two files. You provided {0} files.'.format(len(args.files)))
2329
sys.exit(1)
2430

2531
old, new = args.files[0], args.files[1]
2632

27-
ComposeDiff().diff_images(old, new, args.filter)
33+
diff = ComposeDiff(
34+
filter=args.filter,
35+
diff_versions=args.versions,
36+
diff_instances=args.instances,
37+
)
38+
print(diff.format(diff.diff(old, new), format=args.format))
2839
else:
2940
parser.print_help()

compose_diff/__init__.py

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,86 @@
11
#!/usr/bin/env python3
2+
import itertools
23
from yaml import load
34

45

56
class ComposeDiff:
67

7-
def __init__(self):
8-
pass
8+
def __init__(self, filter=[], diff_versions=False, diff_instances=False):
9+
self.filter = filter
10+
self.diff_versions = diff_versions
11+
self.diff_instances = diff_instances
912

10-
def diff_images(self, old, new, filter):
11-
old = self.images(self.read(old))
12-
new = self.images(self.read(new))
13+
def format(self, result, format='markdown'):
14+
columns = ['name']
15+
if self.diff_versions:
16+
columns.append('version')
17+
if self.diff_instances:
18+
columns.append('instances')
19+
CAPTIONS = {'name': 'Name', 'version': 'Version', 'instances': 'Instances'}
20+
res = []
1321

14-
print('| {0} | {1} |'.format('Name', 'Version'))
15-
print('| - | - |')
16-
for key in sorted(set(list(old.keys()) + list(new.keys()))):
17-
if filter is not None:
18-
if filter not in key:
22+
res.append([CAPTIONS[column] for column in columns])
23+
if format == 'markdown':
24+
res.append(['-' for _ in columns])
25+
for item in result:
26+
27+
def escape(data):
28+
data = str(data)
29+
30+
if format == 'markdown':
31+
data = data.replace('_', '\_')
32+
data = data.replace('|', '\|')
33+
if format == 'csv':
34+
data = data.replace(';', '\;')
35+
return data
36+
37+
res.append(list(escape(item[column]) for column in columns))
38+
39+
def format_table(res):
40+
string = ''
41+
42+
for item in res:
43+
if format == 'csv':
44+
string += ';'.join(item)
45+
elif format == 'markdown':
46+
string += '| {0} |'.format(' | '.join(item))
47+
string += '\n'
48+
return string
49+
return format_table(res)
50+
51+
def diff(self, old, new):
52+
old, new = self.read(old), self.read(new)
53+
54+
old_versions, new_versions = self.image_versions(old), self.image_versions(new)
55+
old_instances, new_instances = self.image_instances(old), self.image_instances(new)
56+
57+
keys = set(itertools.chain(old_versions.keys(), new_versions.keys(), old_instances.keys(), new_instances.keys()))
58+
result = []
59+
60+
for key in sorted(keys):
61+
if self.filter is not None:
62+
skip = True
63+
64+
for filter in self.filter:
65+
if filter in key:
66+
skip = False
67+
if skip:
1968
continue
20-
version = self.format_version(
21-
old=old[key] if key in old else None,
22-
new=new[key] if key in new else None)
69+
version = self.format_diff(
70+
old=old_versions[key] if key in old_versions else None,
71+
new=new_versions[key] if key in new_versions else None)
72+
instances = self.format_diff(
73+
old=old_instances[key] if key in old_instances else None,
74+
new=new_instances[key] if key in new_instances else None)
2375

24-
key = key.replace('_', '\_')
25-
version = version.replace('_', '\_')
26-
print('| {0} | {1} |'.format(key.split('/')[-1], version))
76+
result.append({'name': key.split('/')[-1], 'version': version, 'instances': instances}) # TODO: named tuple
77+
return result
2778

2879
@staticmethod
29-
def format_version(old, new):
80+
def format_diff(old, new):
3081
if old is None:
82+
if new is None:
83+
return ''
3184
return '{0} (new)'.format(new)
3285
elif new is None:
3386
return '{0} (deleted)'.format(old)
@@ -41,20 +94,39 @@ def read(path):
4194
return load(file.read())
4295

4396
@staticmethod
44-
def images(data):
97+
def image_versions(data):
4598
result = {}
4699

47-
if 'services' in data:
48-
data = data['services']
49-
for name, content in data.items():
100+
for name, content in ComposeDiff.services(data).items():
50101
if content is None:
51102
continue
52103
if 'image' not in content:
53104
continue
54105
image_name, tag = ComposeDiff.split_image(content['image'])
106+
55107
result[image_name] = tag
56108
return result
57109

110+
@staticmethod
111+
def image_instances(data):
112+
result = {}
113+
114+
for name, content in ComposeDiff.services(data).items():
115+
if content is None:
116+
continue
117+
if 'image' not in content:
118+
continue
119+
image_name, _ = ComposeDiff.split_image(content['image'])
120+
121+
if image_name not in result:
122+
result[image_name] = 0
123+
result[image_name] += 1
124+
return result
125+
126+
@staticmethod
127+
def services(data):
128+
return data['services'] if 'services' in data else data
129+
58130
@staticmethod
59131
def split_image(data):
60132
if ':' not in data.split('/')[-1]:

features/format.feature

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
Feature: Format
2+
As a DevOps
3+
I want to specify the format
4+
so that I can combine the tool with other tools
5+
6+
Scenario: CSV
7+
Given a file named "A.yml" with:
8+
"""
9+
version: "2"
10+
services:
11+
one:
12+
image: one:1
13+
"""
14+
And a file named "B.yml" with:
15+
"""
16+
version: "2"
17+
services:
18+
one:
19+
image: one:2
20+
"""
21+
When I run `bin/compose_diff --versions --format=csv A.yml B.yml`
22+
Then it should pass with exactly:
23+
"""
24+
Name;Version
25+
one;2 (1)
26+
"""
27+
28+
Scenario: Markdown
29+
Given a file named "A.yml" with:
30+
"""
31+
version: "2"
32+
services:
33+
one:
34+
image: one:1
35+
"""
36+
And a file named "B.yml" with:
37+
"""
38+
version: "2"
39+
services:
40+
one:
41+
image: one:2
42+
"""
43+
When I run `bin/compose_diff --versions --format=markdown A.yml B.yml`
44+
Then it should pass with exactly:
45+
"""
46+
| Name | Version |
47+
| - | - |
48+
| one | 2 (1) |
49+
"""

features/instances.feature

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Feature: Instances
2+
As an Operations Manager
3+
I want to see how many instances are deployed
4+
so that I know where the scalling happens
5+
6+
Scenario: Instances
7+
Given a file named "A.yml" with:
8+
"""
9+
version: "2"
10+
services:
11+
one:
12+
image: one:1
13+
"""
14+
And a file named "B.yml" with:
15+
"""
16+
version: "2"
17+
services:
18+
one:
19+
image: one:1
20+
one_2:
21+
image: one:1
22+
"""
23+
When I run `bin/compose_diff --instances A.yml B.yml`
24+
Then it should pass with exactly:
25+
"""
26+
| Name | Instances |
27+
| - | - |
28+
| one | 2 (1) |
29+
"""

features/images.feature renamed to features/versions.feature

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
Feature: Images
1+
Feature: Versions
22
As a Tester
3-
I want to see which images have changed
3+
I want to see which image versions have changed
44
so that I know where to test especially
55

66
Scenario: Diff Images
@@ -26,7 +26,7 @@ Feature: Images
2626
three:
2727
image: three:1
2828
"""
29-
When I run `bin/compose_diff --images A.yml B.yml`
29+
When I run `bin/compose_diff --versions A.yml B.yml`
3030
Then it should pass with exactly:
3131
"""
3232
| Name | Version |
@@ -56,10 +56,10 @@ Feature: Images
5656
two:
5757
image: two:2
5858
"""
59-
When I run `bin/compose_diff --images --filter one A.yml B.yml`
59+
When I run `bin/compose_diff --version --filter one A.yml B.yml`
6060
Then it should pass with exactly:
6161
"""
6262
| Name | Version |
6363
| - | - |
6464
| one | 2 (1) |
65-
"""
65+
"""

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def readme():
88

99
setup(
1010
name='compose_diff',
11-
version='0.2.0',
11+
version='0.3.0',
1212
description='diff docker-compose files',
1313
long_description=readme(),
1414
url='http://github.com/funkwerk/compose_diff',

0 commit comments

Comments
 (0)