From b2c2460a52a3c79e308e21f6d333e4f00e4bd6dc Mon Sep 17 00:00:00 2001 From: Platform team Date: Mon, 2 Nov 2026 09:55:39 +0000 Subject: [PATCH] the existing infrastructure --- .../after/envs/dev/vms/build-agent-02.yaml | 10 +++ .quai/tests/vm/create-dev-vm/request.yaml | 12 ++++ .../after/envs/prod/vms/web01.yaml | 17 +++++ .quai/tests/vm/resize-web01/request.yaml | 5 ++ .quai/types/vm.yaml | 65 ++++++++++++++++++ README.md | 21 ++++++ blueprints/app-servers.yaml | 68 +++++++++++++++++++ blueprints/linux-vm.yaml | 66 ++++++++++++++++++ catalog/schemas/vm.schema.json | 57 ++++++++++++++++ envs/dev/main.tf | 22 ++++++ envs/dev/vms/build-agent-01.yaml | 10 +++ envs/prod/main.tf | 22 ++++++ envs/prod/vms/db01.yaml | 13 ++++ envs/prod/vms/legacy-ftp.yaml | 10 +++ envs/prod/vms/web01.yaml | 17 +++++ envs/test/main.tf | 22 ++++++ envs/test/vms/.gitkeep | 0 modules/vm/main.tf | 17 +++++ modules/vm/variables.tf | 24 +++++++ pipelines/apply.yml | 31 +++++++++ pipelines/plan.yml | 28 ++++++++ 21 files changed, 537 insertions(+) create mode 100644 .quai/tests/vm/create-dev-vm/after/envs/dev/vms/build-agent-02.yaml create mode 100644 .quai/tests/vm/create-dev-vm/request.yaml create mode 100644 .quai/tests/vm/resize-web01/after/envs/prod/vms/web01.yaml create mode 100644 .quai/tests/vm/resize-web01/request.yaml create mode 100644 .quai/types/vm.yaml create mode 100644 README.md create mode 100644 blueprints/app-servers.yaml create mode 100644 blueprints/linux-vm.yaml create mode 100644 catalog/schemas/vm.schema.json create mode 100644 envs/dev/main.tf create mode 100644 envs/dev/vms/build-agent-01.yaml create mode 100644 envs/prod/main.tf create mode 100644 envs/prod/vms/db01.yaml create mode 100644 envs/prod/vms/legacy-ftp.yaml create mode 100644 envs/prod/vms/web01.yaml create mode 100644 envs/test/main.tf create mode 100644 envs/test/vms/.gitkeep create mode 100644 modules/vm/main.tf create mode 100644 modules/vm/variables.tf create mode 100644 pipelines/apply.yml create mode 100644 pipelines/plan.yml diff --git a/.quai/tests/vm/create-dev-vm/after/envs/dev/vms/build-agent-02.yaml b/.quai/tests/vm/create-dev-vm/after/envs/dev/vms/build-agent-02.yaml new file mode 100644 index 0000000..7c4f29e --- /dev/null +++ b/.quai/tests/vm/create-dev-vm/after/envs/dev/vms/build-agent-02.yaml @@ -0,0 +1,10 @@ +owner: carol +description: Second CI build agent +template: ubuntu-24.04 +cpu: 4 +memory_gb: 8 +disks: + - size_gb: 100 +network: + vlan: 110 +backup: false diff --git a/.quai/tests/vm/create-dev-vm/request.yaml b/.quai/tests/vm/create-dev-vm/request.yaml new file mode 100644 index 0000000..cebb2c2 --- /dev/null +++ b/.quai/tests/vm/create-dev-vm/request.yaml @@ -0,0 +1,12 @@ +# A new VM in dev: the file lists its keys in the schema's order, with the requester as its owner. +kind: create +params: { env: dev, name: build-agent-02 } +owner: carol +values: + description: Second CI build agent + template: ubuntu-24.04 + cpu: 4 + memory_gb: 8 + disks: [{ size_gb: 100 }] + network: { vlan: 110 } + backup: false diff --git a/.quai/tests/vm/resize-web01/after/envs/prod/vms/web01.yaml b/.quai/tests/vm/resize-web01/after/envs/prod/vms/web01.yaml new file mode 100644 index 0000000..61b62a9 --- /dev/null +++ b/.quai/tests/vm/resize-web01/after/envs/prod/vms/web01.yaml @@ -0,0 +1,17 @@ +# Front-end for the shop. Pinned to VLAN 120 by the firewall team. +owner: bob +description: Shop front-end +template: ubuntu-24.04 +cpu: 8 # bumped in INC-4411 +memory_gb: 32 +disks: + - size_gb: 80 + tier: ssd +network: + vlan: 120 + ip: 10.0.12.11 +backup: true +backup_policy: daily +tags: + cost_center: "4711" + app: shop diff --git a/.quai/tests/vm/resize-web01/request.yaml b/.quai/tests/vm/resize-web01/request.yaml new file mode 100644 index 0000000..9c1697b --- /dev/null +++ b/.quai/tests/vm/resize-web01/request.yaml @@ -0,0 +1,5 @@ +# The resize action on web01: only cpu and memory_gb change; comments and layout stay. +kind: action +key: prod/web01 +action: resize +actionInputs: { cpu: 8, memory_gb: 32 } diff --git a/.quai/types/vm.yaml b/.quai/types/vm.yaml new file mode 100644 index 0000000..d1b8d03 --- /dev/null +++ b/.quai/types/vm.yaml @@ -0,0 +1,65 @@ +apiVersion: quai.dev/v1beta1 +kind: ResourceType +metadata: + name: vm + title: Virtual machine (Terraform) + description: One YAML file per VM under envs//vms/. Each environment is a Terraform stack that turns those files into module instances. + icon: server + category: Compute + version: 1.0.0 + tags: [terraform, brownfield] +spec: + locate: + strategy: file + path: envs/{env}/vms/{name}.yaml + params: + - name: env + title: Environment + type: enum + discover: true # offer the environments that exist in the repository + - name: name + title: VM name + description: Becomes the file name and the Terraform for_each key. + pattern: "[a-z][a-z0-9-]{1,30}" + # The contract CI validates every VM file against; the form is generated from it. + schema: catalog/schemas/vm.schema.json + generate: { kind: identity } + ownership: { field: owner } + stack: envs/{env} + address: module.vm["{name}"] + deletion: approval + ui: + order: [description, template, cpu, memory_gb, disks, network, backup, backup_policy, tags] + fields: + template: { immutable: true } + network.vlan: { immutable: true } + backup_policy: { visibleIf: "has(inputs.backup) && inputs.backup" } + approval: + rules: + - name: cost + when: params.env == 'prod' && plan.cost.monthlyDelta > 500 + approvers: [group:finops] + - name: production + when: params.env == 'prod' + approvers: [group:platform-admins] + - name: large-vm + # Only when a VM becomes large, not on every change to an already large one. + when: inputs.cpu > 8 && !(has(previous.cpu) && previous.cpu > 8) + approvers: [group:finops] + actions: + - name: resize + title: Resize + icon: maximize + inputs: + - { name: cpu, title: vCPUs, type: integer, min: 1, max: 32, required: true } + - { name: memory_gb, title: Memory (GB), type: integer, min: 1, max: 256, required: true } + set: + cpu: ${{ action.cpu }} + memory_gb: ${{ action.memory_gb }} + - name: extend-disk + title: Extend system disk + icon: hard-drive + inputs: + - { name: size_gb, title: Size (GB), type: integer, min: 10, max: 4096, required: true } + set: + disks.0.size_gb: ${{ action.size_gb }} diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc2f647 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Example repository + +An existing Terraform repository with Quai definitions added: the demo (the +compose stack seeds it into Gitea) and the fixture of the backend's tests and the +end-to-end scenarios. + +- `envs//vms/*.yaml`: one file per VM. Each environment is a Terraform + stack (`envs//main.tf`) that reads them with `fileset()` and + `yamldecode()` and instantiates `modules/vm` once per file. +- `catalog/schemas/vm.schema.json`: the contract between the VM files and the + module. CI validates every file against it; Quai generates its form from it. +- `.quai/types/vm.yaml`: what Quai needs to know about the layout: where VM + files live, which schema they follow, who owns them, which changes need + approval, and the day-2 actions. +- `blueprints/`: catalog items built from the `vm` type: a single VM sized from + t-shirt sizes, and a web server with its database server. +- `pipelines/`: Azure Pipelines that plan `envs/prod` on pull requests (as build + validation) and apply it on main, each reporting to Quai with the `quai report` + step template of `deploy/runners/azure-pipelines`. + +Quai edits the YAML files and nothing else. diff --git a/blueprints/app-servers.yaml b/blueprints/app-servers.yaml new file mode 100644 index 0000000..721f622 --- /dev/null +++ b/blueprints/app-servers.yaml @@ -0,0 +1,68 @@ +apiVersion: quai.dev/v1beta1 +kind: Blueprint +metadata: + name: app-servers + title: Application servers + description: A web server and its database server in one environment, requested and approved together. Built on the designer canvas. + icon: layers + category: Applications + version: 1.0.0 + owners: [team:platform] + tags: [terraform, vm] +spec: + inputs: + type: object + required: [environment, app] + additionalProperties: false + properties: + environment: { type: string, title: Environment, enum: [dev, test, prod], default: dev } + app: { type: string, title: Application, pattern: "^[a-z][a-z0-9]{1,20}$" } + webCpu: { type: integer, title: Web server vCPUs, minimum: 1, maximum: 16, default: 2 } + withDatabase: { type: boolean, title: Add a database server, default: true } + dbDiskGb: { type: integer, title: Database data disk (GB), minimum: 50, maximum: 4096, default: 200 } + ui: + order: [environment, app, webCpu, withDatabase, dbDiskGb] + fields: + dbDiskGb: { visibleIf: "inputs.withDatabase" } + resources: + - id: web + type: { name: vm } + params: + env: ${{ inputs.environment }} + name: ${{ inputs.app + '-web01' }} + values: + template: ubuntu-24.04 + cpu: ${{ inputs.webCpu }} + memory_gb: ${{ inputs.webCpu * 2 }} + disks: [{ size_gb: 50 }] + network: { vlan: 110 } + dependsOn: [db] + - id: db + when: inputs.withDatabase + type: { name: vm } + params: + env: ${{ inputs.environment }} + name: ${{ inputs.app + '-db01' }} + values: + template: rhel-9 + cpu: 4 + memory_gb: 16 + disks: + - { size_gb: 100, tier: ssd } + - { size_gb: "${{ inputs.dbDiskGb }}", tier: ssd } + network: { vlan: 130 } + backup: true + backup_policy: "${{ inputs.environment == 'prod' ? 'hourly' : 'daily' }}" + approval: + rules: + - name: production + when: inputs.environment == 'prod' + approvers: [group:platform-admins] + actions: + - name: scale-web + title: Scale the web server + icon: maximize + inputs: + - { name: cpu, title: vCPUs, type: integer, min: 1, max: 16, required: true } + apply: + web: { action: resize, inputs: { cpu: "${{ action.cpu }}", memory_gb: "${{ action.cpu * 2 }}" } } diff --git a/blueprints/linux-vm.yaml b/blueprints/linux-vm.yaml new file mode 100644 index 0000000..793b274 --- /dev/null +++ b/blueprints/linux-vm.yaml @@ -0,0 +1,66 @@ +apiVersion: quai.dev/v1beta1 +kind: Blueprint +metadata: + name: linux-vm + title: Linux virtual machine + description: A VM in the Terraform repository, sized from t-shirt sizes. The vRA classic, now a pull request. + icon: server + category: Compute + version: 2.0.0 + owners: [team:platform] + tags: [terraform, vm] +spec: + inputs: + type: object + required: [environment, name, os, size] + additionalProperties: false + properties: + environment: { type: string, title: Environment, enum: [dev, test, prod], default: dev } + name: { type: string, title: VM name, pattern: "^[a-z][a-z0-9-]{1,30}$" } + os: + type: string + title: Operating system + enum: [ubuntu-24.04, rhel-9] + default: ubuntu-24.04 + size: + type: string + title: Size + description: small = 2 vCPU / 4 GB, medium = 4 / 16, large = 16 / 64 + enum: [small, medium, large] + default: small + diskGb: { type: integer, title: System disk (GB), minimum: 20, maximum: 2048, default: 50 } + vlan: { type: integer, title: VLAN, enum: [110, 120, 130], default: 110 } + ui: + order: [environment, name, os, size, diskGb, vlan] + fields: + os: { immutable: true } + resources: + - id: vm + type: { name: vm } # a type in this repository + params: + env: ${{ inputs.environment }} + name: ${{ inputs.name }} + values: + template: ${{ inputs.os }} + cpu: "${{ inputs.size == 'small' ? 2 : inputs.size == 'medium' ? 4 : 16 }}" + memory_gb: "${{ inputs.size == 'small' ? 4 : inputs.size == 'medium' ? 16 : 64 }}" + disks: [{ size_gb: "${{ inputs.diskGb }}", tier: standard }] + network: { vlan: "${{ inputs.vlan }}" } + backup: ${{ inputs.environment == 'prod' }} + approval: + rules: + - name: large-vm-cost + when: inputs.size == 'large' + approvers: [group:finops] + actions: + - name: resize + title: Resize + icon: maximize + description: Change the VM size. The VM restarts to apply it. + inputs: + - { name: size, title: New size, type: enum, options: [small, medium, large], required: true } + apply: + vm: + values: + cpu: "${{ action.size == 'small' ? 2 : action.size == 'medium' ? 4 : 16 }}" + memory_gb: "${{ action.size == 'small' ? 4 : action.size == 'medium' ? 16 : 64 }}" diff --git a/catalog/schemas/vm.schema.json b/catalog/schemas/vm.schema.json new file mode 100644 index 0000000..6ac8abd --- /dev/null +++ b/catalog/schemas/vm.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/schemas/vm.schema.json", + "title": "Virtual machine", + "description": "Contract between the VM files in envs/*/vms/ and modules/vm. CI validates every file against it; Quai renders its form from it.", + "type": "object", + "required": ["template", "cpu", "memory_gb", "disks", "network"], + "additionalProperties": false, + "properties": { + "owner": { "type": "string", "title": "Owner" }, + "description": { "type": "string", "title": "Description", "maxLength": 200 }, + "template": { + "type": "string", + "title": "OS template", + "enum": ["ubuntu-24.04", "rhel-9", "windows-2022"], + "default": "ubuntu-24.04" + }, + "cpu": { "type": "integer", "title": "vCPUs", "minimum": 1, "maximum": 32, "default": 2 }, + "memory_gb": { "type": "integer", "title": "Memory (GB)", "minimum": 1, "maximum": 256, "default": 4 }, + "disks": { + "type": "array", + "title": "Disks", + "minItems": 1, + "maxItems": 8, + "items": { "$ref": "#/$defs/disk" }, + "default": [{ "size_gb": 50, "tier": "standard" }] + }, + "network": { + "type": "object", + "title": "Network", + "required": ["vlan"], + "additionalProperties": false, + "properties": { + "vlan": { "type": "integer", "title": "VLAN", "enum": [110, 120, 130], "default": 110 }, + "ip": { "type": "string", "title": "Static IP", "format": "ipv4", "description": "Leave empty for DHCP." } + } + }, + "backup": { "type": "boolean", "title": "Backup", "default": true }, + "backup_policy": { "type": "string", "title": "Backup policy", "enum": ["daily", "hourly"], "default": "daily" }, + "tags": { + "type": "object", + "title": "Tags", + "additionalProperties": { "type": "string" } + } + }, + "$defs": { + "disk": { + "type": "object", + "required": ["size_gb"], + "additionalProperties": false, + "properties": { + "size_gb": { "type": "integer", "title": "Size (GB)", "minimum": 10, "maximum": 4096 }, + "tier": { "type": "string", "title": "Tier", "enum": ["standard", "ssd"], "default": "standard" } + } + } + } +} diff --git a/envs/dev/main.tf b/envs/dev/main.tf new file mode 100644 index 0000000..0c84f37 --- /dev/null +++ b/envs/dev/main.tf @@ -0,0 +1,22 @@ +# Stack for the dev environment. Every file in vms/ becomes one module +# instance, keyed by its file name. Quai only ever edits the YAML. +locals { + vm_files = fileset("${path.module}/vms", "*.yaml") + vms = { for f in local.vm_files : trimsuffix(f, ".yaml") => yamldecode(file("${path.module}/vms/${f}")) } +} + +module "vm" { + source = "../../modules/vm" + for_each = local.vms + + name = each.key + environment = "dev" + template = each.value.template + cpu = each.value.cpu + memory_gb = each.value.memory_gb + disks = each.value.disks + vlan = each.value.network.vlan + ip = try(each.value.network.ip, null) + backup_policy = try(each.value.backup, false) ? try(each.value.backup_policy, "daily") : null + tags = merge(try(each.value.tags, {}), { owner = try(each.value.owner, "unknown") }) +} diff --git a/envs/dev/vms/build-agent-01.yaml b/envs/dev/vms/build-agent-01.yaml new file mode 100644 index 0000000..81707bf --- /dev/null +++ b/envs/dev/vms/build-agent-01.yaml @@ -0,0 +1,10 @@ +owner: bob +description: CI build agent +template: ubuntu-24.04 +cpu: 8 +memory_gb: 16 +disks: + - size_gb: 100 +network: + vlan: 110 +backup: false diff --git a/envs/prod/main.tf b/envs/prod/main.tf new file mode 100644 index 0000000..2c4acfb --- /dev/null +++ b/envs/prod/main.tf @@ -0,0 +1,22 @@ +# Stack for the prod environment. Every file in vms/ becomes one module +# instance, keyed by its file name. Quai only ever edits the YAML. +locals { + vm_files = fileset("${path.module}/vms", "*.yaml") + vms = { for f in local.vm_files : trimsuffix(f, ".yaml") => yamldecode(file("${path.module}/vms/${f}")) } +} + +module "vm" { + source = "../../modules/vm" + for_each = local.vms + + name = each.key + environment = "prod" + template = each.value.template + cpu = each.value.cpu + memory_gb = each.value.memory_gb + disks = each.value.disks + vlan = each.value.network.vlan + ip = try(each.value.network.ip, null) + backup_policy = try(each.value.backup, false) ? try(each.value.backup_policy, "daily") : null + tags = merge(try(each.value.tags, {}), { owner = try(each.value.owner, "unknown") }) +} diff --git a/envs/prod/vms/db01.yaml b/envs/prod/vms/db01.yaml new file mode 100644 index 0000000..cc5668f --- /dev/null +++ b/envs/prod/vms/db01.yaml @@ -0,0 +1,13 @@ +owner: dana +description: Orders database (PostgreSQL) +template: rhel-9 +cpu: 12 +memory_gb: 64 +disks: + - size_gb: 100 + tier: ssd + - size_gb: 1000 # data volume + tier: ssd +network: { vlan: 130, ip: 10.0.13.20 } +backup: true +backup_policy: hourly diff --git a/envs/prod/vms/legacy-ftp.yaml b/envs/prod/vms/legacy-ftp.yaml new file mode 100644 index 0000000..813ba8b --- /dev/null +++ b/envs/prod/vms/legacy-ftp.yaml @@ -0,0 +1,10 @@ +# Created before Quai. No owner on record: only platform admins can change it. +description: Partner file drop (to be retired) +template: rhel-9 +cpu: 2 +memory_gb: 4 +disks: + - size_gb: 200 +network: + vlan: 110 +backup: false diff --git a/envs/prod/vms/web01.yaml b/envs/prod/vms/web01.yaml new file mode 100644 index 0000000..c8e66d7 --- /dev/null +++ b/envs/prod/vms/web01.yaml @@ -0,0 +1,17 @@ +# Front-end for the shop. Pinned to VLAN 120 by the firewall team. +owner: bob +description: Shop front-end +template: ubuntu-24.04 +cpu: 4 # bumped in INC-4411 +memory_gb: 16 +disks: + - size_gb: 80 + tier: ssd +network: + vlan: 120 + ip: 10.0.12.11 +backup: true +backup_policy: daily +tags: + cost_center: "4711" + app: shop diff --git a/envs/test/main.tf b/envs/test/main.tf new file mode 100644 index 0000000..75a0020 --- /dev/null +++ b/envs/test/main.tf @@ -0,0 +1,22 @@ +# Stack for the test environment. Every file in vms/ becomes one module +# instance, keyed by its file name. Quai only ever edits the YAML. +locals { + vm_files = fileset("${path.module}/vms", "*.yaml") + vms = { for f in local.vm_files : trimsuffix(f, ".yaml") => yamldecode(file("${path.module}/vms/${f}")) } +} + +module "vm" { + source = "../../modules/vm" + for_each = local.vms + + name = each.key + environment = "test" + template = each.value.template + cpu = each.value.cpu + memory_gb = each.value.memory_gb + disks = each.value.disks + vlan = each.value.network.vlan + ip = try(each.value.network.ip, null) + backup_policy = try(each.value.backup, false) ? try(each.value.backup_policy, "daily") : null + tags = merge(try(each.value.tags, {}), { owner = try(each.value.owner, "unknown") }) +} diff --git a/envs/test/vms/.gitkeep b/envs/test/vms/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/modules/vm/main.tf b/modules/vm/main.tf new file mode 100644 index 0000000..411f6e5 --- /dev/null +++ b/modules/vm/main.tf @@ -0,0 +1,17 @@ +# Illustrative only: swap in vsphere_virtual_machine, proxmox_vm_qemu, +# kubevirt_virtual_machine or a cloud provider's instance resource. +resource "terraform_data" "vm" { + input = { + name = "${var.environment}-${var.name}" + template = var.template + cpu = var.cpu + memory_mb = var.memory_gb * 1024 + disks = var.disks + vlan = var.vlan + ip = var.ip + backup_policy = var.backup_policy + tags = var.tags + } +} + +output "id" { value = terraform_data.vm.id } diff --git a/modules/vm/variables.tf b/modules/vm/variables.tf new file mode 100644 index 0000000..064efeb --- /dev/null +++ b/modules/vm/variables.tf @@ -0,0 +1,24 @@ +variable "name" { type = string } +variable "environment" { type = string } +variable "template" { type = string } +variable "cpu" { type = number } +variable "memory_gb" { type = number } +variable "disks" { + type = list(object({ + size_gb = number + tier = optional(string, "standard") + })) +} +variable "vlan" { type = number } +variable "ip" { + type = string + default = null +} +variable "backup_policy" { + type = string + default = null +} +variable "tags" { + type = map(string) + default = {} +} diff --git a/pipelines/apply.yml b/pipelines/apply.yml new file mode 100644 index 0000000..b1b1a7f --- /dev/null +++ b/pipelines/apply.yml @@ -0,0 +1,31 @@ +# Applies envs/prod when main changes and reports the apply to Quai: spec.runner.pipelines +# names it (kind: apply), and requests merged to main wait for it. +trigger: + branches: + include: [main] + paths: + include: [envs/prod, modules] +pr: none + +resources: + repositories: + - repository: quai + type: github + name: damhau/quai + endpoint: github # a GitHub service connection + +variables: + - group: quai # quaiUrl, quaiRepository and the secret quaiRunToken + +pool: + vmImage: ubuntu-latest + +steps: + - checkout: self + - script: | + set -euo pipefail + terraform -chdir=envs/prod init -input=false + terraform -chdir=envs/prod apply -input=false -auto-approve + displayName: terraform apply + - template: deploy/runners/azure-pipelines/quai-report.yml@quai + parameters: { stack: envs/prod, kind: apply } diff --git a/pipelines/plan.yml b/pipelines/plan.yml new file mode 100644 index 0000000..36b1ced --- /dev/null +++ b/pipelines/plan.yml @@ -0,0 +1,28 @@ +# Plans envs/prod on each pull request to main and reports the plan to Quai: a "Build +# validation" branch policy runs it, and spec.runner.pipelines names it (kind: plan). +trigger: none +pr: none # run by the branch policy + +resources: + repositories: + - repository: quai + type: github + name: damhau/quai + endpoint: github # a GitHub service connection + +variables: + - group: quai # quaiUrl, quaiRepository and the secret quaiRunToken + +pool: + vmImage: ubuntu-latest + +steps: + - checkout: self + - script: | + set -euo pipefail + terraform -chdir=envs/prod init -input=false + terraform -chdir=envs/prod plan -input=false -out=tfplan + terraform -chdir=envs/prod show -json tfplan > plan.json + displayName: terraform plan + - template: deploy/runners/azure-pipelines/quai-report.yml@quai + parameters: { stack: envs/prod, kind: plan, plan: plan.json }