Skip to content

libcnb

Cloud Native Buildpack API bindings for Python

Usage

You can install libcnb from PyPi via -

pip install libcnb

Buildpack Interface

According to the CNB specification, the buildpack interface is composed of both a detect and build phase. Each of these phases has a corresponding set of libcnb primitives enable developers to easily implement a buildpack.

For more details see the Cloud Native Buildpack Spec

Detect Phase

The purpose of the detect phase is for buildpacks to declare dependencies that are provided or required for the buildpack to execute. Implementing the detect phase can be achieved by calling the detect function and providing a detector callback to be invoked during that phase. Below is an example of a simple detect phase that provides the "pip" dependency and requires the "python" dependency.

 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
import libcnb


def detector(context: libcnb.DetectContext) -> libcnb.DetectResult:
    # The DetectContext includes an application_dir field that 
    # specifies the location of the application source code. 
    # This field can be combined with other paths to find and
    # inspect files included in the application source
    # code that is provided to the buildpack.
    has_requirements_file = (
        context.application_dir / "requirements.txt"
    ).exists()

    # Once the existence of a requirements.txt file has been confirmed,
    # the detect phase can return a result that indicates the provision
    # of pip and the requirement of python. As can be seen below,
    # the BuildPlanRequire may also include optional metadata information
    # such as the the version information for a given requirement.

    # By default the result is initialized to 
    # `passed` = False and has empty build plans
    result = libcnb.DetectResult()
    if has_requirements_file:
        result.passed = True
        # The detect phase provides pip and requires python.
        # When requiring python, it requests a version greater
        # than 3 as being acceptable by this buildpack.
        result.plans.append(
            libcnb.BuildPlan(
                provides=[
                    libcnb.BuildPlanProvide(name="pip"),
                ],
                requires=[
                    libcnb.BuildPlanRequire(
                        name="python",
                        metadata={"version": ">=3"},
                    ),
                ],
            )
        )
    return result


if __name__ == "__main__":
    # Calling libcnb.detect with the correct detector
    libcnb.detect(detector=detector)

Build Phase

The purpose of the build phase is to perform the operation of providing whatever dependencies were declared in the detect phase for the given application code. Implementing the build phase can be achieved by calling the build function and providing a builder callback to be invoked during that phase. Below is an example that adds "pip" as a dependency to the application source code.

 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
from pathlib import Path

import libcnb


def builder(context: libcnb.BuildContext) -> libcnb.BuildResult:
    # The BuildContext includes a BuildpackPlan with entries
    # that specify a requirement on a dependency provided by
    # the buildpack. This example simply chooses the first entry,
    # but more intelligent resolution processes can and likely
    # should be used in real implementations.
    entry = context.plan.entries[0]
    pip_version = entry.metadata["version"]

    # The BuildContext also provides a mechanism whereby
    # a layer can be created to store the results of a given
    # portion of the build process. This example creates a
    # layer called "pip" that will hold the pip cli.
    # We can also pass `load_all` to `get` set to `True`
    # to load all the existing layer metadata including env vars
    # and profile.d scripts if they exist.
    # Otherwise the layer is loaded only with the layer.metadata
    # hydrated from the <layer>.toml file.

    layer = context.layers.get("pip", load_all=False)

    # libcnb.Layer also provides some handy utilities to check
    # the expected metadata with the actual metadata and reset
    # the existing content from cache if it isn't what is expected.
    expected_metadata = {"pip_version": pip_version}
    if not layer.compare_metadata(expected_metadata, exact=False):
        # Clear all the existing content of the layer if the
        # version is not what we expect.
        layer.reset()
        layer.metadata = expected_metadata
        # We can also set env. variables
        layer.shared_env.default(
            "PIP_INDEX_URL", "http://mypypi.org/simple"
        )
        # Or add profile.d scripts
        layer.profile.add("pip.sh", "export PIP_USER=1")
        _install_pip(layer.path, pip_version)
    # We can also set the layer types and metadata easily
    layer.launch = True
    layer.build = True
    layer.cache = True
    # After the installation of the pip cli, a BuildResult
    # can be returned that included details of the executed
    # BuildpackPlan, the Layers to provide back to the lifecycle
    # and the Processes to execute at launch.

    # NOTE - Only the layers provided in the BuildResult will be exported.
    result = libcnb.BuildResult(
        layers=[layer],
        launch_metadata=libcnb.LaunchMetadata(
            labels=[libcnb.Label(key="pip-installed", value="true")],
        ),
    )
    result.launch_metadata.processes.append(
        libcnb.Process(
            type_="pip-installer",
            command="pip",
            args=["install"],
            direct=False,
        )
    )
    # We can also add a bom (Bill of Materials) to the output image.
    result.launch_metadata.bom.append(
        libcnb.BOMEntry(
            name="pip",
            metadata={"version": pip_version},
        )
    )
    result.build_metadata.bom.append(
        libcnb.BOMEntry(
            name="pip",
            metadata={"version": pip_version},
        )

    )
    return result


# Executes the process of installing the pip cli.
def _install_pip(layer_path: Path, version: str) -> None:
    # Implemention omitted.
    pass


if __name__ == "__main__":
    libcnb.build(builder)

Run

Buildpacks can be created with a single entrypoint executable using the run function. Here, you can combine both the Detect and Build phases and run will ensure that the correct phase is called when the matching executable is called by the Cloud Native Buildpack Lifecycle. Below is an example that combines a simple detect and build into a single python script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import libcnb

def detector(context: libcnb.DetectContext) -> libcnb.DetectResult:
    return libcnb.DetectResult(passed=True)

def builder(context: libcnb.BuildContext) -> libcnb.BuildResult:
    return libcnb.BuildResult()

def main():
    libcnb.run(detector=detector, builder=detector)

if __name__ == "__main__":
    main()

Summary

These examples show the very basics of what a buildpack implementation using libcnb might entail. For more details, please consult the API Reference for the types and functions declared within the libcnb package.