Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate a GAPIC library from api definition #3208

Merged
merged 23 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/scripts/hermetic_library_generation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,29 @@ git show "${target_branch}":"${generation_config}" > "${baseline_generation_conf
# get .m2 folder so it's mapped into the docker container
m2_folder=$(dirname "$(mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout)")

# download api definition from googleapis repository
googleapis_commitish=$(grep googleapis_commitish "${generation_config}" | cut -d ":" -f 2 | xargs)
api_def_dir=$(mktemp -d)
git clone https://github.com/googleapis/googleapis.git "${api_def_dir}"
pushd "${api_def_dir}"
git checkout "${googleapis_commitish}"
popd

# run hermetic code generation docker image.
docker run \
--rm \
-u "$(id -u):$(id -g)" \
-v "$(pwd):${workspace_name}" \
-v "${m2_folder}":/home/.m2 \
-v "${api_def_dir}:${workspace_name}/api" \
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is essentially googleapis folder, maybe we can just use /googleapis to make it easier to understand?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to use a generalized name here to specify that the script can generate from any protos, not necessarily from googleapis.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docker image can be used by any protos, but I think hermetic_library_generation.sh is only going to be used for generating googleapis. For generating other protos, the volumn name actually doesn't really matter, it's good as long as the volumn and api-definition-path match.
That being said, it's fine if you want to make it generic, but maybe change it to apis to imply that it could contain multiple.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I think hermetic_library_generation.sh is only going to be used for generating googleapis

Makes sense, I changed it to googleapis.

-e GENERATOR_VERSION="${image_tag}" \
gcr.io/cloud-devrel-public-resources/java-library-generation:"${image_tag}" \
--baseline-generation-config-path="${workspace_name}/${baseline_generation_config}" \
--current-generation-config-path="${workspace_name}/${generation_config}"
--current-generation-config-path="${workspace_name}/${generation_config}" \
--api-definition-path="${workspace_name}/api"

# remove api definition after generation
rm -rf "${api_def_dir}"

# commit the change to the pull request.
rm -rdf output googleapis "${baseline_generation_config}"
Expand Down
36 changes: 27 additions & 9 deletions library_generation/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,14 @@ shell session.

## Running the script
The entrypoint script (`library_generation/cli/entry_point.py`) allows you to
update the target repository with the latest changes starting from the
googleapis committish declared in `generation_config.yaml`.
generate a GAPIC repository with a given api definition (proto, service yaml).

### Download the api definition
For example, googleapis
```
git clone https://github.com/googleapis/googleapis
export api_definition_path="$(pwd)/googleapis"
```

### Download the repo
For example, google-cloud-java
Expand All @@ -118,7 +124,9 @@ python -m pip install .

### Run the script
```
python cli/entry_point.py generate --repository-path="${path_to_repo}"
python cli/entry_point.py generate \
--repository-path="${path_to_repo}" \
--api-definition-path="${api_definition_path}"
```


Expand All @@ -144,16 +152,21 @@ repo to this folder).

To run the docker container on the google-cloud-java repo, you must run:
```bash
docker run -u "$(id -u)":"$(id -g)" -v/path/to/google-cloud-java:/workspace $(cat image-id)
docker run \
-u "$(id -u)":"$(id -g)" \
-v /path/to/google-cloud-java:/workspace \
$(cat image-id)
```

* `-u "$(id -u)":"$(id -g)"` makes docker run the container impersonating
yourself. This avoids folder ownership changes since it runs as root by
default.
* `-v/path/to/google-cloud-java:/workspace` maps the host machine's
google-cloud-java folder to the /workspace folder. The image is configured to
perform changes in this directory
* `$(cat image-id)` obtains the image ID created in the build step
* `-v /path/to/google-cloud-java:/workspace` maps the host machine's
google-cloud-java folder to the /workspace folder.
The image is configured to perform changes in this directory.
Note that this setup assumes the api definition resides in host machine's
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change this example to use the api_definition_path parameter?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

google-cloud-java folder.
* `$(cat image-id)` obtains the image ID created in the build step.

## Debug the created containers
If you are working on changing the way the containers are created, you may want
Expand All @@ -173,5 +186,10 @@ We add `less` and `vim` as text tools for further inspection.
You can also run a shell in a new container by running:

```bash
docker run --rm -it -u=$(id -u):$(id -g) -v/path/to/google-cloud-java:/workspace --entrypoint="bash" $(cat image-id)
docker run \
--rm -it \
-u $(id -u):$(id -g) \
-v /path/to/google-cloud-java:/workspace \
--entrypoint="bash" \
$(cat image-id)
```
43 changes: 31 additions & 12 deletions library_generation/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generate a repository containing GAPIC Client Libraries

