change builder API to return imageID
this allows more flexibility running CLI builder, especially we don't
have anymore to parse build output to get build status/imageID and can
pass the console FDs to buildkit
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Nicolas De Loof authored 3 years ago
Nicolas De loof committed 3 years ago
0 | 0 | import enum |
1 | 1 | import itertools |
2 | import json | |
3 | 2 | import logging |
4 | 3 | import os |
5 | 4 | import re |
1124 | 1123 | 'Impossible to perform platform-targeted builds for API version < 1.35' |
1125 | 1124 | ) |
1126 | 1125 | |
1127 | builder = self.client if not cli else _CLIBuilder(progress) | |
1128 | build_output = builder.build( | |
1126 | builder = _ClientBuilder(self.client) if not cli else _CLIBuilder(progress) | |
1127 | return builder.build( | |
1128 | service=self, | |
1129 | 1129 | path=path, |
1130 | 1130 | tag=self.image_name, |
1131 | 1131 | rm=rm, |
1146 | 1146 | gzip=gzip, |
1147 | 1147 | isolation=build_opts.get('isolation', self.options.get('isolation', None)), |
1148 | 1148 | platform=self.platform, |
1149 | ) | |
1150 | ||
1151 | try: | |
1152 | all_events = list(stream_output(build_output, output_stream)) | |
1153 | except StreamOutputError as e: | |
1154 | raise BuildError(self, str(e)) | |
1155 | ||
1156 | # Ensure the HTTP connection is not reused for another | |
1157 | # streaming command, as the Docker daemon can sometimes | |
1158 | # complain about it | |
1159 | self.client.close() | |
1160 | ||
1161 | image_id = None | |
1162 | ||
1163 | for event in all_events: | |
1164 | if 'stream' in event: | |
1165 | match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', '')) | |
1166 | if match: | |
1167 | image_id = match.group(1) | |
1168 | ||
1169 | if image_id is None: | |
1170 | raise BuildError(self, event if all_events else 'Unknown') | |
1171 | ||
1172 | return image_id | |
1149 | output_stream=output_stream) | |
1173 | 1150 | |
1174 | 1151 | def get_cache_from(self, build_opts): |
1175 | 1152 | cache_from = build_opts.get('cache_from', None) |
1826 | 1803 | return path |
1827 | 1804 | |
1828 | 1805 | |
1829 | class _CLIBuilder: | |
1830 | def __init__(self, progress): | |
1831 | self._progress = progress | |
1832 | ||
1833 | def build(self, path, tag=None, quiet=False, fileobj=None, | |
1806 | class _ClientBuilder: | |
1807 | def __init__(self, client): | |
1808 | self.client = client | |
1809 | ||
1810 | def build(self, service, path, tag=None, quiet=False, fileobj=None, | |
1834 | 1811 | nocache=False, rm=False, timeout=None, |
1835 | 1812 | custom_context=False, encoding=None, pull=False, |
1836 | 1813 | forcerm=False, dockerfile=None, container_limits=None, |
1837 | 1814 | decode=False, buildargs=None, gzip=False, shmsize=None, |
1838 | 1815 | labels=None, cache_from=None, target=None, network_mode=None, |
1839 | 1816 | squash=None, extra_hosts=None, platform=None, isolation=None, |
1840 | use_config_proxy=True): | |
1817 | use_config_proxy=True, output_stream=sys.stdout): | |
1818 | build_output = self.client.build( | |
1819 | path=path, | |
1820 | tag=tag, | |
1821 | nocache=nocache, | |
1822 | rm=rm, | |
1823 | pull=pull, | |
1824 | forcerm=forcerm, | |
1825 | dockerfile=dockerfile, | |
1826 | labels=labels, | |
1827 | cache_from=cache_from, | |
1828 | buildargs=buildargs, | |
1829 | network_mode=network_mode, | |
1830 | target=target, | |
1831 | shmsize=shmsize, | |
1832 | extra_hosts=extra_hosts, | |
1833 | container_limits=container_limits, | |
1834 | gzip=gzip, | |
1835 | isolation=isolation, | |
1836 | platform=platform) | |
1837 | ||
1838 | try: | |
1839 | all_events = list(stream_output(build_output, output_stream)) | |
1840 | except StreamOutputError as e: | |
1841 | raise BuildError(service, str(e)) | |
1842 | ||
1843 | # Ensure the HTTP connection is not reused for another | |
1844 | # streaming command, as the Docker daemon can sometimes | |
1845 | # complain about it | |
1846 | self.client.close() | |
1847 | ||
1848 | image_id = None | |
1849 | ||
1850 | for event in all_events: | |
1851 | if 'stream' in event: | |
1852 | match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', '')) | |
1853 | if match: | |
1854 | image_id = match.group(1) | |
1855 | ||
1856 | if image_id is None: | |
1857 | raise BuildError(service, event if all_events else 'Unknown') | |
1858 | ||
1859 | return image_id | |
1860 | ||
1861 | ||
1862 | class _CLIBuilder: | |
1863 | def __init__(self, progress): | |
1864 | self._progress = progress | |
1865 | ||
1866 | def build(self, service, path, tag=None, quiet=False, fileobj=None, | |
1867 | nocache=False, rm=False, timeout=None, | |
1868 | custom_context=False, encoding=None, pull=False, | |
1869 | forcerm=False, dockerfile=None, container_limits=None, | |
1870 | decode=False, buildargs=None, gzip=False, shmsize=None, | |
1871 | labels=None, cache_from=None, target=None, network_mode=None, | |
1872 | squash=None, extra_hosts=None, platform=None, isolation=None, | |
1873 | use_config_proxy=True, output_stream=sys.stdout): | |
1841 | 1874 | """ |
1842 | 1875 | Args: |
1876 | service (str): Service to be built | |
1843 | 1877 | path (str): Path to the directory containing the Dockerfile |
1844 | 1878 | buildargs (dict): A dictionary of build arguments |
1845 | 1879 | cache_from (:py:class:`list`): A list of images used for build |
1888 | 1922 | configuration file (``~/.docker/config.json`` by default) |
1889 | 1923 | contains a proxy configuration, the corresponding environment |
1890 | 1924 | variables will be set in the container being built. |
1925 | output_stream (writer): stream to use for build logs | |
1891 | 1926 | Returns: |
1892 | 1927 | A generator for the build output. |
1893 | 1928 | """ |
1920 | 1955 | |
1921 | 1956 | args = command_builder.build([path]) |
1922 | 1957 | |
1923 | magic_word = "Successfully built " | |
1924 | appear = False | |
1925 | with subprocess.Popen(args, stdout=subprocess.PIPE, | |
1958 | with subprocess.Popen(args, stdout=output_stream, stderr=sys.stderr, | |
1926 | 1959 | universal_newlines=True) as p: |
1927 | while True: | |
1928 | line = p.stdout.readline() | |
1929 | if not line: | |
1930 | break | |
1931 | if line.startswith(magic_word): | |
1932 | appear = True | |
1933 | yield json.dumps({"stream": line}) | |
1934 | ||
1935 | 1960 | p.communicate() |
1936 | 1961 | if p.returncode != 0: |
1937 | raise StreamOutputError() | |
1962 | raise BuildError(service, "Build failed") | |
1938 | 1963 | |
1939 | 1964 | with open(iidfile) as f: |
1940 | 1965 | line = f.readline() |
1941 | 1966 | image_id = line.split(":")[1].strip() |
1942 | 1967 | os.remove(iidfile) |
1943 | 1968 | |
1944 | # In case of `DOCKER_BUILDKIT=1` | |
1945 | # there is no success message already present in the output. | |
1946 | # Since that's the way `Service::build` gets the `image_id` | |
1947 | # it has to be added `manually` | |
1948 | if not appear: | |
1949 | yield json.dumps({"stream": "{}{}\n".format(magic_word, image_id)}) | |
1969 | return image_id | |
1950 | 1970 | |
1951 | 1971 | |
1952 | 1972 | class _CommandBuilder: |