Repository: grafana/terraform-provider-grafana

Issue:
Make plugin version optional for cloud_plugin_install
### Feature Request

https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/cloud_plugin_installation

Given running the latest version is recommended and in cloud we are increasingly automating this, it would be beneficial to support this better within the terraform provider. 

To start with, we should make the version optional

Discussion:
@s4kh: Scenario:
1. Plugin is installed without the version specified.
    a. We don't return installed version in the read context(current state) function - if we do, terraform will try to delete and create again due to installed version != "" (declared in the resource.tf) **loop happens**
3. New plugin version is released
4. We run `terraform plan`  
    a. It will show nothing to apply - because we don't show version in the readContext function because of **1a.**
6. What do we want in this scenario?

If we prefer stability and don't want to auto update, current implementation is fine. But in order to install the latest after the release, user either has to define the version number in the resource.tf@sympatheticmoose: are we not able to omit any version information in the response, so it is more a check on whether the plugin is installed or not. If it is installed, nothing to apply?@s4kh: @sympatheticmoose current PR does that - omit version info in the response. But we have to include when user defines the version that way terraform checks diff between desired and current state and decides to whether create is needed. @tolzhabayev: I think we need to extend the docs a bit to clarify what happens when version is omitted - it _might_ mean that they automatically get updated by our "managed plugins" functionality. but it's not a case for everyone yet - so it might just stay the version that was used when the apply ran for the first time. And terraform will NOT attempt to update it, which I think is correct behavior. So tl;dr we need to clarify what happens in the docs.@s4kh: https://github.com/grafana/terraform-provider-grafana/blob/14b876174ec56170b29ad892b9c5f67a6c693636/docs/resources/cloud_plugin_installation.md#optional@s4kh: Got a suggestion from @DanCech - ask user to explicitly specify `latest` in the version (required: true) →

> I'm thinking that if the user asks for the latest we should keep it up to date, and if they want a specific version they should specify it. Hence the idea of using an explicit version: latest in the spec and having read return version: latest if the installed version is the latest, but version: <installed version> if it's not the latest, TF will show drift which would trigger it to update to latest.

Current change in the PR makes the `version` field optional with default value: `latest` and does the same thing - latest if the installed version is the latest, but version: <installed version> if it's not the latest, TF will show drift which would trigger it to update to latest.
Main difference is **required** or **optional**.

Doc:

> version (String) Version of the plugin to be installed. Defaults to 'latest' and installs the most recent version. Terraform will detect new version as drift for plan/apply. 


We are achieving the end goal either way - **running the latest version is recommended**@tolzhabayev: I think the nuance here is, and why initially this issue exists, is that the managed plugins feature ist most likely updating users to the latest but it might not always be the case or there might be some lag between new version is published and we want to update users to it.

So I think supporting latest as optional with this read check would cover those who want to force update. So it is a possibility. But it might have the same unwanted effect of moving instances to a different version than a managed plugin functionality would. It's still better than plugins stalling. 

So, I like Dans suggestion - make it required. And we might want to consider having version = managed in the future and handle the logic via read checks. That would make latest = truly latest and = managed = whatever our service decides. 

Solution patch:
diff --git a/docs/resources/cloud_plugin_installation.md b/docs/resources/cloud_plugin_installation.md
index 3c8c25d9..0432a4f2 100644
--- a/docs/resources/cloud_plugin_installation.md
+++ b/docs/resources/cloud_plugin_installation.md
@@ -38,7 +38,10 @@ resource "grafana_cloud_plugin_installation" "test" {
 
 - `slug` (String) Slug of the plugin to be installed.
 - `stack_slug` (String) The stack id to which the plugin should be installed.
-- `version` (String) Version of the plugin to be installed.
+
+### Optional
+
+- `version` (String) Version of the plugin to be installed. Defaults to 'latest' and installs the most recent version. Terraform will detect new version as drift for plan/apply. Defaults to `latest`.
 
 ### Read-Only
 
diff --git a/internal/resources/cloud/resource_cloud_plugin_installation.go b/internal/resources/cloud/resource_cloud_plugin_installation.go
index 14ecf4e0..9278e356 100644
--- a/internal/resources/cloud/resource_cloud_plugin_installation.go
+++ b/internal/resources/cloud/resource_cloud_plugin_installation.go
@@ -19,6 +19,8 @@ var (
 	)
 )
 