The script, `entry_point.py`, allows you to generate a repository containing
GAPIC client libraries with googleapis commit history (a monorepo, for example,
GAPIC client libraries with change history (a monorepo, for example,
google-cloud-java) from a configuration file.

## Environment
Expand Down Expand Up @@ -48,6 +48,20 @@ right version for each library.
Please refer [here](go/java-client-releasing#versionstxt-manifest) for more info
of versions.txt.

### Api definition path (`api_definition_path`), optional

The path to where the api definition (proto, service yaml) resides.

The default value is the current working directory when running the script.

Note that you need not only the protos defined the service, but also the transitive
blakeli0 marked this conversation as resolved.
Show resolved Hide resolved
dependencies of those protos.
For example, if your service is defined in `example_service.proto` and it imports
`google/api/annotations.proto`, you need the `annotations.proto` resides in a
folder that has the exact structure of the import statement (`google/api` in this
case), and set `api_definition_path` to the path contains the root folder (`google`
in this case).

## Output of `entry_point.py`

### GAPIC libraries
Expand All @@ -74,11 +88,13 @@ will be created/modified:
| pom.xml (repo root dir) | Always generated from inputs |
| versions.txt | New entries will be added if they don’t exist |

### googleapis commit history
### Change history

If both `baseline_generation_config` and `current_generation_config` are
specified, and they contain different googleapis commit, the commit history will
be generated into `pr_description.txt` in the `repository_path`.
specified and the contents are different, the changed contents will be generated
into `pr_description.txt` in the `repository_path`.
In addition, if the `googleapis_commitish` is different, the googleapis commit
history will be generated.

## Configuration to generate a repository

Expand All @@ -96,7 +112,7 @@ They are shared by library level parameters.
| gapic_generator_version | No | set through env variable if not specified |
| protoc_version | No | inferred from the generator if not specified |
| grpc_version | No | inferred from the generator if not specified |
| googleapis-commitish | Yes | |
| googleapis_commitish | Yes | |
| libraries_bom_version | No | empty string if not specified |

### Library level parameters
Expand Down Expand Up @@ -183,22 +199,25 @@ The virtual environment can be installed to any folder, usually it is recommende
2. Assuming the virtual environment is installed under `sdk-platform-java`.
Run the following command under the root folder of `sdk-platform-java` to install the dependencies of `library_generation`

```bash
python -m pip install -r library_generation/requirements.txt
```
```bash
python -m pip install -r library_generation/requirements.txt
```

3. Run the following command to install `library_generation` as a module, which allows the `library_generation` module to be imported from anywhere
```bash
python -m pip install library_generation/
```
```bash
python -m pip install library_generation/
```

4. Download api definition to a local directory

## An example to generate a repository using `entry_point.py`

```bash
python library_generation/entry_point.py generate \
--baseline-generation-config-path=/path/to/baseline_config_file \
--current-generation-config-path=/path/to/current_config_file \
--repository-path=path/to/repository
--repository-path=path/to/repository \
--api-definition-path=path/to/api_definition
```
If you run `entry_point.py` with the example [configuration](#an-example-of-generation-configuration)
shown above, the repository structure is:
Expand Down
20 changes: 19 additions & 1 deletion library_generation/cli/entry_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,21 @@ def main(ctx):
directory.
""",
)
@click.option(
"--api-definition-path",
type=str,
default=".",
show_default=True,
help="""
The path to which the api definition (proto and service yaml) resides.
If not specified, the path is the current working directory.
""",
)
def generate(
baseline_generation_config_path: str,
current_generation_config_path: str,
repository_path: str,
api_definition_path: str,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more like a root folder of all api definitions. e.g. The proto_path configure in generation_config.yaml is a relative path to this path right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change this to api_definitions_path or api_defs_path?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to api_definitions_path.

):
"""
Compare baseline generation config and current generation config and
Expand All @@ -90,14 +101,18 @@ def generate(
repository_path/pr_description.txt.
"""
__generate_repo_and_pr_description_impl(
baseline_generation_config_path, current_generation_config_path, repository_path
baseline_generation_config_path=baseline_generation_config_path,
current_generation_config_path=current_generation_config_path,
repository_path=repository_path,
api_definition_path=api_definition_path,
)


def __generate_repo_and_pr_description_impl(
baseline_generation_config_path: str,
current_generation_config_path: str,
repository_path: str,
api_definition_path: str,
):
"""
Implementation method for generate().
Expand Down Expand Up @@ -129,13 +144,15 @@ def __generate_repo_and_pr_description_impl(

current_generation_config_path = os.path.abspath(current_generation_config_path)
repository_path = os.path.abspath(repository_path)
api_definition_path = os.path.abspath(api_definition_path)
if not baseline_generation_config_path:
# Execute full generation based on current_generation_config if
# baseline_generation_config is not specified.
# Do not generate pull request description.
generate_from_yaml(
config=from_yaml(current_generation_config_path),
repository_path=repository_path,
api_definition_path=api_definition_path,
)
return

Expand All @@ -155,6 +172,7 @@ def __generate_repo_and_pr_description_impl(
generate_from_yaml(
config=config_change.current_config,
repository_path=repository_path,
api_definition_path=api_definition_path,
target_library_names=target_library_names,
)
generate_pr_descriptions(
Expand Down
6 changes: 1 addition & 5 deletions library_generation/generate_composed_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ def generate_composed_library(
:return None
"""
output_folder = repo_config.output_folder
util.pull_api_definition(
config=config, library=library, output_folder=output_folder
)

base_arguments = __construct_tooling_arg(config=config)
owlbot_cli_source_folder = util.sh_util("mktemp -d")
os.makedirs(f"{library_path}", exist_ok=True)
Expand All @@ -73,7 +69,7 @@ def generate_composed_library(
# generate postprocessing prerequisite files (.repo-metadata.json, .OwlBot-hermetic.yaml,
# owlbot.py) here because transport is parsed from BUILD.bazel,
# which lives in a versioned proto_path. The value of transport will be
# overriden by the config object if specified. Note that this override
# overridden by the config object if specified. Note that this override
# does not affect library generation but instead used only for
# generating postprocessing files such as README.
util.generate_postprocessing_prerequisite_files(
Expand Down
7 changes: 7 additions & 0 deletions library_generation/generate_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import shutil

import library_generation.utils.utilities as util
from library_generation.generate_composed_library import generate_composed_library
from library_generation.model.generation_config import GenerationConfig
Expand All @@ -22,6 +25,7 @@
def generate_from_yaml(
config: GenerationConfig,
repository_path: str,
api_definition_path: str,
target_library_names: list[str] = None,
) -> None:
"""
Expand All @@ -31,6 +35,7 @@ def generate_from_yaml(
:param config: a GenerationConfig object.
:param repository_path: The repository path to which the generated files
will be sent.
:param api_definition_path: The path to where the api definition resides.
:param target_library_names: a list of libraries to be generated.
If specified, only the library whose library_name is in target_library_names
will be generated.
Expand All @@ -43,6 +48,8 @@ def generate_from_yaml(
repo_config = util.prepare_repo(
gen_config=config, library_config=target_libraries, repo_path=repository_path
)
# copy api definition to output folder.
shutil.copytree(api_definition_path, repo_config.output_folder, dirs_exist_ok=True)

for library_path, library in repo_config.get_libraries().items():
print(f"generating library {library.get_library_name()}")
Expand Down
24 changes: 20 additions & 4 deletions library_generation/test/cli/entry_point_unit_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,13 @@ def test_generate_non_monorepo_without_changes_triggers_full_generation(
baseline_generation_config_path=config_path,
current_generation_config_path=config_path,
repository_path=".",
api_definition_path=".",
)
generate_from_yaml.assert_called_with(
config=ANY, repository_path=ANY, target_library_names=None
config=ANY,
repository_path=ANY,
api_definition_path=ANY,
target_library_names=None,
)

@patch("library_generation.cli.entry_point.generate_from_yaml")
Expand All @@ -134,9 +138,13 @@ def test_generate_non_monorepo_with_changes_triggers_full_generation(
baseline_generation_config_path=baseline_config_path,
current_generation_config_path=current_config_path,
repository_path=".",
api_definition_path=".",
)
generate_from_yaml.assert_called_with(
config=ANY, repository_path=ANY, target_library_names=None
config=ANY,
repository_path=ANY,
api_definition_path=ANY,
target_library_names=None,
)

@patch("library_generation.cli.entry_point.generate_from_yaml")
Expand All @@ -160,9 +168,13 @@ def test_generate_monorepo_with_common_protos_triggers_full_generation(
baseline_generation_config_path=config_path,
current_generation_config_path=config_path,
repository_path=".",
api_definition_path=".",
)
generate_from_yaml.assert_called_with(
config=ANY, repository_path=ANY, target_library_names=None
config=ANY,
repository_path=ANY,
api_definition_path=ANY,
target_library_names=None,
)

@patch("library_generation.cli.entry_point.generate_from_yaml")
Expand All @@ -187,7 +199,11 @@ def test_generate_monorepo_without_common_protos_does_not_trigger_full_generatio
baseline_generation_config_path=config_path,
current_generation_config_path=config_path,
repository_path=".",
api_definition_path=".",
)
generate_from_yaml.assert_called_with(
config=ANY, repository_path=ANY, target_library_names=[]
config=ANY,
repository_path=ANY,
api_definition_path=ANY,
target_library_names=[],
)
Loading
Loading