diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index 1af27ea98..69ba86a5a 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: |
+ python tools/validate_metadata.py -v blueprints
diff --git a/blueprints/data-solutions/vertex-mlops/metadata.yaml b/blueprints/data-solutions/vertex-mlops/metadata.yaml
index c85c8995b..d09c24473 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/blueprints/gke/multitenant-fleet/README.md b/blueprints/gke/multitenant-fleet/README.md
index c263317b8..0818479b4 100644
--- a/blueprints/gke/multitenant-fleet/README.md
+++ b/blueprints/gke/multitenant-fleet/README.md
@@ -247,9 +247,9 @@ module "gke" {
|---|---|:---:|:---:|:---:|
| [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | |
| [folder_id](variables.tf#L132) | Folder used for the GKE project in folders/nnnnnnnnnnn format. | string | ✓ | |
-| [prefix](variables.tf#L179) | Prefix used for resource names. | string | ✓ | |
-| [project_id](variables.tf#L188) | ID of the project that will contain all the clusters. | string | ✓ | |
-| [vpc_config](variables.tf#L200) | Shared VPC project and VPC details. | object({…}) | ✓ | |
+| [prefix](variables.tf#L183) | Prefix used for resource names. | string | ✓ | |
+| [project_id](variables.tf#L192) | ID of the project that will contain all the clusters. | string | ✓ | |
+| [vpc_config](variables.tf#L204) | Shared VPC project and VPC details. | object({…}) | ✓ | |
| [clusters](variables.tf#L22) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} |
| [fleet_configmanagement_clusters](variables.tf#L70) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} |
| [fleet_configmanagement_templates](variables.tf#L77) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} |
@@ -258,8 +258,8 @@ module "gke" {
| [group_iam](variables.tf#L137) | Project-level IAM bindings for groups. Use group emails as keys, list of roles as values. | map(list(string)) | | {} |
| [iam](variables.tf#L144) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
| [labels](variables.tf#L151) | Project-level labels. | map(string) | | {} |
-| [nodepools](variables.tf#L157) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} |
-| [project_services](variables.tf#L193) | Additional project services to enable. | list(string) | | [] |
+| [nodepools](variables.tf#L157) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} |
+| [project_services](variables.tf#L197) | Additional project services to enable. | list(string) | | [] |
## Outputs
diff --git a/blueprints/gke/multitenant-fleet/variables.tf b/blueprints/gke/multitenant-fleet/variables.tf
index 2cfd26a1b..13760606e 100644
--- a/blueprints/gke/multitenant-fleet/variables.tf
+++ b/blueprints/gke/multitenant-fleet/variables.tf
@@ -170,7 +170,11 @@ variable "nodepools" {
service_account = optional(any)
sole_tenant_nodegroup = optional(string)
tags = optional(list(string))
- taints = optional(list(any))
+ taints = optional(list(object({
+ key = string
+ value = string
+ effect = string
+ })))
})))
default = {}
nullable = false
diff --git a/fast/stages/3-gke-multitenant/dev/README.md b/fast/stages/3-gke-multitenant/dev/README.md
index 3cc4e3304..2152003a1 100644
--- a/fast/stages/3-gke-multitenant/dev/README.md
+++ b/fast/stages/3-gke-multitenant/dev/README.md
@@ -166,8 +166,8 @@ Leave all these variables unset (or set to `null`) to disable fleet management.
| [billing_account](variables.tf#L29) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap |
| [folder_ids](variables.tf#L153) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman |
| [host_project_ids](variables.tf#L168) | Host project for the shared VPC. | object({…}) | ✓ | | 2-networking |
-| [prefix](variables.tf#L217) | Prefix used for resources that need unique names. | string | ✓ | | |
-| [vpc_self_links](variables.tf#L233) | Self link for the shared VPC. | object({…}) | ✓ | | 2-networking |
+| [prefix](variables.tf#L221) | Prefix used for resources that need unique names. | string | ✓ | | |
+| [vpc_self_links](variables.tf#L237) | Self link for the shared VPC. | object({…}) | ✓ | | 2-networking |
| [clusters](variables.tf#L42) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | |
| [fleet_configmanagement_clusters](variables.tf#L90) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | |
| [fleet_configmanagement_templates](variables.tf#L98) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | |
@@ -176,9 +176,9 @@ Leave all these variables unset (or set to `null`) to disable fleet management.
| [group_iam](variables.tf#L161) | Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values. | map(list(string)) | | {} | |
| [iam](variables.tf#L176) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | |
| [labels](variables.tf#L183) | Project-level labels. | map(string) | | {} | |
-| [nodepools](variables.tf#L189) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | |
-| [outputs_location](variables.tf#L211) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | |
-| [project_services](variables.tf#L226) | Additional project services to enable. | list(string) | | [] | |
+| [nodepools](variables.tf#L189) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | |
+| [outputs_location](variables.tf#L215) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | |
+| [project_services](variables.tf#L230) | Additional project services to enable. | list(string) | | [] | |
## Outputs
diff --git a/fast/stages/3-gke-multitenant/dev/variables.tf b/fast/stages/3-gke-multitenant/dev/variables.tf
index db532dd0e..a872b49d1 100644
--- a/fast/stages/3-gke-multitenant/dev/variables.tf
+++ b/fast/stages/3-gke-multitenant/dev/variables.tf
@@ -202,7 +202,11 @@ variable "nodepools" {
service_account = optional(any)
sole_tenant_nodegroup = optional(string)
tags = optional(list(string))
- taints = optional(list(any))
+ taints = optional(list(object({
+ key = string
+ value = string
+ effect = string
+ })))
})))
default = {}
nullable = false
diff --git a/tools/bpmetadataschema.json b/tools/bpmetadataschema.json
new file mode 100644
index 000000000..42ae0d3a4
--- /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/requirements.txt b/tools/requirements.txt
index 1b01cfa04..c5d809dd1 100644
--- a/tools/requirements.txt
+++ b/tools/requirements.txt
@@ -6,3 +6,4 @@ marko
requests
yamale
yapf
+jsonschema
diff --git a/tools/validate_metadata.py b/tools/validate_metadata.py
new file mode 100755
index 000000000..08e76ea5b
--- /dev/null
+++ b/tools/validate_metadata.py
@@ -0,0 +1,90 @@
+#!/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: list[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)
+
+ failed_files = {}
+ for instance in instances:
+ result = _validate(instance, validator)
+ if result.state == State.OK:
+ print(f'[✓] {instance}')
+ else:
+ print(f'[✗] {instance}')
+ failed_files[instance] = result.errors
+
+ if verbose:
+ for file_path, errors in failed_files.items():
+ print(f"\n====== {file_path!s} ======")
+ for path, message in errors.items():
+ print(f"{path}: {message}")
+
+ return 0 if not failed_files else 1
+
+
+if __name__ == '__main__':
+ sys.exit(main())