From 6d019ff79336008037fc95db690e73212c8c9616 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Thu, 9 Feb 2023 13:37:03 -0500 Subject: [PATCH 1/8] vSphere terraform unification This commit unifies zonal and non-zonal vsphere terraform projects. Removes non-zonal terraform, zonal terraform replaces it. Removes zonal terraform stage Reduces the required config and terraform var struct fields --- .../install.openshift.io_installconfigs.yaml | 44 ++++--- data/data/vsphere/bootstrap/main.tf | 62 +++++----- data/data/vsphere/bootstrap/variables.tf | 30 ++--- data/data/vsphere/master/main.tf | 58 ++++++--- data/data/vsphere/master/outputs.tf | 4 +- data/data/vsphere/master/variables.tf | 29 ++--- data/data/vsphere/pre-bootstrap/main.tf | 95 +++++++++------ data/data/vsphere/pre-bootstrap/outputs.tf | 24 +--- data/data/vsphere/variables-vsphere.tf | 87 +++---------- data/data/vspherezoning/OWNERS | 7 -- data/data/vspherezoning/bootstrap/main.tf | 50 -------- data/data/vspherezoning/bootstrap/outputs.tf | 7 -- .../data/vspherezoning/bootstrap/variables.tf | 38 ------ data/data/vspherezoning/master/main.tf | 70 ----------- data/data/vspherezoning/master/outputs.tf | 7 -- data/data/vspherezoning/master/variables.tf | 25 ---- data/data/vspherezoning/pre-bootstrap/main.tf | 112 ----------------- .../vspherezoning/pre-bootstrap/outputs.tf | 27 ----- .../vspherezoning/variables-vspherezoning.tf | 114 ------------------ pkg/asset/cluster/tfvars.go | 50 +++----- pkg/destroy/bootstrap/bootstrap.go | 7 -- pkg/terraform/stages/platform/stages.go | 2 - pkg/terraform/stages/vsphere/stages.go | 31 +---- pkg/tfvars/vsphere/vsphere.go | 97 ++++----------- pkg/types/vsphere/doc.go | 3 - 25 files changed, 233 insertions(+), 847 deletions(-) delete mode 100644 data/data/vspherezoning/OWNERS delete mode 100644 data/data/vspherezoning/bootstrap/main.tf delete mode 100644 data/data/vspherezoning/bootstrap/outputs.tf delete mode 100644 data/data/vspherezoning/bootstrap/variables.tf delete mode 100644 data/data/vspherezoning/master/main.tf delete mode 100644 data/data/vspherezoning/master/outputs.tf delete mode 100644 data/data/vspherezoning/master/variables.tf delete mode 100644 data/data/vspherezoning/pre-bootstrap/main.tf delete mode 100644 data/data/vspherezoning/pre-bootstrap/outputs.tf delete mode 100644 data/data/vspherezoning/variables-vspherezoning.tf diff --git a/data/data/install.openshift.io_installconfigs.yaml b/data/data/install.openshift.io_installconfigs.yaml index 7d6cd00e6c0..a097707ea22 100644 --- a/data/data/install.openshift.io_installconfigs.yaml +++ b/data/data/install.openshift.io_installconfigs.yaml @@ -3202,20 +3202,20 @@ spec: type: array uniqueItems: true cluster: - description: Cluster is the name of the cluster virtual machines - will be cloned into. + description: 'Cluster is the name of the cluster virtual machines + will be cloned into. Deprecated: Use FailureDomains.Topology.Cluster' type: string clusterOSImage: description: ClusterOSImage overrides the url provided in rhcos.json to download the RHCOS OVA type: string datacenter: - description: Datacenter is the name of the datacenter to use in - the vCenter. + description: 'Datacenter is the name of the datacenter to use + in the vCenter. Deprecated: Use FailureDomains.Topology.Datacenter' type: string defaultDatastore: - description: DefaultDatastore is the default datastore to use - for provisioning volumes. + description: 'DefaultDatastore is the default datastore to use + for provisioning volumes. Deprecated: Use FailureDomains.Topology.Datastore' type: string defaultMachinePlatform: description: DefaultMachinePlatform is the default configuration @@ -3273,7 +3273,7 @@ spec: properties: name: description: name defines the name of the FailureDomain - This name is abritrary but will be used in VSpherePlatformDeploymentZone + This name is arbitrary but will be used in VSpherePlatformDeploymentZone for association. maxLength: 256 minLength: 1 @@ -3352,9 +3352,10 @@ spec: type: object type: array folder: - description: Folder is the absolute path of the folder that will + description: 'Folder is the absolute path of the folder that will be used and/or created for virtual machines. The absolute path - is of the form //vm//. + is of the form //vm//. Deprecated: + Use FailureDomains.Topology.Folder' pattern: ^/.*?/vm/.*? type: string ingressVIP: @@ -3372,24 +3373,26 @@ spec: type: array uniqueItems: true network: - description: Network specifies the name of the network to be used - by the cluster. + description: 'Network specifies the name of the network to be + used by the cluster. Deprecated: Use FailureDomains.Topology.Network' type: string password: - description: Password is the password for the user to use to connect - to the vCenter. + description: 'Password is the password for the user to use to + connect to the vCenter. Deprecated: Use VCenters.Password' type: string resourcePool: - description: ResourcePool is the absolute path of the resource + description: 'ResourcePool is the absolute path of the resource pool where virtual machines will be created. The absolute path is of the form //host//Resources/. + Deprecated: Use FailureDomains.Topology.ResourcePool' type: string username: - description: Username is the name of the user to use to connect - to the vCenter. + description: 'Username is the name of the user to use to connect + to the vCenter. Deprecated: Use VCenters.Username' type: string vCenter: - description: VCenter is the domain name or IP address of the vCenter. + description: 'VCenter is the domain name or IP address of the + vCenter. Deprecated: Use VCenters.Server' type: string vcenters: description: VCenters holds the connection details for services @@ -3413,6 +3416,7 @@ spec: description: port is the TCP port that will be used to communicate to the vCenter endpoint. This is typically unchanged from the default of HTTPS TCP/443. + format: int32 maximum: 32767 minimum: 1 type: integer @@ -3434,12 +3438,6 @@ spec: maxItems: 1 minItems: 1 type: array - required: - - datacenter - - defaultDatastore - - password - - username - - vCenter type: object type: object proxy: diff --git a/data/data/vsphere/bootstrap/main.tf b/data/data/vsphere/bootstrap/main.tf index 6b1dd3f3228..0cd3ed2d9ed 100644 --- a/data/data/vsphere/bootstrap/main.tf +++ b/data/data/vsphere/bootstrap/main.tf @@ -1,54 +1,58 @@ locals { description = "Created By OpenShift Installer" + vcenter_key = keys(var.vsphere_vcenters)[0] } provider "vsphere" { - user = var.vsphere_username - password = var.vsphere_password - vsphere_server = var.vsphere_url + user = var.vsphere_vcenters[local.vcenter_key].user + password = var.vsphere_vcenters[local.vcenter_key].password + vsphere_server = var.vsphere_vcenters[local.vcenter_key].server allow_unverified_ssl = false } resource "vsphere_virtual_machine" "vm_bootstrap" { - name = "${var.cluster_id}-bootstrap" - resource_pool_id = var.resource_pool - datastore_id = var.datastore - num_cpus = 4 - memory = 16384 - guest_id = var.guest_id - folder = var.folder - enable_disk_uuid = "true" - annotation = local.description - - wait_for_guest_net_timeout = 0 - wait_for_guest_net_routable = false + name = "${var.cluster_id}-bootstrap" + resource_pool_id = var.resource_pool[0].id + datastore_id = var.datastore[0].id + num_cpus = var.vsphere_control_planes[0].numCPUs + num_cores_per_socket = var.vsphere_control_planes[0].numCoresPerSocket + memory = var.vsphere_control_planes[0].memoryMiB + guest_id = var.template[0].guest_id + folder = trimprefix(var.vsphere_control_planes[0].workspace.folder, "/${var.vsphere_control_planes[0].workspace.datacenter}/vm") + enable_disk_uuid = "true" + annotation = local.description + wait_for_guest_net_timeout = "0" + wait_for_guest_net_routable = "false" + tags = var.tags network_interface { - network_id = var.vsphere_network + network_id = var.template[0].network_interfaces.0.network_id } disk { - label = "disk0" - size = 120 - eagerly_scrub = var.scrub_disk - thin_provisioned = var.thin_disk - } - lifecycle { - ignore_changes = [ - disk[0].eagerly_scrub, - ] - } + label = "disk0" + size = var.vsphere_control_planes[0].diskGiB + eagerly_scrub = var.template[0].disks.0.eagerly_scrub + thin_provisioned = var.template[0].disks.0.thin_provisioned + } clone { - template_uuid = var.template + template_uuid = var.template[0].uuid } extra_config = { "guestinfo.ignition.config.data" = base64encode(var.ignition_bootstrap) "guestinfo.ignition.config.data.encoding" = "base64" "guestinfo.hostname" = "${var.cluster_id}-bootstrap" - "guestinfo.domain" = "${var.cluster_domain}" + "stealclock.enable" = "TRUE" + } + + // Potential issues on destroy if disk type changes + // underneath terraform. + lifecycle { + ignore_changes = [ + disk[0], + ] } - tags = var.tags } diff --git a/data/data/vsphere/bootstrap/variables.tf b/data/data/vsphere/bootstrap/variables.tf index ac3dd71abd9..0957d9c7b82 100644 --- a/data/data/vsphere/bootstrap/variables.tf +++ b/data/data/vsphere/bootstrap/variables.tf @@ -1,5 +1,6 @@ variable "resource_pool" { - type = string + type = list(any) + default = [] } variable "bootstrap_moid" { @@ -17,34 +18,21 @@ variable "control_plane_moids" { default = [] } -variable "folder" { - type = string -} - variable "datastore" { - type = string + type = list(any) + default = [] } variable "datacenter" { - type = string + type = list(any) + default = [] } variable "template" { - type = string -} - -variable "guest_id" { - type = string + type = list(any) + default = [] } variable "tags" { - type = list -} - -variable "thin_disk" { - type = bool -} - -variable "scrub_disk" { - type = bool + type = list(any) } diff --git a/data/data/vsphere/master/main.tf b/data/data/vsphere/master/main.tf index ae19012ca30..c523534072e 100644 --- a/data/data/vsphere/master/main.tf +++ b/data/data/vsphere/master/main.tf @@ -1,44 +1,64 @@ locals { - description = "Created By OpenShift Installer" + description = "Created By OpenShift Installer" + vcenter_key = keys(var.vsphere_vcenters)[0] + template_map = { for t in var.template : t.name => t } } provider "vsphere" { - user = var.vsphere_username - password = var.vsphere_password - vsphere_server = var.vsphere_url + user = var.vsphere_vcenters[local.vcenter_key].user + password = var.vsphere_vcenters[local.vcenter_key].password + vsphere_server = var.vsphere_vcenters[local.vcenter_key].server allow_unverified_ssl = false } +data "vsphere_datacenter" "datacenter" { + count = var.master_count + name = var.vsphere_control_planes[count.index].workspace.datacenter +} + +data "vsphere_resource_pool" "resource_pool" { + count = var.master_count + name = var.vsphere_control_planes[count.index].workspace.resourcePool +} + +data "vsphere_datastore" "datastore" { + count = var.master_count + name = var.vsphere_control_planes[count.index].workspace.datastore + datacenter_id = data.vsphere_datacenter.datacenter[count.index].id +} + resource "vsphere_virtual_machine" "vm_master" { count = var.master_count name = "${var.cluster_id}-master-${count.index}" - resource_pool_id = var.resource_pool - datastore_id = var.datastore - num_cpus = var.vsphere_control_plane_num_cpus - num_cores_per_socket = var.vsphere_control_plane_cores_per_socket - memory = var.vsphere_control_plane_memory_mib - guest_id = var.guest_id - folder = var.folder - enable_disk_uuid = "true" - annotation = local.description + resource_pool_id = data.vsphere_resource_pool.resource_pool[count.index].id + datastore_id = data.vsphere_datastore.datastore[count.index].id + num_cpus = var.vsphere_control_planes[0].numCPUs + num_cores_per_socket = var.vsphere_control_planes[0].numCoresPerSocket + memory = var.vsphere_control_planes[0].memoryMiB + folder = trimprefix(var.vsphere_control_planes[count.index].workspace.folder, "/${var.vsphere_control_planes[count.index].workspace.datacenter}/vm") + guest_id = local.template_map[var.vsphere_control_planes[count.index].template].guest_id + + enable_disk_uuid = "true" + annotation = local.description wait_for_guest_net_timeout = "0" wait_for_guest_net_routable = "false" + tags = var.tags network_interface { - network_id = var.vsphere_network + network_id = local.template_map[var.vsphere_control_planes[count.index].template].network_interfaces.0.network_id } disk { label = "disk0" - size = var.vsphere_control_plane_disk_gib - eagerly_scrub = var.scrub_disk - thin_provisioned = var.thin_disk + size = var.vsphere_control_planes[0].diskGiB + eagerly_scrub = local.template_map[var.vsphere_control_planes[count.index].template].disks.0.eagerly_scrub + thin_provisioned = local.template_map[var.vsphere_control_planes[count.index].template].disks.0.thin_provisioned } clone { - template_uuid = var.template + template_uuid = local.template_map[var.vsphere_control_planes[count.index].template].uuid } extra_config = { @@ -47,6 +67,4 @@ resource "vsphere_virtual_machine" "vm_master" { "guestinfo.hostname" = "${var.cluster_id}-master-${count.index}" "stealclock.enable" = "TRUE" } - - tags = var.tags } diff --git a/data/data/vsphere/master/outputs.tf b/data/data/vsphere/master/outputs.tf index 63d699a68cc..4b635c65144 100644 --- a/data/data/vsphere/master/outputs.tf +++ b/data/data/vsphere/master/outputs.tf @@ -1,7 +1,7 @@ output "control_plane_ips" { - value = vsphere_virtual_machine.vm_master.*.default_ip_address + value = [for k, v in vsphere_virtual_machine.vm_master : v.default_ip_address] } output "control_plane_moids" { - value = vsphere_virtual_machine.vm_master.*.moid + value = [for k, v in vsphere_virtual_machine.vm_master : v.moid] } diff --git a/data/data/vsphere/master/variables.tf b/data/data/vsphere/master/variables.tf index 08ba43525ce..c8a32222756 100644 --- a/data/data/vsphere/master/variables.tf +++ b/data/data/vsphere/master/variables.tf @@ -1,9 +1,5 @@ variable "resource_pool" { - type = string -} - -variable "folder" { - type = string + type = list(any) } variable "bootstrap_moid" { @@ -12,29 +8,18 @@ variable "bootstrap_moid" { } variable "datastore" { - type = string + type = list(any) } variable "datacenter" { - type = string -} - -variable "template" { - type = string -} - -variable "guest_id" { - type = string + type = list(any) } variable "tags" { - type = list + type = list(any) } -variable "thin_disk" { - type = bool -} - -variable "scrub_disk" { - type = bool +variable "template" { + type = list(any) + default = [] } diff --git a/data/data/vsphere/pre-bootstrap/main.tf b/data/data/vsphere/pre-bootstrap/main.tf index abd95ab88d8..734bd4af3ca 100644 --- a/data/data/vsphere/pre-bootstrap/main.tf +++ b/data/data/vsphere/pre-bootstrap/main.tf @@ -1,56 +1,92 @@ locals { - folder = var.vsphere_preexisting_folder ? var.vsphere_folder : vsphere_folder.folder[0].path - description = "Created By OpenShift Installer" + description = "Created By OpenShift Installer" + vcenter_key = keys(var.vsphere_vcenters)[0] + failure_domains_count = length(var.vsphere_failure_domains) } provider "vsphere" { - user = var.vsphere_username - password = var.vsphere_password - vsphere_server = var.vsphere_url + user = var.vsphere_vcenters[local.vcenter_key].user + password = var.vsphere_vcenters[local.vcenter_key].password + vsphere_server = var.vsphere_vcenters[local.vcenter_key].server allow_unverified_ssl = false } provider "vsphereprivate" { - user = var.vsphere_username - password = var.vsphere_password - vsphere_server = var.vsphere_url + user = var.vsphere_vcenters[local.vcenter_key].user + password = var.vsphere_vcenters[local.vcenter_key].password + vsphere_server = var.vsphere_vcenters[local.vcenter_key].server allow_unverified_ssl = false } data "vsphere_datacenter" "datacenter" { - name = var.vsphere_datacenter + count = local.failure_domains_count + name = var.vsphere_failure_domains[count.index].topology.datacenter } data "vsphere_compute_cluster" "cluster" { - name = var.vsphere_cluster - datacenter_id = data.vsphere_datacenter.datacenter.id + count = local.failure_domains_count + name = var.vsphere_failure_domains[count.index].topology.computeCluster + datacenter_id = data.vsphere_datacenter.datacenter[count.index].id } data "vsphere_resource_pool" "resource_pool" { - name = var.vsphere_resource_pool + count = local.failure_domains_count + name = var.vsphere_failure_domains[count.index].topology.resourcePool } data "vsphere_datastore" "datastore" { - name = var.vsphere_datastore - datacenter_id = data.vsphere_datacenter.datacenter.id + count = local.failure_domains_count + name = var.vsphere_failure_domains[count.index].topology.datastore + datacenter_id = data.vsphere_datacenter.datacenter[count.index].id } data "vsphere_virtual_machine" "template" { - name = vsphereprivate_import_ova.import.name - datacenter_id = data.vsphere_datacenter.datacenter.id + count = local.failure_domains_count + name = vsphereprivate_import_ova.import[count.index].name + datacenter_id = data.vsphere_datacenter.datacenter[count.index].id +} + +// Why is there two datacenters? +// The vm folder object is defined at the datacenter +// level. Each failure domain has a datacenter folder pair/ +// We need to get only the unique datacenter-folder pair +// and create those folders. See vsphere.go +// createDatacenterFolderMap + +data "vsphere_datacenter" "folder_datacenter" { + for_each = var.vsphere_folders + name = each.value.vsphere_datacenter +} + +resource "vsphere_folder" "folder" { + for_each = var.vsphere_folders + + path = each.value.name + type = "vm" + datacenter_id = data.vsphere_datacenter.folder_datacenter[each.key].id + tags = [vsphere_tag.tag.id] } resource "vsphereprivate_import_ova" "import" { - name = var.vsphere_template + count = local.failure_domains_count + name = format("%s-rhcos-%s-%s", var.cluster_id, var.vsphere_failure_domains[count.index].region, var.vsphere_failure_domains[count.index].zone) + filename = var.vsphere_ova_filepath - cluster = var.vsphere_cluster - resource_pool = var.vsphere_resource_pool - datacenter = var.vsphere_datacenter - datastore = var.vsphere_datastore - network = var.vsphere_network - folder = local.folder - tag = vsphere_tag.tag.id - disk_type = var.vsphere_disk_type + cluster = data.vsphere_compute_cluster.cluster[count.index].name + resource_pool = data.vsphere_resource_pool.resource_pool[count.index].name + datacenter = data.vsphere_datacenter.datacenter[count.index].name + datastore = data.vsphere_datastore.datastore[count.index].name + + network = var.vsphere_networks[var.vsphere_failure_domains[count.index].name] + folder = var.vsphere_failure_domains[count.index].topology.folder + tag = vsphere_tag.tag.id + disk_type = var.vsphere_disk_type + + // Since the folder resource might not be ran because there could be + // user defined folder per failure domain if a folder is created + // the import resource is not waiting. Adding + // this depends_on so the import happens after creating folder(s). + depends_on = [vsphere_folder.folder] } resource "vsphere_tag_category" "category" { @@ -72,12 +108,3 @@ resource "vsphere_tag" "tag" { category_id = vsphere_tag_category.category.id description = "Added by openshift-install do not remove" } - -resource "vsphere_folder" "folder" { - count = var.vsphere_preexisting_folder ? 0 : 1 - - path = var.vsphere_folder - type = "vm" - datacenter_id = data.vsphere_datacenter.datacenter.id - tags = [vsphere_tag.tag.id] -} diff --git a/data/data/vsphere/pre-bootstrap/outputs.tf b/data/data/vsphere/pre-bootstrap/outputs.tf index ded766afcf3..faabdfa3c81 100644 --- a/data/data/vsphere/pre-bootstrap/outputs.tf +++ b/data/data/vsphere/pre-bootstrap/outputs.tf @@ -1,33 +1,17 @@ output "resource_pool" { - value = data.vsphere_resource_pool.resource_pool.id + value = data.vsphere_resource_pool.resource_pool } output "datastore" { - value = data.vsphere_datastore.datastore.id -} - -output "folder" { - value = local.folder + value = data.vsphere_datastore.datastore } output "datacenter" { - value = data.vsphere_datacenter.datacenter.id + value = data.vsphere_datacenter.datacenter } output "template" { - value = data.vsphere_virtual_machine.template.id -} - -output "guest_id" { - value = data.vsphere_virtual_machine.template.guest_id -} - -output "thin_disk" { - value = data.vsphere_virtual_machine.template.disks.0.thin_provisioned -} - -output "scrub_disk" { - value = data.vsphere_virtual_machine.template.disks.0.eagerly_scrub + value = data.vsphere_virtual_machine.template } output "cluster_domain" { diff --git a/data/data/vsphere/variables-vsphere.tf b/data/data/vsphere/variables-vsphere.tf index 8b5f08e20cd..52266b489bb 100644 --- a/data/data/vsphere/variables-vsphere.tf +++ b/data/data/vsphere/variables-vsphere.tf @@ -1,86 +1,33 @@ -////// -// vSphere variables -////// - -variable "vsphere_url" { - type = string - description = "This is the vSphere server for the environment." -} - -variable "vsphere_username" { - type = string - description = "vSphere server user for the environment." +variable "vsphere_disk_type" { + type = string } -variable "vsphere_password" { - type = string - description = "vSphere server password" +variable "vsphere_vcenters" { + type = map(any) + default = {} } -variable "vsphere_cluster" { - type = string - description = "This is the name of the vSphere cluster." +variable "vsphere_networks" { + type = map(any) + default = {} } -variable "vsphere_resource_pool" { - type = string - description = "This is the absolute path to the vSphere resource pool." +variable "vsphere_folders" { + type = map(any) + default = {} } -variable "vsphere_datacenter" { - type = string - description = "This is the name of the vSphere data center." +variable "vsphere_control_planes" { + type = list(any) + default = [] } -variable "vsphere_datastore" { - type = string - description = "This is the name of the vSphere data store." +variable "vsphere_failure_domains" { + type = list(any) + default = [] } variable "vsphere_ova_filepath" { type = string description = "This is the filepath to the ova file that will be imported into vSphere." } - -variable "vsphere_template" { - type = string - description = "This is the name of the VM template to clone." -} - -variable "vsphere_network" { - type = string - description = "This is the Managed Object ID of the publicly accessible network for cluster ingress and access." -} - -variable "vsphere_folder" { - type = string - description = "The relative path to the folder which should be used or created for VMs." -} - -variable "vsphere_preexisting_folder" { - type = bool - description = "If false, creates a top-level folder with the name from vsphere_folder_rel_path." -} - -/////////// -// Control Plane machine variables -/////////// - -variable "vsphere_control_plane_memory_mib" { - type = number -} - -variable "vsphere_control_plane_disk_gib" { - type = number -} - -variable "vsphere_control_plane_num_cpus" { - type = number -} - -variable "vsphere_control_plane_cores_per_socket" { - type = number -} -variable "vsphere_disk_type" { - type = string -} diff --git a/data/data/vspherezoning/OWNERS b/data/data/vspherezoning/OWNERS deleted file mode 100644 index d1c39b1b668..00000000000 --- a/data/data/vspherezoning/OWNERS +++ /dev/null @@ -1,7 +0,0 @@ -# See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md -# This file just uses aliases defined in OWNERS_ALIASES. - -approvers: - - vsphere-approvers -reviewers: - - vsphere-reviewers diff --git a/data/data/vspherezoning/bootstrap/main.tf b/data/data/vspherezoning/bootstrap/main.tf deleted file mode 100644 index 8765fee6cc7..00000000000 --- a/data/data/vspherezoning/bootstrap/main.tf +++ /dev/null @@ -1,50 +0,0 @@ -locals { - description = "Created By OpenShift Installer" - vcenter_key = keys(var.vsphere_vcenters)[0] -} - -provider "vsphere" { - user = var.vsphere_vcenters[local.vcenter_key].user - password = var.vsphere_vcenters[local.vcenter_key].password - vsphere_server = var.vsphere_vcenters[local.vcenter_key].server - allow_unverified_ssl = false -} - -resource "vsphere_virtual_machine" "vm_bootstrap" { - name = "${var.cluster_id}-bootstrap" - resource_pool_id = var.resource_pool[0].id - datastore_id = var.datastore[0].id - num_cpus = var.vsphere_control_planes[0].numCPUs - num_cores_per_socket = var.vsphere_control_planes[0].numCoresPerSocket - memory = var.vsphere_control_planes[0].memoryMiB - guest_id = var.template[0].guest_id - folder = trimprefix(var.vsphere_control_planes[0].workspace.folder, "/${var.vsphere_control_planes[0].workspace.datacenter}/vm") - enable_disk_uuid = "true" - annotation = local.description - wait_for_guest_net_timeout = "0" - wait_for_guest_net_routable = "false" - tags = var.tags - - network_interface { - network_id = var.template[0].network_interfaces.0.network_id - } - - disk { - label = "disk0" - size = var.vsphere_control_planes[0].diskGiB - - eagerly_scrub = var.template[0].disks.0.eagerly_scrub - thin_provisioned = var.template[0].disks.0.thin_provisioned - } - - clone { - template_uuid = var.template[0].uuid - } - - extra_config = { - "guestinfo.ignition.config.data" = base64encode(var.ignition_bootstrap) - "guestinfo.ignition.config.data.encoding" = "base64" - "guestinfo.hostname" = "${var.cluster_id}-bootstrap" - "stealclock.enable" = "TRUE" - } -} diff --git a/data/data/vspherezoning/bootstrap/outputs.tf b/data/data/vspherezoning/bootstrap/outputs.tf deleted file mode 100644 index 0c1134b4e3d..00000000000 --- a/data/data/vspherezoning/bootstrap/outputs.tf +++ /dev/null @@ -1,7 +0,0 @@ -output "bootstrap_ip" { - value = vsphere_virtual_machine.vm_bootstrap.default_ip_address -} - -output "bootstrap_moid" { - value = vsphere_virtual_machine.vm_bootstrap.moid -} diff --git a/data/data/vspherezoning/bootstrap/variables.tf b/data/data/vspherezoning/bootstrap/variables.tf deleted file mode 100644 index 0957d9c7b82..00000000000 --- a/data/data/vspherezoning/bootstrap/variables.tf +++ /dev/null @@ -1,38 +0,0 @@ -variable "resource_pool" { - type = list(any) - default = [] -} - -variable "bootstrap_moid" { - type = string - default = "" -} - -variable "control_plane_ips" { - type = list(string) - default = [] -} - -variable "control_plane_moids" { - type = list(string) - default = [] -} - -variable "datastore" { - type = list(any) - default = [] -} - -variable "datacenter" { - type = list(any) - default = [] -} - -variable "template" { - type = list(any) - default = [] -} - -variable "tags" { - type = list(any) -} diff --git a/data/data/vspherezoning/master/main.tf b/data/data/vspherezoning/master/main.tf deleted file mode 100644 index 9a0af5121b0..00000000000 --- a/data/data/vspherezoning/master/main.tf +++ /dev/null @@ -1,70 +0,0 @@ -locals { - description = "Created By OpenShift Installer" - vcenter_key = keys(var.vsphere_vcenters)[0] - template_map = { for t in var.template : t.name => t } -} - -provider "vsphere" { - user = var.vsphere_vcenters[local.vcenter_key].user - password = var.vsphere_vcenters[local.vcenter_key].password - vsphere_server = var.vsphere_vcenters[local.vcenter_key].server - allow_unverified_ssl = false -} - -data "vsphere_datacenter" "datacenter" { - count = var.master_count - name = var.vsphere_control_planes[count.index].workspace.datacenter -} - -data "vsphere_resource_pool" "resource_pool" { - count = var.master_count - name = var.vsphere_control_planes[count.index].workspace.resourcePool -} - -data "vsphere_datastore" "datastore" { - count = var.master_count - name = var.vsphere_control_planes[count.index].workspace.datastore - datacenter_id = data.vsphere_datacenter.datacenter[count.index].id -} - -resource "vsphere_virtual_machine" "vm_master" { - count = var.master_count - - name = "${var.cluster_id}-master-${count.index}" - resource_pool_id = data.vsphere_resource_pool.resource_pool[count.index].id - datastore_id = data.vsphere_datastore.datastore[count.index].id - num_cpus = var.vsphere_control_planes[count.index].numCPUs - num_cores_per_socket = var.vsphere_control_planes[count.index].numCoresPerSocket - memory = var.vsphere_control_planes[count.index].memoryMiB - folder = trimprefix(var.vsphere_control_planes[count.index].workspace.folder, "/${var.vsphere_control_planes[count.index].workspace.datacenter}/vm") - - guest_id = local.template_map[var.vsphere_control_planes[count.index].template].guest_id - - enable_disk_uuid = "true" - annotation = local.description - wait_for_guest_net_timeout = "0" - wait_for_guest_net_routable = "false" - tags = var.tags - - network_interface { - network_id = local.template_map[var.vsphere_control_planes[count.index].template].network_interfaces.0.network_id - } - - disk { - label = "disk0" - size = var.vsphere_control_planes[count.index].diskGiB - eagerly_scrub = local.template_map[var.vsphere_control_planes[count.index].template].disks.0.eagerly_scrub - thin_provisioned = local.template_map[var.vsphere_control_planes[count.index].template].disks.0.thin_provisioned - } - - clone { - template_uuid = local.template_map[var.vsphere_control_planes[count.index].template].uuid - } - - extra_config = { - "guestinfo.ignition.config.data" = base64encode(var.ignition_master) - "guestinfo.ignition.config.data.encoding" = "base64" - "guestinfo.hostname" = "${var.cluster_id}-master-${count.index}" - "stealclock.enable" = "TRUE" - } -} diff --git a/data/data/vspherezoning/master/outputs.tf b/data/data/vspherezoning/master/outputs.tf deleted file mode 100644 index 4b635c65144..00000000000 --- a/data/data/vspherezoning/master/outputs.tf +++ /dev/null @@ -1,7 +0,0 @@ -output "control_plane_ips" { - value = [for k, v in vsphere_virtual_machine.vm_master : v.default_ip_address] -} - -output "control_plane_moids" { - value = [for k, v in vsphere_virtual_machine.vm_master : v.moid] -} diff --git a/data/data/vspherezoning/master/variables.tf b/data/data/vspherezoning/master/variables.tf deleted file mode 100644 index c8a32222756..00000000000 --- a/data/data/vspherezoning/master/variables.tf +++ /dev/null @@ -1,25 +0,0 @@ -variable "resource_pool" { - type = list(any) -} - -variable "bootstrap_moid" { - type = string - default = "" -} - -variable "datastore" { - type = list(any) -} - -variable "datacenter" { - type = list(any) -} - -variable "tags" { - type = list(any) -} - -variable "template" { - type = list(any) - default = [] -} diff --git a/data/data/vspherezoning/pre-bootstrap/main.tf b/data/data/vspherezoning/pre-bootstrap/main.tf deleted file mode 100644 index 3c7e4cdfc5c..00000000000 --- a/data/data/vspherezoning/pre-bootstrap/main.tf +++ /dev/null @@ -1,112 +0,0 @@ -locals { - description = "Created By OpenShift Installer" - vcenter_key = keys(var.vsphere_vcenters)[0] - failure_domains_count = length(var.vsphere_failure_domains) -} - -provider "vsphere" { - user = var.vsphere_vcenters[local.vcenter_key].user - password = var.vsphere_vcenters[local.vcenter_key].password - vsphere_server = var.vsphere_vcenters[local.vcenter_key].server - allow_unverified_ssl = false -} - -provider "vsphereprivate" { - user = var.vsphere_vcenters[local.vcenter_key].user - password = var.vsphere_vcenters[local.vcenter_key].password - vsphere_server = var.vsphere_vcenters[local.vcenter_key].server - allow_unverified_ssl = false -} - -data "vsphere_datacenter" "datacenter" { - count = local.failure_domains_count - name = var.vsphere_failure_domains[count.index].topology.datacenter -} - -data "vsphere_compute_cluster" "cluster" { - count = local.failure_domains_count - name = var.vsphere_failure_domains[count.index].topology.computeCluster - datacenter_id = data.vsphere_datacenter.datacenter[count.index].id -} - -data "vsphere_resource_pool" "resource_pool" { - count = local.failure_domains_count - name = var.vsphere_failure_domains[count.index].topology.resourcePool -} - -data "vsphere_datastore" "datastore" { - count = local.failure_domains_count - name = var.vsphere_failure_domains[count.index].topology.datastore - datacenter_id = data.vsphere_datacenter.datacenter[count.index].id -} - -data "vsphere_virtual_machine" "template" { - count = local.failure_domains_count - name = vsphereprivate_import_ova.import[count.index].name - datacenter_id = data.vsphere_datacenter.datacenter[count.index].id -} - -// Why is there two datacenters? -// The vm folder object is defined at the datacenter -// level. Each failure domain has a datacenter folder pair/ -// We need to get only the unique datacenter-folder pair -// and create those folders. See vsphere.go -// createDatacenterFolderMap - -data "vsphere_datacenter" "folder_datacenter" { - for_each = var.vsphere_folders - name = each.value.vsphere_datacenter -} - -resource "vsphere_folder" "folder" { - for_each = var.vsphere_folders - - path = each.value.name - type = "vm" - datacenter_id = data.vsphere_datacenter.folder_datacenter[each.key].id - tags = [vsphere_tag.tag.id] -} - -resource "vsphereprivate_import_ova" "import" { - count = local.failure_domains_count - name = format("%s-rhcos-%s-%s", var.cluster_id, var.vsphere_failure_domains[count.index].region, var.vsphere_failure_domains[count.index].zone) - - filename = var.vsphere_ova_filepath - cluster = data.vsphere_compute_cluster.cluster[count.index].name - resource_pool = data.vsphere_resource_pool.resource_pool[count.index].name - datacenter = data.vsphere_datacenter.datacenter[count.index].name - datastore = data.vsphere_datastore.datastore[count.index].name - - network = var.vsphere_networks[var.vsphere_failure_domains[count.index].name] - - - folder = var.vsphere_failure_domains[count.index].topology.folder - tag = vsphere_tag.tag.id - disk_type = var.vsphere_disk_type - - // Since the folder resource might not be ran because there could be - // user defined folder per failure domain if a folder is created - // the import resource is not waiting. Adding - // this depends_on so the import happens after creating folder(s). - depends_on = [vsphere_folder.folder] -} - -resource "vsphere_tag_category" "category" { - name = "openshift-${var.cluster_id}" - description = "Added by openshift-install do not remove" - cardinality = "SINGLE" - - associable_types = [ - "VirtualMachine", - "ResourcePool", - "Folder", - "Datastore", - "StoragePod" - ] -} - -resource "vsphere_tag" "tag" { - name = var.cluster_id - category_id = vsphere_tag_category.category.id - description = "Added by openshift-install do not remove" -} diff --git a/data/data/vspherezoning/pre-bootstrap/outputs.tf b/data/data/vspherezoning/pre-bootstrap/outputs.tf deleted file mode 100644 index faabdfa3c81..00000000000 --- a/data/data/vspherezoning/pre-bootstrap/outputs.tf +++ /dev/null @@ -1,27 +0,0 @@ -output "resource_pool" { - value = data.vsphere_resource_pool.resource_pool -} - -output "datastore" { - value = data.vsphere_datastore.datastore -} - -output "datacenter" { - value = data.vsphere_datacenter.datacenter -} - -output "template" { - value = data.vsphere_virtual_machine.template -} - -output "cluster_domain" { - value = var.cluster_domain -} - -output "cluster_id" { - value = var.cluster_id -} - -output "tags" { - value = [vsphere_tag.tag.id] -} diff --git a/data/data/vspherezoning/variables-vspherezoning.tf b/data/data/vspherezoning/variables-vspherezoning.tf deleted file mode 100644 index d271eca4c23..00000000000 --- a/data/data/vspherezoning/variables-vspherezoning.tf +++ /dev/null @@ -1,114 +0,0 @@ -////// -// vSphere variables -////// - -variable "vsphere_url" { - type = string - description = "This is the vSphere server for the environment." -} - -variable "vsphere_username" { - type = string - description = "vSphere server user for the environment." -} - -variable "vsphere_password" { - type = string - description = "vSphere server password" -} - -variable "vsphere_cluster" { - type = string - description = "This is the name of the vSphere cluster." -} - -variable "vsphere_resource_pool" { - type = string - description = "This is the absolute path to the vSphere resource pool." -} - -variable "vsphere_datacenter" { - type = string - description = "This is the name of the vSphere data center." -} - -variable "vsphere_datastore" { - type = string - description = "This is the name of the vSphere data store." -} - -variable "vsphere_ova_filepath" { - type = string - description = "This is the filepath to the ova file that will be imported into vSphere." -} - -variable "vsphere_template" { - type = string - description = "This is the name of the VM template to clone." -} - -variable "vsphere_network" { - type = string - description = "This is the Managed Object ID of the publicly accessible network for cluster ingress and access." -} - -variable "vsphere_folder" { - type = string - description = "The relative path to the folder which should be used or created for VMs." -} - -variable "vsphere_preexisting_folder" { - type = bool - description = "If false, creates a top-level folder with the name from vsphere_folder_rel_path." -} - -/////////// -// Control Plane machine variables -/////////// - -variable "vsphere_control_plane_memory_mib" { - type = number -} - -variable "vsphere_control_plane_disk_gib" { - type = number -} - -variable "vsphere_control_plane_num_cpus" { - type = number -} - -variable "vsphere_control_plane_cores_per_socket" { - type = number -} -variable "vsphere_disk_type" { - type = string -} - -///////// -// vSphere Zonal variables -//////// -variable "vsphere_vcenters" { - type = map(any) - default = {} -} - -variable "vsphere_networks" { - type = map(any) - default = {} -} - -variable "vsphere_folders" { - type = map(any) - default = {} -} - -variable "vsphere_control_planes" { - type = list(any) - default = [] -} - -variable "vsphere_failure_domains" { - type = list(any) - default = [] -} diff --git a/pkg/asset/cluster/tfvars.go b/pkg/asset/cluster/tfvars.go index 209443d1a8c..6245c822c22 100644 --- a/pkg/asset/cluster/tfvars.go +++ b/pkg/asset/cluster/tfvars.go @@ -890,30 +890,29 @@ func (t *TerraformVariables) Generate(parents asset.Parents) error { }) case vsphere.Name: + vim25Client, _, cleanup, err := vsphereconfig.CreateVSphereClients(context.TODO(), + installConfig.Config.VSphere.VCenters[0].Server, + installConfig.Config.VSphere.VCenters[0].Username, + installConfig.Config.VSphere.VCenters[0].Password) + if err != nil { + return errors.Wrapf(err, "unable to connect to vCenter %s. Ensure provided information is correct and client certs have been added to system trust", installConfig.Config.VSphere.VCenters[0].Server) + } + defer cleanup() + + finder := vsphereconfig.NewFinder(vim25Client) + networkFailureDomainMap := make(map[string]string) - var networkID string + controlPlanes, err := mastersAsset.Machines() if err != nil { return err } - controlPlaneConfigs := make([]*machinev1beta1.VSphereMachineProviderSpec, len(controlPlanes)) for i, c := range controlPlanes { controlPlaneConfigs[i] = c.Spec.ProviderSpec.Value.Object.(*machinev1beta1.VSphereMachineProviderSpec) } - vim25Client, _, cleanup, err := vsphereconfig.CreateVSphereClients(context.TODO(), - installConfig.Config.VSphere.VCenter, - installConfig.Config.VSphere.Username, - installConfig.Config.VSphere.Password) - if err != nil { - return errors.Wrapf(err, "unable to connect to vCenter %s. Ensure provided information is correct and client certs have been added to system trust.", installConfig.Config.VSphere.VCenter) - } - defer cleanup() - finder := vsphereconfig.NewFinder(vim25Client) - for _, fd := range installConfig.Config.VSphere.FailureDomains { - // Must use the Managed Object ID for a port group (e.g. dvportgroup-5258) // instead of the name since port group names aren't always unique in vSphere. // https://bugzilla.redhat.com/show_bug.cgi?id=1918005 @@ -929,30 +928,11 @@ func (t *TerraformVariables) Generate(parents asset.Parents) error { } } - networkID, err = vsphereconfig.GetNetworkMoID(context.TODO(), - vim25Client, - finder, - controlPlaneConfigs[0].Workspace.Datacenter, - installConfig.Config.VSphere.Cluster, - controlPlaneConfigs[0].Network.Devices[0].NetworkName) - if err != nil { - return errors.Wrap(err, "failed to get vSphere network ID") - } - - // Set this flag to use an existing folder specified in the install-config. Otherwise, create one. - preexistingFolder := installConfig.Config.Platform.VSphere.Folder != "" - data, err = vspheretfvars.TFVars( vspheretfvars.TFVarsSources{ - ControlPlaneConfigs: controlPlaneConfigs, - Username: installConfig.Config.VSphere.Username, - Password: installConfig.Config.VSphere.Password, - Cluster: installConfig.Config.VSphere.Cluster, - ImageURL: string(*rhcosImage), - PreexistingFolder: preexistingFolder, - DiskType: installConfig.Config.Platform.VSphere.DiskType, - NetworkID: networkID, - + ControlPlaneConfigs: controlPlaneConfigs, + ImageURL: string(*rhcosImage), + DiskType: installConfig.Config.Platform.VSphere.DiskType, NetworksInFailureDomain: networkFailureDomainMap, InfraID: clusterID.InfraID, InstallConfig: installConfig, diff --git a/pkg/destroy/bootstrap/bootstrap.go b/pkg/destroy/bootstrap/bootstrap.go index e094916bef2..d6833842a27 100644 --- a/pkg/destroy/bootstrap/bootstrap.go +++ b/pkg/destroy/bootstrap/bootstrap.go @@ -15,7 +15,6 @@ import ( platformstages "github.com/openshift/installer/pkg/terraform/stages/platform" typesazure "github.com/openshift/installer/pkg/types/azure" "github.com/openshift/installer/pkg/types/openstack" - typesvsphere "github.com/openshift/installer/pkg/types/vsphere" ) // Destroy uses Terraform to remove bootstrap resources. @@ -46,12 +45,6 @@ func Destroy(dir string) (err error) { platform = typesazure.StackTerraformName } - if platform == typesvsphere.Name { - if metadata.VSphere.TerraformPlatform == typesvsphere.ZoningTerraformName { - platform = typesvsphere.ZoningTerraformName - } - } - varFiles := []string{cluster.TfVarsFileName, cluster.TfPlatformVarsFileName} tfStages := platformstages.StagesForPlatform(platform) for _, stage := range tfStages { diff --git a/pkg/terraform/stages/platform/stages.go b/pkg/terraform/stages/platform/stages.go index 58dbb29570e..3846b0a37fa 100644 --- a/pkg/terraform/stages/platform/stages.go +++ b/pkg/terraform/stages/platform/stages.go @@ -60,8 +60,6 @@ func StagesForPlatform(platform string) []terraform.Stage { return ovirt.PlatformStages case vspheretypes.Name: return vsphere.PlatformStages - case vspheretypes.ZoningTerraformName: - return vsphere.ZoningPlatformStages case nonetypes.Name: // terraform is not used when the platform is "none" return []terraform.Stage{} diff --git a/pkg/terraform/stages/vsphere/stages.go b/pkg/terraform/stages/vsphere/stages.go index 80fcaff0666..c2dbca1c344 100644 --- a/pkg/terraform/stages/vsphere/stages.go +++ b/pkg/terraform/stages/vsphere/stages.go @@ -16,7 +16,8 @@ import ( "github.com/openshift/installer/pkg/types" ) -// PlatformStages are the stages to run to provision the infrastructure in vsphere. +// PlatformStages are the stages to run to provision the infrastructure in a +// multiple region and zone vsphere environment. var PlatformStages = []terraform.Stage{ stages.NewStage( "vsphere", @@ -38,29 +39,6 @@ var PlatformStages = []terraform.Stage{ ), } -// ZoningPlatformStages are the stages to run to provision the infrastructure in a -// multiple region and zone vsphere environment. -var ZoningPlatformStages = []terraform.Stage{ - stages.NewStage( - "vspherezoning", - "pre-bootstrap", - []providers.Provider{providers.VSphere, providers.VSpherePrivate}, - ), - stages.NewStage( - "vspherezoning", - "bootstrap", - []providers.Provider{providers.VSphere}, - stages.WithNormalBootstrapDestroy(), - stages.WithCustomExtractHostAddresses(extractOutputHostAddresses), - ), - stages.NewStage( - "vspherezoning", - "master", - []providers.Provider{providers.VSphere}, - stages.WithCustomExtractHostAddresses(extractOutputHostAddresses), - ), -} - func extractOutputHostAddresses(s stages.SplitStage, directory string, config *types.InstallConfig) (bootstrap string, port int, masters []string, err error) { port = 22 @@ -111,16 +89,15 @@ func extractOutputHostAddresses(s stages.SplitStage, directory string, config *t // hostIP returns the ip address for a host func hostIP(config *types.InstallConfig, moid string) (string, error) { - client, _, cleanup, err := vsphere.CreateVSphereClients(context.TODO(), config.VSphere.VCenter, config.VSphere.Username, config.VSphere.Password) + client, _, cleanup, err := vsphere.CreateVSphereClients(context.TODO(), config.VSphere.VCenters[0].Server, config.VSphere.VCenters[0].Username, config.VSphere.VCenters[0].Password) if err != nil { return "", err } defer cleanup() - var errs []error ip, err := waitForVirtualMachineIP(client, moid) if err != nil { - errs = append(errs, errors.Wrapf(err, "failed to lookup ipv4 address from given moid %s", moid)) + return "", errors.Wrapf(err, "failed to lookup ipv4 address from given moid %s", moid) } return ip, nil diff --git a/pkg/tfvars/vsphere/vsphere.go b/pkg/tfvars/vsphere/vsphere.go index e300c789345..c0856d4874a 100644 --- a/pkg/tfvars/vsphere/vsphere.go +++ b/pkg/tfvars/vsphere/vsphere.go @@ -3,7 +3,6 @@ package vsphere import ( "encoding/json" "fmt" - "strings" "github.com/pkg/errors" @@ -19,95 +18,42 @@ type folder struct { } type config struct { - VSphereURL string `json:"vsphere_url"` - VSphereUsername string `json:"vsphere_username"` - VSpherePassword string `json:"vsphere_password"` - MemoryMiB int64 `json:"vsphere_control_plane_memory_mib"` - DiskGiB int32 `json:"vsphere_control_plane_disk_gib"` - NumCPUs int32 `json:"vsphere_control_plane_num_cpus"` - NumCoresPerSocket int32 `json:"vsphere_control_plane_cores_per_socket"` - Cluster string `json:"vsphere_cluster"` - ResourcePool string `json:"vsphere_resource_pool"` - Datacenter string `json:"vsphere_datacenter"` - Datastore string `json:"vsphere_datastore"` - Folder string `json:"vsphere_folder"` - Network string `json:"vsphere_network"` - Template string `json:"vsphere_template"` - OvaFilePath string `json:"vsphere_ova_filepath"` - PreexistingFolder bool `json:"vsphere_preexisting_folder"` - DiskType vtypes.DiskType `json:"vsphere_disk_type"` - - // vcenters can still remain a map for easy lookups - VCenters map[string]vtypes.VCenter `json:"vsphere_vcenters"` - FailureDomains []vtypes.FailureDomain `json:"vsphere_failure_domains"` - - NetworksInFailureDomains map[string]string `json:"vsphere_networks"` - - ControlPlanes []*machineapi.VSphereMachineProviderSpec `json:"vsphere_control_planes"` - - DatacentersFolders map[string]*folder `json:"vsphere_folders"` + OvaFilePath string `json:"vsphere_ova_filepath"` + DiskType vtypes.DiskType `json:"vsphere_disk_type"` + VCenters map[string]vtypes.VCenter `json:"vsphere_vcenters"` + FailureDomains []vtypes.FailureDomain `json:"vsphere_failure_domains"` + NetworksInFailureDomains map[string]string `json:"vsphere_networks"` + ControlPlanes []*machineapi.VSphereMachineProviderSpec `json:"vsphere_control_planes"` + DatacentersFolders map[string]*folder `json:"vsphere_folders"` } // TFVarsSources contains the parameters to be converted into Terraform variables type TFVarsSources struct { - ControlPlaneConfigs []*machineapi.VSphereMachineProviderSpec - Username string - Password string - Cluster string - ImageURL string - PreexistingFolder bool - DiskType vtypes.DiskType - NetworkID string - + ControlPlaneConfigs []*machineapi.VSphereMachineProviderSpec + ImageURL string + DiskType vtypes.DiskType NetworksInFailureDomain map[string]string InstallConfig *installconfig.InstallConfig InfraID string - - ControlPlaneMachines []machineapi.Machine + ControlPlaneMachines []machineapi.Machine } // TFVars generate vSphere-specific Terraform variables func TFVars(sources TFVarsSources) ([]byte, error) { - controlPlaneConfig := sources.ControlPlaneConfigs[0] cachedImage, err := cache.DownloadImageFile(sources.ImageURL) if err != nil { return nil, errors.Wrap(err, "failed to use cached vsphere image") } - // The vSphere provider needs the relativepath of the folder, - // so get the relPath from the absolute path. Absolute path is always of the form - // //vm/ so we can split on "vm/". - - folderPathList := strings.SplitAfterN(controlPlaneConfig.Workspace.Folder, "vm/", 2) - - // This should never happen - if len(folderPathList) <= 1 { - return nil, errors.Errorf("control plane folder is not defined as a path %s", controlPlaneConfig.Workspace.Folder) - } - folderRelPath := folderPathList[1] - vcenterZones := convertVCentersToMap(sources.InstallConfig.Config.VSphere.VCenters) - datacentersFolders := createDatacenterFolderMap(sources.InfraID, sources.InstallConfig.Config.VSphere.FailureDomains) + datacentersFolders, err := createDatacenterFolderMap(sources.InfraID, sources.InstallConfig.Config.VSphere.FailureDomains) + if err != nil { + return nil, err + } cfg := &config{ - VSphereURL: controlPlaneConfig.Workspace.Server, - VSphereUsername: sources.Username, - VSpherePassword: sources.Password, - MemoryMiB: controlPlaneConfig.MemoryMiB, - DiskGiB: controlPlaneConfig.DiskGiB, - NumCPUs: controlPlaneConfig.NumCPUs, - NumCoresPerSocket: controlPlaneConfig.NumCoresPerSocket, - Cluster: sources.Cluster, - ResourcePool: controlPlaneConfig.Workspace.ResourcePool, - Datacenter: controlPlaneConfig.Workspace.Datacenter, - Datastore: controlPlaneConfig.Workspace.Datastore, - Folder: folderRelPath, - Network: sources.NetworkID, - Template: controlPlaneConfig.Template, - OvaFilePath: cachedImage, - PreexistingFolder: sources.PreexistingFolder, - DiskType: sources.DiskType, - + OvaFilePath: cachedImage, + DiskType: sources.DiskType, VCenters: vcenterZones, FailureDomains: sources.InstallConfig.Config.VSphere.FailureDomains, NetworksInFailureDomains: sources.NetworksInFailureDomain, @@ -126,16 +72,17 @@ func TFVars(sources TFVarsSources) ([]byte, error) { // unique - the key then becomes a string that contains // both the datacenter name and the folder to be created. -func createDatacenterFolderMap(infraID string, failureDomains []vtypes.FailureDomain) map[string]*folder { +func createDatacenterFolderMap(infraID string, failureDomains []vtypes.FailureDomain) (map[string]*folder, error) { folders := make(map[string]*folder) for i, fd := range failureDomains { - tempFolder := new(folder) - tempFolder.Datacenter = fd.Topology.Datacenter tempFolder.Name = fd.Topology.Folder + // Only if the folder is empty do we create a folder resource + // If a folder has been provided it means that it already exists + // and it is to be used. if tempFolder.Name == "" { tempFolder.Name = infraID failureDomains[i].Topology.Folder = infraID @@ -143,7 +90,7 @@ func createDatacenterFolderMap(infraID string, failureDomains []vtypes.FailureDo folders[key] = tempFolder } } - return folders + return folders, nil } func convertVCentersToMap(values []vtypes.VCenter) map[string]vtypes.VCenter { diff --git a/pkg/types/vsphere/doc.go b/pkg/types/vsphere/doc.go index c788515264a..0917509a013 100644 --- a/pkg/types/vsphere/doc.go +++ b/pkg/types/vsphere/doc.go @@ -4,6 +4,3 @@ package vsphere // Name is name for the vsphere platform. const Name string = "vsphere" - -// ZoningTerraformName is the name for the region and zone vsphere platform -const ZoningTerraformName string = "vspherezoning" From 2de77d9cefa3d238e153a48084b06be79dc48bc5 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Thu, 9 Feb 2023 13:37:49 -0500 Subject: [PATCH 2/8] vsphereprivate provider updates Remove OVA hardware version update - no longer needed as OVA is set to v15. Remove folder and cluster regexs - this may cause failures in unique vSphere configurations Allow datastore to now be a path --- .../resource_vsphereprivate_import_ova.go | 128 +++++------------- 1 file changed, 33 insertions(+), 95 deletions(-) diff --git a/terraform/providers/vsphereprivate/resource_vsphereprivate_import_ova.go b/terraform/providers/vsphereprivate/resource_vsphereprivate_import_ova.go index a67fafa9ffa..3c1dfb2646f 100644 --- a/terraform/providers/vsphereprivate/resource_vsphereprivate_import_ova.go +++ b/terraform/providers/vsphereprivate/resource_vsphereprivate_import_ova.go @@ -4,11 +4,8 @@ import ( "context" "fmt" "log" - "regexp" - "strconv" "strings" - "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/pkg/errors" @@ -17,7 +14,6 @@ import ( "github.com/vmware/govmomi/nfc" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/ovf" - "github.com/vmware/govmomi/task" "github.com/vmware/govmomi/vapi/tags" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/mo" @@ -25,12 +21,6 @@ import ( "github.com/vmware/govmomi/vim25/types" ) -const ( - esxi67U3BuildNumber int = 14320388 - vmx15 string = "vmx-15" - vmx13 string = "vmx-13" -) - func resourceVSpherePrivateImportOva() *schema.Resource { return &schema.Resource{ Create: resourceVSpherePrivateImportOvaCreate, @@ -123,6 +113,7 @@ type importOvaParams struct { } func findImportOvaParams(client *vim25.Client, datacenter, cluster, resourcePool, datastore, network, folder string) (*importOvaParams, error) { + var ccrMo mo.ClusterComputeResource var folderPath string @@ -139,19 +130,13 @@ func findImportOvaParams(client *vim25.Client, datacenter, cluster, resourcePool } importOvaParams.Datacenter = dcObj - // First check if the folder contains the datacenter - // If so check the regex - if strings.Contains(folder, datacenter) { - folderPathRegexp := regexp.MustCompile("^\\/(.*?)\\/vm\\/(.*?)$") - folderPathParts := folderPathRegexp.FindStringSubmatch(folder) - - if folderPathParts != nil { - folderPath = folder - } else { - return nil, errors.Errorf("folder path is incorrect, please provide a full path.") - } + // When finder.Datacenter is executed apparently it + // does not set the datacenter, why, who knows + // Replace finder with finder.SetDatacenter + finder = finder.SetDatacenter(dcObj) - } else { + folderPath = folder + if !strings.HasPrefix(folder, "/") { folderPath = fmt.Sprintf("/%s/vm/%s", datacenter, folder) } @@ -170,15 +155,11 @@ func findImportOvaParams(client *vim25.Client, datacenter, cluster, resourcePool } importOvaParams.ResourcePool = resourcePoolObj - clusterPathRegexp := regexp.MustCompile("^\\/(.*?)\\/host\\/(.*?)$") - clusterPathParts := clusterPathRegexp.FindStringSubmatch(cluster) - clusterPath := cluster - if clusterPathParts == nil { - // Find the cluster object by the datacenter and cluster name to - // generate the path e.g. /datacenter/host/cluster + if !strings.HasPrefix(cluster, "/") { clusterPath = fmt.Sprintf("/%s/host/%s", datacenter, cluster) } + clusterComputeResource, err := finder.ClusterComputeResource(ctx, clusterPath) if err != nil { return nil, err @@ -205,19 +186,36 @@ func findImportOvaParams(client *vim25.Client, datacenter, cluster, resourcePool } // Find all the datastores that are configured under the cluster - datastores, err := clusterComputeResource.Datastores(ctx) + clusterDatastores, err := clusterComputeResource.Datastores(ctx) if err != nil { return nil, err } + if len(clusterDatastores) == 0 { + return nil, errors.Errorf("failed to find any datastore(s) in the cluster") + } + // Find the specific datastore by the name provided - for _, datastoreObj := range datastores { - datastoreObjName, err := datastoreObj.ObjectName(ctx) + for _, objectDS := range clusterDatastores { + + objectDSName, err := objectDS.ObjectName(ctx) if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to find datastore name") } - if datastore == datastoreObjName { - importOvaParams.Datastore = datastoreObj + // clusterComputeResource.Datastores(ctx) does not properly + // handle Common - which includes InventoryPath and .Name() + // To workaround this issue we must retrieve the Datastore + // object again with the method below. Do not remove this. + ds, err := finder.Datastore(ctx, objectDSName) + + if err != nil { + return nil, errors.Wrap(err, "unable to find datastore object by name") + } + datastoreName := ds.Name() + datastorePath := ds.InventoryPath + + if datastoreName == datastore || datastorePath == datastore { + importOvaParams.Datastore = ds break } } @@ -225,11 +223,6 @@ func findImportOvaParams(client *vim25.Client, datacenter, cluster, resourcePool return nil, errors.Errorf("failed to find a host in the cluster that contains the provided datastore") } - v67, err := version.NewVersion("6.7") - if err != nil { - return nil, err - } - // Find all the HostSystem(s) under cluster hosts, err := clusterComputeResource.Hosts(ctx) if err != nil { @@ -241,6 +234,7 @@ func findImportOvaParams(client *vim25.Client, datacenter, cluster, resourcePool // available for use on the HostSystem we will import the // OVA to. for _, hostObj := range hosts { + foundDatastore := false foundNetwork := false err := hostObj.Properties(ctx, hostObj.Reference(), []string{"config.product", "network", "datastore", "runtime"}, &hostSystemManagedObject) @@ -248,33 +242,6 @@ func findImportOvaParams(client *vim25.Client, datacenter, cluster, resourcePool return nil, err } - // If HardwareVersion is 13 there is no reason to continue checking - // There is a ESXi host that does not support hardware 15. - if importOvaParams.HardwareVersion != vmx13 { - esxiHostVersion, err := version.NewVersion(hostSystemManagedObject.Config.Product.Version) - if err != nil { - return nil, err - } - - importOvaParams.HardwareVersion = vmx13 - if esxiHostVersion.Equal(v67) { - build, err := strconv.Atoi(hostSystemManagedObject.Config.Product.Build) - if err != nil { - return nil, err - } - // This is the ESXi 6.7 U3 build number - // Anything less than this version is unsupported with the - // out-of-tree CSI. - // https://kb.vmware.com/s/article/2143838 - // https://vsphere-csi-driver.sigs.k8s.io/supported_features_matrix.html - if build >= esxi67U3BuildNumber { - importOvaParams.HardwareVersion = vmx15 - } - } else if esxiHostVersion.GreaterThan(v67) { - importOvaParams.HardwareVersion = vmx15 - } - } - // Skip all hosts that are in maintenance mode. if hostSystemManagedObject.Runtime.InMaintenanceMode { continue @@ -469,22 +436,6 @@ func resourceVSpherePrivateImportOvaCreate(d *schema.ResourceData, meta interfac } log.Printf("[DEBUG] %s: mark as template", vm.Name()) - // https://vdc-download.vmware.com/vmwb-repository/dcr-public/b50dcbbf-051d-4204-a3e7-e1b618c1e384/538cf2ec-b34f-4bae-a332-3820ef9e7773/vim.VirtualMachine.html#upgradeVirtualHardware - // "Upgrades this virtual machine's virtual hardware to the latest revision that is supported by the virtual machine's current host." - task, err := vm.UpgradeVM(ctx, importOvaParams.HardwareVersion) - - if err != nil { - return errors.Errorf("failed to upgrade vm to: %s, %s", importOvaParams.HardwareVersion, err) - } - - err = task.Wait(ctx) - - if err != nil { - if !isAlreadyUpgraded(err) { - return errors.Errorf("failed to upgrade vm to: %s, %s", importOvaParams.HardwareVersion, err) - } - } - err = vm.MarkAsTemplate(ctx) if err != nil { return errors.Errorf("failed to mark vm as template: %s", err) @@ -541,16 +492,3 @@ func resourceVSpherePrivateImportOvaDelete(d *schema.ResourceData, meta interfac return nil } - -// Using govc vm.upgrade as the example -// If the hardware was already upgraded err is not nil -// https://github.com/vmware/govmomi/blob/master/govc/vm/upgrade.go - -func isAlreadyUpgraded(err error) bool { - if fault, ok := err.(task.Error); ok { - _, ok = fault.Fault().(*types.AlreadyUpgraded) - return ok - } - - return false -} From 09fcccbbf9f010335f12840d951a3c21eb29a6b0 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Thu, 9 Feb 2023 13:44:56 -0500 Subject: [PATCH 3/8] cloud provider config updates The changes in this commit are mostly related to the linter. Remove no longer needed functions as all installs are zonal now. If failureDomains are more than one cloud config `[Labels]` will be applied and require appropriate tag and tag categories. --- pkg/asset/manifests/cloudproviderconfig.go | 31 ++------- .../manifests/vsphere/cloudproviderconfig.go | 65 +++++++------------ 2 files changed, 26 insertions(+), 70 deletions(-) diff --git a/pkg/asset/manifests/cloudproviderconfig.go b/pkg/asset/manifests/cloudproviderconfig.go index 0d00cf88e59..de776d138a0 100644 --- a/pkg/asset/manifests/cloudproviderconfig.go +++ b/pkg/asset/manifests/cloudproviderconfig.go @@ -285,34 +285,11 @@ func (cpc *CloudProviderConfig) Generate(dependencies asset.Parents) error { } cm.Data[cloudProviderConfigDataKey] = powervsConfig case vspheretypes.Name: - vSphere := installConfig.Config.Platform.VSphere - if len(vSphere.VCenters) > 0 { - folderPath := installConfig.Config.Platform.VSphere.Folder - if len(folderPath) == 0 { - dataCenter := installConfig.Config.Platform.VSphere.Datacenter - folderPath = fmt.Sprintf("/%s/vm/%s", dataCenter, clusterID.InfraID) - } - - vsphereConfig, err := vspheremanifests.MultiZoneIniCloudProviderConfig(folderPath, installConfig.Config.Platform.VSphere) - if err != nil { - return errors.Wrap(err, "could not create cloud provider config") - } - cm.Data[cloudProviderConfigDataKey] = vsphereConfig - } else { - folderPath := installConfig.Config.Platform.VSphere.Folder - if len(folderPath) == 0 { - dataCenter := installConfig.Config.Platform.VSphere.Datacenter - folderPath = fmt.Sprintf("/%s/vm/%s", dataCenter, clusterID.InfraID) - } - vsphereConfig, err := vspheremanifests.InTreeCloudProviderConfig( - folderPath, - installConfig.Config.Platform.VSphere, - ) - if err != nil { - return errors.Wrap(err, "could not create cloud provider config") - } - cm.Data[cloudProviderConfigDataKey] = vsphereConfig + vsphereConfig, err := vspheremanifests.CloudProviderConfigIni(clusterID.InfraID, installConfig.Config.Platform.VSphere) + if err != nil { + return errors.Wrap(err, "could not create cloud provider config") } + cm.Data[cloudProviderConfigDataKey] = vsphereConfig case nutanixtypes.Name: configJSON, err := nutanixmanifests.CloudConfigJSON(installConfig.Config.Nutanix) if err != nil { diff --git a/pkg/asset/manifests/vsphere/cloudproviderconfig.go b/pkg/asset/manifests/vsphere/cloudproviderconfig.go index 96646fab171..33b0f38473b 100644 --- a/pkg/asset/manifests/vsphere/cloudproviderconfig.go +++ b/pkg/asset/manifests/vsphere/cloudproviderconfig.go @@ -22,18 +22,18 @@ func printIfNotEmpty(buf *bytes.Buffer, k, v string) { } } -// MultiZoneYamlCloudProviderConfig generates the yaml out of tree cloud provider config for the vSphere platform. -func MultiZoneYamlCloudProviderConfig(p *vspheretypes.Platform) (string, error) { +// CloudProviderConfigYaml generates the yaml out of tree cloud provider config for the vSphere platform. +func CloudProviderConfigYaml(infraID string, p *vspheretypes.Platform) (string, error) { vCenters := make(map[string]*cloudconfig.VirtualCenterConfigYAML) for _, vCenter := range p.VCenters { - vCenterPort := uint(443) + vCenterPort := int32(443) if vCenter.Port != 0 { vCenterPort = vCenter.Port } vCenterConfig := cloudconfig.VirtualCenterConfigYAML{ VCenterIP: vCenter.Server, - VCenterPort: vCenterPort, + VCenterPort: uint(vCenterPort), Datacenters: vCenter.Datacenters, } vCenters[vCenter.Server] = &vCenterConfig @@ -58,10 +58,10 @@ func MultiZoneYamlCloudProviderConfig(p *vspheretypes.Platform) (string, error) return string(cloudProviderConfigYaml), nil } -// MultiZoneIniCloudProviderConfig generates the multi-zone ini cloud provider config +// CloudProviderConfigIni generates the multi-zone ini cloud provider config // for the vSphere platform. folderPath is the absolute path to the VM folder that will be // used for installation. p is the vSphere platform struct. -func MultiZoneIniCloudProviderConfig(folderPath string, p *vspheretypes.Platform) (string, error) { +func CloudProviderConfigIni(infraID string, p *vspheretypes.Platform) (string, error) { buf := new(bytes.Buffer) fmt.Fprintln(buf, "[Global]") @@ -76,10 +76,8 @@ func MultiZoneIniCloudProviderConfig(folderPath string, p *vspheretypes.Platform printIfNotEmpty(buf, "port", fmt.Sprintf("%d", vcenter.Port)) fmt.Fprintln(buf, "") } - var datacenters []string - for _, datacenter := range vcenter.Datacenters { - datacenters = append(datacenters, datacenter) - } + datacenters := make([]string, 0, len(vcenter.Datacenters)) + datacenters = append(datacenters, vcenter.Datacenters...) for _, failureDomain := range p.FailureDomains { if failureDomain.Server == vcenter.Server { failureDomainDatacenter := failureDomain.Topology.Datacenter @@ -90,7 +88,7 @@ func MultiZoneIniCloudProviderConfig(folderPath string, p *vspheretypes.Platform break } } - if exists == false { + if !exists { datacenters = append(datacenters, failureDomainDatacenter) } } @@ -100,42 +98,23 @@ func MultiZoneIniCloudProviderConfig(folderPath string, p *vspheretypes.Platform fmt.Fprintln(buf, "") fmt.Fprintln(buf, "[Workspace]") - printIfNotEmpty(buf, "server", p.VCenter) - printIfNotEmpty(buf, "datacenter", p.Datacenter) - printIfNotEmpty(buf, "default-datastore", p.DefaultDatastore) - printIfNotEmpty(buf, "folder", folderPath) - printIfNotEmpty(buf, "resourcepool-path", p.ResourcePool) - fmt.Fprintln(buf, "") - - fmt.Fprintln(buf, "[Labels]") - printIfNotEmpty(buf, "region", vspheretypes.TagCategoryRegion) - printIfNotEmpty(buf, "zone", vspheretypes.TagCategoryZone) - - return buf.String(), nil -} - -// InTreeCloudProviderConfig generates the in-tree cloud provider config for the vSphere platform. -// folderPath is the absolute path to the VM folder that will be used for installation. -// p is the vSphere platform struct. -func InTreeCloudProviderConfig(folderPath string, p *vspheretypes.Platform) (string, error) { - buf := new(bytes.Buffer) + printIfNotEmpty(buf, "server", p.FailureDomains[0].Server) + printIfNotEmpty(buf, "datacenter", p.FailureDomains[0].Topology.Datacenter) + printIfNotEmpty(buf, "default-datastore", p.FailureDomains[0].Topology.Datastore) - fmt.Fprintln(buf, "[Global]") - printIfNotEmpty(buf, "secret-name", "vsphere-creds") - printIfNotEmpty(buf, "secret-namespace", "kube-system") - printIfNotEmpty(buf, "insecure-flag", "1") - fmt.Fprintln(buf, "") - - fmt.Fprintln(buf, "[Workspace]") - printIfNotEmpty(buf, "server", p.VCenter) - printIfNotEmpty(buf, "datacenter", p.Datacenter) - printIfNotEmpty(buf, "default-datastore", p.DefaultDatastore) + folderPath := fmt.Sprintf("/%s/vm/%s", p.FailureDomains[0].Topology.Datacenter, infraID) + if p.FailureDomains[0].Topology.Folder != "" { + folderPath = p.FailureDomains[0].Topology.Folder + } printIfNotEmpty(buf, "folder", folderPath) - printIfNotEmpty(buf, "resourcepool-path", p.ResourcePool) + printIfNotEmpty(buf, "resourcepool-path", p.FailureDomains[0].Topology.ResourcePool) fmt.Fprintln(buf, "") - fmt.Fprintf(buf, "[VirtualCenter %q]\n", p.VCenter) - printIfNotEmpty(buf, "datacenters", p.Datacenter) + if len(p.FailureDomains) > 1 { + fmt.Fprintln(buf, "[Labels]") + printIfNotEmpty(buf, "region", regionTagCategory) + printIfNotEmpty(buf, "zone", zoneTagCategory) + } return buf.String(), nil } From f52bf67fbdf016c31d7df195bd2a970212a39302 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Thu, 9 Feb 2023 13:45:24 -0500 Subject: [PATCH 4/8] machine changes In this commit the changes are related to the platform spec depreciation. failureDomain is the main determination of how machines and machineset are configured. --- pkg/asset/machines/vsphere/machines.go | 86 ++++++------- pkg/asset/machines/vsphere/machinesets.go | 130 +++++++------------- pkg/asset/machines/worker.go | 8 -- pkg/types/vsphere/validation/machinepool.go | 2 +- 4 files changed, 80 insertions(+), 146 deletions(-) diff --git a/pkg/asset/machines/vsphere/machines.go b/pkg/asset/machines/vsphere/machines.go index b0181921c13..3bd57f2c01a 100644 --- a/pkg/asset/machines/vsphere/machines.go +++ b/pkg/asset/machines/vsphere/machines.go @@ -3,7 +3,6 @@ package vsphere import ( "fmt" - "regexp" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -24,56 +23,45 @@ func Machines(clusterID string, config *types.InstallConfig, pool *types.Machine return nil, fmt.Errorf("non-VSphere machine-pool: %q", poolPlatform) } + var failureDomain vsphere.FailureDomain var machines []machineapi.Machine - platform := config.Platform.VSphere mpool := pool.Platform.VSphere + replicas := int64(1) - azs := mpool.Zones - numOfAZs := len(azs) - definedZones := make(map[string]*vsphere.Platform) + numOfZones := len(mpool.Zones) - if numOfAZs > 0 { - zones, err := getDefinedZones(platform) - if err != nil { - return machines, err - } - definedZones = zones + zones, err := getDefinedZonesFromTopology(platform) + if err != nil { + return machines, err } - replicas := int64(1) if pool.Replicas != nil { replicas = *pool.Replicas } for idx := int64(0); idx < replicas; idx++ { - var failureDomain *vsphere.FailureDomain - if numOfAZs > 0 { - desiredZone := mpool.Zones[int(idx)%numOfAZs] - if _, exists := definedZones[desiredZone]; !exists { - return nil, errors.Errorf("zone [%s] specified by machinepool is not defined", desiredZone) - } - platform = definedZones[desiredZone] - for idx, knownFailureDomain := range platform.FailureDomains { - if knownFailureDomain.Name == desiredZone { - failureDomain = &platform.FailureDomains[idx] - } - } + desiredZone := mpool.Zones[int(idx)%numOfZones] + + if _, exists := zones[desiredZone]; !exists { + return nil, errors.Errorf("zone [%s] specified by machinepool is not defined", desiredZone) } + failureDomain = zones[desiredZone] + machineLabels := map[string]string{ "machine.openshift.io/cluster-api-cluster": clusterID, "machine.openshift.io/cluster-api-machine-role": role, "machine.openshift.io/cluster-api-machine-type": role, } - osImageForZone := osImage + osImageForZone := fmt.Sprintf("%s-%s-%s", osImage, failureDomain.Region, failureDomain.Zone) - if failureDomain != nil { - osImageForZone = fmt.Sprintf("%s-%s-%s", osImage, failureDomain.Region, failureDomain.Zone) + vcenter, err := getVCenterFromServerName(failureDomain.Server, platform) + if err != nil { + return nil, errors.Wrap(err, "unable to find vCenter in failure domains") } - - provider, err := provider(clusterID, platform, mpool, osImageForZone, userDataSecret) + provider, err := provider(clusterID, vcenter, failureDomain, mpool, osImageForZone, userDataSecret) if err != nil { return nil, errors.Wrap(err, "failed to create provider") } @@ -100,21 +88,25 @@ func Machines(clusterID string, config *types.InstallConfig, pool *types.Machine return machines, nil } -func provider(clusterID string, platform *vsphere.Platform, mpool *vsphere.MachinePool, osImage string, userDataSecret string) (*machineapi.VSphereMachineProviderSpec, error) { - folder := fmt.Sprintf("/%s/vm/%s", platform.Datacenter, clusterID) +func provider(clusterID string, vcenter *vsphere.VCenter, failureDomain vsphere.FailureDomain, mpool *vsphere.MachinePool, osImage string, userDataSecret string) (*machineapi.VSphereMachineProviderSpec, error) { + networkDeviceSpec := make([]machineapi.NetworkDeviceSpec, len(failureDomain.Topology.Networks)) - resourcePool := fmt.Sprintf("/%s/host/%s/Resources", platform.Datacenter, platform.Cluster) - resourcePoolPrefix := "^\\/(.*?)\\/host\\/(.*?)" - hasFullPath, _ := regexp.MatchString(resourcePoolPrefix, platform.Cluster) - if hasFullPath { - resourcePool = fmt.Sprintf("%s/Resources", platform.Cluster) - } + // If failureDomain.Topology.Folder is empty this will be used + folder := fmt.Sprintf("/%s/vm/%s", failureDomain.Topology.Datacenter, clusterID) - if platform.Folder != "" { - folder = platform.Folder + // If failureDomain.Topology.ResourcePool is empty this will be used + // computeCluster is required to be a path + resourcePool := fmt.Sprintf("%s/Resources", failureDomain.Topology.ComputeCluster) + + if failureDomain.Topology.Folder != "" { + folder = failureDomain.Topology.Folder } - if platform.ResourcePool != "" { - resourcePool = platform.ResourcePool + if failureDomain.Topology.ResourcePool != "" { + resourcePool = failureDomain.Topology.ResourcePool + } + + for i, network := range failureDomain.Topology.Networks { + networkDeviceSpec[i] = machineapi.NetworkDeviceSpec{NetworkName: network} } return &machineapi.VSphereMachineProviderSpec{ @@ -126,16 +118,12 @@ func provider(clusterID string, platform *vsphere.Platform, mpool *vsphere.Machi CredentialsSecret: &corev1.LocalObjectReference{Name: "vsphere-cloud-credentials"}, Template: osImage, Network: machineapi.NetworkSpec{ - Devices: []machineapi.NetworkDeviceSpec{ - { - NetworkName: platform.Network, - }, - }, + Devices: networkDeviceSpec, }, Workspace: &machineapi.Workspace{ - Server: platform.VCenter, - Datacenter: platform.Datacenter, - Datastore: platform.DefaultDatastore, + Server: vcenter.Server, + Datacenter: failureDomain.Topology.Datacenter, + Datastore: failureDomain.Topology.Datastore, Folder: folder, ResourcePool: resourcePool, }, diff --git a/pkg/asset/machines/vsphere/machinesets.go b/pkg/asset/machines/vsphere/machinesets.go index b7901f10877..deec9fff2e6 100644 --- a/pkg/asset/machines/vsphere/machinesets.go +++ b/pkg/asset/machines/vsphere/machinesets.go @@ -18,11 +18,12 @@ func getMachineSetWithPlatform( name string, mpool *vsphere.MachinePool, osImage string, - platform *vsphere.Platform, + failureDomain vsphere.FailureDomain, + vcenter *vsphere.VCenter, replicas int32, role, userDataSecret string) (*machineapi.MachineSet, error) { - provider, err := provider(clusterID, platform, mpool, osImage, userDataSecret) + provider, err := provider(clusterID, vcenter, failureDomain, mpool, osImage, userDataSecret) if err != nil { return nil, errors.Wrap(err, "failed to create provider") } @@ -77,55 +78,11 @@ func getVCenterFromServerName(server string, platformSpec *vsphere.Platform) (*v return nil, errors.Errorf("unable to find vCenter %s", server) } -func getFailureDomain(domainName string, platformSpec *vsphere.Platform) (*vsphere.FailureDomain, error) { - for _, failureDomain := range platformSpec.FailureDomains { - if failureDomain.Name == domainName { - return &failureDomain, nil - } - } - return nil, errors.Errorf("%s is not a defined failure domain", domainName) -} - -func getfailureDomain(failureDomainName string, platformSpec *vsphere.Platform) (*vsphere.FailureDomain, error) { - for _, failureDomain := range platformSpec.FailureDomains { - if failureDomain.Name == failureDomainName { - return &failureDomain, nil - } - } - return nil, errors.Errorf("%s is not a defined deployment zone", failureDomainName) -} - -// getDefinedZones retrieves zones and associated platform specs that are appropriate to the machine role -func getDefinedZones(platformSpec *vsphere.Platform) (map[string]*vsphere.Platform, error) { - zones := make(map[string]*vsphere.Platform) - - for _, failureDomain := range platformSpec.FailureDomains { - vCenter, err := getVCenterFromServerName(failureDomain.Server, platformSpec) - if err != nil { - return nil, err - } - failureDomain, err := getFailureDomain(failureDomain.Name, platformSpec) - if err != nil { - return nil, err - } - var vcPlatform = vsphere.Platform{ - VCenter: vCenter.Server, - Username: vCenter.Username, - Password: vCenter.Password, - Datacenter: failureDomain.Topology.Datacenter, - DefaultDatastore: failureDomain.Topology.Datastore, - Folder: failureDomain.Topology.Folder, - Cluster: failureDomain.Topology.ComputeCluster, - ResourcePool: failureDomain.Topology.ResourcePool, - APIVIPs: platformSpec.APIVIPs, - IngressVIPs: platformSpec.IngressVIPs, - Network: failureDomain.Topology.Networks[0], - DiskType: platformSpec.DiskType, - FailureDomains: platformSpec.FailureDomains, - } - zones[failureDomain.Name] = &vcPlatform +func getDefinedZonesFromTopology(p *vsphere.Platform) (map[string]vsphere.FailureDomain, error) { + zones := make(map[string]vsphere.FailureDomain) + for _, failureDomain := range p.FailureDomains { + zones[failureDomain.Name] = failureDomain } - return zones, nil } @@ -139,58 +96,54 @@ func MachineSets(clusterID string, config *types.InstallConfig, pool *types.Mach } platform := config.Platform.VSphere mpool := pool.Platform.VSphere + // The machinepool has no zones defined, there are FailureDomains + // This is a vSphere zonal installation. Generate machinepool zone + // list. + if len(mpool.Zones) == 0 { + for _, fd := range config.VSphere.FailureDomains { + mpool.Zones = append(mpool.Zones, fd.Name) + } + } azs := mpool.Zones total := 0 if pool.Replicas != nil { total = int(*pool.Replicas) } numOfAZs := len(azs) - var machinesets []*machineapi.MachineSet - if numOfAZs > 0 { - zones, err := getDefinedZones(platform) - if err != nil { - return machinesets, err + machinesets := make([]*machineapi.MachineSet, 0, numOfAZs) + + zones, err := getDefinedZonesFromTopology(platform) + + if err != nil { + return machinesets, err + } + for idx := range azs { + replicas := int32(total / numOfAZs) + if idx < total%numOfAZs { + replicas++ } - for idx := range azs { - replicas := int32(total / numOfAZs) - if idx < total%numOfAZs { - replicas++ - } - desiredZone := azs[idx] - if _, exists := zones[desiredZone]; !exists { - return nil, errors.Errorf("zone [%s] specified by machinepool is not defined", desiredZone) - } - name := fmt.Sprintf("%s-%s-%d", clusterID, pool.Name, idx) + desiredZone := azs[idx] + if _, exists := zones[desiredZone]; !exists { + return nil, errors.Errorf("zone [%s] specified by machinepool is not defined", desiredZone) + } + name := fmt.Sprintf("%s-%s-%d", clusterID, pool.Name, idx) - failureDomain, err := getFailureDomain(desiredZone, platform) - if err != nil { - return nil, err - } + failureDomain := zones[desiredZone] - osImageForZone := fmt.Sprintf("%s-%s-%s", osImage, failureDomain.Region, failureDomain.Zone) - machineset, err := getMachineSetWithPlatform( - clusterID, - name, - mpool, - osImageForZone, - zones[desiredZone], - replicas, - role, - userDataSecret) - if err != nil { - return machinesets, err - } - machinesets = append(machinesets, machineset) + vcenter, err := getVCenterFromServerName(failureDomain.Server, platform) + if err != nil { + return nil, err } - } else { - name := fmt.Sprintf("%s-%s", clusterID, pool.Name) + + osImageForZone := fmt.Sprintf("%s-%s-%s", osImage, failureDomain.Region, failureDomain.Zone) machineset, err := getMachineSetWithPlatform( clusterID, name, mpool, - osImage, - platform, - int32(total), + osImageForZone, + failureDomain, + vcenter, + replicas, role, userDataSecret) if err != nil { @@ -198,5 +151,6 @@ func MachineSets(clusterID string, config *types.InstallConfig, pool *types.Mach } machinesets = append(machinesets, machineset) } + return machinesets, nil } diff --git a/pkg/asset/machines/worker.go b/pkg/asset/machines/worker.go index bb9172fa33e..ce5df8b4d77 100644 --- a/pkg/asset/machines/worker.go +++ b/pkg/asset/machines/worker.go @@ -515,14 +515,6 @@ func (w *Worker) Generate(dependencies asset.Parents) error { mpool := defaultVSphereMachinePoolPlatform() mpool.Set(ic.Platform.VSphere.DefaultMachinePlatform) mpool.Set(pool.Platform.VSphere) - // The machinepool has no zones defined, there are FailureDomains - // This is a vSphere zonal installation. Generate machinepool zone - // list. - if len(mpool.Zones) == 0 && len(ic.VSphere.FailureDomains) != 0 { - for _, fd := range ic.VSphere.FailureDomains { - mpool.Zones = append(mpool.Zones, fd.Name) - } - } pool.Platform.VSphere = &mpool templateName := clusterID.InfraID + "-rhcos" diff --git a/pkg/types/vsphere/validation/machinepool.go b/pkg/types/vsphere/validation/machinepool.go index 645e31e6c6a..bb1e28dbc62 100644 --- a/pkg/types/vsphere/validation/machinepool.go +++ b/pkg/types/vsphere/validation/machinepool.go @@ -65,7 +65,7 @@ func ValidateMachinePool(platform *vsphere.Platform, machinePool *types.MachineP zoneDefined = true } } - if zoneDefined == false { + if !zoneDefined { allErrs = append(allErrs, field.Invalid(fldPath.Child("zones"), zone, "zone not defined in failureDomains")) } } From 8579a12abd50a1b84c115ba71336e446698239b1 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Thu, 9 Feb 2023 13:47:13 -0500 Subject: [PATCH 5/8] vSphere: Platform spec deprecation To make vSphere installation easier for zonal and provide additional options for non-zonal installation the existing fields will be depreciated with this commit. To facilitate this we extended the networking converstion logic to be specific for vsphere. The conversion will take existing IPI, UPI and 4.12 Zonal platform specs and modify to work with the new vcenters and failure domains. Additional Details: - Updated agent installer's use of platform spec fields - Add additional validation for vSphere versions of vCenter and ESXi hosts within requested clusters. - Modify validations to failure domain and vcenters requirements - including requiring paths. - Create pkg/types/vsphere/conversion/installconfig.go to modify a given platform spec for the new requirements - Modified platform spec renaming fields depreciated as required. - DNS and LB check broken apart to assist with unit testing. - Changes from linting. --- pkg/asset/agent/installconfig.go | 36 +-- pkg/asset/cluster/cluster.go | 7 - pkg/asset/cluster/vsphere/vsphere.go | 13 +- .../installconfig/platformprovisioncheck.go | 8 +- pkg/asset/installconfig/vsphere/client.go | 5 +- .../installconfig/vsphere/permissions.go | 6 +- pkg/asset/installconfig/vsphere/validation.go | 275 ++++++++++-------- pkg/asset/installconfig/vsphere/vsphere.go | 57 ++-- pkg/asset/manifests/openshift.go | 16 +- pkg/asset/manifests/operators.go | 43 ++- pkg/asset/manifests/vsphere/infrastructure.go | 27 +- pkg/destroy/vsphere/vsphere.go | 11 - pkg/types/conversion/installconfig.go | 4 + pkg/types/vsphere/conversion/installconfig.go | 128 ++++++++ pkg/types/vsphere/defaults/platform.go | 13 +- pkg/types/vsphere/platform.go | 33 ++- pkg/types/vsphere/validation/platform.go | 218 +++----------- 17 files changed, 480 insertions(+), 420 deletions(-) create mode 100644 pkg/types/vsphere/conversion/installconfig.go diff --git a/pkg/asset/agent/installconfig.go b/pkg/asset/agent/installconfig.go index 0f5ef75bf24..143ea67774e 100644 --- a/pkg/asset/agent/installconfig.go +++ b/pkg/asset/agent/installconfig.go @@ -309,37 +309,37 @@ func warnUnusedConfig(installConfig *types.InstallConfig) { case vsphere.Name: vspherePlatform := installConfig.Platform.VSphere - if vspherePlatform.VCenter != "" { + if vspherePlatform.DeprecatedVCenter != "" { fieldPath := field.NewPath("Platform", "VSphere", "VCenter") - logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.VCenter)) + logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DeprecatedVCenter)) } - if vspherePlatform.Username != "" { + if vspherePlatform.DeprecatedUsername != "" { fieldPath := field.NewPath("Platform", "VSphere", "Username") - logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.Username)) + logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DeprecatedUsername)) } - if vspherePlatform.Password != "" { + if vspherePlatform.DeprecatedPassword != "" { fieldPath := field.NewPath("Platform", "VSphere", "Password") - logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.Password)) + logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DeprecatedPassword)) } - if vspherePlatform.Datacenter != "" { + if vspherePlatform.DeprecatedDatacenter != "" { fieldPath := field.NewPath("Platform", "VSphere", "Datacenter") - logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.Datacenter)) + logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DeprecatedDatacenter)) } - if vspherePlatform.DefaultDatastore != "" { + if vspherePlatform.DeprecatedDefaultDatastore != "" { fieldPath := field.NewPath("Platform", "VSphere", "DefaultDatastore") - logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DefaultDatastore)) + logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DeprecatedDefaultDatastore)) } - if vspherePlatform.Folder != "" { + if vspherePlatform.DeprecatedFolder != "" { fieldPath := field.NewPath("Platform", "VSphere", "Folder") - logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.Folder)) + logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DeprecatedFolder)) } - if vspherePlatform.Cluster != "" { + if vspherePlatform.DeprecatedCluster != "" { fieldPath := field.NewPath("Platform", "VSphere", "Cluster") - logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.Cluster)) + logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DeprecatedCluster)) } - if vspherePlatform.ResourcePool != "" { + if vspherePlatform.DeprecatedResourcePool != "" { fieldPath := field.NewPath("Platform", "VSphere", "ResourcePool") - logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.ResourcePool)) + logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DeprecatedResourcePool)) } if vspherePlatform.ClusterOSImage != "" { fieldPath := field.NewPath("Platform", "VSphere", "ClusterOSImage") @@ -349,9 +349,9 @@ func warnUnusedConfig(installConfig *types.InstallConfig) { fieldPath := field.NewPath("Platform", "VSphere", "DefaultMachinePlatform") logrus.Warnf(fmt.Sprintf("%s: %v is ignored", fieldPath, vspherePlatform.DefaultMachinePlatform)) } - if vspherePlatform.Network != "" { + if vspherePlatform.DeprecatedNetwork != "" { fieldPath := field.NewPath("Platform", "VSphere", "Network") - logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.Network)) + logrus.Warnf(fmt.Sprintf("%s: %s is ignored", fieldPath, vspherePlatform.DeprecatedNetwork)) } if vspherePlatform.DiskType != "" { fieldPath := field.NewPath("Platform", "VSphere", "DiskType") diff --git a/pkg/asset/cluster/cluster.go b/pkg/asset/cluster/cluster.go index 861c510261a..51b3c75124a 100644 --- a/pkg/asset/cluster/cluster.go +++ b/pkg/asset/cluster/cluster.go @@ -24,7 +24,6 @@ import ( typesaws "github.com/openshift/installer/pkg/types/aws" typesazure "github.com/openshift/installer/pkg/types/azure" typesopenstack "github.com/openshift/installer/pkg/types/openstack" - typesvsphere "github.com/openshift/installer/pkg/types/vsphere" ) var ( @@ -93,12 +92,6 @@ func (c *Cluster) Generate(parents asset.Parents) (err error) { platform = typesazure.StackTerraformName } - if vsphere := installConfig.Config.Platform.VSphere; vsphere != nil { - if len(vsphere.FailureDomains) != 0 { - platform = typesvsphere.ZoningTerraformName - } - } - stages := platformstages.StagesForPlatform(platform) terraformDir := filepath.Join(InstallDir, "terraform") diff --git a/pkg/asset/cluster/vsphere/vsphere.go b/pkg/asset/cluster/vsphere/vsphere.go index 12be9d8a6fa..ea3168988fe 100644 --- a/pkg/asset/cluster/vsphere/vsphere.go +++ b/pkg/asset/cluster/vsphere/vsphere.go @@ -9,16 +9,13 @@ import ( func Metadata(config *types.InstallConfig) *typesvsphere.Metadata { terraformPlatform := "vsphere" - if vsphere := config.Platform.VSphere; vsphere != nil { - if len(vsphere.FailureDomains) != 0 { - terraformPlatform = typesvsphere.ZoningTerraformName - } - } + // Since currently we only support a single vCenter + // just use the first entry in the VCenters slice. return &typesvsphere.Metadata{ - VCenter: config.VSphere.VCenter, - Username: config.VSphere.Username, - Password: config.VSphere.Password, + VCenter: config.VSphere.VCenters[0].Server, + Username: config.VSphere.VCenters[0].Username, + Password: config.VSphere.VCenters[0].Password, TerraformPlatform: terraformPlatform, } } diff --git a/pkg/asset/installconfig/platformprovisioncheck.go b/pkg/asset/installconfig/platformprovisioncheck.go index 27b3f048aa6..988a1db90d9 100644 --- a/pkg/asset/installconfig/platformprovisioncheck.go +++ b/pkg/asset/installconfig/platformprovisioncheck.go @@ -109,13 +109,7 @@ func (a *PlatformProvisionCheck) Generate(dependencies asset.Parents) error { return err } case vsphere.Name: - var err error - if len(ic.Config.VSphere.VCenters) > 0 { - err = vsconfig.ValidateMultiZoneForProvisioning(ic.Config) - } else { - err = vsconfig.ValidateForProvisioning(ic.Config) - } - if err != nil { + if err := vsconfig.ValidateForProvisioning(ic.Config); err != nil { return err } case ovirt.Name: diff --git a/pkg/asset/installconfig/vsphere/client.go b/pkg/asset/installconfig/vsphere/client.go index f0482a479a9..aa0037fd2dd 100644 --- a/pkg/asset/installconfig/vsphere/client.go +++ b/pkg/asset/installconfig/vsphere/client.go @@ -63,7 +63,10 @@ func CreateVSphereClients(ctx context.Context, vcenter, username, password strin restClient := rest.NewClient(c.Client) err = restClient.Login(ctx, u.User) if err != nil { - c.Logout(context.TODO()) + logoutErr := c.Logout(context.TODO()) + if logoutErr != nil { + err = logoutErr + } return nil, nil, nil, err } diff --git a/pkg/asset/installconfig/vsphere/permissions.go b/pkg/asset/installconfig/vsphere/permissions.go index 0f85a335d74..6d616193f9e 100644 --- a/pkg/asset/installconfig/vsphere/permissions.go +++ b/pkg/asset/installconfig/vsphere/permissions.go @@ -226,11 +226,11 @@ func comparePrivileges(ctx context.Context, validationCtx *validationContext, mo } } } - if hasPrivilege == false { + if !hasPrivilege { if missingPrivileges != "" { - missingPrivileges = missingPrivileges + ", " + missingPrivileges += ", " } - missingPrivileges = missingPrivileges + neededPrivilege + missingPrivileges += neededPrivilege } } if missingPrivileges != "" { diff --git a/pkg/asset/installconfig/vsphere/validation.go b/pkg/asset/installconfig/vsphere/validation.go index 33a9c91a703..c4b3cb9ce82 100644 --- a/pkg/asset/installconfig/vsphere/validation.go +++ b/pkg/asset/installconfig/vsphere/validation.go @@ -5,9 +5,11 @@ import ( "fmt" "net" "regexp" + "strconv" "strings" "time" + "github.com/hashicorp/go-version" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/vmware/govmomi/find" @@ -35,6 +37,14 @@ type TagManager interface { GetAttachedTagsOnObjects(ctx context.Context, objectID []mo.Reference) ([]vapitags.AttachedTags, error) } +const ( + esxi7U2BuildNumber int = 17630552 + vcenter7U2BuildNumber int = 17694817 + vcenter7U2Version string = "7.0.2" +) + +var localLogger = logrus.New() + type validationContext struct { AuthManager AuthManager Finder Finder @@ -78,18 +88,13 @@ func getVCenterClient(failureDomain vsphere.FailureDomain, ic *types.InstallConf return nil, nil, fmt.Errorf("vcenter %s not defined in vcenters", server) } -// ValidateMultiZoneForProvisioning performs platform validation specifically +// ValidateForProvisioning performs platform validation specifically // for multi-zone installer-provisioned infrastructure. In this case, // self-hosted networking is a requirement when the installer creates // infrastructure for vSphere clusters. -func ValidateMultiZoneForProvisioning(ic *types.InstallConfig) error { +func ValidateForProvisioning(ic *types.InstallConfig) error { allErrs := field.ErrorList{} - err := ValidateForProvisioning(ic) - if err != nil { - return err - } - // If APIVIPs and IngressVIPs is equal to zero // then don't validate the VIPs. // Instead, ensure there is a configured @@ -102,27 +107,35 @@ func ValidateMultiZoneForProvisioning(ic *types.InstallConfig) error { // This will allow the use of an external load balancer // and RHCOS nodes to be on multiple L2 segments. if len(ic.Platform.VSphere.APIVIPs) == 0 && len(ic.Platform.VSphere.IngressVIPs) == 0 { - allErrs = append(allErrs, ensureLoadBalancerDNS(ic, field.NewPath("platform"))...) + allErrs = append(allErrs, ensureDNS(ic, field.NewPath("platform"), nil)...) + ensureLoadBalancer(ic) } var clients = make(map[string]*validationContext, 0) - for _, failureDomain := range ic.VSphere.FailureDomains { + + checkTags := false + if len(ic.VSphere.FailureDomains) > 1 { + checkTags = true + } + + for i, failureDomain := range ic.VSphere.FailureDomains { if _, exists := clients[failureDomain.Server]; !exists { validationCtx, cleanup, err := getVCenterClient(failureDomain, ic) if err != nil { return err } defer cleanup() + allErrs = append(allErrs, validateVCenterVersion(validationCtx, field.NewPath("platform").Child("vsphere").Child("vcenters"))...) clients[failureDomain.Server] = validationCtx } validationCtx := clients[failureDomain.Server] - allErrs = append(allErrs, validateMultiZoneProvisioning(validationCtx, &failureDomain)...) + allErrs = append(allErrs, validateFailureDomain(validationCtx, &ic.VSphere.FailureDomains[i], checkTags)...) } return allErrs.ToAggregate() } -func validateMultiZoneProvisioning(validationCtx *validationContext, failureDomain *vsphere.FailureDomain) field.ErrorList { +func validateFailureDomain(validationCtx *validationContext, failureDomain *vsphere.FailureDomain, checkTags bool) field.ErrorList { allErrs := field.ErrorList{} checkDatacenterPrivileges := true checkComputeClusterPrivileges := true @@ -136,12 +149,14 @@ func validateMultiZoneProvisioning(validationCtx *validationContext, failureDoma vsphereField := field.NewPath("platform").Child("vsphere") topologyField := vsphereField.Child("failureDomains").Child("topology") - regionTagCategoryID, zoneTagCategoryID, err := validateTagCategories(validationCtx) - if err != nil { - allErrs = append(allErrs, field.InternalError(vsphereField, err)) + if checkTags { + regionTagCategoryID, zoneTagCategoryID, err := validateTagCategories(validationCtx) + if err != nil { + allErrs = append(allErrs, field.InternalError(vsphereField, err)) + } + validationCtx.regionTagCategoryID = regionTagCategoryID + validationCtx.zoneTagCategoryID = zoneTagCategoryID } - validationCtx.regionTagCategoryID = regionTagCategoryID - validationCtx.zoneTagCategoryID = zoneTagCategoryID allErrs = append(allErrs, resourcePoolExists(validationCtx, resourcePool, topologyField.Child("resourcePool"))...) @@ -151,28 +166,18 @@ func validateMultiZoneProvisioning(validationCtx *validationContext, failureDoma } computeCluster := failureDomain.Topology.ComputeCluster - clusterPathRegexp := regexp.MustCompile("^\\/(.*?)\\/host\\/(.*?)$") + clusterPathRegexp := regexp.MustCompile(`^/(.*?)/host/(.*?)$`) clusterPathParts := clusterPathRegexp.FindStringSubmatch(computeCluster) if len(clusterPathParts) < 3 { return append(allErrs, field.Invalid(topologyField.Child("computeCluster"), computeCluster, "full path of cluster is required")) } computeClusterName := clusterPathParts[2] - errs := validateVcenterPrivileges(validationCtx, topologyField.Child("server")) - if len(errs) > 0 { - return append(allErrs, errs...) - } - errs = computeClusterExists(validationCtx, computeCluster, topologyField.Child("computeCluster"), checkComputeClusterPrivileges) - if len(errs) > 0 { - return append(allErrs, errs...) - } - errs = datacenterExists(validationCtx, failureDomain.Topology.Datacenter, topologyField.Child("datacenter"), checkDatacenterPrivileges) - if len(errs) > 0 { - return append(allErrs, errs...) - } - errs = datastoreExists(validationCtx, failureDomain.Topology.Datacenter, failureDomain.Topology.Datastore, topologyField.Child("datastore")) - if len(errs) > 0 { - return append(allErrs, errs...) - } + + allErrs = append(allErrs, validateESXiVersion(validationCtx, computeCluster, vsphereField, topologyField.Child("computeCluster"))...) + allErrs = append(allErrs, validateVcenterPrivileges(validationCtx, topologyField.Child("server"))...) + allErrs = append(allErrs, computeClusterExists(validationCtx, computeCluster, topologyField.Child("computeCluster"), checkComputeClusterPrivileges, checkTags)...) + allErrs = append(allErrs, datacenterExists(validationCtx, failureDomain.Topology.Datacenter, topologyField.Child("datacenter"), checkDatacenterPrivileges)...) + allErrs = append(allErrs, datastoreExists(validationCtx, failureDomain.Topology.Datacenter, failureDomain.Topology.Datastore, topologyField.Child("datastore"))...) for _, network := range failureDomain.Topology.Networks { allErrs = append(allErrs, validateNetwork(validationCtx, failureDomain.Topology.Datacenter, computeClusterName, network, topologyField)...) @@ -181,106 +186,124 @@ func validateMultiZoneProvisioning(validationCtx *validationContext, failureDoma return allErrs } -// ValidateForProvisioning performs platform validation specifically for installer- -// provisioned infrastructure. In this case, self-hosted networking is a requirement -// when the installer creates infrastructure for vSphere clusters. -func ValidateForProvisioning(ic *types.InstallConfig) error { - if ic.Platform.VSphere == nil { - return errors.New(field.Required(field.NewPath("platform", "vsphere"), "vSphere validation requires a vSphere platform configuration").Error()) - } - - p := ic.Platform.VSphere - vim25Client, _, cleanup, err := CreateVSphereClients(context.TODO(), - p.VCenter, - p.Username, - p.Password) - - if err != nil { - return errors.New(field.InternalError(field.NewPath("platform", "vsphere"), errors.Wrapf(err, "unable to connect to vCenter %s.", p.VCenter)).Error()) +// folderExists returns an error if a folder is specified in the vSphere platform but a folder with that name is not found in the datacenter. +func folderExists(validationCtx *validationContext, folderPath string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + finder := validationCtx.Finder + // If no folder is specified, skip this check as the folder will be created. + if folderPath == "" { + return allErrs } - defer cleanup() - finder := NewFinder(vim25Client) - validationCtx := &validationContext{ - AuthManager: newAuthManager(vim25Client), - Finder: finder, - Client: vim25Client, - } ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) defer cancel() - err = pruneToAvailablePermissions(ctx, validationCtx.AuthManager) + folder, err := finder.Folder(ctx, folderPath) if err != nil { - return errors.New(field.InternalError(field.NewPath("platform", "vsphere"), errors.Wrapf(err, "unable to determine available vCenter privileges.")).Error()) + return append(allErrs, field.Invalid(fldPath, folderPath, err.Error())) } + permissionGroup := permissions[permissionFolder] - return validateProvisioning(validationCtx, ic) + err = comparePrivileges(ctx, validationCtx, folder.Reference(), permissionGroup) + if err != nil { + return append(allErrs, field.InternalError(fldPath, err)) + } + return allErrs } -func validateProvisioning(validationCtx *validationContext, ic *types.InstallConfig) error { +func validateVCenterVersion(validationCtx *validationContext, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - platform := ic.Platform.VSphere - vsphereField := field.NewPath("platform").Child("vsphere") - checkDatacenterPrivileges := ic.VSphere.Folder == "" - checkComputeClusterPrivileges := ic.VSphere.ResourcePool == "" - allErrs = append(allErrs, validation.ValidateForProvisioning(platform, vsphereField)...) - allErrs = append(allErrs, validateVcenterPrivileges(validationCtx, vsphereField.Child("vcenter"))...) - allErrs = append(allErrs, folderExists(validationCtx, ic.VSphere.Folder, vsphereField.Child("folder"))...) - allErrs = append(allErrs, resourcePoolExists(validationCtx, ic.VSphere.ResourcePool, vsphereField.Child("resourcePool"))...) - - // if the datacenter or cluster fail to be found or is missing privileges, this will cascade through the balance - // of checks. exit if they fail to limit multiple errors from being thrown. - errs := datacenterExists(validationCtx, platform.Datacenter, vsphereField.Child("datacenter"), checkDatacenterPrivileges) - if len(errs) > 0 { - allErrs = append(allErrs, errs...) - return allErrs.ToAggregate() - } - computeCluster := platform.Cluster - if computeCluster == "" { - return field.Required(vsphereField.Child("cluster"), "must specify the cluster") + + constraints, err := version.NewConstraint(fmt.Sprintf("< %s", vcenter7U2Version)) + if err != nil { + allErrs = append(allErrs, field.InternalError(fldPath, err)) } - clusterPathRegexp := regexp.MustCompile("^\\/(.*?)\\/host\\/(.*?)$") - clusterPathParts := clusterPathRegexp.FindStringSubmatch(computeCluster) - if len(clusterPathParts) < 3 { - computeCluster = fmt.Sprintf("/%s/host/%s", platform.Datacenter, computeCluster) + + vCenterVersion, err := version.NewVersion(validationCtx.Client.ServiceContent.About.Version) + if err != nil { + allErrs = append(allErrs, field.InternalError(fldPath, err)) } - errs = computeClusterExists(validationCtx, computeCluster, vsphereField.Child("cluster"), checkComputeClusterPrivileges) - if len(errs) > 0 { - allErrs = append(allErrs, errs...) - return allErrs.ToAggregate() + build, err := strconv.Atoi(validationCtx.Client.ServiceContent.About.Build) + if err != nil { + allErrs = append(allErrs, field.InternalError(fldPath, err)) } - errs = validateNetwork(validationCtx, platform.Datacenter, platform.Cluster, platform.Network, vsphereField.Child("network")) - if len(errs) > 0 { - allErrs = append(allErrs, errs...) - return allErrs.ToAggregate() + detail := fmt.Sprintf("The vSphere storage driver requires a minimum of vSphere 7 Update 2. Current vCenter version: %s, build: %s", + validationCtx.Client.ServiceContent.About.Version, validationCtx.Client.ServiceContent.About.Build) + + if constraints.Check(vCenterVersion) { + allErrs = append(allErrs, field.Required(fldPath, detail)) + } else if build < vcenter7U2BuildNumber { + allErrs = append(allErrs, field.Required(fldPath, detail)) } - allErrs = append(allErrs, datastoreExists(validationCtx, platform.Datacenter, platform.DefaultDatastore, vsphereField.Child("defaultDatastore"))...) - return allErrs.ToAggregate() + return allErrs } -// folderExists returns an error if a folder is specified in the vSphere platform but a folder with that name is not found in the datacenter. -func folderExists(validationCtx *validationContext, folderPath string, fldPath *field.Path) field.ErrorList { +func validateESXiVersion(validationCtx *validationContext, clusterPath string, vSphereFldPath, computeClusterFldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} finder := validationCtx.Finder - // If no folder is specified, skip this check as the folder will be created. - if folderPath == "" { - return allErrs - } ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) defer cancel() - folder, err := finder.Folder(ctx, folderPath) + clusters, err := finder.ClusterComputeResourceList(ctx, clusterPath) + if err != nil { - return append(allErrs, field.Invalid(fldPath, folderPath, err.Error())) + var notFoundError *find.NotFoundError + var defaultNotFoundError *find.DefaultNotFoundError + + /* These error types also exist, but it seems less likely to occur. + var *find.MultipleFoundError + var *find.DefaultMultipleFoundError + */ + switch { + case errors.As(err, ¬FoundError): + return field.ErrorList{field.Invalid(computeClusterFldPath, clusterPath, notFoundError.Error())} + case errors.As(err, &defaultNotFoundError): + return field.ErrorList{field.Invalid(computeClusterFldPath, clusterPath, defaultNotFoundError.Error())} + default: + return append(allErrs, field.InternalError(vSphereFldPath, err)) + } } - permissionGroup := permissions[permissionFolder] - err = comparePrivileges(ctx, validationCtx, folder.Reference(), permissionGroup) + v7, err := version.NewVersion("7.0") if err != nil { - return append(allErrs, field.InternalError(fldPath, err)) + return append(allErrs, field.InternalError(vSphereFldPath, err)) + } + + hosts, err := clusters[0].Hosts(context.TODO()) + if err != nil { + err = errors.Wrapf(err, "unable to find hosts from cluster on path: %s", clusterPath) + return append(allErrs, field.InternalError(vSphereFldPath, err)) + } + + for _, h := range hosts { + var mh mo.HostSystem + err := h.Properties(context.TODO(), h.Reference(), []string{"config.product"}, &mh) + if err != nil { + return append(allErrs, field.InternalError(vSphereFldPath, err)) + } + + esxiHostVersion, err := version.NewVersion(mh.Config.Product.Version) + if err != nil { + return append(allErrs, field.InternalError(vSphereFldPath, err)) + } + + detail := fmt.Sprintf("The vSphere storage driver requires a minimum of vSphere 7 Update 2. The ESXi host: %s is version: %s and build: %s", + h.Name(), mh.Config.Product.Version, mh.Config.Product.Build) + + if esxiHostVersion.LessThan(v7) { + allErrs = append(allErrs, field.Required(computeClusterFldPath, detail)) + } else { + build, err := strconv.Atoi(mh.Config.Product.Build) + if err != nil { + return append(allErrs, field.InternalError(vSphereFldPath, err)) + } + if build < esxi7U2BuildNumber { + allErrs = append(allErrs, field.Required(computeClusterFldPath, detail)) + } + } } return allErrs } @@ -321,7 +344,7 @@ func validateNetwork(validationCtx *validationContext, datacenterName string, cl } // resourcePoolExists returns an error if a resourcePool is specified in the vSphere platform but a resourcePool with that name is not found in the datacenter. -func computeClusterExists(validationCtx *validationContext, computeCluster string, fldPath *field.Path, checkPrivileges bool) field.ErrorList { +func computeClusterExists(validationCtx *validationContext, computeCluster string, fldPath *field.Path, checkPrivileges, checkTagAttachment bool) field.ErrorList { if computeCluster == "" { return field.ErrorList{field.Required(fldPath, "must specify the cluster")} } @@ -343,9 +366,11 @@ func computeClusterExists(validationCtx *validationContext, computeCluster strin } } - err = validateTagAttachment(validationCtx, computeClusterMo.Reference()) - if err != nil { - return field.ErrorList{field.InternalError(fldPath, err)} + if checkTagAttachment { + err = validateTagAttachment(validationCtx, computeClusterMo.Reference()) + if err != nil { + return field.ErrorList{field.InternalError(fldPath, err)} + } } return field.ErrorList{} @@ -422,7 +447,7 @@ func datastoreExists(validationCtx *validationContext, datacenterName string, da var datastoreMo *vim25types.ManagedObjectReference for _, datastore := range datastores { - if datastore.Name() == datastoreName { + if datastore.InventoryPath == datastoreName || datastore.Name() == datastoreName { mo := datastore.Reference() datastoreMo = &mo } @@ -457,31 +482,43 @@ func validateVcenterPrivileges(validationCtx *validationContext, fldPath *field. return field.ErrorList{} } -func ensureLoadBalancerDNS(installConfig *types.InstallConfig, fldPath *field.Path) field.ErrorList { - var lastErr error +func ensureDNS(installConfig *types.InstallConfig, fldPath *field.Path, resolver *net.Resolver) field.ErrorList { var uris []string - dialTimeout := time.Second - tcpTimeout := time.Second * 10 - errorCount := 0 errList := field.ErrorList{} - tcpContext, cancel := context.WithTimeout(context.TODO(), tcpTimeout) + ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) defer cancel() uris = append(uris, fmt.Sprintf("api.%s", installConfig.ClusterDomain())) uris = append(uris, fmt.Sprintf("api-int.%s", installConfig.ClusterDomain())) - apiURIPort := fmt.Sprintf("%s:%s", uris[0], "6443") + if resolver == nil { + resolver = &net.Resolver{ + PreferGo: true, + } + } // DNS lookup uri for _, u := range uris { logrus.Debugf("Performing DNS Lookup: %s", u) - _, err := net.LookupHost(u) + _, err := resolver.LookupHost(ctx, u) // Append error if DNS entry does not exist if err != nil { errList = append(errList, field.Invalid(fldPath, u, err.Error())) } } + return errList +} + +func ensureLoadBalancer(installConfig *types.InstallConfig) { + var lastErr error + dialTimeout := time.Second + tcpTimeout := time.Second * 10 + errorCount := 0 + apiURIPort := fmt.Sprintf("api.%s:%s", installConfig.ClusterDomain(), "6443") + tcpContext, cancel := context.WithTimeout(context.TODO(), tcpTimeout) + defer cancel() + // If the load balancer is configured properly even // without members we should be available to make // a connection to port 6443. Check for 10 seconds @@ -506,11 +543,9 @@ func ensureLoadBalancerDNS(installConfig *types.InstallConfig, fldPath *field.Pa err := tcpContext.Err() if err != nil && !errors.Is(err, context.Canceled) { if lastErr != nil { - logrus.Warnf("Installation may fail, load balancer not available: %v", lastErr) + localLogger.Warnf("Installation may fail, load balancer not available: %v", lastErr) } } - - return errList } func validateTagCategories(validationCtx *validationContext) (string, string, error) { diff --git a/pkg/asset/installconfig/vsphere/vsphere.go b/pkg/asset/installconfig/vsphere/vsphere.go index a8b9ee1f0c3..c04acd0fda1 100644 --- a/pkg/asset/installconfig/vsphere/vsphere.go +++ b/pkg/asset/installconfig/vsphere/vsphere.go @@ -72,17 +72,34 @@ func Platform() (*vsphere.Platform, error) { return nil, errors.Wrap(err, "failed to get VIPs") } + failureDomain := vsphere.FailureDomain{ + Name: "generated-failure-domain", + Zone: "generated-zone", + Region: "generated-region", + Server: vCenter.VCenter, + Topology: vsphere.Topology{ + Datacenter: dc, + ComputeCluster: cluster, + Datastore: datastore, + Networks: []string{network}, + }, + } + + vcenter := vsphere.VCenter{ + Server: vCenter.VCenter, + Port: 443, + Username: vCenter.Username, + Password: vCenter.Password, + Datacenters: []string{dc}, + } + platform := &vsphere.Platform{ - Datacenter: dc, - Cluster: cluster, - DefaultDatastore: datastore, - Network: network, - VCenter: vCenter.VCenter, - Username: vCenter.Username, - Password: vCenter.Password, - APIVIPs: []string{apiVIP}, - IngressVIPs: []string{ingressVIP}, + VCenters: []vsphere.VCenter{vcenter}, + FailureDomains: []vsphere.FailureDomain{failureDomain}, + APIVIPs: []string{apiVIP}, + IngressVIPs: []string{ingressVIP}, } + return platform, nil } @@ -140,7 +157,7 @@ func getClients() (*vCenterClient, error) { // Survey does not allow validation of groups of input // so we perform our own validation. if err != nil { - return nil, errors.Wrapf(err, "unable to connect to vCenter %s. Ensure provided information is correct and client certs have been added to system trust.", vcenter) + return nil, errors.Wrapf(err, "unable to connect to vCenter %s. Ensure provided information is correct and client certs have been added to system trust", vcenter) } return &vCenterClient{ @@ -176,7 +193,7 @@ func getDataCenter(ctx context.Context, finder Finder, client *vim25.Client) (st } dataCenterPaths := make(map[string]string) - var dataCenterChoices []string + dataCenterChoices := make([]string, 0, len(dataCenters)) for _, dc := range dataCenters { name := strings.TrimPrefix(dc.InventoryPath, "/") dataCenterPaths[name] = dc.InventoryPath @@ -215,15 +232,13 @@ func getCluster(ctx context.Context, path string, finder Finder, client *vim25.C return "", errors.New("did not find any clusters") } if len(clusters) == 1 { - name := strings.TrimPrefix(clusters[0].InventoryPath, path+"/host/") - logrus.Infof("Defaulting to only available cluster: %s", name) - return name, nil + logrus.Infof("Defaulting to only available cluster: %s", clusters[0].InventoryPath) + return clusters[0].InventoryPath, nil } - var clusterChoices []string + clusterChoices := make([]string, 0, len(clusters)) for _, c := range clusters { - name := strings.TrimPrefix(c.InventoryPath, path+"/host/") - clusterChoices = append(clusterChoices, name) + clusterChoices = append(clusterChoices, c.InventoryPath) } sort.Strings(clusterChoices) @@ -258,13 +273,13 @@ func getDataStore(ctx context.Context, path string, finder Finder, client *vim25 return "", errors.New("did not find any datastores") } if len(dataStores) == 1 { - logrus.Infof("Defaulting to only available datastore: %s", dataStores[0].Name()) + logrus.Infof("Defaulting to only available datastore: %s", dataStores[0].InventoryPath) return dataStores[0].Name(), nil } - var dataStoreChoices []string + dataStoreChoices := make([]string, 0, len(dataStores)) for _, ds := range dataStores { - dataStoreChoices = append(dataStoreChoices, ds.Name()) + dataStoreChoices = append(dataStoreChoices, ds.InventoryPath) } sort.Strings(dataStoreChoices) @@ -317,7 +332,7 @@ func getNetwork(ctx context.Context, datacenter string, cluster string, finder F var networkChoices []string for _, network := range networks { if validNetworkTypes.Has(network.Reference().Type) { - // TODO Below results in an API call. Can it be eliminated somehow? + // Below results in an API call. Can it be eliminated somehow? n, err := GetNetworkName(ctx, client, network) if err != nil { return "", errors.Wrap(err, "unable to get network name") diff --git a/pkg/asset/manifests/openshift.go b/pkg/asset/manifests/openshift.go index 5a832837729..ecf62bd8f9a 100644 --- a/pkg/asset/manifests/openshift.go +++ b/pkg/asset/manifests/openshift.go @@ -184,24 +184,16 @@ func (o *Openshift) Generate(dependencies asset.Parents) error { } case vspheretypes.Name: vsphereCredList := make([]*VSphereCredsSecretData, 0) - if len(installConfig.Config.VSphere.VCenters) > 0 { - for _, vCenter := range installConfig.Config.VSphere.VCenters { - vsphereCred := VSphereCredsSecretData{ - VCenter: vCenter.Server, - Base64encodeUsername: base64.StdEncoding.EncodeToString([]byte(vCenter.Username)), - Base64encodePassword: base64.StdEncoding.EncodeToString([]byte(vCenter.Password)), - } - vsphereCredList = append(vsphereCredList, &vsphereCred) - } - } else { - vCenter := installConfig.Config.VSphere + + for _, vCenter := range installConfig.Config.VSphere.VCenters { vsphereCred := VSphereCredsSecretData{ - VCenter: vCenter.VCenter, + VCenter: vCenter.Server, Base64encodeUsername: base64.StdEncoding.EncodeToString([]byte(vCenter.Username)), Base64encodePassword: base64.StdEncoding.EncodeToString([]byte(vCenter.Password)), } vsphereCredList = append(vsphereCredList, &vsphereCred) } + cloudCreds = cloudCredsSecretData{ VSphere: &vsphereCredList, } diff --git a/pkg/asset/manifests/operators.go b/pkg/asset/manifests/operators.go index caf8ca92ee4..cf1e56b535c 100644 --- a/pkg/asset/manifests/operators.go +++ b/pkg/asset/manifests/operators.go @@ -16,6 +16,7 @@ import ( "github.com/openshift/installer/pkg/asset/templates/content/bootkube" "github.com/openshift/installer/pkg/asset/tls" "github.com/openshift/installer/pkg/types" + "github.com/openshift/installer/pkg/types/vsphere" ) const ( @@ -225,14 +226,40 @@ func (m *Manifests) Load(f asset.FileFetcher) (bool, error) { } func redactedInstallConfig(config types.InstallConfig) ([]byte, error) { - config.PullSecret = "" - if config.Platform.VSphere != nil { - p := *config.Platform.VSphere - p.Username = "" - p.Password = "" - config.Platform.VSphere = &p - } - return yaml.Marshal(config) + newConfig := config + + newConfig.PullSecret = "" + if newConfig.Platform.VSphere != nil { + p := config.VSphere + newVCenters := make([]vsphere.VCenter, len(p.VCenters)) + for i, v := range p.VCenters { + newVCenters[i].Server = v.Server + newVCenters[i].Datacenters = v.Datacenters + } + newVSpherePlatform := vsphere.Platform{ + DeprecatedVCenter: p.DeprecatedVCenter, + DeprecatedUsername: "", + DeprecatedPassword: "", + DeprecatedDatacenter: p.DeprecatedDatacenter, + DeprecatedDefaultDatastore: p.DeprecatedDefaultDatastore, + DeprecatedFolder: p.DeprecatedFolder, + DeprecatedCluster: p.DeprecatedCluster, + DeprecatedResourcePool: p.DeprecatedResourcePool, + ClusterOSImage: p.ClusterOSImage, + DeprecatedAPIVIP: p.DeprecatedAPIVIP, + APIVIPs: p.APIVIPs, + DeprecatedIngressVIP: p.DeprecatedIngressVIP, + IngressVIPs: p.IngressVIPs, + DefaultMachinePlatform: p.DefaultMachinePlatform, + DeprecatedNetwork: p.DeprecatedNetwork, + DiskType: p.DiskType, + VCenters: newVCenters, + FailureDomains: p.FailureDomains, + } + newConfig.Platform.VSphere = &newVSpherePlatform + } + + return yaml.Marshal(newConfig) } func indent(indention int, v string) string { diff --git a/pkg/asset/manifests/vsphere/infrastructure.go b/pkg/asset/manifests/vsphere/infrastructure.go index dfa2588618f..cee6e970b2d 100644 --- a/pkg/asset/manifests/vsphere/infrastructure.go +++ b/pkg/asset/manifests/vsphere/infrastructure.go @@ -1,8 +1,6 @@ package vsphere import ( - "fmt" - configv1 "github.com/openshift/api/config/v1" "github.com/openshift/installer/pkg/asset/installconfig" ) @@ -12,22 +10,17 @@ func GetInfraPlatformSpec(ic *installconfig.InstallConfig) *configv1.VSpherePlat var platformSpec configv1.VSpherePlatformSpec icPlatformSpec := ic.Config.VSphere - if len(icPlatformSpec.FailureDomains) == 0 { + for _, vcenter := range icPlatformSpec.VCenters { platformSpec.VCenters = append(platformSpec.VCenters, configv1.VSpherePlatformVCenterSpec{ - Server: icPlatformSpec.VCenter, - Port: 443, - Datacenters: []string{icPlatformSpec.Datacenter}, + Server: vcenter.Server, + Port: vcenter.Port, + Datacenters: vcenter.Datacenters, }) - } else { - for _, vcenter := range icPlatformSpec.VCenters { - platformSpec.VCenters = append(platformSpec.VCenters, configv1.VSpherePlatformVCenterSpec{ - Server: vcenter.Server, - Port: int32(vcenter.Port), - Datacenters: vcenter.Datacenters, - }) - } - for _, failureDomain := range icPlatformSpec.FailureDomains { - topology := failureDomain.Topology + } + + for _, failureDomain := range icPlatformSpec.FailureDomains { + topology := failureDomain.Topology + if topology.ComputeCluster != "" && topology.Networks[0] != "" { platformSpec.FailureDomains = append(platformSpec.FailureDomains, configv1.VSpherePlatformFailureDomainSpec{ Name: failureDomain.Name, Region: failureDomain.Region, @@ -37,7 +30,7 @@ func GetInfraPlatformSpec(ic *installconfig.InstallConfig) *configv1.VSpherePlat Datacenter: topology.Datacenter, ComputeCluster: topology.ComputeCluster, Networks: topology.Networks, - Datastore: fmt.Sprintf("/%s/datastore/%s", topology.Datacenter, topology.Datastore), + Datastore: topology.Datastore, ResourcePool: topology.ResourcePool, Folder: topology.Folder, }, diff --git a/pkg/destroy/vsphere/vsphere.go b/pkg/destroy/vsphere/vsphere.go index 0f871a42afe..a4c569442e1 100644 --- a/pkg/destroy/vsphere/vsphere.go +++ b/pkg/destroy/vsphere/vsphere.go @@ -14,7 +14,6 @@ import ( "github.com/openshift/installer/pkg/destroy/providers" installertypes "github.com/openshift/installer/pkg/types" - vspheretypes "github.com/openshift/installer/pkg/types/vsphere" ) // ClusterUninstaller holds the various options for the cluster we want to delete. @@ -24,7 +23,6 @@ type ClusterUninstaller struct { terraformPlatform string Logger logrus.FieldLogger - client API } @@ -59,15 +57,6 @@ func (o *ClusterUninstaller) deleteFolder(ctx context.Context) error { return err } - // The installer should create at most one parent, - // the parent to the VirtualMachines. - // If there are more or less fail with error message. - if o.terraformPlatform != vspheretypes.ZoningTerraformName { - if len(folderMoList) > 1 { - return errors.Errorf("Expected 1 Folder per tag but got %d", len(folderMoList)) - } - } - if len(folderMoList) == 0 { o.Logger.Debug("All folders deleted") return nil diff --git a/pkg/types/conversion/installconfig.go b/pkg/types/conversion/installconfig.go index ed270a5bcf6..fb11abe5239 100644 --- a/pkg/types/conversion/installconfig.go +++ b/pkg/types/conversion/installconfig.go @@ -16,6 +16,7 @@ import ( "github.com/openshift/installer/pkg/types/openstack" "github.com/openshift/installer/pkg/types/ovirt" "github.com/openshift/installer/pkg/types/vsphere" + vsphereconversion "github.com/openshift/installer/pkg/types/vsphere/conversion" ) // ConvertInstallConfig is modeled after the k8s conversion schemes, which is @@ -52,6 +53,9 @@ func ConvertInstallConfig(config *types.InstallConfig) error { return err } case vsphere.Name: + if err := vsphereconversion.ConvertInstallConfig(config); err != nil { + return err + } if err := convertVSphere(config); err != nil { return err } diff --git a/pkg/types/vsphere/conversion/installconfig.go b/pkg/types/vsphere/conversion/installconfig.go new file mode 100644 index 00000000000..6f7637cad80 --- /dev/null +++ b/pkg/types/vsphere/conversion/installconfig.go @@ -0,0 +1,128 @@ +package conversion + +import ( + "fmt" + "strings" + + "github.com/sirupsen/logrus" + + "github.com/openshift/installer/pkg/types" + "github.com/openshift/installer/pkg/types/vsphere" +) + +var localLogger = logrus.New() + +func ConvertInstallConfig(config *types.InstallConfig) error { + platform := config.Platform.VSphere + + // Scenario: IPI or 4.12 Zonal IPI w/o vcenters defined + if len(platform.VCenters) == 0 { + createVCenters(platform) + + // Scenario: 4.12 Zonal IPI + if len(platform.FailureDomains) > 0 { + for i := range platform.FailureDomains { + if platform.FailureDomains[i].Topology.Datacenter == "" { + platform.FailureDomains[i].Topology.Datacenter = platform.DeprecatedDatacenter + } + if platform.FailureDomains[i].Server == "" { + // Assumption: by the time it is possible to use multiple vcenters + // it will be past 4.15 + // so this conversion can be removed. + platform.FailureDomains[i].Server = platform.VCenters[0].Server + } + } + } + } + + // Scenario: Fields are not paths + if len(platform.FailureDomains) > 0 { + for i := range platform.FailureDomains { + platform.FailureDomains[i].Topology.ComputeCluster = setComputeClusterPath(platform.FailureDomains[i].Topology.ComputeCluster, + platform.FailureDomains[i].Topology.Datacenter) + + platform.FailureDomains[i].Topology.Datastore = setDatastorePath(platform.FailureDomains[i].Topology.Datastore, + platform.FailureDomains[i].Topology.Datacenter) + + platform.FailureDomains[i].Topology.Folder = setFolderPath(platform.FailureDomains[i].Topology.Folder, + platform.FailureDomains[i].Topology.Datacenter) + } + } + + // Scenario: legacy UPI or IPI + if len(platform.FailureDomains) == 0 { + localLogger.Warn("vsphere topology fields are now depreciated please use failureDomains") + + platform.FailureDomains = make([]vsphere.FailureDomain, 1) + platform.FailureDomains[0].Name = "generated-failure-domain" + platform.FailureDomains[0].Server = platform.VCenters[0].Server + platform.FailureDomains[0].Region = "generated-region" + platform.FailureDomains[0].Zone = "generated-zone" + + platform.FailureDomains[0].Topology.Datacenter = platform.DeprecatedDatacenter + platform.FailureDomains[0].Topology.ResourcePool = platform.DeprecatedResourcePool + platform.FailureDomains[0].Topology.ComputeCluster = setComputeClusterPath(platform.DeprecatedCluster, platform.DeprecatedDatacenter) + platform.FailureDomains[0].Topology.Networks = make([]string, 1) + platform.FailureDomains[0].Topology.Networks[0] = platform.DeprecatedNetwork + platform.FailureDomains[0].Topology.Datastore = setDatastorePath(platform.DeprecatedDefaultDatastore, platform.DeprecatedDatacenter) + platform.FailureDomains[0].Topology.Folder = setFolderPath(platform.DeprecatedFolder, platform.DeprecatedDatacenter) + } + + return nil +} + +func setComputeClusterPath(cluster, datacenter string) string { + if cluster != "" && !strings.HasPrefix(cluster, "/") { + localLogger.Warnf("computeCluster as a non-path is now depreciated please use the form: /%s/host/%s", datacenter, cluster) + return fmt.Sprintf("/%s/host/%s", datacenter, cluster) + } + return cluster +} + +func setDatastorePath(datastore, datacenter string) string { + if datastore != "" && !strings.HasPrefix(datastore, "/") { + localLogger.Warnf("datastore as a non-path is now depreciated please use the form: /%s/datastore/%s", datacenter, datastore) + return fmt.Sprintf("/%s/datastore/%s", datacenter, datastore) + } + return datastore +} + +func setFolderPath(folder, datacenter string) string { + if folder != "" && !strings.HasPrefix(folder, "/") { + localLogger.Warnf("folder as a non-path is now depreciated please use the form: /%s/vm/%s", datacenter, folder) + return fmt.Sprintf("/%s/vm/%s", datacenter, folder) + } + return folder +} + +func createVCenters(platform *vsphere.Platform) { + localLogger.Warn("vsphere authentication fields are now depreciated please use vcenters") + + platform.VCenters = make([]vsphere.VCenter, 1) + platform.VCenters[0].Server = platform.DeprecatedVCenter + platform.VCenters[0].Username = platform.DeprecatedUsername + platform.VCenters[0].Password = platform.DeprecatedPassword + platform.VCenters[0].Port = 443 + + if platform.DeprecatedDatacenter != "" { + platform.VCenters[0].Datacenters = append(platform.VCenters[0].Datacenters, platform.DeprecatedDatacenter) + } + + // Scenario: Zonal IPI w/o vcenters defined + // Confirms the list of datacenters from FailureDomains are updated + // in vcenters[0].datacenters + for _, failureDomain := range platform.FailureDomains { + found := false + if failureDomain.Topology.Datacenter != "" { + for _, dc := range platform.VCenters[0].Datacenters { + if dc == failureDomain.Topology.Datacenter { + found = true + } + } + + if !found { + platform.VCenters[0].Datacenters = append(platform.VCenters[0].Datacenters, failureDomain.Topology.Datacenter) + } + } + } +} diff --git a/pkg/types/vsphere/defaults/platform.go b/pkg/types/vsphere/defaults/platform.go index b0c77c3b002..a293416f2ac 100644 --- a/pkg/types/vsphere/defaults/platform.go +++ b/pkg/types/vsphere/defaults/platform.go @@ -1,11 +1,22 @@ package defaults import ( + "fmt" + "github.com/openshift/installer/pkg/types" "github.com/openshift/installer/pkg/types/vsphere" ) // SetPlatformDefaults sets the defaults for the platform. func SetPlatformDefaults(p *vsphere.Platform, installConfig *types.InstallConfig) { - + // We need to deploy templates (OVA) via DeploymentZones + // since we could have compute (workers) in those zones + // but _not_ control plane nodes. If the placementConstraints + // are not defined we must use the default for the datacenter + // and cluster. + for i := range p.FailureDomains { + if p.FailureDomains[i].Topology.ResourcePool == "" { + p.FailureDomains[i].Topology.ResourcePool = fmt.Sprintf("%s/%s", p.FailureDomains[i].Topology.ComputeCluster, "/Resources") + } + } } diff --git a/pkg/types/vsphere/platform.go b/pkg/types/vsphere/platform.go index a1b8de769ff..1bb67ad7ca1 100644 --- a/pkg/types/vsphere/platform.go +++ b/pkg/types/vsphere/platform.go @@ -27,28 +27,36 @@ const ( TagCategoryZone = "openshift-zone" ) -// Platform stores any global configuration used for vsphere platforms +// Platform stores any global configuration used for vsphere platforms. type Platform struct { // VCenter is the domain name or IP address of the vCenter. - VCenter string `json:"vCenter"` + // Deprecated: Use VCenters.Server + DeprecatedVCenter string `json:"vCenter,omitempty"` // Username is the name of the user to use to connect to the vCenter. - Username string `json:"username"` + // Deprecated: Use VCenters.Username + DeprecatedUsername string `json:"username,omitempty"` // Password is the password for the user to use to connect to the vCenter. - Password string `json:"password"` + // Deprecated: Use VCenters.Password + DeprecatedPassword string `json:"password,omitempty"` // Datacenter is the name of the datacenter to use in the vCenter. - Datacenter string `json:"datacenter"` + // Deprecated: Use FailureDomains.Topology.Datacenter + DeprecatedDatacenter string `json:"datacenter,omitempty"` // DefaultDatastore is the default datastore to use for provisioning volumes. - DefaultDatastore string `json:"defaultDatastore"` + // Deprecated: Use FailureDomains.Topology.Datastore + DeprecatedDefaultDatastore string `json:"defaultDatastore,omitempty"` // Folder is the absolute path of the folder that will be used and/or created for // virtual machines. The absolute path is of the form //vm//. // +kubebuilder:validation:Pattern=`^/.*?/vm/.*?` // +optional - Folder string `json:"folder,omitempty"` + // Deprecated: Use FailureDomains.Topology.Folder + DeprecatedFolder string `json:"folder,omitempty"` // Cluster is the name of the cluster virtual machines will be cloned into. - Cluster string `json:"cluster,omitempty"` + // Deprecated: Use FailureDomains.Topology.Cluster + DeprecatedCluster string `json:"cluster,omitempty"` // ResourcePool is the absolute path of the resource pool where virtual machines will be // created. The absolute path is of the form //host//Resources/. - ResourcePool string `json:"resourcePool,omitempty"` + // Deprecated: Use FailureDomains.Topology.ResourcePool + DeprecatedResourcePool string `json:"resourcePool,omitempty"` // ClusterOSImage overrides the url provided in rhcos.json to download the RHCOS OVA ClusterOSImage string `json:"clusterOSImage,omitempty"` @@ -90,7 +98,8 @@ type Platform struct { // +optional DefaultMachinePlatform *MachinePool `json:"defaultMachinePlatform,omitempty"` // Network specifies the name of the network to be used by the cluster. - Network string `json:"network,omitempty"` + // Deprecated: Use FailureDomains.Topology.Network + DeprecatedNetwork string `json:"network,omitempty"` // DiskType is the name of the disk provisioning type, // valid values are thin, thick, and eagerZeroedThick. When not // specified, it will be set according to the default storage policy @@ -113,7 +122,7 @@ type Platform struct { // the vCenter topology of that failure domain. type FailureDomain struct { // name defines the name of the FailureDomain - // This name is abritrary but will be used + // This name is arbitrary but will be used // in VSpherePlatformDeploymentZone for association. // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 @@ -192,7 +201,7 @@ type VCenter struct { // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=32767 // +kubebuilder:default=443 - Port uint `json:"port,omitempty"` + Port int32 `json:"port,omitempty"` // Username is the username that will be used to connect to vCenter // +kubebuilder:validation:Required Username string `json:"user"` diff --git a/pkg/types/vsphere/validation/platform.go b/pkg/types/vsphere/validation/platform.go index d7da6e37fd8..3801086651c 100644 --- a/pkg/types/vsphere/validation/platform.go +++ b/pkg/types/vsphere/validation/platform.go @@ -5,6 +5,7 @@ import ( "regexp" "strings" + "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" @@ -14,129 +15,33 @@ import ( // ValidatePlatform checks that the specified platform is valid. func ValidatePlatform(p *vsphere.Platform, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - if !validate.IsAgentBasedInstallation() { - if len(p.VCenter) == 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("vCenter"), "must specify the name of the vCenter")) - } - if len(p.Username) == 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("username"), "must specify the username")) - } - if len(p.Password) == 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("password"), "must specify the password")) - } - if len(p.Datacenter) == 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("datacenter"), "must specify the datacenter")) - } - if len(p.DefaultDatastore) == 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("defaultDatastore"), "must specify the default datastore")) - } - } + isLegacyUpi := false + // This is to cover existing UPI non-zonal case + // where neither network or cluster is required. + // In 4.13 we will warn for this, in later releases this + // should be removed. - if len(p.VCenter) != 0 { - if err := validate.Host(p.VCenter); err != nil { - allErrs = append(allErrs, field.Invalid(fldPath.Child("vCenter"), p.VCenter, "must be the domain name or IP address of the vCenter")) - } - } - - // folder is optional, but if provided should pass validation - if len(p.Folder) != 0 { - allErrs = append(allErrs, validateFolder(p, fldPath)...) - } - - // resource pool is optional, but if provided should pass validation - if len(p.ResourcePool) != 0 { - allErrs = append(allErrs, validateResourcePool(p, fldPath)...) + if p.DeprecatedNetwork == "" && p.DeprecatedCluster == "" && p.DeprecatedVCenter != "" { + isLegacyUpi = true } + allErrs := field.ErrorList{} // diskType is optional, but if provided should pass validation if len(p.DiskType) != 0 { allErrs = append(allErrs, validateDiskType(p, fldPath)...) } - if len(p.FailureDomains) > 0 || len(p.VCenters) > 0 { - allErrs = append(allErrs, validateMultiZone(p, fldPath)...) - } - - return allErrs -} - -func validateMultiZone(p *vsphere.Platform, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - if len(p.VCenters) == 0 { - // if p.VCenters is empty, populate a single vCenter based on the legacy platform spec - p.VCenters = append(p.VCenters, vsphere.VCenter{ - Server: p.VCenter, - Port: 443, - Username: p.Username, - Password: p.Password, - Datacenters: []string{p.Datacenter}, - }) - } - - // populate failure domains that dont explicitly define a server - for idx, failureDomain := range p.FailureDomains { - if len(failureDomain.Server) == 0 { - p.FailureDomains[idx].Server = p.VCenter - } - if len(failureDomain.Topology.Datacenter) == 0 { - p.FailureDomains[idx].Topology.Datacenter = p.Datacenter - } - if len(failureDomain.Topology.ComputeCluster) == 0 { - p.FailureDomains[idx].Topology.ComputeCluster = fmt.Sprintf("/%s/host/%s", p.Datacenter, p.Cluster) - } - if len(failureDomain.Topology.Networks) == 0 && len(p.Network) > 0 { - if len(failureDomain.Topology.Networks) == 0 { - p.FailureDomains[idx].Topology.Networks = []string{p.Network} - } - } - if len(failureDomain.Topology.Datastore) == 0 { - p.FailureDomains[idx].Topology.Datastore = p.DefaultDatastore - } - if len(failureDomain.Topology.Folder) == 0 { - // If the legacy folder is not defined we can't use it for FailureDomain - if len(p.Folder) != 0 { - // Only use the legacy folder platform spec parameter if the datacenter exists in the path. - if strings.Contains(p.Folder, p.FailureDomains[idx].Topology.Datacenter) { - p.FailureDomains[idx].Topology.Folder = p.Folder - } else { - allErrs = append(allErrs, field.Invalid(fldPath.Child("folder"), p.Folder, fmt.Sprintf("folder must be in datacenter %s; please define it in a topology", p.FailureDomains[idx].Topology.Datacenter))) - } - } + if !validate.IsAgentBasedInstallation() { + if len(p.VCenters) == 0 { + return append(allErrs, field.Required(fldPath.Child("vcenters"), "must be defined")) } + allErrs = append(allErrs, validateVCenters(p, fldPath.Child("vcenters"))...) - // Always try to set the default resourcePool - if len(failureDomain.Topology.ResourcePool) == 0 { - if len(p.ResourcePool) != 0 { - if strings.Contains(p.ResourcePool, p.FailureDomains[idx].Topology.Datacenter) { - // Only use the legacy resourcePool platform spec parameter if the datacenter exists in the path. - if strings.Contains(p.ResourcePool, p.FailureDomains[idx].Topology.ComputeCluster) { - p.FailureDomains[idx].Topology.ResourcePool = p.ResourcePool - } else { - allErrs = append(allErrs, field.Invalid(fldPath.Child("resourcePool"), p.ResourcePool, fmt.Sprintf("resource pool must be in compute cluster %s; please define it in a topology", p.FailureDomains[idx].Topology.ComputeCluster))) - } - } else { - allErrs = append(allErrs, field.Invalid(fldPath.Child("resourcePool"), p.ResourcePool, fmt.Sprintf("resource pool must be in datacenter %s; please define it in a topology", p.FailureDomains[idx].Topology.Datacenter))) - } - } else { - // Default to the resourcePool inside compute cluster since there is no legacy resourcePool - p.FailureDomains[idx].Topology.ResourcePool = fmt.Sprintf("%s/%s", p.FailureDomains[idx].Topology.ComputeCluster, "Resources") - } + if len(p.FailureDomains) == 0 { + return append(allErrs, field.Required(fldPath.Child("failureDomains"), "must be defined")) } - } - - allErrs = append(allErrs, validateVCenters(p, fldPath.Child("vcenters"))...) - if len(allErrs) > 0 { - // if vcenters fails validation, this will cascade to failureDomains and deploymentZones - return allErrs - } - - if len(p.FailureDomains) > 0 { - allErrs = append(allErrs, validateFailureDomains(p, fldPath.Child("failureDomains"))...) - } else if len(p.VCenters) > 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("failureDomains"), "must be defined if vcenters is defined")) + allErrs = append(allErrs, validateFailureDomains(p, fldPath.Child("failureDomains"), isLegacyUpi)...) } return allErrs @@ -169,7 +74,7 @@ func validateVCenters(p *vsphere.Platform, fldPath *field.Path) field.ErrorList return allErrs } -func validateFailureDomains(p *vsphere.Platform, fldPath *field.Path) field.ErrorList { +func validateFailureDomains(p *vsphere.Platform, fldPath *field.Path, isLegacyUpi bool) field.ErrorList { allErrs := field.ErrorList{} topologyFld := fldPath.Child("topology") var associatedVCenter *vsphere.VCenter @@ -178,9 +83,9 @@ func validateFailureDomains(p *vsphere.Platform, fldPath *field.Path) field.Erro allErrs = append(allErrs, field.Required(fldPath.Child("name"), "must specify the name")) } if len(failureDomain.Server) > 0 { - for _, vcenter := range p.VCenters { + for i, vcenter := range p.VCenters { if vcenter.Server == failureDomain.Server { - associatedVCenter = &vcenter + associatedVCenter = &p.VCenters[i] break } } @@ -202,17 +107,33 @@ func validateFailureDomains(p *vsphere.Platform, fldPath *field.Path) field.Erro if len(failureDomain.Topology.Datacenter) == 0 { allErrs = append(allErrs, field.Required(topologyFld.Child("datacenter"), "must specify a datacenter")) } - if len(failureDomain.Topology.Datastore) == 0 { allErrs = append(allErrs, field.Required(topologyFld.Child("datastore"), "must specify a datastore")) + } else { + datastore := failureDomain.Topology.Datastore + + datastorePathRegexp := regexp.MustCompile(`^/(.*?)/datastore/(.*?)$`) + datastorePathParts := datastorePathRegexp.FindStringSubmatch(datastore) + if len(datastorePathParts) < 3 { + return append(allErrs, field.Invalid(topologyFld.Child("datastore"), datastore, "full path of datastore must be provided in format /")) + } + + if !strings.Contains(failureDomain.Topology.Datastore, failureDomain.Topology.Datacenter) { + return append(allErrs, field.Invalid(topologyFld.Child("datastore"), failureDomain.Topology.Datastore, "the datastore defined does not exist in the correct datacenter")) + } } if len(failureDomain.Topology.Networks) == 0 { - allErrs = append(allErrs, field.Required(topologyFld.Child("networks"), "must specify a network")) + if isLegacyUpi { + logrus.Warn("network field empty is now deprecated, in later releases this field will be required.") + } else { + allErrs = append(allErrs, field.Required(topologyFld.Child("networks"), "must specify a network")) + } } + // Folder in failuredomain is optional if len(failureDomain.Topology.Folder) != 0 { - folderPathRegexp := regexp.MustCompile(`^\/(.*?)\/vm\/(.*?)$`) + folderPathRegexp := regexp.MustCompile(`^/(.*?)/vm/(.*?)$`) folderPathParts := folderPathRegexp.FindStringSubmatch(failureDomain.Topology.Folder) if len(folderPathParts) < 3 { return append(allErrs, field.Invalid(topologyFld.Child("folder"), failureDomain.Topology.Folder, "full path of folder must be provided in format //vm/")) @@ -224,10 +145,14 @@ func validateFailureDomains(p *vsphere.Platform, fldPath *field.Path) field.Erro } if len(failureDomain.Topology.ComputeCluster) == 0 { - allErrs = append(allErrs, field.Required(topologyFld.Child("computeCluster"), "must specify a computeCluster")) + if isLegacyUpi { + logrus.Warn("cluster field empty is not deprecated, in later releases this field will be required.") + } else { + allErrs = append(allErrs, field.Required(topologyFld.Child("computeCluster"), "must specify a computeCluster")) + } } else { computeCluster := failureDomain.Topology.ComputeCluster - clusterPathRegexp := regexp.MustCompile(`^\/(.*?)\/host\/(.*?)$`) + clusterPathRegexp := regexp.MustCompile(`^/(.*?)/host/(.*?)$`) clusterPathParts := clusterPathRegexp.FindStringSubmatch(computeCluster) if len(clusterPathParts) < 3 { return append(allErrs, field.Invalid(topologyFld.Child("computeCluster"), computeCluster, "full path of compute cluster must be provided in format //host/")) @@ -260,61 +185,6 @@ func validateFailureDomains(p *vsphere.Platform, fldPath *field.Path) field.Erro return allErrs } -// ValidateForProvisioning checks that the specified platform is valid. -func ValidateForProvisioning(p *vsphere.Platform, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - if len(p.Cluster) == 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("cluster"), "must specify the cluster")) - } - - if len(p.Network) == 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("network"), "must specify the network")) - } - - return allErrs -} - -// validateFolder checks that a provided folder is an absolute path in the correct datacenter. -func validateFolder(p *vsphere.Platform, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - dc := p.Datacenter - if len(dc) == 0 { - dc = "" - } - expectedPrefix := fmt.Sprintf("/%s/vm/", dc) - - if !strings.HasPrefix(p.Folder, expectedPrefix) { - errMsg := fmt.Sprintf("folder must be absolute path: expected prefix %s", expectedPrefix) - allErrs = append(allErrs, field.Invalid(fldPath.Child("folder"), p.Folder, errMsg)) - } - - return allErrs -} - -// validateResourcePool checks that a provided resource pool is an absolute path in the correct cluster. -func validateResourcePool(p *vsphere.Platform, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - dc := p.Datacenter - if len(dc) == 0 { - dc = "" - } - cluster := p.Cluster - if len(cluster) == 0 { - cluster = "" - } - expectedPrefix := fmt.Sprintf("/%s/host/%s/Resources/", dc, cluster) - - if !strings.HasPrefix(p.ResourcePool, expectedPrefix) { - errMsg := fmt.Sprintf("resourcePool must be absolute path: expected prefix %s", expectedPrefix) - allErrs = append(allErrs, field.Invalid(fldPath.Child("resourcePool"), p.ResourcePool, errMsg)) - } - - return allErrs -} - // validateDiskType checks that the specified diskType is valid func validateDiskType(p *vsphere.Platform, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} From 5a465c4763142b86c298049e8d27beb7b5a0a89b Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Thu, 9 Feb 2023 13:40:09 -0500 Subject: [PATCH 6/8] tests --- pkg/asset/agent/installconfig_test.go | 41 +- .../installconfig/vsphere/mock/vsphere_sim.go | 52 +- .../installconfig/vsphere/permission_test.go | 324 ++++------ .../installconfig/vsphere/validation_test.go | 597 ++++++++++++------ pkg/asset/machines/vsphere/machines_test.go | 81 +-- .../machines/vsphere/machinesets_test.go | 68 +- pkg/asset/manifests/operators_test.go | 52 +- .../vsphere/cloudproviderconfig_test.go | 88 ++- pkg/destroy/vsphere/vsphere_test.go | 14 +- pkg/types/conversion/installconfig_test.go | 42 ++ pkg/types/validation/installconfig_test.go | 57 +- .../vsphere/conversion/installconfig_test.go | 593 +++++++++++++++++ .../vsphere/validation/machinepool_test.go | 11 +- pkg/types/vsphere/validation/platform_test.go | 280 +------- 14 files changed, 1457 insertions(+), 843 deletions(-) create mode 100644 pkg/types/vsphere/conversion/installconfig_test.go diff --git a/pkg/asset/agent/installconfig_test.go b/pkg/asset/agent/installconfig_test.go index 19912451965..591b1071839 100644 --- a/pkg/asset/agent/installconfig_test.go +++ b/pkg/asset/agent/installconfig_test.go @@ -102,7 +102,7 @@ platform: pullSecret: "{\"auths\":{\"example.com\":{\"auth\":\"authorization value\"}}}" `, expectedFound: false, - expectedError: "failed to create install config: invalid \"install-config.yaml\" file: [platform.vsphere.apiVIPs: Invalid value: \"192.168.122.10\": IP expected to be in one of the machine networks: 10.0.0.0/16, platform.vsphere.ingressVIPs: Required value: must specify VIP for ingress, when VIP for API is set, platform.vsphere.vCenter: Required value: must specify the name of the vCenter, platform.vsphere.username: Required value: must specify the username, platform.vsphere.password: Required value: must specify the password, platform.vsphere.datacenter: Required value: must specify the datacenter, platform.vsphere.defaultDatastore: Required value: must specify the default datastore]", + expectedError: `failed to create install config: invalid "install-config.yaml" file: [platform.vsphere.apiVIPs: Invalid value: "192.168.122.10": IP expected to be in one of the machine networks: 10.0.0.0/16, platform.vsphere.ingressVIPs: Required value: must specify VIP for ingress, when VIP for API is set, platform.vsphere.vcenters.server: Required value: must be the domain name or IP address of the vCenter, platform.vsphere.vcenters.username: Required value: must specify the username, platform.vsphere.vcenters.password: Required value: must specify the password, platform.vsphere.vcenters.datacenters: Required value: must specify at least one datacenter, platform.vsphere.failureDomains.server: Required value: must specify a vCenter server, platform.vsphere.failureDomains.topology.datacenter: Required value: must specify a datacenter, platform.vsphere.failureDomains.topology.datastore: Required value: must specify a datastore, platform.vsphere.failureDomains.topology.computeCluster: Required value: must specify a computeCluster, platform.vsphere.failureDomains.topology.resourcePool: Invalid value: "//Resources": full path of resource pool must be provided in format //host//...]`, }, { name: "invalid configuration for none platform for sno", @@ -505,6 +505,7 @@ platform: password: testPassword datacenter: testDataCenter defaultDataStore: testDefaultDataStore + cluster: testCluster apiVIP: 192.168.122.10 ingressVIPs: - 192.168.122.11 @@ -549,14 +550,36 @@ pullSecret: "{\"auths\":{\"example.com\":{\"auth\":\"authorization value\"}}}" }, Platform: types.Platform{ VSphere: &vsphere.Platform{ - VCenter: "192.168.122.30", - Username: "testUsername", - Password: "testPassword", - Datacenter: "testDataCenter", - DefaultDatastore: "testDefaultDataStore", - DeprecatedAPIVIP: "192.168.122.10", - APIVIPs: []string{"192.168.122.10"}, - IngressVIPs: []string{"192.168.122.11"}, + DeprecatedVCenter: "192.168.122.30", + DeprecatedUsername: "testUsername", + DeprecatedPassword: "testPassword", + DeprecatedDatacenter: "testDataCenter", + DeprecatedCluster: "testCluster", + DeprecatedDefaultDatastore: "testDefaultDataStore", + DeprecatedAPIVIP: "192.168.122.10", + APIVIPs: []string{"192.168.122.10"}, + IngressVIPs: []string{"192.168.122.11"}, + VCenters: []vsphere.VCenter{{ + Server: "192.168.122.30", + Port: 443, + Username: "testUsername", + Password: "testPassword", + Datacenters: []string{"testDataCenter"}, + }}, + FailureDomains: []vsphere.FailureDomain{{ + Name: "generated-failure-domain", + Region: "generated-region", + Zone: "generated-zone", + Server: "192.168.122.30", + Topology: vsphere.Topology{ + Datacenter: "testDataCenter", + ComputeCluster: "/testDataCenter/host/testCluster", + Networks: []string{""}, + Datastore: "/testDataCenter/datastore/testDefaultDataStore", + ResourcePool: "/testDataCenter/host/testCluster//Resources", + Folder: "", + }, + }}, }, }, PullSecret: `{"auths":{"example.com":{"auth":"authorization value"}}}`, diff --git a/pkg/asset/installconfig/vsphere/mock/vsphere_sim.go b/pkg/asset/installconfig/vsphere/mock/vsphere_sim.go index 6b6aa4293d8..f8911089269 100644 --- a/pkg/asset/installconfig/vsphere/mock/vsphere_sim.go +++ b/pkg/asset/installconfig/vsphere/mock/vsphere_sim.go @@ -5,11 +5,14 @@ import ( "crypto/tls" "encoding/pem" "errors" + "io/fs" "os" + "strconv" "github.com/vmware/govmomi/find" "github.com/vmware/govmomi/session" "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/simulator/esx" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/soap" @@ -19,27 +22,55 @@ import ( _ "github.com/vmware/govmomi/vapi/simulator" ) +const ( + esxi7U2BuildNumber int = 17630552 + esxi7U2Version string = "7.0.2" + vcenter7U2BuildNumber int = 17694817 + vcenter7U2Version string = "7.0.2" +) + // StartSimulator starts an instance of the simulator which listens on 127.0.0.1. // Call GetClient to retrieve a vim25.client which will connect to and trust this // simulator -func StartSimulator() *simulator.Server { +func StartSimulator(setVersionToSupported bool) (*simulator.Server, error) { model := simulator.VPX() + + // Change the simulated vCenter and ESXi hosts + // to the version and build we support. + if setVersionToSupported { + esx.HostSystem.Config.Product.Build = strconv.Itoa(esxi7U2BuildNumber) + esx.HostSystem.Config.Product.Version = esxi7U2Version + model.ServiceContent.About.Build = strconv.Itoa(vcenter7U2BuildNumber) + model.ServiceContent.About.Version = vcenter7U2Version + } + model.Folder = 1 model.Datacenter = 2 model.OpaqueNetwork = 1 - model.Create() + err := model.Create() + if err != nil { + return nil, err + } + model.Service.TLS = new(tls.Config) model.Service.TLS.ServerName = "127.0.0.1" model.Service.RegisterEndpoints = true server := model.Service.NewServer() - return server + return server, nil } // GetClient returns a vim25 client which connects to and trusts the simulator func GetClient(server *simulator.Server) (*vim25.Client, *session.Manager, error) { tmpCAdir := "/tmp/vcsimca" - os.Mkdir(tmpCAdir, os.ModePerm) + err := os.Mkdir(tmpCAdir, os.ModePerm) + + if err != nil { + // If the error is not file existing return err + if !errors.Is(err, fs.ErrExist) { + return nil, nil, err + } + } pemBlock := pem.Block{ Type: "CERTIFICATE", Headers: nil, @@ -49,11 +80,20 @@ func GetClient(server *simulator.Server) (*vim25.Client, *session.Manager, error if err != nil { return nil, nil, err } - tempFile.Write(pem.EncodeToMemory(&pemBlock)) + _, err = tempFile.Write(pem.EncodeToMemory(&pemBlock)) + if err != nil { + return nil, nil, err + } soapClient := soap.NewClient(server.URL, false) - soapClient.SetRootCAs(tempFile.Name()) + err = soapClient.SetRootCAs(tempFile.Name()) + if err != nil { + return nil, nil, err + } vimClient, err := vim25.NewClient(context.TODO(), soapClient) + if err != nil { + return nil, nil, err + } sessionMgr := session.NewManager(vimClient) if sessionMgr == nil { return nil, nil, errors.New("unable to retrieve session manager") diff --git a/pkg/asset/installconfig/vsphere/permission_test.go b/pkg/asset/installconfig/vsphere/permission_test.go index b1242d889ff..550e1aafd45 100644 --- a/pkg/asset/installconfig/vsphere/permission_test.go +++ b/pkg/asset/installconfig/vsphere/permission_test.go @@ -19,13 +19,14 @@ import ( "github.com/openshift/installer/pkg/types/vsphere" ) -func buildPermissionGroup(authManagerMock *mock.MockAuthManager, +func buildPermissionGroup(t *testing.T, authManagerMock *mock.MockAuthManager, managedObjectRef vim25types.ManagedObjectReference, username string, group PermissionGroupDefinition, groupName permissionGroup, overrideGroup *permissionGroup, permissionToExcludeSet sets.String) { + t.Helper() permissionsToApply := group.Permissions if overrideGroup != nil && *overrideGroup == groupName { @@ -43,12 +44,14 @@ func buildPermissionGroup(authManagerMock *mock.MockAuthManager, } func buildAuthManagerClient(ctx context.Context, + t *testing.T, mockCtrl *gomock.Controller, finder Finder, username string, overrideGroup *permissionGroup, permissionsToRemoveFromResource sets.String, permissionsToRemoveFromAvailable sets.String) (*mock.MockAuthManager, error) { + t.Helper() authManagerClient := mock.NewMockAuthManager(mockCtrl) authManagerMo := vim25types.ManagedObjectReference{ Type: "auth-manager", @@ -72,14 +75,14 @@ func buildAuthManagerClient(ctx context.Context, if err != nil { return nil, err } - buildPermissionGroup(authManagerClient, vcenter.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) + buildPermissionGroup(t, authManagerClient, vcenter.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) case permissionDatacenter: datacenters, err := finder.DatacenterList(ctx, "/...") if err != nil { return nil, err } for _, datacenter := range datacenters { - buildPermissionGroup(authManagerClient, datacenter.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) + buildPermissionGroup(t, authManagerClient, datacenter.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) } case permissionDatastore: datastores, err := finder.DatastoreList(ctx, "/...") @@ -87,7 +90,7 @@ func buildAuthManagerClient(ctx context.Context, return nil, err } for _, datastore := range datastores { - buildPermissionGroup(authManagerClient, datastore.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) + buildPermissionGroup(t, authManagerClient, datastore.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) } case permissionCluster: clusters, err := finder.ClusterComputeResourceList(ctx, "/...") @@ -95,7 +98,7 @@ func buildAuthManagerClient(ctx context.Context, return nil, err } for _, cluster := range clusters { - buildPermissionGroup(authManagerClient, cluster.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) + buildPermissionGroup(t, authManagerClient, cluster.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) } case permissionPortgroup: networks, err := finder.NetworkList(ctx, "/...") @@ -103,7 +106,7 @@ func buildAuthManagerClient(ctx context.Context, return nil, err } for _, network := range networks { - buildPermissionGroup(authManagerClient, network.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) + buildPermissionGroup(t, authManagerClient, network.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) } case permissionResourcePool: resourcePools := []string{"/DC0/host/DC0_C0/Resources/test-resourcepool", "/DC0/host/DC0_C0/Resources"} @@ -112,7 +115,7 @@ func buildAuthManagerClient(ctx context.Context, if err != nil { return nil, err } - buildPermissionGroup(authManagerClient, resourcePool.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) + buildPermissionGroup(t, authManagerClient, resourcePool.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) } case permissionFolder: var folders = []string{"/DC0/vm", "/DC0/vm/my-folder"} @@ -121,7 +124,7 @@ func buildAuthManagerClient(ctx context.Context, if err != nil { return nil, err } - buildPermissionGroup(authManagerClient, folder.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) + buildPermissionGroup(t, authManagerClient, folder.Reference(), username, group, groupName, overrideGroup, permissionsToRemoveFromResource) } } } @@ -139,7 +142,7 @@ func buildAuthManagerClient(ctx context.Context, } func validIPIMultiZoneInstallConfig() *types.InstallConfig { - installConfig := validIPIInstallConfig("DC0", "") + installConfig := validIPIInstallConfig() validMultiZonePlatform := validMultiVCenterPlatform() installConfig.VSphere.VCenters = validMultiZonePlatform.VCenters installConfig.VSphere.FailureDomains = validMultiZonePlatform.FailureDomains @@ -149,7 +152,11 @@ func validIPIMultiZoneInstallConfig() *types.InstallConfig { func TestPermissionValidate(t *testing.T) { ctx := context.TODO() - server := mock.StartSimulator() + server, err := mock.StartSimulator(true) + if err != nil { + t.Error(err) + return + } defer server.Close() client, _, err := mock.GetClient(server) @@ -170,6 +177,11 @@ func TestPermissionValidate(t *testing.T) { rootFolder := object.NewRootFolder(client) _, err = rootFolder.CreateFolder(ctx, "/DC0/vm/my-folder") + if err != nil { + t.Error(err) + return + } + resourcePools, err := finder.ResourcePoolList(ctx, "/DC0/host/DC0_C0") if err != nil { t.Error(err) @@ -181,15 +193,8 @@ func TestPermissionValidate(t *testing.T) { return } - validInstallConfig := validIPIInstallConfig("DC0", "") validMultiZoneInstallConfig := validIPIMultiZoneInstallConfig() - userDefinedFolderInstallConfig := validIPIInstallConfig("DC0", "") - userDefinedFolderInstallConfig.VSphere.Folder = "/DC0/vm/my-folder" - - invalidDatacenterInstallConfig := validIPIInstallConfig("DC0", "") - invalidDatacenterInstallConfig.VSphere.Datacenter = "invalid" - sessionMgr := session.NewManager(client) userSession, err := sessionMgr.UserSession(ctx) if err != nil { @@ -198,296 +203,203 @@ func TestPermissionValidate(t *testing.T) { } username := userSession.UserName - validPermissionsAuthManagerClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, nil, nil, nil) - if err != nil { - t.Error(err) - return - } - validPermissionsPre70AuthManagerClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionDatastore, sets.NewString("InventoryService.Tagging.ObjectAttachable"), sets.NewString("InventoryService.Tagging.ObjectAttachable")) - if err != nil { - t.Error(err) - return - } - - missingObjectAttachableDatacenter, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionDatacenter, sets.NewString("InventoryService.Tagging.ObjectAttachable"), nil) + validPermissionsAuthManagerClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, nil, nil, nil) if err != nil { t.Error(err) return } - missingObjectAttachableFolder, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionFolder, sets.NewString("InventoryService.Tagging.ObjectAttachable"), nil) + missingObjectAttachableDatacenter, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionDatacenter, sets.NewString("InventoryService.Tagging.ObjectAttachable"), nil) if err != nil { t.Error(err) return } - missingPortgroupPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionPortgroup, sets.NewString("Network.Assign"), nil) + missingPortgroupPermissionsClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionPortgroup, sets.NewString("Network.Assign"), nil) if err != nil { t.Error(err) return } - missingVCenterPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionVcenter, sets.NewString("StorageProfile.View"), nil) + missingVCenterPermissionsClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionVcenter, sets.NewString("StorageProfile.View"), nil) if err != nil { t.Error(err) return } - missingClusterPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionCluster, sets.NewString("VirtualMachine.Config.AddNewDisk"), nil) + missingClusterPermissionsClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionCluster, sets.NewString("VirtualMachine.Config.AddNewDisk"), nil) if err != nil { t.Error(err) return } - readOnlyClusterPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionCluster, sets.NewString(permissions[permissionCluster].Permissions...), nil) + readOnlyClusterPermissionsClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionCluster, sets.NewString(permissions[permissionCluster].Permissions...), nil) if err != nil { t.Error(err) return } - missingDatastorePermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionDatastore, sets.NewString("InventoryService.Tagging.ObjectAttachable"), nil) + missingDatastorePermissionsClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionDatastore, sets.NewString("InventoryService.Tagging.ObjectAttachable"), nil) if err != nil { t.Error(err) return } - missingDatacenterPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionDatacenter, sets.NewString("Folder.Delete"), nil) + missingDatacenterPermissionsClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionDatacenter, sets.NewString("Folder.Delete"), nil) if err != nil { t.Error(err) return } - readOnlyDatacenterPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionDatacenter, sets.NewString(permissions[permissionDatacenter].Permissions...), nil) + readOnlyDatacenterPermissionsClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionDatacenter, sets.NewString(permissions[permissionDatacenter].Permissions...), nil) if err != nil { t.Error(err) return } - missingFolderPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionFolder, sets.NewString("VirtualMachine.Provisioning.DeployTemplate"), nil) + missingFolderPermissionsClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionFolder, sets.NewString("VirtualMachine.Provisioning.DeployTemplate"), nil) if err != nil { t.Error(err) return } - missingResourcePoolPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionResourcePool, sets.NewString("VirtualMachine.Config.AddNewDisk"), nil) + missingResourcePoolPermissionsClient, err := buildAuthManagerClient(ctx, t, mockCtrl, finder, username, &permissionResourcePool, sets.NewString("VirtualMachine.Config.AddNewDisk"), nil) if err != nil { t.Error(err) return } tests := []struct { - name string - installConfig *types.InstallConfig - validationMethod func(*validationContext, *types.InstallConfig) error - multiZoneValidationMethod func(*validationContext, *vsphere.FailureDomain) field.ErrorList - failureDomain *vsphere.FailureDomain - expectErr string - authManager AuthManager + name string + installConfig *types.InstallConfig + validationMethod func(*validationContext, *vsphere.FailureDomain, bool) field.ErrorList + failureDomain *vsphere.FailureDomain + expectErr string + authManager AuthManager }{ { - name: "valid Permissions", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, + name: "multi-zone valid Permissions", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], authManager: validPermissionsAuthManagerClient, }, { - name: "valid Permissions with pre 7.0 privileges missing", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, - authManager: validPermissionsPre70AuthManagerClient, - }, - { - name: "missing portgroup Permissions", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, + name: "multi-zone missing portgroup Permissions", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], authManager: missingPortgroupPermissionsClient, expectErr: "privileges missing for vSphere Port Group: Network.Assign", }, { - name: "missing vCenter Permissions", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, + name: "multi-zone missing vCenter Permissions", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], authManager: missingVCenterPermissionsClient, expectErr: "privileges missing for vSphere vCenter: StorageProfile.View", }, { - name: "missing cluster Permissions", - installConfig: func() *types.InstallConfig { - installConfig := validIPIInstallConfig("DC0", "") - installConfig.VSphere.ResourcePool = "" - return installConfig + name: "multi-zone missing cluster Permissions", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: func() *vsphere.FailureDomain { + failureDomain := validMultiZoneInstallConfig.VSphere.FailureDomains[0] + failureDomain.Topology.ResourcePool = "" + return &failureDomain }(), - validationMethod: validateProvisioning, - authManager: missingClusterPermissionsClient, - expectErr: "privileges missing for vSphere vCenter Cluster: VirtualMachine.Config.AddNewDisk", + authManager: missingClusterPermissionsClient, + expectErr: "privileges missing for vSphere vCenter Cluster: VirtualMachine.Config.AddNewDisk", }, { - name: "resource pool provided, compute cluster can have read-only", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, + name: "multi-zone resource pool provided, compute cluster can have read-only", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], authManager: readOnlyClusterPermissionsClient, }, { - name: "missing datacenter Permissions", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, - authManager: missingDatacenterPermissionsClient, - expectErr: "privileges missing for vSphere vCenter Datacenter: Folder.Delete", - }, - { - name: "missing datacenter permission InventoryService.Tagging.ObjectAttachable", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, - authManager: missingObjectAttachableDatacenter, - expectErr: "privileges missing for vSphere vCenter Datacenter: InventoryService.Tagging.ObjectAttachable", + name: "multi-zone missing datacenter Permissions", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: func() *vsphere.FailureDomain { + failureDomain := validMultiZoneInstallConfig.VSphere.FailureDomains[0] + failureDomain.Topology.Folder = "" + return &failureDomain + }(), + authManager: missingDatacenterPermissionsClient, + expectErr: "privileges missing for vSphere vCenter Datacenter: Folder.Delete", }, { - name: "user-defined folder provided, datacenter can have read-only", - installConfig: func() *types.InstallConfig { - installConfig := validIPIInstallConfig("DC0", "") - installConfig.VSphere.Folder = "/DC0/vm/my-folder" - return installConfig - }(), - validationMethod: validateProvisioning, + name: "multi-zone user-defined folder provided, datacenter can have read-only", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], authManager: readOnlyDatacenterPermissionsClient, }, { - name: "missing datastore Permissions", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, + name: "multi-zone missing datastore Permissions", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], authManager: missingDatastorePermissionsClient, expectErr: "privileges missing for vSphere vCenter Datastore: InventoryService.Tagging.ObjectAttachable", }, { - name: "missing resource pool Permissions", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, + name: "multi-zone missing resource pool Permissions", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], authManager: missingResourcePoolPermissionsClient, expectErr: "privileges missing for vSphere vCenter Resource Pool: VirtualMachine.Config.AddNewDisk", }, { - name: "missing user-defined folder Permissions but no folder defined", - installConfig: validInstallConfig, - validationMethod: validateProvisioning, - authManager: missingFolderPermissionsClient, - }, - { - name: "missing user-defined folder Permissions", - installConfig: userDefinedFolderInstallConfig, - validationMethod: validateProvisioning, - authManager: missingFolderPermissionsClient, - expectErr: "privileges missing for Pre-existing Virtual Machine Folder: VirtualMachine.Provisioning.DeployTemplate", - }, - { - name: "missing user-defined folder permission InventoryService.Tagging.ObjectAttachable", - installConfig: userDefinedFolderInstallConfig, - validationMethod: validateProvisioning, - authManager: missingObjectAttachableFolder, - expectErr: "privileges missing for Pre-existing Virtual Machine Folder: InventoryService.Tagging.ObjectAttachable", - }, - { - name: "invalid defined datacenter", - installConfig: invalidDatacenterInstallConfig, - validationMethod: validateProvisioning, - authManager: validPermissionsAuthManagerClient, - expectErr: "platform.vsphere.datacenter: Invalid value: \"invalid\": datacenter 'invalid' not found", - }, - { - name: "multi-zone valid Permissions", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], - authManager: validPermissionsAuthManagerClient, - }, - { - name: "multi-zone missing portgroup Permissions", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], - authManager: missingPortgroupPermissionsClient, - expectErr: "privileges missing for vSphere Port Group: Network.Assign", - }, - { - name: "multi-zone missing vCenter Permissions", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], - authManager: missingVCenterPermissionsClient, - expectErr: "privileges missing for vSphere vCenter: StorageProfile.View", - }, - { - name: "multi-zone missing cluster Permissions", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, + name: "multi-zone missing user-defined folder Permissions but no folder defined", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, failureDomain: func() *vsphere.FailureDomain { failureDomain := validMultiZoneInstallConfig.VSphere.FailureDomains[0] + failureDomain.Topology.Folder = "" failureDomain.Topology.ResourcePool = "" return &failureDomain }(), - authManager: missingClusterPermissionsClient, - expectErr: "privileges missing for vSphere vCenter Cluster: VirtualMachine.Config.AddNewDisk", + authManager: missingFolderPermissionsClient, }, { - name: "multi-zone resource pool provided, compute cluster can have read-only", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], - authManager: readOnlyClusterPermissionsClient, + name: "multi-zone missing user-defined folder Permissions", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, + failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], + authManager: missingFolderPermissionsClient, + expectErr: "privileges missing for Pre-existing Virtual Machine Folder: VirtualMachine.Provisioning.DeployTemplate", }, { - name: "multi-zone missing datacenter Permissions", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, + name: "missing datacenter permission InventoryService.Tagging.ObjectAttachable", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, failureDomain: func() *vsphere.FailureDomain { failureDomain := validMultiZoneInstallConfig.VSphere.FailureDomains[0] + // If folder is empty permissions are checked at the folder level failureDomain.Topology.Folder = "" return &failureDomain }(), - authManager: missingDatacenterPermissionsClient, - expectErr: "privileges missing for vSphere vCenter Datacenter: Folder.Delete", - }, - { - name: "multi-zone user-defined folder provided, datacenter can have read-only", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], - authManager: readOnlyDatacenterPermissionsClient, - }, - { - name: "multi-zone missing datastore Permissions", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], - authManager: missingDatastorePermissionsClient, - expectErr: "privileges missing for vSphere vCenter Datastore: InventoryService.Tagging.ObjectAttachable", - }, - { - name: "multi-zone missing resource pool Permissions", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], - authManager: missingResourcePoolPermissionsClient, - expectErr: "privileges missing for vSphere vCenter Resource Pool: VirtualMachine.Config.AddNewDisk", + + authManager: missingObjectAttachableDatacenter, + expectErr: "privileges missing for vSphere vCenter Datacenter: InventoryService.Tagging.ObjectAttachable", }, { - name: "multi-zone missing user-defined folder Permissions but no folder defined", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, + name: "invalid defined datacenter", + installConfig: validMultiZoneInstallConfig, + validationMethod: validateFailureDomain, failureDomain: func() *vsphere.FailureDomain { failureDomain := validMultiZoneInstallConfig.VSphere.FailureDomains[0] - failureDomain.Topology.Folder = "" - failureDomain.Topology.ResourcePool = "" + // If folder is empty permissions are checked at the folder level + failureDomain.Topology.Datacenter = "invalid" return &failureDomain }(), - authManager: missingFolderPermissionsClient, - }, - { - name: "multi-zone missing user-defined folder Permissions", - installConfig: validMultiZoneInstallConfig, - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiZoneInstallConfig.VSphere.FailureDomains[0], - authManager: missingFolderPermissionsClient, - expectErr: "privileges missing for Pre-existing Virtual Machine Folder: VirtualMachine.Provisioning.DeployTemplate", + authManager: validPermissionsAuthManagerClient, + expectErr: `platform.vsphere.failureDomains.topology.datacenter: Invalid value: "invalid": datacenter 'invalid' not found`, }, } @@ -504,9 +416,7 @@ func TestPermissionValidate(t *testing.T) { assert.NoError(t, err) } if test.validationMethod != nil { - err = test.validationMethod(validationCtx, test.installConfig) - } else if test.multiZoneValidationMethod != nil { - err = test.multiZoneValidationMethod(validationCtx, test.failureDomain).ToAggregate() + err = test.validationMethod(validationCtx, test.failureDomain, false).ToAggregate() } else { err = errors.New("no test method defined") } @@ -525,9 +435,8 @@ var permissionsBackup = map[permissionGroup]PermissionGroupDefinition{} func pushPrivileges() { for permissionGroupKey, permissionGroup := range permissions { var permissions []string - for _, permission := range permissionGroup.Permissions { - permissions = append(permissions, permission) - } + permissions = append(permissions, permissionGroup.Permissions...) + permissionsBackup[permissionGroupKey] = PermissionGroupDefinition{ Permissions: permissions, Description: permissionGroup.Description, @@ -538,9 +447,8 @@ func pushPrivileges() { func popPrivileges() { for permissionGroupKey, permissionGroup := range permissionsBackup { var permissionsSet []string - for _, permission := range permissionGroup.Permissions { - permissionsSet = append(permissionsSet, permission) - } + + permissionsSet = append(permissionsSet, permissionGroup.Permissions...) permissions[permissionGroupKey] = PermissionGroupDefinition{ Permissions: permissionsSet, Description: permissionGroup.Description, diff --git a/pkg/asset/installconfig/vsphere/validation_test.go b/pkg/asset/installconfig/vsphere/validation_test.go index b3d1e6b0588..5d50f10cbf8 100644 --- a/pkg/asset/installconfig/vsphere/validation_test.go +++ b/pkg/asset/installconfig/vsphere/validation_test.go @@ -3,11 +3,13 @@ package vsphere import ( "context" "errors" - "fmt" + "net" + "sync" "testing" "time" "github.com/golang/mock/gomock" + "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/session" @@ -24,7 +26,9 @@ import ( ) var ( - validCIDR = "10.0.0.0/16" + validCIDR = "10.0.0.0/16" + mu sync.Mutex + stopListening = false ) const ( @@ -35,7 +39,9 @@ const ( tagTestNothingCreatedOrAttached = 0x10 ) -func validIPIInstallConfig(dcName string, fName string) *types.InstallConfig { +const wildcardDNS = "nip.io" + +func validIPIInstallConfig() *types.InstallConfig { return &types.InstallConfig{ Networking: &types.Networking{ MachineNetwork: []types.MachineNetworkEntry{ @@ -45,16 +51,8 @@ func validIPIInstallConfig(dcName string, fName string) *types.InstallConfig { Publish: types.ExternalPublishingStrategy, Platform: types.Platform{ VSphere: &vsphere.Platform{ - Cluster: fmt.Sprintf("%s/%s_C0", fName, dcName), - Datacenter: fmt.Sprintf("%s/%s", fName, dcName), - DefaultDatastore: "LocalDS_0", - ResourcePool: "/DC0/host/DC0_C0/Resources/test-resourcepool", - Network: fmt.Sprintf("%s_DVPG0", dcName), - Password: "valid_password", - Username: "valid_username", - VCenter: "valid-vcenter", - APIVIPs: []string{"192.168.111.0"}, - IngressVIPs: []string{"192.168.111.1"}, + APIVIPs: []string{"192.168.111.0"}, + IngressVIPs: []string{"192.168.111.1"}, }, }, } @@ -76,7 +74,7 @@ func validMultiVCenterPlatform() *vsphere.Platform { FailureDomains: []vsphere.FailureDomain{ { Name: "test-east-1a", - Region: "test-region--east", + Region: "test-region-east", Zone: "test-zone-1a", Topology: vsphere.Topology{ Datacenter: "DC0", @@ -193,166 +191,31 @@ func setupTagAttachmentTest(ctx context.Context, restClient *rest.Client, finder return tagMgr, nil } -func TestValidate(t *testing.T) { - server := mock.StartSimulator() - defer server.Close() +// simulatorHelper starts the govmomi simulator +// returning the simulator.Server so that we can defer closing later and +// shutdown the simulator to change versions. +func simulatorHelper(t *testing.T, setVersionToSupported bool) (*validationContext, *simulator.Server, *rest.Client, error) { + t.Helper() + + server, err := mock.StartSimulator(setVersionToSupported) + if err != nil { + return nil, nil, nil, err + } ctrl := gomock.NewController(t) defer ctrl.Finish() - dcName := "DC0" - fName := "/F0" - dcName1 := "DC1" - tests := []struct { - name string - installConfig *types.InstallConfig - validationMethod func(*validationContext, *types.InstallConfig) error - multiZoneValidationMethod func(*validationContext, *vsphere.FailureDomain) field.ErrorList - failureDomain *vsphere.FailureDomain - tagTestMask int64 - expectErr string - }{{ - name: "valid IPI install config", - installConfig: validIPIInstallConfig(dcName, ""), - validationMethod: validateProvisioning, - }, { - name: "valid IPI install config - DC in folder", - installConfig: validIPIInstallConfig(dcName1, fName), - validationMethod: validateProvisioning, - }, { - name: "invalid IPI - no network", - installConfig: func() *types.InstallConfig { - c := validIPIInstallConfig(dcName, "") - c.Platform.VSphere.Network = "" - return c - }(), - validationMethod: validateProvisioning, - expectErr: `^platform\.vsphere\.network: Required value: must specify the network$`, - }, { - name: "invalid IPI - invalid datacenter", - installConfig: func() *types.InstallConfig { - c := validIPIInstallConfig(dcName, "") - c.Platform.VSphere.Datacenter = "invalid_dc" - return c - }(), - validationMethod: validateProvisioning, - expectErr: `^platform.vsphere.datacenter: Invalid value: "invalid_dc": datacenter 'invalid_dc' not found`, - }, { - name: "invalid IPI - invalid network", - installConfig: func() *types.InstallConfig { - c := validIPIInstallConfig(dcName, "") - c.Platform.VSphere.Network = "invalid_network" - return c - }(), - validationMethod: validateProvisioning, - expectErr: `^platform.vsphere.network: Invalid value: "invalid_network": unable to find network provided$`, - }, { - name: "invalid IPI - invalid network - DC in folder", - installConfig: func() *types.InstallConfig { - c := validIPIInstallConfig(dcName1, fName) - c.Platform.VSphere.Network = "invalid_network" - return c - }(), - validationMethod: validateProvisioning, - expectErr: `^platform.vsphere.network: Invalid value: "invalid_network": unable to find network provided$`, - }, { - name: "invalid IPI - no cluster", - installConfig: func() *types.InstallConfig { - c := validIPIInstallConfig(dcName, "") - c.Platform.VSphere.Cluster = "" - return c - }(), - validationMethod: validateProvisioning, - expectErr: `^platform\.vsphere\.cluster: Required value: must specify the cluster$`, - }, { - name: "multi-zone validation", - failureDomain: &validMultiVCenterPlatform().FailureDomains[0], - multiZoneValidationMethod: validateMultiZoneProvisioning, - }, { - name: "multi-zone validation - invalid datacenter", - failureDomain: func() *vsphere.FailureDomain { - failureDomain := &validMultiVCenterPlatform().FailureDomains[0] - failureDomain.Topology.Datacenter = "invalid-dc" - return failureDomain - }(), - multiZoneValidationMethod: validateMultiZoneProvisioning, - expectErr: `^platform.vsphere.failureDomains.topology.datacenter: Invalid value: "invalid-dc": datacenter 'invalid-dc' not found$`, - }, { - name: "multi-zone validation - invalid cluster", - failureDomain: func() *vsphere.FailureDomain { - failureDomain := &validMultiVCenterPlatform().FailureDomains[0] - failureDomain.Topology.ComputeCluster = "/DC0/host/invalid-cluster" - return failureDomain - }(), - multiZoneValidationMethod: validateMultiZoneProvisioning, - expectErr: `^platform.vsphere.failureDomains.topology.computeCluster: Invalid value: "/DC0/host/invalid-cluster": cluster '/DC0/host/invalid-cluster' not found$`, - }, { - name: "multi-zone validation - invalid resource pool", - failureDomain: func() *vsphere.FailureDomain { - failureDomain := &validMultiVCenterPlatform().FailureDomains[0] - failureDomain.Topology.ResourcePool = "/DC0/host/DC0_C0/Resources/invalid-resourcepool" - return failureDomain - }(), - multiZoneValidationMethod: validateMultiZoneProvisioning, - expectErr: `^platform.vsphere.failureDomains.topology.resourcePool: Invalid value: "/DC0/host/DC0_C0/Resources/invalid-resourcepool": resource pool '/DC0/host/DC0_C0/Resources/invalid-resourcepool' not found$`, - }, { - name: "multi-zone validation - invalid network", - failureDomain: func() *vsphere.FailureDomain { - failureDomain := &validMultiVCenterPlatform().FailureDomains[0] - failureDomain.Topology.Networks = []string{ - "invalid-network", - } - return failureDomain - }(), - multiZoneValidationMethod: validateMultiZoneProvisioning, - expectErr: `^platform.vsphere.failureDomains.topology: Invalid value: "invalid-network": unable to find network provided$`, - }, { - name: "multi-zone validation - invalid folder", - failureDomain: func() *vsphere.FailureDomain { - failureDomain := &validMultiVCenterPlatform().FailureDomains[0] - failureDomain.Topology.Folder = "/DC0/vm/invalid-folder" - return failureDomain - }(), - multiZoneValidationMethod: validateMultiZoneProvisioning, - expectErr: `^platform.vsphere.failureDomains.topology.folder: Invalid value: "/DC0/vm/invalid-folder": folder '/DC0/vm/invalid-folder' not found$`, - }, { - name: "multi-zone tag categories present and tags attached", - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiVCenterPlatform().FailureDomains[0], - tagTestMask: tagTestCreateZoneCategory | - tagTestCreateRegionCategory | - tagTestAttachRegionTags | - tagTestAttachZoneTags, - }, { - name: "multi-zone tag categories, missing zone tag attachment", - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiVCenterPlatform().FailureDomains[0], - tagTestMask: tagTestCreateZoneCategory | - tagTestCreateRegionCategory | - tagTestAttachRegionTags, - expectErr: "platform.vsphere.failureDomains.topology.computeCluster: Internal error: tag associated with tag category openshift-zone not attached to this resource or ancestor", - }, { - name: "multi-zone tag categories, missing zone and region tag categories", - multiZoneValidationMethod: validateMultiZoneProvisioning, - failureDomain: &validMultiVCenterPlatform().FailureDomains[0], - tagTestMask: tagTestNothingCreatedOrAttached, - expectErr: "platform.vsphere: Internal error: tag categories openshift-zone and openshift-region must be created", - }, - } - ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) defer cancel() finder, err := mock.GetFinder(server) if err != nil { - t.Error(err) - return + return nil, nil, nil, err } client, _, err := mock.GetClient(server) if err != nil { - t.Error(err) - return + return nil, nil, nil, err } restClient := rest.NewClient(client) @@ -360,54 +223,191 @@ func TestValidate(t *testing.T) { defer restClient.CloseIdleConnections() err = restClient.Login(context.TODO(), simulator.DefaultLogin) if err != nil { - t.Error(err) + return nil, nil, nil, err } rootFolder := object.NewRootFolder(client) _, err = rootFolder.CreateFolder(ctx, "/DC0/vm/my-folder") if err != nil { - t.Error(err) + return nil, nil, nil, err } resourcePools, err := finder.ResourcePoolList(ctx, "/DC0/host/DC0_C0") if err != nil { - t.Error(err) - return + return nil, nil, nil, err } _, err = resourcePools[0].Create(ctx, "test-resourcepool", vim25types.DefaultResourceConfigSpec()) if err != nil { - t.Error(err) - return + return nil, nil, nil, err } sessionMgr := session.NewManager(client) userSession, err := sessionMgr.UserSession(ctx) if err != nil { - t.Error(err) - return + return nil, nil, nil, err } username := userSession.UserName - validPermissionsAuthManagerClient, err := buildAuthManagerClient(ctx, ctrl, finder, username, nil, nil, nil) + validPermissionsAuthManagerClient, err := buildAuthManagerClient(ctx, t, ctrl, finder, username, nil, nil, nil) if err != nil { - t.Error(err) - return + return nil, nil, nil, err } - validationCtx := &validationContext{ + return &validationContext{ AuthManager: validPermissionsAuthManagerClient, Finder: finder, Client: client, + }, server, restClient, nil +} + +func TestValidateFailureDomains(t *testing.T) { + validationCtx, server, restClient, err := simulatorHelper(t, true) + if err != nil { + t.Error(err) + return + } + defer server.Close() + + ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) + defer cancel() + + tests := []struct { + name string + installConfig *types.InstallConfig + validationMethod func(*validationContext, *vsphere.FailureDomain, bool) field.ErrorList + failureDomain *vsphere.FailureDomain + tagTestMask int64 + checkTags bool + expectErr string + }{ + { + name: "multi-zone validation", + failureDomain: &validMultiVCenterPlatform().FailureDomains[0], + validationMethod: validateFailureDomain, + }, { + name: "multi-zone validation - invalid datacenter", + failureDomain: func() *vsphere.FailureDomain { + failureDomain := &validMultiVCenterPlatform().FailureDomains[0] + failureDomain.Topology.Datacenter = "invalid-dc" + return failureDomain + }(), + validationMethod: validateFailureDomain, + checkTags: false, + expectErr: `[platform.vsphere.failureDomains.topology.datacenter: Invalid value: "invalid-dc": datacenter 'invalid-dc' not found, platform.vsphere.failureDomains.topology.datastore: Invalid value: "invalid-dc": unable to find datacenter invalid-dc: datacenter 'invalid-dc' not found, platform.vsphere.failureDomains.topology: Invalid value: "invalid-dc": datacenter './invalid-dc' not found]`, + }, + { + name: "multi-zone validation - invalid cluster", + failureDomain: func() *vsphere.FailureDomain { + failureDomain := &validMultiVCenterPlatform().FailureDomains[0] + failureDomain.Topology.ComputeCluster = "/DC0/host/invalid-cluster" + return failureDomain + }(), + validationMethod: validateFailureDomain, + checkTags: false, + expectErr: `\[platform.vsphere.failureDomains.topology.computeCluster: Invalid value: "/DC0/host/invalid-cluster": cluster '/DC0/host/invalid-cluster' not found, platform.vsphere.failureDomains.topology: Invalid value: "DC0_DVPG0": could not find vSphere cluster at /DC0/host/invalid-cluster: cluster '/DC0/host/invalid-cluster' not found\]`, + }, + { + name: "multi-zone validation - missing cluster", + failureDomain: func() *vsphere.FailureDomain { + failureDomain := &validMultiVCenterPlatform().FailureDomains[0] + failureDomain.Topology.ComputeCluster = "" + return failureDomain + }(), + validationMethod: validateFailureDomain, + checkTags: false, + expectErr: `^platform.vsphere.failureDomains.topology.computeCluster: Invalid value: "": full path of cluster is required$`, + }, + { + name: "multi-zone validation - missing datastore", + failureDomain: func() *vsphere.FailureDomain { + failureDomain := &validMultiVCenterPlatform().FailureDomains[0] + failureDomain.Topology.Datastore = "" + return failureDomain + }(), + validationMethod: validateFailureDomain, + checkTags: false, + expectErr: `^platform.vsphere.failureDomains.topology.datastore: Required value: must specify the datastore$`, + }, + { + name: "multi-zone validation - invalid resource pool", + failureDomain: func() *vsphere.FailureDomain { + failureDomain := &validMultiVCenterPlatform().FailureDomains[0] + failureDomain.Topology.ResourcePool = "/DC0/host/DC0_C0/Resources/invalid-resourcepool" + return failureDomain + }(), + validationMethod: validateFailureDomain, + checkTags: false, + expectErr: `^platform.vsphere.failureDomains.topology.resourcePool: Invalid value: "/DC0/host/DC0_C0/Resources/invalid-resourcepool": resource pool '/DC0/host/DC0_C0/Resources/invalid-resourcepool' not found$`, + }, + { + name: "multi-zone validation - invalid network", + failureDomain: func() *vsphere.FailureDomain { + failureDomain := &validMultiVCenterPlatform().FailureDomains[0] + failureDomain.Topology.Networks = []string{ + "invalid-network", + } + return failureDomain + }(), + validationMethod: validateFailureDomain, + checkTags: false, + expectErr: `^platform.vsphere.failureDomains.topology: Invalid value: "invalid-network": unable to find network provided$`, + }, { + name: "multi-zone validation - invalid folder", + failureDomain: func() *vsphere.FailureDomain { + failureDomain := &validMultiVCenterPlatform().FailureDomains[0] + failureDomain.Topology.Folder = "/DC0/vm/invalid-folder" + return failureDomain + }(), + validationMethod: validateFailureDomain, + checkTags: false, + expectErr: `^platform.vsphere.failureDomains.topology.folder: Invalid value: "/DC0/vm/invalid-folder": folder '/DC0/vm/invalid-folder' not found$`, + }, + + { + name: "multi-zone validation - invalid folder", + failureDomain: func() *vsphere.FailureDomain { + failureDomain := &validMultiVCenterPlatform().FailureDomains[0] + failureDomain.Topology.Folder = "/DC0/vm/invalid-folder" + return failureDomain + }(), + validationMethod: validateFailureDomain, + checkTags: false, + expectErr: `^platform.vsphere.failureDomains.topology.folder: Invalid value: "/DC0/vm/invalid-folder": folder '/DC0/vm/invalid-folder' not found$`, + }, { + name: "multi-zone tag categories present and tags attached", + validationMethod: validateFailureDomain, + failureDomain: &validMultiVCenterPlatform().FailureDomains[0], + checkTags: true, + tagTestMask: tagTestCreateZoneCategory | + tagTestCreateRegionCategory | + tagTestAttachRegionTags | + tagTestAttachZoneTags, + }, { + name: "multi-zone tag categories, missing zone tag attachment", + validationMethod: validateFailureDomain, + failureDomain: &validMultiVCenterPlatform().FailureDomains[0], + checkTags: true, + tagTestMask: tagTestCreateZoneCategory | + tagTestCreateRegionCategory | + tagTestAttachRegionTags, + expectErr: "platform.vsphere.failureDomains.topology.computeCluster: Internal error: tag associated with tag category openshift-zone not attached to this resource or ancestor", + }, { + name: "multi-zone tag categories, missing zone and region tag categories", + validationMethod: validateFailureDomain, + failureDomain: &validMultiVCenterPlatform().FailureDomains[0], + checkTags: true, + tagTestMask: tagTestNothingCreatedOrAttached, + expectErr: "platform.vsphere: Internal error: tag categories openshift-zone and openshift-region must be created", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var err error if test.validationMethod != nil { - err = test.validationMethod(validationCtx, test.installConfig) - } else if test.multiZoneValidationMethod != nil { var tagMgr *vapitags.Manager + if test.tagTestMask != 0 { - tagMgr, err = setupTagAttachmentTest(ctx, restClient, finder, test.tagTestMask) + tagMgr, err = setupTagAttachmentTest(ctx, restClient, validationCtx.Finder, test.tagTestMask) if err != nil { assert.NoError(t, err) } @@ -415,7 +415,8 @@ func TestValidate(t *testing.T) { validationCtx.regionTagCategoryID = "" validationCtx.TagManager = tagMgr } - err = test.multiZoneValidationMethod(validationCtx, test.failureDomain).ToAggregate() + + err = test.validationMethod(validationCtx, test.failureDomain, test.checkTags).ToAggregate() if test.tagTestMask != 0 { err := teardownTagAttachmentTest(ctx, tagMgr) if err != nil { @@ -436,3 +437,245 @@ func TestValidate(t *testing.T) { }) } } + +func Test_validateVCenterVersion(t *testing.T) { + tests := []struct { + name string + setVersionToSupported bool + fldPath *field.Path + expectErr string + }{ + { + name: "valid vcenter version", + setVersionToSupported: true, + fldPath: field.NewPath("platform").Child("vsphere").Child("vcenters"), + expectErr: ``, + }, + { + name: "unsupported vcenter version", + setVersionToSupported: false, + fldPath: field.NewPath("platform").Child("vsphere").Child("vcenters"), + expectErr: `platform.vsphere.vcenters: Required value: The vSphere storage driver requires a minimum of vSphere 7 Update 2. Current vCenter version: 6.5.0, build: 5973321`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + validationCtx, server, _, err := simulatorHelper(t, test.setVersionToSupported) + + if err != nil { + t.Error(err) + } + defer server.Close() + + err = validateVCenterVersion(validationCtx, test.fldPath).ToAggregate() + + if test.expectErr != "" { + assert.Regexp(t, test.expectErr, err) + } else if err != nil { + assert.NoError(t, err) + } + }) + } +} + +func Test_validateESXiVersion(t *testing.T) { + vSphereFldPath := field.NewPath("platform").Child("vsphere") + computeClusterFldPath := vSphereFldPath.Child("failureDomains").Child("topology").Child("computeCluster") + platform := validMultiVCenterPlatform() + + tests := []struct { + name string + setVersionToSupported bool + computeClusterPath string + expectErr string + }{ + { + name: "valid esxi version", + setVersionToSupported: true, + computeClusterPath: platform.FailureDomains[0].Topology.ComputeCluster, + expectErr: ``, + }, + { + name: "unsupported esxi version", + setVersionToSupported: false, + computeClusterPath: platform.FailureDomains[0].Topology.ComputeCluster, + expectErr: `[platform.vsphere.failureDomains.topology.computeCluster: Required value: The vSphere storage driver requires a minimum of vSphere 7 Update 2. The ESXi host: DC0_C0_H0 is version: 6.5.0 and build: 5969303, platform.vsphere.failureDomains.topology.computeCluster: Required value: The vSphere storage driver requires a minimum of vSphere 7 Update 2. The ESXi host: DC0_C0_H1 is version: 6.5.0 and build: 5969303, platform.vsphere.failureDomains.topology.computeCluster: Required value: The vSphere storage driver requires a minimum of vSphere 7 Update 2. The ESXi host: DC0_C0_H2 is version: 6.5.0 and build: 5969303]`, + }, + { + name: "computeCluster not found", + setVersionToSupported: true, + computeClusterPath: "/DC0/host/invalid-cluster", + expectErr: `platform.vsphere.failureDomains.topology.computeCluster: Invalid value: "/DC0/host/invalid-cluster": cluster '/DC0/host/invalid-cluster' not found`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + validationCtx, server, _, err := simulatorHelper(t, test.setVersionToSupported) + + if err != nil { + t.Error(err) + } + defer server.Close() + + err = validateESXiVersion(validationCtx, test.computeClusterPath, vSphereFldPath, computeClusterFldPath).ToAggregate() + + if test.expectErr != "" { + assert.Regexp(t, test.expectErr, err) + } else if err != nil { + t.Error(err) + } + }) + } +} + +func Test_ensureDNS(t *testing.T) { + platformFieldPath := field.NewPath("platform") + + resolver := &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: time.Millisecond * time.Duration(10000), + } + return d.DialContext(ctx, network, "1.1.1.1:53") + }, + } + + tests := []struct { + name string + installConfig *types.InstallConfig + expectErr string + }{ + { + name: "valid dns", + installConfig: func() *types.InstallConfig { + installConfig := validIPIInstallConfig() + installConfig.ObjectMeta.Name = "0a000803" + installConfig.BaseDomain = wildcardDNS + + return installConfig + }(), + expectErr: ``, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ensureDNS(test.installConfig, platformFieldPath, resolver).ToAggregate() + if test.expectErr != "" { + assert.Regexp(t, test.expectErr, err) + } else if err != nil { + t.Error(err) + } + }) + } +} + +// lbHelper creates a listening port of 6443 on loopback (127.0.0.1) +// If stopListening is true the loop is broken and the port closes. +func lbHelper(t *testing.T) { + t.Helper() + mu.Lock() + stopListening = false + mu.Unlock() + addr, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:6443") + if err != nil { + t.Error(err) + } + + listen, err := net.ListenTCP("tcp4", addr) + if err != nil { + t.Error(err) + } + for { + mu.Lock() + if stopListening { + err = listen.Close() + if err != nil { + t.Logf("closing tcp listener: %s", err.Error()) + } + break + } + mu.Unlock() + + _, err = listen.Accept() + + if err != nil { + t.Error(err) + } + } + mu.Unlock() +} + +// Test_ensureLoadBalancer uses lbHelper to create an open +// on 127.0.0.1:6443. Also uses nip.io as the base domain +// and cluster name as 7f000001 which equals 127.0.0.1 +// Examples for testing logrus output from here: +// https://maxchadwick.xyz/blog/testing-log-output-in-go-logrus +func Test_ensureLoadBalancer(t *testing.T) { + go lbHelper(t) + logger, hook := test.NewNullLogger() + localLogger = logger + + tests := []struct { + name string + installConfig *types.InstallConfig + stopTCPListen bool + expectLevel string + expectWarn string + }{ + { + name: "valid lb", + installConfig: func() *types.InstallConfig { + installConfig := validIPIInstallConfig() + installConfig.ObjectMeta.Name = "7f000001" + installConfig.BaseDomain = wildcardDNS + installConfig.VSphere.APIVIPs = []string{} + installConfig.VSphere.IngressVIPs = []string{} + + return installConfig + }(), + stopTCPListen: false, + expectLevel: ``, + expectWarn: ``, + }, + { + name: "warn lb", + installConfig: func() *types.InstallConfig { + installConfig := validIPIInstallConfig() + installConfig.ObjectMeta.Name = "7f000002" + installConfig.BaseDomain = wildcardDNS + installConfig.VSphere.APIVIPs = []string{} + installConfig.VSphere.IngressVIPs = []string{} + + return installConfig + }(), + stopTCPListen: true, + expectLevel: `warning`, + expectWarn: `Installation may fail, load balancer not available: dial tcp 127.0.0.2:6443: connect: connection refused`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.stopTCPListen { + mu.Lock() + stopListening = true + mu.Unlock() + } + mu.Lock() + ensureLoadBalancer(test.installConfig) + mu.Unlock() + if test.expectWarn != "" { + // there should be only one entry + entries := hook.AllEntries() + assert.NotEmpty(t, entries) + for _, e := range entries { + assert.Equal(t, test.expectLevel, e.Level.String()) + assert.Regexp(t, test.expectWarn, e.Message) + } + } + hook.Reset() + }) + } +} diff --git a/pkg/asset/machines/vsphere/machines_test.go b/pkg/asset/machines/vsphere/machines_test.go index 92b3dce7675..9a51b35c24a 100644 --- a/pkg/asset/machines/vsphere/machines_test.go +++ b/pkg/asset/machines/vsphere/machines_test.go @@ -1,10 +1,10 @@ package vsphere import ( - "reflect" "testing" "github.com/ghodss/yaml" + "github.com/google/go-cmp/cmp" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -49,18 +49,15 @@ metadata: name: test-cluster platform: vSphere: - vCenter: your.vcenter.example.com - username: username - password: password - datacenter: datacenter - defaultDatastore: datastore - network: portgroup vcenters: - server: your.vcenter.example.com username: username password: password datacenters: - - datacenter + - dc1 + - dc2 + - dc3 + - dc4 failureDomains: - name: deployzone-us-east-1a server: your.vcenter.example.com @@ -73,7 +70,7 @@ platform: computeCluster: /dc1/host/c1 networks: - network1 - datastore: datastore1 + datastore: /dc1/datastore/datastore1 - name: deployzone-us-east-2a server: your.vcenter.example.com region: us-east @@ -85,7 +82,7 @@ platform: computeCluster: /dc2/host/c2 networks: - network1 - datastore: datastore2 + datastore: /dc2/datastore/datastore2 - name: deployzone-us-east-3a server: your.vcenter.example.com region: us-east @@ -97,7 +94,7 @@ platform: computeCluster: /dc3/host/c3 networks: - network1 - datastore: datastore3 + datastore: /dc3/datastore/datastore3 - name: deployzone-us-east-4a server: your.vcenter.example.com region: us-east @@ -109,7 +106,7 @@ platform: computeCluster: /dc4/host/c4 networks: - network1 - datastore: datastore4 + datastore: /dc4/datastore/datastore4 pullSecret: sshKey:` @@ -179,23 +176,6 @@ var machinePoolUndefinedZones = types.MachinePool{ Architecture: types.ArchitectureAMD64, } -var machinePoolNoZones = types.MachinePool{ - Name: "master", - Replicas: &machinePoolReplicas, - Platform: types.MachinePoolPlatform{ - VSphere: &vsphere.MachinePool{ - NumCPUs: 4, - NumCoresPerSocket: 2, - MemoryMiB: 16384, - OSDisk: vsphere.OSDisk{ - DiskSizeGB: 60, - }, - }, - }, - Hyperthreading: "true", - Architecture: types.ArchitectureAMD64, -} - func parseInstallConfig() (*types.InstallConfig, error) { config := &types.InstallConfig{} if err := yaml.Unmarshal([]byte(installConfigSample), config); err != nil { @@ -209,7 +189,8 @@ func parseInstallConfig() (*types.InstallConfig, error) { return config, nil } -func assertOnUnexpectedErrorState(expectedError string, err error, t *testing.T) { +func assertOnUnexpectedErrorState(t *testing.T, expectedError string, err error) { + t.Helper() if expectedError == "" && err != nil { t.Errorf("unexpected error encountered: %s", err.Error()) } else if expectedError != "" { @@ -231,6 +212,10 @@ func TestConfigMasters(t *testing.T) { return } defaultClusterResourcePool, err := parseInstallConfig() + if err != nil { + t.Error(err) + return + } defaultClusterResourcePool.VSphere.FailureDomains[0].Topology.ResourcePool = "" testCases := []struct { @@ -242,22 +227,6 @@ func TestConfigMasters(t *testing.T) { maxAllowedWorkspaceMatches int minAllowedWorkspaceMatches int }{ - { - testCase: "machinepool with no defined zones should use legacy configuration", - machinePool: &machinePoolNoZones, - minAllowedWorkspaceMatches: 3, - maxAllowedWorkspaceMatches: 3, - installConfig: installConfig, - workspaces: []machineapi.Workspace{ - { - Server: "your.vcenter.example.com", - Datacenter: "datacenter", - Folder: "/datacenter/vm/test", - Datastore: "datastore", - ResourcePool: "/datacenter/host//Resources", - }, - }, - }, { testCase: "zones distributed among control plane machines(zone count matches machine count)", machinePool: &machinePoolValidZones, @@ -268,21 +237,21 @@ func TestConfigMasters(t *testing.T) { Server: "your.vcenter.example.com", Datacenter: "dc1", Folder: "/dc1/vm/folder1", - Datastore: "datastore1", + Datastore: "/dc1/datastore/datastore1", ResourcePool: "/dc1/host/c1/Resources/rp1", }, { Server: "your.vcenter.example.com", Datacenter: "dc2", Folder: "/dc2/vm/folder2", - Datastore: "datastore2", + Datastore: "/dc2/datastore/datastore2", ResourcePool: "/dc2/host/c2/Resources/rp2", }, { Server: "your.vcenter.example.com", Datacenter: "dc3", Folder: "/dc3/vm/folder3", - Datastore: "datastore3", + Datastore: "/dc3/datastore/datastore3", ResourcePool: "/dc3/host/c3/Resources/rp3", }, }, @@ -304,14 +273,14 @@ func TestConfigMasters(t *testing.T) { Server: "your.vcenter.example.com", Datacenter: "dc1", Folder: "/dc1/vm/folder1", - Datastore: "datastore1", + Datastore: "/dc1/datastore/datastore1", ResourcePool: "/dc1/host/c1/Resources/rp1", }, { Server: "your.vcenter.example.com", Datacenter: "dc2", Folder: "/dc2/vm/folder2", - Datastore: "datastore2", + Datastore: "/dc2/datastore/datastore2", ResourcePool: "/dc2/host/c2/Resources/rp2", }, }, @@ -327,21 +296,21 @@ func TestConfigMasters(t *testing.T) { Server: "your.vcenter.example.com", Datacenter: "dc1", Folder: "/dc1/vm/folder1", - Datastore: "datastore1", + Datastore: "/dc1/datastore/datastore1", ResourcePool: "/dc1/host/c1/Resources", }, { Server: "your.vcenter.example.com", Datacenter: "dc2", Folder: "/dc2/vm/folder2", - Datastore: "datastore2", + Datastore: "/dc2/datastore/datastore2", ResourcePool: "/dc2/host/c2/Resources/rp2", }, { Server: "your.vcenter.example.com", Datacenter: "dc3", Folder: "/dc3/vm/folder3", - Datastore: "datastore3", + Datastore: "/dc3/datastore/datastore3", ResourcePool: "/dc3/host/c3/Resources/rp3", }, }, @@ -351,7 +320,7 @@ func TestConfigMasters(t *testing.T) { for _, tc := range testCases { t.Run(tc.testCase, func(t *testing.T) { machines, err := Machines(clusterID, tc.installConfig, tc.machinePool, "", "", "") - assertOnUnexpectedErrorState(tc.expectedError, err, t) + assertOnUnexpectedErrorState(t, tc.expectedError, err) if len(tc.workspaces) > 0 { var matchCountByIndex []int @@ -363,7 +332,7 @@ func TestConfigMasters(t *testing.T) { // check if expected workspaces are returned machineWorkspace := machine.Spec.ProviderSpec.Value.Object.(*machineapi.VSphereMachineProviderSpec).Workspace for idx, workspace := range tc.workspaces { - if reflect.DeepEqual(workspace, *machineWorkspace) { + if cmp.Equal(workspace, *machineWorkspace) { matchCountByIndex[idx]++ } } diff --git a/pkg/asset/machines/vsphere/machinesets_test.go b/pkg/asset/machines/vsphere/machinesets_test.go index 48b6316a00b..f5feacc9c6c 100644 --- a/pkg/asset/machines/vsphere/machinesets_test.go +++ b/pkg/asset/machines/vsphere/machinesets_test.go @@ -1,9 +1,10 @@ package vsphere import ( - "reflect" "testing" + "github.com/google/go-cmp/cmp" + machineapi "github.com/openshift/api/machine/v1beta1" "github.com/openshift/installer/pkg/types" "github.com/openshift/installer/pkg/types/vsphere" @@ -94,24 +95,6 @@ var machineComputePoolUndefinedZones = types.MachinePool{ Architecture: types.ArchitectureAMD64, } -var machineComputePoolNoZones = types.MachinePool{ - Name: "worker", - Replicas: &machinePoolReplicas, - Platform: types.MachinePoolPlatform{ - VSphere: &vsphere.MachinePool{ - NumCPUs: 4, - NumCoresPerSocket: 2, - MemoryMiB: 16384, - OSDisk: vsphere.OSDisk{ - DiskSizeGB: 60, - }, - Zones: []string{}, - }, - }, - Hyperthreading: "true", - Architecture: types.ArchitectureAMD64, -} - func TestConfigMachinesets(t *testing.T) { clusterID := "test" osImage := "test-cluster-xyzxyz-rhcos" @@ -123,6 +106,10 @@ func TestConfigMachinesets(t *testing.T) { } defaultClusterResourcePool, err := parseInstallConfig() + if err != nil { + t.Error(err) + return + } defaultClusterResourcePool.VSphere.FailureDomains[0].Topology.ResourcePool = "" testCases := []struct { @@ -135,23 +122,6 @@ func TestConfigMachinesets(t *testing.T) { totalReplicas int installConfig *types.InstallConfig }{ - { - testCase: "machinepool with no defined zones should use legacy configuration", - machinePool: &machineComputePoolNoZones, - minReplicas: 3, - maxReplicas: 3, - totalReplicas: 3, - installConfig: installConfig, - workspaces: []machineapi.Workspace{ - { - Server: "your.vcenter.example.com", - Datacenter: "datacenter", - Folder: "/datacenter/vm/test", - Datastore: "datastore", - ResourcePool: "/datacenter/host//Resources", - }, - }, - }, { testCase: "zones distributed among compute machinesets(zone count matches machineset count)", machinePool: &machineComputePoolValidZones, @@ -164,21 +134,21 @@ func TestConfigMachinesets(t *testing.T) { Server: "your.vcenter.example.com", Datacenter: "dc1", Folder: "/dc1/vm/folder1", - Datastore: "datastore1", + Datastore: "/dc1/datastore/datastore1", ResourcePool: "/dc1/host/c1/Resources/rp1", }, { Server: "your.vcenter.example.com", Datacenter: "dc2", Folder: "/dc2/vm/folder2", - Datastore: "datastore2", + Datastore: "/dc2/datastore/datastore2", ResourcePool: "/dc2/host/c2/Resources/rp2", }, { Server: "your.vcenter.example.com", Datacenter: "dc3", Folder: "/dc3/vm/folder3", - Datastore: "datastore3", + Datastore: "/dc3/datastore/datastore3", ResourcePool: "/dc3/host/c3/Resources/rp3", }, }, @@ -201,14 +171,14 @@ func TestConfigMachinesets(t *testing.T) { Server: "your.vcenter.example.com", Datacenter: "dc1", Folder: "/dc1/vm/folder1", - Datastore: "datastore1", + Datastore: "/dc1/datastore/datastore1", ResourcePool: "/dc1/host/c1/Resources/rp1", }, { Server: "your.vcenter.example.com", Datacenter: "dc2", Folder: "/dc2/vm/folder2", - Datastore: "datastore2", + Datastore: "/dc2/datastore/datastore2", ResourcePool: "/dc2/host/c2/Resources/rp2", }, }, @@ -225,14 +195,14 @@ func TestConfigMachinesets(t *testing.T) { Server: "your.vcenter.example.com", Datacenter: "dc1", Folder: "/dc1/vm/folder1", - Datastore: "datastore1", + Datastore: "/dc1/datastore/datastore1", ResourcePool: "/dc1/host/c1/Resources/rp1", }, { Server: "your.vcenter.example.com", Datacenter: "dc2", Folder: "/dc2/vm/folder2", - Datastore: "datastore2", + Datastore: "/dc2/datastore/datastore2", ResourcePool: "/dc2/host/c2/Resources/rp2", }, }, @@ -249,21 +219,21 @@ func TestConfigMachinesets(t *testing.T) { Server: "your.vcenter.example.com", Datacenter: "dc1", Folder: "/dc1/vm/folder1", - Datastore: "datastore1", + Datastore: "/dc1/datastore/datastore1", ResourcePool: "/dc1/host/c1/Resources", }, { Server: "your.vcenter.example.com", Datacenter: "dc2", Folder: "/dc2/vm/folder2", - Datastore: "datastore2", + Datastore: "/dc2/datastore/datastore2", ResourcePool: "/dc2/host/c2/Resources/rp2", }, { Server: "your.vcenter.example.com", Datacenter: "dc3", Folder: "/dc3/vm/folder3", - Datastore: "datastore3", + Datastore: "/dc3/datastore/datastore3", ResourcePool: "/dc3/host/c3/Resources/rp3", }, }, @@ -272,7 +242,7 @@ func TestConfigMachinesets(t *testing.T) { for _, tc := range testCases { t.Run(tc.testCase, func(t *testing.T) { machineSets, err := MachineSets(clusterID, tc.installConfig, tc.machinePool, osImage, "", "") - assertOnUnexpectedErrorState(tc.expectedError, err, t) + assertOnUnexpectedErrorState(t, tc.expectedError, err) if len(tc.workspaces) > 0 { var matchCountByIndex []int @@ -283,7 +253,7 @@ func TestConfigMachinesets(t *testing.T) { for _, machineSet := range machineSets { // check if replica counts match expected replicas := int(*machineSet.Spec.Replicas) - replicaCount = replicaCount + replicas + replicaCount += replicas if replicas > tc.maxReplicas { t.Errorf("machineset %s has too many replicas[max: %d] found %d", machineSet.Name, tc.maxReplicas, replicas) } else if replicas < tc.minReplicas { @@ -293,7 +263,7 @@ func TestConfigMachinesets(t *testing.T) { // check if expected workspaces are returned machineWorkspace := machineSet.Spec.Template.Spec.ProviderSpec.Value.Object.(*machineapi.VSphereMachineProviderSpec).Workspace for idx, workspace := range tc.workspaces { - if reflect.DeepEqual(workspace, *machineWorkspace) { + if cmp.Equal(workspace, *machineWorkspace) { matchCountByIndex[idx]++ } } diff --git a/pkg/asset/manifests/operators_test.go b/pkg/asset/manifests/operators_test.go index ba7bc3877cd..a101813882a 100644 --- a/pkg/asset/manifests/operators_test.go +++ b/pkg/asset/manifests/operators_test.go @@ -48,11 +48,29 @@ func TestRedactedInstallConfig(t *testing.T) { }, Platform: types.Platform{ VSphere: &vspheretypes.Platform{ - VCenter: "test-server-1", - Username: "test-user-1", - Password: "test-pass-1", - Datacenter: "test-datacenter", - DefaultDatastore: "test-datastore", + DeprecatedUsername: "test-username", + DeprecatedPassword: "test-password", + VCenters: []vspheretypes.VCenter{{ + Server: "test-server-1", + Port: 443, + Username: "", + Password: "", + Datacenters: []string{"test-datacenter"}, + }}, + FailureDomains: []vspheretypes.FailureDomain{{ + Name: "test-failuredomain", + Region: "test-region", + Zone: "test-zone", + Server: "test-server-1", + Topology: vspheretypes.Topology{ + Datacenter: "test-datacenter", + ComputeCluster: "test-computecluster", + Networks: []string{"test-network"}, + Datastore: "test-datastore", + ResourcePool: "test-resourcepool", + Folder: "test-folder", + }, + }}, }, }, PullSecret: "test-pull-secret", @@ -84,11 +102,25 @@ networking: - 1.2.3.4/5 platform: vsphere: - datacenter: test-datacenter - defaultDatastore: test-datastore - password: "" - username: "" - vCenter: test-server-1 + failureDomains: + - name: test-failuredomain + region: test-region + server: test-server-1 + topology: + computeCluster: test-computecluster + datacenter: test-datacenter + datastore: test-datastore + folder: test-folder + networks: + - test-network + resourcePool: test-resourcepool + zone: test-zone + vcenters: + - datacenters: + - test-datacenter + password: "" + server: test-server-1 + user: "" pullSecret: "" sshKey: test-ssh-key ` diff --git a/pkg/asset/manifests/vsphere/cloudproviderconfig_test.go b/pkg/asset/manifests/vsphere/cloudproviderconfig_test.go index ca78405d843..dc650a7d96c 100644 --- a/pkg/asset/manifests/vsphere/cloudproviderconfig_test.go +++ b/pkg/asset/manifests/vsphere/cloudproviderconfig_test.go @@ -1,7 +1,7 @@ package vsphere import ( - "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -10,22 +10,7 @@ import ( ) var ( - expectedInTreeConfig = `[Global] -secret-name = "vsphere-creds" -secret-namespace = "kube-system" -insecure-flag = "1" - -[Workspace] -server = "test-vcenter" -datacenter = "test-datacenter" -default-datastore = "test-datastore" -folder = "/test-datacenter/vm/clusterID" - -[VirtualCenter "test-vcenter"] -datacenters = "test-datacenter" -` - - expectedIniMultiZoneConfig = `[Global] + expectedIniConfig = `[Global] secret-name = "vsphere-creds" secret-namespace = "kube-system" insecure-flag = "1" @@ -36,13 +21,20 @@ port = "443" datacenters = "test-datacenter,test-datacenter2" [Workspace] -folder = "/test-datacenter/vm/clusterID" +server = "test-vcenter" +datacenter = "test-datacenter" +default-datastore = "test-datastore" +folder = "/test-datacenter/vm/test-folder" +resourcepool-path = "/test-datacenter/host/cluster/Resources/test-resourcepool" + +` -[Labels] + expectIniLabelsSection = `[Labels] region = "openshift-region" zone = "openshift-zone" ` - expectedYamlMultiZoneConfig = `global: + + expectedYamlConfig = `global: user: "" password: "" server: "" @@ -68,6 +60,7 @@ vcenter: insecureFlag: false datacenters: - test-datacenter + - test-datacenter2 soapRoundtripCount: 0 caFile: "" thumbprint: "" @@ -81,16 +74,7 @@ labels: ` ) -func validInTreePlatform() *vsphere.Platform { - return &vsphere.Platform{ - VCenter: "test-vcenter", - Username: "test-username", - Password: "test-password", - Datacenter: "test-datacenter", - DefaultDatastore: "test-datastore", - } -} -func validMultiVCenterPlatform() *vsphere.Platform { +func validPlatform() *vsphere.Platform { return &vsphere.Platform{ VCenters: []vsphere.VCenter{ { @@ -100,6 +84,7 @@ func validMultiVCenterPlatform() *vsphere.Platform { Password: "test-password", Datacenters: []string{ "test-datacenter", + "test-datacenter2", }, }, }, @@ -151,24 +136,40 @@ func validMultiVCenterPlatform() *vsphere.Platform { } func TestCloudProviderConfig(t *testing.T) { - folderPath := fmt.Sprintf("/%s/vm/%s", "test-datacenter", "clusterID") cases := []struct { name string platform *vsphere.Platform + cloudProviderFunc func(string, *vsphere.Platform) (string, error) expectedCloudConfig string - outOfTree bool }{ { - name: "valid in-tree cloud provider config", - platform: validInTreePlatform(), - expectedCloudConfig: expectedInTreeConfig, - outOfTree: false, + name: "valid intree cloud provider config", + platform: validPlatform(), + cloudProviderFunc: CloudProviderConfigIni, + expectedCloudConfig: expectedIniConfig + expectIniLabelsSection, + }, + { + name: "valid single failure domain intree cloud provider config", + platform: func() *vsphere.Platform { + p := validPlatform() + + p.FailureDomains = p.FailureDomains[0:1] + p.VCenters[0].Datacenters = p.VCenters[0].Datacenters[0:1] + + return p + }(), + cloudProviderFunc: CloudProviderConfigIni, + expectedCloudConfig: func() string { + // only a single datacenter would be provided to the datacenters + ini := strings.ReplaceAll(expectedIniConfig, ",test-datacenter2", "") + return ini + }(), }, { - name: "valid out of tree cloud provider config", - platform: validMultiVCenterPlatform(), - expectedCloudConfig: expectedIniMultiZoneConfig, - outOfTree: true, + name: "valid out of tree yaml cloud provider config", + platform: validPlatform(), + cloudProviderFunc: CloudProviderConfigYaml, + expectedCloudConfig: expectedYamlConfig, }, } @@ -176,12 +177,7 @@ func TestCloudProviderConfig(t *testing.T) { t.Run(tc.name, func(t *testing.T) { var cloudConfig string var err error - if tc.outOfTree { - cloudConfig, err = MultiZoneIniCloudProviderConfig(folderPath, tc.platform) - fmt.Println(cloudConfig) - } else { - cloudConfig, err = InTreeCloudProviderConfig(folderPath, tc.platform) - } + cloudConfig, err = tc.cloudProviderFunc("infraID", tc.platform) assert.NoError(t, err, "failed to create cloud provider config") assert.Equal(t, tc.expectedCloudConfig, cloudConfig, "unexpected cloud provider config") }) diff --git a/pkg/destroy/vsphere/vsphere_test.go b/pkg/destroy/vsphere/vsphere_test.go index 5e155e6b499..ddb4645481f 100644 --- a/pkg/destroy/vsphere/vsphere_test.go +++ b/pkg/destroy/vsphere/vsphere_test.go @@ -39,9 +39,6 @@ var ( logger.SetOutput(io.Discard) return logger }() - zoningTerraform = func(m *types.ClusterMetadata) { - m.VSphere.TerraformPlatform = vsphere.ZoningTerraformName - } runningVM = func() mo.VirtualMachine { vm := mo.VirtualMachine{} vm.Name = "runningVM" @@ -127,7 +124,7 @@ func TestVsphereDeleteFolder(t *testing.T) { }, { name: "Delete non-empty folder fails", - editFuncs: editMetadataFuncs{zoningTerraform, manyChildrenFolder}, + editFuncs: editMetadataFuncs{manyChildrenFolder}, errorMsg: "Expected Folder .* to be empty", }, { @@ -137,17 +134,12 @@ func TestVsphereDeleteFolder(t *testing.T) { }, { name: "Delete folders Zoning Terraform", - editFuncs: editMetadataFuncs{zoningTerraform, manyFoldersPerTag}, - errorMsg: "", - }, - { - name: "Delete multiple folders per tag fails in non-Zoning terraform", editFuncs: editMetadataFuncs{manyFoldersPerTag}, - errorMsg: "Expected 1 Folder per tag but got [0-9]+", + errorMsg: "", }, { name: "Delete folder fails", - editFuncs: editMetadataFuncs{zoningTerraform, deleteFails}, + editFuncs: editMetadataFuncs{deleteFails}, errorMsg: "some vsphere error", }, } diff --git a/pkg/types/conversion/installconfig_test.go b/pkg/types/conversion/installconfig_test.go index eb4a1a71647..6b2e786758b 100644 --- a/pkg/types/conversion/installconfig_test.go +++ b/pkg/types/conversion/installconfig_test.go @@ -459,6 +459,27 @@ func TestConvertInstallConfig(t *testing.T) { }, Platform: types.Platform{ VSphere: &vsphere.Platform{ + VCenters: []vsphere.VCenter{{ + Server: "", + Port: 443, + Username: "", + Password: "", + Datacenters: nil, + }}, + FailureDomains: []vsphere.FailureDomain{{ + Name: "generated-failure-domain", + Region: "generated-region", + Zone: "generated-zone", + Server: "", + Topology: vsphere.Topology{ + Datacenter: "", + ComputeCluster: "", + Networks: []string{""}, + Datastore: "", + ResourcePool: "", + Folder: "", + }, + }}, DeprecatedAPIVIP: "1.2.3.4", APIVIPs: []string{"1.2.3.4"}, }, @@ -483,6 +504,27 @@ func TestConvertInstallConfig(t *testing.T) { }, Platform: types.Platform{ VSphere: &vsphere.Platform{ + VCenters: []vsphere.VCenter{{ + Server: "", + Port: 443, + Username: "", + Password: "", + Datacenters: nil, + }}, + FailureDomains: []vsphere.FailureDomain{{ + Name: "generated-failure-domain", + Region: "generated-region", + Zone: "generated-zone", + Server: "", + Topology: vsphere.Topology{ + Datacenter: "", + ComputeCluster: "", + Networks: []string{""}, + Datastore: "", + ResourcePool: "", + Folder: "", + }, + }}, DeprecatedIngressVIP: "1.2.3.4", IngressVIPs: []string{"1.2.3.4"}, }, diff --git a/pkg/types/validation/installconfig_test.go b/pkg/types/validation/installconfig_test.go index ab6e0f7dd5d..9aa962c0ab3 100644 --- a/pkg/types/validation/installconfig_test.go +++ b/pkg/types/validation/installconfig_test.go @@ -111,11 +111,46 @@ func validLibvirtPlatform() *libvirt.Platform { func validVSpherePlatform() *vsphere.Platform { return &vsphere.Platform{ - VCenter: "test-server", - Username: "test-username", - Password: "test-password", - Datacenter: "test-datacenter", - DefaultDatastore: "test-datastore", + VCenters: []vsphere.VCenter{ + { + Server: "test-vcenter", + Port: 443, + Username: "test-username", + Password: "test-password", + Datacenters: []string{ + "test-datacenter", + }, + }, + }, + FailureDomains: []vsphere.FailureDomain{ + { + Name: "test-east-1a", + Region: "test-east", + Zone: "test-east-1a", + Server: "test-vcenter", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter", + ComputeCluster: "/test-datacenter/host/test-cluster", + Datastore: "/test-datacenter/datastore/test-datastore", + Networks: []string{"test-portgroup"}, + ResourcePool: "/test-datacenter/host/test-cluster/Resources/test-resourcepool", + Folder: "/test-datacenter/vm/test-folder", + }, + }, + { + Name: "test-east-2a", + Region: "test-east", + Zone: "test-east-2a", + Server: "test-vcenter", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter", + ComputeCluster: "/test-datacenter/host/test-cluster", + Datastore: "/test-datacenter/datastore/test-datastore", + Networks: []string{"test-portgroup"}, + Folder: "/test-datacenter/vm/test-folder", + }, + }, + }, } } @@ -647,10 +682,10 @@ func TestValidateInstallConfig(t *testing.T) { c.Platform = types.Platform{ VSphere: validVSpherePlatform(), } - c.Platform.VSphere.VCenter = "" + c.Platform.VSphere.VCenters[0].Server = "" return c }(), - expectedError: `^platform\.vsphere.vCenter: Required value: must specify the name of the vCenter$`, + expectedError: `platform\.vsphere\.vcenters\.server: Required value: must be the domain name or IP address of the vCenter(.*)`, }, { name: "invalid vsphere folder", @@ -659,10 +694,10 @@ func TestValidateInstallConfig(t *testing.T) { c.Platform = types.Platform{ VSphere: validVSpherePlatform(), } - c.Platform.VSphere.Folder = "my-folder" + c.Platform.VSphere.FailureDomains[0].Topology.Folder = "my-folder" return c }(), - expectedError: `^platform\.vsphere\.folder: Invalid value: \"my-folder\": folder must be absolute path: expected prefix /test-datacenter/vm/$`, + expectedError: `^platform\.vsphere\.failureDomains\.topology.folder: Invalid value: "my-folder": full path of folder must be provided in format //vm/$`, }, { name: "invalid vsphere resource pool", @@ -671,10 +706,10 @@ func TestValidateInstallConfig(t *testing.T) { c.Platform = types.Platform{ VSphere: validVSpherePlatform(), } - c.Platform.VSphere.ResourcePool = "my-resource-pool" + c.Platform.VSphere.FailureDomains[0].Topology.ResourcePool = "my-resource-pool" return c }(), - expectedError: `^platform\.vsphere\.resourcePool: Invalid value: \"my-resource-pool\": resourcePool must be absolute path: expected prefix /test-datacenter/host//Resources/$`, + expectedError: `^platform\.vsphere\.failureDomains\.topology\.resourcePool: Invalid value: "my-resource-pool": full path of resource pool must be provided in format //host//\.\.\.$`, }, { name: "empty proxy settings", diff --git a/pkg/types/vsphere/conversion/installconfig_test.go b/pkg/types/vsphere/conversion/installconfig_test.go new file mode 100644 index 00000000000..73b5e6e6d31 --- /dev/null +++ b/pkg/types/vsphere/conversion/installconfig_test.go @@ -0,0 +1,593 @@ +package conversion + +import ( + "testing" + + "github.com/onsi/gomega" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + + "github.com/openshift/installer/pkg/ipnet" + "github.com/openshift/installer/pkg/types" + "github.com/openshift/installer/pkg/types/vsphere" +) + +var ( + validCIDR = "10.0.0.0/16" +) + +func validInstallConfig() *types.InstallConfig { + return &types.InstallConfig{ + Networking: &types.Networking{ + MachineNetwork: []types.MachineNetworkEntry{ + {CIDR: *ipnet.MustParseCIDR(validCIDR)}, + }, + }, + Publish: types.ExternalPublishingStrategy, + Platform: types.Platform{ + VSphere: &vsphere.Platform{ + APIVIPs: []string{"192.168.111.0"}, + IngressVIPs: []string{"192.168.111.1"}, + }, + }, + } +} + +func validLegacyUpiInstallConfig() *types.InstallConfig { + installConfig := validInstallConfig() + + // The fields below are the original UPI required + installConfig.VSphere.DeprecatedVCenter = "test-server" + installConfig.VSphere.DeprecatedUsername = "test-username" + installConfig.VSphere.DeprecatedPassword = "test-password" + installConfig.VSphere.DeprecatedDefaultDatastore = "test-datastore" + installConfig.VSphere.DeprecatedDatacenter = "test-datacenter" + + return installConfig +} + +func validLegacyIpiInstallConfig() *types.InstallConfig { + installConfig := validLegacyUpiInstallConfig() + + installConfig.VSphere.DeprecatedCluster = "test-cluster" + installConfig.VSphere.DeprecatedNetwork = "test-network" + installConfig.VSphere.DeprecatedFolder = "" + installConfig.VSphere.DeprecatedResourcePool = "" + + return installConfig +} + +func convertedLegacyUpiInstallConfig() *types.InstallConfig { + installConfig := validInstallConfig() + installConfig.VSphere = &vsphere.Platform{ + DeprecatedVCenter: "test-server", + DeprecatedUsername: "test-username", + DeprecatedPassword: "test-password", + DeprecatedDatacenter: "test-datacenter", + DeprecatedDefaultDatastore: "test-datastore", + DeprecatedFolder: "", + DeprecatedCluster: "", + DeprecatedResourcePool: "", + ClusterOSImage: "", + DeprecatedAPIVIP: "", + APIVIPs: []string{"192.168.111.0"}, + DeprecatedIngressVIP: "", + IngressVIPs: []string{"192.168.111.1"}, + DefaultMachinePlatform: nil, + DeprecatedNetwork: "", + DiskType: "", + VCenters: []vsphere.VCenter{{ + Server: "test-server", + Port: 443, + Username: "test-username", + Password: "test-password", + Datacenters: []string{"test-datacenter"}, + }}, + FailureDomains: []vsphere.FailureDomain{{ + Name: "generated-failure-domain", + Region: "generated-region", + Zone: "generated-zone", + Server: "test-server", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter", + ComputeCluster: "", + Networks: []string{""}, + Datastore: "/test-datacenter/datastore/test-datastore", + ResourcePool: "", + Folder: "", + }, + }}, + } + + return installConfig +} +func convertedLegacyIpiInstallConfig() *types.InstallConfig { + installConfig := validInstallConfig() + installConfig.VSphere = &vsphere.Platform{ + DeprecatedVCenter: "test-server", + DeprecatedUsername: "test-username", + DeprecatedPassword: "test-password", + DeprecatedDatacenter: "test-datacenter", + DeprecatedDefaultDatastore: "test-datastore", + DeprecatedFolder: "", + DeprecatedCluster: "test-cluster", + DeprecatedResourcePool: "", + ClusterOSImage: "", + DeprecatedAPIVIP: "", + APIVIPs: []string{"192.168.111.0"}, + DeprecatedIngressVIP: "", + IngressVIPs: []string{"192.168.111.1"}, + DefaultMachinePlatform: nil, + DeprecatedNetwork: "test-network", + DiskType: "", + VCenters: []vsphere.VCenter{{ + Server: "test-server", + Port: 443, + Username: "test-username", + Password: "test-password", + Datacenters: []string{"test-datacenter"}, + }}, + FailureDomains: []vsphere.FailureDomain{{ + Name: "generated-failure-domain", + Region: "generated-region", + Zone: "generated-zone", + Server: "test-server", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter", + ComputeCluster: "/test-datacenter/host/test-cluster", + Networks: []string{"test-network"}, + Datastore: "/test-datacenter/datastore/test-datastore", + ResourcePool: "", + Folder: "", + }, + }}, + } + + return installConfig +} + +func convertedLegacyIpiZonalInstallConfig() *types.InstallConfig { + installConfig := convertedLegacyIpiInstallConfig() + + installConfig.VSphere.VCenters = []vsphere.VCenter{ + { + Server: "test-server", + Port: 443, + Username: "test-username", + Password: "test-password", + Datacenters: []string{ + "test-datacenter", + "test-datacenter4", + }, + }, + } + + installConfig.VSphere.FailureDomains = []vsphere.FailureDomain{ + { + Name: "region-1-zone-1a", + Region: "region-1", + Zone: "zone-1a", + Server: "test-server", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter", + ComputeCluster: "/test-datacenter1/host/test-computecluster1", + Networks: []string{"network1"}, + Datastore: "/test-datacenter/datastore/datastore1", + }, + }, + { + Name: "region-2-zone-2a", + Region: "region-2", + Zone: "zone-2a", + Server: "test-server", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter", + ComputeCluster: "/test-datacenter1/host/test-computecluster2", + Networks: []string{"network2"}, + Datastore: "/test-datacenter/datastore/datastore2", + }, + }, + { + Name: "region-3-zone-3a", + Region: "region-3", + Zone: "zone-3a", + Server: "test-server", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter", + ComputeCluster: "/test-datacenter1/host/test-computecluster3", + Networks: []string{"network3"}, + Datastore: "/test-datacenter/datastore/datastore3", + }, + }, + { + Name: "region-4-zone-4a", + Region: "region-4", + Zone: "zone-4a", + Server: "test-server", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter4", + ComputeCluster: "/test-datacenter4/host/test-computecluster4", + Networks: []string{"network4"}, + Datastore: "/test-datacenter4/datastore/datastore4", + }, + }, + } + + return installConfig +} + +// validLegacyIpiZonalInstallConfig defines an install-config +// that was valid during 4.12 tech preview of zonal. +func validLegacyIpiZonalInstallConfig() *types.InstallConfig { + installConfig := validLegacyIpiInstallConfig() + + installConfig.VSphere.FailureDomains = []vsphere.FailureDomain{ + { + Name: "region-1-zone-1a", + Region: "region-1", + Zone: "zone-1a", + Server: "", + Topology: vsphere.Topology{ + Datacenter: "", + ComputeCluster: "/test-datacenter1/host/test-computecluster1", + Networks: []string{"network1"}, + Datastore: "datastore1", + ResourcePool: "", + Folder: "", + }, + }, + { + Name: "region-2-zone-2a", + Region: "region-2", + Zone: "zone-2a", + Server: "", + Topology: vsphere.Topology{ + Datacenter: "", + ComputeCluster: "/test-datacenter1/host/test-computecluster2", + Networks: []string{"network2"}, + Datastore: "datastore2", + ResourcePool: "", + Folder: "", + }, + }, + { + Name: "region-3-zone-3a", + Region: "region-3", + Zone: "zone-3a", + Server: "", + Topology: vsphere.Topology{ + Datacenter: "", + ComputeCluster: "/test-datacenter1/host/test-computecluster3", + Networks: []string{"network3"}, + Datastore: "datastore3", + ResourcePool: "", + Folder: "", + }, + }, + { + Name: "region-4-zone-4a", + Region: "region-4", + Zone: "zone-4a", + Server: "", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter4", + ComputeCluster: "/test-datacenter4/host/test-computecluster4", + Networks: []string{"network4"}, + Datastore: "datastore4", + ResourcePool: "", + Folder: "", + }, + }, + } + + return installConfig +} + +func validIpiInstallConfig() *types.InstallConfig { + installConfig := validInstallConfig() + + installConfig.VSphere = &vsphere.Platform{ + DeprecatedVCenter: "", + DeprecatedUsername: "", + DeprecatedPassword: "", + DeprecatedDatacenter: "", + DeprecatedDefaultDatastore: "", + DeprecatedFolder: "", + DeprecatedCluster: "", + DeprecatedResourcePool: "", + ClusterOSImage: "", + DeprecatedAPIVIP: "", + APIVIPs: []string{"192.168.111.0"}, + DeprecatedIngressVIP: "", + IngressVIPs: []string{"192.168.111.1"}, + DefaultMachinePlatform: nil, + DeprecatedNetwork: "", + DiskType: "", + VCenters: []vsphere.VCenter{{ + Server: "test-server", + Port: 443, + Username: "test-username", + Password: "test-password", + Datacenters: []string{"test-datacenter1", "test-datacenter4"}, + }}, + FailureDomains: []vsphere.FailureDomain{ + { + Name: "region-1-zone-1a", + Region: "region-1", + Zone: "zone-1a", + Server: "test-server", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter1", + ComputeCluster: "/test-datacenter1/host/test-computecluster1", + Networks: []string{"network1"}, + Datastore: "/test-datacenter1/datstore/test-datastore1", + ResourcePool: "", + Folder: "", + }, + }, + { + Name: "region-2-zone-2a", + Region: "region-2", + Zone: "zone-2a", + Server: "", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter1", + ComputeCluster: "/test-datacenter1/host/test-computecluster2", + Networks: []string{"network2"}, + Datastore: "/test-datacenter1/datastore/test-datastore2", + ResourcePool: "", + Folder: "", + }, + }, + { + Name: "region-3-zone-3a", + Region: "region-3", + Zone: "zone-3a", + Server: "", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter1", + ComputeCluster: "/test-datacenter1/host/test-computecluster3", + Networks: []string{"network3"}, + Datastore: "/test-datacenter1/datastore/test-datastore3", + ResourcePool: "/test-datacenter1/host/test-computecluster3/Resources/test-resourcepool4", + Folder: "", + }, + }, + { + Name: "region-4-zone-4a", + Region: "region-4", + Zone: "zone-4a", + Server: "", + Topology: vsphere.Topology{ + Datacenter: "test-datacenter4", + ComputeCluster: "/test-datacenter4/host/test-computecluster4", + Networks: []string{"network4"}, + Datastore: "/test-datacenter4/datastore/datastore4", + ResourcePool: "", + Folder: "/test-datacenter4/vm/test-folder4", + }, + }, + }, + } + + return installConfig +} + +func TestConvertInstallConfig(t *testing.T) { + logger, hook := test.NewNullLogger() + localLogger = logger + tests := []struct { + name string + actualInstallConfig func() *types.InstallConfig + expectInstallConfig func() *types.InstallConfig + expectWarn []string + expectLevel logrus.Level + }{ + { + name: "legacy upi conversion", + actualInstallConfig: validLegacyUpiInstallConfig, + expectInstallConfig: convertedLegacyUpiInstallConfig, + expectLevel: logrus.WarnLevel, + expectWarn: []string{ + "vsphere authentication fields are now depreciated please use vcenters", + "vsphere topology fields are now depreciated please use failureDomains", + "datastore as a non-path is now depreciated please use the form: /test-datacenter/datastore/test-datastore", + }, + }, + { + name: "legacy ipi conversion", + actualInstallConfig: validLegacyIpiInstallConfig, + expectInstallConfig: convertedLegacyIpiInstallConfig, + expectLevel: logrus.WarnLevel, + expectWarn: []string{ + "vsphere authentication fields are now depreciated please use vcenters", + "vsphere topology fields are now depreciated please use failureDomains", + "computeCluster as a non-path is now depreciated please use the form: /test-datacenter/host/test-cluster", + "datastore as a non-path is now depreciated please use the form: /test-datacenter/datastore/test-datastore", + }, + }, + { + name: "legacy zonal ipi conversion", + actualInstallConfig: validLegacyIpiZonalInstallConfig, + expectInstallConfig: convertedLegacyIpiZonalInstallConfig, + expectLevel: logrus.WarnLevel, + expectWarn: []string{ + "vsphere authentication fields are now depreciated please use vcenters", + "datastore as a non-path is now depreciated please use the form: /test-datacenter/datastore/datastore1", + "datastore as a non-path is now depreciated please use the form: /test-datacenter/datastore/datastore2", + "datastore as a non-path is now depreciated please use the form: /test-datacenter/datastore/datastore3", + "datastore as a non-path is now depreciated please use the form: /test-datacenter4/datastore/datastore4", + }, + }, + { + name: "zonal ipi", + actualInstallConfig: validIpiInstallConfig, + expectInstallConfig: validIpiInstallConfig, + expectLevel: logrus.WarnLevel, + expectWarn: []string{""}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := gomega.NewWithT(t) + + actual := tt.actualInstallConfig() + expect := tt.expectInstallConfig() + + if err := ConvertInstallConfig(actual); err != nil { + g.Expect(err).Should(gomega.Equal(nil)) + } + + if tt.expectWarn[0] != "" { + entries := hook.AllEntries() + assert.NotEmpty(t, entries) + for i, entry := range entries { + g.Expect(entry.Level).To(gomega.Equal(tt.expectLevel)) + g.Expect(entry.Message).To(gomega.Equal(tt.expectWarn[i])) + } + } + hook.Reset() + + g.Expect(actual).Should(gomega.BeComparableTo(expect)) + }) + } +} + +func Test_setComputeClusterPath(t *testing.T) { + logger, hook := test.NewNullLogger() + localLogger = logger + + tests := []struct { + name string + computeCluster string + datacenter string + expectLevel string + expectWarn string + expectCluster string + }{ + { + name: "correct cluster path", + computeCluster: "/DC1/host/C1", + datacenter: "DC1", + expectLevel: ``, + expectWarn: ``, + expectCluster: "/DC1/host/C1", + }, + { + name: "cluster name, not path", + computeCluster: "C1", + datacenter: "DC1", + expectLevel: `warning`, + expectWarn: `computeCluster as a non-path is now depreciated please use the form: /DC1/host/C1`, + expectCluster: "/DC1/host/C1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := setComputeClusterPath(tt.computeCluster, tt.datacenter) + assert.Equal(t, tt.expectCluster, got) + if tt.expectWarn != "" { + entries := hook.AllEntries() + assert.NotEmpty(t, entries) + for _, e := range entries { + assert.Equal(t, tt.expectLevel, e.Level.String()) + assert.Regexp(t, tt.expectWarn, e.Message) + } + } + hook.Reset() + }) + } +} + +func Test_setDatastorePath(t *testing.T) { + logger, hook := test.NewNullLogger() + localLogger = logger + + tests := []struct { + name string + datastore string + datacenter string + expectLevel string + expectWarn string + expectDatstore string + }{ + { + name: "correct datastore path", + datastore: "/DC1/datastore/DS1", + datacenter: "DC1", + expectLevel: ``, + expectWarn: ``, + expectDatstore: "/DC1/datastore/DS1", + }, + { + name: "cluster name, not path", + datastore: "DS1", + datacenter: "DC1", + expectLevel: `warning`, + expectWarn: `datastore as a non-path is now depreciated please use the form: /DC1/datastore/DS1`, + expectDatstore: "/DC1/datastore/DS1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := setDatastorePath(tt.datastore, tt.datacenter) + assert.Equal(t, tt.expectDatstore, got) + if tt.expectWarn != "" { + entries := hook.AllEntries() + assert.NotEmpty(t, entries) + for _, e := range entries { + assert.Equal(t, tt.expectLevel, e.Level.String()) + assert.Regexp(t, tt.expectWarn, e.Message) + } + } + hook.Reset() + }) + } +} + +func Test_setFolderPath(t *testing.T) { + logger, hook := test.NewNullLogger() + localLogger = logger + + tests := []struct { + name string + folder string + datacenter string + expectLevel string + expectWarn string + expectFolder string + }{ + { + name: "correct folder path", + folder: "/DC1/vm/Folder1", + datacenter: "DC1", + expectLevel: ``, + expectWarn: ``, + expectFolder: "/DC1/vm/Folder1", + }, + { + name: "folder as name not path", + folder: "Folder1", + datacenter: "DC1", + expectLevel: `warning`, + expectWarn: `folder as a non-path is now depreciated please use the form: /DC1/vm/Folder1`, + expectFolder: "/DC1/vm/Folder1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := setFolderPath(tt.folder, tt.datacenter) + assert.Equal(t, tt.expectFolder, got) + if tt.expectWarn != "" { + entries := hook.AllEntries() + assert.NotEmpty(t, entries) + for _, e := range entries { + assert.Equal(t, tt.expectLevel, e.Level.String()) + assert.Regexp(t, tt.expectWarn, e.Message) + } + } + hook.Reset() + }) + } +} diff --git a/pkg/types/vsphere/validation/machinepool_test.go b/pkg/types/vsphere/validation/machinepool_test.go index 80173d9babe..b0c44abb47e 100644 --- a/pkg/types/vsphere/validation/machinepool_test.go +++ b/pkg/types/vsphere/validation/machinepool_test.go @@ -114,7 +114,7 @@ func TestValidateMachinePool(t *testing.T) { { name: "multi-zone invalid zone name", platform: func() *vsphere.Platform { - platform := validMultiVCenterPlatform() + platform := validPlatform() platform.FailureDomains[0].Name = "Zone%^@112233" return platform }(), @@ -131,7 +131,7 @@ func TestValidateMachinePool(t *testing.T) { }, { name: "multi-zone valid", - platform: validMultiVCenterPlatform(), + platform: validPlatform(), pool: &types.MachinePool{ Platform: types.MachinePoolPlatform{ VSphere: &vsphere.MachinePool{ @@ -144,7 +144,7 @@ func TestValidateMachinePool(t *testing.T) { }, { name: "multi-zone no zones defined for control plane pool", - platform: validMultiVCenterPlatform(), + platform: validPlatform(), pool: &types.MachinePool{ Name: types.MachinePoolControlPlaneRoleName, Platform: types.MachinePoolPlatform{ @@ -156,7 +156,7 @@ func TestValidateMachinePool(t *testing.T) { }, { name: "multi-zone no zones defined for compute pool", - platform: validMultiVCenterPlatform(), + platform: validPlatform(), pool: &types.MachinePool{ Name: types.MachinePoolComputeRoleName, Platform: types.MachinePoolPlatform{ @@ -168,7 +168,7 @@ func TestValidateMachinePool(t *testing.T) { }, { name: "multi-zone undefined zone", - platform: validMultiVCenterPlatform(), + platform: validPlatform(), pool: &types.MachinePool{ Platform: types.MachinePoolPlatform{ VSphere: &vsphere.MachinePool{ @@ -214,7 +214,6 @@ func TestValidateMachinePool(t *testing.T) { if found == false { t.Errorf("unexpected zone %s", zone) } - } } }) diff --git a/pkg/types/vsphere/validation/platform_test.go b/pkg/types/vsphere/validation/platform_test.go index c89a283372e..3fbe2681598 100644 --- a/pkg/types/vsphere/validation/platform_test.go +++ b/pkg/types/vsphere/validation/platform_test.go @@ -1,8 +1,7 @@ package validation import ( - "errors" - "fmt" + "regexp" "testing" "github.com/stretchr/testify/assert" @@ -13,25 +12,6 @@ import ( func validPlatform() *vsphere.Platform { return &vsphere.Platform{ - VCenter: "test-vcenter", - Username: "test-username", - Password: "test-password", - Datacenter: "test-datacenter", - DefaultDatastore: "test-datastore", - } -} - -func validMultiVCenterPlatform() *vsphere.Platform { - return &vsphere.Platform{ - VCenter: "test-vcenter", - Username: "test-username", - Password: "test-password", - Datacenter: "test-datacenter", - DefaultDatastore: "test-datastore", - Folder: "/test-datacenter/vm/test-folder", - Cluster: "test-cluster", - ResourcePool: "/test-datacenter/host/test-cluster/Resources/test-resource-pool", - Network: "test-network", VCenters: []vsphere.VCenter{ { Server: "test-vcenter", @@ -52,7 +32,7 @@ func validMultiVCenterPlatform() *vsphere.Platform { Topology: vsphere.Topology{ Datacenter: "test-datacenter", ComputeCluster: "/test-datacenter/host/test-cluster", - Datastore: "test-datastore", + Datastore: "/test-datacenter/datastore/test-datastore", Networks: []string{"test-portgroup"}, ResourcePool: "/test-datacenter/host/test-cluster/Resources/test-resourcepool", Folder: "/test-datacenter/vm/test-folder", @@ -66,7 +46,7 @@ func validMultiVCenterPlatform() *vsphere.Platform { Topology: vsphere.Topology{ Datacenter: "test-datacenter", ComputeCluster: "/test-datacenter/host/test-cluster", - Datastore: "test-datastore", + Datastore: "/test-datacenter/datastore/test-datastore", Networks: []string{"test-portgroup"}, Folder: "/test-datacenter/vm/test-folder", }, @@ -75,37 +55,6 @@ func validMultiVCenterPlatform() *vsphere.Platform { } } -func checkIfFailureDomainDefaultsApplied(platform *vsphere.Platform) error { - for _, failureDomain := range platform.FailureDomains { - defaultsApplied := failureDomain.Server == platform.VCenter && - failureDomain.Topology.ResourcePool == platform.ResourcePool && - failureDomain.Topology.ComputeCluster == fmt.Sprintf("/%s/host/%s", platform.Datacenter, platform.Cluster) && - failureDomain.Topology.Networks[0] == platform.Network && - failureDomain.Topology.Datastore == platform.DefaultDatastore && - failureDomain.Topology.Folder == platform.Folder && - failureDomain.Topology.Datacenter == platform.Datacenter - if defaultsApplied == false { - return errors.New("defaults not applied") - } - } - return nil -} - -func checkIfFailureDomainOverridesApplied(platform *vsphere.Platform) error { - for _, failureDomain := range platform.FailureDomains { - defaultsNotApplied := failureDomain.Topology.ResourcePool != platform.ResourcePool && - failureDomain.Topology.ComputeCluster != fmt.Sprintf("/%s/host/%s", platform.Datacenter, platform.Cluster) && - failureDomain.Topology.Networks[0] != platform.Network && - failureDomain.Topology.Datastore != platform.DefaultDatastore && - failureDomain.Topology.Folder != platform.Folder && - failureDomain.Topology.Datacenter != platform.Datacenter - if defaultsNotApplied == false { - return errors.New("overrides not applied") - } - } - return nil -} - func TestValidatePlatform(t *testing.T) { cases := []struct { name string @@ -113,82 +62,6 @@ func TestValidatePlatform(t *testing.T) { postValidationFunction func(*vsphere.Platform) error expectedError string }{ - { - name: "minimal", - platform: validPlatform(), - }, - { - name: "missing vCenter name", - platform: func() *vsphere.Platform { - p := validPlatform() - p.VCenter = "" - return p - }(), - expectedError: `^test-path\.vCenter: Required value: must specify the name of the vCenter$`, - }, - { - name: "missing username", - platform: func() *vsphere.Platform { - p := validPlatform() - p.Username = "" - return p - }(), - expectedError: `^test-path\.username: Required value: must specify the username$`, - }, - { - name: "missing password", - platform: func() *vsphere.Platform { - p := validPlatform() - p.Password = "" - return p - }(), - expectedError: `^test-path\.password: Required value: must specify the password$`, - }, - { - name: "missing datacenter", - platform: func() *vsphere.Platform { - p := validPlatform() - p.Datacenter = "" - return p - }(), - expectedError: `^test-path\.datacenter: Required value: must specify the datacenter$`, - }, - { - name: "missing default datastore", - platform: func() *vsphere.Platform { - p := validPlatform() - p.DefaultDatastore = "" - return p - }(), - expectedError: `^test-path\.defaultDatastore: Required value: must specify the default datastore$`, - }, - { - name: "Invalid folder path", - platform: func() *vsphere.Platform { - p := validPlatform() - p.Folder = "test-folder" - return p - }(), - expectedError: `^test-path\.folder: Invalid value: "test-folder": folder must be absolute path: expected prefix /test-datacenter/vm/`, - }, - { - name: "Capital letters in vCenter", - platform: func() *vsphere.Platform { - p := validPlatform() - p.VCenter = "tEsT-vCenter" - return p - }(), - expectedError: `^test-path\.vCenter: Invalid value: "tEsT-vCenter": must be the domain name or IP address of the vCenter`, - }, - { - name: "URL as vCenter", - platform: func() *vsphere.Platform { - p := validPlatform() - p.VCenter = "https://test-center" - return p - }(), - expectedError: `^test-path\.vCenter: Invalid value: "https://test-center": must be the domain name or IP address of the vCenter$`, - }, { name: "Valid diskType", platform: func() *vsphere.Platform { @@ -206,34 +79,33 @@ func TestValidatePlatform(t *testing.T) { }(), expectedError: `^test-path\.diskType: Invalid value: "invalidDiskType": diskType must be one of \[eagerZeroedThick thick thin\]$`, }, + { - name: "Valid Multi-zone platform", - platform: func() *vsphere.Platform { - return validMultiVCenterPlatform() - }(), + name: "Valid Multi-zone platform", + platform: validPlatform(), }, { name: "Multi-zone platform missing failureDomains", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.FailureDomains = make([]vsphere.FailureDomain, 0) return p }(), - expectedError: `^test-path.failureDomains: Required value: must be defined if vcenters is defined`, + expectedError: `^test-path.failureDomains: Required value: must be defined`, }, { name: "Multi-zone platform vCenter missing server", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.VCenters[0].Server = "" return p }(), - expectedError: `^test-path\.vcenters\.server: Required value: must be the domain name or IP address of the vCenter`, + expectedError: `test-path\.vcenters\.server: Required value: must be the domain name or IP address of the vCenter(.*)`, }, { name: "Multi-zone platform more than one vCenter", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.VCenters = append(p.VCenters, vsphere.VCenter{ Server: "additional-vcenter", }) @@ -244,16 +116,16 @@ func TestValidatePlatform(t *testing.T) { { name: "Multi-zone platform Capital letters in vCenter", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.VCenters[0].Server = "tEsT-vCenter" return p }(), - expectedError: `^test-path\.vcenters.server: Invalid value: "tEsT-vCenter": must be the domain name or IP address of the vCenter`, + expectedError: `(.*)test-path\.vcenters.server: Invalid value: "tEsT-vCenter": must be the domain name or IP address of the vCenter`, }, { name: "Multi-zone missing username", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.VCenters[0].Username = "" return p }(), @@ -262,7 +134,7 @@ func TestValidatePlatform(t *testing.T) { { name: "Multi-zone missing password", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.VCenters[0].Password = "" return p }(), @@ -271,7 +143,7 @@ func TestValidatePlatform(t *testing.T) { { name: "Multi-zone missing datacenter", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.VCenters[0].Datacenters = []string{} return p }(), @@ -280,7 +152,7 @@ func TestValidatePlatform(t *testing.T) { { name: "Multi-zone platform wrong vCenter name in failureDomain zone", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.FailureDomains[0].Server = "bad-vcenter" return p }(), @@ -289,44 +161,17 @@ func TestValidatePlatform(t *testing.T) { { name: "Multi-zone platform failure domain topology cluster relative path", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.FailureDomains[0].Topology.ComputeCluster = "incomplete-path" + p.FailureDomains[0].Topology.ResourcePool = "/test-datacenter/host/incomplete-path/Resources/test-resourcepool" return p }(), - expectedError: `^test-path\.failureDomains\.topology\.computeCluster: Invalid value: "incomplete-path": full path of compute cluster must be provided in format //host/`, - }, - { - name: "Multi-zone platform failure domain topology compute cluster required", - platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() - p.FailureDomains[0].Topology.ComputeCluster = "" - return p - }(), - postValidationFunction: func(platform *vsphere.Platform) error { - if platform.FailureDomains[0].Topology.ComputeCluster != fmt.Sprintf("/%s/host/%s", platform.Datacenter, platform.Cluster) { - return errors.New("topology cluster not set to default") - } - return nil - }, - }, - { - name: "Multi-zone platform failure domain topology datastore required", - platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() - p.FailureDomains[0].Topology.Datastore = "" - return p - }(), - postValidationFunction: func(platform *vsphere.Platform) error { - if platform.FailureDomains[0].Topology.Datastore != platform.DefaultDatastore { - return errors.New("topology datastore not set to default") - } - return nil - }, + expectedError: `(.*)test-path\.failureDomains\.topology\.computeCluster: Invalid value: "incomplete-path": full path of compute cluster must be provided in format //host/`, }, { name: "Multi-zone platform datacenter in failure domain topology doesn't match cluster datacenter", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.FailureDomains[0].Topology.ComputeCluster = "/other-datacenter/host/cluster" return p }(), @@ -335,7 +180,7 @@ func TestValidatePlatform(t *testing.T) { { name: "Multi-zone platform failureDomain missing name", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.FailureDomains[0].Name = "" return p }(), @@ -344,7 +189,7 @@ func TestValidatePlatform(t *testing.T) { { name: "Multi-zone platform failureDomain region missing name", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.FailureDomains[0].Region = "" return p }(), @@ -353,7 +198,7 @@ func TestValidatePlatform(t *testing.T) { { name: "Multi-zone platform failureDomain zone missing name", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.FailureDomains[0].Name = "" return p }(), @@ -362,85 +207,12 @@ func TestValidatePlatform(t *testing.T) { { name: "Multi-zone platform failureDomain zone missing tag category", platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() + p := validPlatform() p.FailureDomains[0].Zone = "" return p }(), expectedError: `^test-path\.failureDomains\.zone: Required value: must specify zone tag value`, }, - { - name: "Multi-zone platform failureDomain topology missing datacenter", - platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() - p.FailureDomains[0].Topology.Datacenter = "" - return p - }(), - postValidationFunction: func(platform *vsphere.Platform) error { - if platform.FailureDomains[0].Topology.Datacenter != platform.Datacenter { - return errors.New("topology datacenter not set to default") - } - return nil - }, - }, - { - name: "Multi-zone platform failureDomain topology empty networks", - platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() - p.FailureDomains[0].Topology.Networks = []string{} - return p - }(), - postValidationFunction: func(platform *vsphere.Platform) error { - if len(platform.FailureDomains[0].Topology.Networks) != 1 || - platform.FailureDomains[0].Topology.Networks[0] != platform.Network { - return errors.New("topology networks not set to default") - } - return nil - }, - }, - { - name: "Multi-zone platform failureDomain defaults applied", - platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() - p.VCenters = []vsphere.VCenter{} - p.FailureDomains[0].Server = "" - p.FailureDomains[0].Topology = vsphere.Topology{} - p.FailureDomains[1].Server = "" - p.FailureDomains[1].Topology = vsphere.Topology{} - return p - }(), - postValidationFunction: checkIfFailureDomainDefaultsApplied, - }, - { - name: "Multi-zone platform failureDomain overrides applied", - platform: func() *vsphere.Platform { - p := validMultiVCenterPlatform() - p.VCenters = []vsphere.VCenter{} - p.FailureDomains[0].Server = "test-vcenter" - p.FailureDomains[0].Topology = vsphere.Topology{ - Datacenter: "dc1", - ComputeCluster: "/dc1/host/c1", - Networks: []string{ - "network1", - }, - Datastore: "ds1", - ResourcePool: "/dc1/host/c1/Resources/rp1", - Folder: "/dc1/vm/folder1", - } - p.FailureDomains[1].Server = "test-vcenter" - p.FailureDomains[1].Topology = vsphere.Topology{ - Datacenter: "dc2", - ComputeCluster: "/dc2/host/c2", - Networks: []string{ - "network2", - }, - Datastore: "ds2", - ResourcePool: "/dc2/host/c2/Resources/rp2", - Folder: "/dc2/vm/folder2", - } - return p - }(), - postValidationFunction: checkIfFailureDomainOverridesApplied, - }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -448,7 +220,7 @@ func TestValidatePlatform(t *testing.T) { if tc.expectedError == "" { assert.NoError(t, err) } else { - assert.Regexp(t, tc.expectedError, err) + assert.Regexp(t, regexp.MustCompile(tc.expectedError), err) } if tc.postValidationFunction != nil { err := tc.postValidationFunction(tc.platform) From 2003498d7fba24ecaeb948a4c94a044f4fe6793a Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Thu, 9 Feb 2023 13:38:37 -0500 Subject: [PATCH 7/8] vsphereprivate vendor --- terraform/providers/vsphereprivate/go.mod | 13 +- terraform/providers/vsphereprivate/go.sum | 30 ++- .../vendor/github.com/fatih/color/.travis.yml | 5 - .../vendor/github.com/fatih/color/Gopkg.lock | 27 --- .../vendor/github.com/fatih/color/Gopkg.toml | 30 --- .../vendor/github.com/fatih/color/README.md | 29 ++- .../vendor/github.com/fatih/color/color.go | 25 ++- .../vendor/github.com/fatih/color/doc.go | 2 + .../github.com/hashicorp/go-hclog/README.md | 12 +- .../hashicorp/go-hclog/colorize_unix.go | 2 + .../hashicorp/go-hclog/colorize_windows.go | 7 +- .../github.com/hashicorp/go-hclog/global.go | 2 + .../hashicorp/go-hclog/interceptlogger.go | 79 +++---- .../hashicorp/go-hclog/intlogger.go | 196 +++++++++++++----- .../github.com/hashicorp/go-hclog/logger.go | 28 +++ .../github.com/hashicorp/go-hclog/stdlog.go | 23 +- .../github.com/mattn/go-colorable/.travis.yml | 9 - .../github.com/mattn/go-colorable/README.md | 6 +- .../mattn/go-colorable/colorable_appengine.go | 9 + .../mattn/go-colorable/colorable_others.go | 12 +- .../mattn/go-colorable/colorable_windows.go | 70 +++++-- .../github.com/mattn/go-colorable/go.test.sh | 12 ++ .../mattn/go-colorable/noncolorable.go | 12 +- .../github.com/mattn/go-isatty/.travis.yml | 13 -- .../github.com/mattn/go-isatty/README.md | 2 +- .../github.com/mattn/go-isatty/go.test.sh | 12 ++ .../mattn/go-isatty/isatty_android.go | 23 -- .../github.com/mattn/go-isatty/isatty_bsd.go | 13 +- .../mattn/go-isatty/isatty_others.go | 3 +- .../mattn/go-isatty/isatty_plan9.go | 3 +- .../mattn/go-isatty/isatty_solaris.go | 9 +- .../mattn/go-isatty/isatty_tcgets.go | 4 +- .../mattn/go-isatty/isatty_windows.go | 6 +- .../go-testing-interface/.travis.yml | 1 - .../mitchellh/go-testing-interface/README.md | 8 + .../mitchellh/go-testing-interface/testing.go | 35 ++-- .../go-testing-interface/testing_go19.go | 113 ---------- .../vsphereprivate/vendor/modules.txt | 20 +- 38 files changed, 464 insertions(+), 441 deletions(-) delete mode 100644 terraform/providers/vsphereprivate/vendor/github.com/fatih/color/.travis.yml delete mode 100644 terraform/providers/vsphereprivate/vendor/github.com/fatih/color/Gopkg.lock delete mode 100644 terraform/providers/vsphereprivate/vendor/github.com/fatih/color/Gopkg.toml delete mode 100644 terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/.travis.yml create mode 100644 terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/go.test.sh delete mode 100644 terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/.travis.yml create mode 100644 terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/go.test.sh delete mode 100644 terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_android.go delete mode 100644 terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/testing_go19.go diff --git a/terraform/providers/vsphereprivate/go.mod b/terraform/providers/vsphereprivate/go.mod index cfec36e9a72..daab86823a5 100644 --- a/terraform/providers/vsphereprivate/go.mod +++ b/terraform/providers/vsphereprivate/go.mod @@ -3,7 +3,6 @@ module github.com/openshift/installer/terraform/providers/vsphereprivate go 1.18 require ( - github.com/hashicorp/go-version v1.3.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0 github.com/hashicorp/terraform-provider-vsphere v1.26.1-0.20220308165248-f78c6d6f1da6 github.com/pkg/errors v0.9.1 @@ -19,20 +18,22 @@ require ( github.com/aws/aws-sdk-go v1.37.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/color v1.7.0 // indirect + github.com/fatih/color v1.13.0 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.4.2 // indirect + github.com/google/go-cmp v0.5.8 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-getter v1.5.3 // indirect - github.com/hashicorp/go-hclog v0.15.0 // indirect + github.com/hashicorp/go-hclog v1.2.1 // indirect github.com/hashicorp/go-multierror v1.0.0 // indirect github.com/hashicorp/go-plugin v1.4.1 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-uuid v1.0.1 // indirect + github.com/hashicorp/go-version v1.3.0 // indirect github.com/hashicorp/hcl/v2 v2.8.2 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.14.0 // indirect @@ -44,11 +45,11 @@ require ( github.com/klauspost/compress v1.11.2 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-colorable v0.1.4 // indirect - github.com/mattn/go-isatty v0.0.10 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.0.4 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/terraform/providers/vsphereprivate/go.sum b/terraform/providers/vsphereprivate/go.sum index 5f0a3eb39a8..c754253dc8d 100644 --- a/terraform/providers/vsphereprivate/go.sum +++ b/terraform/providers/vsphereprivate/go.sum @@ -90,8 +90,9 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= @@ -145,8 +146,9 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs= @@ -179,8 +181,9 @@ github.com/hashicorp/go-getter v1.6.2 h1:7jX7xcB+uVCliddZgeKyNxv0xoT7qL5KDtH7rU4 github.com/hashicorp/go-getter v1.6.2/go.mod h1:IZCrswsZPeWv9IkVnLElzRU/gz/QPi6pZHn4tv6vbwA= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.15.0 h1:qMuK0wxsoW4D0ddCCYwPSTm4KQv1X1ke3WmPWZ0Mvsk= github.com/hashicorp/go-hclog v0.15.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= +github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYtXdgmf1AVNs0= @@ -251,13 +254,17 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -268,8 +275,9 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.4 h1:ZU1VNC02qyufSZsjjs7+khruk2fKvbQ3TwRV/IBCeFA= github.com/mitchellh/go-testing-interface v1.0.4/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= @@ -305,8 +313,9 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -405,6 +414,7 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -421,6 +431,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -580,8 +593,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/.travis.yml b/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/.travis.yml deleted file mode 100644 index 95f8a1ff5c7..00000000000 --- a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: go -go: - - 1.8.x - - tip - diff --git a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/Gopkg.lock b/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/Gopkg.lock deleted file mode 100644 index 7d879e9caf0..00000000000 --- a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/Gopkg.lock +++ /dev/null @@ -1,27 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/mattn/go-colorable" - packages = ["."] - revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" - version = "v0.0.9" - -[[projects]] - name = "github.com/mattn/go-isatty" - packages = ["."] - revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" - version = "v0.0.3" - -[[projects]] - branch = "master" - name = "golang.org/x/sys" - packages = ["unix"] - revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "e8a50671c3cb93ea935bf210b1cd20702876b9d9226129be581ef646d1565cdc" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/Gopkg.toml b/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/Gopkg.toml deleted file mode 100644 index ff1617f71da..00000000000 --- a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/Gopkg.toml +++ /dev/null @@ -1,30 +0,0 @@ - -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" - - -[[constraint]] - name = "github.com/mattn/go-colorable" - version = "0.0.9" - -[[constraint]] - name = "github.com/mattn/go-isatty" - version = "0.0.3" diff --git a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/README.md b/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/README.md index 3fc95446028..5152bf59bf8 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/README.md +++ b/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/README.md @@ -1,14 +1,11 @@ -# Color [![GoDoc](https://godoc.org/github.com/fatih/color?status.svg)](https://godoc.org/github.com/fatih/color) [![Build Status](https://img.shields.io/travis/fatih/color.svg?style=flat-square)](https://travis-ci.org/fatih/color) - - +# color [![](https://github.com/fatih/color/workflows/build/badge.svg)](https://github.com/fatih/color/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/fatih/color)](https://pkg.go.dev/github.com/fatih/color) Color lets you use colorized outputs in terms of [ANSI Escape Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It has support for Windows too! The API can be used in several ways, pick one that suits you. - -![Color](https://i.imgur.com/c1JI0lA.png) +![Color](https://user-images.githubusercontent.com/438920/96832689-03b3e000-13f4-11eb-9803-46f4c4de3406.jpg) ## Install @@ -17,9 +14,6 @@ suits you. go get github.com/fatih/color ``` -Note that the `vendor` folder is here for stability. Remove the folder if you -already have the dependencies in your GOPATH. - ## Examples ### Standard colors @@ -84,7 +78,7 @@ notice("Don't forget this...") ### Custom fprint functions (FprintFunc) ```go -blue := color.New(FgBlue).FprintfFunc() +blue := color.New(color.FgBlue).FprintfFunc() blue(myWriter, "important notice: %s", stars) // Mix up with multiple attributes @@ -133,14 +127,16 @@ fmt.Println("All text will now be bold magenta.") There might be a case where you want to explicitly disable/enable color output. the `go-isatty` package will automatically disable color output for non-tty output streams -(for example if the output were piped directly to `less`) +(for example if the output were piped directly to `less`). -`Color` has support to disable/enable colors both globally and for single color -definitions. For example suppose you have a CLI app and a `--no-color` bool flag. You -can easily disable the color output with: +The `color` package also disables color output if the [`NO_COLOR`](https://no-color.org) environment +variable is set (regardless of its value). -```go +`Color` has support to disable/enable colors programatically both globally and +for single color definitions. For example suppose you have a CLI app and a +`--no-color` bool flag. You can easily disable the color output with: +```go var flagNoColor = flag.Bool("no-color", false, "Disable color output") if *flagNoColor { @@ -162,6 +158,10 @@ c.EnableColor() c.Println("This prints again cyan...") ``` +## GitHub Actions + +To output color in GitHub Actions (or other CI systems that support ANSI colors), make sure to set `color.NoColor = false` so that it bypasses the check for non-tty output streams. + ## Todo * Save/Return previous values @@ -176,4 +176,3 @@ c.Println("This prints again cyan...") ## License The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details - diff --git a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/color.go b/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/color.go index 91c8e9f0620..98a60f3c88d 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/color.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/color.go @@ -15,9 +15,11 @@ import ( var ( // NoColor defines if the output is colorized or not. It's dynamically set to // false or true based on the stdout's file descriptor referring to a terminal - // or not. This is a global option and affects all colors. For more control - // over each color block use the methods DisableColor() individually. - NoColor = os.Getenv("TERM") == "dumb" || + // or not. It's also set to true if the NO_COLOR environment variable is + // set (regardless of its value). This is a global option and affects all + // colors. For more control over each color block use the methods + // DisableColor() individually. + NoColor = noColorExists() || os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) // Output defines the standard output of the print functions. By default @@ -33,6 +35,12 @@ var ( colorsCacheMu sync.Mutex // protects colorsCache ) +// noColorExists returns true if the environment variable NO_COLOR exists. +func noColorExists() bool { + _, exists := os.LookupEnv("NO_COLOR") + return exists +} + // Color defines a custom color object which is defined by SGR parameters. type Color struct { params []Attribute @@ -108,7 +116,14 @@ const ( // New returns a newly created color object. func New(value ...Attribute) *Color { - c := &Color{params: make([]Attribute, 0)} + c := &Color{ + params: make([]Attribute, 0), + } + + if noColorExists() { + c.noColor = boolPtr(true) + } + c.Add(value...) return c } @@ -387,7 +402,7 @@ func (c *Color) EnableColor() { } func (c *Color) isNoColorSet() bool { - // check first if we have user setted action + // check first if we have user set action if c.noColor != nil { return *c.noColor } diff --git a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/doc.go b/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/doc.go index cf1e96500f4..04541de786f 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/doc.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/fatih/color/doc.go @@ -118,6 +118,8 @@ the color output with: color.NoColor = true // disables colorized output } +You can also disable the color by setting the NO_COLOR environment variable to any value. + It also has support for single color definitions (local). You can disable/enable color output on the fly: diff --git a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/README.md b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/README.md index 5d56f4b59c3..21a17c5af39 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/README.md +++ b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/README.md @@ -17,11 +17,8 @@ JSON output mode for production. ## Stability Note -While this library is fully open source and HashiCorp will be maintaining it -(since we are and will be making extensive use of it), the API and output -format is subject to minor changes as we fully bake and vet it in our projects. -This notice will be removed once it's fully integrated into our major projects -and no further changes are anticipated. +This library has reached 1.0 stability. Its API can be considered solidified +and promised through future versions. ## Installation and Docs @@ -102,7 +99,7 @@ into all the callers. ### Using `hclog.Fmt()` ```go -var int totalBandwidth = 200 +totalBandwidth := 200 appLogger.Info("total bandwidth exceeded", "bandwidth", hclog.Fmt("%d GB/s", totalBandwidth)) ``` @@ -146,3 +143,6 @@ log.Printf("[DEBUG] %d", 42) Notice that if `appLogger` is initialized with the `INFO` log level _and_ you specify `InferLevels: true`, you will not see any output here. You must change `appLogger` to `DEBUG` to see output. See the docs for more information. + +If the log lines start with a timestamp you can use the +`InferLevelsWithTimestamp` option to try and ignore them. diff --git a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/colorize_unix.go b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/colorize_unix.go index 44aa9bf2c62..9635c838b4e 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/colorize_unix.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/colorize_unix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package hclog @@ -21,6 +22,7 @@ func (l *intLogger) setColorization(opts *LoggerOptions) { isCygwinTerm := isatty.IsCygwinTerminal(fi.Fd()) isTerm := isUnixTerm || isCygwinTerm if !isTerm { + l.headerColor = ColorOff l.writer.color = ColorOff } } diff --git a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/colorize_windows.go b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/colorize_windows.go index 23486b6d74f..30859168eeb 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/colorize_windows.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/colorize_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package hclog @@ -26,8 +27,12 @@ func (l *intLogger) setColorization(opts *LoggerOptions) { isTerm := isUnixTerm || isCygwinTerm if !isTerm { l.writer.color = ColorOff + l.headerColor = ColorOff return } - l.writer.w = colorable.NewColorable(fi) + + if l.headerColor == ColorOff { + l.writer.w = colorable.NewColorable(fi) + } } } diff --git a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/global.go b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/global.go index 22ebc57d877..b9f00217cae 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/global.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/global.go @@ -2,6 +2,7 @@ package hclog import ( "sync" + "time" ) var ( @@ -14,6 +15,7 @@ var ( DefaultOptions = &LoggerOptions{ Level: DefaultLevel, Output: DefaultOutput, + TimeFn: time.Now, } ) diff --git a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/interceptlogger.go b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/interceptlogger.go index d8e2e76fc38..ff42f1bfc1d 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/interceptlogger.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/interceptlogger.go @@ -18,8 +18,13 @@ type interceptLogger struct { } func NewInterceptLogger(opts *LoggerOptions) InterceptLogger { + l := newLogger(opts) + if l.callerOffset > 0 { + // extra frames for interceptLogger.{Warn,Info,Log,etc...}, and interceptLogger.log + l.callerOffset += 2 + } intercept := &interceptLogger{ - Logger: New(opts), + Logger: l, mu: new(sync.Mutex), sinkCount: new(int32), Sinks: make(map[SinkAdapter]struct{}), @@ -31,6 +36,14 @@ func NewInterceptLogger(opts *LoggerOptions) InterceptLogger { } func (i *interceptLogger) Log(level Level, msg string, args ...interface{}) { + i.log(level, msg, args...) +} + +// log is used to make the caller stack frame lookup consistent. If Warn,Info,etc +// all called Log then direct calls to Log would have a different stack frame +// depth. By having all the methods call the same helper we ensure the stack +// frame depth is the same. +func (i *interceptLogger) log(level Level, msg string, args ...interface{}) { i.Logger.Log(level, msg, args...) if atomic.LoadInt32(i.sinkCount) == 0 { return @@ -45,72 +58,27 @@ func (i *interceptLogger) Log(level Level, msg string, args ...interface{}) { // Emit the message and args at TRACE level to log and sinks func (i *interceptLogger) Trace(msg string, args ...interface{}) { - i.Logger.Trace(msg, args...) - if atomic.LoadInt32(i.sinkCount) == 0 { - return - } - - i.mu.Lock() - defer i.mu.Unlock() - for s := range i.Sinks { - s.Accept(i.Name(), Trace, msg, i.retrieveImplied(args...)...) - } + i.log(Trace, msg, args...) } // Emit the message and args at DEBUG level to log and sinks func (i *interceptLogger) Debug(msg string, args ...interface{}) { - i.Logger.Debug(msg, args...) - if atomic.LoadInt32(i.sinkCount) == 0 { - return - } - - i.mu.Lock() - defer i.mu.Unlock() - for s := range i.Sinks { - s.Accept(i.Name(), Debug, msg, i.retrieveImplied(args...)...) - } + i.log(Debug, msg, args...) } // Emit the message and args at INFO level to log and sinks func (i *interceptLogger) Info(msg string, args ...interface{}) { - i.Logger.Info(msg, args...) - if atomic.LoadInt32(i.sinkCount) == 0 { - return - } - - i.mu.Lock() - defer i.mu.Unlock() - for s := range i.Sinks { - s.Accept(i.Name(), Info, msg, i.retrieveImplied(args...)...) - } + i.log(Info, msg, args...) } // Emit the message and args at WARN level to log and sinks func (i *interceptLogger) Warn(msg string, args ...interface{}) { - i.Logger.Warn(msg, args...) - if atomic.LoadInt32(i.sinkCount) == 0 { - return - } - - i.mu.Lock() - defer i.mu.Unlock() - for s := range i.Sinks { - s.Accept(i.Name(), Warn, msg, i.retrieveImplied(args...)...) - } + i.log(Warn, msg, args...) } // Emit the message and args at ERROR level to log and sinks func (i *interceptLogger) Error(msg string, args ...interface{}) { - i.Logger.Error(msg, args...) - if atomic.LoadInt32(i.sinkCount) == 0 { - return - } - - i.mu.Lock() - defer i.mu.Unlock() - for s := range i.Sinks { - s.Accept(i.Name(), Error, msg, i.retrieveImplied(args...)...) - } + i.log(Error, msg, args...) } func (i *interceptLogger) retrieveImplied(args ...interface{}) []interface{} { @@ -123,7 +91,7 @@ func (i *interceptLogger) retrieveImplied(args ...interface{}) []interface{} { return cp } -// Create a new sub-Logger that a name decending from the current name. +// Create a new sub-Logger that a name descending from the current name. // This is used to create a subsystem specific Logger. // Registered sinks will subscribe to these messages as well. func (i *interceptLogger) Named(name string) Logger { @@ -212,9 +180,10 @@ func (i *interceptLogger) StandardWriterIntercept(opts *StandardLoggerOptions) i func (i *interceptLogger) StandardWriter(opts *StandardLoggerOptions) io.Writer { return &stdlogAdapter{ - log: i, - inferLevels: opts.InferLevels, - forceLevel: opts.ForceLevel, + log: i, + inferLevels: opts.InferLevels, + inferLevelsWithTimestamp: opts.InferLevelsWithTimestamp, + forceLevel: opts.ForceLevel, } } diff --git a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/intlogger.go b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/intlogger.go index f961ed91913..83232f7a622 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/intlogger.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/intlogger.go @@ -21,10 +21,14 @@ import ( "github.com/fatih/color" ) -// TimeFormat to use for logging. This is a version of RFC3339 that contains -// contains millisecond precision +// TimeFormat is the time format to use for plain (non-JSON) output. +// This is a version of RFC3339 that contains millisecond precision. const TimeFormat = "2006-01-02T15:04:05.000Z0700" +// TimeFormatJSON is the time format to use for JSON output. +// This is a version of RFC3339 that contains microsecond precision. +const TimeFormatJSON = "2006-01-02T15:04:05.000000Z07:00" + // errJsonUnsupportedTypeMsg is included in log json entries, if an arg cannot be serialized to json const errJsonUnsupportedTypeMsg = "logging contained values that don't serialize to json" @@ -52,10 +56,12 @@ var _ Logger = &intLogger{} // intLogger is an internal logger implementation. Internal in that it is // defined entirely by this package. type intLogger struct { - json bool - caller bool - name string - timeFormat string + json bool + callerOffset int + name string + timeFormat string + timeFn TimeFunction + disableTime bool // This is an interface so that it's shared by any derived loggers, since // those derived loggers share the bufio.Writer as well. @@ -63,6 +69,8 @@ type intLogger struct { writer *writer level *int32 + headerColor ColorOption + implied []interface{} exclude func(level Level, msg string, args ...interface{}) bool @@ -79,7 +87,12 @@ func New(opts *LoggerOptions) Logger { // NewSinkAdapter returns a SinkAdapter with configured settings // defined by LoggerOptions func NewSinkAdapter(opts *LoggerOptions) SinkAdapter { - return newLogger(opts) + l := newLogger(opts) + if l.callerOffset > 0 { + // extra frames for interceptLogger.{Warn,Info,Log,etc...}, and SinkAdapter.Accept + l.callerOffset += 2 + } + return l } func newLogger(opts *LoggerOptions) *intLogger { @@ -102,31 +115,54 @@ func newLogger(opts *LoggerOptions) *intLogger { mutex = new(sync.Mutex) } + var primaryColor, headerColor ColorOption + + if opts.ColorHeaderOnly { + primaryColor = ColorOff + headerColor = opts.Color + } else { + primaryColor = opts.Color + headerColor = ColorOff + } + l := &intLogger{ json: opts.JSONFormat, - caller: opts.IncludeLocation, name: opts.Name, timeFormat: TimeFormat, + timeFn: time.Now, + disableTime: opts.DisableTime, mutex: mutex, - writer: newWriter(output, opts.Color), + writer: newWriter(output, primaryColor), level: new(int32), exclude: opts.Exclude, independentLevels: opts.IndependentLevels, + headerColor: headerColor, + } + if opts.IncludeLocation { + l.callerOffset = offsetIntLogger + opts.AdditionalLocationOffset } - l.setColorization(opts) - - if opts.DisableTime { - l.timeFormat = "" - } else if opts.TimeFormat != "" { + if l.json { + l.timeFormat = TimeFormatJSON + } + if opts.TimeFn != nil { + l.timeFn = opts.TimeFn + } + if opts.TimeFormat != "" { l.timeFormat = opts.TimeFormat } + l.setColorization(opts) + atomic.StoreInt32(l.level, int32(level)) return l } +// offsetIntLogger is the stack frame offset in the call stack for the caller to +// one of the Warn,Info,Log,etc methods. +const offsetIntLogger = 3 + // Log a message and a set of key/value pairs if the given level is at // or more severe that the threshold configured in the Logger. func (l *intLogger) log(name string, level Level, msg string, args ...interface{}) { @@ -134,7 +170,7 @@ func (l *intLogger) log(name string, level Level, msg string, args ...interface{ return } - t := time.Now() + t := l.timeFn() l.mutex.Lock() defer l.mutex.Unlock() @@ -181,31 +217,46 @@ func trimCallerPath(path string) string { return path[idx+1:] } +// isNormal indicates if the rune is one allowed to exist as an unquoted +// string value. This is a subset of ASCII, `-` through `~`. +func isNormal(r rune) bool { + return 0x2D <= r && r <= 0x7E // - through ~ +} + +// needsQuoting returns false if all the runes in string are normal, according +// to isNormal +func needsQuoting(str string) bool { + for _, r := range str { + if !isNormal(r) { + return true + } + } + + return false +} + // Non-JSON logging format function func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string, args ...interface{}) { - if len(l.timeFormat) > 0 { + + if !l.disableTime { l.writer.WriteString(t.Format(l.timeFormat)) l.writer.WriteByte(' ') } s, ok := _levelToBracket[level] if ok { - l.writer.WriteString(s) + if l.headerColor != ColorOff { + color := _levelToColor[level] + color.Fprint(l.writer, s) + } else { + l.writer.WriteString(s) + } } else { l.writer.WriteString("[?????]") } - offset := 3 - if l.caller { - // Check if the caller is inside our package and inside - // a logger implementation file - if _, file, _, ok := runtime.Caller(3); ok { - if strings.HasSuffix(file, "intlogger.go") || strings.HasSuffix(file, "interceptlogger.go") { - offset = 4 - } - } - - if _, file, line, ok := runtime.Caller(offset); ok { + if l.callerOffset > 0 { + if _, file, line, ok := runtime.Caller(l.callerOffset); ok { l.writer.WriteByte(' ') l.writer.WriteString(trimCallerPath(file)) l.writer.WriteByte(':') @@ -251,6 +302,10 @@ func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string, switch st := args[i+1].(type) { case string: val = st + if st == "" { + val = `""` + raw = true + } case int: val = strconv.FormatInt(int64(st), 10) case int64: @@ -282,6 +337,9 @@ func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string, continue FOR case Format: val = fmt.Sprintf(st[0].(string), st[1:]...) + case Quote: + raw = true + val = strconv.Quote(string(st)) default: v := reflect.ValueOf(st) if v.Kind() == reflect.Slice { @@ -292,20 +350,30 @@ func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string, } } - l.writer.WriteByte(' ') + var key string + switch st := args[i].(type) { case string: - l.writer.WriteString(st) + key = st default: - l.writer.WriteString(fmt.Sprintf("%s", st)) + key = fmt.Sprintf("%s", st) } - l.writer.WriteByte('=') - if !raw && strings.ContainsAny(val, " \t\n\r") { - l.writer.WriteByte('"') - l.writer.WriteString(val) - l.writer.WriteByte('"') + if strings.Contains(val, "\n") { + l.writer.WriteString("\n ") + l.writer.WriteString(key) + l.writer.WriteString("=\n") + writeIndent(l.writer, val, " | ") + l.writer.WriteString(" ") + } else if !raw && needsQuoting(val) { + l.writer.WriteByte(' ') + l.writer.WriteString(key) + l.writer.WriteByte('=') + l.writer.WriteString(strconv.Quote(val)) } else { + l.writer.WriteByte(' ') + l.writer.WriteString(key) + l.writer.WriteByte('=') l.writer.WriteString(val) } } @@ -319,6 +387,25 @@ func (l *intLogger) logPlain(t time.Time, name string, level Level, msg string, } } +func writeIndent(w *writer, str string, indent string) { + for { + nl := strings.IndexByte(str, "\n"[0]) + if nl == -1 { + if str != "" { + w.WriteString(indent) + w.WriteString(str) + w.WriteString("\n") + } + return + } + + w.WriteString(indent) + w.WriteString(str[:nl]) + w.WriteString("\n") + str = str[nl+1:] + } +} + func (l *intLogger) renderSlice(v reflect.Value) string { var buf bytes.Buffer @@ -335,22 +422,19 @@ func (l *intLogger) renderSlice(v reflect.Value) string { switch sv.Kind() { case reflect.String: - val = sv.String() + val = strconv.Quote(sv.String()) case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: val = strconv.FormatInt(sv.Int(), 10) case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: val = strconv.FormatUint(sv.Uint(), 10) default: val = fmt.Sprintf("%v", sv.Interface()) + if strings.ContainsAny(val, " \t\n\r") { + val = strconv.Quote(val) + } } - if strings.ContainsAny(val, " \t\n\r") { - buf.WriteByte('"') - buf.WriteString(val) - buf.WriteByte('"') - } else { - buf.WriteString(val) - } + buf.WriteString(val) } buf.WriteRune(']') @@ -416,8 +500,10 @@ func (l *intLogger) logJSON(t time.Time, name string, level Level, msg string, a func (l intLogger) jsonMapEntry(t time.Time, name string, level Level, msg string) map[string]interface{} { vals := map[string]interface{}{ - "@message": msg, - "@timestamp": t.Format("2006-01-02T15:04:05.000000Z07:00"), + "@message": msg, + } + if !l.disableTime { + vals["@timestamp"] = t.Format(l.timeFormat) } var levelStr string @@ -442,8 +528,8 @@ func (l intLogger) jsonMapEntry(t time.Time, name string, level Level, msg strin vals["@module"] = name } - if l.caller { - if _, file, line, ok := runtime.Caller(4); ok { + if l.callerOffset > 0 { + if _, file, line, ok := runtime.Caller(l.callerOffset + 1); ok { vals["@caller"] = fmt.Sprintf("%s:%d", file, line) } } @@ -633,10 +719,18 @@ func (l *intLogger) StandardLogger(opts *StandardLoggerOptions) *log.Logger { } func (l *intLogger) StandardWriter(opts *StandardLoggerOptions) io.Writer { + newLog := *l + if l.callerOffset > 0 { + // the stack is + // logger.printf() -> l.Output() ->l.out.writer(hclog:stdlogAdaptor.write) -> hclog:stdlogAdaptor.dispatch() + // So plus 4. + newLog.callerOffset = l.callerOffset + 4 + } return &stdlogAdapter{ - log: l, - inferLevels: opts.InferLevels, - forceLevel: opts.ForceLevel, + log: &newLog, + inferLevels: opts.InferLevels, + inferLevelsWithTimestamp: opts.InferLevelsWithTimestamp, + forceLevel: opts.ForceLevel, } } diff --git a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/logger.go b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/logger.go index 83eafc152c6..85814302842 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/logger.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/logger.go @@ -5,6 +5,7 @@ import ( "log" "os" "strings" + "time" ) var ( @@ -67,6 +68,12 @@ type Octal int // text output. For example: L.Info("bits", Binary(17)) type Binary int +// A simple shortcut to format strings with Go quoting. Control and +// non-printable characters will be escaped with their backslash equivalents in +// output. Intended for untrusted or multiline strings which should be logged +// as concisely as possible. +type Quote string + // ColorOption expresses how the output should be colored, if at all. type ColorOption uint8 @@ -206,6 +213,15 @@ type StandardLoggerOptions struct { // [DEBUG] and strip it off before reapplying it. InferLevels bool + // Indicate that some minimal parsing should be done on strings to try + // and detect their level and re-emit them while ignoring possible + // timestamp values in the beginning of the string. + // This supports the strings like [ERROR], [ERR] [TRACE], [WARN], [INFO], + // [DEBUG] and strip it off before reapplying it. + // The timestamp detection may result in false positives and incomplete + // string outputs. + InferLevelsWithTimestamp bool + // ForceLevel is used to force all output from the standard logger to be at // the specified level. Similar to InferLevels, this will strip any level // prefix contained in the logged string before applying the forced level. @@ -213,6 +229,8 @@ type StandardLoggerOptions struct { ForceLevel Level } +type TimeFunction = func() time.Time + // LoggerOptions can be used to configure a new logger. type LoggerOptions struct { // Name of the subsystem to prefix logs with @@ -235,9 +253,16 @@ type LoggerOptions struct { // Include file and line information in each log line IncludeLocation bool + // AdditionalLocationOffset is the number of additional stack levels to skip + // when finding the file and line information for the log line + AdditionalLocationOffset int + // The time format to use instead of the default TimeFormat string + // A function which is called to get the time object that is formatted using `TimeFormat` + TimeFn TimeFunction + // Control whether or not to display the time at all. This is required // because setting TimeFormat to empty assumes the default format. DisableTime bool @@ -246,6 +271,9 @@ type LoggerOptions struct { // are concretely instances of *os.File. Color ColorOption + // Only color the header, not the body. This can help with readability of long messages. + ColorHeaderOnly bool + // A function which is called with the log information and if it returns true the value // should not be logged. // This is useful when interacting with a system that you wish to suppress the log diff --git a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/stdlog.go b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/stdlog.go index f35d875d327..641f20ccbcc 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/stdlog.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/hashicorp/go-hclog/stdlog.go @@ -3,16 +3,22 @@ package hclog import ( "bytes" "log" + "regexp" "strings" ) +// Regex to ignore characters commonly found in timestamp formats from the +// beginning of inputs. +var logTimestampRegexp = regexp.MustCompile(`^[\d\s\:\/\.\+-TZ]*`) + // Provides a io.Writer to shim the data out of *log.Logger // and back into our Logger. This is basically the only way to // build upon *log.Logger. type stdlogAdapter struct { - log Logger - inferLevels bool - forceLevel Level + log Logger + inferLevels bool + inferLevelsWithTimestamp bool + forceLevel Level } // Take the data, infer the levels if configured, and send it through @@ -28,6 +34,10 @@ func (s *stdlogAdapter) Write(data []byte) (int, error) { // Log at the forced level s.dispatch(str, s.forceLevel) } else if s.inferLevels { + if s.inferLevelsWithTimestamp { + str = s.trimTimestamp(str) + } + level, str := s.pickLevel(str) s.dispatch(str, level) } else { @@ -64,7 +74,7 @@ func (s *stdlogAdapter) pickLevel(str string) (Level, string) { case strings.HasPrefix(str, "[INFO]"): return Info, strings.TrimSpace(str[6:]) case strings.HasPrefix(str, "[WARN]"): - return Warn, strings.TrimSpace(str[7:]) + return Warn, strings.TrimSpace(str[6:]) case strings.HasPrefix(str, "[ERROR]"): return Error, strings.TrimSpace(str[7:]) case strings.HasPrefix(str, "[ERR]"): @@ -74,6 +84,11 @@ func (s *stdlogAdapter) pickLevel(str string) (Level, string) { } } +func (s *stdlogAdapter) trimTimestamp(str string) string { + idx := logTimestampRegexp.FindStringIndex(str) + return str[idx[1]:] +} + type logWriter struct { l *log.Logger } diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/.travis.yml b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/.travis.yml deleted file mode 100644 index 98db8f060bd..00000000000 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: go -go: - - tip - -before_install: - - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cmd/cover -script: - - $HOME/gopath/bin/goveralls -repotoken xnXqRGwgW3SXIguzxf90ZSK1GPYZPaGrw diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/README.md b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/README.md index 56729a92ca6..ca0483711c9 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/README.md +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/README.md @@ -1,8 +1,8 @@ # go-colorable -[![Godoc Reference](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable) -[![Build Status](https://travis-ci.org/mattn/go-colorable.svg?branch=master)](https://travis-ci.org/mattn/go-colorable) -[![Coverage Status](https://coveralls.io/repos/github/mattn/go-colorable/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-colorable?branch=master) +[![Build Status](https://github.com/mattn/go-colorable/workflows/test/badge.svg)](https://github.com/mattn/go-colorable/actions?query=workflow%3Atest) +[![Codecov](https://codecov.io/gh/mattn/go-colorable/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-colorable) +[![GoDoc](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable) [![Go Report Card](https://goreportcard.com/badge/mattn/go-colorable)](https://goreportcard.com/report/mattn/go-colorable) Colorable writer for windows. diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_appengine.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_appengine.go index 0b0aef83700..416d1bbbf83 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_appengine.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_appengine.go @@ -1,3 +1,4 @@ +//go:build appengine // +build appengine package colorable @@ -27,3 +28,11 @@ func NewColorableStdout() io.Writer { func NewColorableStderr() io.Writer { return os.Stderr } + +// EnableColorsStdout enable colors if possible. +func EnableColorsStdout(enabled *bool) func() { + if enabled != nil { + *enabled = true + } + return func() {} +} diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_others.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_others.go index 3fb771dcca2..766d94603ac 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_others.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_others.go @@ -1,5 +1,5 @@ -// +build !windows -// +build !appengine +//go:build !windows && !appengine +// +build !windows,!appengine package colorable @@ -28,3 +28,11 @@ func NewColorableStdout() io.Writer { func NewColorableStderr() io.Writer { return os.Stderr } + +// EnableColorsStdout enable colors if possible. +func EnableColorsStdout(enabled *bool) func() { + if enabled != nil { + *enabled = true + } + return func() {} +} diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_windows.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_windows.go index 1bd628f25c0..1846ad5ab41 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_windows.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/colorable_windows.go @@ -1,5 +1,5 @@ -// +build windows -// +build !appengine +//go:build windows && !appengine +// +build windows,!appengine package colorable @@ -10,6 +10,7 @@ import ( "os" "strconv" "strings" + "sync" "syscall" "unsafe" @@ -27,6 +28,9 @@ const ( backgroundRed = 0x40 backgroundIntensity = 0x80 backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) + commonLvbUnderscore = 0x8000 + + cENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4 ) const ( @@ -78,6 +82,8 @@ var ( procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo") procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procSetConsoleMode = kernel32.NewProc("SetConsoleMode") procCreateConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer") ) @@ -89,6 +95,7 @@ type Writer struct { oldattr word oldpos coord rest bytes.Buffer + mutex sync.Mutex } // NewColorable returns new instance of Writer which handles escape sequence from File. @@ -98,6 +105,10 @@ func NewColorable(file *os.File) io.Writer { } if isatty.IsTerminal(file.Fd()) { + var mode uint32 + if r, _, _ := procGetConsoleMode.Call(file.Fd(), uintptr(unsafe.Pointer(&mode))); r != 0 && mode&cENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 { + return file + } var csbi consoleScreenBufferInfo handle := syscall.Handle(file.Fd()) procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) @@ -424,6 +435,8 @@ func atoiWithDefault(s string, def int) (int, error) { // Write writes data on console func (w *Writer) Write(data []byte) (n int, err error) { + w.mutex.Lock() + defer w.mutex.Unlock() var csbi consoleScreenBufferInfo procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) @@ -439,18 +452,22 @@ func (w *Writer) Write(data []byte) (n int, err error) { } else { er = bytes.NewReader(data) } - var bw [1]byte + var plaintext bytes.Buffer loop: for { c1, err := er.ReadByte() if err != nil { + plaintext.WriteTo(w.out) break loop } if c1 != 0x1b { - bw[0] = c1 - w.out.Write(bw[:]) + plaintext.WriteByte(c1) continue } + _, err = plaintext.WriteTo(w.out) + if err != nil { + break loop + } c2, err := er.ReadByte() if err != nil { break loop @@ -675,14 +692,19 @@ loop: switch { case n == 0 || n == 100: attr = w.oldattr - case 1 <= n && n <= 5: - attr |= foregroundIntensity - case n == 7: - attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4) - case n == 22 || n == 25: + case n == 4: + attr |= commonLvbUnderscore + case (1 <= n && n <= 3) || n == 5: attr |= foregroundIntensity - case n == 27: - attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4) + case n == 7 || n == 27: + attr = + (attr &^ (foregroundMask | backgroundMask)) | + ((attr & foregroundMask) << 4) | + ((attr & backgroundMask) >> 4) + case n == 22: + attr &^= foregroundIntensity + case n == 24: + attr &^= commonLvbUnderscore case 30 <= n && n <= 37: attr &= backgroundMask if (n-30)&1 != 0 { @@ -701,7 +723,7 @@ loop: n256setup() } attr &= backgroundMask - attr |= n256foreAttr[n256] + attr |= n256foreAttr[n256%len(n256foreAttr)] i += 2 } } else if len(token) == 5 && token[i+1] == "2" { @@ -743,7 +765,7 @@ loop: n256setup() } attr &= foregroundMask - attr |= n256backAttr[n256] + attr |= n256backAttr[n256%len(n256backAttr)] i += 2 } } else if len(token) == 5 && token[i+1] == "2" { @@ -1003,3 +1025,23 @@ func n256setup() { n256backAttr[i] = c.backgroundAttr() } } + +// EnableColorsStdout enable colors if possible. +func EnableColorsStdout(enabled *bool) func() { + var mode uint32 + h := os.Stdout.Fd() + if r, _, _ := procGetConsoleMode.Call(h, uintptr(unsafe.Pointer(&mode))); r != 0 { + if r, _, _ = procSetConsoleMode.Call(h, uintptr(mode|cENABLE_VIRTUAL_TERMINAL_PROCESSING)); r != 0 { + if enabled != nil { + *enabled = true + } + return func() { + procSetConsoleMode.Call(h, uintptr(mode)) + } + } + } + if enabled != nil { + *enabled = true + } + return func() {} +} diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/go.test.sh b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/go.test.sh new file mode 100644 index 00000000000..012162b077c --- /dev/null +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/go.test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v vendor); do + go test -race -coverprofile=profile.out -covermode=atomic "$d" + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/noncolorable.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/noncolorable.go index 95f2c6be257..05d6f74bf6b 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/noncolorable.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-colorable/noncolorable.go @@ -18,18 +18,22 @@ func NewNonColorable(w io.Writer) io.Writer { // Write writes data on console func (w *NonColorable) Write(data []byte) (n int, err error) { er := bytes.NewReader(data) - var bw [1]byte + var plaintext bytes.Buffer loop: for { c1, err := er.ReadByte() if err != nil { + plaintext.WriteTo(w.out) break loop } if c1 != 0x1b { - bw[0] = c1 - w.out.Write(bw[:]) + plaintext.WriteByte(c1) continue } + _, err = plaintext.WriteTo(w.out) + if err != nil { + break loop + } c2, err := er.ReadByte() if err != nil { break loop @@ -38,7 +42,6 @@ loop: continue } - var buf bytes.Buffer for { c, err := er.ReadByte() if err != nil { @@ -47,7 +50,6 @@ loop: if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { break } - buf.Write([]byte(string(c))) } } diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/.travis.yml b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/.travis.yml deleted file mode 100644 index 5597e026ddf..00000000000 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: go -go: - - tip - -os: - - linux - - osx - -before_install: - - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cmd/cover -script: - - $HOME/gopath/bin/goveralls -repotoken 3gHdORO5k5ziZcWMBxnd9LrMZaJs8m9x5 diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/README.md b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/README.md index 1e69004bb03..38418353e31 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/README.md +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/README.md @@ -1,7 +1,7 @@ # go-isatty [![Godoc Reference](https://godoc.org/github.com/mattn/go-isatty?status.svg)](http://godoc.org/github.com/mattn/go-isatty) -[![Build Status](https://travis-ci.org/mattn/go-isatty.svg?branch=master)](https://travis-ci.org/mattn/go-isatty) +[![Codecov](https://codecov.io/gh/mattn/go-isatty/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-isatty) [![Coverage Status](https://coveralls.io/repos/github/mattn/go-isatty/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-isatty?branch=master) [![Go Report Card](https://goreportcard.com/badge/mattn/go-isatty)](https://goreportcard.com/report/mattn/go-isatty) diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/go.test.sh b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/go.test.sh new file mode 100644 index 00000000000..012162b077c --- /dev/null +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/go.test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v vendor); do + go test -race -coverprofile=profile.out -covermode=atomic "$d" + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_android.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_android.go deleted file mode 100644 index d3567cb5bf2..00000000000 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_android.go +++ /dev/null @@ -1,23 +0,0 @@ -// +build android - -package isatty - -import ( - "syscall" - "unsafe" -) - -const ioctlReadTermios = syscall.TCGETS - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} - -// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 -// terminal. This is also always false on this environment. -func IsCygwinTerminal(fd uintptr) bool { - return false -} diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_bsd.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_bsd.go index 07e93039dbe..39bbcf00f0c 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_bsd.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_bsd.go @@ -1,20 +1,15 @@ +//go:build (darwin || freebsd || openbsd || netbsd || dragonfly) && !appengine // +build darwin freebsd openbsd netbsd dragonfly // +build !appengine package isatty -import ( - "syscall" - "unsafe" -) - -const ioctlReadTermios = syscall.TIOCGETA +import "golang.org/x/sys/unix" // IsTerminal return true if the file descriptor is terminal. func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 + _, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA) + return err == nil } // IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_others.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_others.go index ff714a37615..31503226f6c 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_others.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_others.go @@ -1,4 +1,5 @@ -// +build appengine js nacl +//go:build appengine || js || nacl || wasm +// +build appengine js nacl wasm package isatty diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_plan9.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_plan9.go index bc0a70920f4..bae7f9bb3dc 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_plan9.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_plan9.go @@ -1,3 +1,4 @@ +//go:build plan9 // +build plan9 package isatty @@ -8,7 +9,7 @@ import ( // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd uintptr) bool { - path, err := syscall.Fd2path(fd) + path, err := syscall.Fd2path(int(fd)) if err != nil { return false } diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_solaris.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_solaris.go index bdd5c79a07f..0c3acf2dc28 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_solaris.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_solaris.go @@ -1,5 +1,5 @@ -// +build solaris -// +build !appengine +//go:build solaris && !appengine +// +build solaris,!appengine package isatty @@ -8,10 +8,9 @@ import ( ) // IsTerminal returns true if the given file descriptor is a terminal. -// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c +// see: https://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/isatty.c func IsTerminal(fd uintptr) bool { - var termio unix.Termio - err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio) + _, err := unix.IoctlGetTermio(int(fd), unix.TCGETA) return err == nil } diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_tcgets.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_tcgets.go index 453b025d0df..67787657fb2 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_tcgets.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_tcgets.go @@ -1,6 +1,6 @@ -// +build linux aix +//go:build (linux || aix || zos) && !appengine +// +build linux aix zos // +build !appengine -// +build !android package isatty diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_windows.go b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_windows.go index 1fa86915405..8e3c99171bf 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_windows.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mattn/go-isatty/isatty_windows.go @@ -1,5 +1,5 @@ -// +build windows -// +build !appengine +//go:build windows && !appengine +// +build windows,!appengine package isatty @@ -76,7 +76,7 @@ func isCygwinPipeName(name string) bool { } // getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler -// since GetFileInformationByHandleEx is not avilable under windows Vista and still some old fashion +// since GetFileInformationByHandleEx is not available under windows Vista and still some old fashion // guys are using Windows XP, this is a workaround for those guys, it will also work on system from // Windows vista to 10 // see https://stackoverflow.com/a/18792477 for details diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/.travis.yml b/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/.travis.yml index 928d000ec49..cca949103af 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/.travis.yml +++ b/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/.travis.yml @@ -1,7 +1,6 @@ language: go go: - - 1.8 - 1.x - tip diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/README.md b/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/README.md index 26781bbae88..ee435adc54d 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/README.md +++ b/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/README.md @@ -38,6 +38,14 @@ You can also call the test helper at runtime if needed: TestHelper(&testing.RuntimeT{}) } +## Versioning + +The tagged version matches the version of Go that the interface is +compatible with. For example, the version "1.14.0" is for Go 1.14 and +introduced the `Cleanup` function. The patch version (the ".0" in the +prior example) is used to fix any bugs found in this library and has no +correlation to the supported Go version. + ## Why?! **Why would I call a test helper that takes a *testing.T at runtime?** diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/testing.go b/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/testing.go index b05a49a6907..86510322abf 100644 --- a/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/testing.go +++ b/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/testing.go @@ -1,5 +1,3 @@ -// +build !go1.9 - package testing import ( @@ -12,6 +10,7 @@ import ( // In unit tests you can just pass a *testing.T struct. At runtime, outside // of tests, you can pass in a RuntimeT struct from this package. type T interface { + Cleanup(func()) Error(args ...interface{}) Errorf(format string, args ...interface{}) Fail() @@ -19,6 +18,7 @@ type T interface { Failed() bool Fatal(args ...interface{}) Fatalf(format string, args ...interface{}) + Helper() Log(args ...interface{}) Logf(format string, args ...interface{}) Name() string @@ -34,10 +34,13 @@ type T interface { // for calls to Fatal. For calls to Error, you'll have to check the errors // list to determine whether to exit yourself. // +// Cleanup does NOT work, so if you're using a helper that uses Cleanup, +// there may be dangling resources. +// // Parallel does not do anything. type RuntimeT struct { - failed bool skipped bool + failed bool } func (t *RuntimeT) Error(args ...interface{}) { @@ -46,20 +49,10 @@ func (t *RuntimeT) Error(args ...interface{}) { } func (t *RuntimeT) Errorf(format string, args ...interface{}) { - log.Println(fmt.Sprintf(format, args...)) + log.Printf(format, args...) t.Fail() } -func (t *RuntimeT) Fatal(args ...interface{}) { - log.Println(fmt.Sprintln(args...)) - t.FailNow() -} - -func (t *RuntimeT) Fatalf(format string, args ...interface{}) { - log.Println(fmt.Sprintf(format, args...)) - t.FailNow() -} - func (t *RuntimeT) Fail() { t.failed = true } @@ -72,6 +65,16 @@ func (t *RuntimeT) Failed() bool { return t.failed } +func (t *RuntimeT) Fatal(args ...interface{}) { + log.Print(args...) + t.FailNow() +} + +func (t *RuntimeT) Fatalf(format string, args ...interface{}) { + log.Printf(format, args...) + t.FailNow() +} + func (t *RuntimeT) Log(args ...interface{}) { log.Println(fmt.Sprintln(args...)) } @@ -103,3 +106,7 @@ func (t *RuntimeT) Skipf(format string, args ...interface{}) { func (t *RuntimeT) Skipped() bool { return t.skipped } + +func (t *RuntimeT) Helper() {} + +func (t *RuntimeT) Cleanup(func()) {} diff --git a/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/testing_go19.go b/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/testing_go19.go deleted file mode 100644 index f09c066a424..00000000000 --- a/terraform/providers/vsphereprivate/vendor/github.com/mitchellh/go-testing-interface/testing_go19.go +++ /dev/null @@ -1,113 +0,0 @@ -// +build go1.9 - -// NOTE: This is a temporary copy of testing.go for Go 1.9 with the addition -// of "Helper" to the T interface. Go 1.9 at the time of typing is in RC -// and is set for release shortly. We'll support this on master as the default -// as soon as 1.9 is released. - -package testing - -import ( - "fmt" - "log" -) - -// T is the interface that mimics the standard library *testing.T. -// -// In unit tests you can just pass a *testing.T struct. At runtime, outside -// of tests, you can pass in a RuntimeT struct from this package. -type T interface { - Error(args ...interface{}) - Errorf(format string, args ...interface{}) - Fail() - FailNow() - Failed() bool - Fatal(args ...interface{}) - Fatalf(format string, args ...interface{}) - Log(args ...interface{}) - Logf(format string, args ...interface{}) - Name() string - Parallel() - Skip(args ...interface{}) - SkipNow() - Skipf(format string, args ...interface{}) - Skipped() bool - Helper() -} - -// RuntimeT implements T and can be instantiated and run at runtime to -// mimic *testing.T behavior. Unlike *testing.T, this will simply panic -// for calls to Fatal. For calls to Error, you'll have to check the errors -// list to determine whether to exit yourself. -// -// Parallel does not do anything. -type RuntimeT struct { - failed bool - skipped bool -} - -func (t *RuntimeT) Error(args ...interface{}) { - log.Println(fmt.Sprintln(args...)) - t.Fail() -} - -func (t *RuntimeT) Errorf(format string, args ...interface{}) { - log.Printf(format, args...) - t.Fail() -} - -func (t *RuntimeT) Fail() { - t.failed = true -} - -func (t *RuntimeT) FailNow() { - panic("testing.T failed, see logs for output (if any)") -} - -func (t *RuntimeT) Failed() bool { - return t.failed -} - -func (t *RuntimeT) Fatal(args ...interface{}) { - log.Print(args...) - t.FailNow() -} - -func (t *RuntimeT) Fatalf(format string, args ...interface{}) { - log.Printf(format, args...) - t.FailNow() -} - -func (t *RuntimeT) Log(args ...interface{}) { - log.Println(fmt.Sprintln(args...)) -} - -func (t *RuntimeT) Logf(format string, args ...interface{}) { - log.Println(fmt.Sprintf(format, args...)) -} - -func (t *RuntimeT) Name() string { - return "" -} - -func (t *RuntimeT) Parallel() {} - -func (t *RuntimeT) Skip(args ...interface{}) { - log.Print(args...) - t.SkipNow() -} - -func (t *RuntimeT) SkipNow() { - t.skipped = true -} - -func (t *RuntimeT) Skipf(format string, args ...interface{}) { - log.Printf(format, args...) - t.SkipNow() -} - -func (t *RuntimeT) Skipped() bool { - return t.skipped -} - -func (t *RuntimeT) Helper() {} diff --git a/terraform/providers/vsphereprivate/vendor/modules.txt b/terraform/providers/vsphereprivate/vendor/modules.txt index 1d8d3f923ef..9434409bbfb 100644 --- a/terraform/providers/vsphereprivate/vendor/modules.txt +++ b/terraform/providers/vsphereprivate/vendor/modules.txt @@ -76,8 +76,8 @@ github.com/bgentry/go-netrc/netrc # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew -# github.com/fatih/color v1.7.0 -## explicit +# github.com/fatih/color v1.13.0 +## explicit; go 1.13 github.com/fatih/color # github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e ## explicit @@ -93,6 +93,8 @@ github.com/golang/protobuf/ptypes/any github.com/golang/protobuf/ptypes/duration github.com/golang/protobuf/ptypes/empty github.com/golang/protobuf/ptypes/timestamp +# github.com/google/go-cmp v0.5.8 +## explicit; go 1.13 # github.com/googleapis/gax-go/v2 v2.0.5 ## explicit github.com/googleapis/gax-go/v2 @@ -117,7 +119,7 @@ github.com/hashicorp/go-cty/cty/set ## explicit; go 1.13 github.com/hashicorp/go-getter github.com/hashicorp/go-getter/helper/url -# github.com/hashicorp/go-hclog v0.15.0 +# github.com/hashicorp/go-hclog v1.2.1 ## explicit; go 1.13 github.com/hashicorp/go-hclog # github.com/hashicorp/go-multierror v1.0.0 @@ -238,11 +240,11 @@ github.com/kr/pretty # github.com/kr/text v0.2.0 ## explicit github.com/kr/text -# github.com/mattn/go-colorable v0.1.4 -## explicit +# github.com/mattn/go-colorable v0.1.12 +## explicit; go 1.13 github.com/mattn/go-colorable -# github.com/mattn/go-isatty v0.0.10 -## explicit; go 1.14 +# github.com/mattn/go-isatty v0.0.14 +## explicit; go 1.12 github.com/mattn/go-isatty # github.com/mitchellh/copystructure v1.2.0 ## explicit; go 1.15 @@ -250,8 +252,8 @@ github.com/mitchellh/copystructure # github.com/mitchellh/go-homedir v1.1.0 ## explicit github.com/mitchellh/go-homedir -# github.com/mitchellh/go-testing-interface v1.0.4 -## explicit +# github.com/mitchellh/go-testing-interface v1.14.1 +## explicit; go 1.14 github.com/mitchellh/go-testing-interface # github.com/mitchellh/go-wordwrap v1.0.0 ## explicit From a06dfe9a63a1cfaeabf67102d763bae7559a54af Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Thu, 9 Feb 2023 13:40:49 -0500 Subject: [PATCH 8/8] installer vendor --- go.mod | 3 +- go.sum | 5 +- vendor/github.com/onsi/gomega/.gitignore | 7 + vendor/github.com/onsi/gomega/CHANGELOG.md | 520 + vendor/github.com/onsi/gomega/CONTRIBUTING.md | 14 + vendor/github.com/onsi/gomega/LICENSE | 20 + vendor/github.com/onsi/gomega/README.md | 21 + vendor/github.com/onsi/gomega/RELEASING.md | 23 + .../github.com/onsi/gomega/format/format.go | 502 + vendor/github.com/onsi/gomega/gomega_dsl.go | 537 + .../onsi/gomega/internal/assertion.go | 161 + .../onsi/gomega/internal/async_assertion.go | 563 + .../onsi/gomega/internal/duration_bundle.go | 71 + .../github.com/onsi/gomega/internal/gomega.go | 129 + .../onsi/gomega/internal/gutil/post_ioutil.go | 48 + .../gomega/internal/gutil/using_ioutil.go | 47 + .../gomega/internal/polling_signal_error.go | 106 + .../onsi/gomega/internal/vetoptdesc.go | 22 + vendor/github.com/onsi/gomega/matchers.go | 641 + vendor/github.com/onsi/gomega/matchers/and.go | 62 + .../matchers/assignable_to_type_of_matcher.go | 37 + .../onsi/gomega/matchers/attributes_slice.go | 14 + .../onsi/gomega/matchers/be_a_directory.go | 56 + .../onsi/gomega/matchers/be_a_regular_file.go | 56 + .../gomega/matchers/be_an_existing_file.go | 40 + .../onsi/gomega/matchers/be_closed_matcher.go | 48 + .../matchers/be_comparable_to_matcher.go | 49 + .../gomega/matchers/be_element_of_matcher.go | 43 + .../onsi/gomega/matchers/be_empty_matcher.go | 29 + .../matchers/be_equivalent_to_matcher.go | 36 + .../onsi/gomega/matchers/be_false_matcher.go | 28 + .../onsi/gomega/matchers/be_identical_to.go | 39 + .../onsi/gomega/matchers/be_key_of_matcher.go | 45 + .../onsi/gomega/matchers/be_nil_matcher.go | 20 + .../gomega/matchers/be_numerically_matcher.go | 134 + .../onsi/gomega/matchers/be_sent_matcher.go | 73 + .../gomega/matchers/be_temporally_matcher.go | 68 + .../onsi/gomega/matchers/be_true_matcher.go | 28 + .../onsi/gomega/matchers/be_zero_matcher.go | 28 + .../onsi/gomega/matchers/consist_of.go | 144 + .../matchers/contain_element_matcher.go | 174 + .../matchers/contain_elements_matcher.go | 44 + .../matchers/contain_substring_matcher.go | 40 + .../onsi/gomega/matchers/equal_matcher.go | 42 + .../onsi/gomega/matchers/have_cap_matcher.go | 30 + .../onsi/gomega/matchers/have_each_matcher.go | 65 + .../matchers/have_existing_field_matcher.go | 36 + .../onsi/gomega/matchers/have_field.go | 99 + .../gomega/matchers/have_http_body_matcher.go | 101 + .../have_http_header_with_value_matcher.go | 81 + .../matchers/have_http_status_matcher.go | 96 + .../onsi/gomega/matchers/have_key_matcher.go | 56 + .../matchers/have_key_with_value_matcher.go | 76 + .../onsi/gomega/matchers/have_len_matcher.go | 28 + .../gomega/matchers/have_occurred_matcher.go | 35 + .../gomega/matchers/have_prefix_matcher.go | 36 + .../gomega/matchers/have_suffix_matcher.go | 36 + .../onsi/gomega/matchers/have_value.go | 54 + .../gomega/matchers/match_error_matcher.go | 65 + .../gomega/matchers/match_json_matcher.go | 65 + .../gomega/matchers/match_regexp_matcher.go | 43 + .../onsi/gomega/matchers/match_xml_matcher.go | 134 + .../gomega/matchers/match_yaml_matcher.go | 76 + vendor/github.com/onsi/gomega/matchers/not.go | 29 + vendor/github.com/onsi/gomega/matchers/or.go | 66 + .../onsi/gomega/matchers/panic_matcher.go | 114 + .../onsi/gomega/matchers/receive_matcher.go | 130 + .../onsi/gomega/matchers/satisfy_matcher.go | 66 + .../matchers/semi_structured_data_support.go | 94 + .../onsi/gomega/matchers/succeed_matcher.go | 42 + .../goraph/bipartitegraph/bipartitegraph.go | 56 + .../bipartitegraph/bipartitegraphmatching.go | 164 + .../matchers/support/goraph/edge/edge.go | 61 + .../matchers/support/goraph/node/node.go | 8 + .../matchers/support/goraph/util/util.go | 7 + .../onsi/gomega/matchers/type_support.go | 182 + .../onsi/gomega/matchers/with_transform.go | 90 + vendor/github.com/onsi/gomega/tools | 8 + vendor/github.com/onsi/gomega/types/types.go | 93 + vendor/golang.org/x/net/html/atom/atom.go | 78 + vendor/golang.org/x/net/html/atom/table.go | 783 + .../golang.org/x/net/html/charset/charset.go | 257 + vendor/golang.org/x/net/html/const.go | 111 + vendor/golang.org/x/net/html/doc.go | 106 + vendor/golang.org/x/net/html/doctype.go | 156 + vendor/golang.org/x/net/html/entity.go | 2253 + vendor/golang.org/x/net/html/escape.go | 258 + vendor/golang.org/x/net/html/foreign.go | 222 + vendor/golang.org/x/net/html/node.go | 225 + vendor/golang.org/x/net/html/parse.go | 2460 + vendor/golang.org/x/net/html/render.go | 273 + vendor/golang.org/x/net/html/token.go | 1228 + .../x/text/encoding/htmlindex/htmlindex.go | 86 + .../x/text/encoding/htmlindex/map.go | 105 + .../x/text/encoding/htmlindex/tables.go | 353 + .../x/text/encoding/japanese/all.go | 12 + .../x/text/encoding/japanese/eucjp.go | 225 + .../x/text/encoding/japanese/iso2022jp.go | 299 + .../x/text/encoding/japanese/shiftjis.go | 189 + .../x/text/encoding/japanese/tables.go | 26971 ++++++++++ .../x/text/encoding/korean/euckr.go | 177 + .../x/text/encoding/korean/tables.go | 34152 ++++++++++++ .../x/text/encoding/simplifiedchinese/all.go | 12 + .../x/text/encoding/simplifiedchinese/gbk.go | 269 + .../encoding/simplifiedchinese/hzgb2312.go | 245 + .../text/encoding/simplifiedchinese/tables.go | 43999 ++++++++++++++++ .../text/encoding/traditionalchinese/big5.go | 199 + .../encoding/traditionalchinese/tables.go | 37142 +++++++++++++ .../x/text/encoding/unicode/override.go | 82 + .../x/text/encoding/unicode/unicode.go | 512 + .../x/text/internal/language/common.go | 16 + .../x/text/internal/language/compact.go | 29 + .../text/internal/language/compact/compact.go | 61 + .../internal/language/compact/language.go | 260 + .../text/internal/language/compact/parents.go | 120 + .../text/internal/language/compact/tables.go | 1015 + .../x/text/internal/language/compact/tags.go | 91 + .../x/text/internal/language/compose.go | 167 + .../x/text/internal/language/coverage.go | 28 + .../x/text/internal/language/language.go | 627 + .../x/text/internal/language/lookup.go | 412 + .../x/text/internal/language/match.go | 226 + .../x/text/internal/language/parse.go | 604 + .../x/text/internal/language/tables.go | 3464 ++ .../x/text/internal/language/tags.go | 48 + vendor/golang.org/x/text/internal/tag/tag.go | 100 + .../internal/utf8internal/utf8internal.go | 87 + vendor/golang.org/x/text/language/coverage.go | 187 + vendor/golang.org/x/text/language/doc.go | 102 + vendor/golang.org/x/text/language/go1_1.go | 39 + vendor/golang.org/x/text/language/go1_2.go | 12 + vendor/golang.org/x/text/language/language.go | 605 + vendor/golang.org/x/text/language/match.go | 735 + vendor/golang.org/x/text/language/parse.go | 250 + vendor/golang.org/x/text/language/tables.go | 298 + vendor/golang.org/x/text/language/tags.go | 145 + vendor/golang.org/x/text/runes/cond.go | 187 + vendor/golang.org/x/text/runes/runes.go | 355 + vendor/modules.txt | 27 + 139 files changed, 170972 insertions(+), 3 deletions(-) create mode 100644 vendor/github.com/onsi/gomega/.gitignore create mode 100644 vendor/github.com/onsi/gomega/CHANGELOG.md create mode 100644 vendor/github.com/onsi/gomega/CONTRIBUTING.md create mode 100644 vendor/github.com/onsi/gomega/LICENSE create mode 100644 vendor/github.com/onsi/gomega/README.md create mode 100644 vendor/github.com/onsi/gomega/RELEASING.md create mode 100644 vendor/github.com/onsi/gomega/format/format.go create mode 100644 vendor/github.com/onsi/gomega/gomega_dsl.go create mode 100644 vendor/github.com/onsi/gomega/internal/assertion.go create mode 100644 vendor/github.com/onsi/gomega/internal/async_assertion.go create mode 100644 vendor/github.com/onsi/gomega/internal/duration_bundle.go create mode 100644 vendor/github.com/onsi/gomega/internal/gomega.go create mode 100644 vendor/github.com/onsi/gomega/internal/gutil/post_ioutil.go create mode 100644 vendor/github.com/onsi/gomega/internal/gutil/using_ioutil.go create mode 100644 vendor/github.com/onsi/gomega/internal/polling_signal_error.go create mode 100644 vendor/github.com/onsi/gomega/internal/vetoptdesc.go create mode 100644 vendor/github.com/onsi/gomega/matchers.go create mode 100644 vendor/github.com/onsi/gomega/matchers/and.go create mode 100644 vendor/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/attributes_slice.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_a_directory.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_a_regular_file.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_an_existing_file.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_closed_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_comparable_to_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_element_of_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_empty_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_equivalent_to_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_false_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_identical_to.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_key_of_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_nil_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_numerically_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_sent_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_temporally_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_true_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/be_zero_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/consist_of.go create mode 100644 vendor/github.com/onsi/gomega/matchers/contain_element_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/contain_elements_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/contain_substring_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/equal_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_cap_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_each_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_existing_field_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_field.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_http_body_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_http_header_with_value_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_http_status_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_key_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_len_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_occurred_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_prefix_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_suffix_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/have_value.go create mode 100644 vendor/github.com/onsi/gomega/matchers/match_error_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/match_json_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/match_regexp_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/match_xml_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/not.go create mode 100644 vendor/github.com/onsi/gomega/matchers/or.go create mode 100644 vendor/github.com/onsi/gomega/matchers/panic_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/receive_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/satisfy_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/semi_structured_data_support.go create mode 100644 vendor/github.com/onsi/gomega/matchers/succeed_matcher.go create mode 100644 vendor/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraph.go create mode 100644 vendor/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraphmatching.go create mode 100644 vendor/github.com/onsi/gomega/matchers/support/goraph/edge/edge.go create mode 100644 vendor/github.com/onsi/gomega/matchers/support/goraph/node/node.go create mode 100644 vendor/github.com/onsi/gomega/matchers/support/goraph/util/util.go create mode 100644 vendor/github.com/onsi/gomega/matchers/type_support.go create mode 100644 vendor/github.com/onsi/gomega/matchers/with_transform.go create mode 100644 vendor/github.com/onsi/gomega/tools create mode 100644 vendor/github.com/onsi/gomega/types/types.go create mode 100644 vendor/golang.org/x/net/html/atom/atom.go create mode 100644 vendor/golang.org/x/net/html/atom/table.go create mode 100644 vendor/golang.org/x/net/html/charset/charset.go create mode 100644 vendor/golang.org/x/net/html/const.go create mode 100644 vendor/golang.org/x/net/html/doc.go create mode 100644 vendor/golang.org/x/net/html/doctype.go create mode 100644 vendor/golang.org/x/net/html/entity.go create mode 100644 vendor/golang.org/x/net/html/escape.go create mode 100644 vendor/golang.org/x/net/html/foreign.go create mode 100644 vendor/golang.org/x/net/html/node.go create mode 100644 vendor/golang.org/x/net/html/parse.go create mode 100644 vendor/golang.org/x/net/html/render.go create mode 100644 vendor/golang.org/x/net/html/token.go create mode 100644 vendor/golang.org/x/text/encoding/htmlindex/htmlindex.go create mode 100644 vendor/golang.org/x/text/encoding/htmlindex/map.go create mode 100644 vendor/golang.org/x/text/encoding/htmlindex/tables.go create mode 100644 vendor/golang.org/x/text/encoding/japanese/all.go create mode 100644 vendor/golang.org/x/text/encoding/japanese/eucjp.go create mode 100644 vendor/golang.org/x/text/encoding/japanese/iso2022jp.go create mode 100644 vendor/golang.org/x/text/encoding/japanese/shiftjis.go create mode 100644 vendor/golang.org/x/text/encoding/japanese/tables.go create mode 100644 vendor/golang.org/x/text/encoding/korean/euckr.go create mode 100644 vendor/golang.org/x/text/encoding/korean/tables.go create mode 100644 vendor/golang.org/x/text/encoding/simplifiedchinese/all.go create mode 100644 vendor/golang.org/x/text/encoding/simplifiedchinese/gbk.go create mode 100644 vendor/golang.org/x/text/encoding/simplifiedchinese/hzgb2312.go create mode 100644 vendor/golang.org/x/text/encoding/simplifiedchinese/tables.go create mode 100644 vendor/golang.org/x/text/encoding/traditionalchinese/big5.go create mode 100644 vendor/golang.org/x/text/encoding/traditionalchinese/tables.go create mode 100644 vendor/golang.org/x/text/encoding/unicode/override.go create mode 100644 vendor/golang.org/x/text/encoding/unicode/unicode.go create mode 100644 vendor/golang.org/x/text/internal/language/common.go create mode 100644 vendor/golang.org/x/text/internal/language/compact.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/compact.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/language.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/parents.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/tables.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/tags.go create mode 100644 vendor/golang.org/x/text/internal/language/compose.go create mode 100644 vendor/golang.org/x/text/internal/language/coverage.go create mode 100644 vendor/golang.org/x/text/internal/language/language.go create mode 100644 vendor/golang.org/x/text/internal/language/lookup.go create mode 100644 vendor/golang.org/x/text/internal/language/match.go create mode 100644 vendor/golang.org/x/text/internal/language/parse.go create mode 100644 vendor/golang.org/x/text/internal/language/tables.go create mode 100644 vendor/golang.org/x/text/internal/language/tags.go create mode 100644 vendor/golang.org/x/text/internal/tag/tag.go create mode 100644 vendor/golang.org/x/text/internal/utf8internal/utf8internal.go create mode 100644 vendor/golang.org/x/text/language/coverage.go create mode 100644 vendor/golang.org/x/text/language/doc.go create mode 100644 vendor/golang.org/x/text/language/go1_1.go create mode 100644 vendor/golang.org/x/text/language/go1_2.go create mode 100644 vendor/golang.org/x/text/language/language.go create mode 100644 vendor/golang.org/x/text/language/match.go create mode 100644 vendor/golang.org/x/text/language/parse.go create mode 100644 vendor/golang.org/x/text/language/tables.go create mode 100644 vendor/golang.org/x/text/language/tags.go create mode 100644 vendor/golang.org/x/text/runes/cond.go create mode 100644 vendor/golang.org/x/text/runes/runes.go diff --git a/go.mod b/go.mod index 26e8ba2a834..082d0cce6d0 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/gophercloud/gophercloud v1.1.1 github.com/gophercloud/utils v0.0.0-20221207145018-e8fba78967ca github.com/h2non/filetype v1.0.12 + github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/terraform-exec v0.17.3 github.com/jongio/azidext/go/azidext v0.4.0 github.com/kdomanski/iso9660 v0.2.1 @@ -54,6 +55,7 @@ require ( github.com/microsoft/kiota-authentication-azure-go v0.5.0 github.com/microsoftgraph/msgraph-sdk-go v0.47.0 github.com/nutanix-cloud-native/prism-go-client v0.2.1-0.20220804130801-c8a253627c64 + github.com/onsi/gomega v1.26.0 github.com/openshift/api v0.0.0-20230201213816-61d971884921 github.com/openshift/assisted-image-service v0.0.0-20220506122314-2f689a1084b8 github.com/openshift/assisted-service v0.0.0-20220928142635-a40422bdea61 @@ -183,7 +185,6 @@ require ( github.com/hashicorp/go-hclog v1.2.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/terraform-json v0.14.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/imdario/mergo v0.3.13 // indirect diff --git a/go.sum b/go.sum index 25f875c5e9a..9736213237c 100644 --- a/go.sum +++ b/go.sum @@ -1027,8 +1027,8 @@ github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3 github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= -github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -1046,7 +1046,8 @@ github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeR github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= -github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= +github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/vendor/github.com/onsi/gomega/.gitignore b/vendor/github.com/onsi/gomega/.gitignore new file mode 100644 index 00000000000..52266eae113 --- /dev/null +++ b/vendor/github.com/onsi/gomega/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +*.test +. +.idea +gomega.iml +TODO.md +.vscode \ No newline at end of file diff --git a/vendor/github.com/onsi/gomega/CHANGELOG.md b/vendor/github.com/onsi/gomega/CHANGELOG.md new file mode 100644 index 00000000000..830467e4da4 --- /dev/null +++ b/vendor/github.com/onsi/gomega/CHANGELOG.md @@ -0,0 +1,520 @@ +## 1.26.0 + +### Features +- When a polled function returns an error, keep track of the actual and report on the matcher state of the last non-errored actual [21f3090] +- improve eventually failure message output [c530fb3] + +### Fixes +- fix several documentation spelling issues [e2eff1f] + + +## 1.25.0 + +### Features +- add `MustPassRepeatedly(int)` to asyncAssertion (#619) [4509f72] +- compare unwrapped errors using DeepEqual (#617) [aaeaa5d] + +### Maintenance +- Bump golang.org/x/net from 0.4.0 to 0.5.0 (#614) [c7cfea4] +- Bump github.com/onsi/ginkgo/v2 from 2.6.1 to 2.7.0 (#615) [71b8adb] +- Docs: Fix typo "MUltiple" -> "Multiple" (#616) [9351dda] +- clean up go.sum [cd1dc1d] + +## 1.24.2 + +### Fixes +- Correctly handle assertion failure panics for eventually/consistnetly "g Gomega"s in a goroutine [78f1660] +- docs:Fix typo "you an" -> "you can" (#607) [3187c1f] +- fixes issue #600 (#606) [808d192] + +### Maintenance +- Bump golang.org/x/net from 0.2.0 to 0.4.0 (#611) [6ebc0bf] +- Bump nokogiri from 1.13.9 to 1.13.10 in /docs (#612) [258cfc8] +- Bump github.com/onsi/ginkgo/v2 from 2.5.0 to 2.5.1 (#609) [e6c3eb9] + +## 1.24.1 + +### Fixes +- maintain backward compatibility for Eventually and Consisntetly's signatures [4c7df5e] +- fix small typo (#601) [ea0ebe6] + +### Maintenance +- Bump golang.org/x/net from 0.1.0 to 0.2.0 (#603) [1ba8372] +- Bump github.com/onsi/ginkgo/v2 from 2.4.0 to 2.5.0 (#602) [f9426cb] +- fix label-filter in test.yml [d795db6] +- stop running flakey tests and rely on external network dependencies in CI [7133290] + +## 1.24.0 + +### Features + +Introducting [gcustom](https://onsi.github.io/gomega/#gcustom-a-convenient-mechanism-for-buildling-custom-matchers) - a convenient mechanism for building custom matchers. + +This is an RC release for `gcustom`. The external API may be tweaked in response to feedback however it is expected to remain mostly stable. + +### Maintenance + +- Update BeComparableTo documentation [756eaa0] + +## 1.23.0 + +### Features +- Custom formatting on a per-type basis can be provided using `format.RegisterCustomFormatter()` -- see the docs [here](https://onsi.github.io/gomega/#adjusting-output) + +- Substantial improvement have been made to `StopTrying()`: + - Users can now use `StopTrying().Wrap(err)` to wrap errors and `StopTrying().Attach(description, object)` to attach arbitrary objects to the `StopTrying()` error + - `StopTrying()` is now always interpreted as a failure. If you are an early adopter of `StopTrying()` you may need to change your code as the prior version would match against the returned value even if `StopTrying()` was returned. Going forward the `StopTrying()` api should remain stable. + - `StopTrying()` and `StopTrying().Now()` can both be used in matchers - not just polled functions. + +- `TryAgainAfter(duration)` is used like `StopTrying()` but instructs `Eventually` and `Consistently` that the poll should be tried again after the specified duration. This allows you to dynamically adjust the polling duration. + +- `ctx` can now be passed-in as the first argument to `Eventually` and `Consistently`. + +## Maintenance + +- Bump github.com/onsi/ginkgo/v2 from 2.3.0 to 2.3.1 (#597) [afed901] +- Bump nokogiri from 1.13.8 to 1.13.9 in /docs (#599) [7c691b3] +- Bump github.com/google/go-cmp from 0.5.8 to 0.5.9 (#587) [ff22665] + +## 1.22.1 + +## Fixes +- When passed a context and no explicit timeout, Eventually will only timeout when the context is cancelled [e5105cf] +- Allow StopTrying() to be wrapped [bf3cba9] + +## Maintenance +- bump to ginkgo v2.3.0 [c5d5c39] + +## 1.22.0 + +### Features + +Several improvements have been made to `Eventually` and `Consistently` in this and the most recent releases: + +- Eventually and Consistently can take a context.Context [65c01bc] + This enables integration with Ginkgo 2.3.0's interruptible nodes and node timeouts. +- Eventually and Consistently that are passed a SpecContext can provide reports when an interrupt occurs [0d063c9] +- Eventually/Consistently will forward an attached context to functions that ask for one [e2091c5] +- Eventually/Consistently supports passing arguments to functions via WithArguments() [a2dc7c3] +- Eventually and Consistently can now be stopped early with StopTrying(message) and StopTrying(message).Now() [52976bb] + +These improvements are all documented in [Gomega's docs](https://onsi.github.io/gomega/#making-asynchronous-assertions) + +## Fixes + +## Maintenance + +## 1.21.1 + +### Features +- Eventually and Consistently that are passed a SpecContext can provide reports when an interrupt occurs [0d063c9] + +## 1.21.0 + +### Features +- Eventually and Consistently can take a context.Context [65c01bc] + This enables integration with Ginkgo 2.3.0's interruptible nodes and node timeouts. +- Introduces Eventually.Within.ProbeEvery with tests and documentation (#591) [f633800] +- New BeKeyOf matcher with documentation and unit tests (#590) [fb586b3] + +## Fixes +- Cover the entire gmeasure suite with leak detection [8c54344] +- Fix gmeasure leak [119d4ce] +- Ignore new Ginkgo ProgressSignal goroutine in gleak [ba548e2] + +## Maintenance + +- Fixes crashes on newer Ruby 3 installations by upgrading github-pages gem dependency (#596) [12469a0] + + +## 1.20.2 + +## Fixes +- label specs that rely on remote access; bump timeout on short-circuit test to make it less flaky [35eeadf] +- gexec: allow more headroom for SIGABRT-related unit tests (#581) [5b78f40] +- Enable reading from a closed gbytes.Buffer (#575) [061fd26] + +## Maintenance +- Bump github.com/onsi/ginkgo/v2 from 2.1.5 to 2.1.6 (#583) [55d895b] +- Bump github.com/onsi/ginkgo/v2 from 2.1.4 to 2.1.5 (#582) [346de7c] + +## 1.20.1 + +## Fixes +- fix false positive gleaks when using ginkgo -p (#577) [cb46517] +- Fix typos in gomega_dsl.go (#569) [5f71ed2] +- don't panic on Eventually(nil), fixing #555 (#567) [9d1186f] +- vet optional description args in assertions, fixing #560 (#566) [8e37808] + +## Maintenance +- test: add new Go 1.19 to test matrix (#571) [40d7efe] +- Bump tzinfo from 1.2.9 to 1.2.10 in /docs (#564) [5f26371] + +## 1.20.0 + +## Features +- New [`gleak`](https://onsi.github.io/gomega/#codegleakcode-finding-leaked-goroutines) experimental goroutine leak detection package! (#538) [85ba7bc] +- New `BeComparableTo` matcher(#546) that uses `gocmp` to make comparisons [e77ea75] +- New `HaveExistingField` matcher (#553) [fd130e1] +- Document how to wrap Gomega (#539) [56714a4] + +## Fixes +- Support pointer receivers in HaveField; fixes #543 (#544) [8dab36e] + +## Maintenance +- Bump various dependencies: + - Upgrade to yaml.v3 (#556) [f5a83b1] + - Bump github/codeql-action from 1 to 2 (#549) [52f5adf] + - Bump github.com/google/go-cmp from 0.5.7 to 0.5.8 (#551) [5f3942d] + - Bump nokogiri from 1.13.4 to 1.13.6 in /docs (#554) [eb4b4c2] + - Use latest ginkgo (#535) [1c29028] + - Bump nokogiri from 1.13.3 to 1.13.4 in /docs (#541) [1ce84d5] + - Bump actions/setup-go from 2 to 3 (#540) [755485e] + - Bump nokogiri from 1.12.5 to 1.13.3 in /docs (#522) [4fbb0dc] + - Bump actions/checkout from 2 to 3 (#526) [ac49202] + +## 1.19.0 + +## Features +- New [`HaveEach`](https://onsi.github.io/gomega/#haveeachelement-interface) matcher to ensure that each and every element in an `array`, `slice`, or `map` satisfies the passed in matcher. (#523) [9fc2ae2] (#524) [c8ba582] +- Users can now wrap the `Gomega` interface to implement custom behavior on each assertion. (#521) [1f2e714] +- [`ContainElement`](https://onsi.github.io/gomega/#containelementelement-interface) now accepts an additional pointer argument. Elements that satisfy the matcher are stored in the pointer enabling developers to easily add subsequent, more detailed, assertions against the matching element. (#527) [1a4e27f] + +## Fixes +- update RELEASING instructions to match ginkgo [0917cde] +- Bump github.com/onsi/ginkgo/v2 from 2.0.0 to 2.1.3 (#519) [49ab4b0] +- Fix CVE-2021-38561 (#534) [f1b4456] +- Fix max number of samples in experiments on non-64-bit systems. (#528) [1c84497] +- Remove dependency on ginkgo v1.16.4 (#530) [4dea8d5] +- Fix for Go 1.18 (#532) [56d2a29] +- Document precendence of timeouts (#533) [b607941] + +## 1.18.1 + +## Fixes +- Add pointer support to HaveField matcher (#495) [79e41a3] + +## 1.18.0 + +## Features +- Docs now live on the master branch in the docs folder which will make for easier PRs. The docs also use Ginkgo 2.0's new docs html/css/js. [2570272] +- New HaveValue matcher can handle actuals that are either values (in which case they are passed on unscathed) or pointers (in which case they are indirected). [Docs here.](https://onsi.github.io/gomega/#working-with-values) (#485) [bdc087c] +- Gmeasure has been declared GA [360db9d] + +## Fixes +- Gomega now uses ioutil for Go 1.15 and lower (#492) - official support is only for the most recent two major versions of Go but this will unblock users who need to stay on older unsupported versions of Go. [c29c1c0] + +## Maintenace +- Remove Travis workflow (#491) [72e6040] +- Upgrade to Ginkgo 2.0.0 GA [f383637] +- chore: fix description of HaveField matcher (#487) [2b4b2c0] +- use tools.go to ensure Ginkgo cli dependencies are included [f58a52b] +- remove dockerfile and simplify github actions to match ginkgo's actions [3f8160d] + +## 1.17.0 + +### Features +- Add HaveField matcher [3a26311] +- add Error() assertions on the final error value of multi-return values (#480) [2f96943] +- separate out offsets and timeouts (#478) [18a4723] +- fix transformation error reporting (#479) [e001fab] +- allow transform functions to report errors (#472) [bf93408] + +### Fixes +Stop using deprecated ioutil package (#467) [07f405d] + +## 1.16.0 + +### Features +- feat: HaveHTTPStatus multiple expected values (#465) [aa69f1b] +- feat: HaveHTTPHeaderWithValue() matcher (#463) [dd83a96] +- feat: HaveHTTPBody matcher (#462) [504e1f2] +- feat: formatter for HTTP responses (#461) [e5b3157] + +## 1.15.0 + +### Fixes +The previous version (1.14.0) introduced a change to allow `Eventually` and `Consistently` to support functions that make assertions. This was accomplished by overriding the global fail handler when running the callbacks passed to `Eventually/Consistently` in order to capture any resulting errors. Issue #457 uncovered a flaw with this approach: when multiple `Eventually`s are running concurrently they race when overriding the singleton global fail handler. + +1.15.0 resolves this by requiring users who want to make assertions in `Eventually/Consistently` call backs to explicitly pass in a function that takes a `Gomega` as an argument. The passed-in `Gomega` instance can be used to make assertions. Any failures will cause `Eventually` to retry the callback. This cleaner interface avoids the issue of swapping out globals but comes at the cost of changing the contract introduced in v1.14.0. As such 1.15.0 introduces a breaking change with respect to 1.14.0 - however we expect that adoption of this feature in 1.14.0 remains limited. + +In addition, 1.15.0 cleans up some of Gomega's internals. Most users shouldn't notice any differences stemming from the refactoring that was made. + +## 1.14.0 + +### Features +- gmeasure.SamplingConfig now suppers a MinSamplingInterval [e94dbca] +- Eventually and Consistently support functions that make assertions [2f04e6e] + - Eventually and Consistently now allow their passed-in functions to make assertions. + These assertions must pass or the function is considered to have failed and is retried. + - Eventually and Consistently can now take functions with no return values. These implicitly return nil + if they contain no failed assertion. Otherwise they return an error wrapping the first assertion failure. This allows + these functions to be used with the Succeed() matcher. + - Introduce InterceptGomegaFailure - an analogue to InterceptGomegaFailures - that captures the first assertion failure + and halts execution in its passed-in callback. + +### Fixes +- Call Verify GHTTPWithGomega receiver funcs (#454) [496e6fd] +- Build a binary with an expected name (#446) [7356360] + +## 1.13.0 + +### Features +- gmeasure provides BETA support for benchmarking (#447) [8f2dfbf] +- Set consistently and eventually defaults on init (#443) [12eb778] + +## 1.12.0 + +### Features +- Add Satisfy() matcher (#437) [c548f31] +- tweak truncation message [3360b8c] +- Add format.GomegaStringer (#427) [cc80b6f] +- Add Clear() method to gbytes.Buffer [c3c0920] + +### Fixes +- Fix error message in BeNumericallyMatcher (#432) [09c074a] +- Bump github.com/onsi/ginkgo from 1.12.1 to 1.16.2 (#442) [e5f6ea0] +- Bump github.com/golang/protobuf from 1.4.3 to 1.5.2 (#431) [adae3bf] +- Bump golang.org/x/net (#441) [3275b35] + +## 1.11.0 + +### Features +- feature: add index to gstruct element func (#419) [334e00d] +- feat(gexec) Add CompileTest functions. Close #410 (#411) [47c613f] + +### Fixes +- Check more carefully for nils in WithTransform (#423) [3c60a15] +- fix: typo in Makefile [b82522a] +- Allow WithTransform function to accept a nil value (#422) [b75d2f2] +- fix: print value type for interface{} containers (#409) [f08e2dc] +- fix(BeElementOf): consistently flatten expected values [1fa9468] + +## 1.10.5 + +### Fixes +- fix: collections matchers should display type of expectation (#408) [6b4eb5a] +- fix(ContainElements): consistently flatten expected values [073b880] +- fix(ConsistOf): consistently flatten expected values [7266efe] + +## 1.10.4 + +### Fixes +- update golang net library to more recent version without vulnerability (#406) [817a8b9] +- Correct spelling: alloted -> allotted (#403) [0bae715] +- fix a panic in MessageWithDiff with long message (#402) [ea06b9b] + +## 1.10.3 + +### Fixes +- updates golang/x/net to fix vulnerability detected by snyk (#394) [c479356] + +## 1.10.2 + +### Fixes +- Add ExpectWithOffset, EventuallyWithOffset and ConsistentlyWithOffset to WithT (#391) [990941a] + +## 1.10.1 + +### Fixes +- Update dependencies (#389) [9f5eecd] + +## 1.10.0 + +### Features +- Add HaveHTTPStatusMatcher (#378) [f335c94] +- Changed matcher for content-type in VerifyJSONRepresenting (#377) [6024f5b] +- Make ghttp usable with x-unit style tests (#376) [c0be499] +- Implement PanicWith matcher (#381) [f8032b4] + +## 1.9.0 + +### Features +- Add ContainElements matcher (#370) [2f57380] +- Output missing and extra elements in ConsistOf failure message [a31eda7] +- Document method LargestMatching [7c5a280] + +## 1.8.1 + +### Fixes +- Fix unexpected MatchError() behaviour (#375) [8ae7b2f] + +## 1.8.0 + +### Features +- Allow optional description to be lazily evaluated function (#364) [bf64010] +- Support wrapped errors (#359) [0a981cb] + +## 1.7.1 + +### Fixes +- Bump go-yaml version to cover fixed ddos heuristic (#362) [95e431e] + +## 1.7.0 + +### Features +- export format property variables (#347) [642e5ba] + +### Fixes +- minor fix in the documentation of ExpectWithOffset (#358) [beea727] + +## 1.6.0 + +### Features + +- Display special chars on error [41e1b26] +- Add BeElementOf matcher [6a48b48] + +### Fixes + +- Remove duplication in XML matcher tests [cc1a6cb] +- Remove unnecessary conversions (#357) [7bf756a] +- Fixed import order (#353) [2e3b965] +- Added missing error handling in test (#355) [c98d3eb] +- Simplify code (#356) [0001ed9] +- Simplify code (#354) [0d9100e] +- Fixed typos (#352) [3f647c4] +- Add failure message tests to BeElementOf matcher [efe19c3] +- Update go-testcov untested sections [37ee382] +- Mark all uncovered files so go-testcov ./... works [53b150e] +- Reenable gotip in travis [5c249dc] +- Fix the typo of comment (#345) [f0e010e] +- Optimize contain_element_matcher [abeb93d] + + +## 1.5.0 + +### Features + +- Added MatchKeys matchers [8b909fc] + +### Fixes and Minor Improvements + +- Add type aliases to remove stuttering [03b0461] +- Don't run session_test.go on windows (#324) [5533ce8] + +## 1.4.3 + +### Fixes: + +- ensure file name and line numbers are correctly reported for XUnit [6fff58f] +- Fixed matcher for content-type (#305) [69d9b43] + +## 1.4.2 + +### Fixes: + +- Add go.mod and go.sum files to define the gomega go module [f3de367, a085d30] +- Work around go vet issue with Go v1.11 (#300) [40dd6ad] +- Better output when using with go XUnit-style tests, fixes #255 (#297) [29a4b97] +- Fix MatchJSON fail to parse json.RawMessage (#298) [ae19f1b] +- show threshold in failure message of BeNumericallyMatcher (#293) [4bbecc8] + +## 1.4.1 + +### Fixes: + +- Update documentation formatting and examples (#289) [9be8410] +- allow 'Receive' matcher to be used with concrete types (#286) [41673fd] +- Fix data race in ghttp server (#283) [7ac6b01] +- Travis badge should only show master [cc102ab] + +## 1.4.0 + +### Features +- Make string pretty diff user configurable (#273) [eb112ce, 649b44d] + +### Fixes +- Use httputil.DumpRequest to pretty-print unhandled requests (#278) [a4ff0fc, b7d1a52] +- fix typo floa32 > float32 (#272) [041ae3b, 6e33911] +- Fix link to documentation on adding your own matchers (#270) [bb2c830, fcebc62] +- Use setters and getters to avoid race condition (#262) [13057c3, a9c79f1] +- Avoid sending a signal if the process is not alive (#259) [b8043e5, 4fc1762] +- Improve message from AssignableToTypeOf when expected value is nil (#281) [9c1fb20] + +## 1.3.0 + +Improvements: + +- The `Equal` matcher matches byte slices more performantly. +- Improved how `MatchError` matches error strings. +- `MatchXML` ignores the order of xml node attributes. +- Improve support for XUnit style golang tests. ([#254](https://github.com/onsi/gomega/issues/254)) + +Bug Fixes: + +- Diff generation now handles multi-byte sequences correctly. +- Multiple goroutines can now call `gexec.Build` concurrently. + +## 1.2.0 + +Improvements: + +- Added `BeSent` which attempts to send a value down a channel and fails if the attempt blocks. Can be paired with `Eventually` to safely send a value down a channel with a timeout. +- `Ω`, `Expect`, `Eventually`, and `Consistently` now immediately `panic` if there is no registered fail handler. This is always a mistake that can hide failing tests. +- `Receive()` no longer errors when passed a closed channel, it's perfectly fine to attempt to read from a closed channel so Ω(c).Should(Receive()) always fails and Ω(c).ShoudlNot(Receive()) always passes with a closed channel. +- Added `HavePrefix` and `HaveSuffix` matchers. +- `ghttp` can now handle concurrent requests. +- Added `Succeed` which allows one to write `Ω(MyFunction()).Should(Succeed())`. +- Improved `ghttp`'s behavior around failing assertions and panics: + - If a registered handler makes a failing assertion `ghttp` will return `500`. + - If a registered handler panics, `ghttp` will return `500` *and* fail the test. This is new behavior that may cause existing code to break. This code is almost certainly incorrect and creating a false positive. +- `ghttp` servers can take an `io.Writer`. `ghttp` will write a line to the writer when each request arrives. +- Added `WithTransform` matcher to allow munging input data before feeding into the relevant matcher +- Added boolean `And`, `Or`, and `Not` matchers to allow creating composite matchers +- Added `gbytes.TimeoutCloser`, `gbytes.TimeoutReader`, and `gbytes.TimeoutWriter` - these are convenience wrappers that timeout if the underlying Closer/Reader/Writer does not return within the alloted time. +- Added `gbytes.BufferReader` - this constructs a `gbytes.Buffer` that asynchronously reads the passed-in `io.Reader` into its buffer. + +Bug Fixes: +- gexec: `session.Wait` now uses `EventuallyWithOffset` to get the right line number in the failure. +- `ContainElement` no longer bails if a passed-in matcher errors. + +## 1.0 (8/2/2014) + +No changes. Dropping "beta" from the version number. + +## 1.0.0-beta (7/8/2014) +Breaking Changes: + +- Changed OmegaMatcher interface. Instead of having `Match` return failure messages, two new methods `FailureMessage` and `NegatedFailureMessage` are called instead. +- Moved and renamed OmegaFailHandler to types.GomegaFailHandler and OmegaMatcher to types.GomegaMatcher. Any references to OmegaMatcher in any custom matchers will need to be changed to point to types.GomegaMatcher + +New Test-Support Features: + +- `ghttp`: supports testing http clients + - Provides a flexible fake http server + - Provides a collection of chainable http handlers that perform assertions. +- `gbytes`: supports making ordered assertions against streams of data + - Provides a `gbytes.Buffer` + - Provides a `Say` matcher to perform ordered assertions against output data +- `gexec`: supports testing external processes + - Provides support for building Go binaries + - Wraps and starts `exec.Cmd` commands + - Makes it easy to assert against stdout and stderr + - Makes it easy to send signals and wait for processes to exit + - Provides an `Exit` matcher to assert against exit code. + +DSL Changes: + +- `Eventually` and `Consistently` can accept `time.Duration` interval and polling inputs. +- The default timeouts for `Eventually` and `Consistently` are now configurable. + +New Matchers: + +- `ConsistOf`: order-independent assertion against the elements of an array/slice or keys of a map. +- `BeTemporally`: like `BeNumerically` but for `time.Time` +- `HaveKeyWithValue`: asserts a map has a given key with the given value. + +Updated Matchers: + +- `Receive` matcher can take a matcher as an argument and passes only if the channel under test receives an objet that satisfies the passed-in matcher. +- Matchers that implement `MatchMayChangeInTheFuture(actual interface{}) bool` can inform `Eventually` and/or `Consistently` when a match has no chance of changing status in the future. For example, `Receive` returns `false` when a channel is closed. + +Misc: + +- Start using semantic versioning +- Start maintaining changelog + +Major refactor: + +- Pull out Gomega's internal to `internal` diff --git a/vendor/github.com/onsi/gomega/CONTRIBUTING.md b/vendor/github.com/onsi/gomega/CONTRIBUTING.md new file mode 100644 index 00000000000..0d7a099289e --- /dev/null +++ b/vendor/github.com/onsi/gomega/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing to Gomega + +Your contributions to Gomega are essential for its long-term maintenance and improvement. To make a contribution: + +- Please **open an issue first** - describe what problem you are trying to solve and give the community a forum for input and feedback ahead of investing time in writing code! +- Ensure adequate test coverage: + - Make sure to add appropriate unit tests + - Please run all tests locally (`ginkgo -r -p`) and make sure they go green before submitting the PR + - Please run following linter locally `go vet ./...` and make sure output does not contain any warnings +- Update the documentation. In addition to standard `godoc` comments Gomega has extensive documentation on the `gh-pages` branch. If relevant, please submit a docs PR to that branch alongside your code PR. + +If you're a committer, check out RELEASING.md to learn how to cut a release. + +Thanks for supporting Gomega! diff --git a/vendor/github.com/onsi/gomega/LICENSE b/vendor/github.com/onsi/gomega/LICENSE new file mode 100644 index 00000000000..9415ee72c17 --- /dev/null +++ b/vendor/github.com/onsi/gomega/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2013-2014 Onsi Fakhouri + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/onsi/gomega/README.md b/vendor/github.com/onsi/gomega/README.md new file mode 100644 index 00000000000..d45a8c4e596 --- /dev/null +++ b/vendor/github.com/onsi/gomega/README.md @@ -0,0 +1,21 @@ +![Gomega: Ginkgo's Preferred Matcher Library](http://onsi.github.io/gomega/images/gomega.png) + +[![test](https://github.com/onsi/gomega/actions/workflows/test.yml/badge.svg)](https://github.com/onsi/gomega/actions/workflows/test.yml) + +Jump straight to the [docs](http://onsi.github.io/gomega/) to learn about Gomega, including a list of [all available matchers](http://onsi.github.io/gomega/#provided-matchers). + +If you have a question, comment, bug report, feature request, etc. please open a GitHub issue. + +## [Ginkgo](http://github.com/onsi/ginkgo): a BDD Testing Framework for Golang + +Learn more about Ginkgo [here](http://onsi.github.io/ginkgo/) + +## Community Matchers + +A collection of community matchers is available on the [wiki](https://github.com/onsi/gomega/wiki). + +## License + +Gomega is MIT-Licensed + +The `ConsistOf` matcher uses [goraph](https://github.com/amitkgupta/goraph) which is embedded in the source to simplify distribution. goraph has an MIT license. diff --git a/vendor/github.com/onsi/gomega/RELEASING.md b/vendor/github.com/onsi/gomega/RELEASING.md new file mode 100644 index 00000000000..9973fff49e0 --- /dev/null +++ b/vendor/github.com/onsi/gomega/RELEASING.md @@ -0,0 +1,23 @@ +A Gomega release is a tagged sha and a GitHub release. To cut a release: + +1. Ensure CHANGELOG.md is up to date. + - Use + ```bash + LAST_VERSION=$(git tag --sort=version:refname | tail -n1) + CHANGES=$(git log --pretty=format:'- %s [%h]' HEAD...$LAST_VERSION) + echo -e "## NEXT\n\n$CHANGES\n\n### Features\n\n### Fixes\n\n### Maintenance\n\n$(cat CHANGELOG.md)" > CHANGELOG.md + ``` + to update the changelog + - Categorize the changes into + - Breaking Changes (requires a major version) + - New Features (minor version) + - Fixes (fix version) + - Maintenance (which in general should not be mentioned in `CHANGELOG.md` as they have no user impact) +1. Update GOMEGA_VERSION in `gomega_dsl.go` +1. Commit, push, and release: + ``` + git commit -m "vM.m.p" + git push + gh release create "vM.m.p" + git fetch --tags origin master + ``` \ No newline at end of file diff --git a/vendor/github.com/onsi/gomega/format/format.go b/vendor/github.com/onsi/gomega/format/format.go new file mode 100644 index 00000000000..1a2ed877a95 --- /dev/null +++ b/vendor/github.com/onsi/gomega/format/format.go @@ -0,0 +1,502 @@ +/* +Gomega's format package pretty-prints objects. It explores input objects recursively and generates formatted, indented output with type information. +*/ + +// untested sections: 4 + +package format + +import ( + "context" + "fmt" + "reflect" + "strconv" + "strings" + "time" +) + +// Use MaxDepth to set the maximum recursion depth when printing deeply nested objects +var MaxDepth = uint(10) + +// MaxLength of the string representation of an object. +// If MaxLength is set to 0, the Object will not be truncated. +var MaxLength = 4000 + +/* +By default, all objects (even those that implement fmt.Stringer and fmt.GoStringer) are recursively inspected to generate output. + +Set UseStringerRepresentation = true to use GoString (for fmt.GoStringers) or String (for fmt.Stringer) instead. + +Note that GoString and String don't always have all the information you need to understand why a test failed! +*/ +var UseStringerRepresentation = false + +/* +Print the content of context objects. By default it will be suppressed. + +Set PrintContextObjects = true to enable printing of the context internals. +*/ +var PrintContextObjects = false + +// TruncatedDiff choose if we should display a truncated pretty diff or not +var TruncatedDiff = true + +// TruncateThreshold (default 50) specifies the maximum length string to print in string comparison assertion error +// messages. +var TruncateThreshold uint = 50 + +// CharactersAroundMismatchToInclude (default 5) specifies how many contextual characters should be printed before and +// after the first diff location in a truncated string assertion error message. +var CharactersAroundMismatchToInclude uint = 5 + +var contextType = reflect.TypeOf((*context.Context)(nil)).Elem() +var timeType = reflect.TypeOf(time.Time{}) + +//The default indentation string emitted by the format package +var Indent = " " + +var longFormThreshold = 20 + +// GomegaStringer allows for custom formating of objects for gomega. +type GomegaStringer interface { + // GomegaString will be used to custom format an object. + // It does not follow UseStringerRepresentation value and will always be called regardless. + // It also ignores the MaxLength value. + GomegaString() string +} + +/* +CustomFormatters can be registered with Gomega via RegisterCustomFormatter() +Any value to be rendered by Gomega is passed to each registered CustomFormatters. +The CustomFormatter signals that it will handle formatting the value by returning (formatted-string, true) +If the CustomFormatter does not want to handle the object it should return ("", false) + +Strings returned by CustomFormatters are not truncated +*/ +type CustomFormatter func(value interface{}) (string, bool) +type CustomFormatterKey uint + +var customFormatterKey CustomFormatterKey = 1 + +type customFormatterKeyPair struct { + CustomFormatter + CustomFormatterKey +} + +/* +RegisterCustomFormatter registers a CustomFormatter and returns a CustomFormatterKey + +You can call UnregisterCustomFormatter with the returned key to unregister the associated CustomFormatter +*/ +func RegisterCustomFormatter(customFormatter CustomFormatter) CustomFormatterKey { + key := customFormatterKey + customFormatterKey += 1 + customFormatters = append(customFormatters, customFormatterKeyPair{customFormatter, key}) + return key +} + +/* +UnregisterCustomFormatter unregisters a previously registered CustomFormatter. You should pass in the key returned by RegisterCustomFormatter +*/ +func UnregisterCustomFormatter(key CustomFormatterKey) { + formatters := []customFormatterKeyPair{} + for _, f := range customFormatters { + if f.CustomFormatterKey == key { + continue + } + formatters = append(formatters, f) + } + customFormatters = formatters +} + +var customFormatters = []customFormatterKeyPair{} + +/* +Generates a formatted matcher success/failure message of the form: + + Expected + + + + +If expected is omitted, then the message looks like: + + Expected + + +*/ +func Message(actual interface{}, message string, expected ...interface{}) string { + if len(expected) == 0 { + return fmt.Sprintf("Expected\n%s\n%s", Object(actual, 1), message) + } + return fmt.Sprintf("Expected\n%s\n%s\n%s", Object(actual, 1), message, Object(expected[0], 1)) +} + +/* + +Generates a nicely formatted matcher success / failure message + +Much like Message(...), but it attempts to pretty print diffs in strings + +Expected + : "...aaaaabaaaaa..." +to equal | + : "...aaaaazaaaaa..." + +*/ + +func MessageWithDiff(actual, message, expected string) string { + if TruncatedDiff && len(actual) >= int(TruncateThreshold) && len(expected) >= int(TruncateThreshold) { + diffPoint := findFirstMismatch(actual, expected) + formattedActual := truncateAndFormat(actual, diffPoint) + formattedExpected := truncateAndFormat(expected, diffPoint) + + spacesBeforeFormattedMismatch := findFirstMismatch(formattedActual, formattedExpected) + + tabLength := 4 + spaceFromMessageToActual := tabLength + len(": ") - len(message) + + paddingCount := spaceFromMessageToActual + spacesBeforeFormattedMismatch + if paddingCount < 0 { + return Message(formattedActual, message, formattedExpected) + } + + padding := strings.Repeat(" ", paddingCount) + "|" + return Message(formattedActual, message+padding, formattedExpected) + } + + actual = escapedWithGoSyntax(actual) + expected = escapedWithGoSyntax(expected) + + return Message(actual, message, expected) +} + +func escapedWithGoSyntax(str string) string { + withQuotes := fmt.Sprintf("%q", str) + return withQuotes[1 : len(withQuotes)-1] +} + +func truncateAndFormat(str string, index int) string { + leftPadding := `...` + rightPadding := `...` + + start := index - int(CharactersAroundMismatchToInclude) + if start < 0 { + start = 0 + leftPadding = "" + } + + // slice index must include the mis-matched character + lengthOfMismatchedCharacter := 1 + end := index + int(CharactersAroundMismatchToInclude) + lengthOfMismatchedCharacter + if end > len(str) { + end = len(str) + rightPadding = "" + + } + return fmt.Sprintf("\"%s\"", leftPadding+str[start:end]+rightPadding) +} + +func findFirstMismatch(a, b string) int { + aSlice := strings.Split(a, "") + bSlice := strings.Split(b, "") + + for index, str := range aSlice { + if index > len(bSlice)-1 { + return index + } + if str != bSlice[index] { + return index + } + } + + if len(b) > len(a) { + return len(a) + 1 + } + + return 0 +} + +const truncateHelpText = ` +Gomega truncated this representation as it exceeds 'format.MaxLength'. +Consider having the object provide a custom 'GomegaStringer' representation +or adjust the parameters in Gomega's 'format' package. + +Learn more here: https://onsi.github.io/gomega/#adjusting-output +` + +func truncateLongStrings(s string) string { + if MaxLength > 0 && len(s) > MaxLength { + var sb strings.Builder + for i, r := range s { + if i < MaxLength { + sb.WriteRune(r) + continue + } + break + } + + sb.WriteString("...\n") + sb.WriteString(truncateHelpText) + + return sb.String() + } + return s +} + +/* +Pretty prints the passed in object at the passed in indentation level. + +Object recurses into deeply nested objects emitting pretty-printed representations of their components. + +Modify format.MaxDepth to control how deep the recursion is allowed to go +Set format.UseStringerRepresentation to true to return object.GoString() or object.String() when available instead of +recursing into the object. + +Set PrintContextObjects to true to print the content of objects implementing context.Context +*/ +func Object(object interface{}, indentation uint) string { + indent := strings.Repeat(Indent, int(indentation)) + value := reflect.ValueOf(object) + return fmt.Sprintf("%s<%s>: %s", indent, formatType(value), formatValue(value, indentation)) +} + +/* +IndentString takes a string and indents each line by the specified amount. +*/ +func IndentString(s string, indentation uint) string { + return indentString(s, indentation, true) +} + +func indentString(s string, indentation uint, indentFirstLine bool) string { + result := &strings.Builder{} + components := strings.Split(s, "\n") + indent := strings.Repeat(Indent, int(indentation)) + for i, component := range components { + if i > 0 || indentFirstLine { + result.WriteString(indent) + } + result.WriteString(component) + if i < len(components)-1 { + result.WriteString("\n") + } + } + + return result.String() +} + +func formatType(v reflect.Value) string { + switch v.Kind() { + case reflect.Invalid: + return "nil" + case reflect.Chan: + return fmt.Sprintf("%s | len:%d, cap:%d", v.Type(), v.Len(), v.Cap()) + case reflect.Ptr: + return fmt.Sprintf("%s | 0x%x", v.Type(), v.Pointer()) + case reflect.Slice: + return fmt.Sprintf("%s | len:%d, cap:%d", v.Type(), v.Len(), v.Cap()) + case reflect.Map: + return fmt.Sprintf("%s | len:%d", v.Type(), v.Len()) + default: + return fmt.Sprintf("%s", v.Type()) + } +} + +func formatValue(value reflect.Value, indentation uint) string { + if indentation > MaxDepth { + return "..." + } + + if isNilValue(value) { + return "nil" + } + + if value.CanInterface() { + obj := value.Interface() + + // if a CustomFormatter handles this values, we'll go with that + for _, customFormatter := range customFormatters { + formatted, handled := customFormatter.CustomFormatter(obj) + // do not truncate a user-provided CustomFormatter() + if handled { + return indentString(formatted, indentation+1, false) + } + } + + // GomegaStringer will take precedence to other representations and disregards UseStringerRepresentation + if x, ok := obj.(GomegaStringer); ok { + // do not truncate a user-defined GomegaString() value + return indentString(x.GomegaString(), indentation+1, false) + } + + if UseStringerRepresentation { + switch x := obj.(type) { + case fmt.GoStringer: + return indentString(truncateLongStrings(x.GoString()), indentation+1, false) + case fmt.Stringer: + return indentString(truncateLongStrings(x.String()), indentation+1, false) + } + } + } + + if !PrintContextObjects { + if value.Type().Implements(contextType) && indentation > 1 { + return "" + } + } + + switch value.Kind() { + case reflect.Bool: + return fmt.Sprintf("%v", value.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return fmt.Sprintf("%v", value.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return fmt.Sprintf("%v", value.Uint()) + case reflect.Uintptr: + return fmt.Sprintf("0x%x", value.Uint()) + case reflect.Float32, reflect.Float64: + return fmt.Sprintf("%v", value.Float()) + case reflect.Complex64, reflect.Complex128: + return fmt.Sprintf("%v", value.Complex()) + case reflect.Chan: + return fmt.Sprintf("0x%x", value.Pointer()) + case reflect.Func: + return fmt.Sprintf("0x%x", value.Pointer()) + case reflect.Ptr: + return formatValue(value.Elem(), indentation) + case reflect.Slice: + return truncateLongStrings(formatSlice(value, indentation)) + case reflect.String: + return truncateLongStrings(formatString(value.String(), indentation)) + case reflect.Array: + return truncateLongStrings(formatSlice(value, indentation)) + case reflect.Map: + return truncateLongStrings(formatMap(value, indentation)) + case reflect.Struct: + if value.Type() == timeType && value.CanInterface() { + t, _ := value.Interface().(time.Time) + return t.Format(time.RFC3339Nano) + } + return truncateLongStrings(formatStruct(value, indentation)) + case reflect.Interface: + return formatInterface(value, indentation) + default: + if value.CanInterface() { + return truncateLongStrings(fmt.Sprintf("%#v", value.Interface())) + } + return truncateLongStrings(fmt.Sprintf("%#v", value)) + } +} + +func formatString(object interface{}, indentation uint) string { + if indentation == 1 { + s := fmt.Sprintf("%s", object) + components := strings.Split(s, "\n") + result := "" + for i, component := range components { + if i == 0 { + result += component + } else { + result += Indent + component + } + if i < len(components)-1 { + result += "\n" + } + } + + return result + } else { + return fmt.Sprintf("%q", object) + } +} + +func formatSlice(v reflect.Value, indentation uint) string { + if v.Kind() == reflect.Slice && v.Type().Elem().Kind() == reflect.Uint8 && isPrintableString(string(v.Bytes())) { + return formatString(v.Bytes(), indentation) + } + + l := v.Len() + result := make([]string, l) + longest := 0 + for i := 0; i < l; i++ { + result[i] = formatValue(v.Index(i), indentation+1) + if len(result[i]) > longest { + longest = len(result[i]) + } + } + + if longest > longFormThreshold { + indenter := strings.Repeat(Indent, int(indentation)) + return fmt.Sprintf("[\n%s%s,\n%s]", indenter+Indent, strings.Join(result, ",\n"+indenter+Indent), indenter) + } + return fmt.Sprintf("[%s]", strings.Join(result, ", ")) +} + +func formatMap(v reflect.Value, indentation uint) string { + l := v.Len() + result := make([]string, l) + + longest := 0 + for i, key := range v.MapKeys() { + value := v.MapIndex(key) + result[i] = fmt.Sprintf("%s: %s", formatValue(key, indentation+1), formatValue(value, indentation+1)) + if len(result[i]) > longest { + longest = len(result[i]) + } + } + + if longest > longFormThreshold { + indenter := strings.Repeat(Indent, int(indentation)) + return fmt.Sprintf("{\n%s%s,\n%s}", indenter+Indent, strings.Join(result, ",\n"+indenter+Indent), indenter) + } + return fmt.Sprintf("{%s}", strings.Join(result, ", ")) +} + +func formatStruct(v reflect.Value, indentation uint) string { + t := v.Type() + + l := v.NumField() + result := []string{} + longest := 0 + for i := 0; i < l; i++ { + structField := t.Field(i) + fieldEntry := v.Field(i) + representation := fmt.Sprintf("%s: %s", structField.Name, formatValue(fieldEntry, indentation+1)) + result = append(result, representation) + if len(representation) > longest { + longest = len(representation) + } + } + if longest > longFormThreshold { + indenter := strings.Repeat(Indent, int(indentation)) + return fmt.Sprintf("{\n%s%s,\n%s}", indenter+Indent, strings.Join(result, ",\n"+indenter+Indent), indenter) + } + return fmt.Sprintf("{%s}", strings.Join(result, ", ")) +} + +func formatInterface(v reflect.Value, indentation uint) string { + return fmt.Sprintf("<%s>%s", formatType(v.Elem()), formatValue(v.Elem(), indentation)) +} + +func isNilValue(a reflect.Value) bool { + switch a.Kind() { + case reflect.Invalid: + return true + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return a.IsNil() + } + + return false +} + +/* +Returns true when the string is entirely made of printable runes, false otherwise. +*/ +func isPrintableString(str string) bool { + for _, runeValue := range str { + if !strconv.IsPrint(runeValue) { + return false + } + } + return true +} diff --git a/vendor/github.com/onsi/gomega/gomega_dsl.go b/vendor/github.com/onsi/gomega/gomega_dsl.go new file mode 100644 index 00000000000..93d33c1374a --- /dev/null +++ b/vendor/github.com/onsi/gomega/gomega_dsl.go @@ -0,0 +1,537 @@ +/* +Gomega is the Ginkgo BDD-style testing framework's preferred matcher library. + +The godoc documentation describes Gomega's API. More comprehensive documentation (with examples!) is available at http://onsi.github.io/gomega/ + +Gomega on Github: http://github.com/onsi/gomega + +Learn more about Ginkgo online: http://onsi.github.io/ginkgo + +Ginkgo on Github: http://github.com/onsi/ginkgo + +Gomega is MIT-Licensed +*/ +package gomega + +import ( + "errors" + "fmt" + "time" + + "github.com/onsi/gomega/internal" + "github.com/onsi/gomega/types" +) + +const GOMEGA_VERSION = "1.26.0" + +const nilGomegaPanic = `You are trying to make an assertion, but haven't registered Gomega's fail handler. +If you're using Ginkgo then you probably forgot to put your assertion in an It(). +Alternatively, you may have forgotten to register a fail handler with RegisterFailHandler() or RegisterTestingT(). +Depending on your vendoring solution you may be inadvertently importing gomega and subpackages (e.g. ghhtp, gexec,...) from different locations. +` + +// Gomega describes the essential Gomega DSL. This interface allows libraries +// to abstract between the standard package-level function implementations +// and alternatives like *WithT. +// +// The types in the top-level DSL have gotten a bit messy due to earlier deprecations that avoid stuttering +// and due to an accidental use of a concrete type (*WithT) in an earlier release. +// +// As of 1.15 both the WithT and Ginkgo variants of Gomega are implemented by the same underlying object +// however one (the Ginkgo variant) is exported as an interface (types.Gomega) whereas the other (the withT variant) +// is shared as a concrete type (*WithT, which is aliased to *internal.Gomega). 1.15 did not clean this mess up to ensure +// that declarations of *WithT in existing code are not broken by the upgrade to 1.15. +type Gomega = types.Gomega + +// DefaultGomega supplies the standard package-level implementation +var Default = Gomega(internal.NewGomega(internal.FetchDefaultDurationBundle())) + +// NewGomega returns an instance of Gomega wired into the passed-in fail handler. +// You generally don't need to use this when using Ginkgo - RegisterFailHandler will wire up the global gomega +// However creating a NewGomega with a custom fail handler can be useful in contexts where you want to use Gomega's +// rich ecosystem of matchers without causing a test to fail. For example, to aggregate a series of potential failures +// or for use in a non-test setting. +func NewGomega(fail types.GomegaFailHandler) Gomega { + return internal.NewGomega(internalGomega(Default).DurationBundle).ConfigureWithFailHandler(fail) +} + +// WithT wraps a *testing.T and provides `Expect`, `Eventually`, and `Consistently` methods. This allows you to leverage +// Gomega's rich ecosystem of matchers in standard `testing` test suites. +// +// Use `NewWithT` to instantiate a `WithT` +// +// As of 1.15 both the WithT and Ginkgo variants of Gomega are implemented by the same underlying object +// however one (the Ginkgo variant) is exported as an interface (types.Gomega) whereas the other (the withT variant) +// is shared as a concrete type (*WithT, which is aliased to *internal.Gomega). 1.15 did not clean this mess up to ensure +// that declarations of *WithT in existing code are not broken by the upgrade to 1.15. +type WithT = internal.Gomega + +// GomegaWithT is deprecated in favor of gomega.WithT, which does not stutter. +type GomegaWithT = WithT + +// inner is an interface that allows users to provide a wrapper around Default. The wrapper +// must implement the inner interface and return either the original Default or the result of +// a call to NewGomega(). +type inner interface { + Inner() Gomega +} + +func internalGomega(g Gomega) *internal.Gomega { + if v, ok := g.(inner); ok { + return v.Inner().(*internal.Gomega) + } + return g.(*internal.Gomega) +} + +// NewWithT takes a *testing.T and returns a `gomega.WithT` allowing you to use `Expect`, `Eventually`, and `Consistently` along with +// Gomega's rich ecosystem of matchers in standard `testing` test suits. +// +// func TestFarmHasCow(t *testing.T) { +// g := gomega.NewWithT(t) +// +// f := farm.New([]string{"Cow", "Horse"}) +// g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow") +// } +func NewWithT(t types.GomegaTestingT) *WithT { + return internal.NewGomega(internalGomega(Default).DurationBundle).ConfigureWithT(t) +} + +// NewGomegaWithT is deprecated in favor of gomega.NewWithT, which does not stutter. +var NewGomegaWithT = NewWithT + +// RegisterFailHandler connects Ginkgo to Gomega. When a matcher fails +// the fail handler passed into RegisterFailHandler is called. +func RegisterFailHandler(fail types.GomegaFailHandler) { + internalGomega(Default).ConfigureWithFailHandler(fail) +} + +// RegisterFailHandlerWithT is deprecated and will be removed in a future release. +// users should use RegisterFailHandler, or RegisterTestingT +func RegisterFailHandlerWithT(_ types.GomegaTestingT, fail types.GomegaFailHandler) { + fmt.Println("RegisterFailHandlerWithT is deprecated. Please use RegisterFailHandler or RegisterTestingT instead.") + internalGomega(Default).ConfigureWithFailHandler(fail) +} + +// RegisterTestingT connects Gomega to Golang's XUnit style +// Testing.T tests. It is now deprecated and you should use NewWithT() instead to get a fresh instance of Gomega for each test. +func RegisterTestingT(t types.GomegaTestingT) { + internalGomega(Default).ConfigureWithT(t) +} + +// InterceptGomegaFailures runs a given callback and returns an array of +// failure messages generated by any Gomega assertions within the callback. +// Execution continues after the first failure allowing users to collect all failures +// in the callback. +// +// This is most useful when testing custom matchers, but can also be used to check +// on a value using a Gomega assertion without causing a test failure. +func InterceptGomegaFailures(f func()) []string { + originalHandler := internalGomega(Default).Fail + failures := []string{} + internalGomega(Default).Fail = func(message string, callerSkip ...int) { + failures = append(failures, message) + } + defer func() { + internalGomega(Default).Fail = originalHandler + }() + f() + return failures +} + +// InterceptGomegaFailure runs a given callback and returns the first +// failure message generated by any Gomega assertions within the callback, wrapped in an error. +// +// The callback ceases execution as soon as the first failed assertion occurs, however Gomega +// does not register a failure with the FailHandler registered via RegisterFailHandler - it is up +// to the user to decide what to do with the returned error +func InterceptGomegaFailure(f func()) (err error) { + originalHandler := internalGomega(Default).Fail + internalGomega(Default).Fail = func(message string, callerSkip ...int) { + err = errors.New(message) + panic("stop execution") + } + + defer func() { + internalGomega(Default).Fail = originalHandler + if e := recover(); e != nil { + if err == nil { + panic(e) + } + } + }() + + f() + return err +} + +func ensureDefaultGomegaIsConfigured() { + if !internalGomega(Default).IsConfigured() { + panic(nilGomegaPanic) + } +} + +// Ω wraps an actual value allowing assertions to be made on it: +// +// Ω("foo").Should(Equal("foo")) +// +// If Ω is passed more than one argument it will pass the *first* argument to the matcher. +// All subsequent arguments will be required to be nil/zero. +// +// This is convenient if you want to make an assertion on a method/function that returns +// a value and an error - a common patter in Go. +// +// For example, given a function with signature: +// +// func MyAmazingThing() (int, error) +// +// Then: +// +// Ω(MyAmazingThing()).Should(Equal(3)) +// +// Will succeed only if `MyAmazingThing()` returns `(3, nil)` +// +// Ω and Expect are identical +func Ω(actual interface{}, extra ...interface{}) Assertion { + ensureDefaultGomegaIsConfigured() + return Default.Ω(actual, extra...) +} + +// Expect wraps an actual value allowing assertions to be made on it: +// +// Expect("foo").To(Equal("foo")) +// +// If Expect is passed more than one argument it will pass the *first* argument to the matcher. +// All subsequent arguments will be required to be nil/zero. +// +// This is convenient if you want to make an assertion on a method/function that returns +// a value and an error - a common patter in Go. +// +// For example, given a function with signature: +// +// func MyAmazingThing() (int, error) +// +// Then: +// +// Expect(MyAmazingThing()).Should(Equal(3)) +// +// Will succeed only if `MyAmazingThing()` returns `(3, nil)` +// +// Expect and Ω are identical +func Expect(actual interface{}, extra ...interface{}) Assertion { + ensureDefaultGomegaIsConfigured() + return Default.Expect(actual, extra...) +} + +// ExpectWithOffset wraps an actual value allowing assertions to be made on it: +// +// ExpectWithOffset(1, "foo").To(Equal("foo")) +// +// Unlike `Expect` and `Ω`, `ExpectWithOffset` takes an additional integer argument +// that is used to modify the call-stack offset when computing line numbers. It is +// the same as `Expect(...).WithOffset`. +// +// This is most useful in helper functions that make assertions. If you want Gomega's +// error message to refer to the calling line in the test (as opposed to the line in the helper function) +// set the first argument of `ExpectWithOffset` appropriately. +func ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Assertion { + ensureDefaultGomegaIsConfigured() + return Default.ExpectWithOffset(offset, actual, extra...) +} + +/* +Eventually enables making assertions on asynchronous behavior. + +Eventually checks that an assertion *eventually* passes. Eventually blocks when called and attempts an assertion periodically until it passes or a timeout occurs. Both the timeout and polling interval are configurable as optional arguments. +The first optional argument is the timeout (which defaults to 1s), the second is the polling interval (which defaults to 10ms). Both intervals can be specified as time.Duration, parsable duration strings or floats/integers (in which case they are interpreted as seconds). In addition an optional context.Context can be passed in - Eventually will keep trying until either the timeout epxires or the context is cancelled, whichever comes first. + +Eventually works with any Gomega compatible matcher and supports making assertions against three categories of actual value: + +**Category 1: Making Eventually assertions on values** + +There are several examples of values that can change over time. These can be passed in to Eventually and will be passed to the matcher repeatedly until a match occurs. For example: + + c := make(chan bool) + go DoStuff(c) + Eventually(c, "50ms").Should(BeClosed()) + +will poll the channel repeatedly until it is closed. In this example `Eventually` will block until either the specified timeout of 50ms has elapsed or the channel is closed, whichever comes first. + +Several Gomega libraries allow you to use Eventually in this way. For example, the gomega/gexec package allows you to block until a *gexec.Session exits successfully via: + + Eventually(session).Should(gexec.Exit(0)) + +And the gomega/gbytes package allows you to monitor a streaming *gbytes.Buffer until a given string is seen: + + Eventually(buffer).Should(gbytes.Say("hello there")) + +In these examples, both `session` and `buffer` are designed to be thread-safe when polled by the `Exit` and `Say` matchers. This is not true in general of most raw values, so while it is tempting to do something like: + + // THIS IS NOT THREAD-SAFE + var s *string + go mutateStringEventually(s) + Eventually(s).Should(Equal("I've changed")) + +this will trigger Go's race detector as the goroutine polling via Eventually will race over the value of s with the goroutine mutating the string. For cases like this you can use channels or introduce your own locking around s by passing Eventually a function. + +**Category 2: Make Eventually assertions on functions** + +Eventually can be passed functions that **return at least one value**. When configured this way, Eventually will poll the function repeatedly and pass the first returned value to the matcher. + +For example: + + Eventually(func() int { + return client.FetchCount() + }).Should(BeNumerically(">=", 17)) + + will repeatedly poll client.FetchCount until the BeNumerically matcher is satisfied. (Note that this example could have been written as Eventually(client.FetchCount).Should(BeNumerically(">=", 17))) + +If multiple values are returned by the function, Eventually will pass the first value to the matcher and require that all others are zero-valued. This allows you to pass Eventually a function that returns a value and an error - a common pattern in Go. + +For example, consider a method that returns a value and an error: + + func FetchFromDB() (string, error) + +Then + + Eventually(FetchFromDB).Should(Equal("got it")) + +will pass only if and when the returned error is nil *and* the returned string satisfies the matcher. + +Eventually can also accept functions that take arguments, however you must provide those arguments using .WithArguments(). For example, consider a function that takes a user-id and makes a network request to fetch a full name: + + func FetchFullName(userId int) (string, error) + +You can poll this function like so: + + Eventually(FetchFullName).WithArguments(1138).Should(Equal("Wookie")) + +It is important to note that the function passed into Eventually is invoked *synchronously* when polled. Eventually does not (in fact, it cannot) kill the function if it takes longer to return than Eventually's configured timeout. A common practice here is to use a context. Here's an example that combines Ginkgo's spec timeout support with Eventually: + + It("fetches the correct count", func(ctx SpecContext) { + Eventually(ctx, func() int { + return client.FetchCount(ctx, "/users") + }).Should(BeNumerically(">=", 17)) + }, SpecTimeout(time.Second)) + +you an also use Eventually().WithContext(ctx) to pass in the context. Passed-in contexts play nicely with paseed-in arguments as long as the context appears first. You can rewrite the above example as: + + It("fetches the correct count", func(ctx SpecContext) { + Eventually(client.FetchCount).WithContext(ctx).WithArguments("/users").Should(BeNumerically(">=", 17)) + }, SpecTimeout(time.Second)) + +Either way the context passd to Eventually is also passed to the underlying funciton. Now, when Ginkgo cancels the context both the FetchCount client and Gomega will be informed and can exit. + +**Category 3: Making assertions _in_ the function passed into Eventually** + +When testing complex systems it can be valuable to assert that a _set_ of assertions passes Eventually. Eventually supports this by accepting functions that take a single Gomega argument and return zero or more values. + +Here's an example that makes some assertions and returns a value and error: + + Eventually(func(g Gomega) (Widget, error) { + ids, err := client.FetchIDs() + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(ids).To(ContainElement(1138)) + return client.FetchWidget(1138) + }).Should(Equal(expectedWidget)) + +will pass only if all the assertions in the polled function pass and the return value satisfied the matcher. + +Eventually also supports a special case polling function that takes a single Gomega argument and returns no values. Eventually assumes such a function is making assertions and is designed to work with the Succeed matcher to validate that all assertions have passed. +For example: + + Eventually(func(g Gomega) { + model, err := client.Find(1138) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(model.Reticulate()).To(Succeed()) + g.Expect(model.IsReticulated()).To(BeTrue()) + g.Expect(model.Save()).To(Succeed()) + }).Should(Succeed()) + +will rerun the function until all assertions pass. + +You can also pass additional arugments to functions that take a Gomega. The only rule is that the Gomega argument must be first. If you also want to pass the context attached to Eventually you must ensure that is the second argument. For example: + + Eventually(func(g Gomega, ctx context.Context, path string, expected ...string){ + tok, err := client.GetToken(ctx) + g.Expect(err).NotTo(HaveOccurred()) + + elements, err := client.Fetch(ctx, tok, path) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(elements).To(ConsistOf(expected)) + }).WithContext(ctx).WithArguments("/names", "Joe", "Jane", "Sam").Should(Succeed()) + +You can ensure that you get a number of consecutive successful tries before succeeding using `MustPassRepeatedly(int)`. For Example: + + int count := 0 + Eventually(func() bool { + count++ + return count > 2 + }).MustPassRepeatedly(2).Should(BeTrue()) + // Because we had to wait for 2 calls that returned true + Expect(count).To(Equal(3)) + +Finally, in addition to passing timeouts and a context to Eventually you can be more explicit with Eventually's chaining configuration methods: + + Eventually(..., "1s", "2s", ctx).Should(...) + +is equivalent to + + Eventually(...).WithTimeout(time.Second).WithPolling(2*time.Second).WithContext(ctx).Should(...) +*/ +func Eventually(actualOrCtx interface{}, args ...interface{}) AsyncAssertion { + ensureDefaultGomegaIsConfigured() + return Default.Eventually(actualOrCtx, args...) +} + +// EventuallyWithOffset operates like Eventually but takes an additional +// initial argument to indicate an offset in the call stack. This is useful when building helper +// functions that contain matchers. To learn more, read about `ExpectWithOffset`. +// +// `EventuallyWithOffset` is the same as `Eventually(...).WithOffset`. +// +// `EventuallyWithOffset` specifying a timeout interval (and an optional polling interval) are +// the same as `Eventually(...).WithOffset(...).WithTimeout` or +// `Eventually(...).WithOffset(...).WithTimeout(...).WithPolling`. +func EventuallyWithOffset(offset int, actualOrCtx interface{}, args ...interface{}) AsyncAssertion { + ensureDefaultGomegaIsConfigured() + return Default.EventuallyWithOffset(offset, actualOrCtx, args...) +} + +/* +Consistently, like Eventually, enables making assertions on asynchronous behavior. + +Consistently blocks when called for a specified duration. During that duration Consistently repeatedly polls its matcher and ensures that it is satisfied. If the matcher is consistently satisfied, then Consistently will pass. Otherwise Consistently will fail. + +Both the total waiting duration and the polling interval are configurable as optional arguments. The first optional argument is the duration that Consistently will run for (defaults to 100ms), and the second argument is the polling interval (defaults to 10ms). As with Eventually, these intervals can be passed in as time.Duration, parsable duration strings or an integer or float number of seconds. You can also pass in an optional context.Context - Consistently will exit early (with a failure) if the context is cancelled before the waiting duration expires. + +Consistently accepts the same three categories of actual as Eventually, check the Eventually docs to learn more. + +Consistently is useful in cases where you want to assert that something *does not happen* for a period of time. For example, you may want to assert that a goroutine does *not* send data down a channel. In this case you could write: + + Consistently(channel, "200ms").ShouldNot(Receive()) + +This will block for 200 milliseconds and repeatedly check the channel and ensure nothing has been received. +*/ +func Consistently(actualOrCtx interface{}, args ...interface{}) AsyncAssertion { + ensureDefaultGomegaIsConfigured() + return Default.Consistently(actualOrCtx, args...) +} + +// ConsistentlyWithOffset operates like Consistently but takes an additional +// initial argument to indicate an offset in the call stack. This is useful when building helper +// functions that contain matchers. To learn more, read about `ExpectWithOffset`. +// +// `ConsistentlyWithOffset` is the same as `Consistently(...).WithOffset` and +// optional `WithTimeout` and `WithPolling`. +func ConsistentlyWithOffset(offset int, actualOrCtx interface{}, args ...interface{}) AsyncAssertion { + ensureDefaultGomegaIsConfigured() + return Default.ConsistentlyWithOffset(offset, actualOrCtx, args...) +} + +/* +StopTrying can be used to signal to Eventually and Consistentlythat they should abort and stop trying. This always results in a failure of the assertion - and the failure message is the content of the StopTrying signal. + +You can send the StopTrying signal by either returning StopTrying("message") as an error from your passed-in function _or_ by calling StopTrying("message").Now() to trigger a panic and end execution. + +You can also wrap StopTrying around an error with `StopTrying("message").Wrap(err)` and can attach additional objects via `StopTrying("message").Attach("description", object). When rendered, the signal will include the wrapped error and any attached objects rendered using Gomega's default formatting. + +Here are a couple of examples. This is how you might use StopTrying() as an error to signal that Eventually should stop: + + playerIndex, numPlayers := 0, 11 + Eventually(func() (string, error) { + if playerIndex == numPlayers { + return "", StopTrying("no more players left") + } + name := client.FetchPlayer(playerIndex) + playerIndex += 1 + return name, nil + }).Should(Equal("Patrick Mahomes")) + +And here's an example where `StopTrying().Now()` is called to halt execution immediately: + + Eventually(func() []string { + names, err := client.FetchAllPlayers() + if err == client.IRRECOVERABLE_ERROR { + StopTrying("Irrecoverable error occurred").Wrap(err).Now() + } + return names + }).Should(ContainElement("Patrick Mahomes")) +*/ +var StopTrying = internal.StopTrying + +/* +TryAgainAfter() allows you to adjust the polling interval for the _next_ iteration of `Eventually` or `Consistently`. Like `StopTrying` you can either return `TryAgainAfter` as an error or trigger it immedieately with `.Now()` + +When `TryAgainAfter(` is triggered `Eventually` and `Consistently` will wait for that duration. If a timeout occurs before the next poll is triggered both `Eventually` and `Consistently` will always fail with the content of the TryAgainAfter message. As with StopTrying you can `.Wrap()` and error and `.Attach()` additional objects to `TryAgainAfter`. +*/ +var TryAgainAfter = internal.TryAgainAfter + +/* +PollingSignalError is the error returned by StopTrying() and TryAgainAfter() +*/ +type PollingSignalError = internal.PollingSignalError + +// SetDefaultEventuallyTimeout sets the default timeout duration for Eventually. Eventually will repeatedly poll your condition until it succeeds, or until this timeout elapses. +func SetDefaultEventuallyTimeout(t time.Duration) { + Default.SetDefaultEventuallyTimeout(t) +} + +// SetDefaultEventuallyPollingInterval sets the default polling interval for Eventually. +func SetDefaultEventuallyPollingInterval(t time.Duration) { + Default.SetDefaultEventuallyPollingInterval(t) +} + +// SetDefaultConsistentlyDuration sets the default duration for Consistently. Consistently will verify that your condition is satisfied for this long. +func SetDefaultConsistentlyDuration(t time.Duration) { + Default.SetDefaultConsistentlyDuration(t) +} + +// SetDefaultConsistentlyPollingInterval sets the default polling interval for Consistently. +func SetDefaultConsistentlyPollingInterval(t time.Duration) { + Default.SetDefaultConsistentlyPollingInterval(t) +} + +// AsyncAssertion is returned by Eventually and Consistently and polls the actual value passed into Eventually against +// the matcher passed to the Should and ShouldNot methods. +// +// Both Should and ShouldNot take a variadic optionalDescription argument. +// This argument allows you to make your failure messages more descriptive. +// If a single argument of type `func() string` is passed, this function will be lazily evaluated if a failure occurs +// and the returned string is used to annotate the failure message. +// Otherwise, this argument is passed on to fmt.Sprintf() and then used to annotate the failure message. +// +// Both Should and ShouldNot return a boolean that is true if the assertion passed and false if it failed. +// +// Example: +// +// Eventually(myChannel).Should(Receive(), "Something should have come down the pipe.") +// Consistently(myChannel).ShouldNot(Receive(), func() string { return "Nothing should have come down the pipe." }) +type AsyncAssertion = types.AsyncAssertion + +// GomegaAsyncAssertion is deprecated in favor of AsyncAssertion, which does not stutter. +type GomegaAsyncAssertion = types.AsyncAssertion + +// Assertion is returned by Ω and Expect and compares the actual value to the matcher +// passed to the Should/ShouldNot and To/ToNot/NotTo methods. +// +// Typically Should/ShouldNot are used with Ω and To/ToNot/NotTo are used with Expect +// though this is not enforced. +// +// All methods take a variadic optionalDescription argument. +// This argument allows you to make your failure messages more descriptive. +// If a single argument of type `func() string` is passed, this function will be lazily evaluated if a failure occurs +// and the returned string is used to annotate the failure message. +// Otherwise, this argument is passed on to fmt.Sprintf() and then used to annotate the failure message. +// +// All methods return a bool that is true if the assertion passed and false if it failed. +// +// Example: +// +// Ω(farm.HasCow()).Should(BeTrue(), "Farm %v should have a cow", farm) +type Assertion = types.Assertion + +// GomegaAssertion is deprecated in favor of Assertion, which does not stutter. +type GomegaAssertion = types.Assertion + +// OmegaMatcher is deprecated in favor of the better-named and better-organized types.GomegaMatcher but sticks around to support existing code that uses it +type OmegaMatcher = types.GomegaMatcher diff --git a/vendor/github.com/onsi/gomega/internal/assertion.go b/vendor/github.com/onsi/gomega/internal/assertion.go new file mode 100644 index 00000000000..08356a610bb --- /dev/null +++ b/vendor/github.com/onsi/gomega/internal/assertion.go @@ -0,0 +1,161 @@ +package internal + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +type Assertion struct { + actuals []interface{} // actual value plus all extra values + actualIndex int // value to pass to the matcher + vet vetinari // the vet to call before calling Gomega matcher + offset int + g *Gomega +} + +// ...obligatory discworld reference, as "vetineer" doesn't sound ... quite right. +type vetinari func(assertion *Assertion, optionalDescription ...interface{}) bool + +func NewAssertion(actualInput interface{}, g *Gomega, offset int, extra ...interface{}) *Assertion { + return &Assertion{ + actuals: append([]interface{}{actualInput}, extra...), + actualIndex: 0, + vet: (*Assertion).vetActuals, + offset: offset, + g: g, + } +} + +func (assertion *Assertion) WithOffset(offset int) types.Assertion { + assertion.offset = offset + return assertion +} + +func (assertion *Assertion) Error() types.Assertion { + return &Assertion{ + actuals: assertion.actuals, + actualIndex: len(assertion.actuals) - 1, + vet: (*Assertion).vetError, + offset: assertion.offset, + g: assertion.g, + } +} + +func (assertion *Assertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + assertion.g.THelper() + vetOptionalDescription("Assertion", optionalDescription...) + return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...) +} + +func (assertion *Assertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + assertion.g.THelper() + vetOptionalDescription("Assertion", optionalDescription...) + return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...) +} + +func (assertion *Assertion) To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + assertion.g.THelper() + vetOptionalDescription("Assertion", optionalDescription...) + return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...) +} + +func (assertion *Assertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + assertion.g.THelper() + vetOptionalDescription("Assertion", optionalDescription...) + return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...) +} + +func (assertion *Assertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + assertion.g.THelper() + vetOptionalDescription("Assertion", optionalDescription...) + return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...) +} + +func (assertion *Assertion) buildDescription(optionalDescription ...interface{}) string { + switch len(optionalDescription) { + case 0: + return "" + case 1: + if describe, ok := optionalDescription[0].(func() string); ok { + return describe() + "\n" + } + } + return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n" +} + +func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool { + actualInput := assertion.actuals[assertion.actualIndex] + matches, err := matcher.Match(actualInput) + assertion.g.THelper() + if err != nil { + description := assertion.buildDescription(optionalDescription...) + assertion.g.Fail(description+err.Error(), 2+assertion.offset) + return false + } + if matches != desiredMatch { + var message string + if desiredMatch { + message = matcher.FailureMessage(actualInput) + } else { + message = matcher.NegatedFailureMessage(actualInput) + } + description := assertion.buildDescription(optionalDescription...) + assertion.g.Fail(description+message, 2+assertion.offset) + return false + } + + return true +} + +// vetActuals vets the actual values, with the (optional) exception of a +// specific value, such as the first value in case non-error assertions, or the +// last value in case of Error()-based assertions. +func (assertion *Assertion) vetActuals(optionalDescription ...interface{}) bool { + success, message := vetActuals(assertion.actuals, assertion.actualIndex) + if success { + return true + } + + description := assertion.buildDescription(optionalDescription...) + assertion.g.THelper() + assertion.g.Fail(description+message, 2+assertion.offset) + return false +} + +// vetError vets the actual values, except for the final error value, in case +// the final error value is non-zero. Otherwise, it doesn't vet the actual +// values, as these are allowed to take on any values unless there is a non-zero +// error value. +func (assertion *Assertion) vetError(optionalDescription ...interface{}) bool { + if err := assertion.actuals[assertion.actualIndex]; err != nil { + // Go error result idiom: all other actual values must be zero values. + return assertion.vetActuals(optionalDescription...) + } + return true +} + +// vetActuals vets a slice of actual values, optionally skipping a particular +// value slice element, such as the first or last value slice element. +func vetActuals(actuals []interface{}, skipIndex int) (bool, string) { + for i, actual := range actuals { + if i == skipIndex { + continue + } + if actual != nil { + zeroValue := reflect.Zero(reflect.TypeOf(actual)).Interface() + if !reflect.DeepEqual(zeroValue, actual) { + var message string + if err, ok := actual.(error); ok { + message = fmt.Sprintf("Unexpected error: %s\n%s", err, format.Object(err, 1)) + } else { + message = fmt.Sprintf("Unexpected non-nil/non-zero argument at index %d:\n\t<%T>: %#v", i, actual, actual) + } + return false, message + } + } + } + return true, "" +} diff --git a/vendor/github.com/onsi/gomega/internal/async_assertion.go b/vendor/github.com/onsi/gomega/internal/async_assertion.go new file mode 100644 index 00000000000..7f622696893 --- /dev/null +++ b/vendor/github.com/onsi/gomega/internal/async_assertion.go @@ -0,0 +1,563 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "reflect" + "runtime" + "sync" + "time" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +var errInterface = reflect.TypeOf((*error)(nil)).Elem() +var gomegaType = reflect.TypeOf((*types.Gomega)(nil)).Elem() +var contextType = reflect.TypeOf(new(context.Context)).Elem() + +type formattedGomegaError interface { + FormattedGomegaError() string +} + +type asyncPolledActualError struct { + message string +} + +func (err *asyncPolledActualError) Error() string { + return err.message +} + +func (err *asyncPolledActualError) FormattedGomegaError() string { + return err.message +} + +type contextWithAttachProgressReporter interface { + AttachProgressReporter(func() string) func() +} + +type asyncGomegaHaltExecutionError struct{} + +func (a asyncGomegaHaltExecutionError) GinkgoRecoverShouldIgnoreThisPanic() {} +func (a asyncGomegaHaltExecutionError) Error() string { + return `An assertion has failed in a goroutine. You should call + + defer GinkgoRecover() + +at the top of the goroutine that caused this panic. This will allow Ginkgo and Gomega to correctly capture and manage this panic.` +} + +type AsyncAssertionType uint + +const ( + AsyncAssertionTypeEventually AsyncAssertionType = iota + AsyncAssertionTypeConsistently +) + +func (at AsyncAssertionType) String() string { + switch at { + case AsyncAssertionTypeEventually: + return "Eventually" + case AsyncAssertionTypeConsistently: + return "Consistently" + } + return "INVALID ASYNC ASSERTION TYPE" +} + +type AsyncAssertion struct { + asyncType AsyncAssertionType + + actualIsFunc bool + actual interface{} + argsToForward []interface{} + + timeoutInterval time.Duration + pollingInterval time.Duration + mustPassRepeatedly int + ctx context.Context + offset int + g *Gomega +} + +func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput interface{}, g *Gomega, timeoutInterval time.Duration, pollingInterval time.Duration, mustPassRepeatedly int, ctx context.Context, offset int) *AsyncAssertion { + out := &AsyncAssertion{ + asyncType: asyncType, + timeoutInterval: timeoutInterval, + pollingInterval: pollingInterval, + mustPassRepeatedly: mustPassRepeatedly, + offset: offset, + ctx: ctx, + g: g, + } + + out.actual = actualInput + if actualInput != nil && reflect.TypeOf(actualInput).Kind() == reflect.Func { + out.actualIsFunc = true + } + + return out +} + +func (assertion *AsyncAssertion) WithOffset(offset int) types.AsyncAssertion { + assertion.offset = offset + return assertion +} + +func (assertion *AsyncAssertion) WithTimeout(interval time.Duration) types.AsyncAssertion { + assertion.timeoutInterval = interval + return assertion +} + +func (assertion *AsyncAssertion) WithPolling(interval time.Duration) types.AsyncAssertion { + assertion.pollingInterval = interval + return assertion +} + +func (assertion *AsyncAssertion) Within(timeout time.Duration) types.AsyncAssertion { + assertion.timeoutInterval = timeout + return assertion +} + +func (assertion *AsyncAssertion) ProbeEvery(interval time.Duration) types.AsyncAssertion { + assertion.pollingInterval = interval + return assertion +} + +func (assertion *AsyncAssertion) WithContext(ctx context.Context) types.AsyncAssertion { + assertion.ctx = ctx + return assertion +} + +func (assertion *AsyncAssertion) WithArguments(argsToForward ...interface{}) types.AsyncAssertion { + assertion.argsToForward = argsToForward + return assertion +} + +func (assertion *AsyncAssertion) MustPassRepeatedly(count int) types.AsyncAssertion { + assertion.mustPassRepeatedly = count + return assertion +} + +func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + assertion.g.THelper() + vetOptionalDescription("Asynchronous assertion", optionalDescription...) + return assertion.match(matcher, true, optionalDescription...) +} + +func (assertion *AsyncAssertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { + assertion.g.THelper() + vetOptionalDescription("Asynchronous assertion", optionalDescription...) + return assertion.match(matcher, false, optionalDescription...) +} + +func (assertion *AsyncAssertion) buildDescription(optionalDescription ...interface{}) string { + switch len(optionalDescription) { + case 0: + return "" + case 1: + if describe, ok := optionalDescription[0].(func() string); ok { + return describe() + "\n" + } + } + return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n" +} + +func (assertion *AsyncAssertion) processReturnValues(values []reflect.Value) (interface{}, error) { + if len(values) == 0 { + return nil, &asyncPolledActualError{ + message: fmt.Sprintf("The function passed to %s did not return any values", assertion.asyncType), + } + } + + actual := values[0].Interface() + if _, ok := AsPollingSignalError(actual); ok { + return actual, actual.(error) + } + + var err error + for i, extraValue := range values[1:] { + extra := extraValue.Interface() + if extra == nil { + continue + } + if _, ok := AsPollingSignalError(extra); ok { + return actual, extra.(error) + } + extraType := reflect.TypeOf(extra) + zero := reflect.Zero(extraType).Interface() + if reflect.DeepEqual(extra, zero) { + continue + } + if i == len(values)-2 && extraType.Implements(errInterface) { + err = extra.(error) + } + if err == nil { + err = &asyncPolledActualError{ + message: fmt.Sprintf("The function passed to %s had an unexpected non-nil/non-zero return value at index %d:\n%s", assertion.asyncType, i+1, format.Object(extra, 1)), + } + } + } + + return actual, err +} + +func (assertion *AsyncAssertion) invalidFunctionError(t reflect.Type) error { + return fmt.Errorf(`The function passed to %s had an invalid signature of %s. Functions passed to %s must either: + + (a) have return values or + (b) take a Gomega interface as their first argument and use that Gomega instance to make assertions. + +You can learn more at https://onsi.github.io/gomega/#eventually +`, assertion.asyncType, t, assertion.asyncType) +} + +func (assertion *AsyncAssertion) noConfiguredContextForFunctionError() error { + return fmt.Errorf(`The function passed to %s requested a context.Context, but no context has been provided. Please pass one in using %s().WithContext(). + +You can learn more at https://onsi.github.io/gomega/#eventually +`, assertion.asyncType, assertion.asyncType) +} + +func (assertion *AsyncAssertion) argumentMismatchError(t reflect.Type, numProvided int) error { + have := "have" + if numProvided == 1 { + have = "has" + } + return fmt.Errorf(`The function passed to %s has signature %s takes %d arguments but %d %s been provided. Please use %s().WithArguments() to pass the corect set of arguments. + +You can learn more at https://onsi.github.io/gomega/#eventually +`, assertion.asyncType, t, t.NumIn(), numProvided, have, assertion.asyncType) +} + +func (assertion *AsyncAssertion) invalidMustPassRepeatedlyError(reason string) error { + return fmt.Errorf(`Invalid use of MustPassRepeatedly with %s %s + +You can learn more at https://onsi.github.io/gomega/#eventually +`, assertion.asyncType, reason) +} + +func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error), error) { + if !assertion.actualIsFunc { + return func() (interface{}, error) { return assertion.actual, nil }, nil + } + actualValue := reflect.ValueOf(assertion.actual) + actualType := reflect.TypeOf(assertion.actual) + numIn, numOut, isVariadic := actualType.NumIn(), actualType.NumOut(), actualType.IsVariadic() + + if numIn == 0 && numOut == 0 { + return nil, assertion.invalidFunctionError(actualType) + } + takesGomega, takesContext := false, false + if numIn > 0 { + takesGomega, takesContext = actualType.In(0).Implements(gomegaType), actualType.In(0).Implements(contextType) + } + if takesGomega && numIn > 1 && actualType.In(1).Implements(contextType) { + takesContext = true + } + if takesContext && len(assertion.argsToForward) > 0 && reflect.TypeOf(assertion.argsToForward[0]).Implements(contextType) { + takesContext = false + } + if !takesGomega && numOut == 0 { + return nil, assertion.invalidFunctionError(actualType) + } + if takesContext && assertion.ctx == nil { + return nil, assertion.noConfiguredContextForFunctionError() + } + + var assertionFailure error + inValues := []reflect.Value{} + if takesGomega { + inValues = append(inValues, reflect.ValueOf(NewGomega(assertion.g.DurationBundle).ConfigureWithFailHandler(func(message string, callerSkip ...int) { + skip := 0 + if len(callerSkip) > 0 { + skip = callerSkip[0] + } + _, file, line, _ := runtime.Caller(skip + 1) + assertionFailure = &asyncPolledActualError{ + message: fmt.Sprintf("The function passed to %s failed at %s:%d with:\n%s", assertion.asyncType, file, line, message), + } + // we throw an asyncGomegaHaltExecutionError so that defer GinkgoRecover() can catch this error if the user makes an assertion in a goroutine + panic(asyncGomegaHaltExecutionError{}) + }))) + } + if takesContext { + inValues = append(inValues, reflect.ValueOf(assertion.ctx)) + } + for _, arg := range assertion.argsToForward { + inValues = append(inValues, reflect.ValueOf(arg)) + } + + if !isVariadic && numIn != len(inValues) { + return nil, assertion.argumentMismatchError(actualType, len(inValues)) + } else if isVariadic && len(inValues) < numIn-1 { + return nil, assertion.argumentMismatchError(actualType, len(inValues)) + } + + if assertion.mustPassRepeatedly != 1 && assertion.asyncType != AsyncAssertionTypeEventually { + return nil, assertion.invalidMustPassRepeatedlyError("it can only be used with Eventually") + } + if assertion.mustPassRepeatedly < 1 { + return nil, assertion.invalidMustPassRepeatedlyError("parameter can't be < 1") + } + + return func() (actual interface{}, err error) { + var values []reflect.Value + assertionFailure = nil + defer func() { + if numOut == 0 && takesGomega { + actual = assertionFailure + } else { + actual, err = assertion.processReturnValues(values) + _, isAsyncError := AsPollingSignalError(err) + if assertionFailure != nil && !isAsyncError { + err = assertionFailure + } + } + if e := recover(); e != nil { + if _, isAsyncError := AsPollingSignalError(e); isAsyncError { + err = e.(error) + } else if assertionFailure == nil { + panic(e) + } + } + }() + values = actualValue.Call(inValues) + return + }, nil +} + +func (assertion *AsyncAssertion) afterTimeout() <-chan time.Time { + if assertion.timeoutInterval >= 0 { + return time.After(assertion.timeoutInterval) + } + + if assertion.asyncType == AsyncAssertionTypeConsistently { + return time.After(assertion.g.DurationBundle.ConsistentlyDuration) + } else { + if assertion.ctx == nil { + return time.After(assertion.g.DurationBundle.EventuallyTimeout) + } else { + return nil + } + } +} + +func (assertion *AsyncAssertion) afterPolling() <-chan time.Time { + if assertion.pollingInterval >= 0 { + return time.After(assertion.pollingInterval) + } + if assertion.asyncType == AsyncAssertionTypeConsistently { + return time.After(assertion.g.DurationBundle.ConsistentlyPollingInterval) + } else { + return time.After(assertion.g.DurationBundle.EventuallyPollingInterval) + } +} + +func (assertion *AsyncAssertion) matcherSaysStopTrying(matcher types.GomegaMatcher, value interface{}) bool { + if assertion.actualIsFunc || types.MatchMayChangeInTheFuture(matcher, value) { + return false + } + return true +} + +func (assertion *AsyncAssertion) pollMatcher(matcher types.GomegaMatcher, value interface{}) (matches bool, err error) { + defer func() { + if e := recover(); e != nil { + if _, isAsyncError := AsPollingSignalError(e); isAsyncError { + err = e.(error) + } else { + panic(e) + } + } + }() + + matches, err = matcher.Match(value) + + return +} + +func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool { + timer := time.Now() + timeout := assertion.afterTimeout() + lock := sync.Mutex{} + + var matches, hasLastValidActual bool + var actual, lastValidActual interface{} + var actualErr, matcherErr error + var oracleMatcherSaysStop bool + + assertion.g.THelper() + + pollActual, buildActualPollerErr := assertion.buildActualPoller() + if buildActualPollerErr != nil { + assertion.g.Fail(buildActualPollerErr.Error(), 2+assertion.offset) + return false + } + + actual, actualErr = pollActual() + if actualErr == nil { + lastValidActual = actual + hasLastValidActual = true + oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, actual) + matches, matcherErr = assertion.pollMatcher(matcher, actual) + } + + renderError := func(preamble string, err error) string { + message := "" + if pollingSignalErr, ok := AsPollingSignalError(err); ok { + message = err.Error() + for _, attachment := range pollingSignalErr.Attachments { + message += fmt.Sprintf("\n%s:\n", attachment.Description) + message += format.Object(attachment.Object, 1) + } + } else { + message = preamble + "\n" + err.Error() + "\n" + format.Object(err, 1) + } + return message + } + + messageGenerator := func() string { + // can be called out of band by Ginkgo if the user requests a progress report + lock.Lock() + defer lock.Unlock() + message := "" + + if actualErr == nil { + if matcherErr == nil { + if desiredMatch { + message += matcher.FailureMessage(actual) + } else { + message += matcher.NegatedFailureMessage(actual) + } + } else { + var fgErr formattedGomegaError + if errors.As(actualErr, &fgErr) { + message += fgErr.FormattedGomegaError() + "\n" + } else { + message += renderError(fmt.Sprintf("The matcher passed to %s returned the following error:", assertion.asyncType), matcherErr) + } + } + } else { + var fgErr formattedGomegaError + if errors.As(actualErr, &fgErr) { + message += fgErr.FormattedGomegaError() + "\n" + } else { + message += renderError(fmt.Sprintf("The function passed to %s returned the following error:", assertion.asyncType), actualErr) + } + if hasLastValidActual { + message += fmt.Sprintf("\nAt one point, however, the function did return successfully.\nYet, %s failed because", assertion.asyncType) + _, e := matcher.Match(lastValidActual) + if e != nil { + message += renderError(" the matcher returned the following error:", e) + } else { + message += " the matcher was not satisfied:\n" + if desiredMatch { + message += matcher.FailureMessage(lastValidActual) + } else { + message += matcher.NegatedFailureMessage(lastValidActual) + } + } + } + } + + description := assertion.buildDescription(optionalDescription...) + return fmt.Sprintf("%s%s", description, message) + } + + fail := func(preamble string) { + assertion.g.THelper() + assertion.g.Fail(fmt.Sprintf("%s after %.3fs.\n%s", preamble, time.Since(timer).Seconds(), messageGenerator()), 3+assertion.offset) + } + + var contextDone <-chan struct{} + if assertion.ctx != nil { + contextDone = assertion.ctx.Done() + if v, ok := assertion.ctx.Value("GINKGO_SPEC_CONTEXT").(contextWithAttachProgressReporter); ok { + detach := v.AttachProgressReporter(messageGenerator) + defer detach() + } + } + + // Used to count the number of times in a row a step passed + passedRepeatedlyCount := 0 + for { + var nextPoll <-chan time.Time = nil + var isTryAgainAfterError = false + + for _, err := range []error{actualErr, matcherErr} { + if pollingSignalErr, ok := AsPollingSignalError(err); ok { + if pollingSignalErr.IsStopTrying() { + fail("Told to stop trying") + return false + } + if pollingSignalErr.IsTryAgainAfter() { + nextPoll = time.After(pollingSignalErr.TryAgainDuration()) + isTryAgainAfterError = true + } + } + } + + if actualErr == nil && matcherErr == nil && matches == desiredMatch { + if assertion.asyncType == AsyncAssertionTypeEventually { + passedRepeatedlyCount += 1 + if passedRepeatedlyCount == assertion.mustPassRepeatedly { + return true + } + } + } else if !isTryAgainAfterError { + if assertion.asyncType == AsyncAssertionTypeConsistently { + fail("Failed") + return false + } + // Reset the consecutive pass count + passedRepeatedlyCount = 0 + } + + if oracleMatcherSaysStop { + if assertion.asyncType == AsyncAssertionTypeEventually { + fail("No future change is possible. Bailing out early") + return false + } else { + return true + } + } + + if nextPoll == nil { + nextPoll = assertion.afterPolling() + } + + select { + case <-nextPoll: + a, e := pollActual() + lock.Lock() + actual, actualErr = a, e + lock.Unlock() + if actualErr == nil { + lock.Lock() + lastValidActual = actual + hasLastValidActual = true + lock.Unlock() + oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, actual) + m, e := assertion.pollMatcher(matcher, actual) + lock.Lock() + matches, matcherErr = m, e + lock.Unlock() + } + case <-contextDone: + fail("Context was cancelled") + return false + case <-timeout: + if assertion.asyncType == AsyncAssertionTypeEventually { + fail("Timed out") + return false + } else { + if isTryAgainAfterError { + fail("Timed out while waiting on TryAgainAfter") + return false + } + return true + } + } + } +} diff --git a/vendor/github.com/onsi/gomega/internal/duration_bundle.go b/vendor/github.com/onsi/gomega/internal/duration_bundle.go new file mode 100644 index 00000000000..6e0d90d3a1a --- /dev/null +++ b/vendor/github.com/onsi/gomega/internal/duration_bundle.go @@ -0,0 +1,71 @@ +package internal + +import ( + "fmt" + "os" + "reflect" + "time" +) + +type DurationBundle struct { + EventuallyTimeout time.Duration + EventuallyPollingInterval time.Duration + ConsistentlyDuration time.Duration + ConsistentlyPollingInterval time.Duration +} + +const ( + EventuallyTimeoutEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_TIMEOUT" + EventuallyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_POLLING_INTERVAL" + + ConsistentlyDurationEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_DURATION" + ConsistentlyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_POLLING_INTERVAL" +) + +func FetchDefaultDurationBundle() DurationBundle { + return DurationBundle{ + EventuallyTimeout: durationFromEnv(EventuallyTimeoutEnvVarName, time.Second), + EventuallyPollingInterval: durationFromEnv(EventuallyPollingIntervalEnvVarName, 10*time.Millisecond), + + ConsistentlyDuration: durationFromEnv(ConsistentlyDurationEnvVarName, 100*time.Millisecond), + ConsistentlyPollingInterval: durationFromEnv(ConsistentlyPollingIntervalEnvVarName, 10*time.Millisecond), + } +} + +func durationFromEnv(key string, defaultDuration time.Duration) time.Duration { + value := os.Getenv(key) + if value == "" { + return defaultDuration + } + duration, err := time.ParseDuration(value) + if err != nil { + panic(fmt.Sprintf("Expected a duration when using %s! Parse error %v", key, err)) + } + return duration +} + +func toDuration(input interface{}) (time.Duration, error) { + duration, ok := input.(time.Duration) + if ok { + return duration, nil + } + + value := reflect.ValueOf(input) + kind := reflect.TypeOf(input).Kind() + + if reflect.Int <= kind && kind <= reflect.Int64 { + return time.Duration(value.Int()) * time.Second, nil + } else if reflect.Uint <= kind && kind <= reflect.Uint64 { + return time.Duration(value.Uint()) * time.Second, nil + } else if reflect.Float32 <= kind && kind <= reflect.Float64 { + return time.Duration(value.Float() * float64(time.Second)), nil + } else if reflect.String == kind { + duration, err := time.ParseDuration(value.String()) + if err != nil { + return 0, fmt.Errorf("%#v is not a valid parsable duration string: %w", input, err) + } + return duration, nil + } + + return 0, fmt.Errorf("%#v is not a valid interval. Must be a time.Duration, a parsable duration string, or a number.", input) +} diff --git a/vendor/github.com/onsi/gomega/internal/gomega.go b/vendor/github.com/onsi/gomega/internal/gomega.go new file mode 100644 index 00000000000..de1f4f336e8 --- /dev/null +++ b/vendor/github.com/onsi/gomega/internal/gomega.go @@ -0,0 +1,129 @@ +package internal + +import ( + "context" + "time" + + "github.com/onsi/gomega/types" +) + +type Gomega struct { + Fail types.GomegaFailHandler + THelper func() + DurationBundle DurationBundle +} + +func NewGomega(bundle DurationBundle) *Gomega { + return &Gomega{ + Fail: nil, + THelper: nil, + DurationBundle: bundle, + } +} + +func (g *Gomega) IsConfigured() bool { + return g.Fail != nil && g.THelper != nil +} + +func (g *Gomega) ConfigureWithFailHandler(fail types.GomegaFailHandler) *Gomega { + g.Fail = fail + g.THelper = func() {} + return g +} + +func (g *Gomega) ConfigureWithT(t types.GomegaTestingT) *Gomega { + g.Fail = func(message string, _ ...int) { + t.Helper() + t.Fatalf("\n%s", message) + } + g.THelper = t.Helper + return g +} + +func (g *Gomega) Ω(actual interface{}, extra ...interface{}) types.Assertion { + return g.ExpectWithOffset(0, actual, extra...) +} + +func (g *Gomega) Expect(actual interface{}, extra ...interface{}) types.Assertion { + return g.ExpectWithOffset(0, actual, extra...) +} + +func (g *Gomega) ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) types.Assertion { + return NewAssertion(actual, g, offset, extra...) +} + +func (g *Gomega) Eventually(actualOrCtx interface{}, args ...interface{}) types.AsyncAssertion { + return g.makeAsyncAssertion(AsyncAssertionTypeEventually, 0, actualOrCtx, args...) +} + +func (g *Gomega) EventuallyWithOffset(offset int, actualOrCtx interface{}, args ...interface{}) types.AsyncAssertion { + return g.makeAsyncAssertion(AsyncAssertionTypeEventually, offset, actualOrCtx, args...) +} + +func (g *Gomega) Consistently(actualOrCtx interface{}, args ...interface{}) types.AsyncAssertion { + return g.makeAsyncAssertion(AsyncAssertionTypeConsistently, 0, actualOrCtx, args...) +} + +func (g *Gomega) ConsistentlyWithOffset(offset int, actualOrCtx interface{}, args ...interface{}) types.AsyncAssertion { + return g.makeAsyncAssertion(AsyncAssertionTypeConsistently, offset, actualOrCtx, args...) +} + +func (g *Gomega) makeAsyncAssertion(asyncAssertionType AsyncAssertionType, offset int, actualOrCtx interface{}, args ...interface{}) types.AsyncAssertion { + baseOffset := 3 + timeoutInterval := -time.Duration(1) + pollingInterval := -time.Duration(1) + intervals := []interface{}{} + var ctx context.Context + + actual := actualOrCtx + startingIndex := 0 + if _, isCtx := actualOrCtx.(context.Context); isCtx && len(args) > 0 { + // the first argument is a context, we should accept it as the context _only if_ it is **not** the only argumnent **and** the second argument is not a parseable duration + // this is due to an unfortunate ambiguity in early version of Gomega in which multi-type durations are allowed after the actual + if _, err := toDuration(args[0]); err != nil { + ctx = actualOrCtx.(context.Context) + actual = args[0] + startingIndex = 1 + } + } + + for _, arg := range args[startingIndex:] { + switch v := arg.(type) { + case context.Context: + ctx = v + default: + intervals = append(intervals, arg) + } + } + var err error + if len(intervals) > 0 { + timeoutInterval, err = toDuration(intervals[0]) + if err != nil { + g.Fail(err.Error(), offset+baseOffset) + } + } + if len(intervals) > 1 { + pollingInterval, err = toDuration(intervals[1]) + if err != nil { + g.Fail(err.Error(), offset+baseOffset) + } + } + + return NewAsyncAssertion(asyncAssertionType, actual, g, timeoutInterval, pollingInterval, 1, ctx, offset) +} + +func (g *Gomega) SetDefaultEventuallyTimeout(t time.Duration) { + g.DurationBundle.EventuallyTimeout = t +} + +func (g *Gomega) SetDefaultEventuallyPollingInterval(t time.Duration) { + g.DurationBundle.EventuallyPollingInterval = t +} + +func (g *Gomega) SetDefaultConsistentlyDuration(t time.Duration) { + g.DurationBundle.ConsistentlyDuration = t +} + +func (g *Gomega) SetDefaultConsistentlyPollingInterval(t time.Duration) { + g.DurationBundle.ConsistentlyPollingInterval = t +} diff --git a/vendor/github.com/onsi/gomega/internal/gutil/post_ioutil.go b/vendor/github.com/onsi/gomega/internal/gutil/post_ioutil.go new file mode 100644 index 00000000000..6864055a5a7 --- /dev/null +++ b/vendor/github.com/onsi/gomega/internal/gutil/post_ioutil.go @@ -0,0 +1,48 @@ +//go:build go1.16 +// +build go1.16 + +// Package gutil is a replacement for ioutil, which should not be used in new +// code as of Go 1.16. With Go 1.16 and higher, this implementation +// uses the ioutil replacement functions in "io" and "os" with some +// Gomega specifics. This means that we should not get deprecation warnings +// for ioutil when they are added. +package gutil + +import ( + "io" + "os" +) + +func NopCloser(r io.Reader) io.ReadCloser { + return io.NopCloser(r) +} + +func ReadAll(r io.Reader) ([]byte, error) { + return io.ReadAll(r) +} + +func ReadDir(dirname string) ([]string, error) { + entries, err := os.ReadDir(dirname) + if err != nil { + return nil, err + } + + var names []string + for _, entry := range entries { + names = append(names, entry.Name()) + } + + return names, nil +} + +func ReadFile(filename string) ([]byte, error) { + return os.ReadFile(filename) +} + +func MkdirTemp(dir, pattern string) (string, error) { + return os.MkdirTemp(dir, pattern) +} + +func WriteFile(filename string, data []byte) error { + return os.WriteFile(filename, data, 0644) +} diff --git a/vendor/github.com/onsi/gomega/internal/gutil/using_ioutil.go b/vendor/github.com/onsi/gomega/internal/gutil/using_ioutil.go new file mode 100644 index 00000000000..5c0ce1ee3da --- /dev/null +++ b/vendor/github.com/onsi/gomega/internal/gutil/using_ioutil.go @@ -0,0 +1,47 @@ +//go:build !go1.16 +// +build !go1.16 + +// Package gutil is a replacement for ioutil, which should not be used in new +// code as of Go 1.16. With Go 1.15 and lower, this implementation +// uses the ioutil functions, meaning that although Gomega is not officially +// supported on these versions, it is still likely to work. +package gutil + +import ( + "io" + "io/ioutil" +) + +func NopCloser(r io.Reader) io.ReadCloser { + return ioutil.NopCloser(r) +} + +func ReadAll(r io.Reader) ([]byte, error) { + return ioutil.ReadAll(r) +} + +func ReadDir(dirname string) ([]string, error) { + files, err := ioutil.ReadDir(dirname) + if err != nil { + return nil, err + } + + var names []string + for _, file := range files { + names = append(names, file.Name()) + } + + return names, nil +} + +func ReadFile(filename string) ([]byte, error) { + return ioutil.ReadFile(filename) +} + +func MkdirTemp(dir, pattern string) (string, error) { + return ioutil.TempDir(dir, pattern) +} + +func WriteFile(filename string, data []byte) error { + return ioutil.WriteFile(filename, data, 0644) +} diff --git a/vendor/github.com/onsi/gomega/internal/polling_signal_error.go b/vendor/github.com/onsi/gomega/internal/polling_signal_error.go new file mode 100644 index 00000000000..83b04b1a4c9 --- /dev/null +++ b/vendor/github.com/onsi/gomega/internal/polling_signal_error.go @@ -0,0 +1,106 @@ +package internal + +import ( + "errors" + "fmt" + "time" +) + +type PollingSignalErrorType int + +const ( + PollingSignalErrorTypeStopTrying PollingSignalErrorType = iota + PollingSignalErrorTypeTryAgainAfter +) + +type PollingSignalError interface { + error + Wrap(err error) PollingSignalError + Attach(description string, obj any) PollingSignalError + Now() +} + +var StopTrying = func(message string) PollingSignalError { + return &PollingSignalErrorImpl{ + message: message, + pollingSignalErrorType: PollingSignalErrorTypeStopTrying, + } +} + +var TryAgainAfter = func(duration time.Duration) PollingSignalError { + return &PollingSignalErrorImpl{ + message: fmt.Sprintf("told to try again after %s", duration), + duration: duration, + pollingSignalErrorType: PollingSignalErrorTypeTryAgainAfter, + } +} + +type PollingSignalErrorAttachment struct { + Description string + Object any +} + +type PollingSignalErrorImpl struct { + message string + wrappedErr error + pollingSignalErrorType PollingSignalErrorType + duration time.Duration + Attachments []PollingSignalErrorAttachment +} + +func (s *PollingSignalErrorImpl) Wrap(err error) PollingSignalError { + s.wrappedErr = err + return s +} + +func (s *PollingSignalErrorImpl) Attach(description string, obj any) PollingSignalError { + s.Attachments = append(s.Attachments, PollingSignalErrorAttachment{description, obj}) + return s +} + +func (s *PollingSignalErrorImpl) Error() string { + if s.wrappedErr == nil { + return s.message + } else { + return s.message + ": " + s.wrappedErr.Error() + } +} + +func (s *PollingSignalErrorImpl) Unwrap() error { + if s == nil { + return nil + } + return s.wrappedErr +} + +func (s *PollingSignalErrorImpl) Now() { + panic(s) +} + +func (s *PollingSignalErrorImpl) IsStopTrying() bool { + return s.pollingSignalErrorType == PollingSignalErrorTypeStopTrying +} + +func (s *PollingSignalErrorImpl) IsTryAgainAfter() bool { + return s.pollingSignalErrorType == PollingSignalErrorTypeTryAgainAfter +} + +func (s *PollingSignalErrorImpl) TryAgainDuration() time.Duration { + return s.duration +} + +func AsPollingSignalError(actual interface{}) (*PollingSignalErrorImpl, bool) { + if actual == nil { + return nil, false + } + if actualErr, ok := actual.(error); ok { + var target *PollingSignalErrorImpl + if errors.As(actualErr, &target) { + return target, true + } else { + return nil, false + } + } + + return nil, false +} diff --git a/vendor/github.com/onsi/gomega/internal/vetoptdesc.go b/vendor/github.com/onsi/gomega/internal/vetoptdesc.go new file mode 100644 index 00000000000..f2958764171 --- /dev/null +++ b/vendor/github.com/onsi/gomega/internal/vetoptdesc.go @@ -0,0 +1,22 @@ +package internal + +import ( + "fmt" + + "github.com/onsi/gomega/types" +) + +// vetOptionalDescription vets the optional description args: if it finds any +// Gomega matcher at the beginning it panics. This allows for rendering Gomega +// matchers as part of an optional Description, as long as they're not in the +// first slot. +func vetOptionalDescription(assertion string, optionalDescription ...interface{}) { + if len(optionalDescription) == 0 { + return + } + if _, isGomegaMatcher := optionalDescription[0].(types.GomegaMatcher); isGomegaMatcher { + panic(fmt.Sprintf("%s has a GomegaMatcher as the first element of optionalDescription.\n\t"+ + "Do you mean to use And/Or/SatisfyAll/SatisfyAny to combine multiple matchers?", + assertion)) + } +} diff --git a/vendor/github.com/onsi/gomega/matchers.go b/vendor/github.com/onsi/gomega/matchers.go new file mode 100644 index 00000000000..857586a9108 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers.go @@ -0,0 +1,641 @@ +package gomega + +import ( + "time" + + "github.com/google/go-cmp/cmp" + "github.com/onsi/gomega/matchers" + "github.com/onsi/gomega/types" +) + +// Equal uses reflect.DeepEqual to compare actual with expected. Equal is strict about +// types when performing comparisons. +// It is an error for both actual and expected to be nil. Use BeNil() instead. +func Equal(expected interface{}) types.GomegaMatcher { + return &matchers.EqualMatcher{ + Expected: expected, + } +} + +// BeEquivalentTo is more lax than Equal, allowing equality between different types. +// This is done by converting actual to have the type of expected before +// attempting equality with reflect.DeepEqual. +// It is an error for actual and expected to be nil. Use BeNil() instead. +func BeEquivalentTo(expected interface{}) types.GomegaMatcher { + return &matchers.BeEquivalentToMatcher{ + Expected: expected, + } +} + +// BeComparableTo uses gocmp.Equal from github.com/google/go-cmp (instead of reflect.DeepEqual) to perform a deep comparison. +// You can pass cmp.Option as options. +// It is an error for actual and expected to be nil. Use BeNil() instead. +func BeComparableTo(expected interface{}, opts ...cmp.Option) types.GomegaMatcher { + return &matchers.BeComparableToMatcher{ + Expected: expected, + Options: opts, + } +} + +// BeIdenticalTo uses the == operator to compare actual with expected. +// BeIdenticalTo is strict about types when performing comparisons. +// It is an error for both actual and expected to be nil. Use BeNil() instead. +func BeIdenticalTo(expected interface{}) types.GomegaMatcher { + return &matchers.BeIdenticalToMatcher{ + Expected: expected, + } +} + +// BeNil succeeds if actual is nil +func BeNil() types.GomegaMatcher { + return &matchers.BeNilMatcher{} +} + +// BeTrue succeeds if actual is true +func BeTrue() types.GomegaMatcher { + return &matchers.BeTrueMatcher{} +} + +// BeFalse succeeds if actual is false +func BeFalse() types.GomegaMatcher { + return &matchers.BeFalseMatcher{} +} + +// HaveOccurred succeeds if actual is a non-nil error +// The typical Go error checking pattern looks like: +// +// err := SomethingThatMightFail() +// Expect(err).ShouldNot(HaveOccurred()) +func HaveOccurred() types.GomegaMatcher { + return &matchers.HaveOccurredMatcher{} +} + +// Succeed passes if actual is a nil error +// Succeed is intended to be used with functions that return a single error value. Instead of +// +// err := SomethingThatMightFail() +// Expect(err).ShouldNot(HaveOccurred()) +// +// You can write: +// +// Expect(SomethingThatMightFail()).Should(Succeed()) +// +// It is a mistake to use Succeed with a function that has multiple return values. Gomega's Ω and Expect +// functions automatically trigger failure if any return values after the first return value are non-zero/non-nil. +// This means that Ω(MultiReturnFunc()).ShouldNot(Succeed()) can never pass. +func Succeed() types.GomegaMatcher { + return &matchers.SucceedMatcher{} +} + +// MatchError succeeds if actual is a non-nil error that matches the passed in string/error. +// +// These are valid use-cases: +// +// Expect(err).Should(MatchError("an error")) //asserts that err.Error() == "an error" +// Expect(err).Should(MatchError(SomeError)) //asserts that err == SomeError (via reflect.DeepEqual) +// +// It is an error for err to be nil or an object that does not implement the Error interface +func MatchError(expected interface{}) types.GomegaMatcher { + return &matchers.MatchErrorMatcher{ + Expected: expected, + } +} + +// BeClosed succeeds if actual is a closed channel. +// It is an error to pass a non-channel to BeClosed, it is also an error to pass nil +// +// In order to check whether or not the channel is closed, Gomega must try to read from the channel +// (even in the `ShouldNot(BeClosed())` case). You should keep this in mind if you wish to make subsequent assertions about +// values coming down the channel. +// +// Also, if you are testing that a *buffered* channel is closed you must first read all values out of the channel before +// asserting that it is closed (it is not possible to detect that a buffered-channel has been closed until all its buffered values are read). +// +// Finally, as a corollary: it is an error to check whether or not a send-only channel is closed. +func BeClosed() types.GomegaMatcher { + return &matchers.BeClosedMatcher{} +} + +// Receive succeeds if there is a value to be received on actual. +// Actual must be a channel (and cannot be a send-only channel) -- anything else is an error. +// +// Receive returns immediately and never blocks: +// +// - If there is nothing on the channel `c` then Expect(c).Should(Receive()) will fail and Ω(c).ShouldNot(Receive()) will pass. +// +// - If the channel `c` is closed then Expect(c).Should(Receive()) will fail and Ω(c).ShouldNot(Receive()) will pass. +// +// - If there is something on the channel `c` ready to be read, then Expect(c).Should(Receive()) will pass and Ω(c).ShouldNot(Receive()) will fail. +// +// If you have a go-routine running in the background that will write to channel `c` you can: +// +// Eventually(c).Should(Receive()) +// +// This will timeout if nothing gets sent to `c` (you can modify the timeout interval as you normally do with `Eventually`) +// +// A similar use-case is to assert that no go-routine writes to a channel (for a period of time). You can do this with `Consistently`: +// +// Consistently(c).ShouldNot(Receive()) +// +// You can pass `Receive` a matcher. If you do so, it will match the received object against the matcher. For example: +// +// Expect(c).Should(Receive(Equal("foo"))) +// +// When given a matcher, `Receive` will always fail if there is nothing to be received on the channel. +// +// Passing Receive a matcher is especially useful when paired with Eventually: +// +// Eventually(c).Should(Receive(ContainSubstring("bar"))) +// +// will repeatedly attempt to pull values out of `c` until a value matching "bar" is received. +// +// Finally, if you want to have a reference to the value *sent* to the channel you can pass the `Receive` matcher a pointer to a variable of the appropriate type: +// +// var myThing thing +// Eventually(thingChan).Should(Receive(&myThing)) +// Expect(myThing.Sprocket).Should(Equal("foo")) +// Expect(myThing.IsValid()).Should(BeTrue()) +func Receive(args ...interface{}) types.GomegaMatcher { + var arg interface{} + if len(args) > 0 { + arg = args[0] + } + + return &matchers.ReceiveMatcher{ + Arg: arg, + } +} + +// BeSent succeeds if a value can be sent to actual. +// Actual must be a channel (and cannot be a receive-only channel) that can sent the type of the value passed into BeSent -- anything else is an error. +// In addition, actual must not be closed. +// +// BeSent never blocks: +// +// - If the channel `c` is not ready to receive then Expect(c).Should(BeSent("foo")) will fail immediately +// - If the channel `c` is eventually ready to receive then Eventually(c).Should(BeSent("foo")) will succeed.. presuming the channel becomes ready to receive before Eventually's timeout +// - If the channel `c` is closed then Expect(c).Should(BeSent("foo")) and Ω(c).ShouldNot(BeSent("foo")) will both fail immediately +// +// Of course, the value is actually sent to the channel. The point of `BeSent` is less to make an assertion about the availability of the channel (which is typically an implementation detail that your test should not be concerned with). +// Rather, the point of `BeSent` is to make it possible to easily and expressively write tests that can timeout on blocked channel sends. +func BeSent(arg interface{}) types.GomegaMatcher { + return &matchers.BeSentMatcher{ + Arg: arg, + } +} + +// MatchRegexp succeeds if actual is a string or stringer that matches the +// passed-in regexp. Optional arguments can be provided to construct a regexp +// via fmt.Sprintf(). +func MatchRegexp(regexp string, args ...interface{}) types.GomegaMatcher { + return &matchers.MatchRegexpMatcher{ + Regexp: regexp, + Args: args, + } +} + +// ContainSubstring succeeds if actual is a string or stringer that contains the +// passed-in substring. Optional arguments can be provided to construct the substring +// via fmt.Sprintf(). +func ContainSubstring(substr string, args ...interface{}) types.GomegaMatcher { + return &matchers.ContainSubstringMatcher{ + Substr: substr, + Args: args, + } +} + +// HavePrefix succeeds if actual is a string or stringer that contains the +// passed-in string as a prefix. Optional arguments can be provided to construct +// via fmt.Sprintf(). +func HavePrefix(prefix string, args ...interface{}) types.GomegaMatcher { + return &matchers.HavePrefixMatcher{ + Prefix: prefix, + Args: args, + } +} + +// HaveSuffix succeeds if actual is a string or stringer that contains the +// passed-in string as a suffix. Optional arguments can be provided to construct +// via fmt.Sprintf(). +func HaveSuffix(suffix string, args ...interface{}) types.GomegaMatcher { + return &matchers.HaveSuffixMatcher{ + Suffix: suffix, + Args: args, + } +} + +// MatchJSON succeeds if actual is a string or stringer of JSON that matches +// the expected JSON. The JSONs are decoded and the resulting objects are compared via +// reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter. +func MatchJSON(json interface{}) types.GomegaMatcher { + return &matchers.MatchJSONMatcher{ + JSONToMatch: json, + } +} + +// MatchXML succeeds if actual is a string or stringer of XML that matches +// the expected XML. The XMLs are decoded and the resulting objects are compared via +// reflect.DeepEqual so things like whitespaces shouldn't matter. +func MatchXML(xml interface{}) types.GomegaMatcher { + return &matchers.MatchXMLMatcher{ + XMLToMatch: xml, + } +} + +// MatchYAML succeeds if actual is a string or stringer of YAML that matches +// the expected YAML. The YAML's are decoded and the resulting objects are compared via +// reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter. +func MatchYAML(yaml interface{}) types.GomegaMatcher { + return &matchers.MatchYAMLMatcher{ + YAMLToMatch: yaml, + } +} + +// BeEmpty succeeds if actual is empty. Actual must be of type string, array, map, chan, or slice. +func BeEmpty() types.GomegaMatcher { + return &matchers.BeEmptyMatcher{} +} + +// HaveLen succeeds if actual has the passed-in length. Actual must be of type string, array, map, chan, or slice. +func HaveLen(count int) types.GomegaMatcher { + return &matchers.HaveLenMatcher{ + Count: count, + } +} + +// HaveCap succeeds if actual has the passed-in capacity. Actual must be of type array, chan, or slice. +func HaveCap(count int) types.GomegaMatcher { + return &matchers.HaveCapMatcher{ + Count: count, + } +} + +// BeZero succeeds if actual is the zero value for its type or if actual is nil. +func BeZero() types.GomegaMatcher { + return &matchers.BeZeroMatcher{} +} + +// ContainElement succeeds if actual contains the passed in element. By default +// ContainElement() uses Equal() to perform the match, however a matcher can be +// passed in instead: +// +// Expect([]string{"Foo", "FooBar"}).Should(ContainElement(ContainSubstring("Bar"))) +// +// Actual must be an array, slice or map. For maps, ContainElement searches +// through the map's values. +// +// If you want to have a copy of the matching element(s) found you can pass a +// pointer to a variable of the appropriate type. If the variable isn't a slice +// or map, then exactly one match will be expected and returned. If the variable +// is a slice or map, then at least one match is expected and all matches will be +// stored in the variable. +// +// var findings []string +// Expect([]string{"Foo", "FooBar"}).Should(ContainElement(ContainSubString("Bar", &findings))) +func ContainElement(element interface{}, result ...interface{}) types.GomegaMatcher { + return &matchers.ContainElementMatcher{ + Element: element, + Result: result, + } +} + +// BeElementOf succeeds if actual is contained in the passed in elements. +// BeElementOf() always uses Equal() to perform the match. +// When the passed in elements are comprised of a single element that is either an Array or Slice, BeElementOf() behaves +// as the reverse of ContainElement() that operates with Equal() to perform the match. +// +// Expect(2).Should(BeElementOf([]int{1, 2})) +// Expect(2).Should(BeElementOf([2]int{1, 2})) +// +// Otherwise, BeElementOf() provides a syntactic sugar for Or(Equal(_), Equal(_), ...): +// +// Expect(2).Should(BeElementOf(1, 2)) +// +// Actual must be typed. +func BeElementOf(elements ...interface{}) types.GomegaMatcher { + return &matchers.BeElementOfMatcher{ + Elements: elements, + } +} + +// BeKeyOf succeeds if actual is contained in the keys of the passed in map. +// BeKeyOf() always uses Equal() to perform the match between actual and the map keys. +// +// Expect("foo").Should(BeKeyOf(map[string]bool{"foo": true, "bar": false})) +func BeKeyOf(element interface{}) types.GomegaMatcher { + return &matchers.BeKeyOfMatcher{ + Map: element, + } +} + +// ConsistOf succeeds if actual contains precisely the elements passed into the matcher. The ordering of the elements does not matter. +// By default ConsistOf() uses Equal() to match the elements, however custom matchers can be passed in instead. Here are some examples: +// +// Expect([]string{"Foo", "FooBar"}).Should(ConsistOf("FooBar", "Foo")) +// Expect([]string{"Foo", "FooBar"}).Should(ConsistOf(ContainSubstring("Bar"), "Foo")) +// Expect([]string{"Foo", "FooBar"}).Should(ConsistOf(ContainSubstring("Foo"), ContainSubstring("Foo"))) +// +// Actual must be an array, slice or map. For maps, ConsistOf matches against the map's values. +// +// You typically pass variadic arguments to ConsistOf (as in the examples above). However, if you need to pass in a slice you can provided that it +// is the only element passed in to ConsistOf: +// +// Expect([]string{"Foo", "FooBar"}).Should(ConsistOf([]string{"FooBar", "Foo"})) +// +// Note that Go's type system does not allow you to write this as ConsistOf([]string{"FooBar", "Foo"}...) as []string and []interface{} are different types - hence the need for this special rule. +func ConsistOf(elements ...interface{}) types.GomegaMatcher { + return &matchers.ConsistOfMatcher{ + Elements: elements, + } +} + +// ContainElements succeeds if actual contains the passed in elements. The ordering of the elements does not matter. +// By default ContainElements() uses Equal() to match the elements, however custom matchers can be passed in instead. Here are some examples: +// +// Expect([]string{"Foo", "FooBar"}).Should(ContainElements("FooBar")) +// Expect([]string{"Foo", "FooBar"}).Should(ContainElements(ContainSubstring("Bar"), "Foo")) +// +// Actual must be an array, slice or map. +// For maps, ContainElements searches through the map's values. +func ContainElements(elements ...interface{}) types.GomegaMatcher { + return &matchers.ContainElementsMatcher{ + Elements: elements, + } +} + +// HaveEach succeeds if actual solely contains elements that match the passed in element. +// Please note that if actual is empty, HaveEach always will succeed. +// By default HaveEach() uses Equal() to perform the match, however a +// matcher can be passed in instead: +// +// Expect([]string{"Foo", "FooBar"}).Should(HaveEach(ContainSubstring("Foo"))) +// +// Actual must be an array, slice or map. +// For maps, HaveEach searches through the map's values. +func HaveEach(element interface{}) types.GomegaMatcher { + return &matchers.HaveEachMatcher{ + Element: element, + } +} + +// HaveKey succeeds if actual is a map with the passed in key. +// By default HaveKey uses Equal() to perform the match, however a +// matcher can be passed in instead: +// +// Expect(map[string]string{"Foo": "Bar", "BazFoo": "Duck"}).Should(HaveKey(MatchRegexp(`.+Foo$`))) +func HaveKey(key interface{}) types.GomegaMatcher { + return &matchers.HaveKeyMatcher{ + Key: key, + } +} + +// HaveKeyWithValue succeeds if actual is a map with the passed in key and value. +// By default HaveKeyWithValue uses Equal() to perform the match, however a +// matcher can be passed in instead: +// +// Expect(map[string]string{"Foo": "Bar", "BazFoo": "Duck"}).Should(HaveKeyWithValue("Foo", "Bar")) +// Expect(map[string]string{"Foo": "Bar", "BazFoo": "Duck"}).Should(HaveKeyWithValue(MatchRegexp(`.+Foo$`), "Bar")) +func HaveKeyWithValue(key interface{}, value interface{}) types.GomegaMatcher { + return &matchers.HaveKeyWithValueMatcher{ + Key: key, + Value: value, + } +} + +// HaveField succeeds if actual is a struct and the value at the passed in field +// matches the passed in matcher. By default HaveField used Equal() to perform the match, +// however a matcher can be passed in in stead. +// +// The field must be a string that resolves to the name of a field in the struct. Structs can be traversed +// using the '.' delimiter. If the field ends with '()' a method named field is assumed to exist on the struct and is invoked. +// Such methods must take no arguments and return a single value: +// +// type Book struct { +// Title string +// Author Person +// } +// type Person struct { +// FirstName string +// LastName string +// DOB time.Time +// } +// Expect(book).To(HaveField("Title", "Les Miserables")) +// Expect(book).To(HaveField("Title", ContainSubstring("Les")) +// Expect(book).To(HaveField("Author.FirstName", Equal("Victor")) +// Expect(book).To(HaveField("Author.DOB.Year()", BeNumerically("<", 1900)) +func HaveField(field string, expected interface{}) types.GomegaMatcher { + return &matchers.HaveFieldMatcher{ + Field: field, + Expected: expected, + } +} + +// HaveExistingField succeeds if actual is a struct and the specified field +// exists. +// +// HaveExistingField can be combined with HaveField in order to cover use cases +// with optional fields. HaveField alone would trigger an error in such situations. +// +// Expect(MrHarmless).NotTo(And(HaveExistingField("Title"), HaveField("Title", "Supervillain"))) +func HaveExistingField(field string) types.GomegaMatcher { + return &matchers.HaveExistingFieldMatcher{ + Field: field, + } +} + +// HaveValue applies the given matcher to the value of actual, optionally and +// repeatedly dereferencing pointers or taking the concrete value of interfaces. +// Thus, the matcher will always be applied to non-pointer and non-interface +// values only. HaveValue will fail with an error if a pointer or interface is +// nil. It will also fail for more than 31 pointer or interface dereferences to +// guard against mistakenly applying it to arbitrarily deep linked pointers. +// +// HaveValue differs from gstruct.PointTo in that it does not expect actual to +// be a pointer (as gstruct.PointTo does) but instead also accepts non-pointer +// and even interface values. +// +// actual := 42 +// Expect(actual).To(HaveValue(42)) +// Expect(&actual).To(HaveValue(42)) +func HaveValue(matcher types.GomegaMatcher) types.GomegaMatcher { + return &matchers.HaveValueMatcher{ + Matcher: matcher, + } +} + +// BeNumerically performs numerical assertions in a type-agnostic way. +// Actual and expected should be numbers, though the specific type of +// number is irrelevant (float32, float64, uint8, etc...). +// +// There are six, self-explanatory, supported comparators: +// +// Expect(1.0).Should(BeNumerically("==", 1)) +// Expect(1.0).Should(BeNumerically("~", 0.999, 0.01)) +// Expect(1.0).Should(BeNumerically(">", 0.9)) +// Expect(1.0).Should(BeNumerically(">=", 1.0)) +// Expect(1.0).Should(BeNumerically("<", 3)) +// Expect(1.0).Should(BeNumerically("<=", 1.0)) +func BeNumerically(comparator string, compareTo ...interface{}) types.GomegaMatcher { + return &matchers.BeNumericallyMatcher{ + Comparator: comparator, + CompareTo: compareTo, + } +} + +// BeTemporally compares time.Time's like BeNumerically +// Actual and expected must be time.Time. The comparators are the same as for BeNumerically +// +// Expect(time.Now()).Should(BeTemporally(">", time.Time{})) +// Expect(time.Now()).Should(BeTemporally("~", time.Now(), time.Second)) +func BeTemporally(comparator string, compareTo time.Time, threshold ...time.Duration) types.GomegaMatcher { + return &matchers.BeTemporallyMatcher{ + Comparator: comparator, + CompareTo: compareTo, + Threshold: threshold, + } +} + +// BeAssignableToTypeOf succeeds if actual is assignable to the type of expected. +// It will return an error when one of the values is nil. +// +// Expect(0).Should(BeAssignableToTypeOf(0)) // Same values +// Expect(5).Should(BeAssignableToTypeOf(-1)) // different values same type +// Expect("foo").Should(BeAssignableToTypeOf("bar")) // different values same type +// Expect(struct{ Foo string }{}).Should(BeAssignableToTypeOf(struct{ Foo string }{})) +func BeAssignableToTypeOf(expected interface{}) types.GomegaMatcher { + return &matchers.AssignableToTypeOfMatcher{ + Expected: expected, + } +} + +// Panic succeeds if actual is a function that, when invoked, panics. +// Actual must be a function that takes no arguments and returns no results. +func Panic() types.GomegaMatcher { + return &matchers.PanicMatcher{} +} + +// PanicWith succeeds if actual is a function that, when invoked, panics with a specific value. +// Actual must be a function that takes no arguments and returns no results. +// +// By default PanicWith uses Equal() to perform the match, however a +// matcher can be passed in instead: +// +// Expect(fn).Should(PanicWith(MatchRegexp(`.+Foo$`))) +func PanicWith(expected interface{}) types.GomegaMatcher { + return &matchers.PanicMatcher{Expected: expected} +} + +// BeAnExistingFile succeeds if a file exists. +// Actual must be a string representing the abs path to the file being checked. +func BeAnExistingFile() types.GomegaMatcher { + return &matchers.BeAnExistingFileMatcher{} +} + +// BeARegularFile succeeds if a file exists and is a regular file. +// Actual must be a string representing the abs path to the file being checked. +func BeARegularFile() types.GomegaMatcher { + return &matchers.BeARegularFileMatcher{} +} + +// BeADirectory succeeds if a file exists and is a directory. +// Actual must be a string representing the abs path to the file being checked. +func BeADirectory() types.GomegaMatcher { + return &matchers.BeADirectoryMatcher{} +} + +// HaveHTTPStatus succeeds if the Status or StatusCode field of an HTTP response matches. +// Actual must be either a *http.Response or *httptest.ResponseRecorder. +// Expected must be either an int or a string. +// +// Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) // asserts that resp.StatusCode == 200 +// Expect(resp).Should(HaveHTTPStatus("404 Not Found")) // asserts that resp.Status == "404 Not Found" +// Expect(resp).Should(HaveHTTPStatus(http.StatusOK, http.StatusNoContent)) // asserts that resp.StatusCode == 200 || resp.StatusCode == 204 +func HaveHTTPStatus(expected ...interface{}) types.GomegaMatcher { + return &matchers.HaveHTTPStatusMatcher{Expected: expected} +} + +// HaveHTTPHeaderWithValue succeeds if the header is found and the value matches. +// Actual must be either a *http.Response or *httptest.ResponseRecorder. +// Expected must be a string header name, followed by a header value which +// can be a string, or another matcher. +func HaveHTTPHeaderWithValue(header string, value interface{}) types.GomegaMatcher { + return &matchers.HaveHTTPHeaderWithValueMatcher{ + Header: header, + Value: value, + } +} + +// HaveHTTPBody matches if the body matches. +// Actual must be either a *http.Response or *httptest.ResponseRecorder. +// Expected must be either a string, []byte, or other matcher +func HaveHTTPBody(expected interface{}) types.GomegaMatcher { + return &matchers.HaveHTTPBodyMatcher{Expected: expected} +} + +// And succeeds only if all of the given matchers succeed. +// The matchers are tried in order, and will fail-fast if one doesn't succeed. +// +// Expect("hi").To(And(HaveLen(2), Equal("hi")) +// +// And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions. +func And(ms ...types.GomegaMatcher) types.GomegaMatcher { + return &matchers.AndMatcher{Matchers: ms} +} + +// SatisfyAll is an alias for And(). +// +// Expect("hi").Should(SatisfyAll(HaveLen(2), Equal("hi"))) +func SatisfyAll(matchers ...types.GomegaMatcher) types.GomegaMatcher { + return And(matchers...) +} + +// Or succeeds if any of the given matchers succeed. +// The matchers are tried in order and will return immediately upon the first successful match. +// +// Expect("hi").To(Or(HaveLen(3), HaveLen(2)) +// +// And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions. +func Or(ms ...types.GomegaMatcher) types.GomegaMatcher { + return &matchers.OrMatcher{Matchers: ms} +} + +// SatisfyAny is an alias for Or(). +// +// Expect("hi").SatisfyAny(Or(HaveLen(3), HaveLen(2)) +func SatisfyAny(matchers ...types.GomegaMatcher) types.GomegaMatcher { + return Or(matchers...) +} + +// Not negates the given matcher; it succeeds if the given matcher fails. +// +// Expect(1).To(Not(Equal(2)) +// +// And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions. +func Not(matcher types.GomegaMatcher) types.GomegaMatcher { + return &matchers.NotMatcher{Matcher: matcher} +} + +// WithTransform applies the `transform` to the actual value and matches it against `matcher`. +// The given transform must be either a function of one parameter that returns one value or a +// function of one parameter that returns two values, where the second value must be of the +// error type. +// +// var plus1 = func(i int) int { return i + 1 } +// Expect(1).To(WithTransform(plus1, Equal(2)) +// +// var failingplus1 = func(i int) (int, error) { return 42, "this does not compute" } +// Expect(1).To(WithTransform(failingplus1, Equal(2))) +// +// And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions. +func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher { + return matchers.NewWithTransformMatcher(transform, matcher) +} + +// Satisfy matches the actual value against the `predicate` function. +// The given predicate must be a function of one paramter that returns bool. +// +// var isEven = func(i int) bool { return i%2 == 0 } +// Expect(2).To(Satisfy(isEven)) +func Satisfy(predicate interface{}) types.GomegaMatcher { + return matchers.NewSatisfyMatcher(predicate) +} diff --git a/vendor/github.com/onsi/gomega/matchers/and.go b/vendor/github.com/onsi/gomega/matchers/and.go new file mode 100644 index 00000000000..6bd826adc5c --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/and.go @@ -0,0 +1,62 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +type AndMatcher struct { + Matchers []types.GomegaMatcher + + // state + firstFailedMatcher types.GomegaMatcher +} + +func (m *AndMatcher) Match(actual interface{}) (success bool, err error) { + m.firstFailedMatcher = nil + for _, matcher := range m.Matchers { + success, err := matcher.Match(actual) + if !success || err != nil { + m.firstFailedMatcher = matcher + return false, err + } + } + return true, nil +} + +func (m *AndMatcher) FailureMessage(actual interface{}) (message string) { + return m.firstFailedMatcher.FailureMessage(actual) +} + +func (m *AndMatcher) NegatedFailureMessage(actual interface{}) (message string) { + // not the most beautiful list of matchers, but not bad either... + return format.Message(actual, fmt.Sprintf("To not satisfy all of these matchers: %s", m.Matchers)) +} + +func (m *AndMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + /* + Example with 3 matchers: A, B, C + + Match evaluates them: T, F, => F + So match is currently F, what should MatchMayChangeInTheFuture() return? + Seems like it only depends on B, since currently B MUST change to allow the result to become T + + Match eval: T, T, T => T + So match is currently T, what should MatchMayChangeInTheFuture() return? + Seems to depend on ANY of them being able to change to F. + */ + + if m.firstFailedMatcher == nil { + // so all matchers succeeded.. Any one of them changing would change the result. + for _, matcher := range m.Matchers { + if types.MatchMayChangeInTheFuture(matcher, actual) { + return true + } + } + return false // none of were going to change + } + // one of the matchers failed.. it must be able to change in order to affect the result + return types.MatchMayChangeInTheFuture(m.firstFailedMatcher, actual) +} diff --git a/vendor/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher.go b/vendor/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher.go new file mode 100644 index 00000000000..be483952018 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/assignable_to_type_of_matcher.go @@ -0,0 +1,37 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type AssignableToTypeOfMatcher struct { + Expected interface{} +} + +func (matcher *AssignableToTypeOfMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil && matcher.Expected == nil { + return false, fmt.Errorf("Refusing to compare to .\nBe explicit and use BeNil() instead. This is to avoid mistakes where both sides of an assertion are erroneously uninitialized.") + } else if matcher.Expected == nil { + return false, fmt.Errorf("Refusing to compare type to .\nBe explicit and use BeNil() instead. This is to avoid mistakes where both sides of an assertion are erroneously uninitialized.") + } else if actual == nil { + return false, nil + } + + actualType := reflect.TypeOf(actual) + expectedType := reflect.TypeOf(matcher.Expected) + + return actualType.AssignableTo(expectedType), nil +} + +func (matcher *AssignableToTypeOfMatcher) FailureMessage(actual interface{}) string { + return format.Message(actual, fmt.Sprintf("to be assignable to the type: %T", matcher.Expected)) +} + +func (matcher *AssignableToTypeOfMatcher) NegatedFailureMessage(actual interface{}) string { + return format.Message(actual, fmt.Sprintf("not to be assignable to the type: %T", matcher.Expected)) +} diff --git a/vendor/github.com/onsi/gomega/matchers/attributes_slice.go b/vendor/github.com/onsi/gomega/matchers/attributes_slice.go new file mode 100644 index 00000000000..355b362f4b0 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/attributes_slice.go @@ -0,0 +1,14 @@ +package matchers + +import ( + "encoding/xml" + "strings" +) + +type attributesSlice []xml.Attr + +func (attrs attributesSlice) Len() int { return len(attrs) } +func (attrs attributesSlice) Less(i, j int) bool { + return strings.Compare(attrs[i].Name.Local, attrs[j].Name.Local) == -1 +} +func (attrs attributesSlice) Swap(i, j int) { attrs[i], attrs[j] = attrs[j], attrs[i] } diff --git a/vendor/github.com/onsi/gomega/matchers/be_a_directory.go b/vendor/github.com/onsi/gomega/matchers/be_a_directory.go new file mode 100644 index 00000000000..acffc8570f9 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_a_directory.go @@ -0,0 +1,56 @@ +// untested sections: 5 + +package matchers + +import ( + "fmt" + "os" + + "github.com/onsi/gomega/format" +) + +type notADirectoryError struct { + os.FileInfo +} + +func (t notADirectoryError) Error() string { + fileInfo := os.FileInfo(t) + switch { + case fileInfo.Mode().IsRegular(): + return "file is a regular file" + default: + return fmt.Sprintf("file mode is: %s", fileInfo.Mode().String()) + } +} + +type BeADirectoryMatcher struct { + expected interface{} + err error +} + +func (matcher *BeADirectoryMatcher) Match(actual interface{}) (success bool, err error) { + actualFilename, ok := actual.(string) + if !ok { + return false, fmt.Errorf("BeADirectoryMatcher matcher expects a file path") + } + + fileInfo, err := os.Stat(actualFilename) + if err != nil { + matcher.err = err + return false, nil + } + + if !fileInfo.Mode().IsDir() { + matcher.err = notADirectoryError{fileInfo} + return false, nil + } + return true, nil +} + +func (matcher *BeADirectoryMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to be a directory: %s", matcher.err)) +} + +func (matcher *BeADirectoryMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("not be a directory")) +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_a_regular_file.go b/vendor/github.com/onsi/gomega/matchers/be_a_regular_file.go new file mode 100644 index 00000000000..89441c80036 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_a_regular_file.go @@ -0,0 +1,56 @@ +// untested sections: 5 + +package matchers + +import ( + "fmt" + "os" + + "github.com/onsi/gomega/format" +) + +type notARegularFileError struct { + os.FileInfo +} + +func (t notARegularFileError) Error() string { + fileInfo := os.FileInfo(t) + switch { + case fileInfo.IsDir(): + return "file is a directory" + default: + return fmt.Sprintf("file mode is: %s", fileInfo.Mode().String()) + } +} + +type BeARegularFileMatcher struct { + expected interface{} + err error +} + +func (matcher *BeARegularFileMatcher) Match(actual interface{}) (success bool, err error) { + actualFilename, ok := actual.(string) + if !ok { + return false, fmt.Errorf("BeARegularFileMatcher matcher expects a file path") + } + + fileInfo, err := os.Stat(actualFilename) + if err != nil { + matcher.err = err + return false, nil + } + + if !fileInfo.Mode().IsRegular() { + matcher.err = notARegularFileError{fileInfo} + return false, nil + } + return true, nil +} + +func (matcher *BeARegularFileMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to be a regular file: %s", matcher.err)) +} + +func (matcher *BeARegularFileMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("not be a regular file")) +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_an_existing_file.go b/vendor/github.com/onsi/gomega/matchers/be_an_existing_file.go new file mode 100644 index 00000000000..ec6506b001e --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_an_existing_file.go @@ -0,0 +1,40 @@ +// untested sections: 3 + +package matchers + +import ( + "fmt" + "os" + + "github.com/onsi/gomega/format" +) + +type BeAnExistingFileMatcher struct { + expected interface{} +} + +func (matcher *BeAnExistingFileMatcher) Match(actual interface{}) (success bool, err error) { + actualFilename, ok := actual.(string) + if !ok { + return false, fmt.Errorf("BeAnExistingFileMatcher matcher expects a file path") + } + + if _, err = os.Stat(actualFilename); err != nil { + switch { + case os.IsNotExist(err): + return false, nil + default: + return false, err + } + } + + return true, nil +} + +func (matcher *BeAnExistingFileMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to exist")) +} + +func (matcher *BeAnExistingFileMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("not to exist")) +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_closed_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_closed_matcher.go new file mode 100644 index 00000000000..f13c24490f8 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_closed_matcher.go @@ -0,0 +1,48 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type BeClosedMatcher struct { +} + +func (matcher *BeClosedMatcher) Match(actual interface{}) (success bool, err error) { + if !isChan(actual) { + return false, fmt.Errorf("BeClosed matcher expects a channel. Got:\n%s", format.Object(actual, 1)) + } + + channelType := reflect.TypeOf(actual) + channelValue := reflect.ValueOf(actual) + + if channelType.ChanDir() == reflect.SendDir { + return false, fmt.Errorf("BeClosed matcher cannot determine if a send-only channel is closed or open. Got:\n%s", format.Object(actual, 1)) + } + + winnerIndex, _, open := reflect.Select([]reflect.SelectCase{ + {Dir: reflect.SelectRecv, Chan: channelValue}, + {Dir: reflect.SelectDefault}, + }) + + var closed bool + if winnerIndex == 0 { + closed = !open + } else if winnerIndex == 1 { + closed = false + } + + return closed, nil +} + +func (matcher *BeClosedMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be closed") +} + +func (matcher *BeClosedMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be open") +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_comparable_to_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_comparable_to_matcher.go new file mode 100644 index 00000000000..8ab4bb91949 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_comparable_to_matcher.go @@ -0,0 +1,49 @@ +package matchers + +import ( + "bytes" + "fmt" + + "github.com/google/go-cmp/cmp" + "github.com/onsi/gomega/format" +) + +type BeComparableToMatcher struct { + Expected interface{} + Options cmp.Options +} + +func (matcher *BeComparableToMatcher) Match(actual interface{}) (success bool, matchErr error) { + if actual == nil && matcher.Expected == nil { + return false, fmt.Errorf("Refusing to compare to .\nBe explicit and use BeNil() instead. This is to avoid mistakes where both sides of an assertion are erroneously uninitialized.") + } + // Shortcut for byte slices. + // Comparing long byte slices with reflect.DeepEqual is very slow, + // so use bytes.Equal if actual and expected are both byte slices. + if actualByteSlice, ok := actual.([]byte); ok { + if expectedByteSlice, ok := matcher.Expected.([]byte); ok { + return bytes.Equal(actualByteSlice, expectedByteSlice), nil + } + } + + defer func() { + if r := recover(); r != nil { + success = false + if err, ok := r.(error); ok { + matchErr = err + } else if errMsg, ok := r.(string); ok { + matchErr = fmt.Errorf(errMsg) + } + } + }() + + return cmp.Equal(actual, matcher.Expected, matcher.Options...), nil +} + +func (matcher *BeComparableToMatcher) FailureMessage(actual interface{}) (message string) { + return cmp.Diff(matcher.Expected, actual, matcher.Options) +} + +func (matcher *BeComparableToMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to equal", matcher.Expected) +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_element_of_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_element_of_matcher.go new file mode 100644 index 00000000000..9ee75a5d511 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_element_of_matcher.go @@ -0,0 +1,43 @@ +// untested sections: 1 + +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type BeElementOfMatcher struct { + Elements []interface{} +} + +func (matcher *BeElementOfMatcher) Match(actual interface{}) (success bool, err error) { + if reflect.TypeOf(actual) == nil { + return false, fmt.Errorf("BeElement matcher expects actual to be typed") + } + + var lastError error + for _, m := range flatten(matcher.Elements) { + matcher := &EqualMatcher{Expected: m} + success, err := matcher.Match(actual) + if err != nil { + lastError = err + continue + } + if success { + return true, nil + } + } + + return false, lastError +} + +func (matcher *BeElementOfMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be an element of", presentable(matcher.Elements)) +} + +func (matcher *BeElementOfMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be an element of", presentable(matcher.Elements)) +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_empty_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_empty_matcher.go new file mode 100644 index 00000000000..527c1a1c104 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_empty_matcher.go @@ -0,0 +1,29 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type BeEmptyMatcher struct { +} + +func (matcher *BeEmptyMatcher) Match(actual interface{}) (success bool, err error) { + length, ok := lengthOf(actual) + if !ok { + return false, fmt.Errorf("BeEmpty matcher expects a string/array/map/channel/slice. Got:\n%s", format.Object(actual, 1)) + } + + return length == 0, nil +} + +func (matcher *BeEmptyMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be empty") +} + +func (matcher *BeEmptyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be empty") +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_equivalent_to_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_equivalent_to_matcher.go new file mode 100644 index 00000000000..263627f4083 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_equivalent_to_matcher.go @@ -0,0 +1,36 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type BeEquivalentToMatcher struct { + Expected interface{} +} + +func (matcher *BeEquivalentToMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil && matcher.Expected == nil { + return false, fmt.Errorf("Both actual and expected must not be nil.") + } + + convertedActual := actual + + if actual != nil && matcher.Expected != nil && reflect.TypeOf(actual).ConvertibleTo(reflect.TypeOf(matcher.Expected)) { + convertedActual = reflect.ValueOf(actual).Convert(reflect.TypeOf(matcher.Expected)).Interface() + } + + return reflect.DeepEqual(convertedActual, matcher.Expected), nil +} + +func (matcher *BeEquivalentToMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be equivalent to", matcher.Expected) +} + +func (matcher *BeEquivalentToMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be equivalent to", matcher.Expected) +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_false_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_false_matcher.go new file mode 100644 index 00000000000..e326c015774 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_false_matcher.go @@ -0,0 +1,28 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type BeFalseMatcher struct { +} + +func (matcher *BeFalseMatcher) Match(actual interface{}) (success bool, err error) { + if !isBool(actual) { + return false, fmt.Errorf("Expected a boolean. Got:\n%s", format.Object(actual, 1)) + } + + return actual == false, nil +} + +func (matcher *BeFalseMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be false") +} + +func (matcher *BeFalseMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be false") +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_identical_to.go b/vendor/github.com/onsi/gomega/matchers/be_identical_to.go new file mode 100644 index 00000000000..631ce11e33b --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_identical_to.go @@ -0,0 +1,39 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + "runtime" + + "github.com/onsi/gomega/format" +) + +type BeIdenticalToMatcher struct { + Expected interface{} +} + +func (matcher *BeIdenticalToMatcher) Match(actual interface{}) (success bool, matchErr error) { + if actual == nil && matcher.Expected == nil { + return false, fmt.Errorf("Refusing to compare to .\nBe explicit and use BeNil() instead. This is to avoid mistakes where both sides of an assertion are erroneously uninitialized.") + } + + defer func() { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + success = false + matchErr = nil + } + } + }() + + return actual == matcher.Expected, nil +} + +func (matcher *BeIdenticalToMatcher) FailureMessage(actual interface{}) string { + return format.Message(actual, "to be identical to", matcher.Expected) +} + +func (matcher *BeIdenticalToMatcher) NegatedFailureMessage(actual interface{}) string { + return format.Message(actual, "not to be identical to", matcher.Expected) +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_key_of_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_key_of_matcher.go new file mode 100644 index 00000000000..449a291ef9a --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_key_of_matcher.go @@ -0,0 +1,45 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type BeKeyOfMatcher struct { + Map interface{} +} + +func (matcher *BeKeyOfMatcher) Match(actual interface{}) (success bool, err error) { + if !isMap(matcher.Map) { + return false, fmt.Errorf("BeKeyOf matcher needs expected to be a map type") + } + + if reflect.TypeOf(actual) == nil { + return false, fmt.Errorf("BeKeyOf matcher expects actual to be typed") + } + + var lastError error + for _, key := range reflect.ValueOf(matcher.Map).MapKeys() { + matcher := &EqualMatcher{Expected: key.Interface()} + success, err := matcher.Match(actual) + if err != nil { + lastError = err + continue + } + if success { + return true, nil + } + } + + return false, lastError +} + +func (matcher *BeKeyOfMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be a key of", presentable(valuesOf(matcher.Map))) +} + +func (matcher *BeKeyOfMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be a key of", presentable(valuesOf(matcher.Map))) +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_nil_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_nil_matcher.go new file mode 100644 index 00000000000..551d99d7474 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_nil_matcher.go @@ -0,0 +1,20 @@ +// untested sections: 2 + +package matchers + +import "github.com/onsi/gomega/format" + +type BeNilMatcher struct { +} + +func (matcher *BeNilMatcher) Match(actual interface{}) (success bool, err error) { + return isNil(actual), nil +} + +func (matcher *BeNilMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be nil") +} + +func (matcher *BeNilMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be nil") +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_numerically_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_numerically_matcher.go new file mode 100644 index 00000000000..100735de325 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_numerically_matcher.go @@ -0,0 +1,134 @@ +// untested sections: 4 + +package matchers + +import ( + "fmt" + "math" + + "github.com/onsi/gomega/format" +) + +type BeNumericallyMatcher struct { + Comparator string + CompareTo []interface{} +} + +func (matcher *BeNumericallyMatcher) FailureMessage(actual interface{}) (message string) { + return matcher.FormatFailureMessage(actual, false) +} + +func (matcher *BeNumericallyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return matcher.FormatFailureMessage(actual, true) +} + +func (matcher *BeNumericallyMatcher) FormatFailureMessage(actual interface{}, negated bool) (message string) { + if len(matcher.CompareTo) == 1 { + message = fmt.Sprintf("to be %s", matcher.Comparator) + } else { + message = fmt.Sprintf("to be within %v of %s", matcher.CompareTo[1], matcher.Comparator) + } + if negated { + message = "not " + message + } + return format.Message(actual, message, matcher.CompareTo[0]) +} + +func (matcher *BeNumericallyMatcher) Match(actual interface{}) (success bool, err error) { + if len(matcher.CompareTo) == 0 || len(matcher.CompareTo) > 2 { + return false, fmt.Errorf("BeNumerically requires 1 or 2 CompareTo arguments. Got:\n%s", format.Object(matcher.CompareTo, 1)) + } + if !isNumber(actual) { + return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(actual, 1)) + } + if !isNumber(matcher.CompareTo[0]) { + return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[0], 1)) + } + if len(matcher.CompareTo) == 2 && !isNumber(matcher.CompareTo[1]) { + return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[1], 1)) + } + + switch matcher.Comparator { + case "==", "~", ">", ">=", "<", "<=": + default: + return false, fmt.Errorf("Unknown comparator: %s", matcher.Comparator) + } + + if isFloat(actual) || isFloat(matcher.CompareTo[0]) { + var secondOperand float64 = 1e-8 + if len(matcher.CompareTo) == 2 { + secondOperand = toFloat(matcher.CompareTo[1]) + } + success = matcher.matchFloats(toFloat(actual), toFloat(matcher.CompareTo[0]), secondOperand) + } else if isInteger(actual) { + var secondOperand int64 = 0 + if len(matcher.CompareTo) == 2 { + secondOperand = toInteger(matcher.CompareTo[1]) + } + success = matcher.matchIntegers(toInteger(actual), toInteger(matcher.CompareTo[0]), secondOperand) + } else if isUnsignedInteger(actual) { + var secondOperand uint64 = 0 + if len(matcher.CompareTo) == 2 { + secondOperand = toUnsignedInteger(matcher.CompareTo[1]) + } + success = matcher.matchUnsignedIntegers(toUnsignedInteger(actual), toUnsignedInteger(matcher.CompareTo[0]), secondOperand) + } else { + return false, fmt.Errorf("Failed to compare:\n%s\n%s:\n%s", format.Object(actual, 1), matcher.Comparator, format.Object(matcher.CompareTo[0], 1)) + } + + return success, nil +} + +func (matcher *BeNumericallyMatcher) matchIntegers(actual, compareTo, threshold int64) (success bool) { + switch matcher.Comparator { + case "==", "~": + diff := actual - compareTo + return -threshold <= diff && diff <= threshold + case ">": + return (actual > compareTo) + case ">=": + return (actual >= compareTo) + case "<": + return (actual < compareTo) + case "<=": + return (actual <= compareTo) + } + return false +} + +func (matcher *BeNumericallyMatcher) matchUnsignedIntegers(actual, compareTo, threshold uint64) (success bool) { + switch matcher.Comparator { + case "==", "~": + if actual < compareTo { + actual, compareTo = compareTo, actual + } + return actual-compareTo <= threshold + case ">": + return (actual > compareTo) + case ">=": + return (actual >= compareTo) + case "<": + return (actual < compareTo) + case "<=": + return (actual <= compareTo) + } + return false +} + +func (matcher *BeNumericallyMatcher) matchFloats(actual, compareTo, threshold float64) (success bool) { + switch matcher.Comparator { + case "~": + return math.Abs(actual-compareTo) <= threshold + case "==": + return (actual == compareTo) + case ">": + return (actual > compareTo) + case ">=": + return (actual >= compareTo) + case "<": + return (actual < compareTo) + case "<=": + return (actual <= compareTo) + } + return false +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_sent_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_sent_matcher.go new file mode 100644 index 00000000000..cf582a3fcbd --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_sent_matcher.go @@ -0,0 +1,73 @@ +// untested sections: 3 + +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type BeSentMatcher struct { + Arg interface{} + channelClosed bool +} + +func (matcher *BeSentMatcher) Match(actual interface{}) (success bool, err error) { + if !isChan(actual) { + return false, fmt.Errorf("BeSent expects a channel. Got:\n%s", format.Object(actual, 1)) + } + + channelType := reflect.TypeOf(actual) + channelValue := reflect.ValueOf(actual) + + if channelType.ChanDir() == reflect.RecvDir { + return false, fmt.Errorf("BeSent matcher cannot be passed a receive-only channel. Got:\n%s", format.Object(actual, 1)) + } + + argType := reflect.TypeOf(matcher.Arg) + assignable := argType.AssignableTo(channelType.Elem()) + + if !assignable { + return false, fmt.Errorf("Cannot pass:\n%s to the channel:\n%s\nThe types don't match.", format.Object(matcher.Arg, 1), format.Object(actual, 1)) + } + + argValue := reflect.ValueOf(matcher.Arg) + + defer func() { + if e := recover(); e != nil { + success = false + err = fmt.Errorf("Cannot send to a closed channel") + matcher.channelClosed = true + } + }() + + winnerIndex, _, _ := reflect.Select([]reflect.SelectCase{ + {Dir: reflect.SelectSend, Chan: channelValue, Send: argValue}, + {Dir: reflect.SelectDefault}, + }) + + var didSend bool + if winnerIndex == 0 { + didSend = true + } + + return didSend, nil +} + +func (matcher *BeSentMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to send:", matcher.Arg) +} + +func (matcher *BeSentMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to send:", matcher.Arg) +} + +func (matcher *BeSentMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + if !isChan(actual) { + return false + } + + return !matcher.channelClosed +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_temporally_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_temporally_matcher.go new file mode 100644 index 00000000000..dec4db024e4 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_temporally_matcher.go @@ -0,0 +1,68 @@ +// untested sections: 3 + +package matchers + +import ( + "fmt" + "time" + + "github.com/onsi/gomega/format" +) + +type BeTemporallyMatcher struct { + Comparator string + CompareTo time.Time + Threshold []time.Duration +} + +func (matcher *BeTemporallyMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to be %s", matcher.Comparator), matcher.CompareTo) +} + +func (matcher *BeTemporallyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("not to be %s", matcher.Comparator), matcher.CompareTo) +} + +func (matcher *BeTemporallyMatcher) Match(actual interface{}) (bool, error) { + // predicate to test for time.Time type + isTime := func(t interface{}) bool { + _, ok := t.(time.Time) + return ok + } + + if !isTime(actual) { + return false, fmt.Errorf("Expected a time.Time. Got:\n%s", format.Object(actual, 1)) + } + + switch matcher.Comparator { + case "==", "~", ">", ">=", "<", "<=": + default: + return false, fmt.Errorf("Unknown comparator: %s", matcher.Comparator) + } + + var threshold = time.Millisecond + if len(matcher.Threshold) == 1 { + threshold = matcher.Threshold[0] + } + + return matcher.matchTimes(actual.(time.Time), matcher.CompareTo, threshold), nil +} + +func (matcher *BeTemporallyMatcher) matchTimes(actual, compareTo time.Time, threshold time.Duration) (success bool) { + switch matcher.Comparator { + case "==": + return actual.Equal(compareTo) + case "~": + diff := actual.Sub(compareTo) + return -threshold <= diff && diff <= threshold + case ">": + return actual.After(compareTo) + case ">=": + return !actual.Before(compareTo) + case "<": + return actual.Before(compareTo) + case "<=": + return !actual.After(compareTo) + } + return false +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_true_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_true_matcher.go new file mode 100644 index 00000000000..60bc1e3fa7e --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_true_matcher.go @@ -0,0 +1,28 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type BeTrueMatcher struct { +} + +func (matcher *BeTrueMatcher) Match(actual interface{}) (success bool, err error) { + if !isBool(actual) { + return false, fmt.Errorf("Expected a boolean. Got:\n%s", format.Object(actual, 1)) + } + + return actual.(bool), nil +} + +func (matcher *BeTrueMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be true") +} + +func (matcher *BeTrueMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be true") +} diff --git a/vendor/github.com/onsi/gomega/matchers/be_zero_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_zero_matcher.go new file mode 100644 index 00000000000..26196f168f4 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/be_zero_matcher.go @@ -0,0 +1,28 @@ +package matchers + +import ( + "reflect" + + "github.com/onsi/gomega/format" +) + +type BeZeroMatcher struct { +} + +func (matcher *BeZeroMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil { + return true, nil + } + zeroValue := reflect.Zero(reflect.TypeOf(actual)).Interface() + + return reflect.DeepEqual(zeroValue, actual), nil + +} + +func (matcher *BeZeroMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be zero-valued") +} + +func (matcher *BeZeroMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be zero-valued") +} diff --git a/vendor/github.com/onsi/gomega/matchers/consist_of.go b/vendor/github.com/onsi/gomega/matchers/consist_of.go new file mode 100644 index 00000000000..e8ef0dee1f4 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/consist_of.go @@ -0,0 +1,144 @@ +// untested sections: 3 + +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph" +) + +type ConsistOfMatcher struct { + Elements []interface{} + missingElements []interface{} + extraElements []interface{} +} + +func (matcher *ConsistOfMatcher) Match(actual interface{}) (success bool, err error) { + if !isArrayOrSlice(actual) && !isMap(actual) { + return false, fmt.Errorf("ConsistOf matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1)) + } + + matchers := matchers(matcher.Elements) + values := valuesOf(actual) + + bipartiteGraph, err := bipartitegraph.NewBipartiteGraph(values, matchers, neighbours) + if err != nil { + return false, err + } + + edges := bipartiteGraph.LargestMatching() + if len(edges) == len(values) && len(edges) == len(matchers) { + return true, nil + } + + var missingMatchers []interface{} + matcher.extraElements, missingMatchers = bipartiteGraph.FreeLeftRight(edges) + matcher.missingElements = equalMatchersToElements(missingMatchers) + + return false, nil +} + +func neighbours(value, matcher interface{}) (bool, error) { + match, err := matcher.(omegaMatcher).Match(value) + return match && err == nil, nil +} + +func equalMatchersToElements(matchers []interface{}) (elements []interface{}) { + for _, matcher := range matchers { + equalMatcher, ok := matcher.(*EqualMatcher) + if ok { + matcher = equalMatcher.Expected + } + elements = append(elements, matcher) + } + return +} + +func flatten(elems []interface{}) []interface{} { + if len(elems) != 1 || !isArrayOrSlice(elems[0]) { + return elems + } + + value := reflect.ValueOf(elems[0]) + flattened := make([]interface{}, value.Len()) + for i := 0; i < value.Len(); i++ { + flattened[i] = value.Index(i).Interface() + } + return flattened +} + +func matchers(expectedElems []interface{}) (matchers []interface{}) { + for _, e := range flatten(expectedElems) { + matcher, isMatcher := e.(omegaMatcher) + if !isMatcher { + matcher = &EqualMatcher{Expected: e} + } + matchers = append(matchers, matcher) + } + return +} + +func presentable(elems []interface{}) interface{} { + elems = flatten(elems) + + if len(elems) == 0 { + return []interface{}{} + } + + sv := reflect.ValueOf(elems) + tt := sv.Index(0).Elem().Type() + for i := 1; i < sv.Len(); i++ { + if sv.Index(i).Elem().Type() != tt { + return elems + } + } + + ss := reflect.MakeSlice(reflect.SliceOf(tt), sv.Len(), sv.Len()) + for i := 0; i < sv.Len(); i++ { + ss.Index(i).Set(sv.Index(i).Elem()) + } + + return ss.Interface() +} + +func valuesOf(actual interface{}) []interface{} { + value := reflect.ValueOf(actual) + values := []interface{}{} + if isMap(actual) { + keys := value.MapKeys() + for i := 0; i < value.Len(); i++ { + values = append(values, value.MapIndex(keys[i]).Interface()) + } + } else { + for i := 0; i < value.Len(); i++ { + values = append(values, value.Index(i).Interface()) + } + } + + return values +} + +func (matcher *ConsistOfMatcher) FailureMessage(actual interface{}) (message string) { + message = format.Message(actual, "to consist of", presentable(matcher.Elements)) + message = appendMissingElements(message, matcher.missingElements) + if len(matcher.extraElements) > 0 { + message = fmt.Sprintf("%s\nthe extra elements were\n%s", message, + format.Object(presentable(matcher.extraElements), 1)) + } + return +} + +func appendMissingElements(message string, missingElements []interface{}) string { + if len(missingElements) == 0 { + return message + } + return fmt.Sprintf("%s\nthe missing elements were\n%s", message, + format.Object(presentable(missingElements), 1)) +} + +func (matcher *ConsistOfMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to consist of", presentable(matcher.Elements)) +} diff --git a/vendor/github.com/onsi/gomega/matchers/contain_element_matcher.go b/vendor/github.com/onsi/gomega/matchers/contain_element_matcher.go new file mode 100644 index 00000000000..3d45c9ebc69 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/contain_element_matcher.go @@ -0,0 +1,174 @@ +// untested sections: 2 + +package matchers + +import ( + "errors" + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type ContainElementMatcher struct { + Element interface{} + Result []interface{} +} + +func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, err error) { + if !isArrayOrSlice(actual) && !isMap(actual) { + return false, fmt.Errorf("ContainElement matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1)) + } + + var actualT reflect.Type + var result reflect.Value + switch l := len(matcher.Result); { + case l > 1: + return false, errors.New("ContainElement matcher expects at most a single optional pointer to store its findings at") + case l == 1: + if reflect.ValueOf(matcher.Result[0]).Kind() != reflect.Ptr { + return false, fmt.Errorf("ContainElement matcher expects a non-nil pointer to store its findings at. Got\n%s", + format.Object(matcher.Result[0], 1)) + } + actualT = reflect.TypeOf(actual) + resultReference := matcher.Result[0] + result = reflect.ValueOf(resultReference).Elem() // what ResultReference points to, to stash away our findings + switch result.Kind() { + case reflect.Array: + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + reflect.SliceOf(actualT.Elem()).String(), result.Type().String()) + case reflect.Slice: + if !isArrayOrSlice(actual) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + reflect.MapOf(actualT.Key(), actualT.Elem()).String(), result.Type().String()) + } + if !actualT.Elem().AssignableTo(result.Type().Elem()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.String(), result.Type().String()) + } + case reflect.Map: + if !isMap(actual) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.String(), result.Type().String()) + } + if !actualT.AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.String(), result.Type().String()) + } + default: + if !actualT.Elem().AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.Elem().String(), result.Type().String()) + } + } + } + + elemMatcher, elementIsMatcher := matcher.Element.(omegaMatcher) + if !elementIsMatcher { + elemMatcher = &EqualMatcher{Expected: matcher.Element} + } + + value := reflect.ValueOf(actual) + var valueAt func(int) interface{} + + var getFindings func() reflect.Value + var foundAt func(int) + + if isMap(actual) { + keys := value.MapKeys() + valueAt = func(i int) interface{} { + return value.MapIndex(keys[i]).Interface() + } + if result.Kind() != reflect.Invalid { + fm := reflect.MakeMap(actualT) + getFindings = func() reflect.Value { + return fm + } + foundAt = func(i int) { + fm.SetMapIndex(keys[i], value.MapIndex(keys[i])) + } + } + } else { + valueAt = func(i int) interface{} { + return value.Index(i).Interface() + } + if result.Kind() != reflect.Invalid { + var f reflect.Value + if result.Kind() == reflect.Slice { + f = reflect.MakeSlice(result.Type(), 0, 0) + } else { + f = reflect.MakeSlice(reflect.SliceOf(result.Type()), 0, 0) + } + getFindings = func() reflect.Value { + return f + } + foundAt = func(i int) { + f = reflect.Append(f, value.Index(i)) + } + } + } + + var lastError error + for i := 0; i < value.Len(); i++ { + elem := valueAt(i) + success, err := elemMatcher.Match(elem) + if err != nil { + lastError = err + continue + } + if success { + if result.Kind() == reflect.Invalid { + return true, nil + } + foundAt(i) + } + } + + // when the expectation isn't interested in the findings except for success + // or non-success, then we're done here and return the last matcher error + // seen, if any, as well as non-success. + if result.Kind() == reflect.Invalid { + return false, lastError + } + + // pick up any findings the test is interested in as it specified a non-nil + // result reference. However, the expection always is that there are at + // least one or multiple findings. So, if a result is expected, but we had + // no findings, then this is an error. + findings := getFindings() + if findings.Len() == 0 { + return false, lastError + } + + // there's just a single finding and the result is neither a slice nor a map + // (so it's a scalar): pick the one and only finding and return it in the + // place the reference points to. + if findings.Len() == 1 && !isArrayOrSlice(result.Interface()) && !isMap(result.Interface()) { + if isMap(actual) { + miter := findings.MapRange() + miter.Next() + result.Set(miter.Value()) + } else { + result.Set(findings.Index(0)) + } + return true, nil + } + + // at least one or even multiple findings and a the result references a + // slice or a map, so all we need to do is to store our findings where the + // reference points to. + if !findings.Type().AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return multiple findings. Need *%s, got *%s", + findings.Type().String(), result.Type().String()) + } + result.Set(findings) + return true, nil +} + +func (matcher *ContainElementMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to contain element matching", matcher.Element) +} + +func (matcher *ContainElementMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to contain element matching", matcher.Element) +} diff --git a/vendor/github.com/onsi/gomega/matchers/contain_elements_matcher.go b/vendor/github.com/onsi/gomega/matchers/contain_elements_matcher.go new file mode 100644 index 00000000000..946cd8bea52 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/contain_elements_matcher.go @@ -0,0 +1,44 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph" +) + +type ContainElementsMatcher struct { + Elements []interface{} + missingElements []interface{} +} + +func (matcher *ContainElementsMatcher) Match(actual interface{}) (success bool, err error) { + if !isArrayOrSlice(actual) && !isMap(actual) { + return false, fmt.Errorf("ContainElements matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1)) + } + + matchers := matchers(matcher.Elements) + bipartiteGraph, err := bipartitegraph.NewBipartiteGraph(valuesOf(actual), matchers, neighbours) + if err != nil { + return false, err + } + + edges := bipartiteGraph.LargestMatching() + if len(edges) == len(matchers) { + return true, nil + } + + _, missingMatchers := bipartiteGraph.FreeLeftRight(edges) + matcher.missingElements = equalMatchersToElements(missingMatchers) + + return false, nil +} + +func (matcher *ContainElementsMatcher) FailureMessage(actual interface{}) (message string) { + message = format.Message(actual, "to contain elements", presentable(matcher.Elements)) + return appendMissingElements(message, matcher.missingElements) +} + +func (matcher *ContainElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to contain elements", presentable(matcher.Elements)) +} diff --git a/vendor/github.com/onsi/gomega/matchers/contain_substring_matcher.go b/vendor/github.com/onsi/gomega/matchers/contain_substring_matcher.go new file mode 100644 index 00000000000..e725f8c2753 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/contain_substring_matcher.go @@ -0,0 +1,40 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + "strings" + + "github.com/onsi/gomega/format" +) + +type ContainSubstringMatcher struct { + Substr string + Args []interface{} +} + +func (matcher *ContainSubstringMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + return false, fmt.Errorf("ContainSubstring matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) + } + + return strings.Contains(actualString, matcher.stringToMatch()), nil +} + +func (matcher *ContainSubstringMatcher) stringToMatch() string { + stringToMatch := matcher.Substr + if len(matcher.Args) > 0 { + stringToMatch = fmt.Sprintf(matcher.Substr, matcher.Args...) + } + return stringToMatch +} + +func (matcher *ContainSubstringMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to contain substring", matcher.stringToMatch()) +} + +func (matcher *ContainSubstringMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to contain substring", matcher.stringToMatch()) +} diff --git a/vendor/github.com/onsi/gomega/matchers/equal_matcher.go b/vendor/github.com/onsi/gomega/matchers/equal_matcher.go new file mode 100644 index 00000000000..befb7bdfd8f --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/equal_matcher.go @@ -0,0 +1,42 @@ +package matchers + +import ( + "bytes" + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type EqualMatcher struct { + Expected interface{} +} + +func (matcher *EqualMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil && matcher.Expected == nil { + return false, fmt.Errorf("Refusing to compare to .\nBe explicit and use BeNil() instead. This is to avoid mistakes where both sides of an assertion are erroneously uninitialized.") + } + // Shortcut for byte slices. + // Comparing long byte slices with reflect.DeepEqual is very slow, + // so use bytes.Equal if actual and expected are both byte slices. + if actualByteSlice, ok := actual.([]byte); ok { + if expectedByteSlice, ok := matcher.Expected.([]byte); ok { + return bytes.Equal(actualByteSlice, expectedByteSlice), nil + } + } + return reflect.DeepEqual(actual, matcher.Expected), nil +} + +func (matcher *EqualMatcher) FailureMessage(actual interface{}) (message string) { + actualString, actualOK := actual.(string) + expectedString, expectedOK := matcher.Expected.(string) + if actualOK && expectedOK { + return format.MessageWithDiff(actualString, "to equal", expectedString) + } + + return format.Message(actual, "to equal", matcher.Expected) +} + +func (matcher *EqualMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to equal", matcher.Expected) +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_cap_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_cap_matcher.go new file mode 100644 index 00000000000..9856752f13f --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_cap_matcher.go @@ -0,0 +1,30 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type HaveCapMatcher struct { + Count int +} + +func (matcher *HaveCapMatcher) Match(actual interface{}) (success bool, err error) { + length, ok := capOf(actual) + if !ok { + return false, fmt.Errorf("HaveCap matcher expects a array/channel/slice. Got:\n%s", format.Object(actual, 1)) + } + + return length == matcher.Count, nil +} + +func (matcher *HaveCapMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nto have capacity %d", format.Object(actual, 1), matcher.Count) +} + +func (matcher *HaveCapMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nnot to have capacity %d", format.Object(actual, 1), matcher.Count) +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_each_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_each_matcher.go new file mode 100644 index 00000000000..025b6e1ac2c --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_each_matcher.go @@ -0,0 +1,65 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type HaveEachMatcher struct { + Element interface{} +} + +func (matcher *HaveEachMatcher) Match(actual interface{}) (success bool, err error) { + if !isArrayOrSlice(actual) && !isMap(actual) { + return false, fmt.Errorf("HaveEach matcher expects an array/slice/map. Got:\n%s", + format.Object(actual, 1)) + } + + elemMatcher, elementIsMatcher := matcher.Element.(omegaMatcher) + if !elementIsMatcher { + elemMatcher = &EqualMatcher{Expected: matcher.Element} + } + + value := reflect.ValueOf(actual) + if value.Len() == 0 { + return false, fmt.Errorf("HaveEach matcher expects a non-empty array/slice/map. Got:\n%s", + format.Object(actual, 1)) + } + + var valueAt func(int) interface{} + if isMap(actual) { + keys := value.MapKeys() + valueAt = func(i int) interface{} { + return value.MapIndex(keys[i]).Interface() + } + } else { + valueAt = func(i int) interface{} { + return value.Index(i).Interface() + } + } + + // if there are no elements, then HaveEach will match. + for i := 0; i < value.Len(); i++ { + success, err := elemMatcher.Match(valueAt(i)) + if err != nil { + return false, err + } + if !success { + return false, nil + } + } + + return true, nil +} + +// FailureMessage returns a suitable failure message. +func (matcher *HaveEachMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to contain element matching", matcher.Element) +} + +// NegatedFailureMessage returns a suitable negated failure message. +func (matcher *HaveEachMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to contain element matching", matcher.Element) +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_existing_field_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_existing_field_matcher.go new file mode 100644 index 00000000000..b57018745fa --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_existing_field_matcher.go @@ -0,0 +1,36 @@ +package matchers + +import ( + "errors" + "fmt" + + "github.com/onsi/gomega/format" +) + +type HaveExistingFieldMatcher struct { + Field string +} + +func (matcher *HaveExistingFieldMatcher) Match(actual interface{}) (success bool, err error) { + // we don't care about the field's actual value, just about any error in + // trying to find the field (or method). + _, err = extractField(actual, matcher.Field, "HaveExistingField") + if err == nil { + return true, nil + } + var mferr missingFieldError + if errors.As(err, &mferr) { + // missing field errors aren't errors in this context, but instead + // unsuccessful matches. + return false, nil + } + return false, err +} + +func (matcher *HaveExistingFieldMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nto have field '%s'", format.Object(actual, 1), matcher.Field) +} + +func (matcher *HaveExistingFieldMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nnot to have field '%s'", format.Object(actual, 1), matcher.Field) +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_field.go b/vendor/github.com/onsi/gomega/matchers/have_field.go new file mode 100644 index 00000000000..6989f78c4b4 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_field.go @@ -0,0 +1,99 @@ +package matchers + +import ( + "fmt" + "reflect" + "strings" + + "github.com/onsi/gomega/format" +) + +// missingFieldError represents a missing field extraction error that +// HaveExistingFieldMatcher can ignore, as opposed to other, sever field +// extraction errors, such as nil pointers, et cetera. +type missingFieldError string + +func (e missingFieldError) Error() string { + return string(e) +} + +func extractField(actual interface{}, field string, matchername string) (interface{}, error) { + fields := strings.SplitN(field, ".", 2) + actualValue := reflect.ValueOf(actual) + + if actualValue.Kind() == reflect.Ptr { + actualValue = actualValue.Elem() + } + if actualValue == (reflect.Value{}) { + return nil, fmt.Errorf("%s encountered nil while dereferencing a pointer of type %T.", matchername, actual) + } + + if actualValue.Kind() != reflect.Struct { + return nil, fmt.Errorf("%s encountered:\n%s\nWhich is not a struct.", matchername, format.Object(actual, 1)) + } + + var extractedValue reflect.Value + + if strings.HasSuffix(fields[0], "()") { + extractedValue = actualValue.MethodByName(strings.TrimSuffix(fields[0], "()")) + if extractedValue == (reflect.Value{}) && actualValue.CanAddr() { + extractedValue = actualValue.Addr().MethodByName(strings.TrimSuffix(fields[0], "()")) + } + if extractedValue == (reflect.Value{}) { + return nil, missingFieldError(fmt.Sprintf("%s could not find method named '%s' in struct of type %T.", matchername, fields[0], actual)) + } + t := extractedValue.Type() + if t.NumIn() != 0 || t.NumOut() != 1 { + return nil, fmt.Errorf("%s found an invalid method named '%s' in struct of type %T.\nMethods must take no arguments and return exactly one value.", matchername, fields[0], actual) + } + extractedValue = extractedValue.Call([]reflect.Value{})[0] + } else { + extractedValue = actualValue.FieldByName(fields[0]) + if extractedValue == (reflect.Value{}) { + return nil, missingFieldError(fmt.Sprintf("%s could not find field named '%s' in struct:\n%s", matchername, fields[0], format.Object(actual, 1))) + } + } + + if len(fields) == 1 { + return extractedValue.Interface(), nil + } else { + return extractField(extractedValue.Interface(), fields[1], matchername) + } +} + +type HaveFieldMatcher struct { + Field string + Expected interface{} + + extractedField interface{} + expectedMatcher omegaMatcher +} + +func (matcher *HaveFieldMatcher) Match(actual interface{}) (success bool, err error) { + matcher.extractedField, err = extractField(actual, matcher.Field, "HaveField") + if err != nil { + return false, err + } + + var isMatcher bool + matcher.expectedMatcher, isMatcher = matcher.Expected.(omegaMatcher) + if !isMatcher { + matcher.expectedMatcher = &EqualMatcher{Expected: matcher.Expected} + } + + return matcher.expectedMatcher.Match(matcher.extractedField) +} + +func (matcher *HaveFieldMatcher) FailureMessage(actual interface{}) (message string) { + message = fmt.Sprintf("Value for field '%s' failed to satisfy matcher.\n", matcher.Field) + message += matcher.expectedMatcher.FailureMessage(matcher.extractedField) + + return message +} + +func (matcher *HaveFieldMatcher) NegatedFailureMessage(actual interface{}) (message string) { + message = fmt.Sprintf("Value for field '%s' satisfied matcher, but should not have.\n", matcher.Field) + message += matcher.expectedMatcher.NegatedFailureMessage(matcher.extractedField) + + return message +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_http_body_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_http_body_matcher.go new file mode 100644 index 00000000000..6a3dcdc3533 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_http_body_matcher.go @@ -0,0 +1,101 @@ +package matchers + +import ( + "fmt" + "net/http" + "net/http/httptest" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/internal/gutil" + "github.com/onsi/gomega/types" +) + +type HaveHTTPBodyMatcher struct { + Expected interface{} + cachedBody []byte +} + +func (matcher *HaveHTTPBodyMatcher) Match(actual interface{}) (bool, error) { + body, err := matcher.body(actual) + if err != nil { + return false, err + } + + switch e := matcher.Expected.(type) { + case string: + return (&EqualMatcher{Expected: e}).Match(string(body)) + case []byte: + return (&EqualMatcher{Expected: e}).Match(body) + case types.GomegaMatcher: + return e.Match(body) + default: + return false, fmt.Errorf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1)) + } +} + +func (matcher *HaveHTTPBodyMatcher) FailureMessage(actual interface{}) (message string) { + body, err := matcher.body(actual) + if err != nil { + return fmt.Sprintf("failed to read body: %s", err) + } + + switch e := matcher.Expected.(type) { + case string: + return (&EqualMatcher{Expected: e}).FailureMessage(string(body)) + case []byte: + return (&EqualMatcher{Expected: e}).FailureMessage(body) + case types.GomegaMatcher: + return e.FailureMessage(body) + default: + return fmt.Sprintf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1)) + } +} + +func (matcher *HaveHTTPBodyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + body, err := matcher.body(actual) + if err != nil { + return fmt.Sprintf("failed to read body: %s", err) + } + + switch e := matcher.Expected.(type) { + case string: + return (&EqualMatcher{Expected: e}).NegatedFailureMessage(string(body)) + case []byte: + return (&EqualMatcher{Expected: e}).NegatedFailureMessage(body) + case types.GomegaMatcher: + return e.NegatedFailureMessage(body) + default: + return fmt.Sprintf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1)) + } +} + +// body returns the body. It is cached because once we read it in Match() +// the Reader is closed and it is not readable again in FailureMessage() +// or NegatedFailureMessage() +func (matcher *HaveHTTPBodyMatcher) body(actual interface{}) ([]byte, error) { + if matcher.cachedBody != nil { + return matcher.cachedBody, nil + } + + body := func(a *http.Response) ([]byte, error) { + if a.Body != nil { + defer a.Body.Close() + var err error + matcher.cachedBody, err = gutil.ReadAll(a.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + } + return matcher.cachedBody, nil + } + + switch a := actual.(type) { + case *http.Response: + return body(a) + case *httptest.ResponseRecorder: + return body(a.Result()) + default: + return nil, fmt.Errorf("HaveHTTPBody matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1)) + } + +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_http_header_with_value_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_http_header_with_value_matcher.go new file mode 100644 index 00000000000..c256f452e84 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_http_header_with_value_matcher.go @@ -0,0 +1,81 @@ +package matchers + +import ( + "fmt" + "net/http" + "net/http/httptest" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +type HaveHTTPHeaderWithValueMatcher struct { + Header string + Value interface{} +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) Match(actual interface{}) (success bool, err error) { + headerValue, err := matcher.extractHeader(actual) + if err != nil { + return false, err + } + + headerMatcher, err := matcher.getSubMatcher() + if err != nil { + return false, err + } + + return headerMatcher.Match(headerValue) +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) FailureMessage(actual interface{}) string { + headerValue, err := matcher.extractHeader(actual) + if err != nil { + panic(err) // protected by Match() + } + + headerMatcher, err := matcher.getSubMatcher() + if err != nil { + panic(err) // protected by Match() + } + + diff := format.IndentString(headerMatcher.FailureMessage(headerValue), 1) + return fmt.Sprintf("HTTP header %q:\n%s", matcher.Header, diff) +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) NegatedFailureMessage(actual interface{}) (message string) { + headerValue, err := matcher.extractHeader(actual) + if err != nil { + panic(err) // protected by Match() + } + + headerMatcher, err := matcher.getSubMatcher() + if err != nil { + panic(err) // protected by Match() + } + + diff := format.IndentString(headerMatcher.NegatedFailureMessage(headerValue), 1) + return fmt.Sprintf("HTTP header %q:\n%s", matcher.Header, diff) +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) getSubMatcher() (types.GomegaMatcher, error) { + switch m := matcher.Value.(type) { + case string: + return &EqualMatcher{Expected: matcher.Value}, nil + case types.GomegaMatcher: + return m, nil + default: + return nil, fmt.Errorf("HaveHTTPHeaderWithValue matcher must be passed a string or a GomegaMatcher. Got:\n%s", format.Object(matcher.Value, 1)) + } +} + +func (matcher *HaveHTTPHeaderWithValueMatcher) extractHeader(actual interface{}) (string, error) { + switch r := actual.(type) { + case *http.Response: + return r.Header.Get(matcher.Header), nil + case *httptest.ResponseRecorder: + return r.Result().Header.Get(matcher.Header), nil + default: + return "", fmt.Errorf("HaveHTTPHeaderWithValue matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1)) + } +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_http_status_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_http_status_matcher.go new file mode 100644 index 00000000000..0f66e46ece4 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_http_status_matcher.go @@ -0,0 +1,96 @@ +package matchers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "strings" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/internal/gutil" +) + +type HaveHTTPStatusMatcher struct { + Expected []interface{} +} + +func (matcher *HaveHTTPStatusMatcher) Match(actual interface{}) (success bool, err error) { + var resp *http.Response + switch a := actual.(type) { + case *http.Response: + resp = a + case *httptest.ResponseRecorder: + resp = a.Result() + default: + return false, fmt.Errorf("HaveHTTPStatus matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1)) + } + + if len(matcher.Expected) == 0 { + return false, fmt.Errorf("HaveHTTPStatus matcher must be passed an int or a string. Got nothing") + } + + for _, expected := range matcher.Expected { + switch e := expected.(type) { + case int: + if resp.StatusCode == e { + return true, nil + } + case string: + if resp.Status == e { + return true, nil + } + default: + return false, fmt.Errorf("HaveHTTPStatus matcher must be passed int or string types. Got:\n%s", format.Object(expected, 1)) + } + } + + return false, nil +} + +func (matcher *HaveHTTPStatusMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "to have HTTP status", matcher.expectedString()) +} + +func (matcher *HaveHTTPStatusMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "not to have HTTP status", matcher.expectedString()) +} + +func (matcher *HaveHTTPStatusMatcher) expectedString() string { + var lines []string + for _, expected := range matcher.Expected { + lines = append(lines, format.Object(expected, 1)) + } + return strings.Join(lines, "\n") +} + +func formatHttpResponse(input interface{}) string { + var resp *http.Response + switch r := input.(type) { + case *http.Response: + resp = r + case *httptest.ResponseRecorder: + resp = r.Result() + default: + return "cannot format invalid HTTP response" + } + + body := "" + if resp.Body != nil { + defer resp.Body.Close() + data, err := gutil.ReadAll(resp.Body) + if err != nil { + data = []byte("") + } + body = format.Object(string(data), 0) + } + + var s strings.Builder + s.WriteString(fmt.Sprintf("%s<%s>: {\n", format.Indent, reflect.TypeOf(input))) + s.WriteString(fmt.Sprintf("%s%sStatus: %s\n", format.Indent, format.Indent, format.Object(resp.Status, 0))) + s.WriteString(fmt.Sprintf("%s%sStatusCode: %s\n", format.Indent, format.Indent, format.Object(resp.StatusCode, 0))) + s.WriteString(fmt.Sprintf("%s%sBody: %s\n", format.Indent, format.Indent, body)) + s.WriteString(fmt.Sprintf("%s}", format.Indent)) + + return s.String() +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_key_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_key_matcher.go new file mode 100644 index 00000000000..00cffec70e0 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_key_matcher.go @@ -0,0 +1,56 @@ +// untested sections: 6 + +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type HaveKeyMatcher struct { + Key interface{} +} + +func (matcher *HaveKeyMatcher) Match(actual interface{}) (success bool, err error) { + if !isMap(actual) { + return false, fmt.Errorf("HaveKey matcher expects a map. Got:%s", format.Object(actual, 1)) + } + + keyMatcher, keyIsMatcher := matcher.Key.(omegaMatcher) + if !keyIsMatcher { + keyMatcher = &EqualMatcher{Expected: matcher.Key} + } + + keys := reflect.ValueOf(actual).MapKeys() + for i := 0; i < len(keys); i++ { + success, err := keyMatcher.Match(keys[i].Interface()) + if err != nil { + return false, fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, err.Error()) + } + if success { + return true, nil + } + } + + return false, nil +} + +func (matcher *HaveKeyMatcher) FailureMessage(actual interface{}) (message string) { + switch matcher.Key.(type) { + case omegaMatcher: + return format.Message(actual, "to have key matching", matcher.Key) + default: + return format.Message(actual, "to have key", matcher.Key) + } +} + +func (matcher *HaveKeyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + switch matcher.Key.(type) { + case omegaMatcher: + return format.Message(actual, "not to have key matching", matcher.Key) + default: + return format.Message(actual, "not to have key", matcher.Key) + } +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go new file mode 100644 index 00000000000..4c591680470 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go @@ -0,0 +1,76 @@ +// untested sections:10 + +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type HaveKeyWithValueMatcher struct { + Key interface{} + Value interface{} +} + +func (matcher *HaveKeyWithValueMatcher) Match(actual interface{}) (success bool, err error) { + if !isMap(actual) { + return false, fmt.Errorf("HaveKeyWithValue matcher expects a map. Got:%s", format.Object(actual, 1)) + } + + keyMatcher, keyIsMatcher := matcher.Key.(omegaMatcher) + if !keyIsMatcher { + keyMatcher = &EqualMatcher{Expected: matcher.Key} + } + + valueMatcher, valueIsMatcher := matcher.Value.(omegaMatcher) + if !valueIsMatcher { + valueMatcher = &EqualMatcher{Expected: matcher.Value} + } + + keys := reflect.ValueOf(actual).MapKeys() + for i := 0; i < len(keys); i++ { + success, err := keyMatcher.Match(keys[i].Interface()) + if err != nil { + return false, fmt.Errorf("HaveKeyWithValue's key matcher failed with:\n%s%s", format.Indent, err.Error()) + } + if success { + actualValue := reflect.ValueOf(actual).MapIndex(keys[i]) + success, err := valueMatcher.Match(actualValue.Interface()) + if err != nil { + return false, fmt.Errorf("HaveKeyWithValue's value matcher failed with:\n%s%s", format.Indent, err.Error()) + } + return success, nil + } + } + + return false, nil +} + +func (matcher *HaveKeyWithValueMatcher) FailureMessage(actual interface{}) (message string) { + str := "to have {key: value}" + if _, ok := matcher.Key.(omegaMatcher); ok { + str += " matching" + } else if _, ok := matcher.Value.(omegaMatcher); ok { + str += " matching" + } + + expect := make(map[interface{}]interface{}, 1) + expect[matcher.Key] = matcher.Value + return format.Message(actual, str, expect) +} + +func (matcher *HaveKeyWithValueMatcher) NegatedFailureMessage(actual interface{}) (message string) { + kStr := "not to have key" + if _, ok := matcher.Key.(omegaMatcher); ok { + kStr = "not to have key matching" + } + + vStr := "or that key's value not be" + if _, ok := matcher.Value.(omegaMatcher); ok { + vStr = "or to have that key's value not matching" + } + + return format.Message(actual, kStr, matcher.Key, vStr, matcher.Value) +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_len_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_len_matcher.go new file mode 100644 index 00000000000..ee4276189de --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_len_matcher.go @@ -0,0 +1,28 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type HaveLenMatcher struct { + Count int +} + +func (matcher *HaveLenMatcher) Match(actual interface{}) (success bool, err error) { + length, ok := lengthOf(actual) + if !ok { + return false, fmt.Errorf("HaveLen matcher expects a string/array/map/channel/slice. Got:\n%s", format.Object(actual, 1)) + } + + return length == matcher.Count, nil +} + +func (matcher *HaveLenMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nto have length %d", format.Object(actual, 1), matcher.Count) +} + +func (matcher *HaveLenMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n%s\nnot to have length %d", format.Object(actual, 1), matcher.Count) +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_occurred_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_occurred_matcher.go new file mode 100644 index 00000000000..5bcfdd2ade9 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_occurred_matcher.go @@ -0,0 +1,35 @@ +// untested sections: 2 + +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type HaveOccurredMatcher struct { +} + +func (matcher *HaveOccurredMatcher) Match(actual interface{}) (success bool, err error) { + // is purely nil? + if actual == nil { + return false, nil + } + + // must be an 'error' type + if !isError(actual) { + return false, fmt.Errorf("Expected an error-type. Got:\n%s", format.Object(actual, 1)) + } + + // must be non-nil (or a pointer to a non-nil) + return !isNil(actual), nil +} + +func (matcher *HaveOccurredMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected an error to have occurred. Got:\n%s", format.Object(actual, 1)) +} + +func (matcher *HaveOccurredMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Unexpected error:\n%s\n%s\n%s", format.Object(actual, 1), format.IndentString(actual.(error).Error(), 1), "occurred") +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_prefix_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_prefix_matcher.go new file mode 100644 index 00000000000..1d8e80270b3 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_prefix_matcher.go @@ -0,0 +1,36 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type HavePrefixMatcher struct { + Prefix string + Args []interface{} +} + +func (matcher *HavePrefixMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + return false, fmt.Errorf("HavePrefix matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) + } + prefix := matcher.prefix() + return len(actualString) >= len(prefix) && actualString[0:len(prefix)] == prefix, nil +} + +func (matcher *HavePrefixMatcher) prefix() string { + if len(matcher.Args) > 0 { + return fmt.Sprintf(matcher.Prefix, matcher.Args...) + } + return matcher.Prefix +} + +func (matcher *HavePrefixMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to have prefix", matcher.prefix()) +} + +func (matcher *HavePrefixMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to have prefix", matcher.prefix()) +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_suffix_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_suffix_matcher.go new file mode 100644 index 00000000000..40a3526eb2d --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_suffix_matcher.go @@ -0,0 +1,36 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type HaveSuffixMatcher struct { + Suffix string + Args []interface{} +} + +func (matcher *HaveSuffixMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + return false, fmt.Errorf("HaveSuffix matcher requires a string or stringer. Got:\n%s", format.Object(actual, 1)) + } + suffix := matcher.suffix() + return len(actualString) >= len(suffix) && actualString[len(actualString)-len(suffix):] == suffix, nil +} + +func (matcher *HaveSuffixMatcher) suffix() string { + if len(matcher.Args) > 0 { + return fmt.Sprintf(matcher.Suffix, matcher.Args...) + } + return matcher.Suffix +} + +func (matcher *HaveSuffixMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to have suffix", matcher.suffix()) +} + +func (matcher *HaveSuffixMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to have suffix", matcher.suffix()) +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_value.go b/vendor/github.com/onsi/gomega/matchers/have_value.go new file mode 100644 index 00000000000..f6725283570 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_value.go @@ -0,0 +1,54 @@ +package matchers + +import ( + "errors" + "reflect" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +const maxIndirections = 31 + +type HaveValueMatcher struct { + Matcher types.GomegaMatcher // the matcher to apply to the "resolved" actual value. + resolvedActual interface{} // the ("resolved") value. +} + +func (m *HaveValueMatcher) Match(actual interface{}) (bool, error) { + val := reflect.ValueOf(actual) + for allowedIndirs := maxIndirections; allowedIndirs > 0; allowedIndirs-- { + // return an error if value isn't valid. Please note that we cannot + // check for nil here, as we might not deal with a pointer or interface + // at this point. + if !val.IsValid() { + return false, errors.New(format.Message( + actual, "not to be ")) + } + switch val.Kind() { + case reflect.Ptr, reflect.Interface: + // resolve pointers and interfaces to their values, then rinse and + // repeat. + if val.IsNil() { + return false, errors.New(format.Message( + actual, "not to be ")) + } + val = val.Elem() + continue + default: + // forward the final value to the specified matcher. + m.resolvedActual = val.Interface() + return m.Matcher.Match(m.resolvedActual) + } + } + // too many indirections: extreme star gazing, indeed...? + return false, errors.New(format.Message(actual, "too many indirections")) +} + +func (m *HaveValueMatcher) FailureMessage(_ interface{}) (message string) { + return m.Matcher.FailureMessage(m.resolvedActual) +} + +func (m *HaveValueMatcher) NegatedFailureMessage(_ interface{}) (message string) { + return m.Matcher.NegatedFailureMessage(m.resolvedActual) +} diff --git a/vendor/github.com/onsi/gomega/matchers/match_error_matcher.go b/vendor/github.com/onsi/gomega/matchers/match_error_matcher.go new file mode 100644 index 00000000000..827475ea51c --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/match_error_matcher.go @@ -0,0 +1,65 @@ +package matchers + +import ( + "errors" + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type MatchErrorMatcher struct { + Expected interface{} +} + +func (matcher *MatchErrorMatcher) Match(actual interface{}) (success bool, err error) { + if isNil(actual) { + return false, fmt.Errorf("Expected an error, got nil") + } + + if !isError(actual) { + return false, fmt.Errorf("Expected an error. Got:\n%s", format.Object(actual, 1)) + } + + actualErr := actual.(error) + expected := matcher.Expected + + if isError(expected) { + // first try the built-in errors.Is + if errors.Is(actualErr, expected.(error)) { + return true, nil + } + // if not, try DeepEqual along the error chain + for unwrapped := actualErr; unwrapped != nil; unwrapped = errors.Unwrap(unwrapped) { + if reflect.DeepEqual(unwrapped, expected) { + return true, nil + } + } + return false, nil + } + + if isString(expected) { + return actualErr.Error() == expected, nil + } + + var subMatcher omegaMatcher + var hasSubMatcher bool + if expected != nil { + subMatcher, hasSubMatcher = (expected).(omegaMatcher) + if hasSubMatcher { + return subMatcher.Match(actualErr.Error()) + } + } + + return false, fmt.Errorf( + "MatchError must be passed an error, a string, or a Matcher that can match on strings. Got:\n%s", + format.Object(expected, 1)) +} + +func (matcher *MatchErrorMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to match error", matcher.Expected) +} + +func (matcher *MatchErrorMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to match error", matcher.Expected) +} diff --git a/vendor/github.com/onsi/gomega/matchers/match_json_matcher.go b/vendor/github.com/onsi/gomega/matchers/match_json_matcher.go new file mode 100644 index 00000000000..f962f139ff8 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/match_json_matcher.go @@ -0,0 +1,65 @@ +package matchers + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/onsi/gomega/format" +) + +type MatchJSONMatcher struct { + JSONToMatch interface{} + firstFailurePath []interface{} +} + +func (matcher *MatchJSONMatcher) Match(actual interface{}) (success bool, err error) { + actualString, expectedString, err := matcher.prettyPrint(actual) + if err != nil { + return false, err + } + + var aval interface{} + var eval interface{} + + // this is guarded by prettyPrint + json.Unmarshal([]byte(actualString), &aval) + json.Unmarshal([]byte(expectedString), &eval) + var equal bool + equal, matcher.firstFailurePath = deepEqual(aval, eval) + return equal, nil +} + +func (matcher *MatchJSONMatcher) FailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.prettyPrint(actual) + return formattedMessage(format.Message(actualString, "to match JSON of", expectedString), matcher.firstFailurePath) +} + +func (matcher *MatchJSONMatcher) NegatedFailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.prettyPrint(actual) + return formattedMessage(format.Message(actualString, "not to match JSON of", expectedString), matcher.firstFailurePath) +} + +func (matcher *MatchJSONMatcher) prettyPrint(actual interface{}) (actualFormatted, expectedFormatted string, err error) { + actualString, ok := toString(actual) + if !ok { + return "", "", fmt.Errorf("MatchJSONMatcher matcher requires a string, stringer, or []byte. Got actual:\n%s", format.Object(actual, 1)) + } + expectedString, ok := toString(matcher.JSONToMatch) + if !ok { + return "", "", fmt.Errorf("MatchJSONMatcher matcher requires a string, stringer, or []byte. Got expected:\n%s", format.Object(matcher.JSONToMatch, 1)) + } + + abuf := new(bytes.Buffer) + ebuf := new(bytes.Buffer) + + if err := json.Indent(abuf, []byte(actualString), "", " "); err != nil { + return "", "", fmt.Errorf("Actual '%s' should be valid JSON, but it is not.\nUnderlying error:%s", actualString, err) + } + + if err := json.Indent(ebuf, []byte(expectedString), "", " "); err != nil { + return "", "", fmt.Errorf("Expected '%s' should be valid JSON, but it is not.\nUnderlying error:%s", expectedString, err) + } + + return abuf.String(), ebuf.String(), nil +} diff --git a/vendor/github.com/onsi/gomega/matchers/match_regexp_matcher.go b/vendor/github.com/onsi/gomega/matchers/match_regexp_matcher.go new file mode 100644 index 00000000000..adac5db6b8e --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/match_regexp_matcher.go @@ -0,0 +1,43 @@ +package matchers + +import ( + "fmt" + "regexp" + + "github.com/onsi/gomega/format" +) + +type MatchRegexpMatcher struct { + Regexp string + Args []interface{} +} + +func (matcher *MatchRegexpMatcher) Match(actual interface{}) (success bool, err error) { + actualString, ok := toString(actual) + if !ok { + return false, fmt.Errorf("RegExp matcher requires a string or stringer.\nGot:%s", format.Object(actual, 1)) + } + + match, err := regexp.Match(matcher.regexp(), []byte(actualString)) + if err != nil { + return false, fmt.Errorf("RegExp match failed to compile with error:\n\t%s", err.Error()) + } + + return match, nil +} + +func (matcher *MatchRegexpMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to match regular expression", matcher.regexp()) +} + +func (matcher *MatchRegexpMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to match regular expression", matcher.regexp()) +} + +func (matcher *MatchRegexpMatcher) regexp() string { + re := matcher.Regexp + if len(matcher.Args) > 0 { + re = fmt.Sprintf(matcher.Regexp, matcher.Args...) + } + return re +} diff --git a/vendor/github.com/onsi/gomega/matchers/match_xml_matcher.go b/vendor/github.com/onsi/gomega/matchers/match_xml_matcher.go new file mode 100644 index 00000000000..5c815f5af74 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/match_xml_matcher.go @@ -0,0 +1,134 @@ +package matchers + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "reflect" + "sort" + "strings" + + "github.com/onsi/gomega/format" + "golang.org/x/net/html/charset" +) + +type MatchXMLMatcher struct { + XMLToMatch interface{} +} + +func (matcher *MatchXMLMatcher) Match(actual interface{}) (success bool, err error) { + actualString, expectedString, err := matcher.formattedPrint(actual) + if err != nil { + return false, err + } + + aval, err := parseXmlContent(actualString) + if err != nil { + return false, fmt.Errorf("Actual '%s' should be valid XML, but it is not.\nUnderlying error:%s", actualString, err) + } + + eval, err := parseXmlContent(expectedString) + if err != nil { + return false, fmt.Errorf("Expected '%s' should be valid XML, but it is not.\nUnderlying error:%s", expectedString, err) + } + + return reflect.DeepEqual(aval, eval), nil +} + +func (matcher *MatchXMLMatcher) FailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.formattedPrint(actual) + return fmt.Sprintf("Expected\n%s\nto match XML of\n%s", actualString, expectedString) +} + +func (matcher *MatchXMLMatcher) NegatedFailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.formattedPrint(actual) + return fmt.Sprintf("Expected\n%s\nnot to match XML of\n%s", actualString, expectedString) +} + +func (matcher *MatchXMLMatcher) formattedPrint(actual interface{}) (actualString, expectedString string, err error) { + var ok bool + actualString, ok = toString(actual) + if !ok { + return "", "", fmt.Errorf("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n%s", format.Object(actual, 1)) + } + expectedString, ok = toString(matcher.XMLToMatch) + if !ok { + return "", "", fmt.Errorf("MatchXMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n%s", format.Object(matcher.XMLToMatch, 1)) + } + return actualString, expectedString, nil +} + +func parseXmlContent(content string) (*xmlNode, error) { + allNodes := []*xmlNode{} + + dec := newXmlDecoder(strings.NewReader(content)) + for { + tok, err := dec.Token() + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("failed to decode next token: %v", err) // untested section + } + + lastNodeIndex := len(allNodes) - 1 + var lastNode *xmlNode + if len(allNodes) > 0 { + lastNode = allNodes[lastNodeIndex] + } else { + lastNode = &xmlNode{} + } + + switch tok := tok.(type) { + case xml.StartElement: + attrs := attributesSlice(tok.Attr) + sort.Sort(attrs) + allNodes = append(allNodes, &xmlNode{XMLName: tok.Name, XMLAttr: tok.Attr}) + case xml.EndElement: + if len(allNodes) > 1 { + allNodes[lastNodeIndex-1].Nodes = append(allNodes[lastNodeIndex-1].Nodes, lastNode) + allNodes = allNodes[:lastNodeIndex] + } + case xml.CharData: + lastNode.Content = append(lastNode.Content, tok.Copy()...) + case xml.Comment: + lastNode.Comments = append(lastNode.Comments, tok.Copy()) // untested section + case xml.ProcInst: + lastNode.ProcInsts = append(lastNode.ProcInsts, tok.Copy()) + } + } + + if len(allNodes) == 0 { + return nil, errors.New("found no nodes") + } + firstNode := allNodes[0] + trimParentNodesContentSpaces(firstNode) + + return firstNode, nil +} + +func newXmlDecoder(reader io.Reader) *xml.Decoder { + dec := xml.NewDecoder(reader) + dec.CharsetReader = charset.NewReaderLabel + return dec +} + +func trimParentNodesContentSpaces(node *xmlNode) { + if len(node.Nodes) > 0 { + node.Content = bytes.TrimSpace(node.Content) + for _, childNode := range node.Nodes { + trimParentNodesContentSpaces(childNode) + } + } +} + +type xmlNode struct { + XMLName xml.Name + Comments []xml.Comment + ProcInsts []xml.ProcInst + XMLAttr []xml.Attr + Content []byte + Nodes []*xmlNode +} diff --git a/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go b/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go new file mode 100644 index 00000000000..2cb6b47db95 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go @@ -0,0 +1,76 @@ +package matchers + +import ( + "fmt" + "strings" + + "github.com/onsi/gomega/format" + "gopkg.in/yaml.v3" +) + +type MatchYAMLMatcher struct { + YAMLToMatch interface{} + firstFailurePath []interface{} +} + +func (matcher *MatchYAMLMatcher) Match(actual interface{}) (success bool, err error) { + actualString, expectedString, err := matcher.toStrings(actual) + if err != nil { + return false, err + } + + var aval interface{} + var eval interface{} + + if err := yaml.Unmarshal([]byte(actualString), &aval); err != nil { + return false, fmt.Errorf("Actual '%s' should be valid YAML, but it is not.\nUnderlying error:%s", actualString, err) + } + if err := yaml.Unmarshal([]byte(expectedString), &eval); err != nil { + return false, fmt.Errorf("Expected '%s' should be valid YAML, but it is not.\nUnderlying error:%s", expectedString, err) + } + + var equal bool + equal, matcher.firstFailurePath = deepEqual(aval, eval) + return equal, nil +} + +func (matcher *MatchYAMLMatcher) FailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.toNormalisedStrings(actual) + return formattedMessage(format.Message(actualString, "to match YAML of", expectedString), matcher.firstFailurePath) +} + +func (matcher *MatchYAMLMatcher) NegatedFailureMessage(actual interface{}) (message string) { + actualString, expectedString, _ := matcher.toNormalisedStrings(actual) + return formattedMessage(format.Message(actualString, "not to match YAML of", expectedString), matcher.firstFailurePath) +} + +func (matcher *MatchYAMLMatcher) toNormalisedStrings(actual interface{}) (actualFormatted, expectedFormatted string, err error) { + actualString, expectedString, err := matcher.toStrings(actual) + return normalise(actualString), normalise(expectedString), err +} + +func normalise(input string) string { + var val interface{} + err := yaml.Unmarshal([]byte(input), &val) + if err != nil { + panic(err) // unreachable since Match already calls Unmarshal + } + output, err := yaml.Marshal(val) + if err != nil { + panic(err) // untested section, unreachable since we Unmarshal above + } + return strings.TrimSpace(string(output)) +} + +func (matcher *MatchYAMLMatcher) toStrings(actual interface{}) (actualFormatted, expectedFormatted string, err error) { + actualString, ok := toString(actual) + if !ok { + return "", "", fmt.Errorf("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n%s", format.Object(actual, 1)) + } + expectedString, ok := toString(matcher.YAMLToMatch) + if !ok { + return "", "", fmt.Errorf("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n%s", format.Object(matcher.YAMLToMatch, 1)) + } + + return actualString, expectedString, nil +} diff --git a/vendor/github.com/onsi/gomega/matchers/not.go b/vendor/github.com/onsi/gomega/matchers/not.go new file mode 100644 index 00000000000..78b71910d12 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/not.go @@ -0,0 +1,29 @@ +package matchers + +import ( + "github.com/onsi/gomega/types" +) + +type NotMatcher struct { + Matcher types.GomegaMatcher +} + +func (m *NotMatcher) Match(actual interface{}) (bool, error) { + success, err := m.Matcher.Match(actual) + if err != nil { + return false, err + } + return !success, nil +} + +func (m *NotMatcher) FailureMessage(actual interface{}) (message string) { + return m.Matcher.NegatedFailureMessage(actual) // works beautifully +} + +func (m *NotMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return m.Matcher.FailureMessage(actual) // works beautifully +} + +func (m *NotMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + return types.MatchMayChangeInTheFuture(m.Matcher, actual) // just return m.Matcher's value +} diff --git a/vendor/github.com/onsi/gomega/matchers/or.go b/vendor/github.com/onsi/gomega/matchers/or.go new file mode 100644 index 00000000000..841ae26ab07 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/or.go @@ -0,0 +1,66 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +type OrMatcher struct { + Matchers []types.GomegaMatcher + + // state + firstSuccessfulMatcher types.GomegaMatcher +} + +func (m *OrMatcher) Match(actual interface{}) (success bool, err error) { + m.firstSuccessfulMatcher = nil + for _, matcher := range m.Matchers { + success, err := matcher.Match(actual) + if err != nil { + return false, err + } + if success { + m.firstSuccessfulMatcher = matcher + return true, nil + } + } + return false, nil +} + +func (m *OrMatcher) FailureMessage(actual interface{}) (message string) { + // not the most beautiful list of matchers, but not bad either... + return format.Message(actual, fmt.Sprintf("To satisfy at least one of these matchers: %s", m.Matchers)) +} + +func (m *OrMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return m.firstSuccessfulMatcher.NegatedFailureMessage(actual) +} + +func (m *OrMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + /* + Example with 3 matchers: A, B, C + + Match evaluates them: F, T, => T + So match is currently T, what should MatchMayChangeInTheFuture() return? + Seems like it only depends on B, since currently B MUST change to allow the result to become F + + Match eval: F, F, F => F + So match is currently F, what should MatchMayChangeInTheFuture() return? + Seems to depend on ANY of them being able to change to T. + */ + + if m.firstSuccessfulMatcher != nil { + // one of the matchers succeeded.. it must be able to change in order to affect the result + return types.MatchMayChangeInTheFuture(m.firstSuccessfulMatcher, actual) + } else { + // so all matchers failed.. Any one of them changing would change the result. + for _, matcher := range m.Matchers { + if types.MatchMayChangeInTheFuture(matcher, actual) { + return true + } + } + return false // none of were going to change + } +} diff --git a/vendor/github.com/onsi/gomega/matchers/panic_matcher.go b/vendor/github.com/onsi/gomega/matchers/panic_matcher.go new file mode 100644 index 00000000000..adc8cee6306 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/panic_matcher.go @@ -0,0 +1,114 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type PanicMatcher struct { + Expected interface{} + object interface{} +} + +func (matcher *PanicMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil { + return false, fmt.Errorf("PanicMatcher expects a non-nil actual.") + } + + actualType := reflect.TypeOf(actual) + if actualType.Kind() != reflect.Func { + return false, fmt.Errorf("PanicMatcher expects a function. Got:\n%s", format.Object(actual, 1)) + } + if !(actualType.NumIn() == 0 && actualType.NumOut() == 0) { + return false, fmt.Errorf("PanicMatcher expects a function with no arguments and no return value. Got:\n%s", format.Object(actual, 1)) + } + + success = false + defer func() { + if e := recover(); e != nil { + matcher.object = e + + if matcher.Expected == nil { + success = true + return + } + + valueMatcher, valueIsMatcher := matcher.Expected.(omegaMatcher) + if !valueIsMatcher { + valueMatcher = &EqualMatcher{Expected: matcher.Expected} + } + + success, err = valueMatcher.Match(e) + if err != nil { + err = fmt.Errorf("PanicMatcher's value matcher failed with:\n%s%s", format.Indent, err.Error()) + } + } + }() + + reflect.ValueOf(actual).Call([]reflect.Value{}) + + return +} + +func (matcher *PanicMatcher) FailureMessage(actual interface{}) (message string) { + if matcher.Expected == nil { + // We wanted any panic to occur, but none did. + return format.Message(actual, "to panic") + } + + if matcher.object == nil { + // We wanted a panic with a specific value to occur, but none did. + switch matcher.Expected.(type) { + case omegaMatcher: + return format.Message(actual, "to panic with a value matching", matcher.Expected) + default: + return format.Message(actual, "to panic with", matcher.Expected) + } + } + + // We got a panic, but the value isn't what we expected. + switch matcher.Expected.(type) { + case omegaMatcher: + return format.Message( + actual, + fmt.Sprintf( + "to panic with a value matching\n%s\nbut panicked with\n%s", + format.Object(matcher.Expected, 1), + format.Object(matcher.object, 1), + ), + ) + default: + return format.Message( + actual, + fmt.Sprintf( + "to panic with\n%s\nbut panicked with\n%s", + format.Object(matcher.Expected, 1), + format.Object(matcher.object, 1), + ), + ) + } +} + +func (matcher *PanicMatcher) NegatedFailureMessage(actual interface{}) (message string) { + if matcher.Expected == nil { + // We didn't want any panic to occur, but one did. + return format.Message(actual, fmt.Sprintf("not to panic, but panicked with\n%s", format.Object(matcher.object, 1))) + } + + // We wanted a to ensure a panic with a specific value did not occur, but it did. + switch matcher.Expected.(type) { + case omegaMatcher: + return format.Message( + actual, + fmt.Sprintf( + "not to panic with a value matching\n%s\nbut panicked with\n%s", + format.Object(matcher.Expected, 1), + format.Object(matcher.object, 1), + ), + ) + default: + return format.Message(actual, "not to panic with", matcher.Expected) + } +} diff --git a/vendor/github.com/onsi/gomega/matchers/receive_matcher.go b/vendor/github.com/onsi/gomega/matchers/receive_matcher.go new file mode 100644 index 00000000000..1936a2ba52f --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/receive_matcher.go @@ -0,0 +1,130 @@ +// untested sections: 3 + +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type ReceiveMatcher struct { + Arg interface{} + receivedValue reflect.Value + channelClosed bool +} + +func (matcher *ReceiveMatcher) Match(actual interface{}) (success bool, err error) { + if !isChan(actual) { + return false, fmt.Errorf("ReceiveMatcher expects a channel. Got:\n%s", format.Object(actual, 1)) + } + + channelType := reflect.TypeOf(actual) + channelValue := reflect.ValueOf(actual) + + if channelType.ChanDir() == reflect.SendDir { + return false, fmt.Errorf("ReceiveMatcher matcher cannot be passed a send-only channel. Got:\n%s", format.Object(actual, 1)) + } + + var subMatcher omegaMatcher + var hasSubMatcher bool + + if matcher.Arg != nil { + subMatcher, hasSubMatcher = (matcher.Arg).(omegaMatcher) + if !hasSubMatcher { + argType := reflect.TypeOf(matcher.Arg) + if argType.Kind() != reflect.Ptr { + return false, fmt.Errorf("Cannot assign a value from the channel:\n%s\nTo:\n%s\nYou need to pass a pointer!", format.Object(actual, 1), format.Object(matcher.Arg, 1)) + } + } + } + + winnerIndex, value, open := reflect.Select([]reflect.SelectCase{ + {Dir: reflect.SelectRecv, Chan: channelValue}, + {Dir: reflect.SelectDefault}, + }) + + var closed bool + var didReceive bool + if winnerIndex == 0 { + closed = !open + didReceive = open + } + matcher.channelClosed = closed + + if closed { + return false, nil + } + + if hasSubMatcher { + if didReceive { + matcher.receivedValue = value + return subMatcher.Match(matcher.receivedValue.Interface()) + } + return false, nil + } + + if didReceive { + if matcher.Arg != nil { + outValue := reflect.ValueOf(matcher.Arg) + + if value.Type().AssignableTo(outValue.Elem().Type()) { + outValue.Elem().Set(value) + return true, nil + } + if value.Type().Kind() == reflect.Interface && value.Elem().Type().AssignableTo(outValue.Elem().Type()) { + outValue.Elem().Set(value.Elem()) + return true, nil + } else { + return false, fmt.Errorf("Cannot assign a value from the channel:\n%s\nType:\n%s\nTo:\n%s", format.Object(actual, 1), format.Object(value.Interface(), 1), format.Object(matcher.Arg, 1)) + } + + } + + return true, nil + } + return false, nil +} + +func (matcher *ReceiveMatcher) FailureMessage(actual interface{}) (message string) { + subMatcher, hasSubMatcher := (matcher.Arg).(omegaMatcher) + + closedAddendum := "" + if matcher.channelClosed { + closedAddendum = " The channel is closed." + } + + if hasSubMatcher { + if matcher.receivedValue.IsValid() { + return subMatcher.FailureMessage(matcher.receivedValue.Interface()) + } + return "When passed a matcher, ReceiveMatcher's channel *must* receive something." + } + return format.Message(actual, "to receive something."+closedAddendum) +} + +func (matcher *ReceiveMatcher) NegatedFailureMessage(actual interface{}) (message string) { + subMatcher, hasSubMatcher := (matcher.Arg).(omegaMatcher) + + closedAddendum := "" + if matcher.channelClosed { + closedAddendum = " The channel is closed." + } + + if hasSubMatcher { + if matcher.receivedValue.IsValid() { + return subMatcher.NegatedFailureMessage(matcher.receivedValue.Interface()) + } + return "When passed a matcher, ReceiveMatcher's channel *must* receive something." + } + return format.Message(actual, "not to receive anything."+closedAddendum) +} + +func (matcher *ReceiveMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + if !isChan(actual) { + return false + } + + return !matcher.channelClosed +} diff --git a/vendor/github.com/onsi/gomega/matchers/satisfy_matcher.go b/vendor/github.com/onsi/gomega/matchers/satisfy_matcher.go new file mode 100644 index 00000000000..ec68fe8b62c --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/satisfy_matcher.go @@ -0,0 +1,66 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type SatisfyMatcher struct { + Predicate interface{} + + // cached type + predicateArgType reflect.Type +} + +func NewSatisfyMatcher(predicate interface{}) *SatisfyMatcher { + if predicate == nil { + panic("predicate cannot be nil") + } + predicateType := reflect.TypeOf(predicate) + if predicateType.Kind() != reflect.Func { + panic("predicate must be a function") + } + if predicateType.NumIn() != 1 { + panic("predicate must have 1 argument") + } + if predicateType.NumOut() != 1 || predicateType.Out(0).Kind() != reflect.Bool { + panic("predicate must return bool") + } + + return &SatisfyMatcher{ + Predicate: predicate, + predicateArgType: predicateType.In(0), + } +} + +func (m *SatisfyMatcher) Match(actual interface{}) (success bool, err error) { + // prepare a parameter to pass to the predicate + var param reflect.Value + if actual != nil && reflect.TypeOf(actual).AssignableTo(m.predicateArgType) { + // The dynamic type of actual is compatible with the predicate argument. + param = reflect.ValueOf(actual) + + } else if actual == nil && m.predicateArgType.Kind() == reflect.Interface { + // The dynamic type of actual is unknown, so there's no way to make its + // reflect.Value. Create a nil of the predicate argument, which is known. + param = reflect.Zero(m.predicateArgType) + + } else { + return false, fmt.Errorf("predicate expects '%s' but we have '%T'", m.predicateArgType, actual) + } + + // call the predicate with `actual` + fn := reflect.ValueOf(m.Predicate) + result := fn.Call([]reflect.Value{param}) + return result[0].Bool(), nil +} + +func (m *SatisfyMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to satisfy predicate", m.Predicate) +} + +func (m *SatisfyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to not satisfy predicate", m.Predicate) +} diff --git a/vendor/github.com/onsi/gomega/matchers/semi_structured_data_support.go b/vendor/github.com/onsi/gomega/matchers/semi_structured_data_support.go new file mode 100644 index 00000000000..1369c1e87f7 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/semi_structured_data_support.go @@ -0,0 +1,94 @@ +// untested sections: 5 + +package matchers + +import ( + "fmt" + "reflect" + "strings" +) + +func formattedMessage(comparisonMessage string, failurePath []interface{}) string { + var diffMessage string + if len(failurePath) == 0 { + diffMessage = "" + } else { + diffMessage = fmt.Sprintf("\n\nfirst mismatched key: %s", formattedFailurePath(failurePath)) + } + return fmt.Sprintf("%s%s", comparisonMessage, diffMessage) +} + +func formattedFailurePath(failurePath []interface{}) string { + formattedPaths := []string{} + for i := len(failurePath) - 1; i >= 0; i-- { + switch p := failurePath[i].(type) { + case int: + formattedPaths = append(formattedPaths, fmt.Sprintf(`[%d]`, p)) + default: + if i != len(failurePath)-1 { + formattedPaths = append(formattedPaths, ".") + } + formattedPaths = append(formattedPaths, fmt.Sprintf(`"%s"`, p)) + } + } + return strings.Join(formattedPaths, "") +} + +func deepEqual(a interface{}, b interface{}) (bool, []interface{}) { + var errorPath []interface{} + if reflect.TypeOf(a) != reflect.TypeOf(b) { + return false, errorPath + } + + switch a.(type) { + case []interface{}: + if len(a.([]interface{})) != len(b.([]interface{})) { + return false, errorPath + } + + for i, v := range a.([]interface{}) { + elementEqual, keyPath := deepEqual(v, b.([]interface{})[i]) + if !elementEqual { + return false, append(keyPath, i) + } + } + return true, errorPath + + case map[interface{}]interface{}: + if len(a.(map[interface{}]interface{})) != len(b.(map[interface{}]interface{})) { + return false, errorPath + } + + for k, v1 := range a.(map[interface{}]interface{}) { + v2, ok := b.(map[interface{}]interface{})[k] + if !ok { + return false, errorPath + } + elementEqual, keyPath := deepEqual(v1, v2) + if !elementEqual { + return false, append(keyPath, k) + } + } + return true, errorPath + + case map[string]interface{}: + if len(a.(map[string]interface{})) != len(b.(map[string]interface{})) { + return false, errorPath + } + + for k, v1 := range a.(map[string]interface{}) { + v2, ok := b.(map[string]interface{})[k] + if !ok { + return false, errorPath + } + elementEqual, keyPath := deepEqual(v1, v2) + if !elementEqual { + return false, append(keyPath, k) + } + } + return true, errorPath + + default: + return a == b, errorPath + } +} diff --git a/vendor/github.com/onsi/gomega/matchers/succeed_matcher.go b/vendor/github.com/onsi/gomega/matchers/succeed_matcher.go new file mode 100644 index 00000000000..da5a395944f --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/succeed_matcher.go @@ -0,0 +1,42 @@ +package matchers + +import ( + "errors" + "fmt" + + "github.com/onsi/gomega/format" +) + +type formattedGomegaError interface { + FormattedGomegaError() string +} + +type SucceedMatcher struct { +} + +func (matcher *SucceedMatcher) Match(actual interface{}) (success bool, err error) { + // is purely nil? + if actual == nil { + return true, nil + } + + // must be an 'error' type + if !isError(actual) { + return false, fmt.Errorf("Expected an error-type. Got:\n%s", format.Object(actual, 1)) + } + + // must be nil (or a pointer to a nil) + return isNil(actual), nil +} + +func (matcher *SucceedMatcher) FailureMessage(actual interface{}) (message string) { + var fgErr formattedGomegaError + if errors.As(actual.(error), &fgErr) { + return fgErr.FormattedGomegaError() + } + return fmt.Sprintf("Expected success, but got an error:\n%s\n%s", format.Object(actual, 1), format.IndentString(actual.(error).Error(), 1)) +} + +func (matcher *SucceedMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return "Expected failure, but got no error." +} diff --git a/vendor/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraph.go b/vendor/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraph.go new file mode 100644 index 00000000000..830e308274e --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraph.go @@ -0,0 +1,56 @@ +package bipartitegraph + +import "fmt" + +import . "github.com/onsi/gomega/matchers/support/goraph/node" +import . "github.com/onsi/gomega/matchers/support/goraph/edge" + +type BipartiteGraph struct { + Left NodeOrderedSet + Right NodeOrderedSet + Edges EdgeSet +} + +func NewBipartiteGraph(leftValues, rightValues []interface{}, neighbours func(interface{}, interface{}) (bool, error)) (*BipartiteGraph, error) { + left := NodeOrderedSet{} + for i, v := range leftValues { + left = append(left, Node{ID: i, Value: v}) + } + + right := NodeOrderedSet{} + for j, v := range rightValues { + right = append(right, Node{ID: j + len(left), Value: v}) + } + + edges := EdgeSet{} + for i, leftValue := range leftValues { + for j, rightValue := range rightValues { + neighbours, err := neighbours(leftValue, rightValue) + if err != nil { + return nil, fmt.Errorf("error determining adjacency for %v and %v: %s", leftValue, rightValue, err.Error()) + } + + if neighbours { + edges = append(edges, Edge{Node1: left[i].ID, Node2: right[j].ID}) + } + } + } + + return &BipartiteGraph{left, right, edges}, nil +} + +// FreeLeftRight returns left node values and right node values +// of the BipartiteGraph's nodes which are not part of the given edges. +func (bg *BipartiteGraph) FreeLeftRight(edges EdgeSet) (leftValues, rightValues []interface{}) { + for _, node := range bg.Left { + if edges.Free(node) { + leftValues = append(leftValues, node.Value) + } + } + for _, node := range bg.Right { + if edges.Free(node) { + rightValues = append(rightValues, node.Value) + } + } + return +} diff --git a/vendor/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraphmatching.go b/vendor/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraphmatching.go new file mode 100644 index 00000000000..1c54edd8f17 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/support/goraph/bipartitegraph/bipartitegraphmatching.go @@ -0,0 +1,164 @@ +package bipartitegraph + +import ( + . "github.com/onsi/gomega/matchers/support/goraph/edge" + . "github.com/onsi/gomega/matchers/support/goraph/node" + "github.com/onsi/gomega/matchers/support/goraph/util" +) + +// LargestMatching implements the Hopcroft–Karp algorithm taking as input a bipartite graph +// and outputting a maximum cardinality matching, i.e. a set of as many edges as possible +// with the property that no two edges share an endpoint. +func (bg *BipartiteGraph) LargestMatching() (matching EdgeSet) { + paths := bg.maximalDisjointSLAPCollection(matching) + + for len(paths) > 0 { + for _, path := range paths { + matching = matching.SymmetricDifference(path) + } + paths = bg.maximalDisjointSLAPCollection(matching) + } + + return +} + +func (bg *BipartiteGraph) maximalDisjointSLAPCollection(matching EdgeSet) (result []EdgeSet) { + guideLayers := bg.createSLAPGuideLayers(matching) + if len(guideLayers) == 0 { + return + } + + used := make(map[int]bool) + + for _, u := range guideLayers[len(guideLayers)-1] { + slap, found := bg.findDisjointSLAP(u, matching, guideLayers, used) + if found { + for _, edge := range slap { + used[edge.Node1] = true + used[edge.Node2] = true + } + result = append(result, slap) + } + } + + return +} + +func (bg *BipartiteGraph) findDisjointSLAP( + start Node, + matching EdgeSet, + guideLayers []NodeOrderedSet, + used map[int]bool, +) ([]Edge, bool) { + return bg.findDisjointSLAPHelper(start, EdgeSet{}, len(guideLayers)-1, matching, guideLayers, used) +} + +func (bg *BipartiteGraph) findDisjointSLAPHelper( + currentNode Node, + currentSLAP EdgeSet, + currentLevel int, + matching EdgeSet, + guideLayers []NodeOrderedSet, + used map[int]bool, +) (EdgeSet, bool) { + used[currentNode.ID] = true + + if currentLevel == 0 { + return currentSLAP, true + } + + for _, nextNode := range guideLayers[currentLevel-1] { + if used[nextNode.ID] { + continue + } + + edge, found := bg.Edges.FindByNodes(currentNode, nextNode) + if !found { + continue + } + + if matching.Contains(edge) == util.Odd(currentLevel) { + continue + } + + currentSLAP = append(currentSLAP, edge) + slap, found := bg.findDisjointSLAPHelper(nextNode, currentSLAP, currentLevel-1, matching, guideLayers, used) + if found { + return slap, true + } + currentSLAP = currentSLAP[:len(currentSLAP)-1] + } + + used[currentNode.ID] = false + return nil, false +} + +func (bg *BipartiteGraph) createSLAPGuideLayers(matching EdgeSet) (guideLayers []NodeOrderedSet) { + used := make(map[int]bool) + currentLayer := NodeOrderedSet{} + + for _, node := range bg.Left { + if matching.Free(node) { + used[node.ID] = true + currentLayer = append(currentLayer, node) + } + } + + if len(currentLayer) == 0 { + return []NodeOrderedSet{} + } + guideLayers = append(guideLayers, currentLayer) + + done := false + + for !done { + lastLayer := currentLayer + currentLayer = NodeOrderedSet{} + + if util.Odd(len(guideLayers)) { + for _, leftNode := range lastLayer { + for _, rightNode := range bg.Right { + if used[rightNode.ID] { + continue + } + + edge, found := bg.Edges.FindByNodes(leftNode, rightNode) + if !found || matching.Contains(edge) { + continue + } + + currentLayer = append(currentLayer, rightNode) + used[rightNode.ID] = true + + if matching.Free(rightNode) { + done = true + } + } + } + } else { + for _, rightNode := range lastLayer { + for _, leftNode := range bg.Left { + if used[leftNode.ID] { + continue + } + + edge, found := bg.Edges.FindByNodes(leftNode, rightNode) + if !found || !matching.Contains(edge) { + continue + } + + currentLayer = append(currentLayer, leftNode) + used[leftNode.ID] = true + } + } + + } + + if len(currentLayer) == 0 { + return []NodeOrderedSet{} + } + guideLayers = append(guideLayers, currentLayer) + } + + return +} diff --git a/vendor/github.com/onsi/gomega/matchers/support/goraph/edge/edge.go b/vendor/github.com/onsi/gomega/matchers/support/goraph/edge/edge.go new file mode 100644 index 00000000000..8c38411b283 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/support/goraph/edge/edge.go @@ -0,0 +1,61 @@ +package edge + +import . "github.com/onsi/gomega/matchers/support/goraph/node" + +type Edge struct { + Node1 int + Node2 int +} + +type EdgeSet []Edge + +func (ec EdgeSet) Free(node Node) bool { + for _, e := range ec { + if e.Node1 == node.ID || e.Node2 == node.ID { + return false + } + } + + return true +} + +func (ec EdgeSet) Contains(edge Edge) bool { + for _, e := range ec { + if e == edge { + return true + } + } + + return false +} + +func (ec EdgeSet) FindByNodes(node1, node2 Node) (Edge, bool) { + for _, e := range ec { + if (e.Node1 == node1.ID && e.Node2 == node2.ID) || (e.Node1 == node2.ID && e.Node2 == node1.ID) { + return e, true + } + } + + return Edge{}, false +} + +func (ec EdgeSet) SymmetricDifference(ec2 EdgeSet) EdgeSet { + edgesToInclude := make(map[Edge]bool) + + for _, e := range ec { + edgesToInclude[e] = true + } + + for _, e := range ec2 { + edgesToInclude[e] = !edgesToInclude[e] + } + + result := EdgeSet{} + for e, include := range edgesToInclude { + if include { + result = append(result, e) + } + } + + return result +} diff --git a/vendor/github.com/onsi/gomega/matchers/support/goraph/node/node.go b/vendor/github.com/onsi/gomega/matchers/support/goraph/node/node.go new file mode 100644 index 00000000000..cd597a2f220 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/support/goraph/node/node.go @@ -0,0 +1,8 @@ +package node + +type Node struct { + ID int + Value interface{} +} + +type NodeOrderedSet []Node diff --git a/vendor/github.com/onsi/gomega/matchers/support/goraph/util/util.go b/vendor/github.com/onsi/gomega/matchers/support/goraph/util/util.go new file mode 100644 index 00000000000..d76a1ee00a2 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/support/goraph/util/util.go @@ -0,0 +1,7 @@ +package util + +import "math" + +func Odd(n int) bool { + return math.Mod(float64(n), 2.0) == 1.0 +} diff --git a/vendor/github.com/onsi/gomega/matchers/type_support.go b/vendor/github.com/onsi/gomega/matchers/type_support.go new file mode 100644 index 00000000000..dced2419ead --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/type_support.go @@ -0,0 +1,182 @@ +/* +Gomega matchers + +This package implements the Gomega matchers and does not typically need to be imported. +See the docs for Gomega for documentation on the matchers + +http://onsi.github.io/gomega/ +*/ + +// untested sections: 11 + +package matchers + +import ( + "encoding/json" + "fmt" + "reflect" +) + +type omegaMatcher interface { + Match(actual interface{}) (success bool, err error) + FailureMessage(actual interface{}) (message string) + NegatedFailureMessage(actual interface{}) (message string) +} + +func isBool(a interface{}) bool { + return reflect.TypeOf(a).Kind() == reflect.Bool +} + +func isNumber(a interface{}) bool { + if a == nil { + return false + } + kind := reflect.TypeOf(a).Kind() + return reflect.Int <= kind && kind <= reflect.Float64 +} + +func isInteger(a interface{}) bool { + kind := reflect.TypeOf(a).Kind() + return reflect.Int <= kind && kind <= reflect.Int64 +} + +func isUnsignedInteger(a interface{}) bool { + kind := reflect.TypeOf(a).Kind() + return reflect.Uint <= kind && kind <= reflect.Uint64 +} + +func isFloat(a interface{}) bool { + kind := reflect.TypeOf(a).Kind() + return reflect.Float32 <= kind && kind <= reflect.Float64 +} + +func toInteger(a interface{}) int64 { + if isInteger(a) { + return reflect.ValueOf(a).Int() + } else if isUnsignedInteger(a) { + return int64(reflect.ValueOf(a).Uint()) + } else if isFloat(a) { + return int64(reflect.ValueOf(a).Float()) + } + panic(fmt.Sprintf("Expected a number! Got <%T> %#v", a, a)) +} + +func toUnsignedInteger(a interface{}) uint64 { + if isInteger(a) { + return uint64(reflect.ValueOf(a).Int()) + } else if isUnsignedInteger(a) { + return reflect.ValueOf(a).Uint() + } else if isFloat(a) { + return uint64(reflect.ValueOf(a).Float()) + } + panic(fmt.Sprintf("Expected a number! Got <%T> %#v", a, a)) +} + +func toFloat(a interface{}) float64 { + if isInteger(a) { + return float64(reflect.ValueOf(a).Int()) + } else if isUnsignedInteger(a) { + return float64(reflect.ValueOf(a).Uint()) + } else if isFloat(a) { + return reflect.ValueOf(a).Float() + } + panic(fmt.Sprintf("Expected a number! Got <%T> %#v", a, a)) +} + +func isError(a interface{}) bool { + _, ok := a.(error) + return ok +} + +func isChan(a interface{}) bool { + if isNil(a) { + return false + } + return reflect.TypeOf(a).Kind() == reflect.Chan +} + +func isMap(a interface{}) bool { + if a == nil { + return false + } + return reflect.TypeOf(a).Kind() == reflect.Map +} + +func isArrayOrSlice(a interface{}) bool { + if a == nil { + return false + } + switch reflect.TypeOf(a).Kind() { + case reflect.Array, reflect.Slice: + return true + default: + return false + } +} + +func isString(a interface{}) bool { + if a == nil { + return false + } + return reflect.TypeOf(a).Kind() == reflect.String +} + +func toString(a interface{}) (string, bool) { + aString, isString := a.(string) + if isString { + return aString, true + } + + aBytes, isBytes := a.([]byte) + if isBytes { + return string(aBytes), true + } + + aStringer, isStringer := a.(fmt.Stringer) + if isStringer { + return aStringer.String(), true + } + + aJSONRawMessage, isJSONRawMessage := a.(json.RawMessage) + if isJSONRawMessage { + return string(aJSONRawMessage), true + } + + return "", false +} + +func lengthOf(a interface{}) (int, bool) { + if a == nil { + return 0, false + } + switch reflect.TypeOf(a).Kind() { + case reflect.Map, reflect.Array, reflect.String, reflect.Chan, reflect.Slice: + return reflect.ValueOf(a).Len(), true + default: + return 0, false + } +} +func capOf(a interface{}) (int, bool) { + if a == nil { + return 0, false + } + switch reflect.TypeOf(a).Kind() { + case reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(a).Cap(), true + default: + return 0, false + } +} + +func isNil(a interface{}) bool { + if a == nil { + return true + } + + switch reflect.TypeOf(a).Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return reflect.ValueOf(a).IsNil() + } + + return false +} diff --git a/vendor/github.com/onsi/gomega/matchers/with_transform.go b/vendor/github.com/onsi/gomega/matchers/with_transform.go new file mode 100644 index 00000000000..6f743b1b32d --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/with_transform.go @@ -0,0 +1,90 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/types" +) + +type WithTransformMatcher struct { + // input + Transform interface{} // must be a function of one parameter that returns one value and an optional error + Matcher types.GomegaMatcher + + // cached value + transformArgType reflect.Type + + // state + transformedValue interface{} +} + +// reflect.Type for error +var errorT = reflect.TypeOf((*error)(nil)).Elem() + +func NewWithTransformMatcher(transform interface{}, matcher types.GomegaMatcher) *WithTransformMatcher { + if transform == nil { + panic("transform function cannot be nil") + } + txType := reflect.TypeOf(transform) + if txType.NumIn() != 1 { + panic("transform function must have 1 argument") + } + if numout := txType.NumOut(); numout != 1 { + if numout != 2 || !txType.Out(1).AssignableTo(errorT) { + panic("transform function must either have 1 return value, or 1 return value plus 1 error value") + } + } + + return &WithTransformMatcher{ + Transform: transform, + Matcher: matcher, + transformArgType: reflect.TypeOf(transform).In(0), + } +} + +func (m *WithTransformMatcher) Match(actual interface{}) (bool, error) { + // prepare a parameter to pass to the Transform function + var param reflect.Value + if actual != nil && reflect.TypeOf(actual).AssignableTo(m.transformArgType) { + // The dynamic type of actual is compatible with the transform argument. + param = reflect.ValueOf(actual) + + } else if actual == nil && m.transformArgType.Kind() == reflect.Interface { + // The dynamic type of actual is unknown, so there's no way to make its + // reflect.Value. Create a nil of the transform argument, which is known. + param = reflect.Zero(m.transformArgType) + + } else { + return false, fmt.Errorf("Transform function expects '%s' but we have '%T'", m.transformArgType, actual) + } + + // call the Transform function with `actual` + fn := reflect.ValueOf(m.Transform) + result := fn.Call([]reflect.Value{param}) + if len(result) == 2 { + if !result[1].IsNil() { + return false, fmt.Errorf("Transform function failed: %s", result[1].Interface().(error).Error()) + } + } + m.transformedValue = result[0].Interface() // expect exactly one value + + return m.Matcher.Match(m.transformedValue) +} + +func (m *WithTransformMatcher) FailureMessage(_ interface{}) (message string) { + return m.Matcher.FailureMessage(m.transformedValue) +} + +func (m *WithTransformMatcher) NegatedFailureMessage(_ interface{}) (message string) { + return m.Matcher.NegatedFailureMessage(m.transformedValue) +} + +func (m *WithTransformMatcher) MatchMayChangeInTheFuture(_ interface{}) bool { + // TODO: Maybe this should always just return true? (Only an issue for non-deterministic transformers.) + // + // Querying the next matcher is fine if the transformer always will return the same value. + // But if the transformer is non-deterministic and returns a different value each time, then there + // is no point in querying the next matcher, since it can only comment on the last transformed value. + return types.MatchMayChangeInTheFuture(m.Matcher, m.transformedValue) +} diff --git a/vendor/github.com/onsi/gomega/tools b/vendor/github.com/onsi/gomega/tools new file mode 100644 index 00000000000..e4195cf362f --- /dev/null +++ b/vendor/github.com/onsi/gomega/tools @@ -0,0 +1,8 @@ +//go:build tools +// +build tools + +package main + +import ( + _ "github.com/onsi/ginkgo/v2/ginkgo" +) diff --git a/vendor/github.com/onsi/gomega/types/types.go b/vendor/github.com/onsi/gomega/types/types.go new file mode 100644 index 00000000000..7c7adb9415d --- /dev/null +++ b/vendor/github.com/onsi/gomega/types/types.go @@ -0,0 +1,93 @@ +package types + +import ( + "context" + "time" +) + +type GomegaFailHandler func(message string, callerSkip ...int) + +// A simple *testing.T interface wrapper +type GomegaTestingT interface { + Helper() + Fatalf(format string, args ...interface{}) +} + +// Gomega represents an object that can perform synchronous and assynchronous assertions with Gomega matchers +type Gomega interface { + Ω(actual interface{}, extra ...interface{}) Assertion + Expect(actual interface{}, extra ...interface{}) Assertion + ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Assertion + + Eventually(actualOrCtx interface{}, args ...interface{}) AsyncAssertion + EventuallyWithOffset(offset int, actualOrCtx interface{}, args ...interface{}) AsyncAssertion + + Consistently(actualOrCtx interface{}, args ...interface{}) AsyncAssertion + ConsistentlyWithOffset(offset int, actualOrCtx interface{}, args ...interface{}) AsyncAssertion + + SetDefaultEventuallyTimeout(time.Duration) + SetDefaultEventuallyPollingInterval(time.Duration) + SetDefaultConsistentlyDuration(time.Duration) + SetDefaultConsistentlyPollingInterval(time.Duration) +} + +// All Gomega matchers must implement the GomegaMatcher interface +// +// For details on writing custom matchers, check out: http://onsi.github.io/gomega/#adding-your-own-matchers +type GomegaMatcher interface { + Match(actual interface{}) (success bool, err error) + FailureMessage(actual interface{}) (message string) + NegatedFailureMessage(actual interface{}) (message string) +} + +/* +GomegaMatchers that also match the OracleMatcher interface can convey information about +whether or not their result will change upon future attempts. + +This allows `Eventually` and `Consistently` to short circuit if success becomes impossible. + +For example, a process' exit code can never change. So, gexec's Exit matcher returns `true` +for `MatchMayChangeInTheFuture` until the process exits, at which point it returns `false` forevermore. +*/ +type OracleMatcher interface { + MatchMayChangeInTheFuture(actual interface{}) bool +} + +func MatchMayChangeInTheFuture(matcher GomegaMatcher, value interface{}) bool { + oracleMatcher, ok := matcher.(OracleMatcher) + if !ok { + return true + } + + return oracleMatcher.MatchMayChangeInTheFuture(value) +} + +// AsyncAssertions are returned by Eventually and Consistently and enable matchers to be polled repeatedly to ensure +// they are eventually satisfied +type AsyncAssertion interface { + Should(matcher GomegaMatcher, optionalDescription ...interface{}) bool + ShouldNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool + + WithOffset(offset int) AsyncAssertion + WithTimeout(interval time.Duration) AsyncAssertion + WithPolling(interval time.Duration) AsyncAssertion + Within(timeout time.Duration) AsyncAssertion + ProbeEvery(interval time.Duration) AsyncAssertion + WithContext(ctx context.Context) AsyncAssertion + WithArguments(argsToForward ...interface{}) AsyncAssertion + MustPassRepeatedly(count int) AsyncAssertion +} + +// Assertions are returned by Ω and Expect and enable assertions against Gomega matchers +type Assertion interface { + Should(matcher GomegaMatcher, optionalDescription ...interface{}) bool + ShouldNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool + + To(matcher GomegaMatcher, optionalDescription ...interface{}) bool + ToNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool + NotTo(matcher GomegaMatcher, optionalDescription ...interface{}) bool + + WithOffset(offset int) Assertion + + Error() Assertion +} diff --git a/vendor/golang.org/x/net/html/atom/atom.go b/vendor/golang.org/x/net/html/atom/atom.go new file mode 100644 index 00000000000..cd0a8ac1545 --- /dev/null +++ b/vendor/golang.org/x/net/html/atom/atom.go @@ -0,0 +1,78 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package atom provides integer codes (also known as atoms) for a fixed set of +// frequently occurring HTML strings: tag names and attribute keys such as "p" +// and "id". +// +// Sharing an atom's name between all elements with the same tag can result in +// fewer string allocations when tokenizing and parsing HTML. Integer +// comparisons are also generally faster than string comparisons. +// +// The value of an atom's particular code is not guaranteed to stay the same +// between versions of this package. Neither is any ordering guaranteed: +// whether atom.H1 < atom.H2 may also change. The codes are not guaranteed to +// be dense. The only guarantees are that e.g. looking up "div" will yield +// atom.Div, calling atom.Div.String will return "div", and atom.Div != 0. +package atom // import "golang.org/x/net/html/atom" + +// Atom is an integer code for a string. The zero value maps to "". +type Atom uint32 + +// String returns the atom's name. +func (a Atom) String() string { + start := uint32(a >> 8) + n := uint32(a & 0xff) + if start+n > uint32(len(atomText)) { + return "" + } + return atomText[start : start+n] +} + +func (a Atom) string() string { + return atomText[a>>8 : a>>8+a&0xff] +} + +// fnv computes the FNV hash with an arbitrary starting value h. +func fnv(h uint32, s []byte) uint32 { + for i := range s { + h ^= uint32(s[i]) + h *= 16777619 + } + return h +} + +func match(s string, t []byte) bool { + for i, c := range t { + if s[i] != c { + return false + } + } + return true +} + +// Lookup returns the atom whose name is s. It returns zero if there is no +// such atom. The lookup is case sensitive. +func Lookup(s []byte) Atom { + if len(s) == 0 || len(s) > maxAtomLen { + return 0 + } + h := fnv(hash0, s) + if a := table[h&uint32(len(table)-1)]; int(a&0xff) == len(s) && match(a.string(), s) { + return a + } + if a := table[(h>>16)&uint32(len(table)-1)]; int(a&0xff) == len(s) && match(a.string(), s) { + return a + } + return 0 +} + +// String returns a string whose contents are equal to s. In that sense, it is +// equivalent to string(s) but may be more efficient. +func String(s []byte) string { + if a := Lookup(s); a != 0 { + return a.String() + } + return string(s) +} diff --git a/vendor/golang.org/x/net/html/atom/table.go b/vendor/golang.org/x/net/html/atom/table.go new file mode 100644 index 00000000000..2a938864cb9 --- /dev/null +++ b/vendor/golang.org/x/net/html/atom/table.go @@ -0,0 +1,783 @@ +// Code generated by go generate gen.go; DO NOT EDIT. + +//go:generate go run gen.go + +package atom + +const ( + A Atom = 0x1 + Abbr Atom = 0x4 + Accept Atom = 0x1a06 + AcceptCharset Atom = 0x1a0e + Accesskey Atom = 0x2c09 + Acronym Atom = 0xaa07 + Action Atom = 0x27206 + Address Atom = 0x6f307 + Align Atom = 0xb105 + Allowfullscreen Atom = 0x2080f + Allowpaymentrequest Atom = 0xc113 + Allowusermedia Atom = 0xdd0e + Alt Atom = 0xf303 + Annotation Atom = 0x1c90a + AnnotationXml Atom = 0x1c90e + Applet Atom = 0x31906 + Area Atom = 0x35604 + Article Atom = 0x3fc07 + As Atom = 0x3c02 + Aside Atom = 0x10705 + Async Atom = 0xff05 + Audio Atom = 0x11505 + Autocomplete Atom = 0x2780c + Autofocus Atom = 0x12109 + Autoplay Atom = 0x13c08 + B Atom = 0x101 + Base Atom = 0x3b04 + Basefont Atom = 0x3b08 + Bdi Atom = 0xba03 + Bdo Atom = 0x14b03 + Bgsound Atom = 0x15e07 + Big Atom = 0x17003 + Blink Atom = 0x17305 + Blockquote Atom = 0x1870a + Body Atom = 0x2804 + Br Atom = 0x202 + Button Atom = 0x19106 + Canvas Atom = 0x10306 + Caption Atom = 0x23107 + Center Atom = 0x22006 + Challenge Atom = 0x29b09 + Charset Atom = 0x2107 + Checked Atom = 0x47907 + Cite Atom = 0x19c04 + Class Atom = 0x56405 + Code Atom = 0x5c504 + Col Atom = 0x1ab03 + Colgroup Atom = 0x1ab08 + Color Atom = 0x1bf05 + Cols Atom = 0x1c404 + Colspan Atom = 0x1c407 + Command Atom = 0x1d707 + Content Atom = 0x58b07 + Contenteditable Atom = 0x58b0f + Contextmenu Atom = 0x3800b + Controls Atom = 0x1de08 + Coords Atom = 0x1ea06 + Crossorigin Atom = 0x1fb0b + Data Atom = 0x4a504 + Datalist Atom = 0x4a508 + Datetime Atom = 0x2b808 + Dd Atom = 0x2d702 + Default Atom = 0x10a07 + Defer Atom = 0x5c705 + Del Atom = 0x45203 + Desc Atom = 0x56104 + Details Atom = 0x7207 + Dfn Atom = 0x8703 + Dialog Atom = 0xbb06 + Dir Atom = 0x9303 + Dirname Atom = 0x9307 + Disabled Atom = 0x16408 + Div Atom = 0x16b03 + Dl Atom = 0x5e602 + Download Atom = 0x46308 + Draggable Atom = 0x17a09 + Dropzone Atom = 0x40508 + Dt Atom = 0x64b02 + Em Atom = 0x6e02 + Embed Atom = 0x6e05 + Enctype Atom = 0x28d07 + Face Atom = 0x21e04 + Fieldset Atom = 0x22608 + Figcaption Atom = 0x22e0a + Figure Atom = 0x24806 + Font Atom = 0x3f04 + Footer Atom = 0xf606 + For Atom = 0x25403 + ForeignObject Atom = 0x2540d + Foreignobject Atom = 0x2610d + Form Atom = 0x26e04 + Formaction Atom = 0x26e0a + Formenctype Atom = 0x2890b + Formmethod Atom = 0x2a40a + Formnovalidate Atom = 0x2ae0e + Formtarget Atom = 0x2c00a + Frame Atom = 0x8b05 + Frameset Atom = 0x8b08 + H1 Atom = 0x15c02 + H2 Atom = 0x2de02 + H3 Atom = 0x30d02 + H4 Atom = 0x34502 + H5 Atom = 0x34f02 + H6 Atom = 0x64d02 + Head Atom = 0x33104 + Header Atom = 0x33106 + Headers Atom = 0x33107 + Height Atom = 0x5206 + Hgroup Atom = 0x2ca06 + Hidden Atom = 0x2d506 + High Atom = 0x2db04 + Hr Atom = 0x15702 + Href Atom = 0x2e004 + Hreflang Atom = 0x2e008 + Html Atom = 0x5604 + HttpEquiv Atom = 0x2e80a + I Atom = 0x601 + Icon Atom = 0x58a04 + Id Atom = 0x10902 + Iframe Atom = 0x2fc06 + Image Atom = 0x30205 + Img Atom = 0x30703 + Input Atom = 0x44b05 + Inputmode Atom = 0x44b09 + Ins Atom = 0x20403 + Integrity Atom = 0x23f09 + Is Atom = 0x16502 + Isindex Atom = 0x30f07 + Ismap Atom = 0x31605 + Itemid Atom = 0x38b06 + Itemprop Atom = 0x19d08 + Itemref Atom = 0x3cd07 + Itemscope Atom = 0x67109 + Itemtype Atom = 0x31f08 + Kbd Atom = 0xb903 + Keygen Atom = 0x3206 + Keytype Atom = 0xd607 + Kind Atom = 0x17704 + Label Atom = 0x5905 + Lang Atom = 0x2e404 + Legend Atom = 0x18106 + Li Atom = 0xb202 + Link Atom = 0x17404 + List Atom = 0x4a904 + Listing Atom = 0x4a907 + Loop Atom = 0x5d04 + Low Atom = 0xc303 + Main Atom = 0x1004 + Malignmark Atom = 0xb00a + Manifest Atom = 0x6d708 + Map Atom = 0x31803 + Mark Atom = 0xb604 + Marquee Atom = 0x32707 + Math Atom = 0x32e04 + Max Atom = 0x33d03 + Maxlength Atom = 0x33d09 + Media Atom = 0xe605 + Mediagroup Atom = 0xe60a + Menu Atom = 0x38704 + Menuitem Atom = 0x38708 + Meta Atom = 0x4b804 + Meter Atom = 0x9805 + Method Atom = 0x2a806 + Mglyph Atom = 0x30806 + Mi Atom = 0x34702 + Min Atom = 0x34703 + Minlength Atom = 0x34709 + Mn Atom = 0x2b102 + Mo Atom = 0xa402 + Ms Atom = 0x67402 + Mtext Atom = 0x35105 + Multiple Atom = 0x35f08 + Muted Atom = 0x36705 + Name Atom = 0x9604 + Nav Atom = 0x1303 + Nobr Atom = 0x3704 + Noembed Atom = 0x6c07 + Noframes Atom = 0x8908 + Nomodule Atom = 0xa208 + Nonce Atom = 0x1a605 + Noscript Atom = 0x21608 + Novalidate Atom = 0x2b20a + Object Atom = 0x26806 + Ol Atom = 0x13702 + Onabort Atom = 0x19507 + Onafterprint Atom = 0x2360c + Onautocomplete Atom = 0x2760e + Onautocompleteerror Atom = 0x27613 + Onauxclick Atom = 0x61f0a + Onbeforeprint Atom = 0x69e0d + Onbeforeunload Atom = 0x6e70e + Onblur Atom = 0x56d06 + Oncancel Atom = 0x11908 + Oncanplay Atom = 0x14d09 + Oncanplaythrough Atom = 0x14d10 + Onchange Atom = 0x41b08 + Onclick Atom = 0x2f507 + Onclose Atom = 0x36c07 + Oncontextmenu Atom = 0x37e0d + Oncopy Atom = 0x39106 + Oncuechange Atom = 0x3970b + Oncut Atom = 0x3a205 + Ondblclick Atom = 0x3a70a + Ondrag Atom = 0x3b106 + Ondragend Atom = 0x3b109 + Ondragenter Atom = 0x3ba0b + Ondragexit Atom = 0x3c50a + Ondragleave Atom = 0x3df0b + Ondragover Atom = 0x3ea0a + Ondragstart Atom = 0x3f40b + Ondrop Atom = 0x40306 + Ondurationchange Atom = 0x41310 + Onemptied Atom = 0x40a09 + Onended Atom = 0x42307 + Onerror Atom = 0x42a07 + Onfocus Atom = 0x43107 + Onhashchange Atom = 0x43d0c + Oninput Atom = 0x44907 + Oninvalid Atom = 0x45509 + Onkeydown Atom = 0x45e09 + Onkeypress Atom = 0x46b0a + Onkeyup Atom = 0x48007 + Onlanguagechange Atom = 0x48d10 + Onload Atom = 0x49d06 + Onloadeddata Atom = 0x49d0c + Onloadedmetadata Atom = 0x4b010 + Onloadend Atom = 0x4c609 + Onloadstart Atom = 0x4cf0b + Onmessage Atom = 0x4da09 + Onmessageerror Atom = 0x4da0e + Onmousedown Atom = 0x4e80b + Onmouseenter Atom = 0x4f30c + Onmouseleave Atom = 0x4ff0c + Onmousemove Atom = 0x50b0b + Onmouseout Atom = 0x5160a + Onmouseover Atom = 0x5230b + Onmouseup Atom = 0x52e09 + Onmousewheel Atom = 0x53c0c + Onoffline Atom = 0x54809 + Ononline Atom = 0x55108 + Onpagehide Atom = 0x5590a + Onpageshow Atom = 0x5730a + Onpaste Atom = 0x57f07 + Onpause Atom = 0x59a07 + Onplay Atom = 0x5a406 + Onplaying Atom = 0x5a409 + Onpopstate Atom = 0x5ad0a + Onprogress Atom = 0x5b70a + Onratechange Atom = 0x5cc0c + Onrejectionhandled Atom = 0x5d812 + Onreset Atom = 0x5ea07 + Onresize Atom = 0x5f108 + Onscroll Atom = 0x60008 + Onsecuritypolicyviolation Atom = 0x60819 + Onseeked Atom = 0x62908 + Onseeking Atom = 0x63109 + Onselect Atom = 0x63a08 + Onshow Atom = 0x64406 + Onsort Atom = 0x64f06 + Onstalled Atom = 0x65909 + Onstorage Atom = 0x66209 + Onsubmit Atom = 0x66b08 + Onsuspend Atom = 0x67b09 + Ontimeupdate Atom = 0x400c + Ontoggle Atom = 0x68408 + Onunhandledrejection Atom = 0x68c14 + Onunload Atom = 0x6ab08 + Onvolumechange Atom = 0x6b30e + Onwaiting Atom = 0x6c109 + Onwheel Atom = 0x6ca07 + Open Atom = 0x1a304 + Optgroup Atom = 0x5f08 + Optimum Atom = 0x6d107 + Option Atom = 0x6e306 + Output Atom = 0x51d06 + P Atom = 0xc01 + Param Atom = 0xc05 + Pattern Atom = 0x6607 + Picture Atom = 0x7b07 + Ping Atom = 0xef04 + Placeholder Atom = 0x1310b + Plaintext Atom = 0x1b209 + Playsinline Atom = 0x1400b + Poster Atom = 0x2cf06 + Pre Atom = 0x47003 + Preload Atom = 0x48607 + Progress Atom = 0x5b908 + Prompt Atom = 0x53606 + Public Atom = 0x58606 + Q Atom = 0xcf01 + Radiogroup Atom = 0x30a + Rb Atom = 0x3a02 + Readonly Atom = 0x35708 + Referrerpolicy Atom = 0x3d10e + Rel Atom = 0x48703 + Required Atom = 0x24c08 + Reversed Atom = 0x8008 + Rows Atom = 0x9c04 + Rowspan Atom = 0x9c07 + Rp Atom = 0x23c02 + Rt Atom = 0x19a02 + Rtc Atom = 0x19a03 + Ruby Atom = 0xfb04 + S Atom = 0x2501 + Samp Atom = 0x7804 + Sandbox Atom = 0x12907 + Scope Atom = 0x67505 + Scoped Atom = 0x67506 + Script Atom = 0x21806 + Seamless Atom = 0x37108 + Section Atom = 0x56807 + Select Atom = 0x63c06 + Selected Atom = 0x63c08 + Shape Atom = 0x1e505 + Size Atom = 0x5f504 + Sizes Atom = 0x5f505 + Slot Atom = 0x1ef04 + Small Atom = 0x20605 + Sortable Atom = 0x65108 + Sorted Atom = 0x33706 + Source Atom = 0x37806 + Spacer Atom = 0x43706 + Span Atom = 0x9f04 + Spellcheck Atom = 0x4740a + Src Atom = 0x5c003 + Srcdoc Atom = 0x5c006 + Srclang Atom = 0x5f907 + Srcset Atom = 0x6f906 + Start Atom = 0x3fa05 + Step Atom = 0x58304 + Strike Atom = 0xd206 + Strong Atom = 0x6dd06 + Style Atom = 0x6ff05 + Sub Atom = 0x66d03 + Summary Atom = 0x70407 + Sup Atom = 0x70b03 + Svg Atom = 0x70e03 + System Atom = 0x71106 + Tabindex Atom = 0x4be08 + Table Atom = 0x59505 + Target Atom = 0x2c406 + Tbody Atom = 0x2705 + Td Atom = 0x9202 + Template Atom = 0x71408 + Textarea Atom = 0x35208 + Tfoot Atom = 0xf505 + Th Atom = 0x15602 + Thead Atom = 0x33005 + Time Atom = 0x4204 + Title Atom = 0x11005 + Tr Atom = 0xcc02 + Track Atom = 0x1ba05 + Translate Atom = 0x1f209 + Tt Atom = 0x6802 + Type Atom = 0xd904 + Typemustmatch Atom = 0x2900d + U Atom = 0xb01 + Ul Atom = 0xa702 + Updateviacache Atom = 0x460e + Usemap Atom = 0x59e06 + Value Atom = 0x1505 + Var Atom = 0x16d03 + Video Atom = 0x2f105 + Wbr Atom = 0x57c03 + Width Atom = 0x64905 + Workertype Atom = 0x71c0a + Wrap Atom = 0x72604 + Xmp Atom = 0x12f03 +) + +const hash0 = 0x81cdf10e + +const maxAtomLen = 25 + +var table = [1 << 9]Atom{ + 0x1: 0xe60a, // mediagroup + 0x2: 0x2e404, // lang + 0x4: 0x2c09, // accesskey + 0x5: 0x8b08, // frameset + 0x7: 0x63a08, // onselect + 0x8: 0x71106, // system + 0xa: 0x64905, // width + 0xc: 0x2890b, // formenctype + 0xd: 0x13702, // ol + 0xe: 0x3970b, // oncuechange + 0x10: 0x14b03, // bdo + 0x11: 0x11505, // audio + 0x12: 0x17a09, // draggable + 0x14: 0x2f105, // video + 0x15: 0x2b102, // mn + 0x16: 0x38704, // menu + 0x17: 0x2cf06, // poster + 0x19: 0xf606, // footer + 0x1a: 0x2a806, // method + 0x1b: 0x2b808, // datetime + 0x1c: 0x19507, // onabort + 0x1d: 0x460e, // updateviacache + 0x1e: 0xff05, // async + 0x1f: 0x49d06, // onload + 0x21: 0x11908, // oncancel + 0x22: 0x62908, // onseeked + 0x23: 0x30205, // image + 0x24: 0x5d812, // onrejectionhandled + 0x26: 0x17404, // link + 0x27: 0x51d06, // output + 0x28: 0x33104, // head + 0x29: 0x4ff0c, // onmouseleave + 0x2a: 0x57f07, // onpaste + 0x2b: 0x5a409, // onplaying + 0x2c: 0x1c407, // colspan + 0x2f: 0x1bf05, // color + 0x30: 0x5f504, // size + 0x31: 0x2e80a, // http-equiv + 0x33: 0x601, // i + 0x34: 0x5590a, // onpagehide + 0x35: 0x68c14, // onunhandledrejection + 0x37: 0x42a07, // onerror + 0x3a: 0x3b08, // basefont + 0x3f: 0x1303, // nav + 0x40: 0x17704, // kind + 0x41: 0x35708, // readonly + 0x42: 0x30806, // mglyph + 0x44: 0xb202, // li + 0x46: 0x2d506, // hidden + 0x47: 0x70e03, // svg + 0x48: 0x58304, // step + 0x49: 0x23f09, // integrity + 0x4a: 0x58606, // public + 0x4c: 0x1ab03, // col + 0x4d: 0x1870a, // blockquote + 0x4e: 0x34f02, // h5 + 0x50: 0x5b908, // progress + 0x51: 0x5f505, // sizes + 0x52: 0x34502, // h4 + 0x56: 0x33005, // thead + 0x57: 0xd607, // keytype + 0x58: 0x5b70a, // onprogress + 0x59: 0x44b09, // inputmode + 0x5a: 0x3b109, // ondragend + 0x5d: 0x3a205, // oncut + 0x5e: 0x43706, // spacer + 0x5f: 0x1ab08, // colgroup + 0x62: 0x16502, // is + 0x65: 0x3c02, // as + 0x66: 0x54809, // onoffline + 0x67: 0x33706, // sorted + 0x69: 0x48d10, // onlanguagechange + 0x6c: 0x43d0c, // onhashchange + 0x6d: 0x9604, // name + 0x6e: 0xf505, // tfoot + 0x6f: 0x56104, // desc + 0x70: 0x33d03, // max + 0x72: 0x1ea06, // coords + 0x73: 0x30d02, // h3 + 0x74: 0x6e70e, // onbeforeunload + 0x75: 0x9c04, // rows + 0x76: 0x63c06, // select + 0x77: 0x9805, // meter + 0x78: 0x38b06, // itemid + 0x79: 0x53c0c, // onmousewheel + 0x7a: 0x5c006, // srcdoc + 0x7d: 0x1ba05, // track + 0x7f: 0x31f08, // itemtype + 0x82: 0xa402, // mo + 0x83: 0x41b08, // onchange + 0x84: 0x33107, // headers + 0x85: 0x5cc0c, // onratechange + 0x86: 0x60819, // onsecuritypolicyviolation + 0x88: 0x4a508, // datalist + 0x89: 0x4e80b, // onmousedown + 0x8a: 0x1ef04, // slot + 0x8b: 0x4b010, // onloadedmetadata + 0x8c: 0x1a06, // accept + 0x8d: 0x26806, // object + 0x91: 0x6b30e, // onvolumechange + 0x92: 0x2107, // charset + 0x93: 0x27613, // onautocompleteerror + 0x94: 0xc113, // allowpaymentrequest + 0x95: 0x2804, // body + 0x96: 0x10a07, // default + 0x97: 0x63c08, // selected + 0x98: 0x21e04, // face + 0x99: 0x1e505, // shape + 0x9b: 0x68408, // ontoggle + 0x9e: 0x64b02, // dt + 0x9f: 0xb604, // mark + 0xa1: 0xb01, // u + 0xa4: 0x6ab08, // onunload + 0xa5: 0x5d04, // loop + 0xa6: 0x16408, // disabled + 0xaa: 0x42307, // onended + 0xab: 0xb00a, // malignmark + 0xad: 0x67b09, // onsuspend + 0xae: 0x35105, // mtext + 0xaf: 0x64f06, // onsort + 0xb0: 0x19d08, // itemprop + 0xb3: 0x67109, // itemscope + 0xb4: 0x17305, // blink + 0xb6: 0x3b106, // ondrag + 0xb7: 0xa702, // ul + 0xb8: 0x26e04, // form + 0xb9: 0x12907, // sandbox + 0xba: 0x8b05, // frame + 0xbb: 0x1505, // value + 0xbc: 0x66209, // onstorage + 0xbf: 0xaa07, // acronym + 0xc0: 0x19a02, // rt + 0xc2: 0x202, // br + 0xc3: 0x22608, // fieldset + 0xc4: 0x2900d, // typemustmatch + 0xc5: 0xa208, // nomodule + 0xc6: 0x6c07, // noembed + 0xc7: 0x69e0d, // onbeforeprint + 0xc8: 0x19106, // button + 0xc9: 0x2f507, // onclick + 0xca: 0x70407, // summary + 0xcd: 0xfb04, // ruby + 0xce: 0x56405, // class + 0xcf: 0x3f40b, // ondragstart + 0xd0: 0x23107, // caption + 0xd4: 0xdd0e, // allowusermedia + 0xd5: 0x4cf0b, // onloadstart + 0xd9: 0x16b03, // div + 0xda: 0x4a904, // list + 0xdb: 0x32e04, // math + 0xdc: 0x44b05, // input + 0xdf: 0x3ea0a, // ondragover + 0xe0: 0x2de02, // h2 + 0xe2: 0x1b209, // plaintext + 0xe4: 0x4f30c, // onmouseenter + 0xe7: 0x47907, // checked + 0xe8: 0x47003, // pre + 0xea: 0x35f08, // multiple + 0xeb: 0xba03, // bdi + 0xec: 0x33d09, // maxlength + 0xed: 0xcf01, // q + 0xee: 0x61f0a, // onauxclick + 0xf0: 0x57c03, // wbr + 0xf2: 0x3b04, // base + 0xf3: 0x6e306, // option + 0xf5: 0x41310, // ondurationchange + 0xf7: 0x8908, // noframes + 0xf9: 0x40508, // dropzone + 0xfb: 0x67505, // scope + 0xfc: 0x8008, // reversed + 0xfd: 0x3ba0b, // ondragenter + 0xfe: 0x3fa05, // start + 0xff: 0x12f03, // xmp + 0x100: 0x5f907, // srclang + 0x101: 0x30703, // img + 0x104: 0x101, // b + 0x105: 0x25403, // for + 0x106: 0x10705, // aside + 0x107: 0x44907, // oninput + 0x108: 0x35604, // area + 0x109: 0x2a40a, // formmethod + 0x10a: 0x72604, // wrap + 0x10c: 0x23c02, // rp + 0x10d: 0x46b0a, // onkeypress + 0x10e: 0x6802, // tt + 0x110: 0x34702, // mi + 0x111: 0x36705, // muted + 0x112: 0xf303, // alt + 0x113: 0x5c504, // code + 0x114: 0x6e02, // em + 0x115: 0x3c50a, // ondragexit + 0x117: 0x9f04, // span + 0x119: 0x6d708, // manifest + 0x11a: 0x38708, // menuitem + 0x11b: 0x58b07, // content + 0x11d: 0x6c109, // onwaiting + 0x11f: 0x4c609, // onloadend + 0x121: 0x37e0d, // oncontextmenu + 0x123: 0x56d06, // onblur + 0x124: 0x3fc07, // article + 0x125: 0x9303, // dir + 0x126: 0xef04, // ping + 0x127: 0x24c08, // required + 0x128: 0x45509, // oninvalid + 0x129: 0xb105, // align + 0x12b: 0x58a04, // icon + 0x12c: 0x64d02, // h6 + 0x12d: 0x1c404, // cols + 0x12e: 0x22e0a, // figcaption + 0x12f: 0x45e09, // onkeydown + 0x130: 0x66b08, // onsubmit + 0x131: 0x14d09, // oncanplay + 0x132: 0x70b03, // sup + 0x133: 0xc01, // p + 0x135: 0x40a09, // onemptied + 0x136: 0x39106, // oncopy + 0x137: 0x19c04, // cite + 0x138: 0x3a70a, // ondblclick + 0x13a: 0x50b0b, // onmousemove + 0x13c: 0x66d03, // sub + 0x13d: 0x48703, // rel + 0x13e: 0x5f08, // optgroup + 0x142: 0x9c07, // rowspan + 0x143: 0x37806, // source + 0x144: 0x21608, // noscript + 0x145: 0x1a304, // open + 0x146: 0x20403, // ins + 0x147: 0x2540d, // foreignObject + 0x148: 0x5ad0a, // onpopstate + 0x14a: 0x28d07, // enctype + 0x14b: 0x2760e, // onautocomplete + 0x14c: 0x35208, // textarea + 0x14e: 0x2780c, // autocomplete + 0x14f: 0x15702, // hr + 0x150: 0x1de08, // controls + 0x151: 0x10902, // id + 0x153: 0x2360c, // onafterprint + 0x155: 0x2610d, // foreignobject + 0x156: 0x32707, // marquee + 0x157: 0x59a07, // onpause + 0x158: 0x5e602, // dl + 0x159: 0x5206, // height + 0x15a: 0x34703, // min + 0x15b: 0x9307, // dirname + 0x15c: 0x1f209, // translate + 0x15d: 0x5604, // html + 0x15e: 0x34709, // minlength + 0x15f: 0x48607, // preload + 0x160: 0x71408, // template + 0x161: 0x3df0b, // ondragleave + 0x162: 0x3a02, // rb + 0x164: 0x5c003, // src + 0x165: 0x6dd06, // strong + 0x167: 0x7804, // samp + 0x168: 0x6f307, // address + 0x169: 0x55108, // ononline + 0x16b: 0x1310b, // placeholder + 0x16c: 0x2c406, // target + 0x16d: 0x20605, // small + 0x16e: 0x6ca07, // onwheel + 0x16f: 0x1c90a, // annotation + 0x170: 0x4740a, // spellcheck + 0x171: 0x7207, // details + 0x172: 0x10306, // canvas + 0x173: 0x12109, // autofocus + 0x174: 0xc05, // param + 0x176: 0x46308, // download + 0x177: 0x45203, // del + 0x178: 0x36c07, // onclose + 0x179: 0xb903, // kbd + 0x17a: 0x31906, // applet + 0x17b: 0x2e004, // href + 0x17c: 0x5f108, // onresize + 0x17e: 0x49d0c, // onloadeddata + 0x180: 0xcc02, // tr + 0x181: 0x2c00a, // formtarget + 0x182: 0x11005, // title + 0x183: 0x6ff05, // style + 0x184: 0xd206, // strike + 0x185: 0x59e06, // usemap + 0x186: 0x2fc06, // iframe + 0x187: 0x1004, // main + 0x189: 0x7b07, // picture + 0x18c: 0x31605, // ismap + 0x18e: 0x4a504, // data + 0x18f: 0x5905, // label + 0x191: 0x3d10e, // referrerpolicy + 0x192: 0x15602, // th + 0x194: 0x53606, // prompt + 0x195: 0x56807, // section + 0x197: 0x6d107, // optimum + 0x198: 0x2db04, // high + 0x199: 0x15c02, // h1 + 0x19a: 0x65909, // onstalled + 0x19b: 0x16d03, // var + 0x19c: 0x4204, // time + 0x19e: 0x67402, // ms + 0x19f: 0x33106, // header + 0x1a0: 0x4da09, // onmessage + 0x1a1: 0x1a605, // nonce + 0x1a2: 0x26e0a, // formaction + 0x1a3: 0x22006, // center + 0x1a4: 0x3704, // nobr + 0x1a5: 0x59505, // table + 0x1a6: 0x4a907, // listing + 0x1a7: 0x18106, // legend + 0x1a9: 0x29b09, // challenge + 0x1aa: 0x24806, // figure + 0x1ab: 0xe605, // media + 0x1ae: 0xd904, // type + 0x1af: 0x3f04, // font + 0x1b0: 0x4da0e, // onmessageerror + 0x1b1: 0x37108, // seamless + 0x1b2: 0x8703, // dfn + 0x1b3: 0x5c705, // defer + 0x1b4: 0xc303, // low + 0x1b5: 0x19a03, // rtc + 0x1b6: 0x5230b, // onmouseover + 0x1b7: 0x2b20a, // novalidate + 0x1b8: 0x71c0a, // workertype + 0x1ba: 0x3cd07, // itemref + 0x1bd: 0x1, // a + 0x1be: 0x31803, // map + 0x1bf: 0x400c, // ontimeupdate + 0x1c0: 0x15e07, // bgsound + 0x1c1: 0x3206, // keygen + 0x1c2: 0x2705, // tbody + 0x1c5: 0x64406, // onshow + 0x1c7: 0x2501, // s + 0x1c8: 0x6607, // pattern + 0x1cc: 0x14d10, // oncanplaythrough + 0x1ce: 0x2d702, // dd + 0x1cf: 0x6f906, // srcset + 0x1d0: 0x17003, // big + 0x1d2: 0x65108, // sortable + 0x1d3: 0x48007, // onkeyup + 0x1d5: 0x5a406, // onplay + 0x1d7: 0x4b804, // meta + 0x1d8: 0x40306, // ondrop + 0x1da: 0x60008, // onscroll + 0x1db: 0x1fb0b, // crossorigin + 0x1dc: 0x5730a, // onpageshow + 0x1dd: 0x4, // abbr + 0x1de: 0x9202, // td + 0x1df: 0x58b0f, // contenteditable + 0x1e0: 0x27206, // action + 0x1e1: 0x1400b, // playsinline + 0x1e2: 0x43107, // onfocus + 0x1e3: 0x2e008, // hreflang + 0x1e5: 0x5160a, // onmouseout + 0x1e6: 0x5ea07, // onreset + 0x1e7: 0x13c08, // autoplay + 0x1e8: 0x63109, // onseeking + 0x1ea: 0x67506, // scoped + 0x1ec: 0x30a, // radiogroup + 0x1ee: 0x3800b, // contextmenu + 0x1ef: 0x52e09, // onmouseup + 0x1f1: 0x2ca06, // hgroup + 0x1f2: 0x2080f, // allowfullscreen + 0x1f3: 0x4be08, // tabindex + 0x1f6: 0x30f07, // isindex + 0x1f7: 0x1a0e, // accept-charset + 0x1f8: 0x2ae0e, // formnovalidate + 0x1fb: 0x1c90e, // annotation-xml + 0x1fc: 0x6e05, // embed + 0x1fd: 0x21806, // script + 0x1fe: 0xbb06, // dialog + 0x1ff: 0x1d707, // command +} + +const atomText = "abbradiogrouparamainavalueaccept-charsetbodyaccesskeygenobrb" + + "asefontimeupdateviacacheightmlabelooptgroupatternoembedetail" + + "sampictureversedfnoframesetdirnameterowspanomoduleacronymali" + + "gnmarkbdialogallowpaymentrequestrikeytypeallowusermediagroup" + + "ingaltfooterubyasyncanvasidefaultitleaudioncancelautofocusan" + + "dboxmplaceholderautoplaysinlinebdoncanplaythrough1bgsoundisa" + + "bledivarbigblinkindraggablegendblockquotebuttonabortcitempro" + + "penoncecolgrouplaintextrackcolorcolspannotation-xmlcommandco" + + "ntrolshapecoordslotranslatecrossoriginsmallowfullscreenoscri" + + "ptfacenterfieldsetfigcaptionafterprintegrityfigurequiredfore" + + "ignObjectforeignobjectformactionautocompleteerrorformenctype" + + "mustmatchallengeformmethodformnovalidatetimeformtargethgroup" + + "osterhiddenhigh2hreflanghttp-equivideonclickiframeimageimgly" + + "ph3isindexismappletitemtypemarqueematheadersortedmaxlength4m" + + "inlength5mtextareadonlymultiplemutedoncloseamlessourceoncont" + + "extmenuitemidoncopyoncuechangeoncutondblclickondragendondrag" + + "enterondragexitemreferrerpolicyondragleaveondragoverondragst" + + "articleondropzonemptiedondurationchangeonendedonerroronfocus" + + "paceronhashchangeoninputmodeloninvalidonkeydownloadonkeypres" + + "spellcheckedonkeyupreloadonlanguagechangeonloadeddatalisting" + + "onloadedmetadatabindexonloadendonloadstartonmessageerroronmo" + + "usedownonmouseenteronmouseleaveonmousemoveonmouseoutputonmou" + + "seoveronmouseupromptonmousewheelonofflineononlineonpagehides" + + "classectionbluronpageshowbronpastepublicontenteditableonpaus" + + "emaponplayingonpopstateonprogressrcdocodeferonratechangeonre" + + "jectionhandledonresetonresizesrclangonscrollonsecuritypolicy" + + "violationauxclickonseekedonseekingonselectedonshowidth6onsor" + + "tableonstalledonstorageonsubmitemscopedonsuspendontoggleonun" + + "handledrejectionbeforeprintonunloadonvolumechangeonwaitingon" + + "wheeloptimumanifestrongoptionbeforeunloaddressrcsetstylesumm" + + "arysupsvgsystemplateworkertypewrap" diff --git a/vendor/golang.org/x/net/html/charset/charset.go b/vendor/golang.org/x/net/html/charset/charset.go new file mode 100644 index 00000000000..13bed1599f7 --- /dev/null +++ b/vendor/golang.org/x/net/html/charset/charset.go @@ -0,0 +1,257 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package charset provides common text encodings for HTML documents. +// +// The mapping from encoding labels to encodings is defined at +// https://encoding.spec.whatwg.org/. +package charset // import "golang.org/x/net/html/charset" + +import ( + "bytes" + "fmt" + "io" + "mime" + "strings" + "unicode/utf8" + + "golang.org/x/net/html" + "golang.org/x/text/encoding" + "golang.org/x/text/encoding/charmap" + "golang.org/x/text/encoding/htmlindex" + "golang.org/x/text/transform" +) + +// Lookup returns the encoding with the specified label, and its canonical +// name. It returns nil and the empty string if label is not one of the +// standard encodings for HTML. Matching is case-insensitive and ignores +// leading and trailing whitespace. Encoders will use HTML escape sequences for +// runes that are not supported by the character set. +func Lookup(label string) (e encoding.Encoding, name string) { + e, err := htmlindex.Get(label) + if err != nil { + return nil, "" + } + name, _ = htmlindex.Name(e) + return &htmlEncoding{e}, name +} + +type htmlEncoding struct{ encoding.Encoding } + +func (h *htmlEncoding) NewEncoder() *encoding.Encoder { + // HTML requires a non-terminating legacy encoder. We use HTML escapes to + // substitute unsupported code points. + return encoding.HTMLEscapeUnsupported(h.Encoding.NewEncoder()) +} + +// DetermineEncoding determines the encoding of an HTML document by examining +// up to the first 1024 bytes of content and the declared Content-Type. +// +// See http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#determining-the-character-encoding +func DetermineEncoding(content []byte, contentType string) (e encoding.Encoding, name string, certain bool) { + if len(content) > 1024 { + content = content[:1024] + } + + for _, b := range boms { + if bytes.HasPrefix(content, b.bom) { + e, name = Lookup(b.enc) + return e, name, true + } + } + + if _, params, err := mime.ParseMediaType(contentType); err == nil { + if cs, ok := params["charset"]; ok { + if e, name = Lookup(cs); e != nil { + return e, name, true + } + } + } + + if len(content) > 0 { + e, name = prescan(content) + if e != nil { + return e, name, false + } + } + + // Try to detect UTF-8. + // First eliminate any partial rune at the end. + for i := len(content) - 1; i >= 0 && i > len(content)-4; i-- { + b := content[i] + if b < 0x80 { + break + } + if utf8.RuneStart(b) { + content = content[:i] + break + } + } + hasHighBit := false + for _, c := range content { + if c >= 0x80 { + hasHighBit = true + break + } + } + if hasHighBit && utf8.Valid(content) { + return encoding.Nop, "utf-8", false + } + + // TODO: change default depending on user's locale? + return charmap.Windows1252, "windows-1252", false +} + +// NewReader returns an io.Reader that converts the content of r to UTF-8. +// It calls DetermineEncoding to find out what r's encoding is. +func NewReader(r io.Reader, contentType string) (io.Reader, error) { + preview := make([]byte, 1024) + n, err := io.ReadFull(r, preview) + switch { + case err == io.ErrUnexpectedEOF: + preview = preview[:n] + r = bytes.NewReader(preview) + case err != nil: + return nil, err + default: + r = io.MultiReader(bytes.NewReader(preview), r) + } + + if e, _, _ := DetermineEncoding(preview, contentType); e != encoding.Nop { + r = transform.NewReader(r, e.NewDecoder()) + } + return r, nil +} + +// NewReaderLabel returns a reader that converts from the specified charset to +// UTF-8. It uses Lookup to find the encoding that corresponds to label, and +// returns an error if Lookup returns nil. It is suitable for use as +// encoding/xml.Decoder's CharsetReader function. +func NewReaderLabel(label string, input io.Reader) (io.Reader, error) { + e, _ := Lookup(label) + if e == nil { + return nil, fmt.Errorf("unsupported charset: %q", label) + } + return transform.NewReader(input, e.NewDecoder()), nil +} + +func prescan(content []byte) (e encoding.Encoding, name string) { + z := html.NewTokenizer(bytes.NewReader(content)) + for { + switch z.Next() { + case html.ErrorToken: + return nil, "" + + case html.StartTagToken, html.SelfClosingTagToken: + tagName, hasAttr := z.TagName() + if !bytes.Equal(tagName, []byte("meta")) { + continue + } + attrList := make(map[string]bool) + gotPragma := false + + const ( + dontKnow = iota + doNeedPragma + doNotNeedPragma + ) + needPragma := dontKnow + + name = "" + e = nil + for hasAttr { + var key, val []byte + key, val, hasAttr = z.TagAttr() + ks := string(key) + if attrList[ks] { + continue + } + attrList[ks] = true + for i, c := range val { + if 'A' <= c && c <= 'Z' { + val[i] = c + 0x20 + } + } + + switch ks { + case "http-equiv": + if bytes.Equal(val, []byte("content-type")) { + gotPragma = true + } + + case "content": + if e == nil { + name = fromMetaElement(string(val)) + if name != "" { + e, name = Lookup(name) + if e != nil { + needPragma = doNeedPragma + } + } + } + + case "charset": + e, name = Lookup(string(val)) + needPragma = doNotNeedPragma + } + } + + if needPragma == dontKnow || needPragma == doNeedPragma && !gotPragma { + continue + } + + if strings.HasPrefix(name, "utf-16") { + name = "utf-8" + e = encoding.Nop + } + + if e != nil { + return e, name + } + } + } +} + +func fromMetaElement(s string) string { + for s != "" { + csLoc := strings.Index(s, "charset") + if csLoc == -1 { + return "" + } + s = s[csLoc+len("charset"):] + s = strings.TrimLeft(s, " \t\n\f\r") + if !strings.HasPrefix(s, "=") { + continue + } + s = s[1:] + s = strings.TrimLeft(s, " \t\n\f\r") + if s == "" { + return "" + } + if q := s[0]; q == '"' || q == '\'' { + s = s[1:] + closeQuote := strings.IndexRune(s, rune(q)) + if closeQuote == -1 { + return "" + } + return s[:closeQuote] + } + + end := strings.IndexAny(s, "; \t\n\f\r") + if end == -1 { + end = len(s) + } + return s[:end] + } + return "" +} + +var boms = []struct { + bom []byte + enc string +}{ + {[]byte{0xfe, 0xff}, "utf-16be"}, + {[]byte{0xff, 0xfe}, "utf-16le"}, + {[]byte{0xef, 0xbb, 0xbf}, "utf-8"}, +} diff --git a/vendor/golang.org/x/net/html/const.go b/vendor/golang.org/x/net/html/const.go new file mode 100644 index 00000000000..ff7acf2d5b4 --- /dev/null +++ b/vendor/golang.org/x/net/html/const.go @@ -0,0 +1,111 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +// Section 12.2.4.2 of the HTML5 specification says "The following elements +// have varying levels of special parsing rules". +// https://html.spec.whatwg.org/multipage/syntax.html#the-stack-of-open-elements +var isSpecialElementMap = map[string]bool{ + "address": true, + "applet": true, + "area": true, + "article": true, + "aside": true, + "base": true, + "basefont": true, + "bgsound": true, + "blockquote": true, + "body": true, + "br": true, + "button": true, + "caption": true, + "center": true, + "col": true, + "colgroup": true, + "dd": true, + "details": true, + "dir": true, + "div": true, + "dl": true, + "dt": true, + "embed": true, + "fieldset": true, + "figcaption": true, + "figure": true, + "footer": true, + "form": true, + "frame": true, + "frameset": true, + "h1": true, + "h2": true, + "h3": true, + "h4": true, + "h5": true, + "h6": true, + "head": true, + "header": true, + "hgroup": true, + "hr": true, + "html": true, + "iframe": true, + "img": true, + "input": true, + "keygen": true, // "keygen" has been removed from the spec, but are kept here for backwards compatibility. + "li": true, + "link": true, + "listing": true, + "main": true, + "marquee": true, + "menu": true, + "meta": true, + "nav": true, + "noembed": true, + "noframes": true, + "noscript": true, + "object": true, + "ol": true, + "p": true, + "param": true, + "plaintext": true, + "pre": true, + "script": true, + "section": true, + "select": true, + "source": true, + "style": true, + "summary": true, + "table": true, + "tbody": true, + "td": true, + "template": true, + "textarea": true, + "tfoot": true, + "th": true, + "thead": true, + "title": true, + "tr": true, + "track": true, + "ul": true, + "wbr": true, + "xmp": true, +} + +func isSpecialElement(element *Node) bool { + switch element.Namespace { + case "", "html": + return isSpecialElementMap[element.Data] + case "math": + switch element.Data { + case "mi", "mo", "mn", "ms", "mtext", "annotation-xml": + return true + } + case "svg": + switch element.Data { + case "foreignObject", "desc", "title": + return true + } + } + return false +} diff --git a/vendor/golang.org/x/net/html/doc.go b/vendor/golang.org/x/net/html/doc.go new file mode 100644 index 00000000000..822ed42a04c --- /dev/null +++ b/vendor/golang.org/x/net/html/doc.go @@ -0,0 +1,106 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package html implements an HTML5-compliant tokenizer and parser. + +Tokenization is done by creating a Tokenizer for an io.Reader r. It is the +caller's responsibility to ensure that r provides UTF-8 encoded HTML. + + z := html.NewTokenizer(r) + +Given a Tokenizer z, the HTML is tokenized by repeatedly calling z.Next(), +which parses the next token and returns its type, or an error: + + for { + tt := z.Next() + if tt == html.ErrorToken { + // ... + return ... + } + // Process the current token. + } + +There are two APIs for retrieving the current token. The high-level API is to +call Token; the low-level API is to call Text or TagName / TagAttr. Both APIs +allow optionally calling Raw after Next but before Token, Text, TagName, or +TagAttr. In EBNF notation, the valid call sequence per token is: + + Next {Raw} [ Token | Text | TagName {TagAttr} ] + +Token returns an independent data structure that completely describes a token. +Entities (such as "<") are unescaped, tag names and attribute keys are +lower-cased, and attributes are collected into a []Attribute. For example: + + for { + if z.Next() == html.ErrorToken { + // Returning io.EOF indicates success. + return z.Err() + } + emitToken(z.Token()) + } + +The low-level API performs fewer allocations and copies, but the contents of +the []byte values returned by Text, TagName and TagAttr may change on the next +call to Next. For example, to extract an HTML page's anchor text: + + depth := 0 + for { + tt := z.Next() + switch tt { + case html.ErrorToken: + return z.Err() + case html.TextToken: + if depth > 0 { + // emitBytes should copy the []byte it receives, + // if it doesn't process it immediately. + emitBytes(z.Text()) + } + case html.StartTagToken, html.EndTagToken: + tn, _ := z.TagName() + if len(tn) == 1 && tn[0] == 'a' { + if tt == html.StartTagToken { + depth++ + } else { + depth-- + } + } + } + } + +Parsing is done by calling Parse with an io.Reader, which returns the root of +the parse tree (the document element) as a *Node. It is the caller's +responsibility to ensure that the Reader provides UTF-8 encoded HTML. For +example, to process each anchor node in depth-first order: + + doc, err := html.Parse(r) + if err != nil { + // ... + } + var f func(*html.Node) + f = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "a" { + // Do something with n... + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + f(doc) + +The relevant specifications include: +https://html.spec.whatwg.org/multipage/syntax.html and +https://html.spec.whatwg.org/multipage/syntax.html#tokenization +*/ +package html // import "golang.org/x/net/html" + +// The tokenization algorithm implemented by this package is not a line-by-line +// transliteration of the relatively verbose state-machine in the WHATWG +// specification. A more direct approach is used instead, where the program +// counter implies the state, such as whether it is tokenizing a tag or a text +// node. Specification compliance is verified by checking expected and actual +// outputs over a test suite rather than aiming for algorithmic fidelity. + +// TODO(nigeltao): Does a DOM API belong in this package or a separate one? +// TODO(nigeltao): How does parsing interact with a JavaScript engine? diff --git a/vendor/golang.org/x/net/html/doctype.go b/vendor/golang.org/x/net/html/doctype.go new file mode 100644 index 00000000000..c484e5a94fb --- /dev/null +++ b/vendor/golang.org/x/net/html/doctype.go @@ -0,0 +1,156 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "strings" +) + +// parseDoctype parses the data from a DoctypeToken into a name, +// public identifier, and system identifier. It returns a Node whose Type +// is DoctypeNode, whose Data is the name, and which has attributes +// named "system" and "public" for the two identifiers if they were present. +// quirks is whether the document should be parsed in "quirks mode". +func parseDoctype(s string) (n *Node, quirks bool) { + n = &Node{Type: DoctypeNode} + + // Find the name. + space := strings.IndexAny(s, whitespace) + if space == -1 { + space = len(s) + } + n.Data = s[:space] + // The comparison to "html" is case-sensitive. + if n.Data != "html" { + quirks = true + } + n.Data = strings.ToLower(n.Data) + s = strings.TrimLeft(s[space:], whitespace) + + if len(s) < 6 { + // It can't start with "PUBLIC" or "SYSTEM". + // Ignore the rest of the string. + return n, quirks || s != "" + } + + key := strings.ToLower(s[:6]) + s = s[6:] + for key == "public" || key == "system" { + s = strings.TrimLeft(s, whitespace) + if s == "" { + break + } + quote := s[0] + if quote != '"' && quote != '\'' { + break + } + s = s[1:] + q := strings.IndexRune(s, rune(quote)) + var id string + if q == -1 { + id = s + s = "" + } else { + id = s[:q] + s = s[q+1:] + } + n.Attr = append(n.Attr, Attribute{Key: key, Val: id}) + if key == "public" { + key = "system" + } else { + key = "" + } + } + + if key != "" || s != "" { + quirks = true + } else if len(n.Attr) > 0 { + if n.Attr[0].Key == "public" { + public := strings.ToLower(n.Attr[0].Val) + switch public { + case "-//w3o//dtd w3 html strict 3.0//en//", "-/w3d/dtd html 4.0 transitional/en", "html": + quirks = true + default: + for _, q := range quirkyIDs { + if strings.HasPrefix(public, q) { + quirks = true + break + } + } + } + // The following two public IDs only cause quirks mode if there is no system ID. + if len(n.Attr) == 1 && (strings.HasPrefix(public, "-//w3c//dtd html 4.01 frameset//") || + strings.HasPrefix(public, "-//w3c//dtd html 4.01 transitional//")) { + quirks = true + } + } + if lastAttr := n.Attr[len(n.Attr)-1]; lastAttr.Key == "system" && + strings.ToLower(lastAttr.Val) == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd" { + quirks = true + } + } + + return n, quirks +} + +// quirkyIDs is a list of public doctype identifiers that cause a document +// to be interpreted in quirks mode. The identifiers should be in lower case. +var quirkyIDs = []string{ + "+//silmaril//dtd html pro v0r11 19970101//", + "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", + "-//as//dtd html 3.0 aswedit + extensions//", + "-//ietf//dtd html 2.0 level 1//", + "-//ietf//dtd html 2.0 level 2//", + "-//ietf//dtd html 2.0 strict level 1//", + "-//ietf//dtd html 2.0 strict level 2//", + "-//ietf//dtd html 2.0 strict//", + "-//ietf//dtd html 2.0//", + "-//ietf//dtd html 2.1e//", + "-//ietf//dtd html 3.0//", + "-//ietf//dtd html 3.2 final//", + "-//ietf//dtd html 3.2//", + "-//ietf//dtd html 3//", + "-//ietf//dtd html level 0//", + "-//ietf//dtd html level 1//", + "-//ietf//dtd html level 2//", + "-//ietf//dtd html level 3//", + "-//ietf//dtd html strict level 0//", + "-//ietf//dtd html strict level 1//", + "-//ietf//dtd html strict level 2//", + "-//ietf//dtd html strict level 3//", + "-//ietf//dtd html strict//", + "-//ietf//dtd html//", + "-//metrius//dtd metrius presentational//", + "-//microsoft//dtd internet explorer 2.0 html strict//", + "-//microsoft//dtd internet explorer 2.0 html//", + "-//microsoft//dtd internet explorer 2.0 tables//", + "-//microsoft//dtd internet explorer 3.0 html strict//", + "-//microsoft//dtd internet explorer 3.0 html//", + "-//microsoft//dtd internet explorer 3.0 tables//", + "-//netscape comm. corp.//dtd html//", + "-//netscape comm. corp.//dtd strict html//", + "-//o'reilly and associates//dtd html 2.0//", + "-//o'reilly and associates//dtd html extended 1.0//", + "-//o'reilly and associates//dtd html extended relaxed 1.0//", + "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//", + "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//", + "-//spyglass//dtd html 2.0 extended//", + "-//sq//dtd html 2.0 hotmetal + extensions//", + "-//sun microsystems corp.//dtd hotjava html//", + "-//sun microsystems corp.//dtd hotjava strict html//", + "-//w3c//dtd html 3 1995-03-24//", + "-//w3c//dtd html 3.2 draft//", + "-//w3c//dtd html 3.2 final//", + "-//w3c//dtd html 3.2//", + "-//w3c//dtd html 3.2s draft//", + "-//w3c//dtd html 4.0 frameset//", + "-//w3c//dtd html 4.0 transitional//", + "-//w3c//dtd html experimental 19960712//", + "-//w3c//dtd html experimental 970421//", + "-//w3c//dtd w3 html//", + "-//w3o//dtd w3 html 3.0//", + "-//webtechs//dtd mozilla html 2.0//", + "-//webtechs//dtd mozilla html//", +} diff --git a/vendor/golang.org/x/net/html/entity.go b/vendor/golang.org/x/net/html/entity.go new file mode 100644 index 00000000000..b628880a014 --- /dev/null +++ b/vendor/golang.org/x/net/html/entity.go @@ -0,0 +1,2253 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +// All entities that do not end with ';' are 6 or fewer bytes long. +const longestEntityWithoutSemicolon = 6 + +// entity is a map from HTML entity names to their values. The semicolon matters: +// https://html.spec.whatwg.org/multipage/syntax.html#named-character-references +// lists both "amp" and "amp;" as two separate entries. +// +// Note that the HTML5 list is larger than the HTML4 list at +// http://www.w3.org/TR/html4/sgml/entities.html +var entity = map[string]rune{ + "AElig;": '\U000000C6', + "AMP;": '\U00000026', + "Aacute;": '\U000000C1', + "Abreve;": '\U00000102', + "Acirc;": '\U000000C2', + "Acy;": '\U00000410', + "Afr;": '\U0001D504', + "Agrave;": '\U000000C0', + "Alpha;": '\U00000391', + "Amacr;": '\U00000100', + "And;": '\U00002A53', + "Aogon;": '\U00000104', + "Aopf;": '\U0001D538', + "ApplyFunction;": '\U00002061', + "Aring;": '\U000000C5', + "Ascr;": '\U0001D49C', + "Assign;": '\U00002254', + "Atilde;": '\U000000C3', + "Auml;": '\U000000C4', + "Backslash;": '\U00002216', + "Barv;": '\U00002AE7', + "Barwed;": '\U00002306', + "Bcy;": '\U00000411', + "Because;": '\U00002235', + "Bernoullis;": '\U0000212C', + "Beta;": '\U00000392', + "Bfr;": '\U0001D505', + "Bopf;": '\U0001D539', + "Breve;": '\U000002D8', + "Bscr;": '\U0000212C', + "Bumpeq;": '\U0000224E', + "CHcy;": '\U00000427', + "COPY;": '\U000000A9', + "Cacute;": '\U00000106', + "Cap;": '\U000022D2', + "CapitalDifferentialD;": '\U00002145', + "Cayleys;": '\U0000212D', + "Ccaron;": '\U0000010C', + "Ccedil;": '\U000000C7', + "Ccirc;": '\U00000108', + "Cconint;": '\U00002230', + "Cdot;": '\U0000010A', + "Cedilla;": '\U000000B8', + "CenterDot;": '\U000000B7', + "Cfr;": '\U0000212D', + "Chi;": '\U000003A7', + "CircleDot;": '\U00002299', + "CircleMinus;": '\U00002296', + "CirclePlus;": '\U00002295', + "CircleTimes;": '\U00002297', + "ClockwiseContourIntegral;": '\U00002232', + "CloseCurlyDoubleQuote;": '\U0000201D', + "CloseCurlyQuote;": '\U00002019', + "Colon;": '\U00002237', + "Colone;": '\U00002A74', + "Congruent;": '\U00002261', + "Conint;": '\U0000222F', + "ContourIntegral;": '\U0000222E', + "Copf;": '\U00002102', + "Coproduct;": '\U00002210', + "CounterClockwiseContourIntegral;": '\U00002233', + "Cross;": '\U00002A2F', + "Cscr;": '\U0001D49E', + "Cup;": '\U000022D3', + "CupCap;": '\U0000224D', + "DD;": '\U00002145', + "DDotrahd;": '\U00002911', + "DJcy;": '\U00000402', + "DScy;": '\U00000405', + "DZcy;": '\U0000040F', + "Dagger;": '\U00002021', + "Darr;": '\U000021A1', + "Dashv;": '\U00002AE4', + "Dcaron;": '\U0000010E', + "Dcy;": '\U00000414', + "Del;": '\U00002207', + "Delta;": '\U00000394', + "Dfr;": '\U0001D507', + "DiacriticalAcute;": '\U000000B4', + "DiacriticalDot;": '\U000002D9', + "DiacriticalDoubleAcute;": '\U000002DD', + "DiacriticalGrave;": '\U00000060', + "DiacriticalTilde;": '\U000002DC', + "Diamond;": '\U000022C4', + "DifferentialD;": '\U00002146', + "Dopf;": '\U0001D53B', + "Dot;": '\U000000A8', + "DotDot;": '\U000020DC', + "DotEqual;": '\U00002250', + "DoubleContourIntegral;": '\U0000222F', + "DoubleDot;": '\U000000A8', + "DoubleDownArrow;": '\U000021D3', + "DoubleLeftArrow;": '\U000021D0', + "DoubleLeftRightArrow;": '\U000021D4', + "DoubleLeftTee;": '\U00002AE4', + "DoubleLongLeftArrow;": '\U000027F8', + "DoubleLongLeftRightArrow;": '\U000027FA', + "DoubleLongRightArrow;": '\U000027F9', + "DoubleRightArrow;": '\U000021D2', + "DoubleRightTee;": '\U000022A8', + "DoubleUpArrow;": '\U000021D1', + "DoubleUpDownArrow;": '\U000021D5', + "DoubleVerticalBar;": '\U00002225', + "DownArrow;": '\U00002193', + "DownArrowBar;": '\U00002913', + "DownArrowUpArrow;": '\U000021F5', + "DownBreve;": '\U00000311', + "DownLeftRightVector;": '\U00002950', + "DownLeftTeeVector;": '\U0000295E', + "DownLeftVector;": '\U000021BD', + "DownLeftVectorBar;": '\U00002956', + "DownRightTeeVector;": '\U0000295F', + "DownRightVector;": '\U000021C1', + "DownRightVectorBar;": '\U00002957', + "DownTee;": '\U000022A4', + "DownTeeArrow;": '\U000021A7', + "Downarrow;": '\U000021D3', + "Dscr;": '\U0001D49F', + "Dstrok;": '\U00000110', + "ENG;": '\U0000014A', + "ETH;": '\U000000D0', + "Eacute;": '\U000000C9', + "Ecaron;": '\U0000011A', + "Ecirc;": '\U000000CA', + "Ecy;": '\U0000042D', + "Edot;": '\U00000116', + "Efr;": '\U0001D508', + "Egrave;": '\U000000C8', + "Element;": '\U00002208', + "Emacr;": '\U00000112', + "EmptySmallSquare;": '\U000025FB', + "EmptyVerySmallSquare;": '\U000025AB', + "Eogon;": '\U00000118', + "Eopf;": '\U0001D53C', + "Epsilon;": '\U00000395', + "Equal;": '\U00002A75', + "EqualTilde;": '\U00002242', + "Equilibrium;": '\U000021CC', + "Escr;": '\U00002130', + "Esim;": '\U00002A73', + "Eta;": '\U00000397', + "Euml;": '\U000000CB', + "Exists;": '\U00002203', + "ExponentialE;": '\U00002147', + "Fcy;": '\U00000424', + "Ffr;": '\U0001D509', + "FilledSmallSquare;": '\U000025FC', + "FilledVerySmallSquare;": '\U000025AA', + "Fopf;": '\U0001D53D', + "ForAll;": '\U00002200', + "Fouriertrf;": '\U00002131', + "Fscr;": '\U00002131', + "GJcy;": '\U00000403', + "GT;": '\U0000003E', + "Gamma;": '\U00000393', + "Gammad;": '\U000003DC', + "Gbreve;": '\U0000011E', + "Gcedil;": '\U00000122', + "Gcirc;": '\U0000011C', + "Gcy;": '\U00000413', + "Gdot;": '\U00000120', + "Gfr;": '\U0001D50A', + "Gg;": '\U000022D9', + "Gopf;": '\U0001D53E', + "GreaterEqual;": '\U00002265', + "GreaterEqualLess;": '\U000022DB', + "GreaterFullEqual;": '\U00002267', + "GreaterGreater;": '\U00002AA2', + "GreaterLess;": '\U00002277', + "GreaterSlantEqual;": '\U00002A7E', + "GreaterTilde;": '\U00002273', + "Gscr;": '\U0001D4A2', + "Gt;": '\U0000226B', + "HARDcy;": '\U0000042A', + "Hacek;": '\U000002C7', + "Hat;": '\U0000005E', + "Hcirc;": '\U00000124', + "Hfr;": '\U0000210C', + "HilbertSpace;": '\U0000210B', + "Hopf;": '\U0000210D', + "HorizontalLine;": '\U00002500', + "Hscr;": '\U0000210B', + "Hstrok;": '\U00000126', + "HumpDownHump;": '\U0000224E', + "HumpEqual;": '\U0000224F', + "IEcy;": '\U00000415', + "IJlig;": '\U00000132', + "IOcy;": '\U00000401', + "Iacute;": '\U000000CD', + "Icirc;": '\U000000CE', + "Icy;": '\U00000418', + "Idot;": '\U00000130', + "Ifr;": '\U00002111', + "Igrave;": '\U000000CC', + "Im;": '\U00002111', + "Imacr;": '\U0000012A', + "ImaginaryI;": '\U00002148', + "Implies;": '\U000021D2', + "Int;": '\U0000222C', + "Integral;": '\U0000222B', + "Intersection;": '\U000022C2', + "InvisibleComma;": '\U00002063', + "InvisibleTimes;": '\U00002062', + "Iogon;": '\U0000012E', + "Iopf;": '\U0001D540', + "Iota;": '\U00000399', + "Iscr;": '\U00002110', + "Itilde;": '\U00000128', + "Iukcy;": '\U00000406', + "Iuml;": '\U000000CF', + "Jcirc;": '\U00000134', + "Jcy;": '\U00000419', + "Jfr;": '\U0001D50D', + "Jopf;": '\U0001D541', + "Jscr;": '\U0001D4A5', + "Jsercy;": '\U00000408', + "Jukcy;": '\U00000404', + "KHcy;": '\U00000425', + "KJcy;": '\U0000040C', + "Kappa;": '\U0000039A', + "Kcedil;": '\U00000136', + "Kcy;": '\U0000041A', + "Kfr;": '\U0001D50E', + "Kopf;": '\U0001D542', + "Kscr;": '\U0001D4A6', + "LJcy;": '\U00000409', + "LT;": '\U0000003C', + "Lacute;": '\U00000139', + "Lambda;": '\U0000039B', + "Lang;": '\U000027EA', + "Laplacetrf;": '\U00002112', + "Larr;": '\U0000219E', + "Lcaron;": '\U0000013D', + "Lcedil;": '\U0000013B', + "Lcy;": '\U0000041B', + "LeftAngleBracket;": '\U000027E8', + "LeftArrow;": '\U00002190', + "LeftArrowBar;": '\U000021E4', + "LeftArrowRightArrow;": '\U000021C6', + "LeftCeiling;": '\U00002308', + "LeftDoubleBracket;": '\U000027E6', + "LeftDownTeeVector;": '\U00002961', + "LeftDownVector;": '\U000021C3', + "LeftDownVectorBar;": '\U00002959', + "LeftFloor;": '\U0000230A', + "LeftRightArrow;": '\U00002194', + "LeftRightVector;": '\U0000294E', + "LeftTee;": '\U000022A3', + "LeftTeeArrow;": '\U000021A4', + "LeftTeeVector;": '\U0000295A', + "LeftTriangle;": '\U000022B2', + "LeftTriangleBar;": '\U000029CF', + "LeftTriangleEqual;": '\U000022B4', + "LeftUpDownVector;": '\U00002951', + "LeftUpTeeVector;": '\U00002960', + "LeftUpVector;": '\U000021BF', + "LeftUpVectorBar;": '\U00002958', + "LeftVector;": '\U000021BC', + "LeftVectorBar;": '\U00002952', + "Leftarrow;": '\U000021D0', + "Leftrightarrow;": '\U000021D4', + "LessEqualGreater;": '\U000022DA', + "LessFullEqual;": '\U00002266', + "LessGreater;": '\U00002276', + "LessLess;": '\U00002AA1', + "LessSlantEqual;": '\U00002A7D', + "LessTilde;": '\U00002272', + "Lfr;": '\U0001D50F', + "Ll;": '\U000022D8', + "Lleftarrow;": '\U000021DA', + "Lmidot;": '\U0000013F', + "LongLeftArrow;": '\U000027F5', + "LongLeftRightArrow;": '\U000027F7', + "LongRightArrow;": '\U000027F6', + "Longleftarrow;": '\U000027F8', + "Longleftrightarrow;": '\U000027FA', + "Longrightarrow;": '\U000027F9', + "Lopf;": '\U0001D543', + "LowerLeftArrow;": '\U00002199', + "LowerRightArrow;": '\U00002198', + "Lscr;": '\U00002112', + "Lsh;": '\U000021B0', + "Lstrok;": '\U00000141', + "Lt;": '\U0000226A', + "Map;": '\U00002905', + "Mcy;": '\U0000041C', + "MediumSpace;": '\U0000205F', + "Mellintrf;": '\U00002133', + "Mfr;": '\U0001D510', + "MinusPlus;": '\U00002213', + "Mopf;": '\U0001D544', + "Mscr;": '\U00002133', + "Mu;": '\U0000039C', + "NJcy;": '\U0000040A', + "Nacute;": '\U00000143', + "Ncaron;": '\U00000147', + "Ncedil;": '\U00000145', + "Ncy;": '\U0000041D', + "NegativeMediumSpace;": '\U0000200B', + "NegativeThickSpace;": '\U0000200B', + "NegativeThinSpace;": '\U0000200B', + "NegativeVeryThinSpace;": '\U0000200B', + "NestedGreaterGreater;": '\U0000226B', + "NestedLessLess;": '\U0000226A', + "NewLine;": '\U0000000A', + "Nfr;": '\U0001D511', + "NoBreak;": '\U00002060', + "NonBreakingSpace;": '\U000000A0', + "Nopf;": '\U00002115', + "Not;": '\U00002AEC', + "NotCongruent;": '\U00002262', + "NotCupCap;": '\U0000226D', + "NotDoubleVerticalBar;": '\U00002226', + "NotElement;": '\U00002209', + "NotEqual;": '\U00002260', + "NotExists;": '\U00002204', + "NotGreater;": '\U0000226F', + "NotGreaterEqual;": '\U00002271', + "NotGreaterLess;": '\U00002279', + "NotGreaterTilde;": '\U00002275', + "NotLeftTriangle;": '\U000022EA', + "NotLeftTriangleEqual;": '\U000022EC', + "NotLess;": '\U0000226E', + "NotLessEqual;": '\U00002270', + "NotLessGreater;": '\U00002278', + "NotLessTilde;": '\U00002274', + "NotPrecedes;": '\U00002280', + "NotPrecedesSlantEqual;": '\U000022E0', + "NotReverseElement;": '\U0000220C', + "NotRightTriangle;": '\U000022EB', + "NotRightTriangleEqual;": '\U000022ED', + "NotSquareSubsetEqual;": '\U000022E2', + "NotSquareSupersetEqual;": '\U000022E3', + "NotSubsetEqual;": '\U00002288', + "NotSucceeds;": '\U00002281', + "NotSucceedsSlantEqual;": '\U000022E1', + "NotSupersetEqual;": '\U00002289', + "NotTilde;": '\U00002241', + "NotTildeEqual;": '\U00002244', + "NotTildeFullEqual;": '\U00002247', + "NotTildeTilde;": '\U00002249', + "NotVerticalBar;": '\U00002224', + "Nscr;": '\U0001D4A9', + "Ntilde;": '\U000000D1', + "Nu;": '\U0000039D', + "OElig;": '\U00000152', + "Oacute;": '\U000000D3', + "Ocirc;": '\U000000D4', + "Ocy;": '\U0000041E', + "Odblac;": '\U00000150', + "Ofr;": '\U0001D512', + "Ograve;": '\U000000D2', + "Omacr;": '\U0000014C', + "Omega;": '\U000003A9', + "Omicron;": '\U0000039F', + "Oopf;": '\U0001D546', + "OpenCurlyDoubleQuote;": '\U0000201C', + "OpenCurlyQuote;": '\U00002018', + "Or;": '\U00002A54', + "Oscr;": '\U0001D4AA', + "Oslash;": '\U000000D8', + "Otilde;": '\U000000D5', + "Otimes;": '\U00002A37', + "Ouml;": '\U000000D6', + "OverBar;": '\U0000203E', + "OverBrace;": '\U000023DE', + "OverBracket;": '\U000023B4', + "OverParenthesis;": '\U000023DC', + "PartialD;": '\U00002202', + "Pcy;": '\U0000041F', + "Pfr;": '\U0001D513', + "Phi;": '\U000003A6', + "Pi;": '\U000003A0', + "PlusMinus;": '\U000000B1', + "Poincareplane;": '\U0000210C', + "Popf;": '\U00002119', + "Pr;": '\U00002ABB', + "Precedes;": '\U0000227A', + "PrecedesEqual;": '\U00002AAF', + "PrecedesSlantEqual;": '\U0000227C', + "PrecedesTilde;": '\U0000227E', + "Prime;": '\U00002033', + "Product;": '\U0000220F', + "Proportion;": '\U00002237', + "Proportional;": '\U0000221D', + "Pscr;": '\U0001D4AB', + "Psi;": '\U000003A8', + "QUOT;": '\U00000022', + "Qfr;": '\U0001D514', + "Qopf;": '\U0000211A', + "Qscr;": '\U0001D4AC', + "RBarr;": '\U00002910', + "REG;": '\U000000AE', + "Racute;": '\U00000154', + "Rang;": '\U000027EB', + "Rarr;": '\U000021A0', + "Rarrtl;": '\U00002916', + "Rcaron;": '\U00000158', + "Rcedil;": '\U00000156', + "Rcy;": '\U00000420', + "Re;": '\U0000211C', + "ReverseElement;": '\U0000220B', + "ReverseEquilibrium;": '\U000021CB', + "ReverseUpEquilibrium;": '\U0000296F', + "Rfr;": '\U0000211C', + "Rho;": '\U000003A1', + "RightAngleBracket;": '\U000027E9', + "RightArrow;": '\U00002192', + "RightArrowBar;": '\U000021E5', + "RightArrowLeftArrow;": '\U000021C4', + "RightCeiling;": '\U00002309', + "RightDoubleBracket;": '\U000027E7', + "RightDownTeeVector;": '\U0000295D', + "RightDownVector;": '\U000021C2', + "RightDownVectorBar;": '\U00002955', + "RightFloor;": '\U0000230B', + "RightTee;": '\U000022A2', + "RightTeeArrow;": '\U000021A6', + "RightTeeVector;": '\U0000295B', + "RightTriangle;": '\U000022B3', + "RightTriangleBar;": '\U000029D0', + "RightTriangleEqual;": '\U000022B5', + "RightUpDownVector;": '\U0000294F', + "RightUpTeeVector;": '\U0000295C', + "RightUpVector;": '\U000021BE', + "RightUpVectorBar;": '\U00002954', + "RightVector;": '\U000021C0', + "RightVectorBar;": '\U00002953', + "Rightarrow;": '\U000021D2', + "Ropf;": '\U0000211D', + "RoundImplies;": '\U00002970', + "Rrightarrow;": '\U000021DB', + "Rscr;": '\U0000211B', + "Rsh;": '\U000021B1', + "RuleDelayed;": '\U000029F4', + "SHCHcy;": '\U00000429', + "SHcy;": '\U00000428', + "SOFTcy;": '\U0000042C', + "Sacute;": '\U0000015A', + "Sc;": '\U00002ABC', + "Scaron;": '\U00000160', + "Scedil;": '\U0000015E', + "Scirc;": '\U0000015C', + "Scy;": '\U00000421', + "Sfr;": '\U0001D516', + "ShortDownArrow;": '\U00002193', + "ShortLeftArrow;": '\U00002190', + "ShortRightArrow;": '\U00002192', + "ShortUpArrow;": '\U00002191', + "Sigma;": '\U000003A3', + "SmallCircle;": '\U00002218', + "Sopf;": '\U0001D54A', + "Sqrt;": '\U0000221A', + "Square;": '\U000025A1', + "SquareIntersection;": '\U00002293', + "SquareSubset;": '\U0000228F', + "SquareSubsetEqual;": '\U00002291', + "SquareSuperset;": '\U00002290', + "SquareSupersetEqual;": '\U00002292', + "SquareUnion;": '\U00002294', + "Sscr;": '\U0001D4AE', + "Star;": '\U000022C6', + "Sub;": '\U000022D0', + "Subset;": '\U000022D0', + "SubsetEqual;": '\U00002286', + "Succeeds;": '\U0000227B', + "SucceedsEqual;": '\U00002AB0', + "SucceedsSlantEqual;": '\U0000227D', + "SucceedsTilde;": '\U0000227F', + "SuchThat;": '\U0000220B', + "Sum;": '\U00002211', + "Sup;": '\U000022D1', + "Superset;": '\U00002283', + "SupersetEqual;": '\U00002287', + "Supset;": '\U000022D1', + "THORN;": '\U000000DE', + "TRADE;": '\U00002122', + "TSHcy;": '\U0000040B', + "TScy;": '\U00000426', + "Tab;": '\U00000009', + "Tau;": '\U000003A4', + "Tcaron;": '\U00000164', + "Tcedil;": '\U00000162', + "Tcy;": '\U00000422', + "Tfr;": '\U0001D517', + "Therefore;": '\U00002234', + "Theta;": '\U00000398', + "ThinSpace;": '\U00002009', + "Tilde;": '\U0000223C', + "TildeEqual;": '\U00002243', + "TildeFullEqual;": '\U00002245', + "TildeTilde;": '\U00002248', + "Topf;": '\U0001D54B', + "TripleDot;": '\U000020DB', + "Tscr;": '\U0001D4AF', + "Tstrok;": '\U00000166', + "Uacute;": '\U000000DA', + "Uarr;": '\U0000219F', + "Uarrocir;": '\U00002949', + "Ubrcy;": '\U0000040E', + "Ubreve;": '\U0000016C', + "Ucirc;": '\U000000DB', + "Ucy;": '\U00000423', + "Udblac;": '\U00000170', + "Ufr;": '\U0001D518', + "Ugrave;": '\U000000D9', + "Umacr;": '\U0000016A', + "UnderBar;": '\U0000005F', + "UnderBrace;": '\U000023DF', + "UnderBracket;": '\U000023B5', + "UnderParenthesis;": '\U000023DD', + "Union;": '\U000022C3', + "UnionPlus;": '\U0000228E', + "Uogon;": '\U00000172', + "Uopf;": '\U0001D54C', + "UpArrow;": '\U00002191', + "UpArrowBar;": '\U00002912', + "UpArrowDownArrow;": '\U000021C5', + "UpDownArrow;": '\U00002195', + "UpEquilibrium;": '\U0000296E', + "UpTee;": '\U000022A5', + "UpTeeArrow;": '\U000021A5', + "Uparrow;": '\U000021D1', + "Updownarrow;": '\U000021D5', + "UpperLeftArrow;": '\U00002196', + "UpperRightArrow;": '\U00002197', + "Upsi;": '\U000003D2', + "Upsilon;": '\U000003A5', + "Uring;": '\U0000016E', + "Uscr;": '\U0001D4B0', + "Utilde;": '\U00000168', + "Uuml;": '\U000000DC', + "VDash;": '\U000022AB', + "Vbar;": '\U00002AEB', + "Vcy;": '\U00000412', + "Vdash;": '\U000022A9', + "Vdashl;": '\U00002AE6', + "Vee;": '\U000022C1', + "Verbar;": '\U00002016', + "Vert;": '\U00002016', + "VerticalBar;": '\U00002223', + "VerticalLine;": '\U0000007C', + "VerticalSeparator;": '\U00002758', + "VerticalTilde;": '\U00002240', + "VeryThinSpace;": '\U0000200A', + "Vfr;": '\U0001D519', + "Vopf;": '\U0001D54D', + "Vscr;": '\U0001D4B1', + "Vvdash;": '\U000022AA', + "Wcirc;": '\U00000174', + "Wedge;": '\U000022C0', + "Wfr;": '\U0001D51A', + "Wopf;": '\U0001D54E', + "Wscr;": '\U0001D4B2', + "Xfr;": '\U0001D51B', + "Xi;": '\U0000039E', + "Xopf;": '\U0001D54F', + "Xscr;": '\U0001D4B3', + "YAcy;": '\U0000042F', + "YIcy;": '\U00000407', + "YUcy;": '\U0000042E', + "Yacute;": '\U000000DD', + "Ycirc;": '\U00000176', + "Ycy;": '\U0000042B', + "Yfr;": '\U0001D51C', + "Yopf;": '\U0001D550', + "Yscr;": '\U0001D4B4', + "Yuml;": '\U00000178', + "ZHcy;": '\U00000416', + "Zacute;": '\U00000179', + "Zcaron;": '\U0000017D', + "Zcy;": '\U00000417', + "Zdot;": '\U0000017B', + "ZeroWidthSpace;": '\U0000200B', + "Zeta;": '\U00000396', + "Zfr;": '\U00002128', + "Zopf;": '\U00002124', + "Zscr;": '\U0001D4B5', + "aacute;": '\U000000E1', + "abreve;": '\U00000103', + "ac;": '\U0000223E', + "acd;": '\U0000223F', + "acirc;": '\U000000E2', + "acute;": '\U000000B4', + "acy;": '\U00000430', + "aelig;": '\U000000E6', + "af;": '\U00002061', + "afr;": '\U0001D51E', + "agrave;": '\U000000E0', + "alefsym;": '\U00002135', + "aleph;": '\U00002135', + "alpha;": '\U000003B1', + "amacr;": '\U00000101', + "amalg;": '\U00002A3F', + "amp;": '\U00000026', + "and;": '\U00002227', + "andand;": '\U00002A55', + "andd;": '\U00002A5C', + "andslope;": '\U00002A58', + "andv;": '\U00002A5A', + "ang;": '\U00002220', + "ange;": '\U000029A4', + "angle;": '\U00002220', + "angmsd;": '\U00002221', + "angmsdaa;": '\U000029A8', + "angmsdab;": '\U000029A9', + "angmsdac;": '\U000029AA', + "angmsdad;": '\U000029AB', + "angmsdae;": '\U000029AC', + "angmsdaf;": '\U000029AD', + "angmsdag;": '\U000029AE', + "angmsdah;": '\U000029AF', + "angrt;": '\U0000221F', + "angrtvb;": '\U000022BE', + "angrtvbd;": '\U0000299D', + "angsph;": '\U00002222', + "angst;": '\U000000C5', + "angzarr;": '\U0000237C', + "aogon;": '\U00000105', + "aopf;": '\U0001D552', + "ap;": '\U00002248', + "apE;": '\U00002A70', + "apacir;": '\U00002A6F', + "ape;": '\U0000224A', + "apid;": '\U0000224B', + "apos;": '\U00000027', + "approx;": '\U00002248', + "approxeq;": '\U0000224A', + "aring;": '\U000000E5', + "ascr;": '\U0001D4B6', + "ast;": '\U0000002A', + "asymp;": '\U00002248', + "asympeq;": '\U0000224D', + "atilde;": '\U000000E3', + "auml;": '\U000000E4', + "awconint;": '\U00002233', + "awint;": '\U00002A11', + "bNot;": '\U00002AED', + "backcong;": '\U0000224C', + "backepsilon;": '\U000003F6', + "backprime;": '\U00002035', + "backsim;": '\U0000223D', + "backsimeq;": '\U000022CD', + "barvee;": '\U000022BD', + "barwed;": '\U00002305', + "barwedge;": '\U00002305', + "bbrk;": '\U000023B5', + "bbrktbrk;": '\U000023B6', + "bcong;": '\U0000224C', + "bcy;": '\U00000431', + "bdquo;": '\U0000201E', + "becaus;": '\U00002235', + "because;": '\U00002235', + "bemptyv;": '\U000029B0', + "bepsi;": '\U000003F6', + "bernou;": '\U0000212C', + "beta;": '\U000003B2', + "beth;": '\U00002136', + "between;": '\U0000226C', + "bfr;": '\U0001D51F', + "bigcap;": '\U000022C2', + "bigcirc;": '\U000025EF', + "bigcup;": '\U000022C3', + "bigodot;": '\U00002A00', + "bigoplus;": '\U00002A01', + "bigotimes;": '\U00002A02', + "bigsqcup;": '\U00002A06', + "bigstar;": '\U00002605', + "bigtriangledown;": '\U000025BD', + "bigtriangleup;": '\U000025B3', + "biguplus;": '\U00002A04', + "bigvee;": '\U000022C1', + "bigwedge;": '\U000022C0', + "bkarow;": '\U0000290D', + "blacklozenge;": '\U000029EB', + "blacksquare;": '\U000025AA', + "blacktriangle;": '\U000025B4', + "blacktriangledown;": '\U000025BE', + "blacktriangleleft;": '\U000025C2', + "blacktriangleright;": '\U000025B8', + "blank;": '\U00002423', + "blk12;": '\U00002592', + "blk14;": '\U00002591', + "blk34;": '\U00002593', + "block;": '\U00002588', + "bnot;": '\U00002310', + "bopf;": '\U0001D553', + "bot;": '\U000022A5', + "bottom;": '\U000022A5', + "bowtie;": '\U000022C8', + "boxDL;": '\U00002557', + "boxDR;": '\U00002554', + "boxDl;": '\U00002556', + "boxDr;": '\U00002553', + "boxH;": '\U00002550', + "boxHD;": '\U00002566', + "boxHU;": '\U00002569', + "boxHd;": '\U00002564', + "boxHu;": '\U00002567', + "boxUL;": '\U0000255D', + "boxUR;": '\U0000255A', + "boxUl;": '\U0000255C', + "boxUr;": '\U00002559', + "boxV;": '\U00002551', + "boxVH;": '\U0000256C', + "boxVL;": '\U00002563', + "boxVR;": '\U00002560', + "boxVh;": '\U0000256B', + "boxVl;": '\U00002562', + "boxVr;": '\U0000255F', + "boxbox;": '\U000029C9', + "boxdL;": '\U00002555', + "boxdR;": '\U00002552', + "boxdl;": '\U00002510', + "boxdr;": '\U0000250C', + "boxh;": '\U00002500', + "boxhD;": '\U00002565', + "boxhU;": '\U00002568', + "boxhd;": '\U0000252C', + "boxhu;": '\U00002534', + "boxminus;": '\U0000229F', + "boxplus;": '\U0000229E', + "boxtimes;": '\U000022A0', + "boxuL;": '\U0000255B', + "boxuR;": '\U00002558', + "boxul;": '\U00002518', + "boxur;": '\U00002514', + "boxv;": '\U00002502', + "boxvH;": '\U0000256A', + "boxvL;": '\U00002561', + "boxvR;": '\U0000255E', + "boxvh;": '\U0000253C', + "boxvl;": '\U00002524', + "boxvr;": '\U0000251C', + "bprime;": '\U00002035', + "breve;": '\U000002D8', + "brvbar;": '\U000000A6', + "bscr;": '\U0001D4B7', + "bsemi;": '\U0000204F', + "bsim;": '\U0000223D', + "bsime;": '\U000022CD', + "bsol;": '\U0000005C', + "bsolb;": '\U000029C5', + "bsolhsub;": '\U000027C8', + "bull;": '\U00002022', + "bullet;": '\U00002022', + "bump;": '\U0000224E', + "bumpE;": '\U00002AAE', + "bumpe;": '\U0000224F', + "bumpeq;": '\U0000224F', + "cacute;": '\U00000107', + "cap;": '\U00002229', + "capand;": '\U00002A44', + "capbrcup;": '\U00002A49', + "capcap;": '\U00002A4B', + "capcup;": '\U00002A47', + "capdot;": '\U00002A40', + "caret;": '\U00002041', + "caron;": '\U000002C7', + "ccaps;": '\U00002A4D', + "ccaron;": '\U0000010D', + "ccedil;": '\U000000E7', + "ccirc;": '\U00000109', + "ccups;": '\U00002A4C', + "ccupssm;": '\U00002A50', + "cdot;": '\U0000010B', + "cedil;": '\U000000B8', + "cemptyv;": '\U000029B2', + "cent;": '\U000000A2', + "centerdot;": '\U000000B7', + "cfr;": '\U0001D520', + "chcy;": '\U00000447', + "check;": '\U00002713', + "checkmark;": '\U00002713', + "chi;": '\U000003C7', + "cir;": '\U000025CB', + "cirE;": '\U000029C3', + "circ;": '\U000002C6', + "circeq;": '\U00002257', + "circlearrowleft;": '\U000021BA', + "circlearrowright;": '\U000021BB', + "circledR;": '\U000000AE', + "circledS;": '\U000024C8', + "circledast;": '\U0000229B', + "circledcirc;": '\U0000229A', + "circleddash;": '\U0000229D', + "cire;": '\U00002257', + "cirfnint;": '\U00002A10', + "cirmid;": '\U00002AEF', + "cirscir;": '\U000029C2', + "clubs;": '\U00002663', + "clubsuit;": '\U00002663', + "colon;": '\U0000003A', + "colone;": '\U00002254', + "coloneq;": '\U00002254', + "comma;": '\U0000002C', + "commat;": '\U00000040', + "comp;": '\U00002201', + "compfn;": '\U00002218', + "complement;": '\U00002201', + "complexes;": '\U00002102', + "cong;": '\U00002245', + "congdot;": '\U00002A6D', + "conint;": '\U0000222E', + "copf;": '\U0001D554', + "coprod;": '\U00002210', + "copy;": '\U000000A9', + "copysr;": '\U00002117', + "crarr;": '\U000021B5', + "cross;": '\U00002717', + "cscr;": '\U0001D4B8', + "csub;": '\U00002ACF', + "csube;": '\U00002AD1', + "csup;": '\U00002AD0', + "csupe;": '\U00002AD2', + "ctdot;": '\U000022EF', + "cudarrl;": '\U00002938', + "cudarrr;": '\U00002935', + "cuepr;": '\U000022DE', + "cuesc;": '\U000022DF', + "cularr;": '\U000021B6', + "cularrp;": '\U0000293D', + "cup;": '\U0000222A', + "cupbrcap;": '\U00002A48', + "cupcap;": '\U00002A46', + "cupcup;": '\U00002A4A', + "cupdot;": '\U0000228D', + "cupor;": '\U00002A45', + "curarr;": '\U000021B7', + "curarrm;": '\U0000293C', + "curlyeqprec;": '\U000022DE', + "curlyeqsucc;": '\U000022DF', + "curlyvee;": '\U000022CE', + "curlywedge;": '\U000022CF', + "curren;": '\U000000A4', + "curvearrowleft;": '\U000021B6', + "curvearrowright;": '\U000021B7', + "cuvee;": '\U000022CE', + "cuwed;": '\U000022CF', + "cwconint;": '\U00002232', + "cwint;": '\U00002231', + "cylcty;": '\U0000232D', + "dArr;": '\U000021D3', + "dHar;": '\U00002965', + "dagger;": '\U00002020', + "daleth;": '\U00002138', + "darr;": '\U00002193', + "dash;": '\U00002010', + "dashv;": '\U000022A3', + "dbkarow;": '\U0000290F', + "dblac;": '\U000002DD', + "dcaron;": '\U0000010F', + "dcy;": '\U00000434', + "dd;": '\U00002146', + "ddagger;": '\U00002021', + "ddarr;": '\U000021CA', + "ddotseq;": '\U00002A77', + "deg;": '\U000000B0', + "delta;": '\U000003B4', + "demptyv;": '\U000029B1', + "dfisht;": '\U0000297F', + "dfr;": '\U0001D521', + "dharl;": '\U000021C3', + "dharr;": '\U000021C2', + "diam;": '\U000022C4', + "diamond;": '\U000022C4', + "diamondsuit;": '\U00002666', + "diams;": '\U00002666', + "die;": '\U000000A8', + "digamma;": '\U000003DD', + "disin;": '\U000022F2', + "div;": '\U000000F7', + "divide;": '\U000000F7', + "divideontimes;": '\U000022C7', + "divonx;": '\U000022C7', + "djcy;": '\U00000452', + "dlcorn;": '\U0000231E', + "dlcrop;": '\U0000230D', + "dollar;": '\U00000024', + "dopf;": '\U0001D555', + "dot;": '\U000002D9', + "doteq;": '\U00002250', + "doteqdot;": '\U00002251', + "dotminus;": '\U00002238', + "dotplus;": '\U00002214', + "dotsquare;": '\U000022A1', + "doublebarwedge;": '\U00002306', + "downarrow;": '\U00002193', + "downdownarrows;": '\U000021CA', + "downharpoonleft;": '\U000021C3', + "downharpoonright;": '\U000021C2', + "drbkarow;": '\U00002910', + "drcorn;": '\U0000231F', + "drcrop;": '\U0000230C', + "dscr;": '\U0001D4B9', + "dscy;": '\U00000455', + "dsol;": '\U000029F6', + "dstrok;": '\U00000111', + "dtdot;": '\U000022F1', + "dtri;": '\U000025BF', + "dtrif;": '\U000025BE', + "duarr;": '\U000021F5', + "duhar;": '\U0000296F', + "dwangle;": '\U000029A6', + "dzcy;": '\U0000045F', + "dzigrarr;": '\U000027FF', + "eDDot;": '\U00002A77', + "eDot;": '\U00002251', + "eacute;": '\U000000E9', + "easter;": '\U00002A6E', + "ecaron;": '\U0000011B', + "ecir;": '\U00002256', + "ecirc;": '\U000000EA', + "ecolon;": '\U00002255', + "ecy;": '\U0000044D', + "edot;": '\U00000117', + "ee;": '\U00002147', + "efDot;": '\U00002252', + "efr;": '\U0001D522', + "eg;": '\U00002A9A', + "egrave;": '\U000000E8', + "egs;": '\U00002A96', + "egsdot;": '\U00002A98', + "el;": '\U00002A99', + "elinters;": '\U000023E7', + "ell;": '\U00002113', + "els;": '\U00002A95', + "elsdot;": '\U00002A97', + "emacr;": '\U00000113', + "empty;": '\U00002205', + "emptyset;": '\U00002205', + "emptyv;": '\U00002205', + "emsp;": '\U00002003', + "emsp13;": '\U00002004', + "emsp14;": '\U00002005', + "eng;": '\U0000014B', + "ensp;": '\U00002002', + "eogon;": '\U00000119', + "eopf;": '\U0001D556', + "epar;": '\U000022D5', + "eparsl;": '\U000029E3', + "eplus;": '\U00002A71', + "epsi;": '\U000003B5', + "epsilon;": '\U000003B5', + "epsiv;": '\U000003F5', + "eqcirc;": '\U00002256', + "eqcolon;": '\U00002255', + "eqsim;": '\U00002242', + "eqslantgtr;": '\U00002A96', + "eqslantless;": '\U00002A95', + "equals;": '\U0000003D', + "equest;": '\U0000225F', + "equiv;": '\U00002261', + "equivDD;": '\U00002A78', + "eqvparsl;": '\U000029E5', + "erDot;": '\U00002253', + "erarr;": '\U00002971', + "escr;": '\U0000212F', + "esdot;": '\U00002250', + "esim;": '\U00002242', + "eta;": '\U000003B7', + "eth;": '\U000000F0', + "euml;": '\U000000EB', + "euro;": '\U000020AC', + "excl;": '\U00000021', + "exist;": '\U00002203', + "expectation;": '\U00002130', + "exponentiale;": '\U00002147', + "fallingdotseq;": '\U00002252', + "fcy;": '\U00000444', + "female;": '\U00002640', + "ffilig;": '\U0000FB03', + "fflig;": '\U0000FB00', + "ffllig;": '\U0000FB04', + "ffr;": '\U0001D523', + "filig;": '\U0000FB01', + "flat;": '\U0000266D', + "fllig;": '\U0000FB02', + "fltns;": '\U000025B1', + "fnof;": '\U00000192', + "fopf;": '\U0001D557', + "forall;": '\U00002200', + "fork;": '\U000022D4', + "forkv;": '\U00002AD9', + "fpartint;": '\U00002A0D', + "frac12;": '\U000000BD', + "frac13;": '\U00002153', + "frac14;": '\U000000BC', + "frac15;": '\U00002155', + "frac16;": '\U00002159', + "frac18;": '\U0000215B', + "frac23;": '\U00002154', + "frac25;": '\U00002156', + "frac34;": '\U000000BE', + "frac35;": '\U00002157', + "frac38;": '\U0000215C', + "frac45;": '\U00002158', + "frac56;": '\U0000215A', + "frac58;": '\U0000215D', + "frac78;": '\U0000215E', + "frasl;": '\U00002044', + "frown;": '\U00002322', + "fscr;": '\U0001D4BB', + "gE;": '\U00002267', + "gEl;": '\U00002A8C', + "gacute;": '\U000001F5', + "gamma;": '\U000003B3', + "gammad;": '\U000003DD', + "gap;": '\U00002A86', + "gbreve;": '\U0000011F', + "gcirc;": '\U0000011D', + "gcy;": '\U00000433', + "gdot;": '\U00000121', + "ge;": '\U00002265', + "gel;": '\U000022DB', + "geq;": '\U00002265', + "geqq;": '\U00002267', + "geqslant;": '\U00002A7E', + "ges;": '\U00002A7E', + "gescc;": '\U00002AA9', + "gesdot;": '\U00002A80', + "gesdoto;": '\U00002A82', + "gesdotol;": '\U00002A84', + "gesles;": '\U00002A94', + "gfr;": '\U0001D524', + "gg;": '\U0000226B', + "ggg;": '\U000022D9', + "gimel;": '\U00002137', + "gjcy;": '\U00000453', + "gl;": '\U00002277', + "glE;": '\U00002A92', + "gla;": '\U00002AA5', + "glj;": '\U00002AA4', + "gnE;": '\U00002269', + "gnap;": '\U00002A8A', + "gnapprox;": '\U00002A8A', + "gne;": '\U00002A88', + "gneq;": '\U00002A88', + "gneqq;": '\U00002269', + "gnsim;": '\U000022E7', + "gopf;": '\U0001D558', + "grave;": '\U00000060', + "gscr;": '\U0000210A', + "gsim;": '\U00002273', + "gsime;": '\U00002A8E', + "gsiml;": '\U00002A90', + "gt;": '\U0000003E', + "gtcc;": '\U00002AA7', + "gtcir;": '\U00002A7A', + "gtdot;": '\U000022D7', + "gtlPar;": '\U00002995', + "gtquest;": '\U00002A7C', + "gtrapprox;": '\U00002A86', + "gtrarr;": '\U00002978', + "gtrdot;": '\U000022D7', + "gtreqless;": '\U000022DB', + "gtreqqless;": '\U00002A8C', + "gtrless;": '\U00002277', + "gtrsim;": '\U00002273', + "hArr;": '\U000021D4', + "hairsp;": '\U0000200A', + "half;": '\U000000BD', + "hamilt;": '\U0000210B', + "hardcy;": '\U0000044A', + "harr;": '\U00002194', + "harrcir;": '\U00002948', + "harrw;": '\U000021AD', + "hbar;": '\U0000210F', + "hcirc;": '\U00000125', + "hearts;": '\U00002665', + "heartsuit;": '\U00002665', + "hellip;": '\U00002026', + "hercon;": '\U000022B9', + "hfr;": '\U0001D525', + "hksearow;": '\U00002925', + "hkswarow;": '\U00002926', + "hoarr;": '\U000021FF', + "homtht;": '\U0000223B', + "hookleftarrow;": '\U000021A9', + "hookrightarrow;": '\U000021AA', + "hopf;": '\U0001D559', + "horbar;": '\U00002015', + "hscr;": '\U0001D4BD', + "hslash;": '\U0000210F', + "hstrok;": '\U00000127', + "hybull;": '\U00002043', + "hyphen;": '\U00002010', + "iacute;": '\U000000ED', + "ic;": '\U00002063', + "icirc;": '\U000000EE', + "icy;": '\U00000438', + "iecy;": '\U00000435', + "iexcl;": '\U000000A1', + "iff;": '\U000021D4', + "ifr;": '\U0001D526', + "igrave;": '\U000000EC', + "ii;": '\U00002148', + "iiiint;": '\U00002A0C', + "iiint;": '\U0000222D', + "iinfin;": '\U000029DC', + "iiota;": '\U00002129', + "ijlig;": '\U00000133', + "imacr;": '\U0000012B', + "image;": '\U00002111', + "imagline;": '\U00002110', + "imagpart;": '\U00002111', + "imath;": '\U00000131', + "imof;": '\U000022B7', + "imped;": '\U000001B5', + "in;": '\U00002208', + "incare;": '\U00002105', + "infin;": '\U0000221E', + "infintie;": '\U000029DD', + "inodot;": '\U00000131', + "int;": '\U0000222B', + "intcal;": '\U000022BA', + "integers;": '\U00002124', + "intercal;": '\U000022BA', + "intlarhk;": '\U00002A17', + "intprod;": '\U00002A3C', + "iocy;": '\U00000451', + "iogon;": '\U0000012F', + "iopf;": '\U0001D55A', + "iota;": '\U000003B9', + "iprod;": '\U00002A3C', + "iquest;": '\U000000BF', + "iscr;": '\U0001D4BE', + "isin;": '\U00002208', + "isinE;": '\U000022F9', + "isindot;": '\U000022F5', + "isins;": '\U000022F4', + "isinsv;": '\U000022F3', + "isinv;": '\U00002208', + "it;": '\U00002062', + "itilde;": '\U00000129', + "iukcy;": '\U00000456', + "iuml;": '\U000000EF', + "jcirc;": '\U00000135', + "jcy;": '\U00000439', + "jfr;": '\U0001D527', + "jmath;": '\U00000237', + "jopf;": '\U0001D55B', + "jscr;": '\U0001D4BF', + "jsercy;": '\U00000458', + "jukcy;": '\U00000454', + "kappa;": '\U000003BA', + "kappav;": '\U000003F0', + "kcedil;": '\U00000137', + "kcy;": '\U0000043A', + "kfr;": '\U0001D528', + "kgreen;": '\U00000138', + "khcy;": '\U00000445', + "kjcy;": '\U0000045C', + "kopf;": '\U0001D55C', + "kscr;": '\U0001D4C0', + "lAarr;": '\U000021DA', + "lArr;": '\U000021D0', + "lAtail;": '\U0000291B', + "lBarr;": '\U0000290E', + "lE;": '\U00002266', + "lEg;": '\U00002A8B', + "lHar;": '\U00002962', + "lacute;": '\U0000013A', + "laemptyv;": '\U000029B4', + "lagran;": '\U00002112', + "lambda;": '\U000003BB', + "lang;": '\U000027E8', + "langd;": '\U00002991', + "langle;": '\U000027E8', + "lap;": '\U00002A85', + "laquo;": '\U000000AB', + "larr;": '\U00002190', + "larrb;": '\U000021E4', + "larrbfs;": '\U0000291F', + "larrfs;": '\U0000291D', + "larrhk;": '\U000021A9', + "larrlp;": '\U000021AB', + "larrpl;": '\U00002939', + "larrsim;": '\U00002973', + "larrtl;": '\U000021A2', + "lat;": '\U00002AAB', + "latail;": '\U00002919', + "late;": '\U00002AAD', + "lbarr;": '\U0000290C', + "lbbrk;": '\U00002772', + "lbrace;": '\U0000007B', + "lbrack;": '\U0000005B', + "lbrke;": '\U0000298B', + "lbrksld;": '\U0000298F', + "lbrkslu;": '\U0000298D', + "lcaron;": '\U0000013E', + "lcedil;": '\U0000013C', + "lceil;": '\U00002308', + "lcub;": '\U0000007B', + "lcy;": '\U0000043B', + "ldca;": '\U00002936', + "ldquo;": '\U0000201C', + "ldquor;": '\U0000201E', + "ldrdhar;": '\U00002967', + "ldrushar;": '\U0000294B', + "ldsh;": '\U000021B2', + "le;": '\U00002264', + "leftarrow;": '\U00002190', + "leftarrowtail;": '\U000021A2', + "leftharpoondown;": '\U000021BD', + "leftharpoonup;": '\U000021BC', + "leftleftarrows;": '\U000021C7', + "leftrightarrow;": '\U00002194', + "leftrightarrows;": '\U000021C6', + "leftrightharpoons;": '\U000021CB', + "leftrightsquigarrow;": '\U000021AD', + "leftthreetimes;": '\U000022CB', + "leg;": '\U000022DA', + "leq;": '\U00002264', + "leqq;": '\U00002266', + "leqslant;": '\U00002A7D', + "les;": '\U00002A7D', + "lescc;": '\U00002AA8', + "lesdot;": '\U00002A7F', + "lesdoto;": '\U00002A81', + "lesdotor;": '\U00002A83', + "lesges;": '\U00002A93', + "lessapprox;": '\U00002A85', + "lessdot;": '\U000022D6', + "lesseqgtr;": '\U000022DA', + "lesseqqgtr;": '\U00002A8B', + "lessgtr;": '\U00002276', + "lesssim;": '\U00002272', + "lfisht;": '\U0000297C', + "lfloor;": '\U0000230A', + "lfr;": '\U0001D529', + "lg;": '\U00002276', + "lgE;": '\U00002A91', + "lhard;": '\U000021BD', + "lharu;": '\U000021BC', + "lharul;": '\U0000296A', + "lhblk;": '\U00002584', + "ljcy;": '\U00000459', + "ll;": '\U0000226A', + "llarr;": '\U000021C7', + "llcorner;": '\U0000231E', + "llhard;": '\U0000296B', + "lltri;": '\U000025FA', + "lmidot;": '\U00000140', + "lmoust;": '\U000023B0', + "lmoustache;": '\U000023B0', + "lnE;": '\U00002268', + "lnap;": '\U00002A89', + "lnapprox;": '\U00002A89', + "lne;": '\U00002A87', + "lneq;": '\U00002A87', + "lneqq;": '\U00002268', + "lnsim;": '\U000022E6', + "loang;": '\U000027EC', + "loarr;": '\U000021FD', + "lobrk;": '\U000027E6', + "longleftarrow;": '\U000027F5', + "longleftrightarrow;": '\U000027F7', + "longmapsto;": '\U000027FC', + "longrightarrow;": '\U000027F6', + "looparrowleft;": '\U000021AB', + "looparrowright;": '\U000021AC', + "lopar;": '\U00002985', + "lopf;": '\U0001D55D', + "loplus;": '\U00002A2D', + "lotimes;": '\U00002A34', + "lowast;": '\U00002217', + "lowbar;": '\U0000005F', + "loz;": '\U000025CA', + "lozenge;": '\U000025CA', + "lozf;": '\U000029EB', + "lpar;": '\U00000028', + "lparlt;": '\U00002993', + "lrarr;": '\U000021C6', + "lrcorner;": '\U0000231F', + "lrhar;": '\U000021CB', + "lrhard;": '\U0000296D', + "lrm;": '\U0000200E', + "lrtri;": '\U000022BF', + "lsaquo;": '\U00002039', + "lscr;": '\U0001D4C1', + "lsh;": '\U000021B0', + "lsim;": '\U00002272', + "lsime;": '\U00002A8D', + "lsimg;": '\U00002A8F', + "lsqb;": '\U0000005B', + "lsquo;": '\U00002018', + "lsquor;": '\U0000201A', + "lstrok;": '\U00000142', + "lt;": '\U0000003C', + "ltcc;": '\U00002AA6', + "ltcir;": '\U00002A79', + "ltdot;": '\U000022D6', + "lthree;": '\U000022CB', + "ltimes;": '\U000022C9', + "ltlarr;": '\U00002976', + "ltquest;": '\U00002A7B', + "ltrPar;": '\U00002996', + "ltri;": '\U000025C3', + "ltrie;": '\U000022B4', + "ltrif;": '\U000025C2', + "lurdshar;": '\U0000294A', + "luruhar;": '\U00002966', + "mDDot;": '\U0000223A', + "macr;": '\U000000AF', + "male;": '\U00002642', + "malt;": '\U00002720', + "maltese;": '\U00002720', + "map;": '\U000021A6', + "mapsto;": '\U000021A6', + "mapstodown;": '\U000021A7', + "mapstoleft;": '\U000021A4', + "mapstoup;": '\U000021A5', + "marker;": '\U000025AE', + "mcomma;": '\U00002A29', + "mcy;": '\U0000043C', + "mdash;": '\U00002014', + "measuredangle;": '\U00002221', + "mfr;": '\U0001D52A', + "mho;": '\U00002127', + "micro;": '\U000000B5', + "mid;": '\U00002223', + "midast;": '\U0000002A', + "midcir;": '\U00002AF0', + "middot;": '\U000000B7', + "minus;": '\U00002212', + "minusb;": '\U0000229F', + "minusd;": '\U00002238', + "minusdu;": '\U00002A2A', + "mlcp;": '\U00002ADB', + "mldr;": '\U00002026', + "mnplus;": '\U00002213', + "models;": '\U000022A7', + "mopf;": '\U0001D55E', + "mp;": '\U00002213', + "mscr;": '\U0001D4C2', + "mstpos;": '\U0000223E', + "mu;": '\U000003BC', + "multimap;": '\U000022B8', + "mumap;": '\U000022B8', + "nLeftarrow;": '\U000021CD', + "nLeftrightarrow;": '\U000021CE', + "nRightarrow;": '\U000021CF', + "nVDash;": '\U000022AF', + "nVdash;": '\U000022AE', + "nabla;": '\U00002207', + "nacute;": '\U00000144', + "nap;": '\U00002249', + "napos;": '\U00000149', + "napprox;": '\U00002249', + "natur;": '\U0000266E', + "natural;": '\U0000266E', + "naturals;": '\U00002115', + "nbsp;": '\U000000A0', + "ncap;": '\U00002A43', + "ncaron;": '\U00000148', + "ncedil;": '\U00000146', + "ncong;": '\U00002247', + "ncup;": '\U00002A42', + "ncy;": '\U0000043D', + "ndash;": '\U00002013', + "ne;": '\U00002260', + "neArr;": '\U000021D7', + "nearhk;": '\U00002924', + "nearr;": '\U00002197', + "nearrow;": '\U00002197', + "nequiv;": '\U00002262', + "nesear;": '\U00002928', + "nexist;": '\U00002204', + "nexists;": '\U00002204', + "nfr;": '\U0001D52B', + "nge;": '\U00002271', + "ngeq;": '\U00002271', + "ngsim;": '\U00002275', + "ngt;": '\U0000226F', + "ngtr;": '\U0000226F', + "nhArr;": '\U000021CE', + "nharr;": '\U000021AE', + "nhpar;": '\U00002AF2', + "ni;": '\U0000220B', + "nis;": '\U000022FC', + "nisd;": '\U000022FA', + "niv;": '\U0000220B', + "njcy;": '\U0000045A', + "nlArr;": '\U000021CD', + "nlarr;": '\U0000219A', + "nldr;": '\U00002025', + "nle;": '\U00002270', + "nleftarrow;": '\U0000219A', + "nleftrightarrow;": '\U000021AE', + "nleq;": '\U00002270', + "nless;": '\U0000226E', + "nlsim;": '\U00002274', + "nlt;": '\U0000226E', + "nltri;": '\U000022EA', + "nltrie;": '\U000022EC', + "nmid;": '\U00002224', + "nopf;": '\U0001D55F', + "not;": '\U000000AC', + "notin;": '\U00002209', + "notinva;": '\U00002209', + "notinvb;": '\U000022F7', + "notinvc;": '\U000022F6', + "notni;": '\U0000220C', + "notniva;": '\U0000220C', + "notnivb;": '\U000022FE', + "notnivc;": '\U000022FD', + "npar;": '\U00002226', + "nparallel;": '\U00002226', + "npolint;": '\U00002A14', + "npr;": '\U00002280', + "nprcue;": '\U000022E0', + "nprec;": '\U00002280', + "nrArr;": '\U000021CF', + "nrarr;": '\U0000219B', + "nrightarrow;": '\U0000219B', + "nrtri;": '\U000022EB', + "nrtrie;": '\U000022ED', + "nsc;": '\U00002281', + "nsccue;": '\U000022E1', + "nscr;": '\U0001D4C3', + "nshortmid;": '\U00002224', + "nshortparallel;": '\U00002226', + "nsim;": '\U00002241', + "nsime;": '\U00002244', + "nsimeq;": '\U00002244', + "nsmid;": '\U00002224', + "nspar;": '\U00002226', + "nsqsube;": '\U000022E2', + "nsqsupe;": '\U000022E3', + "nsub;": '\U00002284', + "nsube;": '\U00002288', + "nsubseteq;": '\U00002288', + "nsucc;": '\U00002281', + "nsup;": '\U00002285', + "nsupe;": '\U00002289', + "nsupseteq;": '\U00002289', + "ntgl;": '\U00002279', + "ntilde;": '\U000000F1', + "ntlg;": '\U00002278', + "ntriangleleft;": '\U000022EA', + "ntrianglelefteq;": '\U000022EC', + "ntriangleright;": '\U000022EB', + "ntrianglerighteq;": '\U000022ED', + "nu;": '\U000003BD', + "num;": '\U00000023', + "numero;": '\U00002116', + "numsp;": '\U00002007', + "nvDash;": '\U000022AD', + "nvHarr;": '\U00002904', + "nvdash;": '\U000022AC', + "nvinfin;": '\U000029DE', + "nvlArr;": '\U00002902', + "nvrArr;": '\U00002903', + "nwArr;": '\U000021D6', + "nwarhk;": '\U00002923', + "nwarr;": '\U00002196', + "nwarrow;": '\U00002196', + "nwnear;": '\U00002927', + "oS;": '\U000024C8', + "oacute;": '\U000000F3', + "oast;": '\U0000229B', + "ocir;": '\U0000229A', + "ocirc;": '\U000000F4', + "ocy;": '\U0000043E', + "odash;": '\U0000229D', + "odblac;": '\U00000151', + "odiv;": '\U00002A38', + "odot;": '\U00002299', + "odsold;": '\U000029BC', + "oelig;": '\U00000153', + "ofcir;": '\U000029BF', + "ofr;": '\U0001D52C', + "ogon;": '\U000002DB', + "ograve;": '\U000000F2', + "ogt;": '\U000029C1', + "ohbar;": '\U000029B5', + "ohm;": '\U000003A9', + "oint;": '\U0000222E', + "olarr;": '\U000021BA', + "olcir;": '\U000029BE', + "olcross;": '\U000029BB', + "oline;": '\U0000203E', + "olt;": '\U000029C0', + "omacr;": '\U0000014D', + "omega;": '\U000003C9', + "omicron;": '\U000003BF', + "omid;": '\U000029B6', + "ominus;": '\U00002296', + "oopf;": '\U0001D560', + "opar;": '\U000029B7', + "operp;": '\U000029B9', + "oplus;": '\U00002295', + "or;": '\U00002228', + "orarr;": '\U000021BB', + "ord;": '\U00002A5D', + "order;": '\U00002134', + "orderof;": '\U00002134', + "ordf;": '\U000000AA', + "ordm;": '\U000000BA', + "origof;": '\U000022B6', + "oror;": '\U00002A56', + "orslope;": '\U00002A57', + "orv;": '\U00002A5B', + "oscr;": '\U00002134', + "oslash;": '\U000000F8', + "osol;": '\U00002298', + "otilde;": '\U000000F5', + "otimes;": '\U00002297', + "otimesas;": '\U00002A36', + "ouml;": '\U000000F6', + "ovbar;": '\U0000233D', + "par;": '\U00002225', + "para;": '\U000000B6', + "parallel;": '\U00002225', + "parsim;": '\U00002AF3', + "parsl;": '\U00002AFD', + "part;": '\U00002202', + "pcy;": '\U0000043F', + "percnt;": '\U00000025', + "period;": '\U0000002E', + "permil;": '\U00002030', + "perp;": '\U000022A5', + "pertenk;": '\U00002031', + "pfr;": '\U0001D52D', + "phi;": '\U000003C6', + "phiv;": '\U000003D5', + "phmmat;": '\U00002133', + "phone;": '\U0000260E', + "pi;": '\U000003C0', + "pitchfork;": '\U000022D4', + "piv;": '\U000003D6', + "planck;": '\U0000210F', + "planckh;": '\U0000210E', + "plankv;": '\U0000210F', + "plus;": '\U0000002B', + "plusacir;": '\U00002A23', + "plusb;": '\U0000229E', + "pluscir;": '\U00002A22', + "plusdo;": '\U00002214', + "plusdu;": '\U00002A25', + "pluse;": '\U00002A72', + "plusmn;": '\U000000B1', + "plussim;": '\U00002A26', + "plustwo;": '\U00002A27', + "pm;": '\U000000B1', + "pointint;": '\U00002A15', + "popf;": '\U0001D561', + "pound;": '\U000000A3', + "pr;": '\U0000227A', + "prE;": '\U00002AB3', + "prap;": '\U00002AB7', + "prcue;": '\U0000227C', + "pre;": '\U00002AAF', + "prec;": '\U0000227A', + "precapprox;": '\U00002AB7', + "preccurlyeq;": '\U0000227C', + "preceq;": '\U00002AAF', + "precnapprox;": '\U00002AB9', + "precneqq;": '\U00002AB5', + "precnsim;": '\U000022E8', + "precsim;": '\U0000227E', + "prime;": '\U00002032', + "primes;": '\U00002119', + "prnE;": '\U00002AB5', + "prnap;": '\U00002AB9', + "prnsim;": '\U000022E8', + "prod;": '\U0000220F', + "profalar;": '\U0000232E', + "profline;": '\U00002312', + "profsurf;": '\U00002313', + "prop;": '\U0000221D', + "propto;": '\U0000221D', + "prsim;": '\U0000227E', + "prurel;": '\U000022B0', + "pscr;": '\U0001D4C5', + "psi;": '\U000003C8', + "puncsp;": '\U00002008', + "qfr;": '\U0001D52E', + "qint;": '\U00002A0C', + "qopf;": '\U0001D562', + "qprime;": '\U00002057', + "qscr;": '\U0001D4C6', + "quaternions;": '\U0000210D', + "quatint;": '\U00002A16', + "quest;": '\U0000003F', + "questeq;": '\U0000225F', + "quot;": '\U00000022', + "rAarr;": '\U000021DB', + "rArr;": '\U000021D2', + "rAtail;": '\U0000291C', + "rBarr;": '\U0000290F', + "rHar;": '\U00002964', + "racute;": '\U00000155', + "radic;": '\U0000221A', + "raemptyv;": '\U000029B3', + "rang;": '\U000027E9', + "rangd;": '\U00002992', + "range;": '\U000029A5', + "rangle;": '\U000027E9', + "raquo;": '\U000000BB', + "rarr;": '\U00002192', + "rarrap;": '\U00002975', + "rarrb;": '\U000021E5', + "rarrbfs;": '\U00002920', + "rarrc;": '\U00002933', + "rarrfs;": '\U0000291E', + "rarrhk;": '\U000021AA', + "rarrlp;": '\U000021AC', + "rarrpl;": '\U00002945', + "rarrsim;": '\U00002974', + "rarrtl;": '\U000021A3', + "rarrw;": '\U0000219D', + "ratail;": '\U0000291A', + "ratio;": '\U00002236', + "rationals;": '\U0000211A', + "rbarr;": '\U0000290D', + "rbbrk;": '\U00002773', + "rbrace;": '\U0000007D', + "rbrack;": '\U0000005D', + "rbrke;": '\U0000298C', + "rbrksld;": '\U0000298E', + "rbrkslu;": '\U00002990', + "rcaron;": '\U00000159', + "rcedil;": '\U00000157', + "rceil;": '\U00002309', + "rcub;": '\U0000007D', + "rcy;": '\U00000440', + "rdca;": '\U00002937', + "rdldhar;": '\U00002969', + "rdquo;": '\U0000201D', + "rdquor;": '\U0000201D', + "rdsh;": '\U000021B3', + "real;": '\U0000211C', + "realine;": '\U0000211B', + "realpart;": '\U0000211C', + "reals;": '\U0000211D', + "rect;": '\U000025AD', + "reg;": '\U000000AE', + "rfisht;": '\U0000297D', + "rfloor;": '\U0000230B', + "rfr;": '\U0001D52F', + "rhard;": '\U000021C1', + "rharu;": '\U000021C0', + "rharul;": '\U0000296C', + "rho;": '\U000003C1', + "rhov;": '\U000003F1', + "rightarrow;": '\U00002192', + "rightarrowtail;": '\U000021A3', + "rightharpoondown;": '\U000021C1', + "rightharpoonup;": '\U000021C0', + "rightleftarrows;": '\U000021C4', + "rightleftharpoons;": '\U000021CC', + "rightrightarrows;": '\U000021C9', + "rightsquigarrow;": '\U0000219D', + "rightthreetimes;": '\U000022CC', + "ring;": '\U000002DA', + "risingdotseq;": '\U00002253', + "rlarr;": '\U000021C4', + "rlhar;": '\U000021CC', + "rlm;": '\U0000200F', + "rmoust;": '\U000023B1', + "rmoustache;": '\U000023B1', + "rnmid;": '\U00002AEE', + "roang;": '\U000027ED', + "roarr;": '\U000021FE', + "robrk;": '\U000027E7', + "ropar;": '\U00002986', + "ropf;": '\U0001D563', + "roplus;": '\U00002A2E', + "rotimes;": '\U00002A35', + "rpar;": '\U00000029', + "rpargt;": '\U00002994', + "rppolint;": '\U00002A12', + "rrarr;": '\U000021C9', + "rsaquo;": '\U0000203A', + "rscr;": '\U0001D4C7', + "rsh;": '\U000021B1', + "rsqb;": '\U0000005D', + "rsquo;": '\U00002019', + "rsquor;": '\U00002019', + "rthree;": '\U000022CC', + "rtimes;": '\U000022CA', + "rtri;": '\U000025B9', + "rtrie;": '\U000022B5', + "rtrif;": '\U000025B8', + "rtriltri;": '\U000029CE', + "ruluhar;": '\U00002968', + "rx;": '\U0000211E', + "sacute;": '\U0000015B', + "sbquo;": '\U0000201A', + "sc;": '\U0000227B', + "scE;": '\U00002AB4', + "scap;": '\U00002AB8', + "scaron;": '\U00000161', + "sccue;": '\U0000227D', + "sce;": '\U00002AB0', + "scedil;": '\U0000015F', + "scirc;": '\U0000015D', + "scnE;": '\U00002AB6', + "scnap;": '\U00002ABA', + "scnsim;": '\U000022E9', + "scpolint;": '\U00002A13', + "scsim;": '\U0000227F', + "scy;": '\U00000441', + "sdot;": '\U000022C5', + "sdotb;": '\U000022A1', + "sdote;": '\U00002A66', + "seArr;": '\U000021D8', + "searhk;": '\U00002925', + "searr;": '\U00002198', + "searrow;": '\U00002198', + "sect;": '\U000000A7', + "semi;": '\U0000003B', + "seswar;": '\U00002929', + "setminus;": '\U00002216', + "setmn;": '\U00002216', + "sext;": '\U00002736', + "sfr;": '\U0001D530', + "sfrown;": '\U00002322', + "sharp;": '\U0000266F', + "shchcy;": '\U00000449', + "shcy;": '\U00000448', + "shortmid;": '\U00002223', + "shortparallel;": '\U00002225', + "shy;": '\U000000AD', + "sigma;": '\U000003C3', + "sigmaf;": '\U000003C2', + "sigmav;": '\U000003C2', + "sim;": '\U0000223C', + "simdot;": '\U00002A6A', + "sime;": '\U00002243', + "simeq;": '\U00002243', + "simg;": '\U00002A9E', + "simgE;": '\U00002AA0', + "siml;": '\U00002A9D', + "simlE;": '\U00002A9F', + "simne;": '\U00002246', + "simplus;": '\U00002A24', + "simrarr;": '\U00002972', + "slarr;": '\U00002190', + "smallsetminus;": '\U00002216', + "smashp;": '\U00002A33', + "smeparsl;": '\U000029E4', + "smid;": '\U00002223', + "smile;": '\U00002323', + "smt;": '\U00002AAA', + "smte;": '\U00002AAC', + "softcy;": '\U0000044C', + "sol;": '\U0000002F', + "solb;": '\U000029C4', + "solbar;": '\U0000233F', + "sopf;": '\U0001D564', + "spades;": '\U00002660', + "spadesuit;": '\U00002660', + "spar;": '\U00002225', + "sqcap;": '\U00002293', + "sqcup;": '\U00002294', + "sqsub;": '\U0000228F', + "sqsube;": '\U00002291', + "sqsubset;": '\U0000228F', + "sqsubseteq;": '\U00002291', + "sqsup;": '\U00002290', + "sqsupe;": '\U00002292', + "sqsupset;": '\U00002290', + "sqsupseteq;": '\U00002292', + "squ;": '\U000025A1', + "square;": '\U000025A1', + "squarf;": '\U000025AA', + "squf;": '\U000025AA', + "srarr;": '\U00002192', + "sscr;": '\U0001D4C8', + "ssetmn;": '\U00002216', + "ssmile;": '\U00002323', + "sstarf;": '\U000022C6', + "star;": '\U00002606', + "starf;": '\U00002605', + "straightepsilon;": '\U000003F5', + "straightphi;": '\U000003D5', + "strns;": '\U000000AF', + "sub;": '\U00002282', + "subE;": '\U00002AC5', + "subdot;": '\U00002ABD', + "sube;": '\U00002286', + "subedot;": '\U00002AC3', + "submult;": '\U00002AC1', + "subnE;": '\U00002ACB', + "subne;": '\U0000228A', + "subplus;": '\U00002ABF', + "subrarr;": '\U00002979', + "subset;": '\U00002282', + "subseteq;": '\U00002286', + "subseteqq;": '\U00002AC5', + "subsetneq;": '\U0000228A', + "subsetneqq;": '\U00002ACB', + "subsim;": '\U00002AC7', + "subsub;": '\U00002AD5', + "subsup;": '\U00002AD3', + "succ;": '\U0000227B', + "succapprox;": '\U00002AB8', + "succcurlyeq;": '\U0000227D', + "succeq;": '\U00002AB0', + "succnapprox;": '\U00002ABA', + "succneqq;": '\U00002AB6', + "succnsim;": '\U000022E9', + "succsim;": '\U0000227F', + "sum;": '\U00002211', + "sung;": '\U0000266A', + "sup;": '\U00002283', + "sup1;": '\U000000B9', + "sup2;": '\U000000B2', + "sup3;": '\U000000B3', + "supE;": '\U00002AC6', + "supdot;": '\U00002ABE', + "supdsub;": '\U00002AD8', + "supe;": '\U00002287', + "supedot;": '\U00002AC4', + "suphsol;": '\U000027C9', + "suphsub;": '\U00002AD7', + "suplarr;": '\U0000297B', + "supmult;": '\U00002AC2', + "supnE;": '\U00002ACC', + "supne;": '\U0000228B', + "supplus;": '\U00002AC0', + "supset;": '\U00002283', + "supseteq;": '\U00002287', + "supseteqq;": '\U00002AC6', + "supsetneq;": '\U0000228B', + "supsetneqq;": '\U00002ACC', + "supsim;": '\U00002AC8', + "supsub;": '\U00002AD4', + "supsup;": '\U00002AD6', + "swArr;": '\U000021D9', + "swarhk;": '\U00002926', + "swarr;": '\U00002199', + "swarrow;": '\U00002199', + "swnwar;": '\U0000292A', + "szlig;": '\U000000DF', + "target;": '\U00002316', + "tau;": '\U000003C4', + "tbrk;": '\U000023B4', + "tcaron;": '\U00000165', + "tcedil;": '\U00000163', + "tcy;": '\U00000442', + "tdot;": '\U000020DB', + "telrec;": '\U00002315', + "tfr;": '\U0001D531', + "there4;": '\U00002234', + "therefore;": '\U00002234', + "theta;": '\U000003B8', + "thetasym;": '\U000003D1', + "thetav;": '\U000003D1', + "thickapprox;": '\U00002248', + "thicksim;": '\U0000223C', + "thinsp;": '\U00002009', + "thkap;": '\U00002248', + "thksim;": '\U0000223C', + "thorn;": '\U000000FE', + "tilde;": '\U000002DC', + "times;": '\U000000D7', + "timesb;": '\U000022A0', + "timesbar;": '\U00002A31', + "timesd;": '\U00002A30', + "tint;": '\U0000222D', + "toea;": '\U00002928', + "top;": '\U000022A4', + "topbot;": '\U00002336', + "topcir;": '\U00002AF1', + "topf;": '\U0001D565', + "topfork;": '\U00002ADA', + "tosa;": '\U00002929', + "tprime;": '\U00002034', + "trade;": '\U00002122', + "triangle;": '\U000025B5', + "triangledown;": '\U000025BF', + "triangleleft;": '\U000025C3', + "trianglelefteq;": '\U000022B4', + "triangleq;": '\U0000225C', + "triangleright;": '\U000025B9', + "trianglerighteq;": '\U000022B5', + "tridot;": '\U000025EC', + "trie;": '\U0000225C', + "triminus;": '\U00002A3A', + "triplus;": '\U00002A39', + "trisb;": '\U000029CD', + "tritime;": '\U00002A3B', + "trpezium;": '\U000023E2', + "tscr;": '\U0001D4C9', + "tscy;": '\U00000446', + "tshcy;": '\U0000045B', + "tstrok;": '\U00000167', + "twixt;": '\U0000226C', + "twoheadleftarrow;": '\U0000219E', + "twoheadrightarrow;": '\U000021A0', + "uArr;": '\U000021D1', + "uHar;": '\U00002963', + "uacute;": '\U000000FA', + "uarr;": '\U00002191', + "ubrcy;": '\U0000045E', + "ubreve;": '\U0000016D', + "ucirc;": '\U000000FB', + "ucy;": '\U00000443', + "udarr;": '\U000021C5', + "udblac;": '\U00000171', + "udhar;": '\U0000296E', + "ufisht;": '\U0000297E', + "ufr;": '\U0001D532', + "ugrave;": '\U000000F9', + "uharl;": '\U000021BF', + "uharr;": '\U000021BE', + "uhblk;": '\U00002580', + "ulcorn;": '\U0000231C', + "ulcorner;": '\U0000231C', + "ulcrop;": '\U0000230F', + "ultri;": '\U000025F8', + "umacr;": '\U0000016B', + "uml;": '\U000000A8', + "uogon;": '\U00000173', + "uopf;": '\U0001D566', + "uparrow;": '\U00002191', + "updownarrow;": '\U00002195', + "upharpoonleft;": '\U000021BF', + "upharpoonright;": '\U000021BE', + "uplus;": '\U0000228E', + "upsi;": '\U000003C5', + "upsih;": '\U000003D2', + "upsilon;": '\U000003C5', + "upuparrows;": '\U000021C8', + "urcorn;": '\U0000231D', + "urcorner;": '\U0000231D', + "urcrop;": '\U0000230E', + "uring;": '\U0000016F', + "urtri;": '\U000025F9', + "uscr;": '\U0001D4CA', + "utdot;": '\U000022F0', + "utilde;": '\U00000169', + "utri;": '\U000025B5', + "utrif;": '\U000025B4', + "uuarr;": '\U000021C8', + "uuml;": '\U000000FC', + "uwangle;": '\U000029A7', + "vArr;": '\U000021D5', + "vBar;": '\U00002AE8', + "vBarv;": '\U00002AE9', + "vDash;": '\U000022A8', + "vangrt;": '\U0000299C', + "varepsilon;": '\U000003F5', + "varkappa;": '\U000003F0', + "varnothing;": '\U00002205', + "varphi;": '\U000003D5', + "varpi;": '\U000003D6', + "varpropto;": '\U0000221D', + "varr;": '\U00002195', + "varrho;": '\U000003F1', + "varsigma;": '\U000003C2', + "vartheta;": '\U000003D1', + "vartriangleleft;": '\U000022B2', + "vartriangleright;": '\U000022B3', + "vcy;": '\U00000432', + "vdash;": '\U000022A2', + "vee;": '\U00002228', + "veebar;": '\U000022BB', + "veeeq;": '\U0000225A', + "vellip;": '\U000022EE', + "verbar;": '\U0000007C', + "vert;": '\U0000007C', + "vfr;": '\U0001D533', + "vltri;": '\U000022B2', + "vopf;": '\U0001D567', + "vprop;": '\U0000221D', + "vrtri;": '\U000022B3', + "vscr;": '\U0001D4CB', + "vzigzag;": '\U0000299A', + "wcirc;": '\U00000175', + "wedbar;": '\U00002A5F', + "wedge;": '\U00002227', + "wedgeq;": '\U00002259', + "weierp;": '\U00002118', + "wfr;": '\U0001D534', + "wopf;": '\U0001D568', + "wp;": '\U00002118', + "wr;": '\U00002240', + "wreath;": '\U00002240', + "wscr;": '\U0001D4CC', + "xcap;": '\U000022C2', + "xcirc;": '\U000025EF', + "xcup;": '\U000022C3', + "xdtri;": '\U000025BD', + "xfr;": '\U0001D535', + "xhArr;": '\U000027FA', + "xharr;": '\U000027F7', + "xi;": '\U000003BE', + "xlArr;": '\U000027F8', + "xlarr;": '\U000027F5', + "xmap;": '\U000027FC', + "xnis;": '\U000022FB', + "xodot;": '\U00002A00', + "xopf;": '\U0001D569', + "xoplus;": '\U00002A01', + "xotime;": '\U00002A02', + "xrArr;": '\U000027F9', + "xrarr;": '\U000027F6', + "xscr;": '\U0001D4CD', + "xsqcup;": '\U00002A06', + "xuplus;": '\U00002A04', + "xutri;": '\U000025B3', + "xvee;": '\U000022C1', + "xwedge;": '\U000022C0', + "yacute;": '\U000000FD', + "yacy;": '\U0000044F', + "ycirc;": '\U00000177', + "ycy;": '\U0000044B', + "yen;": '\U000000A5', + "yfr;": '\U0001D536', + "yicy;": '\U00000457', + "yopf;": '\U0001D56A', + "yscr;": '\U0001D4CE', + "yucy;": '\U0000044E', + "yuml;": '\U000000FF', + "zacute;": '\U0000017A', + "zcaron;": '\U0000017E', + "zcy;": '\U00000437', + "zdot;": '\U0000017C', + "zeetrf;": '\U00002128', + "zeta;": '\U000003B6', + "zfr;": '\U0001D537', + "zhcy;": '\U00000436', + "zigrarr;": '\U000021DD', + "zopf;": '\U0001D56B', + "zscr;": '\U0001D4CF', + "zwj;": '\U0000200D', + "zwnj;": '\U0000200C', + "AElig": '\U000000C6', + "AMP": '\U00000026', + "Aacute": '\U000000C1', + "Acirc": '\U000000C2', + "Agrave": '\U000000C0', + "Aring": '\U000000C5', + "Atilde": '\U000000C3', + "Auml": '\U000000C4', + "COPY": '\U000000A9', + "Ccedil": '\U000000C7', + "ETH": '\U000000D0', + "Eacute": '\U000000C9', + "Ecirc": '\U000000CA', + "Egrave": '\U000000C8', + "Euml": '\U000000CB', + "GT": '\U0000003E', + "Iacute": '\U000000CD', + "Icirc": '\U000000CE', + "Igrave": '\U000000CC', + "Iuml": '\U000000CF', + "LT": '\U0000003C', + "Ntilde": '\U000000D1', + "Oacute": '\U000000D3', + "Ocirc": '\U000000D4', + "Ograve": '\U000000D2', + "Oslash": '\U000000D8', + "Otilde": '\U000000D5', + "Ouml": '\U000000D6', + "QUOT": '\U00000022', + "REG": '\U000000AE', + "THORN": '\U000000DE', + "Uacute": '\U000000DA', + "Ucirc": '\U000000DB', + "Ugrave": '\U000000D9', + "Uuml": '\U000000DC', + "Yacute": '\U000000DD', + "aacute": '\U000000E1', + "acirc": '\U000000E2', + "acute": '\U000000B4', + "aelig": '\U000000E6', + "agrave": '\U000000E0', + "amp": '\U00000026', + "aring": '\U000000E5', + "atilde": '\U000000E3', + "auml": '\U000000E4', + "brvbar": '\U000000A6', + "ccedil": '\U000000E7', + "cedil": '\U000000B8', + "cent": '\U000000A2', + "copy": '\U000000A9', + "curren": '\U000000A4', + "deg": '\U000000B0', + "divide": '\U000000F7', + "eacute": '\U000000E9', + "ecirc": '\U000000EA', + "egrave": '\U000000E8', + "eth": '\U000000F0', + "euml": '\U000000EB', + "frac12": '\U000000BD', + "frac14": '\U000000BC', + "frac34": '\U000000BE', + "gt": '\U0000003E', + "iacute": '\U000000ED', + "icirc": '\U000000EE', + "iexcl": '\U000000A1', + "igrave": '\U000000EC', + "iquest": '\U000000BF', + "iuml": '\U000000EF', + "laquo": '\U000000AB', + "lt": '\U0000003C', + "macr": '\U000000AF', + "micro": '\U000000B5', + "middot": '\U000000B7', + "nbsp": '\U000000A0', + "not": '\U000000AC', + "ntilde": '\U000000F1', + "oacute": '\U000000F3', + "ocirc": '\U000000F4', + "ograve": '\U000000F2', + "ordf": '\U000000AA', + "ordm": '\U000000BA', + "oslash": '\U000000F8', + "otilde": '\U000000F5', + "ouml": '\U000000F6', + "para": '\U000000B6', + "plusmn": '\U000000B1', + "pound": '\U000000A3', + "quot": '\U00000022', + "raquo": '\U000000BB', + "reg": '\U000000AE', + "sect": '\U000000A7', + "shy": '\U000000AD', + "sup1": '\U000000B9', + "sup2": '\U000000B2', + "sup3": '\U000000B3', + "szlig": '\U000000DF', + "thorn": '\U000000FE', + "times": '\U000000D7', + "uacute": '\U000000FA', + "ucirc": '\U000000FB', + "ugrave": '\U000000F9', + "uml": '\U000000A8', + "uuml": '\U000000FC', + "yacute": '\U000000FD', + "yen": '\U000000A5', + "yuml": '\U000000FF', +} + +// HTML entities that are two unicode codepoints. +var entity2 = map[string][2]rune{ + // TODO(nigeltao): Handle replacements that are wider than their names. + // "nLt;": {'\u226A', '\u20D2'}, + // "nGt;": {'\u226B', '\u20D2'}, + "NotEqualTilde;": {'\u2242', '\u0338'}, + "NotGreaterFullEqual;": {'\u2267', '\u0338'}, + "NotGreaterGreater;": {'\u226B', '\u0338'}, + "NotGreaterSlantEqual;": {'\u2A7E', '\u0338'}, + "NotHumpDownHump;": {'\u224E', '\u0338'}, + "NotHumpEqual;": {'\u224F', '\u0338'}, + "NotLeftTriangleBar;": {'\u29CF', '\u0338'}, + "NotLessLess;": {'\u226A', '\u0338'}, + "NotLessSlantEqual;": {'\u2A7D', '\u0338'}, + "NotNestedGreaterGreater;": {'\u2AA2', '\u0338'}, + "NotNestedLessLess;": {'\u2AA1', '\u0338'}, + "NotPrecedesEqual;": {'\u2AAF', '\u0338'}, + "NotRightTriangleBar;": {'\u29D0', '\u0338'}, + "NotSquareSubset;": {'\u228F', '\u0338'}, + "NotSquareSuperset;": {'\u2290', '\u0338'}, + "NotSubset;": {'\u2282', '\u20D2'}, + "NotSucceedsEqual;": {'\u2AB0', '\u0338'}, + "NotSucceedsTilde;": {'\u227F', '\u0338'}, + "NotSuperset;": {'\u2283', '\u20D2'}, + "ThickSpace;": {'\u205F', '\u200A'}, + "acE;": {'\u223E', '\u0333'}, + "bne;": {'\u003D', '\u20E5'}, + "bnequiv;": {'\u2261', '\u20E5'}, + "caps;": {'\u2229', '\uFE00'}, + "cups;": {'\u222A', '\uFE00'}, + "fjlig;": {'\u0066', '\u006A'}, + "gesl;": {'\u22DB', '\uFE00'}, + "gvertneqq;": {'\u2269', '\uFE00'}, + "gvnE;": {'\u2269', '\uFE00'}, + "lates;": {'\u2AAD', '\uFE00'}, + "lesg;": {'\u22DA', '\uFE00'}, + "lvertneqq;": {'\u2268', '\uFE00'}, + "lvnE;": {'\u2268', '\uFE00'}, + "nGg;": {'\u22D9', '\u0338'}, + "nGtv;": {'\u226B', '\u0338'}, + "nLl;": {'\u22D8', '\u0338'}, + "nLtv;": {'\u226A', '\u0338'}, + "nang;": {'\u2220', '\u20D2'}, + "napE;": {'\u2A70', '\u0338'}, + "napid;": {'\u224B', '\u0338'}, + "nbump;": {'\u224E', '\u0338'}, + "nbumpe;": {'\u224F', '\u0338'}, + "ncongdot;": {'\u2A6D', '\u0338'}, + "nedot;": {'\u2250', '\u0338'}, + "nesim;": {'\u2242', '\u0338'}, + "ngE;": {'\u2267', '\u0338'}, + "ngeqq;": {'\u2267', '\u0338'}, + "ngeqslant;": {'\u2A7E', '\u0338'}, + "nges;": {'\u2A7E', '\u0338'}, + "nlE;": {'\u2266', '\u0338'}, + "nleqq;": {'\u2266', '\u0338'}, + "nleqslant;": {'\u2A7D', '\u0338'}, + "nles;": {'\u2A7D', '\u0338'}, + "notinE;": {'\u22F9', '\u0338'}, + "notindot;": {'\u22F5', '\u0338'}, + "nparsl;": {'\u2AFD', '\u20E5'}, + "npart;": {'\u2202', '\u0338'}, + "npre;": {'\u2AAF', '\u0338'}, + "npreceq;": {'\u2AAF', '\u0338'}, + "nrarrc;": {'\u2933', '\u0338'}, + "nrarrw;": {'\u219D', '\u0338'}, + "nsce;": {'\u2AB0', '\u0338'}, + "nsubE;": {'\u2AC5', '\u0338'}, + "nsubset;": {'\u2282', '\u20D2'}, + "nsubseteqq;": {'\u2AC5', '\u0338'}, + "nsucceq;": {'\u2AB0', '\u0338'}, + "nsupE;": {'\u2AC6', '\u0338'}, + "nsupset;": {'\u2283', '\u20D2'}, + "nsupseteqq;": {'\u2AC6', '\u0338'}, + "nvap;": {'\u224D', '\u20D2'}, + "nvge;": {'\u2265', '\u20D2'}, + "nvgt;": {'\u003E', '\u20D2'}, + "nvle;": {'\u2264', '\u20D2'}, + "nvlt;": {'\u003C', '\u20D2'}, + "nvltrie;": {'\u22B4', '\u20D2'}, + "nvrtrie;": {'\u22B5', '\u20D2'}, + "nvsim;": {'\u223C', '\u20D2'}, + "race;": {'\u223D', '\u0331'}, + "smtes;": {'\u2AAC', '\uFE00'}, + "sqcaps;": {'\u2293', '\uFE00'}, + "sqcups;": {'\u2294', '\uFE00'}, + "varsubsetneq;": {'\u228A', '\uFE00'}, + "varsubsetneqq;": {'\u2ACB', '\uFE00'}, + "varsupsetneq;": {'\u228B', '\uFE00'}, + "varsupsetneqq;": {'\u2ACC', '\uFE00'}, + "vnsub;": {'\u2282', '\u20D2'}, + "vnsup;": {'\u2283', '\u20D2'}, + "vsubnE;": {'\u2ACB', '\uFE00'}, + "vsubne;": {'\u228A', '\uFE00'}, + "vsupnE;": {'\u2ACC', '\uFE00'}, + "vsupne;": {'\u228B', '\uFE00'}, +} diff --git a/vendor/golang.org/x/net/html/escape.go b/vendor/golang.org/x/net/html/escape.go new file mode 100644 index 00000000000..d8561396200 --- /dev/null +++ b/vendor/golang.org/x/net/html/escape.go @@ -0,0 +1,258 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "bytes" + "strings" + "unicode/utf8" +) + +// These replacements permit compatibility with old numeric entities that +// assumed Windows-1252 encoding. +// https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference +var replacementTable = [...]rune{ + '\u20AC', // First entry is what 0x80 should be replaced with. + '\u0081', + '\u201A', + '\u0192', + '\u201E', + '\u2026', + '\u2020', + '\u2021', + '\u02C6', + '\u2030', + '\u0160', + '\u2039', + '\u0152', + '\u008D', + '\u017D', + '\u008F', + '\u0090', + '\u2018', + '\u2019', + '\u201C', + '\u201D', + '\u2022', + '\u2013', + '\u2014', + '\u02DC', + '\u2122', + '\u0161', + '\u203A', + '\u0153', + '\u009D', + '\u017E', + '\u0178', // Last entry is 0x9F. + // 0x00->'\uFFFD' is handled programmatically. + // 0x0D->'\u000D' is a no-op. +} + +// unescapeEntity reads an entity like "<" from b[src:] and writes the +// corresponding "<" to b[dst:], returning the incremented dst and src cursors. +// Precondition: b[src] == '&' && dst <= src. +// attribute should be true if parsing an attribute value. +func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) { + // https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference + + // i starts at 1 because we already know that s[0] == '&'. + i, s := 1, b[src:] + + if len(s) <= 1 { + b[dst] = b[src] + return dst + 1, src + 1 + } + + if s[i] == '#' { + if len(s) <= 3 { // We need to have at least "&#.". + b[dst] = b[src] + return dst + 1, src + 1 + } + i++ + c := s[i] + hex := false + if c == 'x' || c == 'X' { + hex = true + i++ + } + + x := '\x00' + for i < len(s) { + c = s[i] + i++ + if hex { + if '0' <= c && c <= '9' { + x = 16*x + rune(c) - '0' + continue + } else if 'a' <= c && c <= 'f' { + x = 16*x + rune(c) - 'a' + 10 + continue + } else if 'A' <= c && c <= 'F' { + x = 16*x + rune(c) - 'A' + 10 + continue + } + } else if '0' <= c && c <= '9' { + x = 10*x + rune(c) - '0' + continue + } + if c != ';' { + i-- + } + break + } + + if i <= 3 { // No characters matched. + b[dst] = b[src] + return dst + 1, src + 1 + } + + if 0x80 <= x && x <= 0x9F { + // Replace characters from Windows-1252 with UTF-8 equivalents. + x = replacementTable[x-0x80] + } else if x == 0 || (0xD800 <= x && x <= 0xDFFF) || x > 0x10FFFF { + // Replace invalid characters with the replacement character. + x = '\uFFFD' + } + + return dst + utf8.EncodeRune(b[dst:], x), src + i + } + + // Consume the maximum number of characters possible, with the + // consumed characters matching one of the named references. + + for i < len(s) { + c := s[i] + i++ + // Lower-cased characters are more common in entities, so we check for them first. + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { + continue + } + if c != ';' { + i-- + } + break + } + + entityName := string(s[1:i]) + if entityName == "" { + // No-op. + } else if attribute && entityName[len(entityName)-1] != ';' && len(s) > i && s[i] == '=' { + // No-op. + } else if x := entity[entityName]; x != 0 { + return dst + utf8.EncodeRune(b[dst:], x), src + i + } else if x := entity2[entityName]; x[0] != 0 { + dst1 := dst + utf8.EncodeRune(b[dst:], x[0]) + return dst1 + utf8.EncodeRune(b[dst1:], x[1]), src + i + } else if !attribute { + maxLen := len(entityName) - 1 + if maxLen > longestEntityWithoutSemicolon { + maxLen = longestEntityWithoutSemicolon + } + for j := maxLen; j > 1; j-- { + if x := entity[entityName[:j]]; x != 0 { + return dst + utf8.EncodeRune(b[dst:], x), src + j + 1 + } + } + } + + dst1, src1 = dst+i, src+i + copy(b[dst:dst1], b[src:src1]) + return dst1, src1 +} + +// unescape unescapes b's entities in-place, so that "a<b" becomes "a': + esc = ">" + case '"': + // """ is shorter than """. + esc = """ + case '\r': + esc = " " + default: + panic("unrecognized escape character") + } + s = s[i+1:] + if _, err := w.WriteString(esc); err != nil { + return err + } + i = strings.IndexAny(s, escapedChars) + } + _, err := w.WriteString(s) + return err +} + +// EscapeString escapes special characters like "<" to become "<". It +// escapes only five such characters: <, >, &, ' and ". +// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't +// always true. +func EscapeString(s string) string { + if strings.IndexAny(s, escapedChars) == -1 { + return s + } + var buf bytes.Buffer + escape(&buf, s) + return buf.String() +} + +// UnescapeString unescapes entities like "<" to become "<". It unescapes a +// larger range of entities than EscapeString escapes. For example, "á" +// unescapes to "á", as does "á" and "&xE1;". +// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't +// always true. +func UnescapeString(s string) string { + for _, c := range s { + if c == '&' { + return string(unescape([]byte(s), false)) + } + } + return s +} diff --git a/vendor/golang.org/x/net/html/foreign.go b/vendor/golang.org/x/net/html/foreign.go new file mode 100644 index 00000000000..9da9e9dc424 --- /dev/null +++ b/vendor/golang.org/x/net/html/foreign.go @@ -0,0 +1,222 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "strings" +) + +func adjustAttributeNames(aa []Attribute, nameMap map[string]string) { + for i := range aa { + if newName, ok := nameMap[aa[i].Key]; ok { + aa[i].Key = newName + } + } +} + +func adjustForeignAttributes(aa []Attribute) { + for i, a := range aa { + if a.Key == "" || a.Key[0] != 'x' { + continue + } + switch a.Key { + case "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show", + "xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "xmlns:xlink": + j := strings.Index(a.Key, ":") + aa[i].Namespace = a.Key[:j] + aa[i].Key = a.Key[j+1:] + } + } +} + +func htmlIntegrationPoint(n *Node) bool { + if n.Type != ElementNode { + return false + } + switch n.Namespace { + case "math": + if n.Data == "annotation-xml" { + for _, a := range n.Attr { + if a.Key == "encoding" { + val := strings.ToLower(a.Val) + if val == "text/html" || val == "application/xhtml+xml" { + return true + } + } + } + } + case "svg": + switch n.Data { + case "desc", "foreignObject", "title": + return true + } + } + return false +} + +func mathMLTextIntegrationPoint(n *Node) bool { + if n.Namespace != "math" { + return false + } + switch n.Data { + case "mi", "mo", "mn", "ms", "mtext": + return true + } + return false +} + +// Section 12.2.6.5. +var breakout = map[string]bool{ + "b": true, + "big": true, + "blockquote": true, + "body": true, + "br": true, + "center": true, + "code": true, + "dd": true, + "div": true, + "dl": true, + "dt": true, + "em": true, + "embed": true, + "h1": true, + "h2": true, + "h3": true, + "h4": true, + "h5": true, + "h6": true, + "head": true, + "hr": true, + "i": true, + "img": true, + "li": true, + "listing": true, + "menu": true, + "meta": true, + "nobr": true, + "ol": true, + "p": true, + "pre": true, + "ruby": true, + "s": true, + "small": true, + "span": true, + "strong": true, + "strike": true, + "sub": true, + "sup": true, + "table": true, + "tt": true, + "u": true, + "ul": true, + "var": true, +} + +// Section 12.2.6.5. +var svgTagNameAdjustments = map[string]string{ + "altglyph": "altGlyph", + "altglyphdef": "altGlyphDef", + "altglyphitem": "altGlyphItem", + "animatecolor": "animateColor", + "animatemotion": "animateMotion", + "animatetransform": "animateTransform", + "clippath": "clipPath", + "feblend": "feBlend", + "fecolormatrix": "feColorMatrix", + "fecomponenttransfer": "feComponentTransfer", + "fecomposite": "feComposite", + "feconvolvematrix": "feConvolveMatrix", + "fediffuselighting": "feDiffuseLighting", + "fedisplacementmap": "feDisplacementMap", + "fedistantlight": "feDistantLight", + "feflood": "feFlood", + "fefunca": "feFuncA", + "fefuncb": "feFuncB", + "fefuncg": "feFuncG", + "fefuncr": "feFuncR", + "fegaussianblur": "feGaussianBlur", + "feimage": "feImage", + "femerge": "feMerge", + "femergenode": "feMergeNode", + "femorphology": "feMorphology", + "feoffset": "feOffset", + "fepointlight": "fePointLight", + "fespecularlighting": "feSpecularLighting", + "fespotlight": "feSpotLight", + "fetile": "feTile", + "feturbulence": "feTurbulence", + "foreignobject": "foreignObject", + "glyphref": "glyphRef", + "lineargradient": "linearGradient", + "radialgradient": "radialGradient", + "textpath": "textPath", +} + +// Section 12.2.6.1 +var mathMLAttributeAdjustments = map[string]string{ + "definitionurl": "definitionURL", +} + +var svgAttributeAdjustments = map[string]string{ + "attributename": "attributeName", + "attributetype": "attributeType", + "basefrequency": "baseFrequency", + "baseprofile": "baseProfile", + "calcmode": "calcMode", + "clippathunits": "clipPathUnits", + "diffuseconstant": "diffuseConstant", + "edgemode": "edgeMode", + "filterunits": "filterUnits", + "glyphref": "glyphRef", + "gradienttransform": "gradientTransform", + "gradientunits": "gradientUnits", + "kernelmatrix": "kernelMatrix", + "kernelunitlength": "kernelUnitLength", + "keypoints": "keyPoints", + "keysplines": "keySplines", + "keytimes": "keyTimes", + "lengthadjust": "lengthAdjust", + "limitingconeangle": "limitingConeAngle", + "markerheight": "markerHeight", + "markerunits": "markerUnits", + "markerwidth": "markerWidth", + "maskcontentunits": "maskContentUnits", + "maskunits": "maskUnits", + "numoctaves": "numOctaves", + "pathlength": "pathLength", + "patterncontentunits": "patternContentUnits", + "patterntransform": "patternTransform", + "patternunits": "patternUnits", + "pointsatx": "pointsAtX", + "pointsaty": "pointsAtY", + "pointsatz": "pointsAtZ", + "preservealpha": "preserveAlpha", + "preserveaspectratio": "preserveAspectRatio", + "primitiveunits": "primitiveUnits", + "refx": "refX", + "refy": "refY", + "repeatcount": "repeatCount", + "repeatdur": "repeatDur", + "requiredextensions": "requiredExtensions", + "requiredfeatures": "requiredFeatures", + "specularconstant": "specularConstant", + "specularexponent": "specularExponent", + "spreadmethod": "spreadMethod", + "startoffset": "startOffset", + "stddeviation": "stdDeviation", + "stitchtiles": "stitchTiles", + "surfacescale": "surfaceScale", + "systemlanguage": "systemLanguage", + "tablevalues": "tableValues", + "targetx": "targetX", + "targety": "targetY", + "textlength": "textLength", + "viewbox": "viewBox", + "viewtarget": "viewTarget", + "xchannelselector": "xChannelSelector", + "ychannelselector": "yChannelSelector", + "zoomandpan": "zoomAndPan", +} diff --git a/vendor/golang.org/x/net/html/node.go b/vendor/golang.org/x/net/html/node.go new file mode 100644 index 00000000000..1350eef22c3 --- /dev/null +++ b/vendor/golang.org/x/net/html/node.go @@ -0,0 +1,225 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "golang.org/x/net/html/atom" +) + +// A NodeType is the type of a Node. +type NodeType uint32 + +const ( + ErrorNode NodeType = iota + TextNode + DocumentNode + ElementNode + CommentNode + DoctypeNode + // RawNode nodes are not returned by the parser, but can be part of the + // Node tree passed to func Render to insert raw HTML (without escaping). + // If so, this package makes no guarantee that the rendered HTML is secure + // (from e.g. Cross Site Scripting attacks) or well-formed. + RawNode + scopeMarkerNode +) + +// Section 12.2.4.3 says "The markers are inserted when entering applet, +// object, marquee, template, td, th, and caption elements, and are used +// to prevent formatting from "leaking" into applet, object, marquee, +// template, td, th, and caption elements". +var scopeMarker = Node{Type: scopeMarkerNode} + +// A Node consists of a NodeType and some Data (tag name for element nodes, +// content for text) and are part of a tree of Nodes. Element nodes may also +// have a Namespace and contain a slice of Attributes. Data is unescaped, so +// that it looks like "a 0 { + return (*s)[i-1] + } + return nil +} + +// index returns the index of the top-most occurrence of n in the stack, or -1 +// if n is not present. +func (s *nodeStack) index(n *Node) int { + for i := len(*s) - 1; i >= 0; i-- { + if (*s)[i] == n { + return i + } + } + return -1 +} + +// contains returns whether a is within s. +func (s *nodeStack) contains(a atom.Atom) bool { + for _, n := range *s { + if n.DataAtom == a && n.Namespace == "" { + return true + } + } + return false +} + +// insert inserts a node at the given index. +func (s *nodeStack) insert(i int, n *Node) { + (*s) = append(*s, nil) + copy((*s)[i+1:], (*s)[i:]) + (*s)[i] = n +} + +// remove removes a node from the stack. It is a no-op if n is not present. +func (s *nodeStack) remove(n *Node) { + i := s.index(n) + if i == -1 { + return + } + copy((*s)[i:], (*s)[i+1:]) + j := len(*s) - 1 + (*s)[j] = nil + *s = (*s)[:j] +} + +type insertionModeStack []insertionMode + +func (s *insertionModeStack) pop() (im insertionMode) { + i := len(*s) + im = (*s)[i-1] + *s = (*s)[:i-1] + return im +} + +func (s *insertionModeStack) top() insertionMode { + if i := len(*s); i > 0 { + return (*s)[i-1] + } + return nil +} diff --git a/vendor/golang.org/x/net/html/parse.go b/vendor/golang.org/x/net/html/parse.go new file mode 100644 index 00000000000..291c91908d7 --- /dev/null +++ b/vendor/golang.org/x/net/html/parse.go @@ -0,0 +1,2460 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package html + +import ( + "errors" + "fmt" + "io" + "strings" + + a "golang.org/x/net/html/atom" +) + +// A parser implements the HTML5 parsing algorithm: +// https://html.spec.whatwg.org/multipage/syntax.html#tree-construction +type parser struct { + // tokenizer provides the tokens for the parser. + tokenizer *Tokenizer + // tok is the most recently read token. + tok Token + // Self-closing tags like
are treated as start tags, except that + // hasSelfClosingToken is set while they are being processed. + hasSelfClosingToken bool + // doc is the document root element. + doc *Node + // The stack of open elements (section 12.2.4.2) and active formatting + // elements (section 12.2.4.3). + oe, afe nodeStack + // Element pointers (section 12.2.4.4). + head, form *Node + // Other parsing state flags (section 12.2.4.5). + scripting, framesetOK bool + // The stack of template insertion modes + templateStack insertionModeStack + // im is the current insertion mode. + im insertionMode + // originalIM is the insertion mode to go back to after completing a text + // or inTableText insertion mode. + originalIM insertionMode + // fosterParenting is whether new elements should be inserted according to + // the foster parenting rules (section 12.2.6.1). + fosterParenting bool + // quirks is whether the parser is operating in "quirks mode." + quirks bool + // fragment is whether the parser is parsing an HTML fragment. + fragment bool + // context is the context element when parsing an HTML fragment + // (section 12.4). + context *Node +} + +func (p *parser) top() *Node { + if n := p.oe.top(); n != nil { + return n + } + return p.doc +} + +// Stop tags for use in popUntil. These come from section 12.2.4.2. +var ( + defaultScopeStopTags = map[string][]a.Atom{ + "": {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object, a.Template}, + "math": {a.AnnotationXml, a.Mi, a.Mn, a.Mo, a.Ms, a.Mtext}, + "svg": {a.Desc, a.ForeignObject, a.Title}, + } +) + +type scope int + +const ( + defaultScope scope = iota + listItemScope + buttonScope + tableScope + tableRowScope + tableBodyScope + selectScope +) + +// popUntil pops the stack of open elements at the highest element whose tag +// is in matchTags, provided there is no higher element in the scope's stop +// tags (as defined in section 12.2.4.2). It returns whether or not there was +// such an element. If there was not, popUntil leaves the stack unchanged. +// +// For example, the set of stop tags for table scope is: "html", "table". If +// the stack was: +// ["html", "body", "font", "table", "b", "i", "u"] +// then popUntil(tableScope, "font") would return false, but +// popUntil(tableScope, "i") would return true and the stack would become: +// ["html", "body", "font", "table", "b"] +// +// If an element's tag is in both the stop tags and matchTags, then the stack +// will be popped and the function returns true (provided, of course, there was +// no higher element in the stack that was also in the stop tags). For example, +// popUntil(tableScope, "table") returns true and leaves: +// ["html", "body", "font"] +func (p *parser) popUntil(s scope, matchTags ...a.Atom) bool { + if i := p.indexOfElementInScope(s, matchTags...); i != -1 { + p.oe = p.oe[:i] + return true + } + return false +} + +// indexOfElementInScope returns the index in p.oe of the highest element whose +// tag is in matchTags that is in scope. If no matching element is in scope, it +// returns -1. +func (p *parser) indexOfElementInScope(s scope, matchTags ...a.Atom) int { + for i := len(p.oe) - 1; i >= 0; i-- { + tagAtom := p.oe[i].DataAtom + if p.oe[i].Namespace == "" { + for _, t := range matchTags { + if t == tagAtom { + return i + } + } + switch s { + case defaultScope: + // No-op. + case listItemScope: + if tagAtom == a.Ol || tagAtom == a.Ul { + return -1 + } + case buttonScope: + if tagAtom == a.Button { + return -1 + } + case tableScope: + if tagAtom == a.Html || tagAtom == a.Table || tagAtom == a.Template { + return -1 + } + case selectScope: + if tagAtom != a.Optgroup && tagAtom != a.Option { + return -1 + } + default: + panic("unreachable") + } + } + switch s { + case defaultScope, listItemScope, buttonScope: + for _, t := range defaultScopeStopTags[p.oe[i].Namespace] { + if t == tagAtom { + return -1 + } + } + } + } + return -1 +} + +// elementInScope is like popUntil, except that it doesn't modify the stack of +// open elements. +func (p *parser) elementInScope(s scope, matchTags ...a.Atom) bool { + return p.indexOfElementInScope(s, matchTags...) != -1 +} + +// clearStackToContext pops elements off the stack of open elements until a +// scope-defined element is found. +func (p *parser) clearStackToContext(s scope) { + for i := len(p.oe) - 1; i >= 0; i-- { + tagAtom := p.oe[i].DataAtom + switch s { + case tableScope: + if tagAtom == a.Html || tagAtom == a.Table || tagAtom == a.Template { + p.oe = p.oe[:i+1] + return + } + case tableRowScope: + if tagAtom == a.Html || tagAtom == a.Tr || tagAtom == a.Template { + p.oe = p.oe[:i+1] + return + } + case tableBodyScope: + if tagAtom == a.Html || tagAtom == a.Tbody || tagAtom == a.Tfoot || tagAtom == a.Thead || tagAtom == a.Template { + p.oe = p.oe[:i+1] + return + } + default: + panic("unreachable") + } + } +} + +// parseGenericRawTextElements implements the generic raw text element parsing +// algorithm defined in 12.2.6.2. +// https://html.spec.whatwg.org/multipage/parsing.html#parsing-elements-that-contain-only-text +// TODO: Since both RAWTEXT and RCDATA states are treated as tokenizer's part +// officially, need to make tokenizer consider both states. +func (p *parser) parseGenericRawTextElement() { + p.addElement() + p.originalIM = p.im + p.im = textIM +} + +// generateImpliedEndTags pops nodes off the stack of open elements as long as +// the top node has a tag name of dd, dt, li, optgroup, option, p, rb, rp, rt or rtc. +// If exceptions are specified, nodes with that name will not be popped off. +func (p *parser) generateImpliedEndTags(exceptions ...string) { + var i int +loop: + for i = len(p.oe) - 1; i >= 0; i-- { + n := p.oe[i] + if n.Type != ElementNode { + break + } + switch n.DataAtom { + case a.Dd, a.Dt, a.Li, a.Optgroup, a.Option, a.P, a.Rb, a.Rp, a.Rt, a.Rtc: + for _, except := range exceptions { + if n.Data == except { + break loop + } + } + continue + } + break + } + + p.oe = p.oe[:i+1] +} + +// addChild adds a child node n to the top element, and pushes n onto the stack +// of open elements if it is an element node. +func (p *parser) addChild(n *Node) { + if p.shouldFosterParent() { + p.fosterParent(n) + } else { + p.top().AppendChild(n) + } + + if n.Type == ElementNode { + p.oe = append(p.oe, n) + } +} + +// shouldFosterParent returns whether the next node to be added should be +// foster parented. +func (p *parser) shouldFosterParent() bool { + if p.fosterParenting { + switch p.top().DataAtom { + case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: + return true + } + } + return false +} + +// fosterParent adds a child node according to the foster parenting rules. +// Section 12.2.6.1, "foster parenting". +func (p *parser) fosterParent(n *Node) { + var table, parent, prev, template *Node + var i int + for i = len(p.oe) - 1; i >= 0; i-- { + if p.oe[i].DataAtom == a.Table { + table = p.oe[i] + break + } + } + + var j int + for j = len(p.oe) - 1; j >= 0; j-- { + if p.oe[j].DataAtom == a.Template { + template = p.oe[j] + break + } + } + + if template != nil && (table == nil || j > i) { + template.AppendChild(n) + return + } + + if table == nil { + // The foster parent is the html element. + parent = p.oe[0] + } else { + parent = table.Parent + } + if parent == nil { + parent = p.oe[i-1] + } + + if table != nil { + prev = table.PrevSibling + } else { + prev = parent.LastChild + } + if prev != nil && prev.Type == TextNode && n.Type == TextNode { + prev.Data += n.Data + return + } + + parent.InsertBefore(n, table) +} + +// addText adds text to the preceding node if it is a text node, or else it +// calls addChild with a new text node. +func (p *parser) addText(text string) { + if text == "" { + return + } + + if p.shouldFosterParent() { + p.fosterParent(&Node{ + Type: TextNode, + Data: text, + }) + return + } + + t := p.top() + if n := t.LastChild; n != nil && n.Type == TextNode { + n.Data += text + return + } + p.addChild(&Node{ + Type: TextNode, + Data: text, + }) +} + +// addElement adds a child element based on the current token. +func (p *parser) addElement() { + p.addChild(&Node{ + Type: ElementNode, + DataAtom: p.tok.DataAtom, + Data: p.tok.Data, + Attr: p.tok.Attr, + }) +} + +// Section 12.2.4.3. +func (p *parser) addFormattingElement() { + tagAtom, attr := p.tok.DataAtom, p.tok.Attr + p.addElement() + + // Implement the Noah's Ark clause, but with three per family instead of two. + identicalElements := 0 +findIdenticalElements: + for i := len(p.afe) - 1; i >= 0; i-- { + n := p.afe[i] + if n.Type == scopeMarkerNode { + break + } + if n.Type != ElementNode { + continue + } + if n.Namespace != "" { + continue + } + if n.DataAtom != tagAtom { + continue + } + if len(n.Attr) != len(attr) { + continue + } + compareAttributes: + for _, t0 := range n.Attr { + for _, t1 := range attr { + if t0.Key == t1.Key && t0.Namespace == t1.Namespace && t0.Val == t1.Val { + // Found a match for this attribute, continue with the next attribute. + continue compareAttributes + } + } + // If we get here, there is no attribute that matches a. + // Therefore the element is not identical to the new one. + continue findIdenticalElements + } + + identicalElements++ + if identicalElements >= 3 { + p.afe.remove(n) + } + } + + p.afe = append(p.afe, p.top()) +} + +// Section 12.2.4.3. +func (p *parser) clearActiveFormattingElements() { + for { + if n := p.afe.pop(); len(p.afe) == 0 || n.Type == scopeMarkerNode { + return + } + } +} + +// Section 12.2.4.3. +func (p *parser) reconstructActiveFormattingElements() { + n := p.afe.top() + if n == nil { + return + } + if n.Type == scopeMarkerNode || p.oe.index(n) != -1 { + return + } + i := len(p.afe) - 1 + for n.Type != scopeMarkerNode && p.oe.index(n) == -1 { + if i == 0 { + i = -1 + break + } + i-- + n = p.afe[i] + } + for { + i++ + clone := p.afe[i].clone() + p.addChild(clone) + p.afe[i] = clone + if i == len(p.afe)-1 { + break + } + } +} + +// Section 12.2.5. +func (p *parser) acknowledgeSelfClosingTag() { + p.hasSelfClosingToken = false +} + +// An insertion mode (section 12.2.4.1) is the state transition function from +// a particular state in the HTML5 parser's state machine. It updates the +// parser's fields depending on parser.tok (where ErrorToken means EOF). +// It returns whether the token was consumed. +type insertionMode func(*parser) bool + +// setOriginalIM sets the insertion mode to return to after completing a text or +// inTableText insertion mode. +// Section 12.2.4.1, "using the rules for". +func (p *parser) setOriginalIM() { + if p.originalIM != nil { + panic("html: bad parser state: originalIM was set twice") + } + p.originalIM = p.im +} + +// Section 12.2.4.1, "reset the insertion mode". +func (p *parser) resetInsertionMode() { + for i := len(p.oe) - 1; i >= 0; i-- { + n := p.oe[i] + last := i == 0 + if last && p.context != nil { + n = p.context + } + + switch n.DataAtom { + case a.Select: + if !last { + for ancestor, first := n, p.oe[0]; ancestor != first; { + ancestor = p.oe[p.oe.index(ancestor)-1] + switch ancestor.DataAtom { + case a.Template: + p.im = inSelectIM + return + case a.Table: + p.im = inSelectInTableIM + return + } + } + } + p.im = inSelectIM + case a.Td, a.Th: + // TODO: remove this divergence from the HTML5 spec. + // + // See https://bugs.chromium.org/p/chromium/issues/detail?id=829668 + p.im = inCellIM + case a.Tr: + p.im = inRowIM + case a.Tbody, a.Thead, a.Tfoot: + p.im = inTableBodyIM + case a.Caption: + p.im = inCaptionIM + case a.Colgroup: + p.im = inColumnGroupIM + case a.Table: + p.im = inTableIM + case a.Template: + // TODO: remove this divergence from the HTML5 spec. + if n.Namespace != "" { + continue + } + p.im = p.templateStack.top() + case a.Head: + // TODO: remove this divergence from the HTML5 spec. + // + // See https://bugs.chromium.org/p/chromium/issues/detail?id=829668 + p.im = inHeadIM + case a.Body: + p.im = inBodyIM + case a.Frameset: + p.im = inFramesetIM + case a.Html: + if p.head == nil { + p.im = beforeHeadIM + } else { + p.im = afterHeadIM + } + default: + if last { + p.im = inBodyIM + return + } + continue + } + return + } +} + +const whitespace = " \t\r\n\f" + +// Section 12.2.6.4.1. +func initialIM(p *parser) bool { + switch p.tok.Type { + case TextToken: + p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) + if len(p.tok.Data) == 0 { + // It was all whitespace, so ignore it. + return true + } + case CommentToken: + p.doc.AppendChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + return true + case DoctypeToken: + n, quirks := parseDoctype(p.tok.Data) + p.doc.AppendChild(n) + p.quirks = quirks + p.im = beforeHTMLIM + return true + } + p.quirks = true + p.im = beforeHTMLIM + return false +} + +// Section 12.2.6.4.2. +func beforeHTMLIM(p *parser) bool { + switch p.tok.Type { + case DoctypeToken: + // Ignore the token. + return true + case TextToken: + p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) + if len(p.tok.Data) == 0 { + // It was all whitespace, so ignore it. + return true + } + case StartTagToken: + if p.tok.DataAtom == a.Html { + p.addElement() + p.im = beforeHeadIM + return true + } + case EndTagToken: + switch p.tok.DataAtom { + case a.Head, a.Body, a.Html, a.Br: + p.parseImpliedToken(StartTagToken, a.Html, a.Html.String()) + return false + default: + // Ignore the token. + return true + } + case CommentToken: + p.doc.AppendChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + return true + } + p.parseImpliedToken(StartTagToken, a.Html, a.Html.String()) + return false +} + +// Section 12.2.6.4.3. +func beforeHeadIM(p *parser) bool { + switch p.tok.Type { + case TextToken: + p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace) + if len(p.tok.Data) == 0 { + // It was all whitespace, so ignore it. + return true + } + case StartTagToken: + switch p.tok.DataAtom { + case a.Head: + p.addElement() + p.head = p.top() + p.im = inHeadIM + return true + case a.Html: + return inBodyIM(p) + } + case EndTagToken: + switch p.tok.DataAtom { + case a.Head, a.Body, a.Html, a.Br: + p.parseImpliedToken(StartTagToken, a.Head, a.Head.String()) + return false + default: + // Ignore the token. + return true + } + case CommentToken: + p.addChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + return true + case DoctypeToken: + // Ignore the token. + return true + } + + p.parseImpliedToken(StartTagToken, a.Head, a.Head.String()) + return false +} + +// Section 12.2.6.4.4. +func inHeadIM(p *parser) bool { + switch p.tok.Type { + case TextToken: + s := strings.TrimLeft(p.tok.Data, whitespace) + if len(s) < len(p.tok.Data) { + // Add the initial whitespace to the current node. + p.addText(p.tok.Data[:len(p.tok.Data)-len(s)]) + if s == "" { + return true + } + p.tok.Data = s + } + case StartTagToken: + switch p.tok.DataAtom { + case a.Html: + return inBodyIM(p) + case a.Base, a.Basefont, a.Bgsound, a.Link, a.Meta: + p.addElement() + p.oe.pop() + p.acknowledgeSelfClosingTag() + return true + case a.Noscript: + if p.scripting { + p.parseGenericRawTextElement() + return true + } + p.addElement() + p.im = inHeadNoscriptIM + // Don't let the tokenizer go into raw text mode when scripting is disabled. + p.tokenizer.NextIsNotRawText() + return true + case a.Script, a.Title: + p.addElement() + p.setOriginalIM() + p.im = textIM + return true + case a.Noframes, a.Style: + p.parseGenericRawTextElement() + return true + case a.Head: + // Ignore the token. + return true + case a.Template: + // TODO: remove this divergence from the HTML5 spec. + // + // We don't handle all of the corner cases when mixing foreign + // content (i.e. or ) with tag. + case a.Template: + return inHeadIM(p) + default: + // Ignore the token. + return true + } + case CommentToken: + p.addChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + return true + case DoctypeToken: + // Ignore the token. + return true + } + + p.parseImpliedToken(StartTagToken, a.Body, a.Body.String()) + p.framesetOK = true + return false +} + +// copyAttributes copies attributes of src not found on dst to dst. +func copyAttributes(dst *Node, src Token) { + if len(src.Attr) == 0 { + return + } + attr := map[string]string{} + for _, t := range dst.Attr { + attr[t.Key] = t.Val + } + for _, t := range src.Attr { + if _, ok := attr[t.Key]; !ok { + dst.Attr = append(dst.Attr, t) + attr[t.Key] = t.Val + } + } +} + +// Section 12.2.6.4.7. +func inBodyIM(p *parser) bool { + switch p.tok.Type { + case TextToken: + d := p.tok.Data + switch n := p.oe.top(); n.DataAtom { + case a.Pre, a.Listing: + if n.FirstChild == nil { + // Ignore a newline at the start of a
 block.
+				if d != "" && d[0] == '\r' {
+					d = d[1:]
+				}
+				if d != "" && d[0] == '\n' {
+					d = d[1:]
+				}
+			}
+		}
+		d = strings.Replace(d, "\x00", "", -1)
+		if d == "" {
+			return true
+		}
+		p.reconstructActiveFormattingElements()
+		p.addText(d)
+		if p.framesetOK && strings.TrimLeft(d, whitespace) != "" {
+			// There were non-whitespace characters inserted.
+			p.framesetOK = false
+		}
+	case StartTagToken:
+		switch p.tok.DataAtom {
+		case a.Html:
+			if p.oe.contains(a.Template) {
+				return true
+			}
+			copyAttributes(p.oe[0], p.tok)
+		case a.Base, a.Basefont, a.Bgsound, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Template, a.Title:
+			return inHeadIM(p)
+		case a.Body:
+			if p.oe.contains(a.Template) {
+				return true
+			}
+			if len(p.oe) >= 2 {
+				body := p.oe[1]
+				if body.Type == ElementNode && body.DataAtom == a.Body {
+					p.framesetOK = false
+					copyAttributes(body, p.tok)
+				}
+			}
+		case a.Frameset:
+			if !p.framesetOK || len(p.oe) < 2 || p.oe[1].DataAtom != a.Body {
+				// Ignore the token.
+				return true
+			}
+			body := p.oe[1]
+			if body.Parent != nil {
+				body.Parent.RemoveChild(body)
+			}
+			p.oe = p.oe[:1]
+			p.addElement()
+			p.im = inFramesetIM
+			return true
+		case a.Address, a.Article, a.Aside, a.Blockquote, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Main, a.Menu, a.Nav, a.Ol, a.P, a.Section, a.Summary, a.Ul:
+			p.popUntil(buttonScope, a.P)
+			p.addElement()
+		case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
+			p.popUntil(buttonScope, a.P)
+			switch n := p.top(); n.DataAtom {
+			case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
+				p.oe.pop()
+			}
+			p.addElement()
+		case a.Pre, a.Listing:
+			p.popUntil(buttonScope, a.P)
+			p.addElement()
+			// The newline, if any, will be dealt with by the TextToken case.
+			p.framesetOK = false
+		case a.Form:
+			if p.form != nil && !p.oe.contains(a.Template) {
+				// Ignore the token
+				return true
+			}
+			p.popUntil(buttonScope, a.P)
+			p.addElement()
+			if !p.oe.contains(a.Template) {
+				p.form = p.top()
+			}
+		case a.Li:
+			p.framesetOK = false
+			for i := len(p.oe) - 1; i >= 0; i-- {
+				node := p.oe[i]
+				switch node.DataAtom {
+				case a.Li:
+					p.oe = p.oe[:i]
+				case a.Address, a.Div, a.P:
+					continue
+				default:
+					if !isSpecialElement(node) {
+						continue
+					}
+				}
+				break
+			}
+			p.popUntil(buttonScope, a.P)
+			p.addElement()
+		case a.Dd, a.Dt:
+			p.framesetOK = false
+			for i := len(p.oe) - 1; i >= 0; i-- {
+				node := p.oe[i]
+				switch node.DataAtom {
+				case a.Dd, a.Dt:
+					p.oe = p.oe[:i]
+				case a.Address, a.Div, a.P:
+					continue
+				default:
+					if !isSpecialElement(node) {
+						continue
+					}
+				}
+				break
+			}
+			p.popUntil(buttonScope, a.P)
+			p.addElement()
+		case a.Plaintext:
+			p.popUntil(buttonScope, a.P)
+			p.addElement()
+		case a.Button:
+			p.popUntil(defaultScope, a.Button)
+			p.reconstructActiveFormattingElements()
+			p.addElement()
+			p.framesetOK = false
+		case a.A:
+			for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- {
+				if n := p.afe[i]; n.Type == ElementNode && n.DataAtom == a.A {
+					p.inBodyEndTagFormatting(a.A, "a")
+					p.oe.remove(n)
+					p.afe.remove(n)
+					break
+				}
+			}
+			p.reconstructActiveFormattingElements()
+			p.addFormattingElement()
+		case a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
+			p.reconstructActiveFormattingElements()
+			p.addFormattingElement()
+		case a.Nobr:
+			p.reconstructActiveFormattingElements()
+			if p.elementInScope(defaultScope, a.Nobr) {
+				p.inBodyEndTagFormatting(a.Nobr, "nobr")
+				p.reconstructActiveFormattingElements()
+			}
+			p.addFormattingElement()
+		case a.Applet, a.Marquee, a.Object:
+			p.reconstructActiveFormattingElements()
+			p.addElement()
+			p.afe = append(p.afe, &scopeMarker)
+			p.framesetOK = false
+		case a.Table:
+			if !p.quirks {
+				p.popUntil(buttonScope, a.P)
+			}
+			p.addElement()
+			p.framesetOK = false
+			p.im = inTableIM
+			return true
+		case a.Area, a.Br, a.Embed, a.Img, a.Input, a.Keygen, a.Wbr:
+			p.reconstructActiveFormattingElements()
+			p.addElement()
+			p.oe.pop()
+			p.acknowledgeSelfClosingTag()
+			if p.tok.DataAtom == a.Input {
+				for _, t := range p.tok.Attr {
+					if t.Key == "type" {
+						if strings.ToLower(t.Val) == "hidden" {
+							// Skip setting framesetOK = false
+							return true
+						}
+					}
+				}
+			}
+			p.framesetOK = false
+		case a.Param, a.Source, a.Track:
+			p.addElement()
+			p.oe.pop()
+			p.acknowledgeSelfClosingTag()
+		case a.Hr:
+			p.popUntil(buttonScope, a.P)
+			p.addElement()
+			p.oe.pop()
+			p.acknowledgeSelfClosingTag()
+			p.framesetOK = false
+		case a.Image:
+			p.tok.DataAtom = a.Img
+			p.tok.Data = a.Img.String()
+			return false
+		case a.Textarea:
+			p.addElement()
+			p.setOriginalIM()
+			p.framesetOK = false
+			p.im = textIM
+		case a.Xmp:
+			p.popUntil(buttonScope, a.P)
+			p.reconstructActiveFormattingElements()
+			p.framesetOK = false
+			p.parseGenericRawTextElement()
+		case a.Iframe:
+			p.framesetOK = false
+			p.parseGenericRawTextElement()
+		case a.Noembed:
+			p.parseGenericRawTextElement()
+		case a.Noscript:
+			if p.scripting {
+				p.parseGenericRawTextElement()
+				return true
+			}
+			p.reconstructActiveFormattingElements()
+			p.addElement()
+			// Don't let the tokenizer go into raw text mode when scripting is disabled.
+			p.tokenizer.NextIsNotRawText()
+		case a.Select:
+			p.reconstructActiveFormattingElements()
+			p.addElement()
+			p.framesetOK = false
+			p.im = inSelectIM
+			return true
+		case a.Optgroup, a.Option:
+			if p.top().DataAtom == a.Option {
+				p.oe.pop()
+			}
+			p.reconstructActiveFormattingElements()
+			p.addElement()
+		case a.Rb, a.Rtc:
+			if p.elementInScope(defaultScope, a.Ruby) {
+				p.generateImpliedEndTags()
+			}
+			p.addElement()
+		case a.Rp, a.Rt:
+			if p.elementInScope(defaultScope, a.Ruby) {
+				p.generateImpliedEndTags("rtc")
+			}
+			p.addElement()
+		case a.Math, a.Svg:
+			p.reconstructActiveFormattingElements()
+			if p.tok.DataAtom == a.Math {
+				adjustAttributeNames(p.tok.Attr, mathMLAttributeAdjustments)
+			} else {
+				adjustAttributeNames(p.tok.Attr, svgAttributeAdjustments)
+			}
+			adjustForeignAttributes(p.tok.Attr)
+			p.addElement()
+			p.top().Namespace = p.tok.Data
+			if p.hasSelfClosingToken {
+				p.oe.pop()
+				p.acknowledgeSelfClosingTag()
+			}
+			return true
+		case a.Caption, a.Col, a.Colgroup, a.Frame, a.Head, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr:
+			// Ignore the token.
+		default:
+			p.reconstructActiveFormattingElements()
+			p.addElement()
+		}
+	case EndTagToken:
+		switch p.tok.DataAtom {
+		case a.Body:
+			if p.elementInScope(defaultScope, a.Body) {
+				p.im = afterBodyIM
+			}
+		case a.Html:
+			if p.elementInScope(defaultScope, a.Body) {
+				p.parseImpliedToken(EndTagToken, a.Body, a.Body.String())
+				return false
+			}
+			return true
+		case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Main, a.Menu, a.Nav, a.Ol, a.Pre, a.Section, a.Summary, a.Ul:
+			p.popUntil(defaultScope, p.tok.DataAtom)
+		case a.Form:
+			if p.oe.contains(a.Template) {
+				i := p.indexOfElementInScope(defaultScope, a.Form)
+				if i == -1 {
+					// Ignore the token.
+					return true
+				}
+				p.generateImpliedEndTags()
+				if p.oe[i].DataAtom != a.Form {
+					// Ignore the token.
+					return true
+				}
+				p.popUntil(defaultScope, a.Form)
+			} else {
+				node := p.form
+				p.form = nil
+				i := p.indexOfElementInScope(defaultScope, a.Form)
+				if node == nil || i == -1 || p.oe[i] != node {
+					// Ignore the token.
+					return true
+				}
+				p.generateImpliedEndTags()
+				p.oe.remove(node)
+			}
+		case a.P:
+			if !p.elementInScope(buttonScope, a.P) {
+				p.parseImpliedToken(StartTagToken, a.P, a.P.String())
+			}
+			p.popUntil(buttonScope, a.P)
+		case a.Li:
+			p.popUntil(listItemScope, a.Li)
+		case a.Dd, a.Dt:
+			p.popUntil(defaultScope, p.tok.DataAtom)
+		case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
+			p.popUntil(defaultScope, a.H1, a.H2, a.H3, a.H4, a.H5, a.H6)
+		case a.A, a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.Nobr, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
+			p.inBodyEndTagFormatting(p.tok.DataAtom, p.tok.Data)
+		case a.Applet, a.Marquee, a.Object:
+			if p.popUntil(defaultScope, p.tok.DataAtom) {
+				p.clearActiveFormattingElements()
+			}
+		case a.Br:
+			p.tok.Type = StartTagToken
+			return false
+		case a.Template:
+			return inHeadIM(p)
+		default:
+			p.inBodyEndTagOther(p.tok.DataAtom, p.tok.Data)
+		}
+	case CommentToken:
+		p.addChild(&Node{
+			Type: CommentNode,
+			Data: p.tok.Data,
+		})
+	case ErrorToken:
+		// TODO: remove this divergence from the HTML5 spec.
+		if len(p.templateStack) > 0 {
+			p.im = inTemplateIM
+			return false
+		}
+		for _, e := range p.oe {
+			switch e.DataAtom {
+			case a.Dd, a.Dt, a.Li, a.Optgroup, a.Option, a.P, a.Rb, a.Rp, a.Rt, a.Rtc, a.Tbody, a.Td, a.Tfoot, a.Th,
+				a.Thead, a.Tr, a.Body, a.Html:
+			default:
+				return true
+			}
+		}
+	}
+
+	return true
+}
+
+func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom, tagName string) {
+	// This is the "adoption agency" algorithm, described at
+	// https://html.spec.whatwg.org/multipage/syntax.html#adoptionAgency
+
+	// TODO: this is a fairly literal line-by-line translation of that algorithm.
+	// Once the code successfully parses the comprehensive test suite, we should
+	// refactor this code to be more idiomatic.
+
+	// Steps 1-2
+	if current := p.oe.top(); current.Data == tagName && p.afe.index(current) == -1 {
+		p.oe.pop()
+		return
+	}
+
+	// Steps 3-5. The outer loop.
+	for i := 0; i < 8; i++ {
+		// Step 6. Find the formatting element.
+		var formattingElement *Node
+		for j := len(p.afe) - 1; j >= 0; j-- {
+			if p.afe[j].Type == scopeMarkerNode {
+				break
+			}
+			if p.afe[j].DataAtom == tagAtom {
+				formattingElement = p.afe[j]
+				break
+			}
+		}
+		if formattingElement == nil {
+			p.inBodyEndTagOther(tagAtom, tagName)
+			return
+		}
+
+		// Step 7. Ignore the tag if formatting element is not in the stack of open elements.
+		feIndex := p.oe.index(formattingElement)
+		if feIndex == -1 {
+			p.afe.remove(formattingElement)
+			return
+		}
+		// Step 8. Ignore the tag if formatting element is not in the scope.
+		if !p.elementInScope(defaultScope, tagAtom) {
+			// Ignore the tag.
+			return
+		}
+
+		// Step 9. This step is omitted because it's just a parse error but no need to return.
+
+		// Steps 10-11. Find the furthest block.
+		var furthestBlock *Node
+		for _, e := range p.oe[feIndex:] {
+			if isSpecialElement(e) {
+				furthestBlock = e
+				break
+			}
+		}
+		if furthestBlock == nil {
+			e := p.oe.pop()
+			for e != formattingElement {
+				e = p.oe.pop()
+			}
+			p.afe.remove(e)
+			return
+		}
+
+		// Steps 12-13. Find the common ancestor and bookmark node.
+		commonAncestor := p.oe[feIndex-1]
+		bookmark := p.afe.index(formattingElement)
+
+		// Step 14. The inner loop. Find the lastNode to reparent.
+		lastNode := furthestBlock
+		node := furthestBlock
+		x := p.oe.index(node)
+		// Step 14.1.
+		j := 0
+		for {
+			// Step 14.2.
+			j++
+			// Step. 14.3.
+			x--
+			node = p.oe[x]
+			// Step 14.4. Go to the next step if node is formatting element.
+			if node == formattingElement {
+				break
+			}
+			// Step 14.5. Remove node from the list of active formatting elements if
+			// inner loop counter is greater than three and node is in the list of
+			// active formatting elements.
+			if ni := p.afe.index(node); j > 3 && ni > -1 {
+				p.afe.remove(node)
+				// If any element of the list of active formatting elements is removed,
+				// we need to take care whether bookmark should be decremented or not.
+				// This is because the value of bookmark may exceed the size of the
+				// list by removing elements from the list.
+				if ni <= bookmark {
+					bookmark--
+				}
+				continue
+			}
+			// Step 14.6. Continue the next inner loop if node is not in the list of
+			// active formatting elements.
+			if p.afe.index(node) == -1 {
+				p.oe.remove(node)
+				continue
+			}
+			// Step 14.7.
+			clone := node.clone()
+			p.afe[p.afe.index(node)] = clone
+			p.oe[p.oe.index(node)] = clone
+			node = clone
+			// Step 14.8.
+			if lastNode == furthestBlock {
+				bookmark = p.afe.index(node) + 1
+			}
+			// Step 14.9.
+			if lastNode.Parent != nil {
+				lastNode.Parent.RemoveChild(lastNode)
+			}
+			node.AppendChild(lastNode)
+			// Step 14.10.
+			lastNode = node
+		}
+
+		// Step 15. Reparent lastNode to the common ancestor,
+		// or for misnested table nodes, to the foster parent.
+		if lastNode.Parent != nil {
+			lastNode.Parent.RemoveChild(lastNode)
+		}
+		switch commonAncestor.DataAtom {
+		case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
+			p.fosterParent(lastNode)
+		default:
+			commonAncestor.AppendChild(lastNode)
+		}
+
+		// Steps 16-18. Reparent nodes from the furthest block's children
+		// to a clone of the formatting element.
+		clone := formattingElement.clone()
+		reparentChildren(clone, furthestBlock)
+		furthestBlock.AppendChild(clone)
+
+		// Step 19. Fix up the list of active formatting elements.
+		if oldLoc := p.afe.index(formattingElement); oldLoc != -1 && oldLoc < bookmark {
+			// Move the bookmark with the rest of the list.
+			bookmark--
+		}
+		p.afe.remove(formattingElement)
+		p.afe.insert(bookmark, clone)
+
+		// Step 20. Fix up the stack of open elements.
+		p.oe.remove(formattingElement)
+		p.oe.insert(p.oe.index(furthestBlock)+1, clone)
+	}
+}
+
+// inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM.
+// "Any other end tag" handling from 12.2.6.5 The rules for parsing tokens in foreign content
+// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inforeign
+func (p *parser) inBodyEndTagOther(tagAtom a.Atom, tagName string) {
+	for i := len(p.oe) - 1; i >= 0; i-- {
+		// Two element nodes have the same tag if they have the same Data (a
+		// string-typed field). As an optimization, for common HTML tags, each
+		// Data string is assigned a unique, non-zero DataAtom (a uint32-typed
+		// field), since integer comparison is faster than string comparison.
+		// Uncommon (custom) tags get a zero DataAtom.
+		//
+		// The if condition here is equivalent to (p.oe[i].Data == tagName).
+		if (p.oe[i].DataAtom == tagAtom) &&
+			((tagAtom != 0) || (p.oe[i].Data == tagName)) {
+			p.oe = p.oe[:i]
+			break
+		}
+		if isSpecialElement(p.oe[i]) {
+			break
+		}
+	}
+}
+
+// Section 12.2.6.4.8.
+func textIM(p *parser) bool {
+	switch p.tok.Type {
+	case ErrorToken:
+		p.oe.pop()
+	case TextToken:
+		d := p.tok.Data
+		if n := p.oe.top(); n.DataAtom == a.Textarea && n.FirstChild == nil {
+			// Ignore a newline at the start of a