Skip to content

Commit b6b5d7b

Browse files
authored
feat: generate a GAPIC library from api definition (#3208)
In this PR: - Generate a GAPIC library from api definition (proto, service yaml), rather than from googleapis repository. To generate a GAPIC library with hermetic build image, the user has to prepare the api definition to a directory and set `--api-definition-path` to this directory. This feature allows the user to generate a GAPIC library from any protos, rather than checking in protos in googleapis repository. Internal ticket: b/362705386
1 parent ad0e00b commit b6b5d7b

File tree

9 files changed

+155
-73
lines changed

9 files changed

+155
-73
lines changed

.github/scripts/hermetic_library_generation.sh

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,29 @@ git show "${target_branch}":"${generation_config}" > "${baseline_generation_conf
8484
# get .m2 folder so it's mapped into the docker container
8585
m2_folder=$(dirname "$(mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout)")
8686

87+
# download api definitions from googleapis repository
88+
googleapis_commitish=$(grep googleapis_commitish "${generation_config}" | cut -d ":" -f 2 | xargs)
89+
api_def_dir=$(mktemp -d)
90+
git clone https://github.com/googleapis/googleapis.git "${api_def_dir}"
91+
pushd "${api_def_dir}"
92+
git checkout "${googleapis_commitish}"
93+
popd
94+
8795
# run hermetic code generation docker image.
8896
docker run \
8997
--rm \
9098
-u "$(id -u):$(id -g)" \
9199
-v "$(pwd):${workspace_name}" \
92100
-v "${m2_folder}":/home/.m2 \
101+
-v "${api_def_dir}:${workspace_name}/googleapis" \
93102
-e GENERATOR_VERSION="${image_tag}" \
94103
gcr.io/cloud-devrel-public-resources/java-library-generation:"${image_tag}" \
95104
--baseline-generation-config-path="${workspace_name}/${baseline_generation_config}" \
96-
--current-generation-config-path="${workspace_name}/${generation_config}"
105+
--current-generation-config-path="${workspace_name}/${generation_config}" \
106+
--api-definitions-path="${workspace_name}/googleapis"
107+
108+
# remove api definitions after generation
109+
rm -rf "${api_def_dir}"
97110

98111
# commit the change to the pull request.
99112
rm -rdf output googleapis "${baseline_generation_config}"

library_generation/DEVELOPMENT.md

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,14 @@ shell session.
101101

102102
## Running the script
103103
The entrypoint script (`library_generation/cli/entry_point.py`) allows you to
104-
update the target repository with the latest changes starting from the
105-
googleapis committish declared in `generation_config.yaml`.
104+
generate a GAPIC repository with a given api definition (proto, service yaml).
105+
106+
### Download the api definition
107+
For example, googleapis
108+
```
109+
git clone https://github.com/googleapis/googleapis
110+
export api_definitions_path="$(pwd)/googleapis"
111+
```
106112

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

119125
### Run the script
120126
```
121-
python cli/entry_point.py generate --repository-path="${path_to_repo}"
127+
python cli/entry_point.py generate \
128+
--repository-path="${path_to_repo}" \
129+
--api-definitions-path="${api_definitions_path}"
122130
```
123131

124132

@@ -144,16 +152,25 @@ repo to this folder).
144152

145153
To run the docker container on the google-cloud-java repo, you must run:
146154
```bash
147-
docker run -u "$(id -u)":"$(id -g)" -v/path/to/google-cloud-java:/workspace $(cat image-id)
155+
docker run \
156+
-u "$(id -u)":"$(id -g)" \
157+
-v /path/to/google-cloud-java:/workspace \
158+
-v /path/to/api-definition:/workspace/apis \
159+
$(cat image-id) \
160+
--api-definitions-path=/workspace/apis
148161
```
149162

150163
* `-u "$(id -u)":"$(id -g)"` makes docker run the container impersonating
151164
yourself. This avoids folder ownership changes since it runs as root by
152165
default.
153-
* `-v/path/to/google-cloud-java:/workspace` maps the host machine's
154-
google-cloud-java folder to the /workspace folder. The image is configured to
155-
perform changes in this directory
156-
* `$(cat image-id)` obtains the image ID created in the build step
166+
* `-v /path/to/google-cloud-java:/workspace` maps the host machine's
167+
google-cloud-java folder to the /workspace folder.
168+
The image is configured to perform changes in this directory.
169+
* `-v /path/to/api-definition:/workspace/apis` maps the host machine's
170+
api-definition folder to /workspace/apis folder.
171+
* `$(cat image-id)` obtains the image ID created in the build step.
172+
* `--api-definitions-path=/workspace/apis` set the API definition path to
173+
`/workspace/apis`.
157174

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

175192
```bash
176-
docker run --rm -it -u=$(id -u):$(id -g) -v/path/to/google-cloud-java:/workspace --entrypoint="bash" $(cat image-id)
193+
docker run \
194+
--rm -it \
195+
-u $(id -u):$(id -g) \
196+
-v /path/to/google-cloud-java:/workspace \
197+
--entrypoint="bash" \
198+
$(cat image-id)
177199
```

