| # Build API Endpoint Tutorial Part 5 | |
| This README is a continuation of | |
| [Hello World Tutorial Part 4](4_hello_faux.md), | |
| creating a "Hello world!" endpoint from scratch. | |
| This tutorial assumes you have already executed all the steps in Parts 1-4, | |
| and have all the code in place. | |
| ## Hello World Part 5: Working in the SDK | |
| In this tutorial, we'll be looking at how to implement API endpoints that | |
| execute inside the SDK (a.k.a. the chroot), and the utilities the Build API | |
| provides to do so. | |
| ### When and where? | |
| If your endpoint depends on executing code inside the SDK, then you will need | |
| this functionality. | |
| Generally, we expect most endpoints to run inside the chroot, since the | |
| build itself should be taking place in the chroot. | |
| Existing endpoints in this category include things like building/installing | |
| packages (`SysrootService/InstallPackages`, a.k.a. `build_packages`), and | |
| running the ebuild tests (`TestService/BuildTargetUnitTest`, a.k.a. | |
| `cros_run_unit_tests`). | |
| If the functionality you're adding an endpoint for can be entirely executed | |
| from outside the chroot, then you won't need this functionality. | |
| These types of endpoints are most often setup or teardown operations, since | |
| the build itself should be inside the SDK. | |
| Existing endpoints in this category include scripts that actually execute | |
| outside of the chroot, such as making the chroot itself (`SdkService/Create`, | |
| a.k.a. `cros_sdk`), operations we run as soon as possible to reduce time spent | |
| on irrelevant builds (e.g. `PackagesService/Uprev`), and cleanup steps (e.g. | |
| `SdkService/Clean`). | |
| If you're ever unsure where yours should run, we're happy to help! | |
| ### Part 1: Getting Inside | |
| The first step is just getting inside. | |
| In [Part 1](1_hello_world.md) we talked about the service and method options | |
| for configuring the endpoints modules and functions that get executed when | |
| calling the endpoints. | |
| These options also contain fields for configuring whether the endpoint gets | |
| run inside or outside of the chroot. | |
| The `service_options` has the `service_chroot_assert` field, and the | |
| `method_options` the `method_chroot_assert` field. | |
| These fields are enums that direct the API how to execute the endpoint. | |
| The functionality requires no additional imports, the fields just need to be | |
| added and set. | |
| The service option allows setting the default for all endpoints in the service, | |
| but when both are set, the method option will take precedence over the service | |
| option. | |
| A value MUST be set if the endpoint does not execute properly both inside and | |
| outside of the SDK. | |
| If the endpoint is capable of executing in either environment, then the field | |
| does not need to be set. | |
| The other important note is that the API needs to know which chroot to enter. | |
| Every endpoint that enters or uses the chroot MUST include a Chroot message | |
| field in the request. | |
| See the updates below for the new import and an example of the new field in the | |
| request. | |
| However, when you're running the endpoints locally, it will mostly likely work | |
| without populating the chroot field. | |
| When not populated, the Build API will use the default chroot location. | |
| If you're a person for whom the default will not work locally, you probably | |
| already know because you had to manually set it up that way. | |
| If you care to check it out, the Chroot message definition is in | |
| `chromite/infra/proto/src/chromiumos/common.proto`. | |
| With all that in mind, let's update our endpoint to execute inside the SDK. | |
| `chromite/infra/proto/src/chromite/api/hello.proto`: | |
| ```protobuf | |
| // Proto config. | |
| syntax = "proto3"; | |
| package chromite.api; | |
| option go_package = "go.chromium.org/chromiumos/infra/proto/go/chromite/api"; | |
| import "chromite/api/build_api.proto"; | |
| // NEW! | |
| import "chromiumos/common.proto"; | |
| // HelloService/Hello request and response messages. | |
| message HelloRequest { | |
| string target = 1; | |
| // NEW! | |
| chromiumos.Chroot chroot = 2; | |
| } | |
| message HelloResponse { | |
| string hello_message = 1; | |
| } | |
| service HelloService { | |
| option (service_options) = { | |
| module: "hello", | |
| // NEW! | |
| service_chroot_assert: INSIDE, | |
| }; | |
| rpc Hello(HelloRequest) returns (HelloResponse); | |
| } | |
| ``` | |
| And now let's regenerate the protobuf bindings. | |
| ```shell script | |
| $> cd ~/chromiumos/chromite/infra/proto | |
| $> ./generate.sh | |
| $> cd ~/chromiumos/chromite/api | |
| $> ./compile_build_api_proto | |
| ``` | |
| That's it! | |
| Our endpoint will now always execute inside the chroot. | |
| ### Part 2: Injecting and Extracting Files and Folders | |
| Let's change our endpoint again. | |
| Instead of passing the target as a string, let's pass a file that contains | |
| multiple targets, one per line. | |
| In addition to printing out the messages, let's also write them out, one message | |
| per file, each named after their target. | |
| The Build API has specific messages that are used to support injecting and | |
| extracting files and folders from the SDK. | |
| We'll use some of these here to implement our new functionality. | |
| #### Proto Changes | |
| `chromite/infra/proto/src/chromite/api/hello.proto`: | |
| ```protobuf | |
| // Proto config. | |
| syntax = "proto3"; | |
| package chromite.api; | |
| option go_package = "go.chromium.org/chromiumos/infra/proto/go/chromite/api"; | |
| import "chromite/api/build_api.proto"; | |
| import "chromiumos/common.proto"; | |
| // HelloService/Hello request and response messages. | |
| message HelloRequest { | |
| // NEW! | |
| reserved "target"; | |
| // NEW! | |
| reserved 1; | |
| chromiumos.Chroot chroot = 2; | |
| // NEW! | |
| chromiumos.Path targets_file = 3; | |
| // NEW! | |
| chromiumos.ResultPath output_dir = 4; | |
| } | |
| message HelloResponse { | |
| // NEW! | |
| reserved "messages"; | |
| // NEW! | |
| reserved 1; | |
| // NEW! | |
| repeated chromiumos.Path message_files = 2; | |
| } | |
| service HelloService { | |
| option (service_options) = { | |
| module: "hello", | |
| service_chroot_assert: INSIDE, | |
| }; | |
| rpc Hello(HelloRequest) returns (HelloResponse); | |
| } | |
| ``` | |
| First thing to note is we deprecated the target and message fields from the | |
| request and response, respectively, by reserving the field name and number. | |
| In the request, we also added `chromiumos.Path` and `chromiumos.ResultPath` | |
| fields, and in the response, a repeated `chromiumos.Path` field. | |
| The `Path` message family are used by the Build API to automatically inject | |
| artifacts (i.e. files and folders) into and extract artifacts from the SDK. | |
| The functionality can handle both files and folders in both directions. | |
| There is also a bidirectional sync with the `SyncedDir`. | |
| For more details, see the [chroot reference](../references/chroot.md). | |
| We will be using the `targets_file` field in the request to inject a file | |
| containing targets, replacing the target field we previously had. | |
| The endpoint will then print the messages as it did before, but also write | |
| each message to a separate file, and return those paths in the `message_files` | |
| field, each named for their target. | |
| Those files will all be collected in the path we specify in the `output_dir` | |
| field. | |
| #### Code Changes | |
| With our proto updated, let's change our lib to write out the message to a | |
| file and return the file path rather than the message itself. | |
| `chromite/lib/hello_lib.py` | |
| ```python | |
| import os | |
| from chromite.lib import osutils | |
| def hello(target, output_dir): | |
| msg = f'Hello, {target}!' | |
| print(msg) | |
| file_path = os.path.join(output_dir, target) | |
| osutils.WriteFile(file_path, msg) | |
| return file_path | |
| ``` | |
| Finally, let's update the controller to use the new fields in the request, | |
| and fix the hello_lib call now that it has a new signature. | |
| ```python | |
| from chromite.api import faux | |
| from chromite.api import validate | |
| from chromite.api.gen.chromiumos import common_pb2 | |
| from chromite.lib import hello_lib | |
| from chromite.lib import osutils | |
| def _hello_success(_input_proto, output_proto, _config_proto): | |
| file_proto = output_proto.message_files.add() | |
| file_proto.path = '/tmp/target' | |
| file_proto.location = common_pb2.Path.OUTSIDE | |
| @faux.success(_hello_success) | |
| @faux.empty_error | |
| @validate.exists('targets_file.path') | |
| @validate.validation_complete | |
| def Hello(input_proto, output_proto, _config_proto): | |
| # Read targets, one per line. | |
| targets = osutils.ReadFile(input_proto.targets_file.path).splitlines() | |
| # Don't delete since we need it to exist afterwords. | |
| with osutils.TempDir(delete=False) as tmp_dir: | |
| for target in targets: | |
| # Print the greeting and get the file it created. | |
| target_file = hello_lib.hello(target=target, output_dir=tmp_dir) | |
| # Add it to the response. | |
| file_proto = output_proto.message_files.add() | |
| file_proto.path = target_file | |
| file_proto.location = common_pb2.Path.INSIDE | |
| ``` | |
| First, we changed the `@validate` decorator to `exists`, which verifies a file | |
| exists. | |
| We then read the targets files, and call the hello_lib for each target, adding | |
| the results to the response as we go. | |
| Finally, the faux success function needs to reflect our new response. | |
| ### Part 3: Running it | |
| First, we'll regenerate the compiled proto to pick up our changes. | |
| ```shell script | |
| $> cd ~/chromiumos/chromite/infra/proto | |
| $> ./generate.sh | |
| $> cd ~/chromiumos/chromite/api | |
| $> ./compile_build_api_proto | |
| ``` | |
| Next we need to create the targets file for the request, and our output | |
| directory. | |
| ```shell script | |
| $> printf "world\nmoon\neverybody" > /tmp/targets-file | |
| $> mkdir /tmp/hello-results | |
| ``` | |
| Now we can set up our new input file, pointing to those locations. | |
| The 2 used for the location fields in our input file below is just the enum | |
| value for `Path.Location.OUTSIDE`. | |
| `~/chromiumos/chromite/api/contrib/call_scripts/hello__hello_input.json` | |
| ```json | |
| { | |
| "targets_file": {"path": "/tmp/targets-file", "location": 2}, | |
| "output_dir": {"path": {"path": "/tmp/hello-results", "location": 2}} | |
| } | |
| ``` | |
| We're all set, now we just run it. | |
| ```shell script | |
| $> cd ~/chromiumos/chromite/api/contrib/call_scripts/ | |
| $> ./hello__hello | |
| Running chromite.api.HelloService/Hello | |
| 15:17:13: DEBUG: Services registered successfully. | |
| 15:17:13: INFO: Re-executing the endpoint inside the chroot. | |
| 15:17:13: DEBUG: Copying /tmp/targets-file to /usr/local/google/home/saklein/chromiumos/chroot/tmp/tmptdazkiqh/targets-file | |
| 15:17:13: INFO: Writing input message to: /usr/local/google/home/saklein/chromiumos/chroot/tmp/tmpx9cumt3j/input_proto | |
| 15:17:13: INFO: Writing config message to: /usr/local/google/home/saklein/chromiumos/chroot/tmp/tmpx9cumt3j/config_proto | |
| 15:17:13: INFO: run: cros_sdk --chroot /usr/local/google/home/saklein/chromiumos/chroot -- build_api chromite.api.HelloService/Hello --input-json /tmp/tmpx9cumt3j/input_proto --output-binary /tmp/tmpx9cumt3j/output_proto --config-json /tmp/tmpx9cumt3j/config_proto --debug | |
| 15:17:15: DEBUG: Services registered successfully. | |
| 15:17:15: DEBUG: Validating targets_file.path exists. | |
| Hello, world! | |
| Hello, moon! | |
| Hello, everybody! | |
| 15:17:15: INFO: Endpoint execution completed, return code: 0 | |
| 15:17:15: DEBUG: Copying /usr/local/google/home/saklein/chromiumos/chroot/tmp/tmpxjuwd_lj/world to /tmp/hello-results/world | |
| 15:17:15: DEBUG: Copying /usr/local/google/home/saklein/chromiumos/chroot/tmp/tmpxjuwd_lj/moon to /tmp/hello-results/moon | |
| 15:17:15: DEBUG: Copying /usr/local/google/home/saklein/chromiumos/chroot/tmp/tmpxjuwd_lj/everybody to /tmp/hello-results/everybody | |
| Completed chromite.api.HelloService/Hello | |
| Success! | |
| Return Code: 0 | |
| Result: | |
| { | |
| "messageFiles": [ | |
| { | |
| "location": 2, | |
| "path": "/tmp/hello-results/world" | |
| }, | |
| { | |
| "location": 2, | |
| "path": "/tmp/hello-results/moon" | |
| }, | |
| { | |
| "location": 2, | |
| "path": "/tmp/hello-results/everybody" | |
| } | |
| ] | |
| } | |
| ``` | |
| We can also check our output files to make sure they also have our messages. | |
| ```shell script | |
| $> tail /tmp/hello-results/world /tmp/hello-results/moon /tmp/hello-results/everybody | |
| ==> /tmp/hello-results/world <== | |
| Hello, world! | |
| ==> /tmp/hello-results/moon <== | |
| Hello, moon! | |
| ==> /tmp/hello-results/everybody <== | |
| Hello, everybody! | |
| ``` | |
| Success! |