From 437c6fed744d04ab03586dd95cad6d3cfb00e0ef Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Fri, 5 May 2023 16:56:21 +0200 Subject: [PATCH] Add blueprint metadata validation tool --- .github/workflows/linting.yml | 5 + .../data-solutions/vertex-mlops/metadata.yaml | 2 +- tools/bpmetadataschema.json | 966 ++++++++++++++++++ tools/validate_metadata.py | 93 ++ 4 files changed, 1065 insertions(+), 1 deletion(-) create mode 100644 tools/bpmetadataschema.json create mode 100755 tools/validate_metadata.py diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 1af27ea98f..025c171174 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -75,3 +75,8 @@ jobs: tools/*.py \ blueprints/cloud-operations/network-dashboard/src/*py \ blueprints/cloud-operations/network-dashboard/src/plugins/*py + + - name: Check blueprint metadata + id: metadata + run: | + pythontools/validate_metadata.py -v blueprints diff --git a/blueprints/data-solutions/vertex-mlops/metadata.yaml b/blueprints/data-solutions/vertex-mlops/metadata.yaml index c85c8995b6..d09c244738 100644 --- a/blueprints/data-solutions/vertex-mlops/metadata.yaml +++ b/blueprints/data-solutions/vertex-mlops/metadata.yaml @@ -129,7 +129,7 @@ spec: type: string default: null required: false - - name: service_encryption_keys + - name: service_encryption_keys description: Cloud KMS to use to encrypt different services. Key location should match service region. type: |- object({ diff --git a/tools/bpmetadataschema.json b/tools/bpmetadataschema.json new file mode 100644 index 0000000000..42ae0d3a44 --- /dev/null +++ b/tools/bpmetadataschema.json @@ -0,0 +1,966 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/cli/bpmetadata/blueprint-metadata", + "$ref": "#/$defs/BlueprintMetadata", + "$defs": { + "BlueprintActuationTool": { + "properties": { + "type": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintArchitecture": { + "properties": { + "diagram": { + "type": "string" + }, + "description": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "diagram", + "description" + ] + }, + "BlueprintAuthor": { + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "title" + ] + }, + "BlueprintCloudProduct": { + "properties": { + "productId": { + "type": "string" + }, + "pageUrl": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "pageUrl" + ] + }, + "BlueprintContent": { + "properties": { + "architecture": { + "$ref": "#/$defs/BlueprintArchitecture" + }, + "diagrams": { + "items": { + "$ref": "#/$defs/BlueprintDiagram" + }, + "type": "array" + }, + "documentation": { + "items": { + "$ref": "#/$defs/BlueprintListContent" + }, + "type": "array" + }, + "subBlueprints": { + "items": { + "$ref": "#/$defs/BlueprintMiscContent" + }, + "type": "array" + }, + "examples": { + "items": { + "$ref": "#/$defs/BlueprintMiscContent" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintCostEstimate": { + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "description", + "url" + ] + }, + "BlueprintDescription": { + "properties": { + "tagline": { + "type": "string" + }, + "detailed": { + "type": "string" + }, + "preDeploy": { + "type": "string" + }, + "html": { + "type": "string" + }, + "eulaUrls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "architecture": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintDiagram": { + "properties": { + "name": { + "type": "string" + }, + "altText": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] + }, + "BlueprintInfo": { + "properties": { + "title": { + "type": "string" + }, + "source": { + "$ref": "#/$defs/BlueprintRepoDetail" + }, + "version": { + "type": "string" + }, + "actuationTool": { + "$ref": "#/$defs/BlueprintActuationTool" + }, + "description": { + "$ref": "#/$defs/BlueprintDescription" + }, + "icon": { + "type": "string" + }, + "deploymentDuration": { + "$ref": "#/$defs/BlueprintTimeEstimate" + }, + "costEstimate": { + "$ref": "#/$defs/BlueprintCostEstimate" + }, + "cloudProducts": { + "items": { + "$ref": "#/$defs/BlueprintCloudProduct" + }, + "type": "array" + }, + "quotaDetails": { + "items": { + "$ref": "#/$defs/BlueprintQuotaDetail" + }, + "type": "array" + }, + "author": { + "$ref": "#/$defs/BlueprintAuthor" + }, + "softwareGroups": { + "items": { + "$ref": "#/$defs/BlueprintSoftwareGroup" + }, + "type": "array" + }, + "supportInfo": { + "$ref": "#/$defs/BlueprintSupport" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "title", + "source" + ] + }, + "BlueprintInterface": { + "properties": { + "variables": { + "items": { + "$ref": "#/$defs/BlueprintVariable" + }, + "type": "array" + }, + "variableGroups": { + "items": { + "$ref": "#/$defs/BlueprintVariableGroup" + }, + "type": "array" + }, + "outputs": { + "items": { + "$ref": "#/$defs/BlueprintOutput" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintListContent": { + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "title" + ] + }, + "BlueprintMetadata": { + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "metadata": { + "$ref": "#/$defs/ObjectMeta" + }, + "spec": { + "$ref": "#/$defs/BlueprintMetadataSpec" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec" + ] + }, + "BlueprintMetadataSpec": { + "properties": { + "info": { + "$ref": "#/$defs/BlueprintInfo" + }, + "content": { + "$ref": "#/$defs/BlueprintContent" + }, + "interfaces": { + "$ref": "#/$defs/BlueprintInterface" + }, + "requirements": { + "$ref": "#/$defs/BlueprintRequirements" + }, + "ui": { + "$ref": "#/$defs/BlueprintUI" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintMiscContent": { + "properties": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] + }, + "BlueprintOutput": { + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] + }, + "BlueprintQuotaDetail": { + "properties": { + "variable": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "GCE_INSTANCE", + "GCE_DISK" + ] + }, + "quotaType": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "quotaType" + ] + }, + "BlueprintRepoDetail": { + "properties": { + "repo": { + "type": "string" + }, + "sourceType": { + "type": "string" + }, + "dir": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "repo", + "sourceType" + ] + }, + "BlueprintRequirements": { + "properties": { + "roles": { + "items": { + "$ref": "#/$defs/BlueprintRoles" + }, + "type": "array" + }, + "services": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintRoles": { + "properties": { + "level": { + "type": "string" + }, + "roles": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "level", + "roles" + ] + }, + "BlueprintSoftware": { + "properties": { + "title": { + "type": "string" + }, + "version": { + "type": "string" + }, + "url": { + "type": "string" + }, + "licenseUrl": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "title" + ] + }, + "BlueprintSoftwareGroup": { + "properties": { + "type": { + "type": "string", + "enum": [ + "UNSPECIFIED", + "OS" + ] + }, + "software": { + "items": { + "$ref": "#/$defs/BlueprintSoftware" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintSupport": { + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "entity": { + "type": "string" + }, + "showSupportId": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "description" + ] + }, + "BlueprintTimeEstimate": { + "properties": { + "configuration": { + "type": "integer" + }, + "deployment": { + "type": "integer" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintUI": { + "properties": { + "input": { + "$ref": "#/$defs/BlueprintUIInput" + }, + "runtime": { + "$ref": "#/$defs/BlueprintUIOutput" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintUIInput": { + "properties": { + "variables": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/DisplayVariable" + } + }, + "type": "object" + }, + "sections": { + "items": { + "$ref": "#/$defs/DisplaySection" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintUIOutput": { + "properties": { + "outputMessage": { + "type": "string" + }, + "suggestedActions": { + "items": { + "$ref": "#/$defs/UIActionItem" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintVariable": { + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + }, + "default": true, + "required": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object" + }, + "BlueprintVariableGroup": { + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "variables": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] + }, + "DisplaySection": { + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "tooltip": { + "type": "string" + }, + "subtext": { + "type": "string" + }, + "parent": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] + }, + "DisplayVariable": { + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "invisible": { + "type": "boolean" + }, + "tooltip": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "regexValidation": { + "type": "string" + }, + "minItems": { + "type": "integer" + }, + "maxItems": { + "type": "integer" + }, + "minLength": { + "type": "integer" + }, + "maxLength": { + "type": "integer" + }, + "min": { + "type": "integer" + }, + "max": { + "type": "integer" + }, + "section": { + "type": "string" + }, + "x-googleProperty": { + "$ref": "#/$defs/GooglePropertyExtension" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] + }, + "GCEDiskSizeExtension": { + "properties": { + "diskTypeVariable": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "diskTypeVariable" + ] + }, + "GCEExternalIPExtension": { + "properties": { + "networkVariable": { + "type": "string" + }, + "externalIpType": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "networkVariable" + ] + }, + "GCEFirewallExtension": { + "properties": { + "networkVariable": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "networkVariable" + ] + }, + "GCEFirewallRangeExtension": { + "properties": { + "firewallVariable": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "firewallVariable" + ] + }, + "GCEGPUCountExtension": { + "properties": { + "machineTypeVariable": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "machineTypeVariable" + ] + }, + "GCEGPUTypeExtension": { + "properties": { + "machineType": { + "type": "string" + }, + "gpuType": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "machineType" + ] + }, + "GCEGenericResourceExtension": { + "properties": { + "resourceVariable": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "resourceVariable" + ] + }, + "GCEIPForwardingExtension": { + "properties": { + "networkVariable": { + "type": "string" + }, + "notConfigurable": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "networkVariable" + ] + }, + "GCELocationExtension": { + "properties": { + "allowlistedZones": { + "items": { + "type": "string" + }, + "type": "array" + }, + "allowlistedRegions": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object" + }, + "GCEMachineTypeExtension": { + "properties": { + "minCpu": { + "type": "integer" + }, + "minRamGb": { + "type": "integer" + }, + "disallowCustomMachineTypes": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object" + }, + "GCENetworkExtension": { + "properties": { + "allowSharedVpcs": { + "type": "boolean" + }, + "machineTypeVariable": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "machineTypeVariable" + ] + }, + "GCESubnetworkExtension": { + "properties": { + "networkVariable": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "networkVariable" + ] + }, + "GooglePropertyExtension": { + "properties": { + "type": { + "type": "string", + "enum": [ + "EMAIL_ADDRESS", + "MULTI_LINE_STRING", + "GCE_DISK_IMAGE", + "GCE_DISK_TYPE", + "GCE_DISK_SIZE", + "GCE_MACHINE_TYPE", + "GCE_NETWORK", + "GCE_ZONE", + "GCE_SUBNETWORK", + "GCE_REGION", + "GCE_GPU_TYPE", + "GCE_GPU_COUNT", + "GCE_EXTERNAL_IP", + "GCE_IP_FORWARDING", + "GCE_FIREWALL", + "GCE_FIREWALL_RANGE", + "GCE_GENERIC_RESOURCE", + "GCS_BUCKET", + "IAM_SERVICE_ACCOUNT" + ] + }, + "zoneProperty": { + "type": "string" + }, + "gceMachineType": { + "$ref": "#/$defs/GCEMachineTypeExtension" + }, + "gceDiskSize": { + "$ref": "#/$defs/GCEDiskSizeExtension" + }, + "gceSubnetwork": { + "$ref": "#/$defs/GCESubnetworkExtension" + }, + "gceResource": { + "$ref": "#/$defs/GCEGenericResourceExtension" + }, + "gceGpuType": { + "$ref": "#/$defs/GCEGPUTypeExtension" + }, + "gceGpuCount": { + "$ref": "#/$defs/GCEGPUCountExtension" + }, + "gceNetwork": { + "$ref": "#/$defs/GCENetworkExtension" + }, + "gceExternalIp": { + "$ref": "#/$defs/GCEExternalIPExtension" + }, + "gceIpForwarding": { + "$ref": "#/$defs/GCEIPForwardingExtension" + }, + "gceFirewall": { + "$ref": "#/$defs/GCEFirewallExtension" + }, + "gceFirewallRange": { + "$ref": "#/$defs/GCEFirewallRangeExtension" + }, + "gceZone": { + "$ref": "#/$defs/GCELocationExtension" + }, + "gceRegion": { + "$ref": "#/$defs/GCELocationExtension" + }, + "iamServiceAccount": { + "$ref": "#/$defs/IAMServiceAccountExtension" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ] + }, + "IAMServiceAccountExtension": { + "properties": { + "roles": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "roles" + ] + }, + "ObjectMeta": { + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "labels": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "annotations": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "additionalProperties": false, + "type": "object" + }, + "UIActionItem": { + "properties": { + "heading": { + "type": "string" + }, + "description": { + "type": "string" + }, + "snippet": { + "type": "string" + }, + "showIf": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "heading" + ] + } + } +} \ No newline at end of file diff --git a/tools/validate_metadata.py b/tools/validate_metadata.py new file mode 100755 index 0000000000..d6d544ed27 --- /dev/null +++ b/tools/validate_metadata.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. +'''Validate a YAML file against the standard blueprint metadata schema[1] + +[1] https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/master/cli/bpmetadata/schema/bpmetadataschema.json +''' + +import enum +import json +import sys +from dataclasses import dataclass +from pathlib import Path + +import click +import jsonschema +import yaml + +SCHEMA_PATH = Path(__file__).parent / 'bpmetadataschema.json' + + +class State(enum.Enum): + INVALID: int = 0 + OK: int = 1 + + +@dataclass +class ValidationResult: + state: State + errors: dict[str, str] + + +def _validate(path: Path, validator) -> ValidationResult: + with open(path) as f: + metadata = yaml.safe_load(f) + + errors = { + error.json_path: error.message + for error in validator.iter_errors(metadata) + } + + state = State.OK if not errors else State.INVALID + return ValidationResult(state=state, errors=errors) + + +@click.command() +@click.argument('dirs', type=click.Path(exists=True, file_okay=False), nargs=-1) +@click.option('-v', '--verbose', is_flag=True, default=False, + help='Print additional validation details.') +def main(dirs: str, verbose: bool) -> int: + instances = set() + for dir_name in dirs: + instances |= set(Path(dir_name).glob("**/metadata.yaml")) + + with open(SCHEMA_PATH) as f: + schema = json.load(f) + validator = jsonschema.validators.Draft202012Validator(schema) + + results = {} + for instance in instances: + result = _validate(instance, validator) + if result.state == State.OK: + print(f'[✓] {instance}') + else: + print(f'[✗] {instance}') + results[instance] = result.errors + + if verbose: + for file_path, errors in results.items(): + print(f"\n====== {file_path!s} ======") + for path, message in errors.items(): + print(f"{path}: {message}") + + if errors: + return 1 + + return 0 + + +if __name__ == '__main__': + sys.exit(main())