|
54 | 54 | LAUNCHES_INPROGRESS = Gauge('binderhub_inprogress_launches', 'Launches currently in progress') |
55 | 55 |
|
56 | 56 |
|
| 57 | +def _generate_build_name(build_slug, ref, prefix='', limit=63, ref_length=6): |
| 58 | + """Generate a unique build name with a limited character length. |
| 59 | +
|
| 60 | + Guaranteed (to acceptable level) to be unique for a given user, repo, |
| 61 | + and ref. |
| 62 | +
|
| 63 | + We really, *really* care that we always end up with the same |
| 64 | + 'build_name' for a particular repo + ref, but the default max |
| 65 | + character limit for build names is 63. To meet this constraint, we |
| 66 | + include a prefixed hash of the user / repo in all build names and do |
| 67 | + some length limiting :) |
| 68 | +
|
| 69 | + Note that ``build`` names only need to be unique over a shorter period |
| 70 | + of time, while ``image`` names need to be unique for longer. Hence, |
| 71 | + different strategies are used. |
| 72 | +
|
| 73 | + We also ensure that the returned value is DNS safe, by only using |
| 74 | + ascii lowercase + digits. everything else is escaped |
| 75 | + """ |
| 76 | + # escape parts that came from providers (build slug, ref) |
| 77 | + # build names are case-insensitive `.lower()` is called at the end |
| 78 | + build_slug = _safe_build_slug(build_slug, limit=limit - len(prefix) - ref_length - 1) |
| 79 | + ref = _safe_build_slug(ref, limit=ref_length, hash_length=2) |
| 80 | + |
| 81 | + return '{prefix}{safe_slug}-{ref}'.format( |
| 82 | + prefix=prefix, |
| 83 | + safe_slug=build_slug, |
| 84 | + ref=ref[:ref_length], |
| 85 | + ).lower() |
| 86 | + |
| 87 | + |
| 88 | +def _safe_build_slug(build_slug, limit, hash_length=6): |
| 89 | + """Create a unique-ish name from a slug. |
| 90 | +
|
| 91 | + This function catches a bug where a build slug may not produce a valid |
| 92 | + image name (e.g. arepo name ending with _, which results in image name |
| 93 | + ending with '-' which is invalid). This ensures that the image name is |
| 94 | + always safe, regardless of build slugs returned by providers |
| 95 | + (rather than requiring all providers to return image-safe build slugs |
| 96 | + below a certain length). |
| 97 | +
|
| 98 | + Since this changes the image name generation scheme, all existing cached |
| 99 | + images will be invalidated. |
| 100 | + """ |
| 101 | + build_slug_hash = hashlib.sha256(build_slug.encode('utf-8')).hexdigest() |
| 102 | + safe_chars = set(string.ascii_letters + string.digits) |
| 103 | + def escape(s): |
| 104 | + return escapism.escape(s, safe=safe_chars, escape_char='-') |
| 105 | + build_slug = escape(build_slug) |
| 106 | + return '{name}-{hash}'.format( |
| 107 | + name=build_slug[:limit - hash_length - 1], |
| 108 | + hash=build_slug_hash[:hash_length], |
| 109 | + ).lower() |
| 110 | + |
| 111 | + |
57 | 112 | class BuildHandler(BaseHandler): |
58 | 113 | """A handler for working with GitHub.""" |
59 | 114 |
|
@@ -125,63 +180,6 @@ def initialize(self): |
125 | 180 |
|
126 | 181 | self.event_log = self.settings['event_log'] |
127 | 182 |
|
128 | | - def _generate_build_name(self, build_slug, ref, prefix='', limit=63, ref_length=6): |
129 | | - """ |
130 | | - Generate a unique build name with a limited character length.. |
131 | | -
|
132 | | - Guaranteed (to acceptable level) to be unique for a given user, repo, |
133 | | - and ref. |
134 | | -
|
135 | | - We really, *really* care that we always end up with the same |
136 | | - 'build_name' for a particular repo + ref, but the default max |
137 | | - character limit for build names is 63. To meet this constraint, we |
138 | | - include a prefixed hash of the user / repo in all build names and do |
139 | | - some length limiting :) |
140 | | -
|
141 | | - Note that ``build`` names only need to be unique over a shorter period |
142 | | - of time, while ``image`` names need to be unique for longer. Hence, |
143 | | - different strategies are used. |
144 | | -
|
145 | | - We also ensure that the returned value is DNS safe, by only using |
146 | | - ascii lowercase + digits. everything else is escaped |
147 | | - """ |
148 | | - |
149 | | - # escape parts that came from providers (build slug, ref) |
150 | | - # only build_slug *really* needs this (refs should be sha1 hashes) |
151 | | - # build names are case-insensitive because ascii_letters are allowed, |
152 | | - # and `.lower()` is called at the end |
153 | | - safe_chars = set(string.ascii_letters + string.digits) |
154 | | - def escape(s): |
155 | | - return escapism.escape(s, safe=safe_chars, escape_char='-') |
156 | | - |
157 | | - build_slug = self._safe_build_slug(build_slug, limit=limit - len(prefix) - ref_length - 1) |
158 | | - ref = escape(ref) |
159 | | - |
160 | | - return '{prefix}{safe_slug}-{ref}'.format( |
161 | | - prefix=prefix, |
162 | | - safe_slug=build_slug, |
163 | | - ref=ref[:ref_length], |
164 | | - ).lower() |
165 | | - |
166 | | - def _safe_build_slug(self, build_slug, limit, hash_length=6): |
167 | | - """ |
168 | | - This function catches a bug where build slug may not produce a valid image name |
169 | | - (e.g. repo name ending with _, which results in image name ending with '-' which is invalid). |
170 | | - This ensures that the image name is always safe, regardless of build slugs returned by providers |
171 | | - (rather than requiring all providers to return image-safe build slugs below a certain length). |
172 | | - Since this changes the image name generation scheme, all existing cached images will be invalidated. |
173 | | - """ |
174 | | - build_slug_hash = hashlib.sha256(build_slug.encode('utf-8')).hexdigest() |
175 | | - safe_chars = set(string.ascii_letters + string.digits) |
176 | | - def escape(s): |
177 | | - return escapism.escape(s, safe=safe_chars, escape_char='-') |
178 | | - build_slug = escape(build_slug) |
179 | | - return '{name}-{hash}'.format( |
180 | | - name=build_slug[:limit - hash_length - 1], |
181 | | - hash=build_slug_hash[:hash_length], |
182 | | - ).lower() |
183 | | - |
184 | | - |
185 | 183 | async def fail(self, message): |
186 | 184 | await self.emit({ |
187 | 185 | 'phase': 'failed', |
@@ -280,9 +278,9 @@ async def get(self, provider_prefix, _unescaped_spec): |
280 | 278 | image_prefix = self.settings['image_prefix'] |
281 | 279 |
|
282 | 280 | # Enforces max 255 characters before image |
283 | | - safe_build_slug = self._safe_build_slug(provider.get_build_slug(), limit=255 - len(image_prefix)) |
| 281 | + safe_build_slug = _safe_build_slug(provider.get_build_slug(), limit=255 - len(image_prefix)) |
284 | 282 |
|
285 | | - build_name = self._generate_build_name(provider.get_build_slug(), ref, prefix='build-') |
| 283 | + build_name = _generate_build_name(provider.get_build_slug(), ref, prefix='build-') |
286 | 284 |
|
287 | 285 | image_name = self.image_name = '{prefix}{build_slug}:{ref}'.format( |
288 | 286 | prefix=image_prefix, |
|
0 commit comments