2022-09-27 03:22:29 -04:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
package terraform
import (
"fmt"
"strings"
2023-06-19 07:02:01 -04:00
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclwrite"
2022-09-27 03:22:29 -04:00
)
// Variables is a struct that holds all variables that are passed to Terraform.
type Variables interface {
fmt . Stringer
}
2023-07-21 04:04:29 -04:00
// ClusterVariables should be used in places where a cluster is created.
type ClusterVariables interface {
Variables
// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config.
// GetCreateMAA does not follow Go's naming convention because we need to keep the CreateMAA property public for now.
// There are functions creating Variables objects outside of this package.
// These functions can only be moved into this package once we have introduced an interface for config.Config,
// since we do not want to introduce a dependency on config.Config in this package.
GetCreateMAA ( ) bool
}
2023-06-09 09:41:02 -04:00
// AWSClusterVariables is user configuration for creating a cluster with Terraform on AWS.
2022-12-07 05:48:54 -05:00
type AWSClusterVariables struct {
2023-06-23 11:19:43 -04:00
// Name of the cluster.
Name string ` hcl:"name" cty:"name" `
2022-10-21 06:24:18 -04:00
// Region is the AWS region to use.
2023-06-23 11:19:43 -04:00
Region string ` hcl:"region" cty:"region" `
2022-10-21 06:24:18 -04:00
// Zone is the AWS zone to use in the given region.
2023-06-23 11:19:43 -04:00
Zone string ` hcl:"zone" cty:"zone" `
2022-10-21 06:24:18 -04:00
// AMIImageID is the ID of the AMI image to use.
2023-06-23 11:19:43 -04:00
AMIImageID string ` hcl:"ami" cty:"ami" `
2022-10-21 06:24:18 -04:00
// IAMGroupControlPlane is the IAM group to use for the control-plane nodes.
2023-06-23 11:19:43 -04:00
IAMProfileControlPlane string ` hcl:"iam_instance_profile_control_plane" cty:"iam_instance_profile_control_plane" `
2022-10-21 06:24:18 -04:00
// IAMGroupWorkerNodes is the IAM group to use for the worker nodes.
2023-06-23 11:19:43 -04:00
IAMProfileWorkerNodes string ` hcl:"iam_instance_profile_worker_nodes" cty:"iam_instance_profile_worker_nodes" `
2022-10-21 06:24:18 -04:00
// Debug is true if debug mode is enabled.
2023-06-23 11:19:43 -04:00
Debug bool ` hcl:"debug" cty:"debug" `
2023-06-09 09:41:02 -04:00
// EnableSNP controls enablement of the EC2 cpu-option "AmdSevSnp".
2023-06-23 11:19:43 -04:00
EnableSNP bool ` hcl:"enable_snp" cty:"enable_snp" `
// NodeGroups is a map of node groups to create.
NodeGroups map [ string ] AWSNodeGroup ` hcl:"node_groups" cty:"node_groups" `
2023-07-21 10:43:51 -04:00
// CustomEndpoint is the (optional) custom dns hostname for the kubernetes api server.
CustomEndpoint string ` hcl:"custom_endpoint" cty:"custom_endpoint" `
2022-10-21 06:24:18 -04:00
}
2023-07-21 04:04:29 -04:00
// GetCreateMAA gets the CreateMAA variable.
// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config.
func ( a * AWSClusterVariables ) GetCreateMAA ( ) bool {
return false
}
// String returns a string representation of the variables, formatted as Terraform variables.
func ( a * AWSClusterVariables ) String ( ) string {
f := hclwrite . NewEmptyFile ( )
gohcl . EncodeIntoBody ( a , f . Body ( ) )
return string ( f . Bytes ( ) )
}
2023-06-23 11:19:43 -04:00
// AWSNodeGroup is a node group to create on AWS.
type AWSNodeGroup struct {
// Role is the role of the node group.
Role string ` hcl:"role" cty:"role" `
// StateDiskSizeGB is the size of the state disk to allocate to each node, in GB.
StateDiskSizeGB int ` hcl:"disk_size" cty:"disk_size" `
// InitialCount is the initial number of nodes to create in the node group.
2023-07-21 04:04:29 -04:00
// During upgrades this value is not set.
InitialCount * int ` hcl:"initial_count" cty:"initial_count" `
2023-06-23 11:19:43 -04:00
// Zone is the AWS availability-zone to use in the given region.
Zone string ` hcl:"zone" cty:"zone" `
// InstanceType is the type of the EC2 instance to use.
InstanceType string ` hcl:"instance_type" cty:"instance_type" `
// DiskType is the EBS disk type to use for the state disk.
DiskType string ` hcl:"disk_type" cty:"disk_type" `
}
2022-12-07 05:48:54 -05:00
// AWSIAMVariables is user configuration for creating the IAM configuration with Terraform on Microsoft Azure.
type AWSIAMVariables struct {
// Region is the AWS location to use. (e.g. us-east-2)
Region string
// Prefix is the name prefix of the resources to use.
Prefix string
}
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.
func ( v * AWSIAMVariables ) String ( ) string {
b := & strings . Builder { }
writeLinef ( b , "name_prefix = %q" , v . Prefix )
writeLinef ( b , "region = %q" , v . Region )
return b . String ( )
}
// GCPClusterVariables is user configuration for creating resources with Terraform on GCP.
type GCPClusterVariables struct {
2023-06-19 07:02:01 -04:00
// Name of the cluster.
Name string ` hcl:"name" cty:"name" `
2022-09-27 03:22:29 -04:00
// Project is the ID of the GCP project to use.
2023-06-19 07:02:01 -04:00
Project string ` hcl:"project" cty:"project" `
2022-09-27 03:22:29 -04:00
// Region is the GCP region to use.
2023-06-19 07:02:01 -04:00
Region string ` hcl:"region" cty:"region" `
2022-09-27 03:22:29 -04:00
// Zone is the GCP zone to use.
2023-06-19 07:02:01 -04:00
Zone string ` hcl:"zone" cty:"zone" `
2022-09-27 03:22:29 -04:00
// ImageID is the ID of the GCP image to use.
2023-06-19 07:02:01 -04:00
ImageID string ` hcl:"image_id" cty:"image_id" `
2022-09-27 03:22:29 -04:00
// Debug is true if debug mode is enabled.
2023-06-19 07:02:01 -04:00
Debug bool ` hcl:"debug" cty:"debug" `
// NodeGroups is a map of node groups to create.
NodeGroups map [ string ] GCPNodeGroup ` hcl:"node_groups" cty:"node_groups" `
2023-07-21 10:43:51 -04:00
// CustomEndpoint is the (optional) custom dns hostname for the kubernetes api server.
CustomEndpoint string ` hcl:"custom_endpoint" cty:"custom_endpoint" `
2023-06-19 07:02:01 -04:00
}
2023-07-21 04:04:29 -04:00
// GetCreateMAA gets the CreateMAA variable.
// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config.
func ( g * GCPClusterVariables ) GetCreateMAA ( ) bool {
return false
}
// String returns a string representation of the variables, formatted as Terraform variables.
func ( g * GCPClusterVariables ) String ( ) string {
f := hclwrite . NewEmptyFile ( )
gohcl . EncodeIntoBody ( g , f . Body ( ) )
return string ( f . Bytes ( ) )
}
2023-06-19 07:02:01 -04:00
// GCPNodeGroup is a node group to create on GCP.
type GCPNodeGroup struct {
// Role is the role of the node group.
Role string ` hcl:"role" cty:"role" `
// StateDiskSizeGB is the size of the state disk to allocate to each node, in GB.
StateDiskSizeGB int ` hcl:"disk_size" cty:"disk_size" `
// InitialCount is the initial number of nodes to create in the node group.
2023-07-21 04:04:29 -04:00
// During upgrades this value is not set.
InitialCount * int ` hcl:"initial_count" cty:"initial_count" `
2023-06-19 07:02:01 -04:00
Zone string ` hcl:"zone" cty:"zone" `
InstanceType string ` hcl:"instance_type" cty:"instance_type" `
DiskType string ` hcl:"disk_type" cty:"disk_type" `
2022-09-27 03:22:29 -04:00
}
2022-12-07 05:48:54 -05:00
// GCPIAMVariables is user configuration for creating the IAM confioguration with Terraform on GCP.
type GCPIAMVariables struct {
// Project is the ID of the GCP project to use.
Project string
// Region is the GCP region to use.
Region string
// Zone is the GCP zone to use.
Zone string
// ServiceAccountID is the ID of the service account to use.
ServiceAccountID string
}
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.
func ( v * GCPIAMVariables ) String ( ) string {
2022-09-27 03:22:29 -04:00
b := & strings . Builder { }
2022-12-07 05:48:54 -05:00
writeLinef ( b , "project_id = %q" , v . Project )
2022-09-27 03:22:29 -04:00
writeLinef ( b , "region = %q" , v . Region )
writeLinef ( b , "zone = %q" , v . Zone )
2022-12-07 05:48:54 -05:00
writeLinef ( b , "service_account_id = %q" , v . ServiceAccountID )
2022-09-27 03:22:29 -04:00
return b . String ( )
}
2022-12-07 05:48:54 -05:00
// AzureClusterVariables is user configuration for creating a cluster with Terraform on Azure.
type AzureClusterVariables struct {
2023-06-22 10:53:40 -04:00
// Name of the cluster.
Name string ` hcl:"name" cty:"name" `
// ImageID is the ID of the Azure image to use.
ImageID string ` hcl:"image_id" cty:"image_id" `
// CreateMAA sets whether a Microsoft Azure attestation provider should be created.
CreateMAA * bool ` hcl:"create_maa" cty:"create_maa" `
// Debug is true if debug mode is enabled.
Debug * bool ` hcl:"debug" cty:"debug" `
2022-10-06 05:52:19 -04:00
// ResourceGroup is the name of the Azure resource group to use.
2023-06-22 10:53:40 -04:00
ResourceGroup string ` hcl:"resource_group" cty:"resource_group" `
2022-10-06 05:52:19 -04:00
// Location is the Azure location to use.
2023-06-22 10:53:40 -04:00
Location string ` hcl:"location" cty:"location" `
2022-10-06 05:52:19 -04:00
// UserAssignedIdentity is the name of the Azure user-assigned identity to use.
2023-06-22 10:53:40 -04:00
UserAssignedIdentity string ` hcl:"user_assigned_identity" cty:"user_assigned_identity" `
2022-10-06 05:52:19 -04:00
// ConfidentialVM sets the VM to be confidential.
2023-06-22 10:53:40 -04:00
ConfidentialVM * bool ` hcl:"confidential_vm" cty:"confidential_vm" `
2022-10-19 07:10:15 -04:00
// SecureBoot sets the VM to use secure boot.
2023-06-22 10:53:40 -04:00
SecureBoot * bool ` hcl:"secure_boot" cty:"secure_boot" `
// NodeGroups is a map of node groups to create.
NodeGroups map [ string ] AzureNodeGroup ` hcl:"node_groups" cty:"node_groups" `
2023-07-21 10:43:51 -04:00
// CustomEndpoint is the (optional) custom dns hostname for the kubernetes api server.
CustomEndpoint string ` hcl:"custom_endpoint" cty:"custom_endpoint" `
2022-10-06 05:52:19 -04:00
}
2023-07-21 04:04:29 -04:00
// GetCreateMAA gets the CreateMAA variable.
// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config.
func ( a * AzureClusterVariables ) GetCreateMAA ( ) bool {
if a . CreateMAA == nil {
return false
}
return * a . CreateMAA
}
2022-10-06 05:52:19 -04:00
// String returns a string representation of the variables, formatted as Terraform variables.
2023-07-21 04:04:29 -04:00
func ( a * AzureClusterVariables ) String ( ) string {
2023-06-22 10:53:40 -04:00
f := hclwrite . NewEmptyFile ( )
2023-07-21 04:04:29 -04:00
gohcl . EncodeIntoBody ( a , f . Body ( ) )
2023-06-22 10:53:40 -04:00
return string ( f . Bytes ( ) )
}
2022-10-06 05:52:19 -04:00
2023-06-22 10:53:40 -04:00
// AzureNodeGroup is a node group to create on Azure.
type AzureNodeGroup struct {
// Role is the role of the node group.
Role string ` hcl:"role" cty:"role" `
2023-06-30 04:53:00 -04:00
// InitialCount is optional for upgrades.
InitialCount * int ` hcl:"initial_count" cty:"initial_count" `
InstanceType string ` hcl:"instance_type" cty:"instance_type" `
DiskSizeGB int ` hcl:"disk_size" cty:"disk_size" `
DiskType string ` hcl:"disk_type" cty:"disk_type" `
Zones * [ ] string ` hcl:"zones" cty:"zones" `
2022-10-06 05:52:19 -04:00
}
2022-12-07 05:48:54 -05:00
// AzureIAMVariables is user configuration for creating the IAM configuration with Terraform on Microsoft Azure.
type AzureIAMVariables struct {
// Region is the Azure region to use. (e.g. westus)
Region string
// ServicePrincipal is the name of the service principal to use.
ServicePrincipal string
// ResourceGroup is the name of the resource group to use.
ResourceGroup string
}
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.
func ( v * AzureIAMVariables ) String ( ) string {
b := & strings . Builder { }
writeLinef ( b , "service_principal_name = %q" , v . ServicePrincipal )
writeLinef ( b , "region = %q" , v . Region )
writeLinef ( b , "resource_group_name = %q" , v . ResourceGroup )
return b . String ( )
}
2023-02-27 12:19:52 -05:00
// OpenStackClusterVariables is user configuration for creating a cluster with Terraform on OpenStack.
type OpenStackClusterVariables struct {
2023-07-03 10:33:00 -04:00
// Name of the cluster.
Name string ` hcl:"name" cty:"name" `
// NodeGroups is a map of node groups to create.
NodeGroups map [ string ] OpenStackNodeGroup ` hcl:"node_groups" cty:"node_groups" `
2023-02-27 12:19:52 -05:00
// Cloud is the (optional) name of the OpenStack cloud to use when reading the "clouds.yaml" configuration file. If empty, environment variables are used.
2023-07-03 10:33:00 -04:00
Cloud * string ` hcl:"cloud" cty:"cloud" `
2023-02-27 12:19:52 -05:00
// FloatingIPPoolID is the ID of the OpenStack floating IP pool to use for public IPs.
2023-07-03 10:33:00 -04:00
FloatingIPPoolID string ` hcl:"floating_ip_pool_id" cty:"floating_ip_pool_id" `
2023-02-27 12:19:52 -05:00
// ImageURL is the URL of the OpenStack image to use.
2023-07-03 10:33:00 -04:00
ImageURL string ` hcl:"image_url" cty:"image_url" `
2023-02-27 12:19:52 -05:00
// DirectDownload decides whether to download the image directly from the URL to OpenStack or to upload it from the local machine.
2023-07-03 10:33:00 -04:00
DirectDownload bool ` hcl:"direct_download" cty:"direct_download" `
2023-03-03 09:28:28 -05:00
// OpenstackUserDomainName is the OpenStack user domain name to use.
2023-07-03 10:33:00 -04:00
OpenstackUserDomainName string ` hcl:"openstack_user_domain_name" cty:"openstack_user_domain_name" `
2023-03-03 09:28:28 -05:00
// OpenstackUsername is the OpenStack user name to use.
2023-07-03 10:33:00 -04:00
OpenstackUsername string ` hcl:"openstack_username" cty:"openstack_username" `
2023-03-03 09:28:28 -05:00
// OpenstackPassword is the OpenStack password to use.
2023-07-03 10:33:00 -04:00
OpenstackPassword string ` hcl:"openstack_password" cty:"openstack_password" `
2023-02-27 12:19:52 -05:00
// Debug is true if debug mode is enabled.
2023-07-03 10:33:00 -04:00
Debug bool ` hcl:"debug" cty:"debug" `
2023-07-21 10:43:51 -04:00
// CustomEndpoint is the (optional) custom dns hostname for the kubernetes api server.
CustomEndpoint string ` hcl:"custom_endpoint" cty:"custom_endpoint" `
2023-07-03 10:33:00 -04:00
}
2023-07-21 04:04:29 -04:00
// GetCreateMAA gets the CreateMAA variable.
// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config.
func ( o * OpenStackClusterVariables ) GetCreateMAA ( ) bool {
return false
}
// String returns a string representation of the variables, formatted as Terraform variables.
func ( o * OpenStackClusterVariables ) String ( ) string {
f := hclwrite . NewEmptyFile ( )
gohcl . EncodeIntoBody ( o , f . Body ( ) )
return string ( f . Bytes ( ) )
}
2023-07-03 10:33:00 -04:00
// OpenStackNodeGroup is a node group to create on OpenStack.
type OpenStackNodeGroup struct {
// Role is the role of the node group.
Role string ` hcl:"role" cty:"role" `
// InitialCount is the number of instances to create.
2023-07-21 04:04:29 -04:00
// InitialCount is optional for upgrades. OpenStack does not support upgrades yet but might in the future.
2023-08-02 04:39:18 -04:00
InitialCount int ` hcl:"initial_count" cty:"initial_count" `
// Flavor is the ID of the OpenStack flavor (machine type) to use.
FlavorID string ` hcl:"flavor_id" cty:"flavor_id" `
2023-07-03 10:33:00 -04:00
// Zone is the OpenStack availability zone to use.
Zone string ` hcl:"zone" cty:"zone" `
// StateDiskType is the OpenStack disk type to use for the state disk.
StateDiskType string ` hcl:"state_disk_type" cty:"state_disk_type" `
// StateDiskSizeGB is the size of the state disk to allocate to each node, in GB.
StateDiskSizeGB int ` hcl:"state_disk_size" cty:"state_disk_size" `
2023-02-27 12:19:52 -05:00
}
2023-06-01 06:33:06 -04:00
// TODO(malt3): Add support for OpenStack IAM variables.
2023-02-27 12:19:52 -05:00
2022-09-27 03:22:29 -04:00
// QEMUVariables is user configuration for creating a QEMU cluster with Terraform.
type QEMUVariables struct {
2023-06-28 08:42:34 -04:00
// Name is the name to use for the cluster.
Name string ` hcl:"name" cty:"name" `
// NodeGroups is a map of node groups to create.
NodeGroups map [ string ] QEMUNodeGroup ` hcl:"node_groups" cty:"node_groups" `
// Machine is the type of machine to use. use 'q35' for secure boot and 'pc' for non secure boot. See 'qemu-system-x86_64 -machine help'
Machine string ` hcl:"machine" cty:"machine" `
2022-10-05 03:11:30 -04:00
// LibvirtURI is the libvirt connection URI.
2023-06-28 08:42:34 -04:00
LibvirtURI string ` hcl:"libvirt_uri" cty:"libvirt_uri" `
2022-10-05 03:11:30 -04:00
// LibvirtSocketPath is the path to the libvirt socket in case of unix socket.
2023-06-28 08:42:34 -04:00
LibvirtSocketPath string ` hcl:"libvirt_socket_path" cty:"libvirt_socket_path" `
2023-05-16 08:13:10 -04:00
// BootMode is the boot mode to use.
// Can be either "uefi" or "direct-linux-boot".
2023-06-28 08:42:34 -04:00
BootMode string ` hcl:"constellation_boot_mode" cty:"constellation_boot_mode" `
// ImagePath is the path to the image to use for the nodes.
ImagePath string ` hcl:"constellation_os_image" cty:"constellation_os_image" `
2022-09-27 03:22:29 -04:00
// ImageFormat is the format of the image from ImagePath.
2023-06-28 08:42:34 -04:00
ImageFormat string ` hcl:"image_format" cty:"image_format" `
2022-09-27 03:22:29 -04:00
// MetadataAPIImage is the container image to use for the metadata API.
2023-06-28 08:42:34 -04:00
MetadataAPIImage string ` hcl:"metadata_api_image" cty:"metadata_api_image" `
2022-10-05 03:11:30 -04:00
// MetadataLibvirtURI is the libvirt connection URI used by the metadata container.
// In case of unix socket, this should be "qemu:///system".
// Other wise it should be the same as LibvirtURI.
2023-06-28 08:42:34 -04:00
MetadataLibvirtURI string ` hcl:"metadata_libvirt_uri" cty:"metadata_libvirt_uri" `
2022-10-19 07:10:15 -04:00
// NVRAM is the path to the NVRAM template.
2023-06-28 08:42:34 -04:00
NVRAM string ` hcl:"nvram" cty:"nvram" `
2022-10-19 07:10:15 -04:00
// Firmware is the path to the firmware.
2023-06-28 08:42:34 -04:00
Firmware * string ` hcl:"firmware" cty:"firmware" `
2023-05-16 08:13:10 -04:00
// BzImagePath is the path to the bzImage (kernel).
2023-06-28 08:42:34 -04:00
BzImagePath * string ` hcl:"constellation_kernel" cty:"constellation_kernel" `
2023-05-16 08:13:10 -04:00
// InitrdPath is the path to the initrd.
2023-06-28 08:42:34 -04:00
InitrdPath * string ` hcl:"constellation_initrd" cty:"constellation_initrd" `
2023-05-16 08:13:10 -04:00
// KernelCmdline is the kernel command line.
2023-06-28 08:42:34 -04:00
KernelCmdline * string ` hcl:"constellation_cmdline" cty:"constellation_cmdline" `
2023-07-21 10:43:51 -04:00
// CustomEndpoint is the (optional) custom dns hostname for the kubernetes api server.
CustomEndpoint string ` hcl:"custom_endpoint" cty:"custom_endpoint" `
2022-09-27 03:22:29 -04:00
}
2023-07-21 04:04:29 -04:00
// GetCreateMAA gets the CreateMAA variable.
// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config.
func ( q * QEMUVariables ) GetCreateMAA ( ) bool {
return false
}
2022-09-27 03:22:29 -04:00
// String returns a string representation of the variables, formatted as Terraform variables.
2023-07-21 04:04:29 -04:00
func ( q * QEMUVariables ) String ( ) string {
2023-06-28 08:42:34 -04:00
// copy v object
2023-07-21 04:04:29 -04:00
vCopy := * q
2023-06-28 08:42:34 -04:00
switch vCopy . NVRAM {
2022-10-19 07:10:15 -04:00
case "production" :
2023-06-28 08:42:34 -04:00
vCopy . NVRAM = "/usr/share/OVMF/constellation_vars.production.fd"
2022-10-19 07:10:15 -04:00
case "testing" :
2023-06-28 08:42:34 -04:00
vCopy . NVRAM = "/usr/share/OVMF/constellation_vars.testing.fd"
2022-10-19 07:10:15 -04:00
}
2023-06-28 08:42:34 -04:00
f := hclwrite . NewEmptyFile ( )
gohcl . EncodeIntoBody ( vCopy , f . Body ( ) )
return string ( f . Bytes ( ) )
}
2022-09-27 03:22:29 -04:00
2023-06-28 08:42:34 -04:00
// QEMUNodeGroup is a node group for a QEMU cluster.
type QEMUNodeGroup struct {
// Role is the role of the node group.
Role string ` hcl:"role" cty:"role" `
2023-06-30 04:53:00 -04:00
// InitialCount is the number of instances to create.
2023-07-21 04:04:29 -04:00
// InitialCount is optional for upgrades.
// Upgrades are not implemented for QEMU. The type is similar to other NodeGroup types for consistency.
InitialCount * int ` hcl:"initial_count" cty:"initial_count" `
2023-06-28 08:42:34 -04:00
// DiskSize is the size of the disk to allocate to each node, in GiB.
DiskSize int ` hcl:"disk_size" cty:"disk_size" `
// CPUCount is the number of CPUs to allocate to each node.
CPUCount int ` hcl:"vcpus" cty:"vcpus" `
// MemorySize is the amount of memory to allocate to each node, in MiB.
MemorySize int ` hcl:"memory" cty:"memory" `
2022-09-27 03:22:29 -04:00
}
2022-10-25 09:51:23 -04:00
func writeLinef ( builder * strings . Builder , format string , a ... any ) {
2022-09-27 03:22:29 -04:00
builder . WriteString ( fmt . Sprintf ( format , a ... ) )
builder . WriteByte ( '\n' )
}
2023-07-21 04:04:29 -04:00
func toPtr [ T any ] ( v T ) * T {
return & v
}