diff --git a/docs/data-sources/lke_cluster.md b/docs/data-sources/lke_cluster.md index 7449ef393..2fd03945d 100644 --- a/docs/data-sources/lke_cluster.md +++ b/docs/data-sources/lke_cluster.md @@ -45,7 +45,7 @@ In addition to all arguments above, the following attributes are exported: * `kubeconfig` - The base64 encoded kubeconfig for the Kubernetes cluster. -* `dashboard_url` - The Kubernetes Dashboard access URL for this cluster. +* `dashboard_url` - The Kubernetes Dashboard access URL for this cluster. LKE Enterprise does not have a dashboard URL. * `pools` - Node pools associated with this cluster. diff --git a/docs/resources/lke_cluster.md b/docs/resources/lke_cluster.md index 75c433fe5..1b391ffaa 100644 --- a/docs/resources/lke_cluster.md +++ b/docs/resources/lke_cluster.md @@ -155,7 +155,7 @@ In addition to all arguments above, the following attributes are exported: * `kubeconfig` - The base64 encoded kubeconfig for the Kubernetes cluster. -* `dashboard_url` - The Kubernetes Dashboard access URL for this cluster. +* `dashboard_url` - The Kubernetes Dashboard access URL for this cluster. LKE Enterprise does not have a dashboard URL. * `pool` - Additional nested attributes: diff --git a/linode/lke/cluster.go b/linode/lke/cluster.go index 515879b78..64b637c01 100644 --- a/linode/lke/cluster.go +++ b/linode/lke/cluster.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "strings" "time" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -613,3 +614,26 @@ func expandNodePoolTaints(poolTaints []map[string]any) []linodego.LKENodePoolTai } return taints } + +func waitForLKEKubeConfig(ctx context.Context, client linodego.Client, intervalMS int, clusterID int) error { + ticker := time.NewTicker(time.Duration(intervalMS) * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + _, err := client.GetLKEClusterKubeconfig(ctx, clusterID) + if err != nil { + if strings.Contains(err.Error(), "Cluster kubeconfig is not yet available") { + continue + } else { + return fmt.Errorf("failed to get Kubeconfig for LKE cluster %d: %w", clusterID, err) + } + } else { + return nil + } + case <-ctx.Done(): + return fmt.Errorf("Error waiting for Cluster %d kubeconfig: %w", clusterID, ctx.Err()) + } + } +} diff --git a/linode/lke/framework_datasource.go b/linode/lke/framework_datasource.go index 5ca4a6684..14da2bd2a 100644 --- a/linode/lke/framework_datasource.go +++ b/linode/lke/framework_datasource.go @@ -69,6 +69,20 @@ func (r *DataSource) Read( return } + var dashboard *linodego.LKEClusterDashboard + + // Only standard LKE has a dashboard URL + if cluster.Tier == TierStandard { + dashboard, err = client.GetLKEClusterDashboard(ctx, clusterId) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to get dashboard URL for LKE cluster %d", clusterId), + err.Error(), + ) + return + } + } + kubeconfig, err := client.GetLKEClusterKubeconfig(ctx, clusterId) if err != nil { resp.Diagnostics.AddError( @@ -89,15 +103,6 @@ func (r *DataSource) Read( return } - dashboard, err := client.GetLKEClusterDashboard(ctx, clusterId) - if err != nil { - resp.Diagnostics.AddError( - fmt.Sprintf("Failed to get dashboard URL for LKE cluster %d", clusterId), - err.Error(), - ) - return - } - acl, err := client.GetLKEClusterControlPlaneACL(ctx, clusterId) if err != nil { if lerr, ok := err.(*linodego.Error); ok && diff --git a/linode/lke/framework_models.go b/linode/lke/framework_models.go index e4935dd7f..8a521ed3a 100644 --- a/linode/lke/framework_models.go +++ b/linode/lke/framework_models.go @@ -179,7 +179,11 @@ func (data *LKEDataModel) parseLKEAttributes( } data.Pools = lkePools - data.Kubeconfig = types.StringValue(kubeconfig.KubeConfig) + if kubeconfig != nil { + data.Kubeconfig = types.StringValue(kubeconfig.KubeConfig) + } else { + data.Kubeconfig = types.StringNull() + } var urls []string for _, e := range endpoints { @@ -192,7 +196,11 @@ func (data *LKEDataModel) parseLKEAttributes( } data.APIEndpoints = apiEndpoints - data.DashboardURL = types.StringValue(dashboard.URL) + if dashboard != nil { + data.DashboardURL = types.StringValue(dashboard.URL) + } else { + data.DashboardURL = types.StringNull() + } return nil } diff --git a/linode/lke/framework_resource_test.go b/linode/lke/framework_resource_test.go index 5dbfc6520..b26765759 100644 --- a/linode/lke/framework_resource_test.go +++ b/linode/lke/framework_resource_test.go @@ -696,3 +696,39 @@ func TestAccResourceLKEClusterNodePoolTaintsLabels(t *testing.T) { }) }) } + +func TestAccResourceLKECluster_enterprise(t *testing.T) { + t.Parallel() + + k8sVersionEnterprise := "v1.31.1+lke1" + + enterpriseRegion, err := acceptance.GetRandomRegionWithCaps([]string{"Kubernetes Enterprise"}, "core") + if err != nil { + log.Fatal(err) + } + acceptance.RunTestWithRetries(t, 2, func(t *acceptance.WrappedT) { + clusterName := acctest.RandomWithPrefix("tf_test") + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: acceptance.CheckLKEClusterDestroy, + Steps: []resource.TestStep{ + { + Config: tmpl.Enterprise(t, clusterName, k8sVersionEnterprise, enterpriseRegion), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceClusterName, "label", clusterName), + resource.TestCheckResourceAttr(resourceClusterName, "region", enterpriseRegion), + resource.TestCheckResourceAttr(resourceClusterName, "k8s_version", k8sVersionEnterprise), + resource.TestCheckResourceAttr(resourceClusterName, "status", "ready"), + resource.TestCheckResourceAttr(resourceClusterName, "tier", "enterprise"), + resource.TestCheckResourceAttr(resourceClusterName, "tags.#", "1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.#", "1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.type", "g6-standard-1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.count", "3"), + resource.TestCheckResourceAttrSet(resourceClusterName, "kubeconfig"), + ), + }, + }, + }) + }) +} diff --git a/linode/lke/resource.go b/linode/lke/resource.go index 35e372d9b..4553eeb65 100644 --- a/linode/lke/resource.go +++ b/linode/lke/resource.go @@ -24,6 +24,8 @@ const ( createLKETimeout = 35 * time.Minute updateLKETimeout = 40 * time.Minute deleteLKETimeout = 15 * time.Minute + TierEnterprise = "enterprise" + TierStandard = "standard" ) func Resource() *schema.Resource { @@ -109,9 +111,14 @@ func readResource(ctx context.Context, d *schema.ResourceData, meta interface{}) flattenedControlPlane := flattenLKEClusterControlPlane(cluster.ControlPlane, acl) - dashboard, err := client.GetLKEClusterDashboard(ctx, id) - if err != nil { - return diag.Errorf("failed to get dashboard URL for LKE cluster %d: %s", id, err) + // Only standard LKE has a dashboard URL + if cluster.Tier == TierStandard { + dashboard, err := client.GetLKEClusterDashboard(ctx, id) + if err != nil { + return diag.Errorf("failed to get dashboard URL for LKE cluster %d: %s", id, err) + } + + d.Set("dashboard_url", dashboard.URL) } d.Set("label", cluster.Label) @@ -121,7 +128,6 @@ func readResource(ctx context.Context, d *schema.ResourceData, meta interface{}) d.Set("status", cluster.Status) d.Set("tier", cluster.Tier) d.Set("kubeconfig", kubeconfig.KubeConfig) - d.Set("dashboard_url", dashboard.URL) d.Set("api_endpoints", flattenLKEClusterAPIEndpoints(endpoints)) matchedPools, err := matchPoolsWithSchema(ctx, pools, declaredPools) @@ -204,6 +210,20 @@ func createResource(ctx context.Context, d *schema.ResourceData, meta interface{ } d.SetId(strconv.Itoa(cluster.ID)) + // Currently the enterprise cluster kube config takes long time to generate. + // Wait for it to be ready before start waiting for nodes and allow a longer timeout for retrying + // to avoid context exceeded or canceled before getting a meaningful result. + var retryContextTimeout time.Duration + if cluster.Tier == TierEnterprise { + retryContextTimeout = time.Second * 120 + err = waitForLKEKubeConfig(ctx, client, meta.(*helper.ProviderMeta).Config.EventPollMilliseconds, cluster.ID) + if err != nil { + return diag.Errorf("failed to get LKE cluster kubeconfig: %s", err) + } + } else { + retryContextTimeout = time.Second * 25 + } + ctx = tflog.SetField(ctx, "cluster_id", cluster.ID) tflog.Debug(ctx, "Waiting for a single LKE cluster node to be ready") @@ -211,8 +231,8 @@ func createResource(ctx context.Context, d *schema.ResourceData, meta interface{ // a cluster is created. We should retry accordingly. // NOTE: This routine has a short retry period because we want to raise // and meaningful errors quickly. - diag.FromErr(retry.RetryContext(ctx, time.Second*25, func() *retry.RetryError { - tflog.Trace(ctx, "client.WaitForLKEClusterCondition(...)", map[string]any{ + diag.FromErr(retry.RetryContext(ctx, retryContextTimeout, func() *retry.RetryError { + tflog.Debug(ctx, "client.WaitForLKEClusterCondition(...)", map[string]any{ "condition": "ClusterHasReadyNode", }) @@ -220,6 +240,7 @@ func createResource(ctx context.Context, d *schema.ResourceData, meta interface{ TimeoutSeconds: 15 * 60, }, k8scondition.ClusterHasReadyNode) if err != nil { + tflog.Debug(ctx, err.Error()) return retry.RetryableError(err) } diff --git a/linode/lke/tmpl/enterprise.gotf b/linode/lke/tmpl/enterprise.gotf new file mode 100644 index 000000000..bd0698a92 --- /dev/null +++ b/linode/lke/tmpl/enterprise.gotf @@ -0,0 +1,17 @@ +{{ define "lke_cluster_enterprise" }} + +resource "linode_lke_cluster" "test" { + label = "{{.Label}}" + region = "{{ .Region }}" + k8s_version = "{{.K8sVersion}}" + tags = ["test"] + tier = "enterprise" + + pool { + type = "g6-standard-1" + count = 3 + tags = ["test"] + } +} + +{{ end }} \ No newline at end of file diff --git a/linode/lke/tmpl/template.go b/linode/lke/tmpl/template.go index c7af81f94..3b467a65f 100644 --- a/linode/lke/tmpl/template.go +++ b/linode/lke/tmpl/template.go @@ -94,6 +94,11 @@ func AutoscalerNoCount(t testing.TB, name, version, region string) string { }) } +func Enterprise(t testing.TB, name, version, region string) string { + return acceptance.ExecuteTemplate(t, + "lke_cluster_enterprise", TemplateData{Label: name, K8sVersion: version, Region: region}) +} + func DataBasic(t testing.TB, name, version, region string) string { return acceptance.ExecuteTemplate(t, "lke_cluster_data_basic", TemplateData{Label: name, K8sVersion: version, Region: region})