async def parse_dockerfile(request: DockerfileInfoRequest) -> DockerfileInfo: wrapped_target = await Get( WrappedTarget, WrappedTargetRequest(request.address, description_of_origin="<infallible>")) target = wrapped_target.target sources = await Get( HydratedSources, HydrateSourcesRequest( target.get(SourcesField), for_sources_types=(DockerImageSourceField, ), enable_codegen=True, ), ) dockerfiles = sources.snapshot.files assert len(dockerfiles) == 1, ( f"Internal error: Expected a single source file to Dockerfile parse request {request}, " f"got: {dockerfiles}.") result = await Get( ProcessResult, DockerfileParseRequest( sources.snapshot.digest, dockerfiles, ), ) try: raw_output = result.stdout.decode("utf-8") outputs = json.loads(raw_output) assert len(outputs) == len(dockerfiles) except Exception as e: raise DockerfileInfoError( f"Unexpected failure to parse Dockerfiles: {', '.join(dockerfiles)}, " f"for the {request.address} target: {e}") from e info = outputs[0] try: return DockerfileInfo( address=request.address, digest=sources.snapshot.digest, source=info["source"], build_args=DockerBuildArgs.from_strings( *info["build_args"], duplicates_must_match=True), copy_source_paths=tuple(info["copy_source_paths"]), from_image_build_args=DockerBuildArgs.from_strings( *info["from_image_build_args"], duplicates_must_match=True), version_tags=tuple(info["version_tags"]), ) except ValueError as e: raise DockerfileInfoError( f"Error while parsing {info['source']} for the {request.address} target: {e}" ) from e
class DockerfileInfo: address: Address digest: Digest # Data from the parsed Dockerfile, keep in sync with # `dockerfile_wrapper_script.py:ParsedDockerfileInfo`: source: str build_args: DockerBuildArgs = DockerBuildArgs() copy_source_paths: tuple[str, ...] = () from_image_build_args: DockerBuildArgs = DockerBuildArgs() version_tags: tuple[str, ...] = ()
class DockerfileInfo: address: Address digest: Digest source: str putative_target_addresses: tuple[str, ...] = () version_tags: tuple[str, ...] = () build_args: DockerBuildArgs = DockerBuildArgs() from_image_build_arg_names: tuple[str, ...] = () copy_sources: tuple[str, ...] = ()
def test_create_docker_build_context() -> None: context = DockerBuildContext.create( build_args=DockerBuildArgs.from_strings("ARGNAME=value1"), snapshot=EMPTY_SNAPSHOT, build_env=DockerBuildEnvironment.create({"ENVNAME": "value2"}), dockerfile_info=DockerfileInfo( address=Address("test"), digest=EMPTY_DIGEST, source="test/Dockerfile", build_args=DockerBuildArgs.from_strings(), copy_source_paths=(), from_image_build_args=DockerBuildArgs.from_strings(), version_tags=("base latest", "stage1 1.2", "dev 2.0", "prod 2.0"), ), ) assert list(context.build_args) == ["ARGNAME=value1"] assert dict(context.build_env.environment) == {"ENVNAME": "value2"} assert context.dockerfile == "test/Dockerfile" assert context.stages == ("base", "dev", "prod")
async def parse_dockerfile(request: DockerfileInfoRequest) -> DockerfileInfo: wrapped_target = await Get(WrappedTarget, Address, request.address) target = wrapped_target.target sources = await Get( HydratedSources, HydrateSourcesRequest( target.get(SourcesField), for_sources_types=(DockerImageSourceField,), enable_codegen=True, ), ) dockerfile = sources.snapshot.files[0] result = await Get( ProcessResult, DockerfileParseRequest( sources.snapshot.digest, ( "version-tags,putative-targets,build-args,from-image-build-args,copy-sources", dockerfile, ), ), ) output = result.stdout.decode("utf-8").strip().split("\n") ( version_tags, putative_targets, build_args, from_image_build_arg_names, copy_sources, ) = split_iterable("---", output) try: return DockerfileInfo( address=request.address, digest=sources.snapshot.digest, source=dockerfile, putative_target_addresses=putative_targets, version_tags=version_tags, build_args=DockerBuildArgs.from_strings(*build_args, duplicates_must_match=True), from_image_build_arg_names=from_image_build_arg_names, copy_sources=copy_sources, ) except ValueError as e: raise DockerfileInfoError( f"Error while parsing {dockerfile} for the {request.address} target: {e}" ) from e
def test_docker_binary_build_image(docker_path: str, docker: DockerBinary) -> None: dockerfile = "src/test/repo/Dockerfile" digest = Digest(sha256().hexdigest(), 123) tags = ( "test:0.1.0", "test:latest", ) env = {"DOCKER_HOST": "tcp://127.0.0.1:1234"} build_request = docker.build_image( tags=tags, digest=digest, dockerfile=dockerfile, build_args=DockerBuildArgs.from_strings("arg1=2"), context_root="build/context", env=env, extra_args=("--pull", "--squash"), ) assert build_request == Process( argv=( docker_path, "build", "--pull", "--squash", "--tag", tags[0], "--tag", tags[1], "--build-arg", "arg1=2", "--file", dockerfile, "build/context", ), env=env, input_digest=digest, cache_scope=ProcessCacheScope.PER_SESSION, description="", # The description field is marked `compare=False` ) assert build_request.description == "Building docker image test:0.1.0 +1 additional tag."
def test_build_args(rule_runner: RuleRunner) -> None: rule_runner.write_files({ "test/BUILD": "docker_image()", "test/Dockerfile": dedent("""\ ARG registry FROM ${registry}/image:latest ARG OPT_A ARG OPT_B=default_b_value ENV A=${OPT_A:-A_value} ENV B=${OPT_B} """), }) addr = Address("test") info = rule_runner.request(DockerfileInfo, [DockerfileInfoRequest(addr)]) assert info.build_args == DockerBuildArgs.from_strings( "registry", "OPT_A", "OPT_B=default_b_value", )
async def create_docker_build_context( request: DockerBuildContextRequest, docker_options: DockerOptions) -> DockerBuildContext: # Get all targets to include in context. transitive_targets = await Get(TransitiveTargets, TransitiveTargetsRequest([request.address])) docker_image = transitive_targets.roots[0] # Get all dependencies for the root target. root_dependencies = await Get( Targets, DependenciesRequest(docker_image.get(Dependencies))) # Get all file sources from the root dependencies. That includes any non-file sources that can # be "codegen"ed into a file source. sources_request = Get( SourceFiles, SourceFilesRequest( sources_fields=[ tgt.get(SourcesField) for tgt in root_dependencies ], for_sources_types=( DockerContextFilesSourcesField, FileSourceField, ), enable_codegen=True, ), ) embedded_pkgs_per_target_request = Get( FieldSetsPerTarget, FieldSetsPerTargetRequest(PackageFieldSet, transitive_targets.dependencies), ) sources, embedded_pkgs_per_target, dockerfile_info = await MultiGet( sources_request, embedded_pkgs_per_target_request, Get(DockerfileInfo, DockerfileInfoRequest(docker_image.address)), ) # Package binary dependencies for build context. embedded_pkgs = await MultiGet( Get(BuiltPackage, PackageFieldSet, field_set) for field_set in embedded_pkgs_per_target.field_sets # Exclude docker images, unless build_upstream_images is true. if request.build_upstream_images or not isinstance( getattr(field_set, "source", None), DockerImageSourceField)) if request.build_upstream_images: images_str = ", ".join(a.tags[0] for p in embedded_pkgs for a in p.artifacts if isinstance(a, BuiltDockerImage)) if images_str: logger.debug(f"Built upstream Docker images: {images_str}") else: logger.debug("Did not build any upstream Docker images") packages_str = ", ".join(a.relpath for p in embedded_pkgs for a in p.artifacts if a.relpath) if packages_str: logger.debug(f"Built packages for Docker image: {packages_str}") else: logger.debug("Did not build any packages for Docker image") embedded_pkgs_digest = [ built_package.digest for built_package in embedded_pkgs ] all_digests = (dockerfile_info.digest, sources.snapshot.digest, *embedded_pkgs_digest) # Merge all digests to get the final docker build context digest. context_request = Get(Snapshot, MergeDigests(d for d in all_digests if d)) # Requests for build args and env build_args_request = Get(DockerBuildArgs, DockerBuildArgsRequest(docker_image)) build_env_request = Get(DockerBuildEnvironment, DockerBuildEnvironmentRequest(docker_image)) context, build_args, build_env = await MultiGet(context_request, build_args_request, build_env_request) if request.build_upstream_images: # Update build arg values for FROM image build args. # Get the FROM image build args with defined values in the Dockerfile. dockerfile_build_args = { arg_name: arg_value for arg_name, arg_value in dockerfile_info.build_args.to_dict().items() if arg_value and arg_name in dockerfile_info.from_image_build_arg_names } # Parse the build args values into Address instances. from_image_addresses = await Get( Addresses, UnparsedAddressInputs( dockerfile_build_args.values(), owning_address=dockerfile_info.address, ), ) # Map those addresses to the corresponding built image ref (tag). address_to_built_image_tag = { field_set.address: image.tags[0] for field_set, built in zip(embedded_pkgs_per_target.field_sets, embedded_pkgs) for image in built.artifacts if isinstance(image, BuiltDockerImage) } # Create the FROM image build args. from_image_build_args = [ f"{arg_name}={address_to_built_image_tag[addr]}" for arg_name, addr in zip(dockerfile_build_args.keys(), from_image_addresses) ] # Merge all build args. build_args = DockerBuildArgs.from_strings(*build_args, *from_image_build_args) return DockerBuildContext.create( build_args=build_args, snapshot=context, dockerfile_info=dockerfile_info, build_env=build_env, )