diff --git a/docs/index.md b/docs/index.md
index 805917f57..e46b15cf9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -174,7 +174,7 @@ For questions or issues with the provider, open up an issue in the provider GitH
- `api_key` (String, Sensitive) The Spectro Cloud API key. Can also be set with the `SPECTROCLOUD_APIKEY` environment variable.
- `host` (String) The Spectro Cloud API host url. Can also be set with the `SPECTROCLOUD_HOST` environment variable. Defaults to https://api.spectrocloud.com
-- `ignore_insecure_tls_error` (Boolean) Ignore insecure TLS errors for Spectro Cloud API endpoints. Defaults to false.
+- `ignore_insecure_tls_error` (Boolean) Ignore insecure TLS errors for Spectro Cloud API endpoints. ⚠️ WARNING: Setting this to true disables SSL certificate verification and makes connections vulnerable to man-in-the-middle attacks. Only use this in development/testing environments or when connecting to self-signed certificates in trusted networks. Defaults to false.
- `project_name` (String) The Palette project the provider will target. If no value is provided, the `Default` Palette project is used. The default value is `Default`.
- `retry_attempts` (Number) Number of retry attempts. Can also be set with the `SPECTROCLOUD_RETRY_ATTEMPTS` environment variable. Defaults to 10.
- `trace` (Boolean) Enable HTTP request tracing. Can also be set with the `SPECTROCLOUD_TRACE` environment variable. To enable Terraform debug logging, set `TF_LOG=DEBUG`. Visit the Terraform documentation to learn more about Terraform [debugging](https://developer.hashicorp.com/terraform/plugin/log/managing).
diff --git a/docs/resources/cloudaccount_azure.md b/docs/resources/cloudaccount_azure.md
index b5e92b572..d5bca362c 100644
--- a/docs/resources/cloudaccount_azure.md
+++ b/docs/resources/cloudaccount_azure.md
@@ -34,12 +34,13 @@ resource "spectrocloud_cloudaccount_azure" "azure-1" {
### Optional
- `cloud` (String) The Azure partition in which the cloud account is located.
-Can be 'AzurePublicCloud' for standard Azure regions or 'AzureUSGovernmentCloud' for Azure GovCloud (US) regions.
+Can be 'AzurePublicCloud' for standard Azure regions or 'AzureUSGovernmentCloud' for Azure GovCloud (US) regions or 'AzureUSSecretCloud' for Azure Secret Cloud regions.
Default is 'AzurePublicCloud'.
- `context` (String) The context of the Azure configuration. Defaults to `project`. If the `project` context is specified, the project name will sourced from the provider configuration parameter [`project_name`](https://registry.terraform.io/providers/spectrocloud/spectrocloud/latest/docs#schema).
- `disable_properties_request` (Boolean) Disable properties request. This is a boolean value that indicates whether to disable properties request or not. If not specified, the default value is `false`.
- `private_cloud_gateway_id` (String) ID of the private cloud gateway. This is the ID of the private cloud gateway that is used to connect to the private cluster endpoint.
- `tenant_name` (String) The name of the tenant. This is the name of the tenant that is used to connect to the Azure cloud.
+- `tls_cert` (String) TLS certificate for authentication. This field is only allowed when cloud is set to 'AzureUSSecretCloud'.
### Read-Only
diff --git a/docs/resources/cluster_maas.md b/docs/resources/cluster_maas.md
index 45e097ad1..9b628f825 100644
--- a/docs/resources/cluster_maas.md
+++ b/docs/resources/cluster_maas.md
@@ -169,6 +169,10 @@ Required:
- `domain` (String) Domain name in which the cluster to be provisioned.
+Optional:
+
+- `enable_lxd_vm` (Boolean) Whether to enable LXD VM. Default is `false`.
+
### Nested Schema for `machine_pool`
@@ -179,6 +183,7 @@ Required:
- `count` (Number) Number of nodes in the machine pool.
- `instance_type` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--machine_pool--instance_type))
- `name` (String) Name of the machine pool.
+- `network` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--machine_pool--network))
- `placement` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--machine_pool--placement))
Optional:
@@ -193,6 +198,7 @@ Optional:
- `node_tags` (Set of String) Node tags to dynamically place nodes in a pool by using MAAS automatic tags. Specify the tag values that you want to apply to all nodes in the node pool.
- `taints` (Block List) (see [below for nested schema](#nestedblock--machine_pool--taints))
- `update_strategy` (String) Update strategy for the machine pool. Valid values are `RollingUpdateScaleOut` and `RollingUpdateScaleIn`.
+- `use_lxd_vm` (Boolean) Whether to use LXD VM. Default is `false`.
### Nested Schema for `machine_pool.instance_type`
@@ -203,6 +209,19 @@ Required:
- `min_memory_mb` (Number) Minimum memory in MB required for the machine pool node.
+
+### Nested Schema for `machine_pool.network`
+
+Required:
+
+- `network_name` (String) The name of the network in which VMs are created/located.
+
+Optional:
+
+- `parent_pool_uid` (String) The UID of the parent pool which allocates IPs for this IPPool.
+- `static_ip` (Boolean) Whether to use static IP. Default is `false`.
+
+
### Nested Schema for `machine_pool.placement`
diff --git a/docs/resources/registry_oci.md b/docs/resources/registry_oci.md
index fa2f9025c..ab55ff85a 100644
--- a/docs/resources/registry_oci.md
+++ b/docs/resources/registry_oci.md
@@ -72,7 +72,7 @@ Optional:
Optional:
- `certificate` (String) Specifies the TLS certificate used for secure communication. Required for enabling SSL/TLS encryption.
-- `insecure_skip_verify` (Boolean) Disables TLS certificate verification when set to true. Use with caution as it may expose connections to security risks.
+- `insecure_skip_verify` (Boolean) Disables TLS certificate verification when set to true. ⚠️ WARNING: Setting this to true disables SSL certificate verification and makes connections vulnerable to man-in-the-middle attacks. Only use this when connecting to registries with self-signed certificates in trusted networks.
diff --git a/docs/resources/sso.md b/docs/resources/sso.md
index 51cc21e0b..e3f328a65 100644
--- a/docs/resources/sso.md
+++ b/docs/resources/sso.md
@@ -102,7 +102,7 @@ Optional:
- `default_team_ids` (Set of String) A set of default team IDs assigned to users.
- `identity_provider_ca_certificate` (String) Certificate authority (CA) certificate for the identity provider.
-- `insecure_skip_tls_verify` (Boolean) Boolean to skip TLS verification for identity provider communication.
+- `insecure_skip_tls_verify` (Boolean) Boolean to skip TLS verification for identity provider communication. ⚠️ WARNING: Setting this to true disables SSL certificate verification and makes connections vulnerable to man-in-the-middle attacks. Only use this when connecting to identity providers with self-signed certificates in trusted networks.
- `user_info_endpoint` (Block List, Max: 1) To allow Palette to query the OIDC userinfo endpoint using the provided Issuer URL. Palette will first attempt to retrieve role and group information from userInfo endpoint. If unavailable, Palette will fall back to using Required Claims as specified above. Use the following fields to specify what Required Claims Palette will include when querying the userinfo endpoint. (see [below for nested schema](#nestedblock--oidc--user_info_endpoint))
Read-Only:
diff --git a/examples/resources/eks/README.md b/examples/resources/eks/README.md
new file mode 100644
index 000000000..68edb19b8
--- /dev/null
+++ b/examples/resources/eks/README.md
@@ -0,0 +1,34 @@
+# Basic Cluster demo
+
+End-to-end example of provisioning a new AWS K8s cluster with all of its dependencies. This terraform configuration
+will provision the following resources on Spectro Cloud:
+- AWS Cloud Account
+- Eks Cluster Profile
+- Eks Cluster
+
+## Instructions:
+
+Clone this repository to a local directory, and then change directory to `examples/e2e/eks`. Proceed with the following:
+1. Follow the Spectro Cloud documentation to create an AWS cloud account with appropriate permissions:
+[AWS Cloud Account](https://docs.spectrocloud.com/clusters/?clusterType=aws_cluster#awscloudaccountpermissions).
+2. From the current directory, copy the template variable file `terraform.template.tfvars` to a new file `terraform.tfvars`.
+3. Specify and update all the placeholder values in the `terraform.tfvars` file.
+4. Initialize and run terraform: `terraform init && terraform apply`.
+5. Wait for the cluster creation to finish.
+
+Once the cluster is provisioned, the cluster _kubeconfig_ file is exported in the current working directly.
+
+Export the kubeconfig and check cluster pods:
+
+```shell
+export KUBECONFIG=kubeconfig_eks
+kubectl get pod -A
+```
+
+## Clean up:
+
+Run the destroy operation:
+
+```shell
+terraform destroy
+```
diff --git a/examples/resources/eks/providers.tf b/examples/resources/eks/providers.tf
new file mode 100644
index 000000000..f3bdb2e05
--- /dev/null
+++ b/examples/resources/eks/providers.tf
@@ -0,0 +1,28 @@
+terraform {
+ required_providers {
+ spectrocloud = {
+ version = ">= 0.1"
+ source = "spectrocloud/spectrocloud"
+ }
+ }
+}
+
+variable "sc_host" {
+ description = "Spectro Cloud Endpoint"
+ default = "api.spectrocloud.com"
+}
+
+variable "sc_api_key" {
+ description = "Spectro Cloud API key"
+}
+
+variable "sc_project_name" {
+ description = "Spectro Cloud Project (e.g: Default)"
+ default = "Default"
+}
+
+provider "spectrocloud" {
+ host = var.sc_host
+ api_key = var.sc_api_key
+ project_name = var.sc_project_name
+}
diff --git a/examples/resources/eks/resource_cluster.tf b/examples/resources/eks/resource_cluster.tf
new file mode 100644
index 000000000..91da8ba82
--- /dev/null
+++ b/examples/resources/eks/resource_cluster.tf
@@ -0,0 +1,36 @@
+resource "spectrocloud_cluster_eks" "cluster" {
+ name = "ran-tf-eks"
+ context = "tenant"
+ tags_map = {"QA" = "ranjithroy@_ .:/=+-@.@123"}
+
+ cluster_profile {
+ id = "68a6e0bc500766a5c9241784"
+ }
+
+ cloud_account_id = "68a6e0ec788fd02b1e0151a4"
+
+
+ cloud_config {
+ ssh_key_name = var.aws_ssh_key_name
+ region = var.aws_region
+ vpc_id = var.aws_vpc_id
+ azs = ["ap-south-1a", "ap-south-1b"]
+ }
+
+ machine_pool {
+ name = "worker-basic"
+ count = 1
+ instance_type = "t3.xlarge"
+ azs = ["ap-south-1a", "ap-south-1b"]
+ disk_size_gb = 60
+ }
+
+ machine_pool {
+ name = "worker-basic-2"
+ count = 1
+ instance_type = "t3.xlarge"
+ azs = ["ap-south-1a", "ap-south-1b"]
+ disk_size_gb = 60
+ }
+
+}
diff --git a/examples/resources/eks/variables.tf b/examples/resources/eks/variables.tf
new file mode 100644
index 000000000..56016e99f
--- /dev/null
+++ b/examples/resources/eks/variables.tf
@@ -0,0 +1,43 @@
+#variable "cloud_account_type" {}
+
+## Option A (When Using access key and secret key)
+#variable "aws_access_key" {
+# default = ""
+#}
+#variable "aws_secret_key" {
+# default = ""
+#}
+
+# Option B (When Using sts info, arn and external id)
+#variable "arn" {
+# default = ""
+#}
+#variable "external_id" {
+# default = ""
+#}
+#
+## Cluster
+variable "aws_ssh_key_name" {
+ default = ""
+}
+variable "aws_region" {}
+variable "aws_vpc_id" {
+ default = ""
+}
+
+# Provisioning Option A (Dynamic)
+variable "azs" {
+ default = []
+ type = list(string)
+}
+
+# Provisioning Option B (Static)
+variable "cp_azs_subnets_map" {
+ default = {}
+ type = map(string)
+}
+
+variable "worker_azs_subnets_map" {
+ default = {}
+ type = map(string)
+}
diff --git a/examples/resources/spectrocloud_cloudaccount_azure/resource.tf b/examples/resources/spectrocloud_cloudaccount_azure/resource.tf
index 5f88cf8a6..3cd0d058f 100644
--- a/examples/resources/spectrocloud_cloudaccount_azure/resource.tf
+++ b/examples/resources/spectrocloud_cloudaccount_azure/resource.tf
@@ -31,3 +31,19 @@ resource "spectrocloud_cloudaccount_azure" "azure_government" {
cloud = "AzureUSGovernmentCloud"
context = "project"
}
+
+# Example 3: Azure US Secret Cloud account with TLS certificate
+resource "spectrocloud_cloudaccount_azure" "azure_secret" {
+ name = "azure-secret-account"
+ azure_tenant_id = var.azure_secret_tenant_id
+ azure_client_id = var.azure_secret_client_id
+ azure_client_secret = var.azure_secret_client_secret
+
+ cloud = "AzureUSSecretCloud"
+ context = "project"
+
+ # TLS certificate is only allowed when cloud is set to "AzureUSSecretCloud"
+ tls_cert = var.azure_secret_tls_cert
+
+ tenant_name = "Secret Cloud Tenant"
+}
\ No newline at end of file
diff --git a/examples/resources/spectrocloud_cloudaccount_azure/variables.tf b/examples/resources/spectrocloud_cloudaccount_azure/variables.tf
index 2c0996aab..c9a0a1e28 100644
--- a/examples/resources/spectrocloud_cloudaccount_azure/variables.tf
+++ b/examples/resources/spectrocloud_cloudaccount_azure/variables.tf
@@ -68,3 +68,10 @@ variable "azure_secret_client_secret" {
default = ""
sensitive = true
}
+
+variable "azure_secret_tls_cert" {
+ description = "TLS certificate for Azure US Secret Cloud authentication"
+ type = string
+ default = ""
+ sensitive = true
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index db87a9af5..c92d15969 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
github.com/hashicorp/terraform-plugin-docs v0.16.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0
github.com/robfig/cron v1.2.0
- github.com/spectrocloud/palette-sdk-go v0.0.0-20250813031623-91ef23e78e8f
+ github.com/spectrocloud/palette-sdk-go v0.0.0-20250826051324-b2ea90bf151b
github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools v2.2.0+incompatible
diff --git a/go.sum b/go.sum
index 5836fcbe1..7f64b0a55 100644
--- a/go.sum
+++ b/go.sum
@@ -598,8 +598,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spectrocloud/palette-sdk-go v0.0.0-20250813031623-91ef23e78e8f h1:KAEMjdoWEQ9FlHIVybcLDILngTmuhD7BqsNve4z8W5E=
-github.com/spectrocloud/palette-sdk-go v0.0.0-20250813031623-91ef23e78e8f/go.mod h1:wIt8g7I7cmcQvTo5ktwhSF0/bWq6uRdxGBs9dwTpleU=
+github.com/spectrocloud/palette-sdk-go v0.0.0-20250826051324-b2ea90bf151b h1:Xhup+ut+6exZDTKu2SYTFHrAM/PZ625/w+X5DRwirG4=
+github.com/spectrocloud/palette-sdk-go v0.0.0-20250826051324-b2ea90bf151b/go.mod h1:wIt8g7I7cmcQvTo5ktwhSF0/bWq6uRdxGBs9dwTpleU=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
diff --git a/spectrocloud/application_create_common.go b/spectrocloud/application_create_common.go
index fe102ab1e..028901fb5 100644
--- a/spectrocloud/application_create_common.go
+++ b/spectrocloud/application_create_common.go
@@ -51,9 +51,9 @@ func toAppDeploymentTargetClusterLimits(d *schema.ResourceData) *models.V1AppDep
limits := config["limits"].([]interface{})[i].(map[string]interface{})
if limits["cpu"] != nil && limits["memory"] != nil {
return &models.V1AppDeploymentTargetClusterLimits{
- CPU: int32(limits["cpu"].(int)),
- MemoryMiB: int32(limits["memory"].(int)),
- StorageGiB: int32(limits["storage"].(int)),
+ CPU: SafeInt32(limits["cpu"].(int)),
+ MemoryMiB: SafeInt32(limits["memory"].(int)),
+ StorageGiB: SafeInt32(limits["storage"].(int)),
}
}
}
diff --git a/spectrocloud/cluster_common_crud.go b/spectrocloud/cluster_common_crud.go
index 997418633..f93ef9550 100644
--- a/spectrocloud/cluster_common_crud.go
+++ b/spectrocloud/cluster_common_crud.go
@@ -2,6 +2,8 @@ package spectrocloud
import (
"context"
+ "errors"
+ "fmt"
"log"
"time"
@@ -140,9 +142,38 @@ func waitForClusterCreation(ctx context.Context, d *schema.ResourceData, uid str
// Wait, catching any errors
_, err := stateConf.WaitForStateContext(ctx)
if err != nil {
+ // Check if this is a timeout error
+ var timeoutErr *retry.TimeoutError
+ if errors.As(err, &timeoutErr) {
+ // Get current cluster state for warning message
+ cluster, stateErr := c.GetCluster(d.Id())
+ currentState := timeoutErr.LastState
+ if currentState == "" {
+ currentState = "Unknown"
+ }
+ if stateErr == nil && cluster != nil && cluster.Status != nil {
+ currentState = cluster.Status.State
+ if cluster.Status.State == "Running" {
+ if clusterSummary, _ := c.GetClusterOverview(d.Id()); clusterSummary != nil && clusterSummary.Status.Health != nil {
+ if clusterSummary.Status.Health.State != "" && clusterSummary.Status.Health.State != "Healthy" {
+ currentState += "-" + clusterSummary.Status.Health.State
+ }
+ }
+ }
+ }
+
+ // Return warning instead of error
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Warning,
+ Summary: "Cluster creation timeout",
+ Detail: fmt.Sprintf("Cluster creation timed out after waiting for %v. Current cluster state is '%s'. The cluster may still be provisioning in the background and will eventually reach the 'Running-Healthy' state.", d.Timeout(schema.TimeoutCreate)-1*time.Minute, currentState),
+ })
+ return diags, false
+ }
+ // For non-timeout errors, still return the error
return diag.FromErr(err), true
}
- return nil, false
+ return diags, false
}
// var resourceClusterUpdatePendingStates = []string{
diff --git a/spectrocloud/cluster_common_hash.go b/spectrocloud/cluster_common_hash.go
index c5d15f535..abf2dbd78 100644
--- a/spectrocloud/cluster_common_hash.go
+++ b/spectrocloud/cluster_common_hash.go
@@ -6,6 +6,7 @@ import (
"hash/fnv"
"sort"
"strings"
+ "time"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"gopkg.in/yaml.v3"
@@ -467,7 +468,10 @@ func hash(s string) uint32 {
func YamlContentHash(yamlContent string) string {
canonicalContent := yamlContentToCanonicalString(yamlContent)
h := fnv.New64a()
- h.Write([]byte(canonicalContent))
+ if _, err := h.Write([]byte(canonicalContent)); err != nil {
+ // If hash writing fails, return a fallback hash
+ return fmt.Sprintf("error_hash_%d", time.Now().UnixNano())
+ }
return fmt.Sprintf("%x", h.Sum64())
}
diff --git a/spectrocloud/constants/constants.go b/spectrocloud/constants/constants.go
new file mode 100644
index 000000000..36a4a77b0
--- /dev/null
+++ b/spectrocloud/constants/constants.go
@@ -0,0 +1,15 @@
+package constants
+
+const (
+ // Int32MaxValue represents the maximum value for int32 type (2^31 - 1)
+ Int32MaxValue = 2147483647
+
+ // Int32MinValue represents the minimum value for int32 type (-2^31)
+ Int32MinValue = -2147483648
+
+ // UInt32MaxValue represents the maximum value for uint32 type (2^32 - 1)
+ UInt32MaxValue = 4294967295
+
+ // Int64MaxValue represents the maximum value for int64 type (2^63 - 1)
+ Int64MaxValue = 9223372036854775807
+)
diff --git a/spectrocloud/kubevirt/schema/k8s/affinity_spec.go b/spectrocloud/kubevirt/schema/k8s/affinity_spec.go
index df960dba5..a5628a2fd 100644
--- a/spectrocloud/kubevirt/schema/k8s/affinity_spec.go
+++ b/spectrocloud/kubevirt/schema/k8s/affinity_spec.go
@@ -1,6 +1,7 @@
package k8s
import (
+ "math"
"regexp"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -11,6 +12,17 @@ import (
"github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/kubevirt/utils"
)
+// safeInt32 converts int to int32 with bounds checking to prevent overflow
+func safeInt32(value int) int32 {
+ if value > math.MaxInt32 {
+ return math.MaxInt32
+ }
+ if value < math.MinInt32 {
+ return math.MinInt32
+ }
+ return int32(value)
+}
+
func affinityFields() map[string]*schema.Schema {
return map[string]*schema.Schema{
"node_affinity": {
@@ -437,7 +449,7 @@ func expandPreferredSchedulingTerms(t []interface{}) []v1.PreferredSchedulingTer
for i, n := range t {
in := n.(map[string]interface{})
if v, ok := in["weight"].(int); ok {
- obj[i].Weight = int32(v)
+ obj[i].Weight = safeInt32(v)
}
if v, ok := in["preference"].([]interface{}); ok && len(v) > 0 {
obj[i].Preference = *expandNodeSelectorTerm(v)
@@ -474,7 +486,7 @@ func expandWeightedPodAffinityTerms(t []interface{}) []v1.WeightedPodAffinityTer
for i, n := range t {
in := n.(map[string]interface{})
if v, ok := in["weight"].(int); ok {
- obj[i].Weight = int32(v)
+ obj[i].Weight = safeInt32(v)
}
if v, ok := in["pod_affinity_term"].([]interface{}); ok && len(v) > 0 {
obj[i].PodAffinityTerm = expandPodAffinityTerms(v)[0]
diff --git a/spectrocloud/kubevirt/schema/virtualmachineinstance/domain_spec.go b/spectrocloud/kubevirt/schema/virtualmachineinstance/domain_spec.go
index e9abc881c..3c45387b2 100644
--- a/spectrocloud/kubevirt/schema/virtualmachineinstance/domain_spec.go
+++ b/spectrocloud/kubevirt/schema/virtualmachineinstance/domain_spec.go
@@ -1,11 +1,14 @@
package virtualmachineinstance
import (
+ "fmt"
+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"k8s.io/apimachinery/pkg/api/resource"
kubevirtapiv1 "kubevirt.io/api/core/v1"
+ "github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/constants"
"github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/kubevirt/utils"
)
@@ -286,12 +289,21 @@ func expandCPU(cpu map[string]interface{}) (kubevirtapiv1.CPU, error) {
}
if v, ok := cpu["cores"].(int); ok {
+ if v < 0 || v > constants.UInt32MaxValue {
+ return result, fmt.Errorf("cores value %d is out of range for uint32", v)
+ }
result.Cores = uint32(v)
}
if v, ok := cpu["sockets"].(int); ok {
+ if v < 0 || v > constants.UInt32MaxValue {
+ return result, fmt.Errorf("sockets value %d is out of range for uint32", v)
+ }
result.Sockets = uint32(v)
}
if v, ok := cpu["threads"].(int); ok {
+ if v < 0 || v > constants.UInt32MaxValue {
+ return result, fmt.Errorf("threads value %d is out of range for uint32", v)
+ }
result.Threads = uint32(v)
}
diff --git a/spectrocloud/provider.go b/spectrocloud/provider.go
index 0cb546ba9..588e5d696 100644
--- a/spectrocloud/provider.go
+++ b/spectrocloud/provider.go
@@ -60,7 +60,7 @@ func New(_ string) func() *schema.Provider {
"ignore_insecure_tls_error": {
Type: schema.TypeBool,
Optional: true,
- Description: "Ignore insecure TLS errors for Spectro Cloud API endpoints. Defaults to false.",
+ Description: "Ignore insecure TLS errors for Spectro Cloud API endpoints. ⚠️ WARNING: Setting this to true disables SSL certificate verification and makes connections vulnerable to man-in-the-middle attacks. Only use this in development/testing environments or when connecting to self-signed certificates in trusted networks. Defaults to false.",
},
},
ResourcesMap: map[string]*schema.Resource{
@@ -204,7 +204,9 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}
projectName := d.Get("project_name").(string)
insecure := d.Get("ignore_insecure_tls_error").(bool)
+
if insecure {
+ // #nosec G402
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
diff --git a/spectrocloud/resource_cloud_account_azure.go b/spectrocloud/resource_cloud_account_azure.go
index ef51e193d..367aa36ec 100644
--- a/spectrocloud/resource_cloud_account_azure.go
+++ b/spectrocloud/resource_cloud_account_azure.go
@@ -71,16 +71,16 @@ func resourceCloudAccountAzure() *schema.Resource {
Type: schema.TypeString,
Optional: true,
Default: "AzurePublicCloud",
- ValidateFunc: validation.StringInSlice([]string{"AzurePublicCloud", "AzureUSGovernmentCloud"}, false), // need to add support for "AzureUSSecretCloud"
+ ValidateFunc: validation.StringInSlice([]string{"AzurePublicCloud", "AzureUSGovernmentCloud", "AzureUSSecretCloud"}, false),
Description: `The Azure partition in which the cloud account is located.
-Can be 'AzurePublicCloud' for standard Azure regions or 'AzureUSGovernmentCloud' for Azure GovCloud (US) regions.
+Can be 'AzurePublicCloud' for standard Azure regions or 'AzureUSGovernmentCloud' for Azure GovCloud (US) regions or 'AzureUSSecretCloud' for Azure Secret Cloud regions.
Default is 'AzurePublicCloud'.`,
},
- // "tls_cert": {
- // Type: schema.TypeString,
- // Optional: true,
- // Description: "TLS certificate for authentication. This field is only allowed when cloud is set to 'AzureUSSecretCloud'.",
- // },
+ "tls_cert": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "TLS certificate for authentication. This field is only allowed when cloud is set to 'AzureUSSecretCloud'.",
+ },
},
}
}
@@ -93,9 +93,9 @@ func resourceCloudAccountAzureCreate(ctx context.Context, d *schema.ResourceData
var diags diag.Diagnostics
// Validate tls_cert is only used with AzureUSSecretCloud
- // if err := validateTlsCertConfiguration(d); err != nil {
- // return diag.FromErr(err)
- // }
+ if err := validateTlsCertConfiguration(d); err != nil {
+ return diag.FromErr(err)
+ }
account := toAzureAccount(d)
@@ -163,11 +163,11 @@ func flattenCloudAccountAzure(d *schema.ResourceData, account *models.V1AzureAcc
return diag.FromErr(err), true
}
}
- // if account.Spec.TLS != nil && account.Spec.TLS.Cert != "" {
- // if err := d.Set("tls_cert", account.Spec.TLS.Cert); err != nil {
- // return diag.FromErr(err), true
- // }
- // }
+ if account.Spec.TLS != nil && account.Spec.TLS.Cert != "" {
+ if err := d.Set("tls_cert", account.Spec.TLS.Cert); err != nil {
+ return diag.FromErr(err), true
+ }
+ }
return nil, false
}
@@ -179,9 +179,9 @@ func resourceCloudAccountAzureUpdate(ctx context.Context, d *schema.ResourceData
var diags diag.Diagnostics
// Validate tls_cert is only used with AzureUSSecretCloud
- // if err := validateTlsCertConfiguration(d); err != nil {
- // return diag.FromErr(err)
- // }
+ if err := validateTlsCertConfiguration(d); err != nil {
+ return diag.FromErr(err)
+ }
account := toAzureAccount(d)
@@ -241,26 +241,26 @@ func toAzureAccount(d *schema.ResourceData) *models.V1AzureAccount {
}
// add TLS configuration if tls_cert is provided
- // if tlsCert, ok := d.GetOk("tls_cert"); ok && tlsCert.(string) != "" {
- // account.Spec.TLS = &models.V1AzureSecretTLSConfig{
- // Cert: tlsCert.(string),
- // }
- // }
+ if tlsCert, ok := d.GetOk("tls_cert"); ok && tlsCert.(string) != "" {
+ account.Spec.TLS = &models.V1AzureSecretTLSConfig{
+ Cert: tlsCert.(string),
+ }
+ }
return account
}
-// func validateTlsCertConfiguration(d *schema.ResourceData) error {
-// cloud := d.Get("cloud").(string)
-// tlsCert := d.Get("tls_cert").(string)
+func validateTlsCertConfiguration(d *schema.ResourceData) error {
+ cloud := d.Get("cloud").(string)
+ tlsCert := d.Get("tls_cert").(string)
-// // If tls_cert is provided but cloud is not AzureUSSecretCloud, return an error
-// if tlsCert != "" && cloud != "AzureUSSecretCloud" {
-// return fmt.Errorf("tls_cert can only be set when cloud is 'AzureUSSecretCloud', but cloud is set to '%s'", cloud)
-// }
+ // If tls_cert is provided but cloud is not AzureUSSecretCloud, return an error
+ if tlsCert != "" && cloud != "AzureUSSecretCloud" {
+ return fmt.Errorf("tls_cert can only be set when cloud is 'AzureUSSecretCloud', but cloud is set to '%s'", cloud)
+ }
-// return nil
-// }
+ return nil
+}
func resourceAccountAzureImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
resourceContext := d.Get("context").(string)
diff --git a/spectrocloud/resource_cloud_account_azure_test.go b/spectrocloud/resource_cloud_account_azure_test.go
index a31f234f2..691ebbd60 100644
--- a/spectrocloud/resource_cloud_account_azure_test.go
+++ b/spectrocloud/resource_cloud_account_azure_test.go
@@ -121,225 +121,225 @@ func TestResourceCloudAccountAzureDelete(t *testing.T) {
assert.Len(t, diags, 0)
}
-// // Test for validateTlsCertConfiguration function
-// func TestValidateTlsCertConfiguration(t *testing.T) {
-// tests := []struct {
-// name string
-// cloud string
-// tlsCert string
-// expectError bool
-// errorMsg string
-// }{
-// {
-// name: "Valid: AzureUSSecretCloud with tls_cert",
-// cloud: "AzureUSSecretCloud",
-// tlsCert: "test-certificate-data",
-// expectError: false,
-// },
-// {
-// name: "Valid: AzureUSSecretCloud without tls_cert",
-// cloud: "AzureUSSecretCloud",
-// tlsCert: "",
-// expectError: false,
-// },
-// {
-// name: "Valid: AzurePublicCloud without tls_cert",
-// cloud: "AzurePublicCloud",
-// tlsCert: "",
-// expectError: false,
-// },
-// {
-// name: "Invalid: AzurePublicCloud with tls_cert",
-// cloud: "AzurePublicCloud",
-// tlsCert: "test-certificate-data",
-// expectError: true,
-// errorMsg: "tls_cert can only be set when cloud is 'AzureUSSecretCloud', but cloud is set to 'AzurePublicCloud'",
-// },
-// {
-// name: "Invalid: AzureUSGovernmentCloud with tls_cert",
-// cloud: "AzureUSGovernmentCloud",
-// tlsCert: "test-certificate-data",
-// expectError: true,
-// errorMsg: "tls_cert can only be set when cloud is 'AzureUSSecretCloud', but cloud is set to 'AzureUSGovernmentCloud'",
-// },
-// }
-
-// for _, tt := range tests {
-// t.Run(tt.name, func(t *testing.T) {
-// rd := resourceCloudAccountAzure().TestResourceData()
-// rd.Set("cloud", tt.cloud)
-// rd.Set("tls_cert", tt.tlsCert)
-
-// err := validateTlsCertConfiguration(rd)
-
-// if tt.expectError {
-// assert.Error(t, err)
-// assert.Equal(t, tt.errorMsg, err.Error())
-// } else {
-// assert.NoError(t, err)
-// }
-// })
-// }
-// }
-
-// // Test for toAzureAccount with TLS certificate
-// func TestToAzureAccountWithTlsCert(t *testing.T) {
-// rd := resourceCloudAccountAzure().TestResourceData()
-// rd.Set("name", "azure_unit_test_acc")
-// rd.Set("context", "tenant")
-// rd.Set("azure_client_id", "test_client_id")
-// rd.Set("azure_client_secret", "test_client_secret")
-// rd.Set("azure_tenant_id", "test_tenant_id")
-// rd.Set("tenant_name", "test_tenant_name")
-// rd.Set("disable_properties_request", true)
-// rd.Set("private_cloud_gateway_id", "12345")
-// rd.Set("cloud", "AzureUSSecretCloud")
-// rd.Set("tls_cert", "test-certificate-data")
-
-// acc := toAzureAccount(rd)
-
-// assert.Equal(t, rd.Get("name"), acc.Metadata.Name)
-// assert.Equal(t, "tenant", acc.Metadata.Annotations["scope"])
-// assert.Equal(t, rd.Get("azure_client_id"), *acc.Spec.ClientID)
-// assert.Equal(t, rd.Get("azure_client_secret"), *acc.Spec.ClientSecret)
-// assert.Equal(t, rd.Get("azure_tenant_id"), *acc.Spec.TenantID)
-// assert.Equal(t, rd.Get("tenant_name"), acc.Spec.TenantName)
-// assert.Equal(t, rd.Get("disable_properties_request"), acc.Spec.Settings.DisablePropertiesRequest)
-// assert.Equal(t, rd.Get("private_cloud_gateway_id"), acc.Metadata.Annotations[OverlordUID])
-// assert.Equal(t, rd.Get("cloud"), *acc.Spec.AzureEnvironment)
-// assert.Equal(t, rd.Id(), acc.Metadata.UID)
-// // Test TLS configuration
-// assert.NotNil(t, acc.Spec.TLS)
-// assert.Equal(t, "test-certificate-data", acc.Spec.TLS.Cert)
-// }
-
-// // Test for toAzureAccount without TLS certificate
-// func TestToAzureAccountWithoutTlsCert(t *testing.T) {
-// rd := resourceCloudAccountAzure().TestResourceData()
-// rd.Set("name", "azure_unit_test_acc")
-// rd.Set("context", "tenant")
-// rd.Set("azure_client_id", "test_client_id")
-// rd.Set("azure_client_secret", "test_client_secret")
-// rd.Set("azure_tenant_id", "test_tenant_id")
-// rd.Set("tenant_name", "test_tenant_name")
-// rd.Set("disable_properties_request", true)
-// rd.Set("private_cloud_gateway_id", "12345")
-// rd.Set("cloud", "AzurePublicCloud")
-// rd.Set("tls_cert", "") // Empty TLS cert
-
-// acc := toAzureAccount(rd)
-
-// assert.Equal(t, rd.Get("name"), acc.Metadata.Name)
-// assert.Equal(t, rd.Get("cloud"), *acc.Spec.AzureEnvironment)
-// // Test TLS configuration should be nil when tls_cert is empty
-// assert.Nil(t, acc.Spec.TLS)
-// }
-
-// // Test for flattenCloudAccountAzure with TLS certificate
-// func TestFlattenCloudAccountAzureWithTlsCert(t *testing.T) {
-// rd := resourceCloudAccountAzure().TestResourceData()
-// account := &models.V1AzureAccount{
-// Metadata: &models.V1ObjectMeta{
-// Name: "test_account",
-// Annotations: map[string]string{OverlordUID: "12345"},
-// UID: "abcdef",
-// },
-// Spec: &models.V1AzureCloudAccount{
-// ClientID: types.Ptr("test_client_id"),
-// ClientSecret: types.Ptr("test_client_secret"),
-// TenantID: types.Ptr("test_tenant_id"),
-// TenantName: "test_tenant_name",
-// Settings: &models.V1CloudAccountSettings{
-// DisablePropertiesRequest: true,
-// },
-// AzureEnvironment: types.Ptr("AzureUSSecretCloud"),
-// TLS: &models.V1AzureSecretTLSConfig{
-// Cert: "test-certificate-data",
-// },
-// },
-// }
-
-// diags, hasError := flattenCloudAccountAzure(rd, account)
-
-// assert.Nil(t, diags)
-// assert.False(t, hasError)
-// assert.Equal(t, "test_account", rd.Get("name"))
-// assert.Equal(t, "12345", rd.Get("private_cloud_gateway_id"))
-// assert.Equal(t, "test_client_id", rd.Get("azure_client_id"))
-// assert.Equal(t, "test_tenant_id", rd.Get("azure_tenant_id"))
-// assert.Equal(t, "test_tenant_name", rd.Get("tenant_name"))
-// assert.Equal(t, true, rd.Get("disable_properties_request"))
-// assert.Equal(t, "AzureUSSecretCloud", rd.Get("cloud"))
-// assert.Equal(t, "test-certificate-data", rd.Get("tls_cert"))
-// }
-
-// // Test for flattenCloudAccountAzure without TLS certificate
-// func TestFlattenCloudAccountAzureWithoutTlsCert(t *testing.T) {
-// rd := resourceCloudAccountAzure().TestResourceData()
-// account := &models.V1AzureAccount{
-// Metadata: &models.V1ObjectMeta{
-// Name: "test_account",
-// Annotations: map[string]string{OverlordUID: "12345"},
-// UID: "abcdef",
-// },
-// Spec: &models.V1AzureCloudAccount{
-// ClientID: types.Ptr("test_client_id"),
-// ClientSecret: types.Ptr("test_client_secret"),
-// TenantID: types.Ptr("test_tenant_id"),
-// TenantName: "test_tenant_name",
-// Settings: &models.V1CloudAccountSettings{
-// DisablePropertiesRequest: true,
-// },
-// AzureEnvironment: types.Ptr("AzurePublicCloud"),
-// TLS: nil, // No TLS config
-// },
-// }
-
-// diags, hasError := flattenCloudAccountAzure(rd, account)
-
-// assert.Nil(t, diags)
-// assert.False(t, hasError)
-// assert.Equal(t, "test_account", rd.Get("name"))
-// assert.Equal(t, "AzurePublicCloud", rd.Get("cloud"))
-// // tls_cert should not be set when TLS config is nil
-// assert.Equal(t, "", rd.Get("tls_cert"))
-// }
-
-// // Test Create function with invalid TLS cert configuration
-// func TestResourceCloudAccountAzureCreateWithInvalidTlsCert(t *testing.T) {
-// rd := resourceCloudAccountAzure().TestResourceData()
-// rd.Set("name", "test-azure-account")
-// rd.Set("context", "project")
-// rd.Set("azure_tenant_id", "tenant-azure-id")
-// rd.Set("azure_client_id", "azure-client-id")
-// rd.Set("azure_client_secret", "test-client-secret")
-// rd.Set("cloud", "AzurePublicCloud")
-// rd.Set("tls_cert", "invalid-cert-for-public-cloud") // This should fail validation
-
-// ctx := context.Background()
-// diags := resourceCloudAccountAzureCreate(ctx, rd, unitTestMockAPIClient)
-
-// assert.Len(t, diags, 1)
-// assert.True(t, diags.HasError())
-// assert.Contains(t, diags[0].Summary, "tls_cert can only be set when cloud is 'AzureUSSecretCloud'")
-// }
-
-// // Test Update function with invalid TLS cert configuration
-// func TestResourceCloudAccountAzureUpdateWithInvalidTlsCert(t *testing.T) {
-// rd := resourceCloudAccountAzure().TestResourceData()
-// rd.SetId("test-azure-account-id")
-// rd.Set("name", "test-azure-account")
-// rd.Set("context", "project")
-// rd.Set("azure_tenant_id", "tenant-azure-id")
-// rd.Set("azure_client_id", "azure-client-id")
-// rd.Set("azure_client_secret", "test-client-secret")
-// rd.Set("cloud", "AzureUSGovernmentCloud")
-// rd.Set("tls_cert", "invalid-cert-for-gov-cloud") // This should fail validation
-
-// ctx := context.Background()
-// diags := resourceCloudAccountAzureUpdate(ctx, rd, unitTestMockAPIClient)
-
-// assert.Len(t, diags, 1)
-// assert.True(t, diags.HasError())
-// assert.Contains(t, diags[0].Summary, "tls_cert can only be set when cloud is 'AzureUSSecretCloud'")
-// }
+// Test for validateTlsCertConfiguration function
+func TestValidateTlsCertConfiguration(t *testing.T) {
+ tests := []struct {
+ name string
+ cloud string
+ tlsCert string
+ expectError bool
+ errorMsg string
+ }{
+ {
+ name: "Valid: AzureUSSecretCloud with tls_cert",
+ cloud: "AzureUSSecretCloud",
+ tlsCert: "test-certificate-data",
+ expectError: false,
+ },
+ {
+ name: "Valid: AzureUSSecretCloud without tls_cert",
+ cloud: "AzureUSSecretCloud",
+ tlsCert: "",
+ expectError: false,
+ },
+ {
+ name: "Valid: AzurePublicCloud without tls_cert",
+ cloud: "AzurePublicCloud",
+ tlsCert: "",
+ expectError: false,
+ },
+ {
+ name: "Invalid: AzurePublicCloud with tls_cert",
+ cloud: "AzurePublicCloud",
+ tlsCert: "test-certificate-data",
+ expectError: true,
+ errorMsg: "tls_cert can only be set when cloud is 'AzureUSSecretCloud', but cloud is set to 'AzurePublicCloud'",
+ },
+ {
+ name: "Invalid: AzureUSGovernmentCloud with tls_cert",
+ cloud: "AzureUSGovernmentCloud",
+ tlsCert: "test-certificate-data",
+ expectError: true,
+ errorMsg: "tls_cert can only be set when cloud is 'AzureUSSecretCloud', but cloud is set to 'AzureUSGovernmentCloud'",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ rd := resourceCloudAccountAzure().TestResourceData()
+ rd.Set("cloud", tt.cloud)
+ rd.Set("tls_cert", tt.tlsCert)
+
+ err := validateTlsCertConfiguration(rd)
+
+ if tt.expectError {
+ assert.Error(t, err)
+ assert.Equal(t, tt.errorMsg, err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+// Test for toAzureAccount with TLS certificate
+func TestToAzureAccountWithTlsCert(t *testing.T) {
+ rd := resourceCloudAccountAzure().TestResourceData()
+ rd.Set("name", "azure_unit_test_acc")
+ rd.Set("context", "tenant")
+ rd.Set("azure_client_id", "test_client_id")
+ rd.Set("azure_client_secret", "test_client_secret")
+ rd.Set("azure_tenant_id", "test_tenant_id")
+ rd.Set("tenant_name", "test_tenant_name")
+ rd.Set("disable_properties_request", true)
+ rd.Set("private_cloud_gateway_id", "12345")
+ rd.Set("cloud", "AzureUSSecretCloud")
+ rd.Set("tls_cert", "test-certificate-data")
+
+ acc := toAzureAccount(rd)
+
+ assert.Equal(t, rd.Get("name"), acc.Metadata.Name)
+ assert.Equal(t, "tenant", acc.Metadata.Annotations["scope"])
+ assert.Equal(t, rd.Get("azure_client_id"), *acc.Spec.ClientID)
+ assert.Equal(t, rd.Get("azure_client_secret"), *acc.Spec.ClientSecret)
+ assert.Equal(t, rd.Get("azure_tenant_id"), *acc.Spec.TenantID)
+ assert.Equal(t, rd.Get("tenant_name"), acc.Spec.TenantName)
+ assert.Equal(t, rd.Get("disable_properties_request"), acc.Spec.Settings.DisablePropertiesRequest)
+ assert.Equal(t, rd.Get("private_cloud_gateway_id"), acc.Metadata.Annotations[OverlordUID])
+ assert.Equal(t, rd.Get("cloud"), *acc.Spec.AzureEnvironment)
+ assert.Equal(t, rd.Id(), acc.Metadata.UID)
+ // Test TLS configuration
+ assert.NotNil(t, acc.Spec.TLS)
+ assert.Equal(t, "test-certificate-data", acc.Spec.TLS.Cert)
+}
+
+// Test for toAzureAccount without TLS certificate
+func TestToAzureAccountWithoutTlsCert(t *testing.T) {
+ rd := resourceCloudAccountAzure().TestResourceData()
+ rd.Set("name", "azure_unit_test_acc")
+ rd.Set("context", "tenant")
+ rd.Set("azure_client_id", "test_client_id")
+ rd.Set("azure_client_secret", "test_client_secret")
+ rd.Set("azure_tenant_id", "test_tenant_id")
+ rd.Set("tenant_name", "test_tenant_name")
+ rd.Set("disable_properties_request", true)
+ rd.Set("private_cloud_gateway_id", "12345")
+ rd.Set("cloud", "AzurePublicCloud")
+ rd.Set("tls_cert", "") // Empty TLS cert
+
+ acc := toAzureAccount(rd)
+
+ assert.Equal(t, rd.Get("name"), acc.Metadata.Name)
+ assert.Equal(t, rd.Get("cloud"), *acc.Spec.AzureEnvironment)
+ // Test TLS configuration should be nil when tls_cert is empty
+ assert.Nil(t, acc.Spec.TLS)
+}
+
+// Test for flattenCloudAccountAzure with TLS certificate
+func TestFlattenCloudAccountAzureWithTlsCert(t *testing.T) {
+ rd := resourceCloudAccountAzure().TestResourceData()
+ account := &models.V1AzureAccount{
+ Metadata: &models.V1ObjectMeta{
+ Name: "test_account",
+ Annotations: map[string]string{OverlordUID: "12345"},
+ UID: "abcdef",
+ },
+ Spec: &models.V1AzureCloudAccount{
+ ClientID: types.Ptr("test_client_id"),
+ ClientSecret: types.Ptr("test_client_secret"),
+ TenantID: types.Ptr("test_tenant_id"),
+ TenantName: "test_tenant_name",
+ Settings: &models.V1CloudAccountSettings{
+ DisablePropertiesRequest: true,
+ },
+ AzureEnvironment: types.Ptr("AzureUSSecretCloud"),
+ TLS: &models.V1AzureSecretTLSConfig{
+ Cert: "test-certificate-data",
+ },
+ },
+ }
+
+ diags, hasError := flattenCloudAccountAzure(rd, account)
+
+ assert.Nil(t, diags)
+ assert.False(t, hasError)
+ assert.Equal(t, "test_account", rd.Get("name"))
+ assert.Equal(t, "12345", rd.Get("private_cloud_gateway_id"))
+ assert.Equal(t, "test_client_id", rd.Get("azure_client_id"))
+ assert.Equal(t, "test_tenant_id", rd.Get("azure_tenant_id"))
+ assert.Equal(t, "test_tenant_name", rd.Get("tenant_name"))
+ assert.Equal(t, true, rd.Get("disable_properties_request"))
+ assert.Equal(t, "AzureUSSecretCloud", rd.Get("cloud"))
+ assert.Equal(t, "test-certificate-data", rd.Get("tls_cert"))
+}
+
+// Test for flattenCloudAccountAzure without TLS certificate
+func TestFlattenCloudAccountAzureWithoutTlsCert(t *testing.T) {
+ rd := resourceCloudAccountAzure().TestResourceData()
+ account := &models.V1AzureAccount{
+ Metadata: &models.V1ObjectMeta{
+ Name: "test_account",
+ Annotations: map[string]string{OverlordUID: "12345"},
+ UID: "abcdef",
+ },
+ Spec: &models.V1AzureCloudAccount{
+ ClientID: types.Ptr("test_client_id"),
+ ClientSecret: types.Ptr("test_client_secret"),
+ TenantID: types.Ptr("test_tenant_id"),
+ TenantName: "test_tenant_name",
+ Settings: &models.V1CloudAccountSettings{
+ DisablePropertiesRequest: true,
+ },
+ AzureEnvironment: types.Ptr("AzurePublicCloud"),
+ TLS: nil, // No TLS config
+ },
+ }
+
+ diags, hasError := flattenCloudAccountAzure(rd, account)
+
+ assert.Nil(t, diags)
+ assert.False(t, hasError)
+ assert.Equal(t, "test_account", rd.Get("name"))
+ assert.Equal(t, "AzurePublicCloud", rd.Get("cloud"))
+ // tls_cert should not be set when TLS config is nil
+ assert.Equal(t, "", rd.Get("tls_cert"))
+}
+
+// Test Create function with invalid TLS cert configuration
+func TestResourceCloudAccountAzureCreateWithInvalidTlsCert(t *testing.T) {
+ rd := resourceCloudAccountAzure().TestResourceData()
+ rd.Set("name", "test-azure-account")
+ rd.Set("context", "project")
+ rd.Set("azure_tenant_id", "tenant-azure-id")
+ rd.Set("azure_client_id", "azure-client-id")
+ rd.Set("azure_client_secret", "test-client-secret")
+ rd.Set("cloud", "AzurePublicCloud")
+ rd.Set("tls_cert", "invalid-cert-for-public-cloud") // This should fail validation
+
+ ctx := context.Background()
+ diags := resourceCloudAccountAzureCreate(ctx, rd, unitTestMockAPIClient)
+
+ assert.Len(t, diags, 1)
+ assert.True(t, diags.HasError())
+ assert.Contains(t, diags[0].Summary, "tls_cert can only be set when cloud is 'AzureUSSecretCloud'")
+}
+
+// Test Update function with invalid TLS cert configuration
+func TestResourceCloudAccountAzureUpdateWithInvalidTlsCert(t *testing.T) {
+ rd := resourceCloudAccountAzure().TestResourceData()
+ rd.SetId("test-azure-account-id")
+ rd.Set("name", "test-azure-account")
+ rd.Set("context", "project")
+ rd.Set("azure_tenant_id", "tenant-azure-id")
+ rd.Set("azure_client_id", "azure-client-id")
+ rd.Set("azure_client_secret", "test-client-secret")
+ rd.Set("cloud", "AzureUSGovernmentCloud")
+ rd.Set("tls_cert", "invalid-cert-for-gov-cloud") // This should fail validation
+
+ ctx := context.Background()
+ diags := resourceCloudAccountAzureUpdate(ctx, rd, unitTestMockAPIClient)
+
+ assert.Len(t, diags, 1)
+ assert.True(t, diags.HasError())
+ assert.Contains(t, diags[0].Summary, "tls_cert can only be set when cloud is 'AzureUSSecretCloud'")
+}
diff --git a/spectrocloud/resource_cluster_aks.go b/spectrocloud/resource_cluster_aks.go
index e980b76f7..2d9f968f1 100644
--- a/spectrocloud/resource_cluster_aks.go
+++ b/spectrocloud/resource_cluster_aks.go
@@ -334,7 +334,7 @@ func resourceClusterAksCreate(ctx context.Context, d *schema.ResourceData, m int
resourceClusterAksRead(ctx, d, m)
- return diags
+ return diagnostics
}
//goland:noinspection GoUnhandledErrorResult
@@ -683,22 +683,22 @@ func toMachinePoolAks(machinePool interface{}) *models.V1AzureMachinePoolConfigE
labels = append(labels, "worker")
}
- min := int32(m["count"].(int))
- max := int32(m["count"].(int))
+ min := SafeInt32(m["count"].(int))
+ max := SafeInt32(m["count"].(int))
if m["min"] != nil {
- min = int32(m["min"].(int))
+ min = SafeInt32(m["min"].(int))
}
if m["max"] != nil {
- max = int32(m["max"].(int))
+ max = SafeInt32(m["max"].(int))
}
mp := &models.V1AzureMachinePoolConfigEntity{
CloudConfig: &models.V1AzureMachinePoolCloudConfigEntity{
InstanceType: m["instance_type"].(string),
OsDisk: &models.V1AzureOSDisk{
- DiskSizeGB: int32(m["disk_size_gb"].(int)),
+ DiskSizeGB: SafeInt32(m["disk_size_gb"].(int)),
ManagedDisk: &models.V1ManagedDisk{
StorageAccountType: m["storage_account_type"].(string),
},
@@ -715,7 +715,7 @@ func toMachinePoolAks(machinePool interface{}) *models.V1AzureMachinePoolConfigE
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
diff --git a/spectrocloud/resource_cluster_attachment.go b/spectrocloud/resource_cluster_attachment.go
index 0e4f60e7c..faa1d2f2e 100644
--- a/spectrocloud/resource_cluster_attachment.go
+++ b/spectrocloud/resource_cluster_attachment.go
@@ -105,7 +105,7 @@ func resourceAddonDeploymentCreate(ctx context.Context, d *schema.ResourceData,
resourceAddonDeploymentRead(ctx, d, m)
- return diags
+ return diagnostics
}
func getAddonDeploymentId(clusterUid string, clusterProfile *models.V1ClusterProfile) string {
diff --git a/spectrocloud/resource_cluster_aws.go b/spectrocloud/resource_cluster_aws.go
index 4d0c7757b..df14f356b 100644
--- a/spectrocloud/resource_cluster_aws.go
+++ b/spectrocloud/resource_cluster_aws.go
@@ -346,7 +346,7 @@ func resourceClusterAwsCreate(ctx context.Context, d *schema.ResourceData, m int
resourceClusterAwsRead(ctx, d, m)
- return diags
+ return diagnostics
}
//goland:noinspection GoUnhandledErrorResult
@@ -696,15 +696,15 @@ func toMachinePoolAws(machinePool interface{}, vpcId string) (*models.V1AwsMachi
azs = append(azs, az.(string))
}
}
- min := int32(m["count"].(int))
- max := int32(m["count"].(int))
+ min := SafeInt32(m["count"].(int))
+ max := SafeInt32(m["count"].(int))
if m["min"] != nil {
- min = int32(m["min"].(int))
+ min = SafeInt32(m["min"].(int))
}
if m["max"] != nil {
- max = int32(m["max"].(int))
+ max = SafeInt32(m["max"].(int))
}
mp := &models.V1AwsMachinePoolConfigEntity{
@@ -712,7 +712,7 @@ func toMachinePoolAws(machinePool interface{}, vpcId string) (*models.V1AwsMachi
Azs: azs,
InstanceType: types.Ptr(m["instance_type"].(string)),
CapacityType: &capacityType,
- RootDeviceSize: int64(m["disk_size_gb"].(int)),
+ RootDeviceSize: SafeInt64(m["disk_size_gb"].(int)),
Subnets: azSubnetsConfigs,
},
PoolConfig: &models.V1MachinePoolConfigEntity{
@@ -721,7 +721,7 @@ func toMachinePoolAws(machinePool interface{}, vpcId string) (*models.V1AwsMachi
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
@@ -736,7 +736,7 @@ func toMachinePoolAws(machinePool interface{}, vpcId string) (*models.V1AwsMachi
if m["node_repave_interval"] != nil {
nodeRepaveInterval = m["node_repave_interval"].(int)
}
- mp.PoolConfig.NodeRepaveInterval = int32(nodeRepaveInterval)
+ mp.PoolConfig.NodeRepaveInterval = SafeInt32(nodeRepaveInterval)
} else {
err := ValidationNodeRepaveIntervalForControlPlane(m["node_repave_interval"].(int))
if err != nil {
diff --git a/spectrocloud/resource_cluster_azure.go b/spectrocloud/resource_cluster_azure.go
index a4af9e394..657dcc5c6 100644
--- a/spectrocloud/resource_cluster_azure.go
+++ b/spectrocloud/resource_cluster_azure.go
@@ -399,7 +399,7 @@ func resourceClusterAzureCreate(ctx context.Context, d *schema.ResourceData, m i
resourceClusterAzureRead(ctx, d, m)
- return diags
+ return diagnostics
}
//goland:noinspection GoUnhandledErrorResult
@@ -797,7 +797,7 @@ func toMachinePoolAzure(machinePool interface{}) (*models.V1AzureMachinePoolConf
Azs: azs,
InstanceType: m["instance_type"].(string),
OsDisk: &models.V1AzureOSDisk{
- DiskSizeGB: int32(diskSize),
+ DiskSizeGB: SafeInt32(diskSize),
ManagedDisk: &models.V1ManagedDisk{
StorageAccountType: diskType,
},
@@ -814,7 +814,7 @@ func toMachinePoolAzure(machinePool interface{}) (*models.V1AzureMachinePoolConf
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
@@ -827,7 +827,7 @@ func toMachinePoolAzure(machinePool interface{}) (*models.V1AzureMachinePoolConf
if m["node_repave_interval"] != nil {
nodeRepaveInterval = m["node_repave_interval"].(int)
}
- mp.PoolConfig.NodeRepaveInterval = int32(nodeRepaveInterval)
+ mp.PoolConfig.NodeRepaveInterval = SafeInt32(nodeRepaveInterval)
} else {
err := ValidationNodeRepaveIntervalForControlPlane(m["node_repave_interval"].(int))
if err != nil {
diff --git a/spectrocloud/resource_cluster_custom_cloud.go b/spectrocloud/resource_cluster_custom_cloud.go
index 96c6f8c48..16dfd0995 100644
--- a/spectrocloud/resource_cluster_custom_cloud.go
+++ b/spectrocloud/resource_cluster_custom_cloud.go
@@ -286,7 +286,7 @@ func resourceClusterCustomCloudCreate(ctx context.Context, d *schema.ResourceDat
resourceClusterCustomCloudRead(ctx, d, m)
- return diags
+ return diagnostics
}
func resourceClusterCustomCloudRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
diff --git a/spectrocloud/resource_cluster_edge_native.go b/spectrocloud/resource_cluster_edge_native.go
index 934aabdeb..0c19e6c2c 100644
--- a/spectrocloud/resource_cluster_edge_native.go
+++ b/spectrocloud/resource_cluster_edge_native.go
@@ -334,9 +334,9 @@ func resourceClusterEdgeNativeCreate(ctx context.Context, d *schema.ResourceData
return diagnostics
}
- diags = resourceClusterEdgeNativeRead(ctx, d, m)
+ resourceClusterEdgeNativeRead(ctx, d, m)
- return diags
+ return diagnostics
}
//goland:noinspection GoUnhandledErrorResult
@@ -690,7 +690,7 @@ func toMachinePoolEdgeNative(machinePool interface{}) (*models.V1EdgeNativeMachi
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(len(cloudConfig.EdgeHosts))),
+ Size: types.Ptr(SafeInt32(len(cloudConfig.EdgeHosts))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
@@ -703,7 +703,7 @@ func toMachinePoolEdgeNative(machinePool interface{}) (*models.V1EdgeNativeMachi
nodeRepaveInterval = m["node_repave_interval"].(int)
}
if !controlPlane {
- mp.PoolConfig.NodeRepaveInterval = int32(nodeRepaveInterval)
+ mp.PoolConfig.NodeRepaveInterval = SafeInt32(nodeRepaveInterval)
} else {
err := ValidationNodeRepaveIntervalForControlPlane(nodeRepaveInterval)
if err != nil {
diff --git a/spectrocloud/resource_cluster_edge_vsphere.go b/spectrocloud/resource_cluster_edge_vsphere.go
index ef06c26e2..6dc9ea681 100644
--- a/spectrocloud/resource_cluster_edge_vsphere.go
+++ b/spectrocloud/resource_cluster_edge_vsphere.go
@@ -334,7 +334,7 @@ func resourceClusterEdgeVsphereCreate(ctx context.Context, d *schema.ResourceDat
resourceClusterEdgeVsphereRead(ctx, d, m)
- return diags
+ return diagnostics
}
func resourceClusterEdgeVsphereRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
@@ -622,9 +622,9 @@ func toMachinePoolEdgeVsphere(machinePool interface{}) (*models.V1VsphereMachine
ins := m["instance_type"].([]interface{})[0].(map[string]interface{})
instanceType := models.V1VsphereInstanceType{
- DiskGiB: types.Ptr(int32(ins["disk_size_gb"].(int))),
- MemoryMiB: types.Ptr(int64(ins["memory_mb"].(int))),
- NumCPUs: types.Ptr(int32(ins["cpu"].(int))),
+ DiskGiB: types.Ptr(SafeInt32(ins["disk_size_gb"].(int))),
+ MemoryMiB: types.Ptr(SafeInt64(ins["memory_mb"].(int))),
+ NumCPUs: types.Ptr(SafeInt32(ins["cpu"].(int))),
}
mp := &models.V1VsphereMachinePoolConfigEntity{
@@ -638,7 +638,7 @@ func toMachinePoolEdgeVsphere(machinePool interface{}) (*models.V1VsphereMachine
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
@@ -651,7 +651,7 @@ func toMachinePoolEdgeVsphere(machinePool interface{}) (*models.V1VsphereMachine
if m["node_repave_interval"] != nil {
nodeRepaveInterval = m["node_repave_interval"].(int)
}
- mp.PoolConfig.NodeRepaveInterval = int32(nodeRepaveInterval)
+ mp.PoolConfig.NodeRepaveInterval = SafeInt32(nodeRepaveInterval)
} else {
err := ValidationNodeRepaveIntervalForControlPlane(m["node_repave_interval"].(int))
if err != nil {
diff --git a/spectrocloud/resource_cluster_eks.go b/spectrocloud/resource_cluster_eks.go
index 6c824081e..cf9da9287 100644
--- a/spectrocloud/resource_cluster_eks.go
+++ b/spectrocloud/resource_cluster_eks.go
@@ -411,7 +411,7 @@ func resourceClusterEksCreate(ctx context.Context, d *schema.ResourceData, m int
resourceClusterEksRead(ctx, d, m)
- return diags
+ return diagnostics
}
func resourceClusterEksRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
@@ -896,15 +896,15 @@ func toMachinePoolEks(machinePool interface{}) *models.V1EksMachinePoolConfigEnt
capacityType = m["capacity_type"].(string)
}
- min := int32(m["count"].(int))
- max := int32(m["count"].(int))
+ min := SafeInt32(m["count"].(int))
+ max := SafeInt32(m["count"].(int))
if m["min"] != nil {
- min = int32(m["min"].(int))
+ min = SafeInt32(m["min"].(int))
}
if m["max"] != nil {
- max = int32(m["max"].(int))
+ max = SafeInt32(m["max"].(int))
}
instanceType := ""
if val, ok := m["instance_type"]; ok {
@@ -914,9 +914,9 @@ func toMachinePoolEks(machinePool interface{}) *models.V1EksMachinePoolConfigEnt
if val, ok := m["ami_type"]; ok {
amiType = val.(string)
}
- diskSizeGb := int64(0)
+ diskSizeGb := SafeInt64(0)
if dVal, ok := m["disk_size_gb"]; ok {
- diskSizeGb = int64(dVal.(int))
+ diskSizeGb = SafeInt64(dVal.(int))
}
mp := &models.V1EksMachinePoolConfigEntity{
CloudConfig: &models.V1EksMachineCloudConfigEntity{
@@ -933,7 +933,7 @@ func toMachinePoolEks(machinePool interface{}) *models.V1EksMachinePoolConfigEnt
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
diff --git a/spectrocloud/resource_cluster_gcp.go b/spectrocloud/resource_cluster_gcp.go
index a3aa2a4c5..d7aa41054 100644
--- a/spectrocloud/resource_cluster_gcp.go
+++ b/spectrocloud/resource_cluster_gcp.go
@@ -279,7 +279,7 @@ func resourceClusterGcpCreate(ctx context.Context, d *schema.ResourceData, m int
resourceClusterGcpRead(ctx, d, m)
- return diags
+ return diagnostics
}
//goland:noinspection GoUnhandledErrorResult
@@ -541,7 +541,7 @@ func toMachinePoolGcp(machinePool interface{}) (*models.V1GcpMachinePoolConfigEn
CloudConfig: &models.V1GcpMachinePoolCloudConfigEntity{
Azs: azs,
InstanceType: types.Ptr(m["instance_type"].(string)),
- RootDeviceSize: int64(m["disk_size_gb"].(int)),
+ RootDeviceSize: SafeInt64(m["disk_size_gb"].(int)),
},
PoolConfig: &models.V1MachinePoolConfigEntity{
AdditionalLabels: toAdditionalNodePoolLabels(m),
@@ -549,7 +549,7 @@ func toMachinePoolGcp(machinePool interface{}) (*models.V1GcpMachinePoolConfigEn
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
@@ -562,7 +562,7 @@ func toMachinePoolGcp(machinePool interface{}) (*models.V1GcpMachinePoolConfigEn
if m["node_repave_interval"] != nil {
nodeRepaveInterval = m["node_repave_interval"].(int)
}
- mp.PoolConfig.NodeRepaveInterval = int32(nodeRepaveInterval)
+ mp.PoolConfig.NodeRepaveInterval = SafeInt32(nodeRepaveInterval)
} else {
err := ValidationNodeRepaveIntervalForControlPlane(m["node_repave_interval"].(int))
if err != nil {
diff --git a/spectrocloud/resource_cluster_gke.go b/spectrocloud/resource_cluster_gke.go
index 107af7d88..2cfe3ab2c 100644
--- a/spectrocloud/resource_cluster_gke.go
+++ b/spectrocloud/resource_cluster_gke.go
@@ -251,7 +251,7 @@ func resourceClusterGkeCreate(ctx context.Context, d *schema.ResourceData, m int
}
resourceClusterGkeRead(ctx, d, m)
- return diags
+ return diagnostics
}
func resourceClusterGkeRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
@@ -488,13 +488,13 @@ func toMachinePoolGke(machinePool interface{}) (*models.V1GcpMachinePoolConfigEn
mp := &models.V1GcpMachinePoolConfigEntity{
CloudConfig: &models.V1GcpMachinePoolCloudConfigEntity{
InstanceType: types.Ptr(m["instance_type"].(string)),
- RootDeviceSize: int64(m["disk_size_gb"].(int)),
+ RootDeviceSize: SafeInt64(m["disk_size_gb"].(int)),
},
PoolConfig: &models.V1MachinePoolConfigEntity{
AdditionalLabels: toAdditionalNodePoolLabels(m),
Taints: toClusterTaints(m),
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
diff --git a/spectrocloud/resource_cluster_group.go b/spectrocloud/resource_cluster_group.go
index 1d8bc2adc..0bc091479 100644
--- a/spectrocloud/resource_cluster_group.go
+++ b/spectrocloud/resource_cluster_group.go
@@ -387,10 +387,10 @@ func toClusterGroupLimitConfig(resources map[string]interface{}) *models.V1Clust
ret := &models.V1ClusterGroupLimitConfig{
- CPUMilliCore: int32(cpu_milli),
- MemoryMiB: int32(mem_in_mb),
- StorageGiB: int32(storage_in_gb),
- OverSubscription: int32(oversubscription),
+ CPUMilliCore: SafeInt32(cpu_milli),
+ MemoryMiB: SafeInt32(mem_in_mb),
+ StorageGiB: SafeInt32(storage_in_gb),
+ OverSubscription: SafeInt32(oversubscription),
}
return ret
diff --git a/spectrocloud/resource_cluster_maas.go b/spectrocloud/resource_cluster_maas.go
index ba46148de..8120d8ae2 100644
--- a/spectrocloud/resource_cluster_maas.go
+++ b/spectrocloud/resource_cluster_maas.go
@@ -145,6 +145,12 @@ func resourceClusterMaas() *schema.Resource {
Required: true,
Description: "Domain name in which the cluster to be provisioned.",
},
+ "enable_lxd_vm": {
+ Type: schema.TypeBool,
+ Optional: true,
+ Default: false,
+ Description: "Whether to enable LXD VM. Default is `false`.",
+ },
},
},
},
@@ -268,6 +274,37 @@ func resourceClusterMaas() *schema.Resource {
},
},
},
+ "use_lxd_vm": {
+ Type: schema.TypeBool,
+ Optional: true,
+ Default: false,
+ Description: "Whether to use LXD VM. Default is `false`.",
+ },
+ "network": {
+ Type: schema.TypeList,
+ Required: true,
+ MaxItems: 1,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "network_name": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The name of the network in which VMs are created/located.",
+ },
+ "parent_pool_uid": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The UID of the parent pool which allocates IPs for this IPPool.",
+ },
+ "static_ip": {
+ Type: schema.TypeBool,
+ Optional: true,
+ Default: false,
+ Description: "Whether to use static IP. Default is `false`.",
+ },
+ },
+ },
+ },
},
},
},
@@ -324,7 +361,7 @@ func resourceClusterMaasCreate(ctx context.Context, d *schema.ResourceData, m in
resourceClusterMaasRead(ctx, d, m)
- return diags
+ return diagnostics
}
func resourceClusterMaasRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
@@ -401,6 +438,9 @@ func flattenClusterConfigsMaas(config *models.V1MaasCloudConfig) []interface{} {
if config.Spec.ClusterConfig.Domain != nil {
m["domain"] = *config.Spec.ClusterConfig.Domain
}
+ if config.Spec.ClusterConfig.EnableLxdVM {
+ m["enable_lxd_vm"] = true
+ }
return []interface{}{m}
}
@@ -444,6 +484,16 @@ func flattenMachinePoolConfigsMaas(machinePools []*models.V1MaasMachinePoolConfi
}
}
oi["node_tags"] = machinePool.Tags
+ oi["use_lxd_vm"] = machinePool.UseLxdVM
+
+ if machinePool.Network != nil {
+ network := make(map[string]interface{})
+ network["network_name"] = *machinePool.Network.NetworkName
+ network["parent_pool_uid"] = machinePool.Network.ParentPoolRef.UID
+ network["static_ip"] = machinePool.Network.StaticIP
+ oi["network"] = []interface{}{network}
+ }
+
ois[i] = oi
}
@@ -562,7 +612,8 @@ func toMaasCluster(c *client.V1Client, d *schema.ResourceData) (*models.V1Spectr
Profiles: profiles,
Policies: toPolicies(d),
CloudConfig: &models.V1MaasClusterConfig{
- Domain: &DomainVal,
+ Domain: &DomainVal,
+ EnableLxdVM: d.Get("enable_lxd_vm").(bool),
},
},
}
@@ -603,15 +654,15 @@ func toMachinePoolMaas(machinePool interface{}) (*models.V1MaasMachinePoolConfig
InstanceType := m["instance_type"].([]interface{})[0].(map[string]interface{})
log.Printf("Create machine pool %s", InstanceType)
- min := int32(m["count"].(int))
- max := int32(m["count"].(int))
+ min := SafeInt32(m["count"].(int))
+ max := SafeInt32(m["count"].(int))
if m["min"] != nil {
- min = int32(m["min"].(int))
+ min = SafeInt32(m["min"].(int))
}
if m["max"] != nil {
- max = int32(m["max"].(int))
+ max = SafeInt32(m["max"].(int))
}
var nodePoolTags []string
for _, nt := range m["node_tags"].(*schema.Set).List() {
@@ -622,10 +673,11 @@ func toMachinePoolMaas(machinePool interface{}) (*models.V1MaasMachinePoolConfig
CloudConfig: &models.V1MaasMachinePoolCloudConfigEntity{
Azs: azs,
InstanceType: &models.V1MaasInstanceType{
- MinCPU: int32(InstanceType["min_cpu"].(int)),
- MinMemInMB: int32(InstanceType["min_memory_mb"].(int)),
+ MinCPU: SafeInt32(InstanceType["min_cpu"].(int)),
+ MinMemInMB: SafeInt32(InstanceType["min_memory_mb"].(int)),
},
- Tags: nodePoolTags,
+ Tags: nodePoolTags,
+ UseLxdVM: m["use_lxd_vm"].(bool),
},
PoolConfig: &models.V1MachinePoolConfigEntity{
AdditionalLabels: toAdditionalNodePoolLabels(m),
@@ -633,7 +685,7 @@ func toMachinePoolMaas(machinePool interface{}) (*models.V1MaasMachinePoolConfig
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
@@ -642,6 +694,17 @@ func toMachinePoolMaas(machinePool interface{}) (*models.V1MaasMachinePoolConfig
MaxSize: max,
},
}
+
+ if len(m["network"].([]interface{})) > 0 {
+ network := m["network"].([]interface{})[0].(map[string]interface{})
+ net := &models.V1MaasNetworkConfigEntity{
+ NetworkName: types.Ptr(network["network_name"].(string)),
+ StaticIP: network["static_ip"].(bool),
+ ParentPoolUID: network["parent_pool_uid"].(string),
+ }
+ mp.CloudConfig.Network = net
+ }
+
if len(m["placement"].([]interface{})) > 0 {
Placement := m["placement"].([]interface{})[0].(map[string]interface{})
mp.CloudConfig.ResourcePool = types.Ptr(Placement["resource_pool"].(string))
@@ -655,7 +718,7 @@ func toMachinePoolMaas(machinePool interface{}) (*models.V1MaasMachinePoolConfig
if m["node_repave_interval"] != nil {
nodeRepaveInterval = m["node_repave_interval"].(int)
}
- mp.PoolConfig.NodeRepaveInterval = int32(nodeRepaveInterval)
+ mp.PoolConfig.NodeRepaveInterval = SafeInt32(nodeRepaveInterval)
} else {
err := ValidationNodeRepaveIntervalForControlPlane(m["node_repave_interval"].(int))
if err != nil {
diff --git a/spectrocloud/resource_cluster_mass_test.go b/spectrocloud/resource_cluster_mass_test.go
index 654ab9a89..ad4003370 100644
--- a/spectrocloud/resource_cluster_mass_test.go
+++ b/spectrocloud/resource_cluster_mass_test.go
@@ -52,6 +52,7 @@ func TestFlattenMachinePoolConfigsMaas(t *testing.T) {
Type: "RollingUpdateScaleOut",
},
UseControlPlaneAsWorker: true,
+ UseLxdVM: false,
}
mockMachinePools = append(mockMachinePools, mp)
config := &models.V1MaasClusterConfig{
@@ -77,8 +78,9 @@ func TestFlattenMachinePoolConfigsMaas(t *testing.T) {
"min_cpu": 2,
},
},
- "azs": []string{"zone1", "zone2"},
- "node_tags": []string{"test"},
+ "azs": []string{"zone1", "zone2"},
+ "node_tags": []string{"test"},
+ "use_lxd_vm": false,
"placement": []interface{}{
map[string]interface{}{
"resource_pool": "maas_resource_pool",
@@ -121,8 +123,16 @@ func TestToMachinePoolMaas(t *testing.T) {
"resource_pool": "test_resource_pool",
},
},
- "azs": schema.NewSet(schema.HashString, []interface{}{"zone1", "zone2"}),
- "node_tags": schema.NewSet(schema.HashString, []interface{}{"test"}),
+ "azs": schema.NewSet(schema.HashString, []interface{}{"zone1", "zone2"}),
+ "node_tags": schema.NewSet(schema.HashString, []interface{}{"test"}),
+ "use_lxd_vm": false,
+ "network": []interface{}{
+ map[string]interface{}{
+ "network_name": "test_network",
+ "parent_pool_uid": "test_pool_uid",
+ "static_ip": false,
+ },
+ },
}
rp := "test_resource_pool"
size := int32(2)
@@ -133,6 +143,12 @@ func TestToMachinePoolMaas(t *testing.T) {
InstanceType: &models.V1MaasInstanceType{MinCPU: 2, MinMemInMB: 500},
ResourcePool: &rp,
Tags: []string{"test"},
+ UseLxdVM: false,
+ Network: &models.V1MaasNetworkConfigEntity{
+ NetworkName: types.Ptr("test_network"),
+ ParentPoolUID: "test_pool_uid",
+ StaticIP: false,
+ },
},
PoolConfig: &models.V1MachinePoolConfigEntity{
AdditionalLabels: map[string]string{"TF": "test_label"},
diff --git a/spectrocloud/resource_cluster_openstack.go b/spectrocloud/resource_cluster_openstack.go
index 88996d73f..5d3d9d786 100644
--- a/spectrocloud/resource_cluster_openstack.go
+++ b/spectrocloud/resource_cluster_openstack.go
@@ -299,7 +299,7 @@ func resourceClusterOpenStackCreate(ctx context.Context, d *schema.ResourceData,
resourceClusterOpenStackRead(ctx, d, m)
- return diags
+ return diagnostics
}
func toOpenStackCluster(c *client.V1Client, d *schema.ResourceData) (*models.V1SpectroOpenStackClusterEntity, error) {
@@ -612,7 +612,7 @@ func toMachinePoolOpenStack(machinePool interface{}) (*models.V1OpenStackMachine
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
@@ -625,7 +625,7 @@ func toMachinePoolOpenStack(machinePool interface{}) (*models.V1OpenStackMachine
if m["node_repave_interval"] != nil {
nodeRepaveInterval = m["node_repave_interval"].(int)
}
- mp.PoolConfig.NodeRepaveInterval = int32(nodeRepaveInterval)
+ mp.PoolConfig.NodeRepaveInterval = SafeInt32(nodeRepaveInterval)
} else {
err := ValidationNodeRepaveIntervalForControlPlane(m["node_repave_interval"].(int))
if err != nil {
diff --git a/spectrocloud/resource_cluster_profile_import_feature.go b/spectrocloud/resource_cluster_profile_import_feature.go
index 6e6fbe356..78700b796 100644
--- a/spectrocloud/resource_cluster_profile_import_feature.go
+++ b/spectrocloud/resource_cluster_profile_import_feature.go
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"os"
+ "path/filepath"
+ "strings"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
@@ -57,6 +59,11 @@ func resourceClusterProfileImportFeatureCreate(ctx context.Context, d *schema.Re
func toClusterProfileImportCreate(d *schema.ResourceData) (*os.File, error) {
importFilePath := d.Get("import_file").(string)
+ // Validate file path to prevent directory traversal attacks
+ if !isValidFilePath(importFilePath) {
+ return nil, fmt.Errorf("invalid file path: %s", importFilePath)
+ }
+ // #nosec G304
importFile, err := os.Open(importFilePath)
if err != nil {
return nil, fmt.Errorf("error opening import file: %s", err)
@@ -71,6 +78,28 @@ func toClusterProfileImportCreate(d *schema.ResourceData) (*os.File, error) {
return importFile, nil
}
+// isValidFilePath checks if the file path is safe and doesn't contain directory traversal attempts
+func isValidFilePath(filePath string) bool {
+ // Check for directory traversal patterns
+ if strings.Contains(filePath, "..") || strings.Contains(filePath, "//") {
+ return false
+ }
+
+ // Ensure the path is absolute or relative to current directory
+ absPath, err := filepath.Abs(filePath)
+ if err != nil {
+ return false
+ }
+
+ // Check if the resolved path is within the current working directory
+ cwd, err := os.Getwd()
+ if err != nil {
+ return false
+ }
+
+ return strings.HasPrefix(absPath, cwd)
+}
+
func resourceClusterProfileImportFeatureRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
resourceContext := d.Get("context").(string)
var diags diag.Diagnostics
diff --git a/spectrocloud/resource_cluster_virtual.go b/spectrocloud/resource_cluster_virtual.go
index 44b7df121..6b79a9c21 100644
--- a/spectrocloud/resource_cluster_virtual.go
+++ b/spectrocloud/resource_cluster_virtual.go
@@ -234,7 +234,7 @@ func resourceClusterVirtualCreate(ctx context.Context, d *schema.ResourceData, m
resourceClusterVirtualRead(ctx, d, m)
- return diags
+ return diagnostics
}
//goland:noinspection GoUnhandledErrorResult
@@ -463,12 +463,12 @@ func toMachinePoolVirtual(resources map[string]interface{}) *models.V1VirtualMac
mp := &models.V1VirtualMachinePoolConfigEntity{
CloudConfig: &models.V1VirtualMachinePoolCloudConfigEntity{
InstanceType: &models.V1VirtualInstanceType{
- MaxCPU: int32(maxCpu),
- MaxMemInMiB: int32(maxMemInMb),
- MaxStorageGiB: int32(maxStorageInGb),
- MinCPU: int32(minCpu),
- MinMemInMiB: int32(minMemInMb),
- MinStorageGiB: int32(minStorageInGb),
+ MaxCPU: SafeInt32(maxCpu),
+ MaxMemInMiB: SafeInt32(maxMemInMb),
+ MaxStorageGiB: SafeInt32(maxStorageInGb),
+ MinCPU: SafeInt32(minCpu),
+ MinMemInMiB: SafeInt32(minMemInMb),
+ MinStorageGiB: SafeInt32(minStorageInGb),
},
},
}
@@ -485,12 +485,12 @@ func toVirtualClusterResize(resources map[string]interface{}) *models.V1VirtualC
minStorageInGb := resources["min_storage_in_gb"].(int)
VCResize := &models.V1VirtualClusterResize{
InstanceType: &models.V1VirtualInstanceType{
- MaxCPU: int32(maxCpu),
- MaxMemInMiB: int32(maxMemInMb),
- MaxStorageGiB: int32(maxStorageInGb),
- MinCPU: int32(minCpu),
- MinMemInMiB: int32(minMemInMb),
- MinStorageGiB: int32(minStorageInGb),
+ MaxCPU: SafeInt32(maxCpu),
+ MaxMemInMiB: SafeInt32(maxMemInMb),
+ MaxStorageGiB: SafeInt32(maxStorageInGb),
+ MinCPU: SafeInt32(minCpu),
+ MinMemInMiB: SafeInt32(minMemInMb),
+ MinStorageGiB: SafeInt32(minStorageInGb),
},
}
return VCResize
diff --git a/spectrocloud/resource_cluster_vsphere.go b/spectrocloud/resource_cluster_vsphere.go
index 6c7f85a46..ed3026721 100644
--- a/spectrocloud/resource_cluster_vsphere.go
+++ b/spectrocloud/resource_cluster_vsphere.go
@@ -15,6 +15,7 @@ import (
"github.com/spectrocloud/palette-sdk-go/api/models"
"github.com/spectrocloud/palette-sdk-go/client"
+ "github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/constants"
"github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/schemas"
"github.com/spectrocloud/terraform-provider-spectrocloud/types"
)
@@ -390,7 +391,7 @@ func resourceClusterVsphereCreate(ctx context.Context, d *schema.ResourceData, m
resourceClusterVsphereRead(ctx, d, m)
- return diags
+ return diagnostics
}
//goland:noinspection GoUnhandledErrorResult
@@ -848,20 +849,44 @@ func toMachinePoolVsphere(machinePool interface{}) (*models.V1VsphereMachinePool
}
ins := m["instance_type"].([]interface{})[0].(map[string]interface{})
+
+ // Check bounds before conversion
+ diskSizeInt := ins["disk_size_gb"].(int)
+ memoryInt := ins["memory_mb"].(int)
+ cpuInt := ins["cpu"].(int)
+
+ if diskSizeInt > constants.Int32MaxValue || memoryInt > constants.Int64MaxValue || cpuInt > constants.Int32MaxValue {
+ return nil, fmt.Errorf("instance type values out of range: disk_size_gb=%d, memory_mb=%d, cpu=%d", diskSizeInt, memoryInt, cpuInt)
+ }
+
instanceType := models.V1VsphereInstanceType{
- DiskGiB: types.Ptr(int32(ins["disk_size_gb"].(int))),
- MemoryMiB: types.Ptr(int64(ins["memory_mb"].(int))),
- NumCPUs: types.Ptr(int32(ins["cpu"].(int))),
+ DiskGiB: types.Ptr(SafeInt32(diskSizeInt)),
+ MemoryMiB: types.Ptr(SafeInt64(memoryInt)),
+ NumCPUs: types.Ptr(SafeInt32(cpuInt)),
+ }
+
+ countInt := m["count"].(int)
+ if countInt > constants.Int32MaxValue {
+ return nil, fmt.Errorf("count value %d is out of range for int32", countInt)
}
- min := int32(m["count"].(int))
- max := int32(m["count"].(int))
+
+ min := SafeInt32(countInt)
+ max := SafeInt32(countInt)
if m["min"] != nil {
- min = int32(m["min"].(int))
+ minInt := m["min"].(int)
+ if minInt > constants.Int32MaxValue {
+ return nil, fmt.Errorf("min value %d is out of range for int32", minInt)
+ }
+ min = SafeInt32(minInt)
}
if m["max"] != nil {
- max = int32(m["max"].(int))
+ maxInt := m["max"].(int)
+ if maxInt > constants.Int32MaxValue {
+ return nil, fmt.Errorf("max value %d is out of range for int32", maxInt)
+ }
+ max = SafeInt32(maxInt)
}
mp := &models.V1VsphereMachinePoolConfigEntity{
@@ -875,7 +900,7 @@ func toMachinePoolVsphere(machinePool interface{}) (*models.V1VsphereMachinePool
IsControlPlane: controlPlane,
Labels: labels,
Name: types.Ptr(m["name"].(string)),
- Size: types.Ptr(int32(m["count"].(int))),
+ Size: types.Ptr(SafeInt32(m["count"].(int))),
UpdateStrategy: &models.V1UpdateStrategy{
Type: getUpdateStrategy(m),
},
@@ -890,9 +915,16 @@ func toMachinePoolVsphere(machinePool interface{}) (*models.V1VsphereMachinePool
if m["node_repave_interval"] != nil {
nodeRepaveInterval = m["node_repave_interval"].(int)
}
- mp.PoolConfig.NodeRepaveInterval = int32(nodeRepaveInterval)
+ if nodeRepaveInterval > constants.Int32MaxValue {
+ return nil, fmt.Errorf("node_repave_interval value %d is out of range for int32", nodeRepaveInterval)
+ }
+ mp.PoolConfig.NodeRepaveInterval = SafeInt32(nodeRepaveInterval)
} else {
- err := ValidationNodeRepaveIntervalForControlPlane(m["node_repave_interval"].(int))
+ nodeRepaveInterval := m["node_repave_interval"].(int)
+ if nodeRepaveInterval > constants.Int32MaxValue {
+ return nil, fmt.Errorf("node_repave_interval value %d is out of range for int32", nodeRepaveInterval)
+ }
+ err := ValidationNodeRepaveIntervalForControlPlane(nodeRepaveInterval)
if err != nil {
return mp, err
}
diff --git a/spectrocloud/resource_developer_setting.go b/spectrocloud/resource_developer_setting.go
index 326691314..62783272a 100644
--- a/spectrocloud/resource_developer_setting.go
+++ b/spectrocloud/resource_developer_setting.go
@@ -3,11 +3,13 @@ package spectrocloud
import (
"context"
"fmt"
+ "time"
+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/spectrocloud/palette-sdk-go/api/models"
- "time"
+ "github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/constants"
)
func resourceDeveloperSetting() *schema.Resource {
@@ -66,17 +68,34 @@ func resourceDeveloperSetting() *schema.Resource {
}
func toDeveloperSetting(d *schema.ResourceData) (*models.V1DeveloperCredit, *models.V1TenantEnableClusterGroup) {
+ cpuInt := d.Get("cpu").(int)
+ memoryInt := d.Get("memory").(int)
+ storageInt := d.Get("storage").(int)
+ virtualClustersLimitInt := d.Get("virtual_clusters_limit").(int)
+
+ // Check bounds for int32 conversion
+ if cpuInt > constants.Int32MaxValue || memoryInt > constants.Int32MaxValue || storageInt > constants.Int32MaxValue || virtualClustersLimitInt > constants.Int32MaxValue {
+ // Return default values if any value is out of range
+ return &models.V1DeveloperCredit{
+ CPU: 12,
+ MemoryGiB: 16,
+ StorageGiB: 20,
+ VirtualClustersLimit: 2,
+ }, &models.V1TenantEnableClusterGroup{
+ HideSystemClusterGroups: false,
+ }
+ }
+
devCredit := &models.V1DeveloperCredit{
- CPU: int32(d.Get("cpu").(int)),
- MemoryGiB: int32(d.Get("memory").(int)),
- StorageGiB: int32(d.Get("storage").(int)),
- VirtualClustersLimit: int32(d.Get("virtual_clusters_limit").(int)),
+ CPU: SafeInt32(cpuInt),
+ MemoryGiB: SafeInt32(memoryInt),
+ StorageGiB: SafeInt32(storageInt),
+ VirtualClustersLimit: SafeInt32(virtualClustersLimitInt),
}
sysClusterGroupPref := &models.V1TenantEnableClusterGroup{
HideSystemClusterGroups: d.Get("hide_system_cluster_group").(bool),
}
return devCredit, sysClusterGroupPref
-
}
func toDeveloperSettingDefault(d *schema.ResourceData) (*models.V1DeveloperCredit, *models.V1TenantEnableClusterGroup) {
diff --git a/spectrocloud/resource_pcg_ippool.go b/spectrocloud/resource_pcg_ippool.go
index cdd94087c..f89a9321e 100644
--- a/spectrocloud/resource_pcg_ippool.go
+++ b/spectrocloud/resource_pcg_ippool.go
@@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/spectrocloud/palette-sdk-go/api/models"
+ "github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/constants"
)
func resourcePrivateCloudGatewayIpPool() *schema.Resource {
@@ -201,10 +202,16 @@ func resourceIpPoolDelete(ctx context.Context, d *schema.ResourceData, m interfa
}
func toIpPool(d *schema.ResourceData) *models.V1IPPoolInputEntity {
+ prefixInt := d.Get("prefix").(int)
+ if prefixInt > constants.Int32MaxValue {
+ // This should not happen in practice as prefix is typically 0-32 for CIDR notation
+ prefixInt = 24 // Default to /24 if out of range
+ }
+
pool := &models.V1Pool{
Gateway: d.Get("gateway").(string),
Nameserver: &models.V1Nameserver{},
- Prefix: int32(d.Get("prefix").(int)),
+ Prefix: SafeInt32(prefixInt),
}
if d.Get("network_type").(string) == "range" {
diff --git a/spectrocloud/resource_platform_setting.go b/spectrocloud/resource_platform_setting.go
index b9166ce2a..e739d24cf 100644
--- a/spectrocloud/resource_platform_setting.go
+++ b/spectrocloud/resource_platform_setting.go
@@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/spectrocloud/palette-sdk-go/api/models"
+ "github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/constants"
)
func resourcePlatformSetting() *schema.Resource {
@@ -140,8 +141,12 @@ func updatePlatformSettings(d *schema.ResourceData, m interface{}) diag.Diagnost
if platformSettingContext == tenantString {
// session timeout
if sessionTime, ok := d.GetOk("session_timeout"); ok {
+ sessionTimeInt := sessionTime.(int)
+ if sessionTimeInt > constants.Int32MaxValue {
+ return diag.FromErr(fmt.Errorf("session_timeout value %d is out of range for int32", sessionTimeInt))
+ }
err = c.UpdateSessionTimeout(tenantUID,
- &models.V1AuthTokenSettings{ExpiryTimeMinutes: int32(sessionTime.(int))})
+ &models.V1AuthTokenSettings{ExpiryTimeMinutes: SafeInt32(sessionTimeInt)})
if err != nil {
return diag.FromErr(err)
}
@@ -391,8 +396,12 @@ func resourcePlatformSettingUpdate(ctx context.Context, d *schema.ResourceData,
// session timeout
if d.HasChange("session_timeout") {
if sessionTime, ok := d.GetOk("session_timeout"); ok {
+ sessionTimeInt := sessionTime.(int)
+ if sessionTimeInt > constants.Int32MaxValue {
+ return diag.FromErr(fmt.Errorf("session_timeout value %d is out of range for int32", sessionTimeInt))
+ }
err = c.UpdateSessionTimeout(tenantUID,
- &models.V1AuthTokenSettings{ExpiryTimeMinutes: int32(sessionTime.(int))})
+ &models.V1AuthTokenSettings{ExpiryTimeMinutes: SafeInt32(sessionTimeInt)})
if err != nil {
return diag.FromErr(err)
}
@@ -508,7 +517,7 @@ func updatePlatformSettingsDefault(d *schema.ResourceData, m interface{}) diag.D
if platformSettingContext == tenantString {
// session timeout
err = c.UpdateSessionTimeout(tenantUID,
- &models.V1AuthTokenSettings{ExpiryTimeMinutes: int32(240)})
+ &models.V1AuthTokenSettings{ExpiryTimeMinutes: SafeInt32(240)})
if err != nil {
return diag.FromErr(err)
}
diff --git a/spectrocloud/resource_registry_oci_ecr.go b/spectrocloud/resource_registry_oci_ecr.go
index 9415abd62..2e8d4d4d5 100644
--- a/spectrocloud/resource_registry_oci_ecr.go
+++ b/spectrocloud/resource_registry_oci_ecr.go
@@ -4,9 +4,10 @@ import (
"context"
"errors"
"fmt"
- "github.com/spectrocloud/palette-sdk-go/client"
"time"
+ "github.com/spectrocloud/palette-sdk-go/client"
+
"github.com/go-openapi/strfmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
@@ -140,7 +141,7 @@ func resourceRegistryOciEcr() *schema.Resource {
Type: schema.TypeBool,
Optional: true,
Default: false,
- Description: "Disables TLS certificate verification when set to true. Use with caution as it may expose connections to security risks.",
+ Description: "Disables TLS certificate verification when set to true. ⚠️ WARNING: Setting this to true disables SSL certificate verification and makes connections vulnerable to man-in-the-middle attacks. Only use this when connecting to registries with self-signed certificates in trusted networks.",
},
},
},
diff --git a/spectrocloud/resource_sso.go b/spectrocloud/resource_sso.go
index 4572edfb3..d67e3c689 100644
--- a/spectrocloud/resource_sso.go
+++ b/spectrocloud/resource_sso.go
@@ -94,7 +94,7 @@ func resourceSSO() *schema.Resource {
Type: schema.TypeBool,
Optional: true,
Default: false,
- Description: "Boolean to skip TLS verification for identity provider communication.",
+ Description: "Boolean to skip TLS verification for identity provider communication. ⚠️ WARNING: Setting this to true disables SSL certificate verification and makes connections vulnerable to man-in-the-middle attacks. Only use this when connecting to identity providers with self-signed certificates in trusted networks.",
},
"client_id": {
Type: schema.TypeString,
diff --git a/spectrocloud/resource_workspace.go b/spectrocloud/resource_workspace.go
index f6115eb58..96923eb43 100644
--- a/spectrocloud/resource_workspace.go
+++ b/spectrocloud/resource_workspace.go
@@ -96,7 +96,10 @@ func resourceWorkspaceCreate(ctx context.Context, d *schema.ResourceData, m inte
var diags diag.Diagnostics
- workspace := toWorkspace(d, c)
+ workspace, err := toWorkspace(d, c)
+ if err != nil {
+ return diag.FromErr(err)
+ }
uid, err := c.CreateWorkspace(workspace)
if err != nil {
@@ -203,7 +206,10 @@ func resourceWorkspaceUpdate(ctx context.Context, d *schema.ResourceData, m inte
if d.HasChange("clusters") || d.HasChange("workspace_quota") {
// resource allocation should go first because clusters are inside.
- namespaces := toUpdateWorkspaceNamespaces(d, c)
+ namespaces, err := toUpdateWorkspaceNamespaces(d, c)
+ if err != nil {
+ return diag.FromErr(err)
+ }
if err := c.UpdateWorkspaceResourceAllocation(d.Id(), namespaces); err != nil {
return diag.FromErr(err)
}
@@ -219,7 +225,11 @@ func resourceWorkspaceUpdate(ctx context.Context, d *schema.ResourceData, m inte
}
}
if d.HasChange("namespaces") {
- if err := c.UpdateWorkspaceResourceAllocation(d.Id(), toUpdateWorkspaceNamespaces(d, c)); err != nil {
+ namespaces, err := toUpdateWorkspaceNamespaces(d, c)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ if err := c.UpdateWorkspaceResourceAllocation(d.Id(), namespaces); err != nil {
return diag.FromErr(err)
}
}
@@ -269,12 +279,17 @@ func resourceWorkspaceDelete(ctx context.Context, d *schema.ResourceData, m inte
return diags
}
-func toWorkspace(d *schema.ResourceData, c *client.V1Client) *models.V1WorkspaceEntity {
+func toWorkspace(d *schema.ResourceData, c *client.V1Client) (*models.V1WorkspaceEntity, error) {
annotations := make(map[string]string)
if len(d.Get("description").(string)) > 0 {
annotations["description"] = d.Get("description").(string)
}
+ quota, err := toQuota(d)
+ if err != nil {
+ return nil, err
+ }
+
workspace := &models.V1WorkspaceEntity{
Metadata: &models.V1ObjectMeta{
Name: d.Get("name").(string),
@@ -287,11 +302,11 @@ func toWorkspace(d *schema.ResourceData, c *client.V1Client) *models.V1Workspace
ClusterRbacs: toWorkspaceRBACs(d),
ClusterRefs: toClusterRefs(d, c),
Policies: toWorkspacePolicies(d),
- Quota: toQuota(d),
+ Quota: quota,
},
}
- return workspace
+ return workspace, nil
}
func resourceWorkspaceImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
diff --git a/spectrocloud/resource_workspace_test.go b/spectrocloud/resource_workspace_test.go
index 025522dd7..31f1a8a90 100644
--- a/spectrocloud/resource_workspace_test.go
+++ b/spectrocloud/resource_workspace_test.go
@@ -91,7 +91,8 @@ func TestToWorkspace(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
// Initialize resource data with input
d := schema.TestResourceDataRaw(t, resourceWorkspace().Schema, tt.input)
- result := toWorkspace(d, nil) // nil client for unit test
+ result, err := toWorkspace(d, nil) // nil client for unit test
+ assert.NoError(t, err)
// Compare the expected and actual result
assert.Equal(t, tt.expected.Metadata.Name, result.Metadata.Name)
diff --git a/spectrocloud/scripts/spectro-tf-format b/spectrocloud/scripts/spectro-tf-format.py
similarity index 95%
rename from spectrocloud/scripts/spectro-tf-format
rename to spectrocloud/scripts/spectro-tf-format.py
index af7e2847c..2c5dc02d4 100755
--- a/spectrocloud/scripts/spectro-tf-format
+++ b/spectrocloud/scripts/spectro-tf-format.py
@@ -15,8 +15,13 @@
import os
import re
import shutil
+import platform
from typing import Dict, List, Any, Tuple, Optional
+# Platform detection for cross-platform compatibility
+IS_WINDOWS = platform.system() == 'Windows'
+IS_POSIX = os.name == 'posix'
+
# Built-in templating configuration for Spectro Cloud
BUILTIN_TEMPLATING_CONFIG = {
@@ -664,8 +669,10 @@ def _apply_yaml_extraction(self, tf_content: str, tf_file_path: Path) -> Tuple[s
self._write_yaml_file(yaml_file, final_yaml_content)
created_files.append(str(yaml_file))
- # Create file reference
+ # Create file reference (ensure forward slashes for Terraform compatibility)
relative_path = os.path.relpath(yaml_file, tf_file_path.parent)
+ if IS_WINDOWS:
+ relative_path = relative_path.replace('\\', '/')
file_reference = f'file("{relative_path}")'
# Replace the values assignment
@@ -701,8 +708,10 @@ def _apply_yaml_extraction(self, tf_content: str, tf_file_path: Path) -> Tuple[s
self._write_yaml_file(yaml_file, final_yaml_content)
created_files.append(str(yaml_file))
- # Create file reference
+ # Create file reference (ensure forward slashes for Terraform compatibility)
relative_path = os.path.relpath(yaml_file, tf_file_path.parent)
+ if IS_WINDOWS:
+ relative_path = relative_path.replace('\\', '/')
file_reference = f'file("{relative_path}")'
# Replace the node_pool_config assignment
@@ -1176,38 +1185,85 @@ def _inject_machine_pool_overrides_for_resource(self, tf_content: str, overrides
return modified_content
+ def _get_terraform_commands(self) -> List[str]:
+ """Get list of terraform commands to try for the current platform"""
+ if IS_WINDOWS:
+ return ['terraform.exe', 'terraform']
+ else:
+ return ['terraform']
+
def _format_terraform_file(self, tf_path: Path) -> None:
- """Format the Terraform file using terraform fmt"""
- try:
- print(f"📝 Formatting Terraform file...")
- result = subprocess.run(
- ['terraform', 'fmt', str(tf_path)],
- cwd=tf_path.parent,
- capture_output=True,
- text=True,
- timeout=30
- )
-
- if result.returncode == 0:
- print("✅ Terraform file formatted successfully")
- else:
- print(f"⚠️ Terraform fmt warning: {result.stderr.strip()}")
+ """Format the Terraform file using terraform fmt (cross-platform)"""
+ print(f"📝 Formatting Terraform file...")
+
+ commands_to_try = self._get_terraform_commands()
+
+ for terraform_cmd in commands_to_try:
+ try:
+ result = subprocess.run(
+ [terraform_cmd, 'fmt', str(tf_path)],
+ cwd=tf_path.parent,
+ capture_output=True,
+ text=True,
+ timeout=30,
+ shell=IS_WINDOWS # Use shell on Windows for better PATH resolution
+ )
- except subprocess.TimeoutExpired:
- print("⚠️ Terraform fmt timed out")
- except FileNotFoundError:
+ if result.returncode == 0:
+ print("✅ Terraform file formatted successfully")
+ return
+ else:
+ print(f"⚠️ Terraform fmt warning: {result.stderr.strip()}")
+ return
+
+ except subprocess.TimeoutExpired:
+ print(f"⚠️ Terraform fmt timed out with command: {terraform_cmd}")
+ return
+ except FileNotFoundError:
+ # Try next command in the list
+ continue
+ except Exception as e:
+ print(f"⚠️ Error running {terraform_cmd}: {e}")
+ continue
+
+ # If we get here, none of the commands worked
+ if IS_WINDOWS:
+ print("⚠️ terraform command not found - ensure terraform.exe is in your PATH")
+ print(" Download from: https://www.terraform.io/downloads.html")
+ else:
print("⚠️ terraform command not found - skipping formatting")
- except Exception as e:
- print(f"⚠️ Could not format Terraform file: {e}")
def main():
"""Main entry point"""
+ # Platform-specific examples
+ if IS_WINDOWS:
+ platform_examples = """
+Windows Examples:
+ # Using batch wrapper (recommended)
+ spectro-tf-format.bat generated.tf
+
+ # Using PowerShell wrapper
+ .\\spectro-tf-format.ps1 generated.tf
+
+ # Direct Python execution
+ python spectro-tf-format generated.tf"""
+ else:
+ platform_examples = """
+Unix/Linux Examples:
+ # Make executable first
+ chmod +x spectro-tf-format
+
+ # Run directly
+ ./spectro-tf-format generated.tf"""
+
parser = argparse.ArgumentParser(
- description='Spectro Terraform Formatter - Built-in YAML processing for Spectro Cloud',
+ description='Spectro Terraform Formatter - Built-in YAML processing for Spectro Cloud (Cross-Platform)',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
-Examples:
+{platform_examples}
+
+General Examples:
# Process terraform file with defaults (YAML extraction only)
spectro-tf-format generated.tf
@@ -1231,6 +1287,7 @@ def main():
Output Directory: {BUILTIN_TEMPLATING_CONFIG["output_dir"]}
Backup Files: {BUILTIN_TEMPLATING_CONFIG["backup"]}
Rules: {', '.join(BUILTIN_TEMPLATING_CONFIG["rules"])}
+ Platform: {platform.system()} ({platform.machine()})
"""
)
diff --git a/spectrocloud/utils.go b/spectrocloud/utils.go
index 840893057..c33225368 100644
--- a/spectrocloud/utils.go
+++ b/spectrocloud/utils.go
@@ -1,6 +1,37 @@
package spectrocloud
-import "time"
+import (
+ "math"
+ "time"
+)
+
+// SafeInt32 converts int to int32 with bounds checking to prevent overflow
+func SafeInt32(value int) int32 {
+ if value > math.MaxInt32 {
+ return math.MaxInt32
+ }
+ if value < math.MinInt32 {
+ return math.MinInt32
+ }
+ return int32(value)
+}
+
+// SafeInt64 converts int to int64 with bounds checking to prevent overflow
+func SafeInt64(value int) int64 {
+
+ return int64(value)
+}
+
+// SafeUint32 converts int to uint32 with bounds checking to prevent overflow
+func SafeUint32(value int) uint32 {
+ if value < 0 {
+ return 0
+ }
+ if value > math.MaxUint32 {
+ return math.MaxUint32
+ }
+ return uint32(value)
+}
func expandStringList(configured []interface{}) []string {
vs := make([]string, 0)
diff --git a/spectrocloud/workspace_namespace.go b/spectrocloud/workspace_namespace.go
index b1db7ad54..74173a0a3 100644
--- a/spectrocloud/workspace_namespace.go
+++ b/spectrocloud/workspace_namespace.go
@@ -1,6 +1,7 @@
package spectrocloud
import (
+ "fmt"
"math"
"regexp"
"strconv"
@@ -9,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/spectrocloud/palette-sdk-go/api/models"
"github.com/spectrocloud/palette-sdk-go/client"
+ "github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/constants"
)
// Helper function to create V1WorkspaceResourceAllocation from resource allocation map
@@ -29,18 +31,19 @@ func toWorkspaceResourceAllocation(resourceAllocation map[string]interface{}) (*
}
// Handle GPU configuration if specified
- if gpuLimit, exists := resourceAllocation["gpu_limit"]; exists && gpuLimit.(string) != "" {
- gpuVal, err := strconv.Atoi(gpuLimit.(string))
- if err == nil && gpuVal > 0 {
- provider := "nvidia" // Default provider for cluster allocations
- // gpu_provider is optional - mainly used for default resource allocations
- if gpuProvider, providerExists := resourceAllocation["gpu_provider"]; providerExists && gpuProvider.(string) != "" {
- provider = gpuProvider.(string)
- }
- resource_alloc.GpuConfig = &models.V1GpuConfig{
- Limit: int32(gpuVal),
- Provider: &provider,
- }
+ if gpuVal, exists := resourceAllocation["gpu"]; exists && gpuVal.(int) > 0 {
+ gpuInt := gpuVal.(int)
+ if gpuInt > constants.Int32MaxValue {
+ return nil, fmt.Errorf("gpu value %d is out of range for int32", gpuInt)
+ }
+ provider := "nvidia" // Default provider for cluster allocations
+ // gpu_provider is optional - mainly used for default resource allocations
+ if gpuProvider, providerExists := resourceAllocation["gpu_provider"]; providerExists && gpuProvider.(string) != "" {
+ provider = gpuProvider.(string)
+ }
+ resource_alloc.GpuConfig = &models.V1GpuConfig{
+ Limit: SafeInt32(gpuInt),
+ Provider: &provider,
}
}
@@ -140,12 +143,17 @@ func IsRegex(name string) bool {
}
-func toUpdateWorkspaceNamespaces(d *schema.ResourceData, c *client.V1Client) *models.V1WorkspaceClusterNamespacesEntity {
+func toUpdateWorkspaceNamespaces(d *schema.ResourceData, c *client.V1Client) (*models.V1WorkspaceClusterNamespacesEntity, error) {
+ quota, err := toQuota(d)
+ if err != nil {
+ return nil, err
+ }
+
return &models.V1WorkspaceClusterNamespacesEntity{
ClusterNamespaces: toWorkspaceNamespaces(d),
ClusterRefs: toClusterRefs(d, c),
- Quota: toQuota(d),
- }
+ Quota: quota,
+ }, nil
}
// Helper function to flatten V1WorkspaceResourceAllocation to resource allocation map
@@ -153,12 +161,34 @@ func toUpdateWorkspaceNamespaces(d *schema.ResourceData, c *client.V1Client) *mo
func flattenWorkspaceResourceAllocation(resourceAlloc *models.V1WorkspaceResourceAllocation, includeProvider bool) map[string]interface{} {
result := make(map[string]interface{})
- result["cpu_cores"] = strconv.Itoa(int(math.Round(resourceAlloc.CPUCores)))
- result["memory_MiB"] = strconv.Itoa(int(math.Round(resourceAlloc.MemoryMiB)))
+ // Convert CPU cores with bounds checking to prevent integer overflow
+ cpuCoresRounded := math.Round(resourceAlloc.CPUCores)
+ if cpuCoresRounded > math.MaxInt || cpuCoresRounded < math.MinInt {
+ // Fallback to string representation if out of int range
+ result["cpu_cores"] = fmt.Sprintf("%.0f", cpuCoresRounded)
+ } else {
+ result["cpu_cores"] = strconv.Itoa(int(cpuCoresRounded))
+ }
+
+ // Convert memory with bounds checking to prevent integer overflow
+ memoryMiBRounded := math.Round(resourceAlloc.MemoryMiB)
+ if memoryMiBRounded > math.MaxInt || memoryMiBRounded < math.MinInt {
+ // Fallback to string representation if out of int range
+ result["memory_MiB"] = fmt.Sprintf("%.0f", memoryMiBRounded)
+ } else {
+ result["memory_MiB"] = strconv.Itoa(int(memoryMiBRounded))
+ }
// Handle GPU configuration if present
if resourceAlloc.GpuConfig != nil {
- result["gpu_limit"] = strconv.Itoa(int(resourceAlloc.GpuConfig.Limit))
+ // Convert GPU limit with bounds checking to prevent integer overflow
+ gpuLimit := int64(resourceAlloc.GpuConfig.Limit)
+ if gpuLimit > math.MaxInt || gpuLimit < math.MinInt {
+ // Fallback to string representation if out of int range
+ result["gpu_limit"] = fmt.Sprintf("%d", gpuLimit)
+ } else {
+ result["gpu_limit"] = strconv.Itoa(int(gpuLimit))
+ }
// Only include gpu_provider for default resource allocations, not cluster-specific ones
if includeProvider {
if resourceAlloc.GpuConfig.Provider != nil {
diff --git a/spectrocloud/workspace_rbac.go b/spectrocloud/workspace_rbac.go
index 1df443021..a7952d60a 100644
--- a/spectrocloud/workspace_rbac.go
+++ b/spectrocloud/workspace_rbac.go
@@ -1,8 +1,11 @@
package spectrocloud
import (
+ "fmt"
+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/spectrocloud/palette-sdk-go/api/models"
+ "github.com/spectrocloud/terraform-provider-spectrocloud/spectrocloud/constants"
)
func toWorkspaceRBACs(d *schema.ResourceData) []*models.V1ClusterRbac {
@@ -18,7 +21,7 @@ func toWorkspaceRBACs(d *schema.ResourceData) []*models.V1ClusterRbac {
return workspace_rbacs
}
-func toQuota(d *schema.ResourceData) *models.V1WorkspaceQuota {
+func toQuota(d *schema.ResourceData) (*models.V1WorkspaceQuota, error) {
wsQuota, ok := d.GetOk("workspace_quota")
if !ok || len(wsQuota.([]interface{})) == 0 {
return &models.V1WorkspaceQuota{
@@ -26,7 +29,7 @@ func toQuota(d *schema.ResourceData) *models.V1WorkspaceQuota {
CPUCores: 0,
MemoryMiB: 0,
},
- }
+ }, nil
}
q := wsQuota.([]interface{})[0].(map[string]interface{})
@@ -37,14 +40,18 @@ func toQuota(d *schema.ResourceData) *models.V1WorkspaceQuota {
// Handle GPU configuration if specified
if gpuVal, exists := q["gpu"]; exists && gpuVal.(int) > 0 {
+ gpuInt := gpuVal.(int)
+ if gpuInt > constants.Int32MaxValue {
+ return nil, fmt.Errorf("gpu value %d is out of range for int32", gpuInt)
+ }
provider := "nvidia" // Default to nvidia as it's the only supported provider
resourceAllocation.GpuConfig = &models.V1GpuConfig{
- Limit: int32(gpuVal.(int)),
+ Limit: SafeInt32(gpuInt),
Provider: &provider,
}
}
return &models.V1WorkspaceQuota{
ResourceAllocation: resourceAllocation,
- }
+ }, nil
}
diff --git a/tests/mockApiServer/apiServerMock.go b/tests/mockApiServer/apiServerMock.go
index b62738676..c7f2f54ce 100644
--- a/tests/mockApiServer/apiServerMock.go
+++ b/tests/mockApiServer/apiServerMock.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"log"
"net/http"
+ "time"
"github.com/gorilla/mux"
"github.com/spectrocloud/terraform-provider-spectrocloud/tests/mockApiServer/routes"
@@ -45,15 +46,29 @@ func main() {
// Start servers on different ports
go func() {
log.Println("Starting server on :8088...")
- if err := http.ListenAndServeTLS(":8088", "mock_server.crt", "mock_server.key", router8088); err != nil {
+ server := &http.Server{
+ Addr: ":8088",
+ Handler: router8088,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 15 * time.Second,
+ IdleTimeout: 60 * time.Second,
+ }
+ if err := server.ListenAndServeTLS("mock_server.crt", "mock_server.key"); err != nil {
log.Fatalf("Server failed to start on port 8088: %v", err)
}
}()
log.Println("Starting server on :8888...")
- if err := http.ListenAndServeTLS(":8888", "mock_server.crt", "mock_server.key", router8888); err != nil {
- log.Fatalf("Server failed to start on port 8088: %v", err)
+ server := &http.Server{
+ Addr: ":8888",
+ Handler: router8888,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 15 * time.Second,
+ IdleTimeout: 60 * time.Second,
+ }
+ if err := server.ListenAndServeTLS("mock_server.crt", "mock_server.key"); err != nil {
+ log.Fatalf("Server failed to start on port 8888: %v", err)
}
}
diff --git a/tests/mockApiServer/routes/mockCloudAccounts.go b/tests/mockApiServer/routes/mockCloudAccounts.go
index 778e9fd0c..9f049203e 100644
--- a/tests/mockApiServer/routes/mockCloudAccounts.go
+++ b/tests/mockApiServer/routes/mockCloudAccounts.go
@@ -376,8 +376,7 @@ func CloudAccountsRoutes() []Route {
UID: "test-gcp-account-id-1",
},
Spec: &models.V1GcpAccountSpec{
- JSONCredentials: "test-json-cred",
- JSONCredentialsFileName: "test-json",
+ JSONCredentials: "test-json-cred",
},
Status: &models.V1CloudAccountStatus{
State: "Running",