From ed7ea5a37ced5aeef7f1cfefb6ae85172c19fb26 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 24 Sep 2025 15:22:34 -0400 Subject: [PATCH 1/4] Add support for the new parameters added to CKS cluster deployement --- cloudstack/provider.go | 1 + .../resource_cloudstack_cni_configuration.go | 222 ++++++++++++++++++ ...ource_cloudstack_cni_configuration_test.go | 121 ++++++++++ .../resource_cloudstack_kubernetes_cluster.go | 148 +++++++++++- .../resource_cloudstack_kubernetes_version.go | 2 +- .../resource_cloudstack_static_route.go | 5 +- cloudstack/resource_cloudstack_template.go | 12 + 7 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 cloudstack/resource_cloudstack_cni_configuration.go create mode 100644 cloudstack/resource_cloudstack_cni_configuration_test.go diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 91c0f5c2..747f4444 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -102,6 +102,7 @@ func Provider() *schema.Provider { "cloudstack_affinity_group": resourceCloudStackAffinityGroup(), "cloudstack_attach_volume": resourceCloudStackAttachVolume(), "cloudstack_autoscale_vm_profile": resourceCloudStackAutoScaleVMProfile(), + "cloudstack_cni_configuration": resourceCloudStackCniConfiguration(), "cloudstack_configuration": resourceCloudStackConfiguration(), "cloudstack_cluster": resourceCloudStackCluster(), "cloudstack_disk": resourceCloudStackDisk(), diff --git a/cloudstack/resource_cloudstack_cni_configuration.go b/cloudstack/resource_cloudstack_cni_configuration.go new file mode 100644 index 00000000..60b320cc --- /dev/null +++ b/cloudstack/resource_cloudstack_cni_configuration.go @@ -0,0 +1,222 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudStackCniConfiguration() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackCniConfigurationCreate, + Read: resourceCloudStackCniConfigurationRead, + Delete: resourceCloudStackCniConfigurationDelete, + Importer: &schema.ResourceImporter{ + State: importStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the CNI configuration", + }, + + "cni_config": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "CNI Configuration content to be registered", + }, + + "account": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional account for the CNI configuration. Must be used with domain_id.", + }, + + "domain_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional domain ID for the CNI configuration. If the account parameter is used, domain_id must also be used.", + }, + + "project_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional project for the CNI configuration", + }, + + "params": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Description: "List of variables declared in CNI configuration content", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceCloudStackCniConfigurationCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + log.Printf("[DEBUG] Creating CNI configuration: %s", name) + + p := cs.Configuration.NewRegisterCniConfigurationParams(name) + + if v, ok := d.GetOk("cni_config"); ok { + cniConfig := v.(string) + log.Printf("[DEBUG] CNI config data length: %d bytes", len(cniConfig)) + p.SetCniconfig(cniConfig) + } else { + return fmt.Errorf("CNI configuration content is required but not provided") + } + + if account := d.Get("account").(string); account != "" { + log.Printf("[DEBUG] Setting account: %s", account) + p.SetAccount(account) + } + + if domainID := d.Get("domain_id").(string); domainID != "" { + log.Printf("[DEBUG] Setting domain ID: %s", domainID) + p.SetDomainid(domainID) + } + + if projectID := d.Get("project_id").(string); projectID != "" { + log.Printf("[DEBUG] Setting project ID: %s", projectID) + p.SetProjectid(projectID) + } + + if params, ok := d.GetOk("params"); ok { + paramsList := []string{} + for _, param := range params.(*schema.Set).List() { + paramsList = append(paramsList, param.(string)) + } + if len(paramsList) > 0 { + paramsStr := strings.Join(paramsList, ",") + log.Printf("[DEBUG] Setting params: %s", paramsStr) + p.SetParams(paramsStr) + } + } + + resp, err := cs.Configuration.RegisterCniConfiguration(p) + if err != nil { + return fmt.Errorf("Error creating CNI configuration %s: %s", name, err) + } + + log.Printf("[DEBUG] CNI configuration creation response: %+v", resp) + + // List configurations to find the created one by name since direct ID access is not available + listParams := cs.Configuration.NewListCniConfigurationParams() + listParams.SetName(name) + + // Add context parameters if available + if account := d.Get("account").(string); account != "" { + listParams.SetAccount(account) + } + if domainID := d.Get("domain_id").(string); domainID != "" { + listParams.SetDomainid(domainID) + } + if projectID := d.Get("project_id").(string); projectID != "" { + listParams.SetProjectid(projectID) + } + + listResp, err := cs.Configuration.ListCniConfiguration(listParams) + if err != nil { + return fmt.Errorf("Error listing CNI configurations after creation: %s", err) + } + + if listResp.Count == 0 { + return fmt.Errorf("CNI configuration %s was created but could not be found", name) + } + + // Use the first (and should be only) result + config := listResp.CniConfiguration[0] + d.SetId(config.Id) + log.Printf("[DEBUG] CNI configuration %s successfully created with ID: %s", name, d.Id()) + + return resourceCloudStackCniConfigurationRead(d, meta) +} + +func resourceCloudStackCniConfigurationRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Reading CNI configuration: %s", d.Id()) + + p := cs.Configuration.NewListCniConfigurationParams() + p.SetId(d.Id()) + + config, err := cs.Configuration.ListCniConfiguration(p) + if err != nil { + return fmt.Errorf("Error listing CNI configuration: %s", err) + } + if config.Count == 0 { + log.Printf("[DEBUG] CNI configuration %s no longer exists", d.Id()) + d.SetId("") + return nil + } + + d.Set("name", config.CniConfiguration[0].Name) + d.Set("cni_config", config.CniConfiguration[0].Userdata) + d.Set("account", config.CniConfiguration[0].Account) + d.Set("domain_id", config.CniConfiguration[0].Domainid) + d.Set("project_id", config.CniConfiguration[0].Projectid) + + if config.CniConfiguration[0].Params != "" { + paramsList := strings.Split(config.CniConfiguration[0].Params, ",") + d.Set("params", paramsList) + } + + return nil +} + +func resourceCloudStackCniConfigurationDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Deleting CNI configuration: %s", d.Id()) + + p := cs.Configuration.NewDeleteCniConfigurationParams(d.Id()) + + _, err := cs.Configuration.DeleteCniConfiguration(p) + if err != nil { + if strings.Contains(err.Error(), "does not exist") || + strings.Contains(err.Error(), "not found") { + log.Printf("[DEBUG] CNI configuration %s already deleted", d.Id()) + return nil + } + return fmt.Errorf("Error deleting CNI configuration %s: %s", d.Id(), err) + } + + log.Printf("[DEBUG] CNI configuration %s deleted", d.Id()) + return nil +} diff --git a/cloudstack/resource_cloudstack_cni_configuration_test.go b/cloudstack/resource_cloudstack_cni_configuration_test.go new file mode 100644 index 00000000..060460f1 --- /dev/null +++ b/cloudstack/resource_cloudstack_cni_configuration_test.go @@ -0,0 +1,121 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackCniConfiguration_basic(t *testing.T) { + var cniConfig cloudstack.CniConfiguration + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackCniConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackCniConfiguration_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackCniConfigurationExists("cloudstack_cni_configuration.foo", &cniConfig), + resource.TestCheckResourceAttr("cloudstack_cni_configuration.foo", "name", "test-cni-config"), + resource.TestCheckResourceAttr("cloudstack_cni_configuration.foo", "params.#", "2"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackCniConfigurationExists(n string, cniConfig *cloudstack.CniConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No CNI configuration ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + config, _, err := cs.Configuration.GetCniConfigurationByID(rs.Primary.ID) + if err != nil { + return err + } + + if config.Id != rs.Primary.ID { + return fmt.Errorf("CNI configuration not found") + } + + *cniConfig = *config + return nil + } +} + +func testAccCheckCloudStackCniConfigurationDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_cni_configuration" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No CNI configuration ID is set") + } + + _, _, err := cs.Configuration.GetCniConfigurationByID(rs.Primary.ID) + if err == nil { + return fmt.Errorf("CNI configuration %s still exists", rs.Primary.ID) + } + } + + return nil +} + +const testAccCloudStackCniConfiguration_basic = ` +resource "cloudstack_cni_configuration" "foo" { + name = "test-cni-config" + cni_config = < Date: Thu, 25 Sep 2025 10:09:07 -0400 Subject: [PATCH 2/4] update doc --- ...ource_cloudstack_cni_configuration_test.go | 65 ++++--- .../docs/r/cni_configuration.html.markdown | 172 ++++++++++++++++++ .../docs/r/kubernetes_cluster.html.markdown | 172 ++++++++++++++---- website/docs/r/template.html.markdown | 117 ++++++++++-- 4 files changed, 449 insertions(+), 77 deletions(-) create mode 100644 website/docs/r/cni_configuration.html.markdown diff --git a/cloudstack/resource_cloudstack_cni_configuration_test.go b/cloudstack/resource_cloudstack_cni_configuration_test.go index 060460f1..fc9bbef9 100644 --- a/cloudstack/resource_cloudstack_cni_configuration_test.go +++ b/cloudstack/resource_cloudstack_cni_configuration_test.go @@ -29,7 +29,7 @@ import ( ) func TestAccCloudStackCniConfiguration_basic(t *testing.T) { - var cniConfig cloudstack.CniConfiguration + var cniConfig cloudstack.UserData resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -48,7 +48,7 @@ func TestAccCloudStackCniConfiguration_basic(t *testing.T) { }) } -func testAccCheckCloudStackCniConfigurationExists(n string, cniConfig *cloudstack.CniConfiguration) resource.TestCheckFunc { +func testAccCheckCloudStackCniConfigurationExists(n string, cniConfig *cloudstack.UserData) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -60,11 +60,19 @@ func testAccCheckCloudStackCniConfigurationExists(n string, cniConfig *cloudstac } cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) - config, _, err := cs.Configuration.GetCniConfigurationByID(rs.Primary.ID) + p := cs.Configuration.NewListCniConfigurationParams() + p.SetId(rs.Primary.ID) + + resp, err := cs.Configuration.ListCniConfiguration(p) if err != nil { return err } + if resp.Count != 1 { + return fmt.Errorf("CNI configuration not found") + } + + config := resp.CniConfiguration[0] if config.Id != rs.Primary.ID { return fmt.Errorf("CNI configuration not found") } @@ -86,8 +94,11 @@ func testAccCheckCloudStackCniConfigurationDestroy(s *terraform.State) error { return fmt.Errorf("No CNI configuration ID is set") } - _, _, err := cs.Configuration.GetCniConfigurationByID(rs.Primary.ID) - if err == nil { + p := cs.Configuration.NewListCniConfigurationParams() + p.SetId(rs.Primary.ID) + + resp, err := cs.Configuration.ListCniConfiguration(p) + if err == nil && resp.Count > 0 { return fmt.Errorf("CNI configuration %s still exists", rs.Primary.ID) } } @@ -98,24 +109,34 @@ func testAccCheckCloudStackCniConfigurationDestroy(s *terraform.State) error { const testAccCloudStackCniConfiguration_basic = ` resource "cloudstack_cni_configuration" "foo" { name = "test-cni-config" - cni_config = < +``` diff --git a/website/docs/r/kubernetes_cluster.html.markdown b/website/docs/r/kubernetes_cluster.html.markdown index 986f2eb6..01e919ed 100644 --- a/website/docs/r/kubernetes_cluster.html.markdown +++ b/website/docs/r/kubernetes_cluster.html.markdown @@ -3,31 +3,103 @@ layout: default page_title: "CloudStack: cloudstack_kubernetes_cluster" sidebar_current: "docs-cloudstack-resource-kubernetes_cluster" description: |- - Creates a Kubernetes Cluster + Creates and manages a CloudStack Kubernetes Service (CKS) cluster --- # CloudStack: cloudstack_kubernetes_cluster -A `cloudstack_kubernetes_cluster` resource manages a Kubernetes cluster within CloudStack. +A `cloudstack_kubernetes_cluster` resource manages a CloudStack Kubernetes Service (CKS) cluster within CloudStack. This resource supports advanced features including mixed node types, custom templates, CNI configurations, and autoscaling. ## Example Usage +### Basic Cluster + +```hcl +resource "cloudstack_kubernetes_cluster" "basic" { + name = "basic-cluster" + zone = "zone1" + kubernetes_version = "1.25.0" + service_offering = "Medium Instance" + size = 3 + description = "Basic Kubernetes cluster" +} +``` + +### Advanced Cluster with CKS Features + ```hcl -resource "cloudstack_kubernetes_cluster" "example" { - name = "example-cluster" - zone = "zone-id" - kubernetes_version = "1.18.6" - service_offering = "small" - size = 1 - autoscaling_enabled = true - min_size = 1 - max_size = 5 - control_nodes_size = 1 - description = "An example Kubernetes cluster" - keypair = "my-ssh-key" - network_id = "net-id" - state = "Running" - project = "my-project" +# Kubernetes version resource +resource "cloudstack_kubernetes_version" "k8s_v1_25" { + semantic_version = "1.25.0" + name = "Kubernetes v1.25.0 with Calico" + url = "http://example.com/k8s-setup-v1.25.0.iso" + min_cpu = 2 + min_memory = 2048 + zone = "zone1" + state = "Enabled" +} + +# CNI configuration +resource "cloudstack_cni_configuration" "calico" { + name = "calico-cni-config" + cni_config = base64encode(jsonencode({ + "name" = "k8s-pod-network", + "cniVersion" = "0.3.1", + "plugins" = [ + { + "type" = "calico", + "datastore_type" = "kubernetes", + "nodename" = "KUBERNETES_NODE_NAME", + "mtu" = "CNI_MTU" + } + ] + })) + + params = ["KUBERNETES_NODE_NAME", "CNI_MTU"] +} + +# Advanced cluster with mixed node types +resource "cloudstack_kubernetes_cluster" "advanced" { + name = "production-cluster" + zone = "zone1" + kubernetes_version = cloudstack_kubernetes_version.k8s_v1_25.semantic_version + service_offering = "Medium Instance" + + # Cluster configuration + size = 3 + control_nodes_size = 3 + etcd_nodes_size = 3 + + # Autoscaling + autoscaling_enabled = true + min_size = 2 + max_size = 10 + + # Node configuration + noderootdisksize = 50 + + # Mixed node offerings + node_offerings = { + "control" = "Large Instance" + "worker" = "Medium Instance" + "etcd" = "Medium Instance" + } + + # Custom templates + node_templates = { + "control" = "ubuntu-20.04-k8s-template" + "worker" = "ubuntu-20.04-k8s-template" + } + + # CNI Configuration + cni_configuration_id = cloudstack_cni_configuration.calico.id + cni_config_details = { + "CNI_MTU" = "1450" + "KUBERNETES_NODE_NAME" = "spec.nodeName" + } + + description = "Production cluster with mixed node types" + hypervisor = "KVM" } ``` @@ -36,40 +108,64 @@ resource "cloudstack_kubernetes_cluster" "example" { The following arguments are supported: +### Required Arguments + * `name` - (Required) The name of the Kubernetes cluster. * `zone` - (Required) The zone where the Kubernetes cluster will be deployed. * `kubernetes_version` - (Required) The Kubernetes version for the cluster. * `service_offering` - (Required) The service offering for the nodes in the cluster. -* `size` - (Optional) The initial size of the Kubernetes cluster. Defaults to `1`. -* `autoscaling_enabled` - (Optional) Whether autoscaling is enabled for the cluster. -* `min_size` - (Optional) The minimum size of the Kubernetes cluster when autoscaling is enabled. -* `max_size` - (Optional) The maximum size of the Kubernetes cluster when autoscaling is enabled. -* `control_nodes_size` - (Optional) The size of the control nodes in the cluster. + +### Basic Configuration + +* `size` - (Optional) The number of worker nodes in the Kubernetes cluster. Defaults to `1`. +* `control_nodes_size` - (Optional) The number of control plane nodes in the cluster. Defaults to `1`. +* `etcd_nodes_size` - (Optional) The number of etcd nodes in the cluster. Defaults to `0` (uses control nodes for etcd). * `description` - (Optional) A description for the Kubernetes cluster. +* `hypervisor` - (Optional) The hypervisor type for the cluster nodes. Defaults to `"KVM"`. + +### Autoscaling Configuration + +* `autoscaling_enabled` - (Optional) Whether autoscaling is enabled for the cluster. Defaults to `false`. +* `min_size` - (Optional) The minimum number of worker nodes when autoscaling is enabled. +* `max_size` - (Optional) The maximum number of worker nodes when autoscaling is enabled. + +### Node Configuration + +* `noderootdisksize` - (Optional) Root disk size in GB for each node. Defaults to `20`. +* `node_offerings` - (Optional) A map of node roles to service offerings. Valid roles are `control`, `worker`, and `etcd`. If not specified, the main `service_offering` is used for all nodes. +* `node_templates` - (Optional) A map of node roles to instance templates. Valid roles are `control`, `worker`, and `etcd`. If not specified, system VM template will be used. + +### CNI Configuration + +* `cni_configuration_id` - (Optional) The ID of a CNI configuration to use for the cluster. If not specified, the default CNI configuration will be used. +* `cni_config_details` - (Optional) A map of CNI configuration parameter values to substitute in the CNI configuration. + +### Network and Security + * `keypair` - (Optional) The SSH key pair to use for the nodes in the cluster. * `network_id` - (Optional) The network ID to connect the Kubernetes cluster to. -* `ip_address` - (Computed) The IP address of the Kubernetes cluster. -* `state` - (Optional) The state of the Kubernetes cluster. Defaults to `"Running"`. + +### Project and Domain + * `project` - (Optional) The project to assign the Kubernetes cluster to. -* `noderootdisksize` - (Optional) root disk size in GB for each node. +* `domain_id` - (Optional) The domain ID for the cluster. +* `account` - (Optional) The account name for the cluster. ## Attributes Reference -The following attributes are exported: +In addition to all arguments above, the following attributes are exported: * `id` - The ID of the Kubernetes cluster. -* `name` - The name of the Kubernetes cluster. -* `description` - The description of the Kubernetes cluster. -* `control_nodes_size` - The size of the control nodes in the cluster. -* `size` - The size of the Kubernetes cluster. -* `autoscaling_enabled` - Whether autoscaling is enabled for the cluster. -* `min_size` - The minimum size of the Kubernetes cluster when autoscaling is enabled. -* `max_size` - The maximum size of the Kubernetes cluster when autoscaling is enabled. -* `keypair` - The SSH key pair used for the nodes in the cluster. -* `network_id` - The network ID connected to the Kubernetes cluster. -* `ip_address` - The IP address of the Kubernetes cluster. -* `state` - The state of the Kubernetes cluster. -* `project` - The project assigned to the Kubernetes cluster. +* `ip_address` - The IP address of the Kubernetes cluster API server. +* `state` - The current state of the Kubernetes cluster. +* `created` - The timestamp when the cluster was created. +* `zone_id` - The zone ID where the cluster is deployed. +* `zone_name` - The zone name where the cluster is deployed. +* `kubernetes_version_id` - The ID of the Kubernetes version used. +* `service_offering_id` - The ID of the service offering used. +* `master_nodes` - The number of master/control nodes in the cluster. +* `cpu_number` - The number of CPUs allocated to the cluster. +* `memory` - The amount of memory (in MB) allocated to the cluster. ## Import diff --git a/website/docs/r/template.html.markdown b/website/docs/r/template.html.markdown index 1b2f8a66..52eb7fa3 100644 --- a/website/docs/r/template.html.markdown +++ b/website/docs/r/template.html.markdown @@ -3,51 +3,98 @@ layout: "cloudstack" page_title: "CloudStack: cloudstack_template" sidebar_current: "docs-cloudstack-resource-template" description: |- - Registers an existing template into the CloudStack cloud. + Registers a template into the CloudStack cloud, including support for CloudStack Kubernetes Service (CKS) templates. --- # cloudstack_template -Registers an existing template into the CloudStack cloud. +Registers a template into the CloudStack cloud. This resource supports both regular VM templates and specialized templates for CloudStack Kubernetes Service (CKS) clusters. ## Example Usage +### Basic Template + ```hcl resource "cloudstack_template" "centos64" { name = "CentOS 6.4 x64" format = "VHD" hypervisor = "XenServer" os_type = "CentOS 6.4 (64bit)" - url = "http://someurl.com/template.vhd" + url = "http://example.com/template.vhd" zone = "zone-1" } ``` +### CKS Template for Kubernetes + +```hcl +resource "cloudstack_template" "cks_ubuntu_template" { + name = "cks-ubuntu-2204-template" + display_text = "CKS Ubuntu 22.04 Template for Kubernetes" + url = "http://example.com/cks-ubuntu-2204-kvm.qcow2.bz2" + format = "QCOW2" + hypervisor = "KVM" + os_type = "Ubuntu 22.04 LTS" + zone = "zone1" + + # CKS specific flag + for_cks = true + + # Template properties + is_extractable = false + is_featured = false + is_public = false + password_enabled = true + is_dynamically_scalable = true + + # Wait for template to be ready + is_ready_timeout = 1800 + + tags = { + Environment = "CKS" + Purpose = "Kubernetes" + OS = "Ubuntu-22.04" + } +} +``` + ## Argument Reference The following arguments are supported: +### Required Arguments + * `name` - (Required) The name of the template. +* `format` - (Required) The format of the template. Valid values are `QCOW2`, `RAW`, `VHD`, `OVA`, and `ISO`. +* `hypervisor` - (Required) The target hypervisor for the template. Valid values include `KVM`, `XenServer`, `VMware`, `Hyperv`, and `LXC`. Changing this forces a new resource to be created. +* `os_type` - (Required) The OS Type that best represents the OS of this template. +* `url` - (Required) The URL of where the template is hosted. Changing this forces a new resource to be created. -* `display_text` - (Optional) The display name of the template. +### Optional Arguments -* `format` - (Required) The format of the template. Valid values are `QCOW2`, - `RAW`, and `VHD`. +* `display_text` - (Optional) The display name of the template. If not specified, defaults to the `name`. +* `zone` - (Optional) The name or ID of the zone where this template will be created. Changing this forces a new resource to be created. +* `project` - (Optional) The name or ID of the project to create this template for. Changing this forces a new resource to be created. +* `account` - (Optional) The account name for the template. +* `domain_id` - (Optional) The domain ID for the template. -* `hypervisor` - (Required) The target hypervisor for the template. Changing - this forces a new resource to be created. +### CKS-Specific Arguments -* `os_type` - (Required) The OS Type that best represents the OS of this - template. +* `for_cks` - (Optional) Set to `true` to indicate this template is for CloudStack Kubernetes Service (CKS). CKS templates have special requirements and capabilities. Defaults to `false`. -* `url` - (Required) The URL of where the template is hosted. Changing this - forces a new resource to be created. +### Template Properties -* `project` - (Optional) The name or ID of the project to create this template for. - Changing this forces a new resource to be created. +* `is_dynamically_scalable` - (Optional) Set to indicate if the template contains tools to support dynamic scaling of VM cpu/memory. Defaults to `false`. +* `is_extractable` - (Optional) Set to indicate if the template is extractable. Defaults to `false`. +* `is_featured` - (Optional) Set to indicate if the template is featured. Defaults to `false`. +* `is_public` - (Optional) Set to indicate if the template is available for all accounts. Defaults to `true`. +* `password_enabled` - (Optional) Set to indicate if the template should be password enabled. Defaults to `false`. +* `sshkey_enabled` - (Optional) Set to indicate if the template supports SSH key injection. Defaults to `false`. +* `is_ready_timeout` - (Optional) The maximum time in seconds to wait until the template is ready for use. Defaults to `300` seconds. -* `zone` - (Optional) The name or ID of the zone where this template will be created. - Changing this forces a new resource to be created. +### Metadata and Tagging + +* `tags` - (Optional) A mapping of tags to assign to the template. * `is_dynamically_scalable` - (Optional) Set to indicate if the template contains tools to support dynamic scaling of VM cpu/memory (defaults false) @@ -78,4 +125,40 @@ The following attributes are exported: * `is_featured` - Set to "true" if the template is featured. * `is_public` - Set to "true" if the template is public. * `password_enabled` - Set to "true" if the template is password enabled. -* `is_ready` - Set to "true" once the template is ready for use. +* `is_ready` - Set to `true` once the template is ready for use. +* `created` - The timestamp when the template was created. +* `size` - The size of the template in bytes. +* `checksum` - The checksum of the template. +* `status` - The current status of the template. +* `zone_id` - The zone ID where the template is registered. +* `zone_name` - The zone name where the template is registered. +* `account` - The account name owning the template. +* `domain` - The domain name where the template belongs. +* `project` - The project name if the template is assigned to a project. + +### Example CKS Template Usage + +```hcl +# Data source to use existing CKS template +data "cloudstack_template" "cks_template" { + template_filter = "executable" + + filter { + name = "name" + value = "cks-ubuntu-2204-template" + } +} + +# Use in Kubernetes cluster +resource "cloudstack_kubernetes_cluster" "example" { + name = "example-cluster" + zone = "zone1" + kubernetes_version = "1.25.0" + service_offering = "Medium Instance" + + node_templates = { + "control" = data.cloudstack_template.cks_template.name + "worker" = data.cloudstack_template.cks_template.name + } +} +``` From b8616b29563b921d02396ac500de15a374bd3626 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Fri, 26 Sep 2025 08:42:13 -0400 Subject: [PATCH 3/4] update sdk version --- go.mod | 5 +---- go.sum | 7 +++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index de1b2059..6e69966d 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ module github.com/terraform-providers/terraform-provider-cloudstack require ( - github.com/apache/cloudstack-go/v2 v2.17.1 + github.com/apache/cloudstack-go/v2 v2.18.1 github.com/go-ini/ini v1.67.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/terraform-plugin-framework v1.12.0 @@ -35,7 +35,6 @@ require ( github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/fatih/color v1.16.0 // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect @@ -85,5 +84,3 @@ require ( ) go 1.23.0 - -toolchain go1.22.4 diff --git a/go.sum b/go.sum index d71638ed..d60bbe41 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/ProtonMail/go-crypto v1.1.0-alpha.0 h1:nHGfwXmFvJrSR9xu8qL7BkO4DqTHXE github.com/ProtonMail/go-crypto v1.1.0-alpha.0/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/apache/cloudstack-go/v2 v2.17.1 h1:XD0bGDOv+MCavXJfc/qxILgJh+cHJbudpqQ1FzA2sDI= -github.com/apache/cloudstack-go/v2 v2.17.1/go.mod h1:p/YBUwIEkQN6CQxFhw8Ff0wzf1MY0qRRRuGYNbcb1F8= +github.com/apache/cloudstack-go/v2 v2.18.1 h1:SgdRUEj5x17wSPfwAacjWgTqbtS/u7iaqnbpILWzE1c= +github.com/apache/cloudstack-go/v2 v2.18.1/go.mod h1:p/YBUwIEkQN6CQxFhw8Ff0wzf1MY0qRRRuGYNbcb1F8= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= @@ -168,7 +168,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -231,4 +230,4 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From d1aab90c50e5ae017aa44db0d90d7760f1501327 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Mon, 29 Sep 2025 08:00:00 -0400 Subject: [PATCH 4/4] skip test if api doesn't exist --- .../resource_cloudstack_cni_configuration_test.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cloudstack/resource_cloudstack_cni_configuration_test.go b/cloudstack/resource_cloudstack_cni_configuration_test.go index fc9bbef9..96b26921 100644 --- a/cloudstack/resource_cloudstack_cni_configuration_test.go +++ b/cloudstack/resource_cloudstack_cni_configuration_test.go @@ -32,7 +32,7 @@ func TestAccCloudStackCniConfiguration_basic(t *testing.T) { var cniConfig cloudstack.UserData resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckCniSupport(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckCloudStackCniConfigurationDestroy, Steps: []resource.TestStep{ @@ -140,3 +140,14 @@ resource "cloudstack_cni_configuration" "foo" { params = ["KUBERNETES_NODE_NAME", "CNI_MTU"] } ` + +func testAccPreCheckCniSupport(t *testing.T) { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + // Try to list CNI configurations to check if the feature is available + p := cs.Configuration.NewListCniConfigurationParams() + _, err := cs.Configuration.ListCniConfiguration(p) + if err != nil { + t.Skipf("CNI configuration not supported in this CloudStack version (requires 4.21.0+): %v", err) + } +}