diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 277dfae2..0e0f6cff 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -109,6 +109,7 @@ func Provider() *schema.Provider { "cloudstack_autoscale_policy": resourceCloudStackAutoScalePolicy(), "cloudstack_autoscale_vm_group": resourceCloudStackAutoScaleVMGroup(), "cloudstack_autoscale_vm_profile": resourceCloudStackAutoScaleVMProfile(), + "cloudstack_cni_configuration": resourceCloudStackCniConfiguration(), "cloudstack_condition": resourceCloudStackCondition(), "cloudstack_configuration": resourceCloudStackConfiguration(), "cloudstack_counter": resourceCloudStackCounter(), 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..96b26921 --- /dev/null +++ b/cloudstack/resource_cloudstack_cni_configuration_test.go @@ -0,0 +1,153 @@ +// +// 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.UserData + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckCniSupport(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.UserData) 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) + 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") + } + + *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") + } + + 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) + } + } + + return nil +} + +const testAccCloudStackCniConfiguration_basic = ` +resource "cloudstack_cni_configuration" "foo" { + name = "test-cni-config" + cni_config = base64encode(jsonencode({ + "name": "test-network", + "cniVersion": "0.4.0", + "plugins": [ + { + "type": "calico", + "log_level": "info", + "datastore_type": "kubernetes", + "nodename": "KUBERNETES_NODE_NAME", + "mtu": "CNI_MTU", + "ipam": { + "type": "calico-ipam" + }, + "policy": { + "type": "k8s" + }, + "kubernetes": { + "kubeconfig": "KUBECONFIG_FILEPATH" + } + }, + { + "type": "portmap", + "snat": true, + "capabilities": {"portMappings": true} + } + ] + })) + + 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) + } +} diff --git a/cloudstack/resource_cloudstack_kubernetes_cluster.go b/cloudstack/resource_cloudstack_kubernetes_cluster.go index 44b500f8..7333c25b 100644 --- a/cloudstack/resource_cloudstack_kubernetes_cluster.go +++ b/cloudstack/resource_cloudstack_kubernetes_cluster.go @@ -121,6 +121,20 @@ func resourceCloudStackKubernetesCluster() *schema.Resource { // Default: "Running", }, + "account": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional account for the Kubernetes cluster. Must be used with domain_id.", + }, + + "domain_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional domain ID for the Kubernetes cluster. If the account parameter is used, domain_id must also be used. Hosts dedicated to the specified domain will be used for deploying the cluster", + }, + "project": { Type: schema.TypeString, Optional: true, @@ -151,6 +165,55 @@ func resourceCloudStackKubernetesCluster() *schema.Resource { ForceNew: true, Sensitive: true, }, + + "as_number": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "An optional as number for the Kubernetes cluster", + }, + + "cni_config_details": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Description: "An optional map of CNI configuration details. It is used to specify the parameters values for the variables in userdata", + }, + + "cni_configuration_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional CNI configuration ID for the Kubernetes cluster. If not specified, the default CNI configuration will be used", + }, + + "etcd_nodes_size": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, // For now + Description: "Number of etcd nodes in the Kubernetes cluster. Default is 0", + }, + + "hypervisor": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The hypervisor on which to deploy the cluster.", + }, + + "node_offerings": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Description: "An optional map of node roles to service offerings. If not specified, the service_offering parameter will be used for all node roles. Valid roles are: worker, control, etcd", + }, + + "node_templates": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Description: "An optional map of node roles to instance templates. If not specified, system VM template will be used. Valid roles are: worker, control, etcd", + }, }, } } @@ -214,6 +277,69 @@ func resourceCloudStackKubernetesClusterCreate(d *schema.ResourceData, meta inte return err } + if account, ok := d.GetOk("account"); ok { + p.SetAccount(account.(string)) + } + if domainID, ok := d.GetOk("domain_id"); ok { + p.SetDomainid(domainID.(string)) + } + + if asNumber, ok := d.GetOk("as_number"); ok { + p.SetAsnumber(int64(asNumber.(int))) + } + + if etcdNodesSize, ok := d.GetOk("etcd_nodes_size"); ok { + p.SetEtcdnodes(int64(etcdNodesSize.(int))) + } + + if hypervisor, ok := d.GetOk("hypervisor"); ok { + p.SetHypervisor(hypervisor.(string)) + } + + if cniConfigID, ok := d.GetOk("cni_configuration_id"); ok { + p.SetCniconfigurationid(cniConfigID.(string)) + } + + if nodeOfferings, ok := d.GetOk("node_offerings"); ok { + nodeOfferingsMap := nodeOfferings.(map[string]interface{}) + nodeOfferingsFormatted := make(map[string]string) + for nodeType, offeringName := range nodeOfferingsMap { + // Retrieve the offering ID + offeringID, e := retrieveID(cs, "service_offering", offeringName.(string)) + if e != nil { + return e.Error() + } + nodeOfferingsFormatted[nodeType] = offeringID + } + p.SetNodeofferings(nodeOfferingsFormatted) + } + + if nodeTemplates, ok := d.GetOk("node_templates"); ok { + nodeTemplatesMap := nodeTemplates.(map[string]interface{}) + nodeTemplatesFormatted := make(map[string]string) + for nodeType, templateName := range nodeTemplatesMap { + zoneID, err := retrieveID(cs, "zone", d.Get("zone").(string)) + if err != nil { + return err.Error() + } + templateID, e := retrieveTemplateID(cs, zoneID, templateName.(string)) + if e != nil { + return e.Error() + } + nodeTemplatesFormatted[nodeType] = templateID + } + p.SetNodetemplates(nodeTemplatesFormatted) + } + + if cniConfigDetails, ok := d.GetOk("cni_config_details"); ok { + cniConfigDetailsMap := cniConfigDetails.(map[string]interface{}) + cniConfigDetailsFormatted := make(map[string]string) + for key, value := range cniConfigDetailsMap { + cniConfigDetailsFormatted[key] = value.(string) + } + p.SetCniconfigdetails(cniConfigDetailsFormatted) + } + log.Printf("[DEBUG] Creating Kubernetes Cluster %s", name) r, err := cs.Kubernetes.CreateKubernetesCluster(p) if err != nil { @@ -273,6 +399,10 @@ func resourceCloudStackKubernetesClusterRead(d *schema.ResourceData, meta interf d.Set("network_id", cluster.Networkid) d.Set("ip_address", cluster.Ipaddress) d.Set("state", cluster.State) + d.Set("account", cluster.Account) + d.Set("domain_id", cluster.Domainid) + d.Set("etcd_nodes_size", cluster.Etcdnodes) + d.Set("cni_configuration_id", cluster.Cniconfigurationid) setValueOrID(d, "kubernetes_version", cluster.Kubernetesversionname, cluster.Kubernetesversionid) setValueOrID(d, "service_offering", cluster.Serviceofferingname, cluster.Serviceofferingid) @@ -295,7 +425,7 @@ func autoscaleKubernetesCluster(d *schema.ResourceData, meta interface{}) error func resourceCloudStackKubernetesClusterUpdate(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) - if d.HasChange("service_offering") || d.HasChange("size") { + if d.HasChange("service_offering") || d.HasChange("size") || d.HasChange("node_offerings") { p := cs.Kubernetes.NewScaleKubernetesClusterParams(d.Id()) serviceOfferingID, e := retrieveID(cs, "service_offering", d.Get("service_offering").(string)) if e != nil { @@ -303,6 +433,22 @@ func resourceCloudStackKubernetesClusterUpdate(d *schema.ResourceData, meta inte } p.SetServiceofferingid(serviceOfferingID) p.SetSize(int64(d.Get("size").(int))) + + // Handle node offerings if they changed + if nodeOfferings, ok := d.GetOk("node_offerings"); ok { + nodeOfferingsMap := nodeOfferings.(map[string]interface{}) + nodeOfferingsFormatted := make(map[string]string) + for nodeType, offeringName := range nodeOfferingsMap { + // Retrieve the offering ID + offeringID, e := retrieveID(cs, "service_offering", offeringName.(string)) + if e != nil { + return e.Error() + } + nodeOfferingsFormatted[nodeType] = offeringID + } + p.SetNodeofferings(nodeOfferingsFormatted) + } + _, err := cs.Kubernetes.ScaleKubernetesCluster(p) if err != nil { return fmt.Errorf( diff --git a/cloudstack/resource_cloudstack_kubernetes_version.go b/cloudstack/resource_cloudstack_kubernetes_version.go index a389d3a1..b1e6d1f0 100644 --- a/cloudstack/resource_cloudstack_kubernetes_version.go +++ b/cloudstack/resource_cloudstack_kubernetes_version.go @@ -115,7 +115,7 @@ func resourceCloudStackKubernetesVersionCreate(d *schema.ResourceData, meta inte p.SetName(name.(string)) } if checksum, ok := d.GetOk("checksum"); ok { - p.SetName(checksum.(string)) + p.SetChecksum(checksum.(string)) } if zone, ok := d.GetOk("zone"); ok { zoneID, e := retrieveID(cs, "zone", zone.(string)) diff --git a/cloudstack/resource_cloudstack_static_route.go b/cloudstack/resource_cloudstack_static_route.go index 09705324..d9240b76 100644 --- a/cloudstack/resource_cloudstack_static_route.go +++ b/cloudstack/resource_cloudstack_static_route.go @@ -56,9 +56,12 @@ func resourceCloudStackStaticRouteCreate(d *schema.ResourceData, meta interface{ // Create a new parameter struct p := cs.VPC.NewCreateStaticRouteParams( d.Get("cidr").(string), - d.Get("gateway_id").(string), ) + if v, ok := d.GetOk("gateway_id"); ok { + p.SetGatewayid(v.(string)) + } + // Create the new private gateway r, err := cs.VPC.CreateStaticRoute(p) if err != nil { diff --git a/cloudstack/resource_cloudstack_template.go b/cloudstack/resource_cloudstack_template.go index edf41c12..4316c7eb 100644 --- a/cloudstack/resource_cloudstack_template.go +++ b/cloudstack/resource_cloudstack_template.go @@ -126,6 +126,13 @@ func resourceCloudStackTemplate() *schema.Resource { Default: 300, }, + "for_cks": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + ForceNew: true, + }, + "tags": tagsSchema(), }, } @@ -182,6 +189,10 @@ func resourceCloudStackTemplateCreate(d *schema.ResourceData, meta interface{}) p.SetPasswordenabled(v.(bool)) } + if v, ok := d.GetOk("for_cks"); ok { + p.SetForcks(v.(bool)) + } + // Retrieve the zone ID if v, ok := d.GetOk("zone"); ok { if v.(string) != "all" { @@ -277,6 +288,7 @@ func resourceCloudStackTemplateRead(d *schema.ResourceData, meta interface{}) er d.Set("is_public", t.Ispublic) d.Set("password_enabled", t.Passwordenabled) d.Set("is_ready", t.Isready) + d.Set("for_cks", t.Forcks) tags := make(map[string]interface{}) for _, tag := range t.Tags { diff --git a/go.mod b/go.mod index 339e856f..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.2 + 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 diff --git a/go.sum b/go.sum index 4b49b663..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.2 h1:fMqLUTpHZeJoSVQiD2hDBSoqOvGJ9QMoOaWABW8lc/0= -github.com/apache/cloudstack-go/v2 v2.17.2/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= diff --git a/website/docs/r/cni_configuration.html.markdown b/website/docs/r/cni_configuration.html.markdown new file mode 100644 index 00000000..de80a761 --- /dev/null +++ b/website/docs/r/cni_configuration.html.markdown @@ -0,0 +1,172 @@ +--- +layout: default +page_title: "CloudStack: cloudstack_cni_configuration" +sidebar_current: "docs-cloudstack-resource-cni_configuration" +description: |- + Creates and manages a CloudStack CNI (Container Network Interface) configuration +--- + +# CloudStack: cloudstack_cni_configuration + +A `cloudstack_cni_configuration` resource manages a Container Network Interface (CNI) configuration for CloudStack Kubernetes Service (CKS) clusters. CNI configurations define how network connectivity is provided to Kubernetes pods. + +## Example Usage + +### Basic Calico CNI Configuration + +```hcl +resource "cloudstack_cni_configuration" "calico" { + name = "calico-cni-config" + cni_config = base64encode(jsonencode({ + "name" = "k8s-pod-network", + "cniVersion" = "0.3.1", + "plugins" = [ + { + "type" = "calico", + "log_level" = "info", + "datastore_type" = "kubernetes", + "nodename" = "KUBERNETES_NODE_NAME", + "mtu" = "CNI_MTU", + "ipam" = { + "type" = "calico-ipam" + }, + "policy" = { + "type" = "k8s" + }, + "kubernetes" = { + "kubeconfig" = "KUBECONFIG_FILEPATH" + } + }, + { + "type" = "portmap", + "snat" = true, + "capabilities" = { "portMappings" = true } + } + ] + })) + + params = [ + "KUBERNETES_NODE_NAME", + "CNI_MTU", + "KUBECONFIG_FILEPATH" + ] +} +``` + +### Flannel CNI Configuration + +```hcl +resource "cloudstack_cni_configuration" "flannel" { + name = "flannel-cni-config" + cni_config = base64encode(jsonencode({ + "name" = "cbr0", + "cniVersion" = "0.3.1", + "plugins" = [ + { + "type" = "flannel", + "delegate" = { + "hairpinMode" = true, + "isDefaultGateway" = true + } + }, + { + "type" = "portmap", + "capabilities" = { + "portMappings" = true + } + } + ] + })) + + params = ["FLANNEL_NETWORK", "FLANNEL_SUBNET"] + + domain_id = "domain-uuid" + account = "admin" +} +``` + +## Argument Reference + +The following arguments are supported: + +### Required Arguments + +* `name` - (Required) The name of the CNI configuration. Must be unique within the account/domain. +* `cni_config` - (Required) The CNI configuration in base64-encoded JSON format. This should contain the complete CNI plugin configuration according to the CNI specification. + +### Optional Arguments + +* `params` - (Optional) A list of parameter names that can be substituted in the CNI configuration. These parameters can be provided with actual values when creating a Kubernetes cluster using `cni_config_details`. +* `domain_id` - (Optional) The domain ID for the CNI configuration. If not specified, uses the default domain. +* `account` - (Optional) The account name for the CNI configuration. If not specified, uses the account of the authenticated user. +* `project_id` - (Optional) The project ID to assign the CNI configuration to. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the CNI configuration. +* `created` - The timestamp when the CNI configuration was created. +* `domain` - The domain name where the CNI configuration belongs. +* `project` - The project name if the CNI configuration is assigned to a project. + +## CNI Configuration Format + +The `cni_config` should be a base64-encoded JSON string that follows the CNI specification. The configuration supports parameter substitution using placeholder names that can be defined in the `params` list. + +### Parameter Substitution + +Parameters in the CNI configuration can be specified as placeholders and will be replaced with actual values when the configuration is used in a Kubernetes cluster: + +```json +{ + "name": "k8s-pod-network", + "cniVersion": "0.3.1", + "plugins": [ + { + "type": "calico", + "nodename": "KUBERNETES_NODE_NAME", + "mtu": "CNI_MTU" + } + ] +} +``` + +The `KUBERNETES_NODE_NAME` and `CNI_MTU` placeholders will be replaced when creating a cluster using this configuration. + +### Supported CNI Plugins + +CloudStack supports various CNI plugins including: + +* **Calico** - Provides networking and network policy for Kubernetes +* **Flannel** - Simple overlay network for Kubernetes +* **Weave** - Container networking solution +* **Custom plugins** - Any CNI-compliant plugin can be configured + +## Usage with Kubernetes Clusters + +CNI configurations are used with Kubernetes clusters by referencing the configuration ID: + +```hcl +resource "cloudstack_kubernetes_cluster" "example" { + name = "example-cluster" + zone = "zone1" + kubernetes_version = "1.25.0" + service_offering = "Medium Instance" + + cni_configuration_id = cloudstack_cni_configuration.calico.id + cni_config_details = { + "CNI_MTU" = "1450" + "KUBERNETES_NODE_NAME" = "spec.nodeName" + "KUBECONFIG_FILEPATH" = "/etc/cni/net.d/calico-kubeconfig" + } +} +``` + +## Import + +CNI configurations can be imported using the configuration ID: + +```shell +$ terraform import cloudstack_cni_configuration.example +``` diff --git a/website/docs/r/kubernetes_cluster.html.markdown b/website/docs/r/kubernetes_cluster.html.markdown index 8bb61c82..ac2ec62e 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" "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" +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 +# 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,43 +108,67 @@ 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. -* `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` - (Optional) The project to assign the Kubernetes cluster to. -* `noderootdisksize` - (Optional) root disk size in GB for each node. +* `hypervisor` - (Optional) The hypervisor type for the cluster nodes. Defaults to `"KVM"`. * `docker_registry_url` - (Optional) URL for the docker image private registry * `docker_registry_username` - (Optional) password for the docker image private registry * `docker_registry_password"` - (Optional) user name for the docker image private registry +### 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. + +### Project and Domain + +* `project` - (Optional) The project to assign the Kubernetes cluster to. +* `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 + } +} +```