-
Notifications
You must be signed in to change notification settings - Fork 2
/
push-images
executable file
·190 lines (147 loc) · 5.94 KB
/
push-images
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#!/usr/bin/env python
import argparse
import os
import subprocess
import sys
BUILD_IMAGE_REPO = 'weaveworks'
SERVICE_REPO = 'github.com/weaveworks/service'
EXCLUDED_COMPONENTS = [
'build',
'postgres-configs-db',
]
class ConfigError(Exception):
"""There was a problem with the local repository's config."""
def no_such_image(build_image, image_tag, component):
"""Raised when we cannot find the build image."""
return ConfigError('''
Expected image {} to exist, given the working directory state:
{}
and the component to push:
{}
But it does not exist. The local build may not have resulted in a new image.
'''.format(build_image, image_tag, component))
def make_parser():
parser = argparse.ArgumentParser(prog='push-images')
# XXX: Not quite the same behaviour as the old one, since that has
# different behaviour depending on whether the flag is given with a value
# or without.
parser.add_argument(
'--if-changed-since', type=str, default=None, dest='since',
help='Only push images if changed since this revision')
parser.add_argument(
'--dry-run', action="store_true",
help='Do not perform side-effect (eg pushing images)')
parser.add_argument(
'components', type=str, nargs='*',
help='Which components to push')
return parser
def get_dependencies(component):
"""Returns the list of files and directories used to build 'component'.
Currently assumes that code is named for directories in components.
"""
# add itself
deps = [component]
# add go dependencies
try:
deps.extend(get_go_dependencies(component))
except KeyError:
raise ConfigError(
"Couldn't find dependencies for component {}".format(component))
# common non-go dependencies
deps.extend(common_dependencies())
return deps
def common_dependencies():
# If this script changes, push all images (should be rare).
# This is required because of the following case:
# 1. someone forgets to add their component to `push-images`
# 2. pushes their work to main
# 3. realises CI hasn't pushed the image
# 4. Edits `push-images` accordingly, but the image, again, isn't pushed
# because the code was not changed.
return ['push-images']
def get_go_dependencies(component):
# Fetch go dependencies using 'go list'
cmd = """
cd {} &&
for dep in `go list -f '{{{{ .Deps }}}}' ./...`; do
echo $dep | grep '{}';
done | xargs
""".format(component, SERVICE_REPO)
deps = subprocess.check_output(cmd, shell=True).strip().split()
deps = list(map(lambda dep: dep[len(SERVICE_REPO + '/'):], deps))
return deps
def has_changed_since(since, component):
"""Has 'component' changed since the given revision?
:raise ConfigError: If any of the dependencies are not files or directories
that exist on disk.
:return: True if component has changed since 'since', otherwise False.
"""
deps = get_dependencies(component)
for dep in deps:
if not os.path.exists(dep):
raise ConfigError(
'Component {} depends on {} but it does not exist'.format(
component, dep))
cmd = ['git', 'diff', '--quiet', since, '--']
cmd.extend(deps)
try:
subprocess.check_output(cmd)
except subprocess.CalledProcessError:
# git diff --quiet exits with 1 if there were differences.
return True
return False
def image_exists(image_name):
"""Is there an image named 'image_name' in the local registry?"""
try:
subprocess.check_output(['docker', 'inspect', image_name])
except subprocess.CalledProcessError:
return False
return True
def push_to_repo(build_image, push_image, dry_run=False):
"""Push 'build_image' to a Docker repository as 'push_image'."""
if build_image != push_image:
print('Tagging build image {} for pushing as {}'.format(build_image, push_image))
subprocess.check_call(['docker', 'tag', build_image, push_image])
print('Pushing {}{}'.format('<dry run> ' if dry_run else '', push_image))
if not dry_run:
subprocess.check_call(['docker', 'push', push_image])
def push_component(component, image_repository, image_tag, since=None, dry_run=False):
"""Push 'compenent' to 'image_repository' with 'image_tag'.
Do not push if not changed since 'since'. If 'since' not supplied, push
regardless.
"""
image = os.path.basename(component)
build_image = '{}/{}:{}'.format(BUILD_IMAGE_REPO, image, image_tag)
if not image_exists(build_image):
raise no_such_image(build_image, image_tag, component)
push_image = '{}/{}:{}'.format(image_repository, image, image_tag)
if since and not has_changed_since(since, component):
print(
'Not pushing image for {}, because no changes in {} since commit {}'.format(
push_image, component, since))
return
push_to_repo(build_image, push_image, dry_run)
def get_components():
components = subprocess.check_output(['make', 'images']).strip().split()
components = list(map(lambda dep: dep[len(BUILD_IMAGE_REPO + '/'):], components))
for excluded in EXCLUDED_COMPONENTS:
if excluded in components:
components.remove(excluded)
return components
def main():
image_repository = os.environ.get('IMAGE_REPOSITORY', BUILD_IMAGE_REPO)
parser = make_parser()
opts = parser.parse_args()
components = opts.components
if not components:
components = get_components()
print('Using components: {}'.format(' '.join(components)))
image_tag = subprocess.check_output(['./tools/image-tag']).strip()
for component in components:
try:
push_component(component, image_repository, image_tag, opts.since, opts.dry_run)
except ConfigError as e:
sys.stderr.write(str(e))
sys.exit(1)
if __name__ == '__main__':
main()