def save( self, chunk_size: Optional[int] = api.DEFAULT_CHUNK_SIZE, named: Union[str, bool] = False, ) -> Iterator[bytes]: """Returns Image as tarball. Args: chunk_size: If None, data will be streamed in received buffer size. If not None, data will be returned in sized buffers. Default: 2MB named: When False, tarball will not retain repository and tag information. When True, first tag is used to identify the Image. when str, value is used as tag to identify the Image. Always ignored. Raises: APIError: when service returns an error. Notes: Format is set to docker-archive, this allows load() to import this tarball. """ _ = named response = self.client.get(f"/images/{self.id}/get", params={"format": ["docker-archive"]}, stream=True) if response.status_code == requests.codes.okay: return response.iter_content(chunk_size=chunk_size) body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def load(self, data: bytes) -> Iterable[Image]: """Restore an image previously saved. Args: data: Image to be loaded in tarball format. Yields: Raises: APIError: when service returns an error. """ response = self.client.post( "/images/load", data=data, headers={"Content-type": "application/x-www-form-urlencoded"}) body = response.json() if response.status_code != 200: raise APIError(body["cause"], response=response, explanation=body["message"]) # Dict[Literal["Names"], List[str]]] for i in body["Names"]: yield self.get(i)
def remove(self, digest: str) -> None: """Remove Image digest from manifest list. Args: digest: Image digest to be removed. Should a full Image reference be provided the digest will be parsed out. Raises: ImageNotFound: When the Image could not be found APIError: When service reports an error """ if "@" in digest: digest = digest.split("@", maxsplit=2)[1] response = self.client.delete(f"/manifests/{self.quoted_name}", params={"digest": digest}) if response.status_code == requests.codes.okay: return self.reload() body = response.json() if response.status_code == requests.codes.not_found: raise ImageNotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def search(self, term: str, **kwargs) -> List[Dict[str, Any]]: """Search Images on registries. Args: term: Used to target Image results. Keyword Args: filters (Mapping[str, List[str]): Refine results of search. Available filters: - is-automated (bool): Image build is automated. - is-official (bool): Image build is owned by product provider. - stars (int): Image has at least this number of stars. noTrunc (bool): Do not truncate any result string. Default: True. limit (int): Maximum number of results. Raises: APIError: when service returns an error """ params = { "term": [term], "noTrunc": True, } if "limit" in kwargs: params["limit"] = kwargs.pop("limit") if "filters" in kwargs: params["filters"] = api.format_filters(kwargs.pop("filters")) response = self.client.get("/images/search", params=params) body = response.json() if response.status_code == 200: return body raise APIError(body["cause"], response=response, explanation=body["message"])
def push( self, destination: str, all: Optional[bool] = None # pylint: disable=redefined-builtin ) -> None: """Push a manifest list or image index to a registry. Args: destination: Target for push. all: Push all images. Raises: NotFound: When the Manifest could not be found APIError: When service reports an error """ params = { "all": all, "destination": destination, } response = self.client.post(f"/manifests/{self.quoted_name}/push", params=params) if response.status_code == requests.codes.okay: return body = response.json() if response.status_code == requests.codes.not_found: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def create( self, name: str, data: bytes, labels: Optional[Mapping[str, Any]] = None, # pylint: disable=unused-argument driver: Optional[str] = None, ) -> Secret: """Create a Secret. Args: name: User-defined name of the secret. data: Secret to be registered with Podman service. labels: Ignored. driver: Secret driver. Raises: APIError when service returns an error. """ params = { "name": name, "driver": driver, } response = self.client.post("/secrets/create", params=params, data=data) body = response.json() if response.status_code == requests.codes.okay: return self.get(body["ID"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def list(self, **kwargs) -> List[Image]: """Report on images. Keyword Args: name (str) – Only show images belonging to the repository name all (bool) – Show intermediate image layers. By default, these are filtered out. filters (Mapping[str, Union[str, List[str]]) – Filters to be used on the image list. Available filters: - dangling (bool) - label (Union[str, List[str]]): format either "key", "key=value" Raises: APIError: when service returns an error. """ params = {} if "all" in kwargs: params["all"] = kwargs.pop("all") if "name" in kwargs: params["name"] = kwargs.pop("name") if "filters" in kwargs: params["filters"] = api.format_filters(kwargs.pop("filters")) response = self.client.get("/images/json", params=params) body = response.json() if response.status_code != 200: raise APIError(body["cause"], response=response, explanation=body["message"]) images: List[Image] = [] for element in body: images.append(self.prepare_model(element)) return images
def save(self, chunk_size: Optional[int] = 2097152, named: Union[str, bool] = False) -> Iterator[bytes]: """Returns Image as tarball. Args: chunk_size: If None, data will be streamed in received buffer size. If not None, data will be returned in sized buffers. Default: 2MB named: When False, tarball will not retain repository and tag information. When True, first tag is used to identify the Image. when str, value is used as tag to identify the Image. Always ignored. Raises: APIError: when service returns an error. """ _ = named response = self.client.get(f"/images/{self.id}/get", stream=True) if response.status_code == 200: return response.iter_content(chunk_size=chunk_size) body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def push( self, repository: str, tag: Optional[str] = None, **kwargs ) -> Union[str, Iterator[Union[str, Dict[str, Any]]]]: """Push Image or repository to the registry. Args: repository: Target repository for push tag: Tag to push, if given Keyword Args: auth_config (Mapping[str, str]: Override configured credentials. Must include username and password keys. decode (bool): return data from server as Dict[str, Any]. Ignored unless stream=True. destination (str): alternate destination for image. (Podman only) stream (bool): return output as blocking generator. Default: False. tlsVerify (bool): Require TLS verification. Raises: APIError: when service returns an error. """ # TODO set X-Registry-Auth headers = { # A base64url-encoded auth configuration "X-Registry-Auth": "" } params = { "destination": kwargs.get("destination"), "tlsVerify": kwargs.get("tlsVerify"), } name = urllib.parse.quote_plus(repository) response = self.client.post(f"/images/{name}/push", params=params, headers=headers) if response.status_code != requests.codes.ok: body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"]) tag_count = 0 if tag is None else 1 body = [ { "status": f"Pushing repository {repository} ({tag_count} tags)", }, { "status": "Pushing", "progressDetail": {}, "id": repository, }, ] stream = kwargs.get("stream", False) decode = kwargs.get("decode", False) if stream: return self._push_helper(decode, body) with io.StringIO() as buffer: for entry in body: buffer.write(json.dumps(entry) + "\n") return buffer.getvalue()
def prune(self, filters: Optional[Mapping[str, Any]] = None) -> Dict[str, Any]: """Delete unused images. Args: filters: Qualify Images to prune. Available filters: - dangling (bool): when true, only delete unused and untagged images. - until (str): Delete images older than this timestamp. Raises: APIError: when service returns an error. Note: The Untagged key will always be "". """ response = self.client.post( "/images/prune", params={"filters": api.prepare_filters(filters)} ) body = response.json() if response.status_code != requests.codes.ok: raise APIError(body["cause"], response=response, explanation=body["message"]) deleted: List[Dict[str, str]] = [] error: List[str] = [] reclaimed: int = 0 for element in response.json(): if "Err" in element and element["Err"] is not None: error.append(element["Err"]) else: reclaimed += element["Size"] deleted.append( { "Deleted": element["Id"], "Untagged": "", } ) if len(error) > 0: raise APIError(response.url, response=response, explanation="; ".join(error)) # body -> Dict[Literal["ImagesDeleted", "SpaceReclaimed"], # List[Dict[Literal["Deleted", "Untagged"], str] return { "ImagesDeleted": deleted, "SpaceReclaimed": reclaimed, }
def add(self, images: List[Union[Image, str]], **kwargs) -> None: """Add Image to manifest list. Args: images: List of Images to be added to manifest. Keyword Args: all (bool): annotation (Dict[str, str]): arch (str): features (List[str]): os (str): os_version (str): variant (str): Raises: ImageNotFound: When Image(s) could not be found APIError: When service reports an error """ params = { "all": kwargs.get("all"), "annotation": kwargs.get("annotation"), "arch": kwargs.get("arch"), "features": kwargs.get("features"), "images": list(), "os": kwargs.get("os"), "os_version": kwargs.get("os_version"), "variant": kwargs.get("variant"), } for item in images: if isinstance(item, Image): item = item.attrs["RepoTags"][0] params["images"].append(item) data = api.prepare_body(params) response = self.client.post(f"/manifests/{self.quoted_name}/add", data=data) if response.status_code == requests.codes.okay: return self.reload() body = response.json() if response.status_code == requests.codes.not_found: raise ImageNotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def _request( self, method: str, path: Union[str, bytes], data: _Data = None, params: Union[None, bytes, Mapping[str, str]] = None, headers: Optional[Mapping[str, str]] = None, timeout: _Timeout = None, stream: Optional[bool] = None, **kwargs, ) -> Response: """HTTP operation against configured Podman service. Args: method: HTTP method to use for request path: Relative path to RESTful resource. base_url will be prepended to path. params: Optional parameters to include with URL. headers: Optional headers to include in request. timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple Keyword Args: compatible: Will override the default path prefix with compatible prefix Raises: APIError: when service returns an Error. """ if timeout is None: timeout = api.DEFAULT_TIMEOUT if not path.startswith("/"): path = f"/{path}" compatible = kwargs.get("compatible", False) path_prefix = self.compatible_prefix if compatible else self.path_prefix uri = self.base_url + path_prefix + path try: return self.request( method.upper(), uri, params=params, data=data, headers=(headers or {}), timeout=timeout, stream=stream, ) except OSError as e: raise APIError( uri, explanation=f"{method.upper()} operation failed") from e
def list(self, **kwargs) -> List[Secret]: """Report on Secrets. Keyword Args: filters (Dict[str, Any]): Ignored. Raises: APIError: When error returned by service. """ response = self.client.get("/secrets/json") body = response.json() if response.status_code == requests.codes.okay: return [self.prepare_model(attrs=item) for item in body] raise APIError(body["cause"], response=response, explanation=body["message"])
def create( self, names: List[str], images: Optional[List[Union[Image, str]]] = None, all: Optional[bool] = None, # pylint: disable=redefined-builtin ) -> Manifest: """Create a Manifest. Args: names: Identifiers to be added to the manifest. There must be at least one. images: Images or Image identifiers to be included in the manifest. all: When True, add all contents from images given. Raises: ValueError: When no names are provided. NotFoundImage: When a given image does not exist. """ if names is None or len(names) == 0: raise ValueError("At least one manifest name is required.") params = {"name": names} if images is not None: params["image"] = list() for item in images: if isinstance(item, Image): item = item.attrs["RepoTags"][0] params["image"].append(item) if all is not None: params["all"] = all response = self.client.post("/manifests/create", params=params) body = response.json() if response.status_code == requests.codes.okay: manifest = self.get(body["Id"]) manifest.attrs["names"] = names return manifest if response.status_code == requests.codes.not_found: raise ImageNotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def remove(self, all: Optional[bool] = None): # pylint: disable=redefined-builtin """Delete secret. Args: all: When True, delete all secrets. Raises: NotFound: Secret does not exist. APIError: Error returned by service. """ response = self.client.delete(f"/secrets/{self.id}", params={"all": all}) if response.status_code == requests.codes.no_content: return body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"])
def get(self, secret_id: str) -> Secret: # pylint: disable=arguments-differ """Return information for Secret by name or id. Args: secret_id: Secret name or id. Raises: NotFound: Secret does not exist. APIError: Error returned by service. """ response = self.client.get(f"/secrets/{secret_id}/json") body = response.json() if response.status_code == requests.codes.okay: return self.prepare_model(attrs=body) if response.status_code == 404: raise NotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def remove(self, image: str, force: bool = False, noprune: bool = False) -> List[Dict[str, Union[str, int]]]: """Delete image from Podman service. Args: image: Name or Id of Image to remove force: delete Image if in use noprune: do not delete untagged parents. Ignored. Returns: List[Dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]] Raises: APIError: when service returns an error. Notes: The dictionaries with keys Errors and ExitCode are added. """ _ = noprune params = {} if force: params["force"] = str(bool(force)) response = self.client.delete(f"/images/{image}", params=params) body = response.json() if response.status_code != 200: raise APIError(body["cause"], response=response, explanation=body["message"]) # Dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[int, List[str]]] results: List[Dict[str, Union[int, str]]] = [] for key in ("Deleted", "Untagged", "Errors"): if key in body: for element in body[key]: results.append({key: element}) results.append({"ExitCode": body["ExitCode"]}) return results
def history(self) -> List[Dict[str, Any]]: """Returns history of the Image. Raises: APIError: when service returns an error. """ response = self.client.get(f"/images/{self.id}/history") body = response.json() if response.status_code == requests.codes.ok: return body if response.status_code == requests.codes.not_found: raise ImageNotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def get(self, name: str) -> Image: # pylint: disable=arguments-differ """Returns an image by name or id. Args: name: Image id or name for which to search Raises: ImageNotFound: when image does not exist. APIError: when service returns an error. """ name = urllib.parse.quote_plus(name) response = self.client.get(f"/images/{name}/json") body = response.json() if response.status_code == requests.codes.ok: return self.prepare_model(body) if response.status_code == requests.codes.not_found: raise ImageNotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"])
def remove( self, image: Union[Image, str], force: Optional[bool] = None, noprune: bool = False ) -> List[Dict[str, Union[str, int]]]: """Delete image from Podman service. Args: image: Name or Id of Image to remove force: Delete Image even if in use noprune: Do not delete untagged parents. Ignored. Returns: List[Dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]] Raises: APIError: when service returns an error. Notes: The dictionaries with keys Errors and ExitCode are added. """ _ = noprune if isinstance(image, Image): image = image.id response = self.client.delete(f"/images/{image}", params={"force": force}) body = response.json() if response.status_code != requests.codes.ok: if response.status_code == requests.codes.not_found: raise ImageNotFound(body["cause"], response=response, explanation=body["message"]) raise APIError(body["cause"], response=response, explanation=body["message"]) # Dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[int, List[str]]] results: List[Dict[str, Union[int, str]]] = [] for key in ("Deleted", "Untagged", "Errors"): if key in body: for element in body[key]: results.append({key: element}) results.append({"ExitCode": body["ExitCode"]}) return results
def load(self, data: bytes) -> Generator[Image, None, None]: """Restore an image previously saved. Args: data: Image to be loaded in tarball format. Raises: APIError: when service returns an error. """ # TODO fix podman swagger cannot use this header! # headers = {"Content-type": "application/x-www-form-urlencoded"} headers = {"Content-type": "application/x-tar"} response = self.client.post("/images/load", data=data, headers=headers) body = response.json() if response.status_code != requests.codes.ok: raise APIError(body["cause"], response=response, explanation=body["message"]) # Dict[Literal["Names"], List[str]]] for item in body["Names"]: yield self.get(item)
def tag(self, repository: str, tag: Optional[str], force: bool = False) -> bool: """Tag Image into repository. Args: repository: The repository for tagging Image. tag: optional tag name. force: force tagging. Always ignored. Returns: True, when operational succeeds. Raises: ImageNotFound: when service cannot find image. APIError: when service returns an error. """ _ = force params = {"repo": repository} if tag is not None: params["tag"] = tag response = self.client.post(f"/images/{self.id}/tag", params=params) if response.status_code == requests.codes.created: return True error = response.json() if response.status_code == requests.codes.not_found: raise ImageNotFound(error["cause"], response=response, explanation=error["message"]) raise APIError(error["cause"], response=response, explanation=error["message"])
def pull( self, repository: str, tag: Optional[str] = None, all_tags: bool = False, **kwargs ) -> Union[Image, List[Image]]: """Request Podman service to pull image(s) from repository. Args: repository: repository to pull from tag: image tag to pull, if None all tags in repository are pulled. all_tags: pull all image tags from repository. Keyword Args: auth_config (Mapping[str, str]) – Override the credentials that are found in the config for this request. auth_config should contain the username and password keys to be valid. platform (str) – Platform in the format os[/arch[/variant]] tls_verify (bool) - Require TLS verification. Default: True. Returns: If all_tags is True, return list of Image's rather than Image pulled. Raises: APIError: when service returns an error. """ if tag is None or len(tag) == 0: tag = "latest" params = { "reference": repository, "tlsVerify": kwargs.get("tls_verify"), } if all_tags: params["allTags"] = True else: params["reference"] = f"{repository}:{tag}" if "platform" in kwargs: platform = kwargs.get("platform") ospm, arch, variant = platform.split("/") if ospm is not None: params["OS"] = ospm if arch is not None: params["Arch"] = arch if variant is not None: params["Variant"] = variant if "auth_config" in kwargs: username = kwargs["auth_config"].get("username") password = kwargs["auth_config"].get("password") if username is None or password is None: raise ValueError("'auth_config' requires keys 'username' and 'password'") params["credentials"] = f"{username}:{password}" response = self.client.post("/images/pull", params=params) if response.status_code != requests.codes.ok: body = response.json() raise APIError(body["cause"], response=response, explanation=body["error"]) for item in response.iter_lines(): body = json.loads(item) if all_tags and "images" in body: images: List[Image] = [] for name in body["images"]: images.append(self.get(name)) return images if "id" in body: return self.get(body["id"]) return self.resource()
def build(self, **kwargs) -> Tuple[Image, Iterator[bytes]]: """Returns built image. Keyword Args: path (str) – Path to the directory containing the Dockerfile fileobj – A file object to use as the Dockerfile. (Or an IO object) tag (str) – A tag to add to the final image quiet (bool) – Whether to return the status nocache (bool) – Don’t use the cache when set to True rm (bool) – Remove intermediate containers. Default True timeout (int) – HTTP timeout custom_context (bool) – Optional if using fileobj (ignored) encoding (str) – The encoding for a stream. Set to gzip for compressing (ignored) pull (bool) – Downloads any updates to the FROM image in Dockerfile forcerm (bool) – Always remove intermediate containers, even after unsuccessful builds dockerfile (str) – path within the build context to the Dockerfile buildargs (Mapping[str,str) – A dictionary of build arguments container_limits (Dict[str, Union[int,str]]) – A dictionary of limits applied to each container created by the build process. Valid keys: - memory (int): set memory limit for build - memswap (int): Total memory (memory + swap), -1 to disable swap - cpushares (int): CPU shares (relative weight) - cpusetcpus (str): CPUs in which to allow execution, For example, "0-3", "0,1" - cpuperiod (int): CPU CFS (Completely Fair Scheduler) period (Podman only) - cpuquota (int): CPU CFS (Completely Fair Scheduler) quota (Podman only) shmsize (int) – Size of /dev/shm in bytes. The size must be greater than 0. If omitted the system uses 64MB labels (Mapping[str,str]) – A dictionary of labels to set on the image cache_from (List[str]) – A list of image's identifier used for build cache resolution target (str) – Name of the build-stage to build in a multi-stage Dockerfile network_mode (str) – networking mode for the run commands during build squash (bool) – Squash the resulting images layers into a single layer. extra_hosts (Dict[str,str]) – Extra hosts to add to /etc/hosts in building containers, as a mapping of hostname to IP address. platform (str) – Platform in the format os[/arch[/variant]]. isolation (str) – Isolation technology used during build. (ignored) use_config_proxy (bool) – (ignored) http_proxy (bool) - Inject http proxy environment variables into container (Podman only) Returns: first item is Image built second item is the build logs Raises: BuildError: when there is an error during the build. APIError: when service returns an error. TypeError: when neither path nor fileobj is not specified. """ params = self._render_params(kwargs) body = None path = None if "fileobj" in kwargs: path = tempfile.TemporaryDirectory() filename = pathlib.Path(path.name) / params["dockerfile"] with open(filename, "w") as file: shutil.copyfileobj(kwargs["fileobj"], file) body = api.create_tar(anchor=path.name, gzip=kwargs.get("gzip", False)) elif "path" in kwargs: # The Dockerfile will be copied into the context_dir if needed params["dockerfile"] = api.prepare_containerfile(kwargs["path"], params["dockerfile"]) excludes = api.prepare_dockerignore(kwargs["path"]) body = api.create_tar( anchor=kwargs["path"], exclude=excludes, gzip=kwargs.get("gzip", False) ) timeout = kwargs.get("timeout", api.DEFAULT_TIMEOUT) response = self.client.post( "/build", params=params, data=body, headers={ "Content-type": "application/x-tar", # "X-Registry-Config": "TODO", }, stream=True, timeout=timeout, ) if hasattr(body, "close"): body.close() if hasattr(path, "cleanup"): path.cleanup() if response.status_code != requests.codes.okay: body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"]) image_id = unknown = None marker = re.compile(r"(^[0-9a-f]+)\n$") report_stream, stream = itertools.tee(response.iter_lines()) for line in stream: result = json.loads(line) if "error" in result: raise BuildError(result["error"], report_stream) if "stream" in result: match = marker.match(result["stream"]) if match: image_id = match.group(1) unknown = line if image_id: return self.get(image_id), report_stream raise BuildError(unknown or "Unknown", report_stream)
def build(self, **kwargs) -> Tuple[Image, Iterator[bytes]]: """Returns built image. Keyword Args: path (str) – Path to the directory containing the Dockerfile fileobj – A file object to use as the Dockerfile. (Or an IO object) tag (str) – A tag to add to the final image quiet (bool) – Whether to return the status nocache (bool) – Don’t use the cache when set to True rm (bool) – Remove intermediate containers. Default True timeout (int) – HTTP timeout custom_context (bool) – Optional if using fileobj (ignored) encoding (str) – The encoding for a stream. Set to gzip for compressing (ignored) pull (bool) – Downloads any updates to the FROM image in Dockerfile forcerm (bool) – Always remove intermediate containers, even after unsuccessful builds dockerfile (str) – path within the build context to the Dockerfile buildargs (Mapping[str,str) – A dictionary of build arguments container_limits (Dict[str, Union[int,str]]) – A dictionary of limits applied to each container created by the build process. Valid keys: - memory (int): set memory limit for build - memswap (int): Total memory (memory + swap), -1 to disable swap - cpushares (int): CPU shares (relative weight) - cpusetcpus (str): CPUs in which to allow execution, For example, "0-3", "0,1" - cpuperiod (int): CPU CFS (Completely Fair Scheduler) period (Podman only) - cpuquota (int): CPU CFS (Completely Fair Scheduler) quota (Podman only) shmsize (int) – Size of /dev/shm in bytes. The size must be greater than 0. If omitted the system uses 64MB labels (Mapping[str,str]) – A dictionary of labels to set on the image cache_from (List[str]) – A list of image's identifier used for build cache resolution target (str) – Name of the build-stage to build in a multi-stage Dockerfile network_mode (str) – networking mode for the run commands during build squash (bool) – Squash the resulting images layers into a single layer. extra_hosts (Dict[str,str]) – Extra hosts to add to /etc/hosts in building containers, as a mapping of hostname to IP address. platform (str) – Platform in the format os[/arch[/variant]]. isolation (str) – Isolation technology used during build. (ignored) use_config_proxy (bool) – (ignored) http_proxy (bool) - Inject http proxy environment variables into container (Podman only) Returns: first item is Image built second item is the build logs Raises: BuildError: when there is an error during the build. APIError: when service returns an error. TypeError: when neither path nor fileobj is not specified. """ if "path" not in kwargs and "fileobj" not in kwargs: raise TypeError("Either path or fileobj must be provided.") if "gzip" in kwargs and "encoding" in kwargs: raise PodmanError( "Custom encoding not supported when gzip enabled.") # All unsupported kwargs are silently ignored params = { "dockerfile": kwargs.get("dockerfile", None), "forcerm": kwargs.get("forcerm", None), "httpproxy": kwargs.get("http_proxy", None), "networkmode": kwargs.get("network_mode", None), "nocache": kwargs.get("nocache", None), "platform": kwargs.get("platform", None), "pull": kwargs.get("pull", None), "q": kwargs.get("quiet", None), "remote": kwargs.get("remote", None), "rm": kwargs.get("rm", None), "shmsize": kwargs.get("shmsize", None), "squash": kwargs.get("squash", None), "t": kwargs.get("tag", None), "target": kwargs.get("target", None), } if "buildargs" in kwargs: params["buildargs"] = json.dumps(kwargs.get("buildargs")) if "cache_from" in kwargs: params["cacheform"] = json.dumps(kwargs.get("cache_from")) if "container_limits" in kwargs: params["cpuperiod"] = kwargs["container_limits"].get( "cpuperiod", None) params["cpuquota"] = kwargs["container_limits"].get( "cpuquota", None) params["cpusetcpus"] = kwargs["container_limits"].get( "cpusetcpus", None) params["cpushares"] = kwargs["container_limits"].get( "cpushares", None) params["memory"] = kwargs["container_limits"].get("memory", None) params["memswap"] = kwargs["container_limits"].get("memswap", None) if "extra_hosts" in kwargs: params["extrahosts"] = json.dumps(kwargs.get("extra_hosts")) if "labels" in kwargs: params["labels"] = json.dumps(kwargs.get("labels")) params = dict(filter(lambda e: e[1] is not None, params.items())) context_dir = params.pop("path", None) timeout = params.pop("timeout", api.DEFAULT_TIMEOUT) body = None if context_dir: # The Dockerfile will be copied into the context_dir if needed params["dockerfile"] = api.prepare_dockerfile( context_dir, params["dockerfile"]) excludes = api.prepare_dockerignore(context_dir) body = api.create_tar(context_dir, exclude=excludes, gzip=kwargs.get("gzip", False)) response = self.client.post( "/build", params=params, data=body, headers={ "Content-type": "application/x-tar", "X-Registry-Config": "TODO", }, stream=True, timeout=timeout, ) if hasattr(body, "close"): body.close() if response.status_code != 200: body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"]) image_id = unknown = None marker = re.compile(r'(^Successfully built |sha256:)([0-9a-f]+)$') report_stream, stream = itertools.tee(response.iter_lines()) for line in stream: result = json.loads(line) if "error" in result: raise BuildError(result["error"], report_stream) if "stream" in result: match = marker.match(result["stream"]) if match: image_id = match.group(2) unknown = line if image_id: return self.get(image_id), report_stream raise BuildError(unknown or 'Unknown', report_stream)