| # Copyright 2019 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from google.protobuf import text_format as textpb |
| from recipe_engine import recipe_api |
| |
| DEFAULT_SPEC_DIR = "infra/config/generated" |
| |
| |
| class SpecApi(recipe_api.RecipeApi): |
| """API for working with textproto files.""" |
| |
| class ParseError(recipe_api.StepFailure): |
| pass |
| |
| def _get_spec_revision(self, spec_remote, Type, spec_dir, spec_revision): |
| build = self.m.buildbucket.build |
| builder_name = build.builder.builder |
| # Have subbuilders use the same specs as the parent builders. |
| if builder_name.endswith("-subbuild"): |
| builder_name = builder_name[: builder_name.index("-subbuild")] |
| spec_checkout_path = self.m.path["cleanup"].join("spec_workspace") |
| spec_path = spec_checkout_path.join( |
| spec_dir, |
| build.builder.project, |
| "specs", |
| build.builder.bucket, |
| builder_name + ".textproto", |
| ) |
| if not spec_revision: |
| assert build.input.gitiles_commit |
| commit_remote = "https://%s/%s" % ( |
| build.input.gitiles_commit.host, |
| build.input.gitiles_commit.project, |
| ) |
| if commit_remote == spec_remote: |
| # The spec_revision will be ignored in this case and the |
| # revision of the gitiles_commit associated with the build |
| # will be used instead. This is just for consistency to |
| # return the correct spec revision being used. |
| spec_revision = build.input.gitiles_commit.id |
| else: |
| # If the commit is associated with a repo other than the one |
| # that contains the specs, then we still want to check out the |
| # spec repo at the same ref because we'll go on to do the |
| # primary checkout at this ref and we want to get the spec at |
| # the same revision as the primary checkout. |
| spec_revision = build.input.gitiles_commit.ref or "refs/heads/main" |
| |
| spec_revision = self.m.git.checkout( |
| url=spec_remote, |
| path=spec_checkout_path, |
| ref=spec_revision, |
| submodules=False, |
| ) |
| if build.input.gerrit_changes: |
| change = build.input.gerrit_changes[0] |
| gerrit_remote = "https://%s/%s" % ( |
| change.host.replace("-review", ""), |
| change.project, |
| ) |
| if gerrit_remote == spec_remote: |
| self.m.git.checkout_cl( |
| cl=change, |
| path=spec_checkout_path, |
| onto=spec_revision, |
| submodules=False, |
| cache=False, |
| ) |
| |
| spec = self.m.file.read_raw(name="read spec", source=spec_path).strip() |
| |
| if not spec: |
| # Return parse error if the file is empty. google.protobuf considers |
| # that a valid proto so this results in an empty spec otherwise. |
| raise self.ParseError("file is empty") |
| |
| try: |
| return textpb.Merge(spec, Type()), spec_revision |
| except Exception as e: |
| raise self.ParseError(e) |
| |
| def get_spec_revision( |
| self, |
| spec_remote, |
| Type, |
| spec_dir=DEFAULT_SPEC_DIR, |
| spec_revision=None, |
| ): |
| """Loads a spec message from the given Buildbucket build input. |
| |
| If the build input is for a gitiles commit, the Gitiles host, project, |
| and ref are inferred from the build input and the spec is fetched from |
| Gitiles. |
| |
| If the build input is for a gerrit change, the project is rebased on top |
| of the parent gitiles_commit in build. |
| |
| Args: |
| spec_remote (string): URL of the specs git repository. |
| spec_dir (string): The directory within spec_remote containing specs. |
| Specifically we'll look for the spec in |
| "<spec_dir>/<project>/specs/<bucket>/<builder>.textproto". |
| spec_revision (string or None): The git revision to fetch the spec from. |
| |
| Returns: |
| spec (Type), rev (string): The parsed proto API spec and its revision. |
| |
| Raises: |
| file.Error if the spec file could not be located. |
| git.RebaseError if the spec file could not be rebased. |
| spec.ParseError if the proto fails to parse into the given Type. |
| """ |
| try: |
| return self._get_spec_revision( |
| spec_remote=spec_remote, |
| Type=Type, |
| spec_dir=spec_dir, |
| spec_revision=spec_revision, |
| ) |
| except self.m.file.Error: |
| # File not found or user does not have read permissions. This may |
| # be encountered in integration CQ if a user makes a bad config |
| # change. It is not a general infrastructure issue. |
| raise |
| except self.m.git.RebaseError: |
| # A failure to rebase is closer to user error than an infra failure. |
| # It can be fixed by the user by rebasing their change and then |
| # retrying the patch. The infrastructure is working properly. |
| raise |
| except self.ParseError: |
| # File is not parsable as a valid proto. This may be encounter in |
| # integration CQ if a bad config change is made, it is however not |
| # a general infrastructure issue, and would not make it thru CQ. |
| raise |
| except recipe_api.StepFailure as e: |
| # All other failures are the infra's fault. |
| raise recipe_api.InfraFailure(e.name or e.reason, result=e.result) |