Skip to content

Commit

Permalink
fix: use correct types for validation set models (#4928)
Browse files Browse the repository at this point in the history
- Use the correct data types for API calls
- Switch to pydantic models
- Catch model errors locally rather than relying on the store
  to validate the data
- Make the boilerplate validation set valid 

Signed-off-by: Callahan Kovacs <[email protected]>
  • Loading branch information
mr-cal authored Jul 30, 2024
1 parent 4ee3c14 commit feed34e
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 423 deletions.
62 changes: 43 additions & 19 deletions snapcraft/commands/validation_sets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2022 Canonical Ltd.
# Copyright 2022,2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
Expand All @@ -24,14 +24,17 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional

import craft_application.util
import yaml
from craft_application.commands import AppCommand
from craft_application.errors import CraftValidationError
from craft_cli import emit
from overrides import overrides

from snapcraft import errors, utils
from snapcraft_legacy._store import StoreClientCLI
from snapcraft_legacy.storeapi.errors import StoreValidationSetsError
from snapcraft_legacy.storeapi.v2 import validation_sets

if TYPE_CHECKING:
import argparse
Expand All @@ -40,7 +43,7 @@
_VALIDATIONS_SETS_SNAPS_TEMPLATE = textwrap.dedent(
"""\
snaps:
# - name: <name> # The name of the snap.
- name: hello # The name of the snap.
# id: <id> # The ID of the snap. Optional, defaults to the current ID for
# the provided name.
# presence: [required|optional|invalid] # Optional, defaults to required.
Expand Down Expand Up @@ -102,7 +105,9 @@ def run(self, parsed_args: "argparse.Namespace"):
validation_sets_path.write_text(validation_sets_template, encoding="utf-8")
edited_validation_sets = edit_validation_sets(validation_sets_path)

if edited_validation_sets == yaml.safe_load(validation_sets_template):
if edited_validation_sets == validation_sets.EditableBuildAssertion(
**yaml.safe_load(validation_sets_template)
):
emit.message("No changes made")
return

Expand All @@ -119,40 +124,51 @@ def run(self, parsed_args: "argparse.Namespace"):
"Do you wish to amend the validation set?"
):
raise errors.SnapcraftError(
"Operation aborted"
"operation aborted"
) from validation_error
edited_validation_sets = edit_validation_sets(validation_sets_path)
finally:
validation_sets_path.unlink()


def _submit_validation_set(
edited_validation_sets: Dict[str, Any],
edited_validation_sets: validation_sets.EditableBuildAssertion,
key_name: Optional[str],
store_client: StoreClientCLI,
) -> None:
emit.debug(f"Posting assertion to build: {edited_validation_sets.json()}")
build_assertion = store_client.post_validation_sets_build_assertion(
validation_sets=edited_validation_sets
validation_sets=edited_validation_sets.marshal()
)
signed_validation_sets = _sign_assertion(
build_assertion.marshal(), key_name=key_name
build_assertion_dict = build_assertion.marshal_scalars_as_strings()

signed_validation_sets = _sign_assertion(build_assertion_dict, key_name=key_name)

emit.debug("Posting signed validation sets.")
response = store_client.post_validation_sets(
signed_validation_sets=signed_validation_sets
)
store_client.post_validation_sets(signed_validation_sets=signed_validation_sets)
emit.debug(f"Response: {response.json()}")


def _generate_template(
asserted_validation_sets, *, account_id: str, set_name: str, sequence: str
asserted_validation_sets: validation_sets.ValidationSets,
*,
account_id: str,
set_name: str,
sequence: str,
) -> str:
"""Generate a template to edit asserted_validation_sets."""
try:
# assertions should only have one item since a specific
# sequence was requested.
revision = asserted_validation_sets.assertions[0].revision
revision = asserted_validation_sets.assertions[0].headers.revision

snaps = yaml.dump(
{
"snaps": [
s.marshal() for s in asserted_validation_sets.assertions[0].snaps
s.marshal()
for s in asserted_validation_sets.assertions[0].headers.snaps
]
},
default_flow_style=False,
Expand All @@ -174,31 +190,39 @@ def _generate_template(
return unverified_validation_sets


def edit_validation_sets(validation_sets_path: Path) -> Dict[str, Any]:
def edit_validation_sets(
validation_sets_path: Path,
) -> validation_sets.EditableBuildAssertion:
"""Spawn an editor to modify the validation-sets."""
editor_cmd = os.getenv("EDITOR", "vi")

while True:
with emit.pause():
subprocess.run([editor_cmd, validation_sets_path], check=True)
try:
edited_validation_sets = yaml.safe_load(
validation_sets_path.read_text(encoding="utf-8")
with validation_sets_path.open() as file:
data = craft_application.util.safe_yaml_load(file)
edited_validation_sets = validation_sets.EditableBuildAssertion.from_yaml_data(
data=data,
# filepath is only shown for pydantic errors and snapcraft should
# not expose the temp file name
filepath=Path("validation-sets"),
)
return edited_validation_sets
except yaml.YAMLError as yaml_error:
emit.message(f"A YAML parsing error occurred {yaml_error!s}")
except (yaml.YAMLError, CraftValidationError) as err:
emit.message(f"{err!s}")
if not utils.confirm_with_user("Do you wish to amend the validation set?"):
raise errors.SnapcraftError("Operation aborted") from yaml_error
raise errors.SnapcraftError("operation aborted") from err


def _sign_assertion(assertion: Dict[str, Any], *, key_name: Optional[str]) -> bytes:
emit.debug("Signing assertion.")
cmdline = ["snap", "sign"]
if key_name:
cmdline += ["-k", key_name]
snap_sign = subprocess.Popen(cmdline, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
signed_assertion, _ = snap_sign.communicate(input=json.dumps(assertion).encode())
if snap_sign.returncode != 0:
raise errors.SnapcraftError("Failed to sign assertion")
raise errors.SnapcraftError("failed to sign assertion")

return signed_assertion
3 changes: 2 additions & 1 deletion snapcraft_legacy/cli/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ def list_validation_sets(name, sequence):
else:
headers = ["Account-ID", "Name", "Sequence", "Revision", "When"]
assertions = list()
for assertion in asserted_validation_sets.assertions:
for assertion_header in asserted_validation_sets.assertions:
assertion = assertion_header.headers
assertions.append(
[
assertion.account_id,
Expand Down
74 changes: 1 addition & 73 deletions snapcraft_legacy/storeapi/v2/_api_schema.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2020 Canonical Ltd
# Copyright 2020,2024 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
Expand Down Expand Up @@ -275,75 +275,3 @@
"required": ["account", "channels", "packages", "permissions"],
"type": "object",
}

# https://dashboard.snapcraft.io/docs/v2/en/validation-sets.html
BUILD_ASSERTION_JSONSCHEMA: Dict[str, Any] = {
"properties": {
"account-id": {
"description": 'The "account-id" assertion header',
"type": "string",
},
"authority-id": {
"description": 'The "authority-id" assertion header',
"type": "string",
},
"name": {"description": 'The "name" assertion header', "type": "string"},
"revision": {
"description": 'The "revision" assertion header',
"type": "string",
},
"sequence": {
"description": 'The "sequence" assertion header',
"type": "string",
},
"series": {"description": 'The "series" assertion header', "type": "string"},
"snaps": {
"items": {
"description": "List of snaps in a Validation Set assertion",
"properties": {
"id": {
"description": "Snap ID",
"maxLength": 100,
"type": "string",
},
"name": {
"description": "Snap name",
"maxLength": 100,
"type": "string",
},
"presence": {
"description": "Snap presence",
"enum": ["required", "optional", "invalid"],
"type": "string",
},
"revision": {"description": "Snap revision", "type": "string"},
},
"required": ["name"],
"type": "object",
},
"minItems": 1,
"type": "array",
},
"timestamp": {
"description": 'The "timestamp" assertion header',
"type": "string",
},
"type": {
"const": "validation-set",
"description": 'The "type" assertion header',
"type": "string",
},
},
"required": [
"type",
"authority-id",
"series",
"account-id",
"name",
"sequence",
# "revision",
"timestamp",
"snaps",
],
"type": "object",
}
Loading

0 comments on commit feed34e

Please sign in to comment.