Skip to content

Commit efa1b87

Browse files
authored
Merge pull request #5857: Add pants-plugins/api_spec to streamline regenerating openapi.yaml
2 parents 09191a2 + dade25a commit efa1b87

File tree

12 files changed

+635
-8
lines changed

12 files changed

+635
-8
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Added
1414
working on StackStorm, improve our security posture, and improve CI reliability thanks in part
1515
to pants' use of PEX lockfiles. This is not a user-facing addition.
1616
#5778 #5789 #5817 #5795 #5830 #5833 #5834 #5841 #5840 #5838 #5842 #5837 #5849 #5850
17-
#5846 #5853 #5848 #5847 #5858
17+
#5846 #5853 #5848 #5847 #5858 #5857
1818
Contributed by @cognifloyd
1919

2020
* Added a joint index to solve the problem of slow mongo queries for scheduled executions. #5805

Makefile

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -460,11 +460,7 @@ generate-api-spec: requirements .generate-api-spec
460460
@echo
461461
@echo "================== Generate openapi.yaml file ===================="
462462
@echo
463-
echo "# NOTE: This file is auto-generated - DO NOT EDIT MANUALLY" > st2common/st2common/openapi.yaml
464-
echo "# Edit st2common/st2common/openapi.yaml.j2 and then run" >> st2common/st2common/openapi.yaml
465-
echo "# make .generate-api-spec" >> st2common/st2common/openapi.yaml
466-
echo "# to generate the final spec file" >> st2common/st2common/openapi.yaml
467-
. $(VIRTUALENV_DIR)/bin/activate; python st2common/bin/st2-generate-api-spec --config-file conf/st2.dev.conf >> st2common/st2common/openapi.yaml
463+
. $(VIRTUALENV_DIR)/bin/activate; python st2common/bin/st2-generate-api-spec --config-file conf/st2.dev.conf > st2common/st2common/openapi.yaml
468464

469465
.PHONY: circle-lint-api-spec
470466
circle-lint-api-spec:

pants-plugins/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,22 @@ The plugins here add custom goals or other logic into pants.
99
To see available goals, do "./pants help goals" and "./pants help $goal".
1010

1111
These StackStorm-specific plugins are probably only useful for the st2 repo.
12+
- `api_spec`
1213
- `schemas`
1314

15+
### `api_spec` plugin
16+
17+
This plugin wires up pants to make sure `st2common/st2common/openapi.yaml`
18+
gets regenerated if needed. Now, whenever someone runs the `fmt` goal
19+
(eg `./pants fmt st2common/st2common/openapi.yaml`), the api spec will
20+
be regenerated if any of the files used to generate it has changed.
21+
Also, running the `lint` goal will fail if the schemas need to be
22+
regenerated.
23+
24+
This plugin also wires up pants so that the `lint` goal runs additional
25+
api spec validation on `st2common/st2common/openapi.yaml` with something
26+
like `./pants lint st2common/st2common/openapi.yaml`.
27+
1428
### `schemas` plugin
1529

1630
This plugin wires up pants to make sure `contrib/schemas/*.json` gets

pants-plugins/api_spec/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
python_sources()
2+
3+
python_tests(
4+
name="tests",
5+
)

pants-plugins/api_spec/__init__.py

Whitespace-only changes.

pants-plugins/api_spec/register.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright 2023 The StackStorm Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from api_spec.rules import rules as api_spec_rules
16+
from api_spec.target_types import APISpec
17+
18+
19+
def rules():
20+
return [*api_spec_rules()]
21+
22+
23+
def target_types():
24+
return [APISpec]