+const LatestVersion = "latest"
+
 func resourcePluginInstallation() *common.Resource {
 	schema := &schema.Resource{
 		Description: `
@@ -46,9 +48,10 @@ Required access policy scopes:
 				ForceNew:    true,
 			},
 			"version": {
-				Description: "Version of the plugin to be installed.",
+				Description: "Version of the plugin to be installed. Defaults to 'latest' and installs the most recent version. Terraform will detect new version as drift for plan/apply.",
 				Type:        schema.TypeString,
-				Required:    true,
+				Optional:    true,
+				Default:     LatestVersion,
 				ForceNew:    true,
 			},
 		},
@@ -92,10 +95,11 @@ func listStackPlugins(ctx context.Context, client *gcom.APIClient, data *ListerD
 func resourcePluginInstallationCreate(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
 	stackSlug := d.Get("stack_slug").(string)
 	pluginSlug := d.Get("slug").(string)
+	version := d.Get("version").(string)
 
 	req := gcom.PostInstancePluginsRequest{
 		Plugin:  pluginSlug,
-		Version: common.Ref(d.Get("version").(string)),
+		Version: common.Ref(version),
 	}
 
 	err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError {
@@ -133,10 +137,24 @@ func resourcePluginInstallationRead(ctx context.Context, d *schema.ResourceData,
 	if err, shouldReturn := common.CheckReadError("plugin", d, err); shouldReturn {
 		return err
 	}
+	desiredVersion := d.Get("version").(string)
+	catalogVersion := ""
+	if desiredVersion == LatestVersion {
+		catalogPlugin, _, err := client.PluginsAPI.GetPlugin(ctx, pluginSlug.(string)).Execute()
+		if err, shouldReturn := common.CheckReadError("plugin", d, err); shouldReturn {
+			return err
+		}
+		catalogVersion = catalogPlugin.Version
+	}
 
 	d.Set("stack_slug", installation.InstanceSlug)
 	d.Set("slug", installation.PluginSlug)
-	d.Set("version", installation.Version)
+
+	if desiredVersion == LatestVersion && installation.Version == catalogVersion {
+		d.Set("version", LatestVersion)
+	} else {
+		d.Set("version", installation.Version)
+	}
 	d.SetId(resourcePluginInstallationID.Make(stackSlug, pluginSlug))
 
 	return nil



Test patch:
diff --git a/internal/resources/cloud/resource_cloud_plugin_test.go b/internal/resources/cloud/resource_cloud_plugin_test.go
index 08b7aa9a..6ace326f 100644
--- a/internal/resources/cloud/resource_cloud_plugin_test.go
+++ b/internal/resources/cloud/resource_cloud_plugin_test.go
@@ -36,6 +36,17 @@ func TestAccResourcePluginInstallation(t *testing.T) {
 					resource.TestCheckResourceAttr("grafana_cloud_plugin_installation.test-installation", "slug", "grafana-googlesheets-datasource"),
 					resource.TestCheckResourceAttr("grafana_cloud_plugin_installation.test-installation", "version", "1.2.5")),
 			},
+			{
+				Config: testAccGrafanaCloudPluginInstallationLatest(stackSlug, "grafana-clock-panel"),
+				Check: resource.ComposeTestCheckFunc(
+					testAccStackCheckExists("grafana_cloud_stack.test", &stack),
+					testAccCloudPluginInstallationCheckExists(stackSlug, "grafana-clock-panel"),
+					resource.TestCheckResourceAttrSet("grafana_cloud_plugin_installation.test-installation-no-version", "id"),
+					resource.TestCheckResourceAttr("grafana_cloud_plugin_installation.test-installation-no-version", "stack_slug", stackSlug),
+					resource.TestCheckResourceAttr("grafana_cloud_plugin_installation.test-installation-no-version", "slug", pluginSlug),
+					resource.TestCheckResourceAttr("grafana_cloud_plugin_installation.test-installation-no-version", "version", "latest"),
+				),
+			},
 			{
 				ResourceName:      "grafana_cloud_plugin_installation.test-installation",
 				ImportState:       true,
@@ -95,6 +106,7 @@ func testAccGrafanaCloudPluginInstallation(stackSlug, name, version string) stri
 		resource "grafana_cloud_stack" "test" {
 			name  = "%[1]s"
 			slug  = "%[1]s"
+			delete_protection = false
 			wait_for_readiness = false
 		}
 
@@ -105,3 +117,18 @@ func testAccGrafanaCloudPluginInstallation(stackSlug, name, version string) stri
 		}
 	`, stackSlug, name, version)
 }
+
+func testAccGrafanaCloudPluginInstallationLatest(stackSlug, name string) string {
+	return fmt.Sprintf(`
+		resource "grafana_cloud_stack" "test" {
+			name  = "%[1]s"
+			slug  = "%[1]s"
+			delete_protection = false
+			wait_for_readiness = false
+		}
+        resource "grafana_cloud_plugin_installation" "test-installation-no-version" {
+            stack_slug = grafana_cloud_stack.test.slug
+            slug       = "%[2]s"
+        }
+    `, stackSlug, name)
+}