library_generation/README.md

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Generate a repository containing GAPIC Client Libraries
22

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

77
## Environment
@@ -48,6 +48,22 @@ right version for each library.
4848
Please refer [here](go/java-client-releasing#versionstxt-manifest) for more info
4949
of versions.txt.
5050

51+
### Api definitions path (`api_definitions_path`), optional
52+
53+
The path to where the api definition (proto, service yaml) resides.
54+
55+
The default value is the current working directory when running the script.
56+
57+
Note that you need not only the protos defined the service, but also the transitive
58+
dependencies of those protos.
59+
Any missing dependencies will cause `File not found` error.
60+
61+
For example, if your service is defined in `example_service.proto` and it imports
62+
`google/api/annotations.proto`, you need the `annotations.proto` resides in a
63+
folder that has the exact structure of the import statement (`google/api` in this
64+
case), and set `api_definitions_path` to the path contains the root folder (`google`
65+
in this case).
66+
5167
## Output of `entry_point.py`
5268

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

77-
### googleapis commit history
93+
### Change history
7894

7995
If both `baseline_generation_config` and `current_generation_config` are
80-
specified, and they contain different googleapis commit, the commit history will
81-
be generated into `pr_description.txt` in the `repository_path`.
96+
specified and the contents are different, the changed contents will be generated
97+
into `pr_description.txt` in the `repository_path`.
98+
In addition, if the `googleapis_commitish` is different, the googleapis commit
99+
history will be generated.
82100

83101
## Configuration to generate a repository
84102

@@ -96,7 +114,7 @@ They are shared by library level parameters.
96114
| gapic_generator_version | No | set through env variable if not specified |
97115
| protoc_version | No | inferred from the generator if not specified |
98116
| grpc_version | No | inferred from the generator if not specified |
99-
| googleapis-commitish | Yes | |
117+
| googleapis_commitish | Yes | |
100118
| libraries_bom_version | No | empty string if not specified |
101119

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

186-
```bash
187-
python -m pip install -r library_generation/requirements.txt
188-
```
204+
```bash
205+
python -m pip install -r library_generation/requirements.txt
206+
```
189207

190208
3. Run the following command to install `library_generation` as a module, which allows the `library_generation` module to be imported from anywhere
191-
```bash
192-
python -m pip install library_generation/
193-
```
209+
```bash
210+
python -m pip install library_generation/
211+
```
212+
213+
4. Download api definition to a local directory
194214

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

197217
```bash
198218
python library_generation/entry_point.py generate \
199219
--baseline-generation-config-path=/path/to/baseline_config_file \
200220
--current-generation-config-path=/path/to/current_config_file \
201-
--repository-path=path/to/repository
221+
--repository-path=path/to/repository \
222+
--api-definitions-path=path/to/api_definition
202223
```
203224
If you run `entry_point.py` with the example [configuration](#an-example-of-generation-configuration)
204225
shown above, the repository structure is:

library_generation/cli/entry_point.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,22 @@ def main(ctx):
6363
directory.
6464
""",
6565
)
66+
@click.option(
67+
"--api-definitions-path",
68+
type=str,
69+
default=".",
70+
show_default=True,
71+
help="""
72+
The path to which the api definition (proto and service yaml) and its
73+
dependencies resides.
74+
If not specified, the path is the current working directory.
75+
""",
76+
)
6677
def generate(
6778
baseline_generation_config_path: str,
6879
current_generation_config_path: str,
6980
repository_path: str,
81+
api_definitions_path: str,
7082
):
7183
"""
7284
Compare baseline generation config and current generation config and
@@ -90,14 +102,18 @@ def generate(
90102
repository_path/pr_description.txt.
91103
"""
92104
__generate_repo_and_pr_description_impl(
93-
baseline_generation_config_path, current_generation_config_path, repository_path
105+
baseline_generation_config_path=baseline_generation_config_path,
106+
current_generation_config_path=current_generation_config_path,
107+
repository_path=repository_path,
108+
api_definitions_path=api_definitions_path,
94109
)
95110

96111

97112
def __generate_repo_and_pr_description_impl(
98113
baseline_generation_config_path: str,
99114
current_generation_config_path: str,
100115
repository_path: str,
116+
api_definitions_path: str,
101117
):
102118
"""
103119
Implementation method for generate().
@@ -129,13 +145,15 @@ def __generate_repo_and_pr_description_impl(
129145

130146
current_generation_config_path = os.path.abspath(current_generation_config_path)
131147
repository_path = os.path.abspath(repository_path)
148+
api_definitions_path = os.path.abspath(api_definitions_path)
132149
if not baseline_generation_config_path:
133150
# Execute full generation based on current_generation_config if
134151
# baseline_generation_config is not specified.
135152
# Do not generate pull request description.
136153
generate_from_yaml(
137154
config=from_yaml(current_generation_config_path),
138155
repository_path=repository_path,
156+
api_definitions_path=api_definitions_path,
139157
)
140158
return
141159

@@ -155,6 +173,7 @@ def __generate_repo_and_pr_description_impl(
155173
generate_from_yaml(
156174
config=config_change.current_config,
157175
repository_path=repository_path,
176+
api_definitions_path=api_definitions_path,
158177
target_library_names=target_library_names,
159178
)
160179
generate_pr_descriptions(

library_generation/generate_composed_library.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@ def generate_composed_library(
5959
:return None
6060
"""
6161
output_folder = repo_config.output_folder
62-
util.pull_api_definition(
63-
config=config, library=library, output_folder=output_folder
64-
)
65-
6662
base_arguments = __construct_tooling_arg(config=config)
6763
owlbot_cli_source_folder = util.sh_util("mktemp -d")
6864
os.makedirs(f"{library_path}", exist_ok=True)
@@ -73,7 +69,7 @@ def generate_composed_library(
7369
# generate postprocessing prerequisite files (.repo-metadata.json, .OwlBot-hermetic.yaml,
7470
# owlbot.py) here because transport is parsed from BUILD.bazel,
7571
# which lives in a versioned proto_path. The value of transport will be
76-
# overriden by the config object if specified. Note that this override
72+
# overridden by the config object if specified. Note that this override
7773
# does not affect library generation but instead used only for
7874
# generating postprocessing files such as README.
7975
util.generate_postprocessing_prerequisite_files(

library_generation/generate_repo.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15+
import os
16+
import shutil
17+
1518
import library_generation.utils.utilities as util
1619
from library_generation.generate_composed_library import generate_composed_library
1720
from library_generation.model.generation_config import GenerationConfig
@@ -22,6 +25,7 @@
2225
def generate_from_yaml(
2326
config: GenerationConfig,
2427
repository_path: str,
28+
api_definitions_path: str,
2529
target_library_names: list[str] = None,
2630
) -> None:
2731
"""
@@ -31,6 +35,7 @@ def generate_from_yaml(
3135
:param config: a GenerationConfig object.
3236
:param repository_path: The repository path to which the generated files
3337
will be sent.
38+
:param api_definitions_path: The path to where the api definition resides.
3439
:param target_library_names: a list of libraries to be generated.
3540
If specified, only the library whose library_name is in target_library_names
3641
will be generated.
@@ -43,6 +48,8 @@ def generate_from_yaml(
4348
repo_config = util.prepare_repo(
4449
gen_config=config, library_config=target_libraries, repo_path=repository_path
4550
)
51+
# copy api definition to output folder.
52+
shutil.copytree(api_definitions_path, repo_config.output_folder, dirs_exist_ok=True)
4653

4754
for library_path, library in repo_config.get_libraries().items():
4855
print(f"generating library {library.get_library_name()}")

library_generation/test/cli/entry_point_unit_tests.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,13 @@ def test_generate_non_monorepo_without_changes_triggers_full_generation(
105105
baseline_generation_config_path=config_path,
106106
current_generation_config_path=config_path,
107107
repository_path=".",
108+
api_definitions_path=".",
108109
)
109110
generate_from_yaml.assert_called_with(
110-
config=ANY, repository_path=ANY, target_library_names=None
111+
config=ANY,
112+
repository_path=ANY,
113+
api_definitions_path=ANY,
114+
target_library_names=None,
111115
)
112116

113117
@patch("library_generation.cli.entry_point.generate_from_yaml")
@@ -134,9 +138,13 @@ def test_generate_non_monorepo_with_changes_triggers_full_generation(
134138
baseline_generation_config_path=baseline_config_path,
135139
current_generation_config_path=current_config_path,
136140
repository_path=".",
141+
api_definitions_path=".",
137142
)
138143
generate_from_yaml.assert_called_with(
139-
config=ANY, repository_path=ANY, target_library_names=None
144+
config=ANY,
145+
repository_path=ANY,
146+
api_definitions_path=ANY,
147+
target_library_names=None,
140148
)
141149

142150
@patch("library_generation.cli.entry_point.generate_from_yaml")
@@ -160,9 +168,13 @@ def test_generate_monorepo_with_common_protos_triggers_full_generation(
160168
baseline_generation_config_path=config_path,
161169
current_generation_config_path=config_path,
162170
repository_path=".",
171+
api_definitions_path=".",
163172
)
164173
generate_from_yaml.assert_called_with(
165-
config=ANY, repository_path=ANY, target_library_names=None
174+
config=ANY,
175+
repository_path=ANY,
176+
api_definitions_path=ANY,
177+
target_library_names=None,
166178
)
167179

168180
@patch("library_generation.cli.entry_point.generate_from_yaml")
@@ -187,7 +199,11 @@ def test_generate_monorepo_without_common_protos_does_not_trigger_full_generatio
187199
baseline_generation_config_path=config_path,
188200
current_generation_config_path=config_path,
189201
repository_path=".",
202+
api_definitions_path=".",
190203
)
191204
generate_from_yaml.assert_called_with(
192-
config=ANY, repository_path=ANY, target_library_names=[]
205+
config=ANY,
206+
repository_path=ANY,
207+
api_definitions_path=ANY,
208+
target_library_names=[],
193209
)

0 commit comments

Comments
 (0)