pants-plugins/api_spec/rules.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Copyright 2023 The StackStorm Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from dataclasses import dataclass
15+
16+
from pants.backend.python.target_types import EntryPoint
17+
from pants.backend.python.util_rules import pex, pex_from_targets
18+
from pants.backend.python.util_rules.pex import (
19+
VenvPex,
20+
VenvPexProcess,
21+
)
22+
from pants.backend.python.util_rules.pex_from_targets import PexFromTargetsRequest
23+
from pants.core.goals.fmt import FmtResult, FmtTargetsRequest
24+
from pants.core.goals.lint import LintResult, LintResults, LintTargetsRequest
25+
from pants.core.target_types import FileSourceField, ResourceSourceField
26+
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
27+
from pants.engine.addresses import Address
28+
from pants.engine.fs import (
29+
CreateDigest,
30+
Digest,
31+
FileContent,
32+
MergeDigests,
33+
Snapshot,
34+
)
35+
from pants.engine.process import FallibleProcessResult, ProcessResult
36+
from pants.engine.rules import Get, MultiGet, collect_rules, rule
37+
from pants.engine.target import (
38+
FieldSet,
39+
SourcesField,
40+
TransitiveTargets,
41+
TransitiveTargetsRequest,
42+
)
43+
from pants.engine.unions import UnionRule
44+
from pants.util.logging import LogLevel
45+
46+
from api_spec.target_types import APISpecSourceField
47+
48+
49+
# these constants are also used in the tests
50+
CMD_SOURCE_ROOT = "st2common"
51+
CMD_DIR = "st2common/st2common/cmd"
52+
CMD_MODULE = "st2common.cmd"
53+
GENERATE_CMD = "generate_api_spec"
54+
VALIDATE_CMD = "validate_api_spec"
55+
56+
57+
@dataclass(frozen=True)
58+
class APISpecFieldSet(FieldSet):
59+
required_fields = (APISpecSourceField,)
60+
61+
source: APISpecSourceField
62+
63+
64+
class GenerateAPISpecViaFmtTargetsRequest(FmtTargetsRequest):
65+
field_set_type = APISpecFieldSet
66+
name = GENERATE_CMD
67+
68+
69+
class ValidateAPISpecRequest(LintTargetsRequest):
70+
field_set_type = APISpecFieldSet
71+
name = VALIDATE_CMD
72+
73+
74+
@rule(
75+
desc="Update openapi.yaml with st2-generate-api-spec",
76+
level=LogLevel.DEBUG,
77+
)
78+
async def generate_api_spec_via_fmt(
79+
request: GenerateAPISpecViaFmtTargetsRequest,
80+
) -> FmtResult:
81+
# There will only be one target+field_set, but we iterate
82+
# to satisfy how fmt expects that there could be more than one.
83+
# If there is more than one, they will all get the same contents.
84+
85+
# Find all the dependencies of our target
86+
transitive_targets = await Get(
87+
TransitiveTargets,
88+
TransitiveTargetsRequest(
89+
[field_set.address for field_set in request.field_sets]
90+
),
91+
)
92+
93+
dependency_files_get = Get(
94+
SourceFiles,
95+
SourceFilesRequest(
96+
sources_fields=[
97+
tgt.get(SourcesField) for tgt in transitive_targets.dependencies
98+
],
99+
for_sources_types=(FileSourceField, ResourceSourceField),
100+
),
101+
)
102+
103+
source_files_get = Get(
104+
SourceFiles,
105+
SourceFilesRequest(field_set.source for field_set in request.field_sets),
106+
)
107+
108+
# actually generate it with an external script.
109+
# Generation cannot be inlined here because it needs to import the st2 code.
110+
pex_get = Get(
111+
VenvPex,
112+
PexFromTargetsRequest(
113+
[
114+
Address(
115+
CMD_DIR,
116+
target_name="cmd",
117+
relative_file_path=f"{GENERATE_CMD}.py",
118+
),
119+
],
120+
output_filename=f"{GENERATE_CMD}.pex",
121+
internal_only=True,
122+
main=EntryPoint.parse(f"{CMD_MODULE}.{GENERATE_CMD}:main"),
123+
),
124+
)
125+
126+
pex, dependency_files, source_files = await MultiGet(
127+
pex_get, dependency_files_get, source_files_get
128+
)
129+
130+
# If we were given an input digest from a previous formatter for the source files, then we
131+
# should use that input digest instead of the one we read from the filesystem.
132+
source_files_snapshot = (
133+
source_files.snapshot if request.snapshot is None else request.snapshot
134+
)
135+
136+
input_digest = await Get(
137+
Digest,
138+
MergeDigests((dependency_files.snapshot.digest, source_files_snapshot.digest)),
139+
)
140+
141+
result = await Get(
142+
ProcessResult,
143+
VenvPexProcess(
144+
pex,
145+
argv=(
146+
"--config-file",
147+
"conf/st2.dev.conf",
148+
),
149+
input_digest=input_digest,
150+
description="Regenerating openapi.yaml api spec",
151+
level=LogLevel.DEBUG,
152+
),
153+
)
154+
155+
contents = [
156+
FileContent(
157+
f"{field_set.address.spec_path}/{field_set.source.value}",
158+
result.stdout,
159+
)
160+
for field_set in request.field_sets
161+
]
162+
163+
output_digest = await Get(Digest, CreateDigest(contents))
164+
output_snapshot = await Get(Snapshot, Digest, output_digest)
165+
# TODO: Drop result.stdout since we already wrote it to a file?
166+
return FmtResult.create(request, result, output_snapshot, strip_chroot_path=True)
167+
168+
169+
@rule(
170+
desc="Validate openapi.yaml with st2-validate-api-spec",
171+
level=LogLevel.DEBUG,
172+
)
173+
async def validate_api_spec(
174+
request: ValidateAPISpecRequest,
175+
) -> LintResults:
176+
# There will only be one target+field_set, but we iterate
177+
# to satisfy how lint expects that there could be more than one.
178+
# If there is more than one, they will all get the same contents.
179+
180+
# Find all the dependencies of our target
181+
transitive_targets = await Get(
182+
TransitiveTargets,
183+
TransitiveTargetsRequest(
184+
[field_set.address for field_set in request.field_sets]
185+
),
186+
)
187+
188+
dependency_files_get = Get(
189+
SourceFiles,
190+
SourceFilesRequest(
191+
sources_fields=[
192+
tgt.get(SourcesField) for tgt in transitive_targets.dependencies
193+
],
194+
for_sources_types=(FileSourceField, ResourceSourceField),
195+
),
196+
)
197+
198+
source_files_get = Get(
199+
SourceFiles,
200+
SourceFilesRequest(field_set.source for field_set in request.field_sets),
201+
)
202+
203+
# actually validate it with an external script.
204+
# Validation cannot be inlined here because it needs to import the st2 code.
205+
pex_get = Get(
206+
VenvPex,
207+
PexFromTargetsRequest(
208+
[
209+
Address(
210+
CMD_DIR,
211+
target_name="cmd",
212+
relative_file_path=f"{VALIDATE_CMD}.py",
213+
),
214+
],
215+
output_filename=f"{VALIDATE_CMD}.pex",
216+
internal_only=True,
217+
main=EntryPoint.parse(f"{CMD_MODULE}.{VALIDATE_CMD}:main"),
218+
),
219+
)
220+
221+
pex, dependency_files, source_files = await MultiGet(
222+
pex_get, dependency_files_get, source_files_get
223+
)
224+
225+
input_digest = await Get(
226+
Digest,
227+
MergeDigests((dependency_files.snapshot.digest, source_files.snapshot.digest)),
228+
)
229+
230+
process_result = await Get(
231+
FallibleProcessResult,
232+
VenvPexProcess(
233+
pex,
234+
argv=(
235+
"--config-file",
236+
"conf/st2.dev.conf",
237+
# TODO: Uncomment these as part of a project to fix the (many) issues it identifies.
238+
# We can uncomment --validate-defs (and possibly --verbose) once the spec defs are valid.
239+
# "--validate-defs", # check for x-api-model in definitions
240+
# "--verbose", # show model definitions on failure (only applies to --validate-defs)
241+
),
242+
input_digest=input_digest,
243+
description="Validating openapi.yaml api spec",
244+
level=LogLevel.DEBUG,
245+
),
246+
)
247+
248+
result = LintResult.from_fallible_process_result(process_result)
249+
return LintResults([result], linter_name=request.name)
250+
251+
252+
def rules():
253+
return [
254+
*collect_rules(),
255+
UnionRule(FmtTargetsRequest, GenerateAPISpecViaFmtTargetsRequest),
256+
UnionRule(LintTargetsRequest, ValidateAPISpecRequest),
257+
*pex.rules(),
258+
*pex_from_targets.rules(),
259+
]

0 commit comments

Comments
 (0)