Repository: rancher/terraform-provider-rancher2

Issue:
[release/v13] fix: cluster proxy config
Backport #1683 to release/v13 for #1735

Copied from PR:

## Issue: <!-- link the issue or issues this PR resolves here -->
This is a reworking of #1589 with a couple of fixes and some cleanups

## Problem
Terraform Provider is lacking ClusterProxyConfig support.
 
## Solution
This adds support for ClusterProxyConfigs to the terraform provider.

```hcl
resource "rancher2_cluster_proxy_config_v2" "cluster-1" {
  cluster_id = "c-wt9cd"
  enabled = true
}
```
 
## Testing

## Engineering Testing
### Manual Testing

Terraform ClusterProxyConfigs
=============================

## Starting hcl

```
$ cat main.tf outputs.tf 
resource "rancher2_cluster_proxy_config_v2" "testing" {
  cluster_id = "local"
  enabled = true
}

resource "rancher2_cluster_proxy_config_v2" "cluster-1" {
  cluster_id = "c-wt9cd"
  enabled = false
}
output "testing_cluster_proxy_cluster_id" {
  value = rancher2_cluster_proxy_config_v2.testing.cluster_id
}

output "testing_cluster_proxy_enabled" {
  value = rancher2_cluster_proxy_config_v2.testing.enabled
}
```

## Initial ClusterProxyConfigs

```
$ kubectl get clusterproxyconfigs -A
No resources found
```

## Initial apply

```
$ terraform apply -auto-approve
╷
│ Warning: Provider development overrides are in effect
│ 
│ The following provider development overrides are set in the CLI configuration:
│  - rancher/rancher2 in /home/kevin/Source/Rancher/terraform-provider-rancher2
│ 
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases.
╵

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # rancher2_cluster_proxy_config_v2.cluster-1 will be created
  + resource "rancher2_cluster_proxy_config_v2" "cluster-1" {
      + annotations = (known after apply)
      + cluster_id  = "c-wt9cd"
      + enabled     = false
      + id          = (known after apply)
      + labels      = (known after apply)
    }

  # rancher2_cluster_proxy_config_v2.testing will be created
  + resource "rancher2_cluster_proxy_config_v2" "testing" {
      + annotations = (known after apply)
      + cluster_id  = "local"
      + enabled     = true
      + id          = (known after apply)
      + labels      = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + testing_cluster_proxy_cluster_id = "local"
  + testing_cluster_proxy_enabled    = true
rancher2_cluster_proxy_config_v2.cluster-1: Creating...
rancher2_cluster_proxy_config_v2.testing: Creating...
rancher2_cluster_proxy_config_v2.cluster-1: Creation complete after 1s [id=c-wt9cd/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.testing: Creation complete after 1s [id=local/clusterproxyconfig]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

testing_cluster_proxy_cluster_id = "local"
testing_cluster_proxy_enabled = true
```

## Subsequent Apply with no changes

```
terraform apply -auto-approve
╷
│ Warning: Provider development overrides are in effect
│ 
│ The following provider development overrides are set in the CLI configuration:
│  - rancher/rancher2 in /home/kevin/Source/Rancher/terraform-provider-rancher2
│ 
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases.
╵
rancher2_cluster_proxy_config_v2.testing: Refreshing state... [id=local/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.cluster-1: Refreshing state... [id=c-wt9cd/clusterproxyconfig]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # rancher2_cluster_proxy_config_v2.cluster-1 will be updated in-place
  ~ resource "rancher2_cluster_proxy_config_v2" "cluster-1" {
      + annotations = (known after apply)
        id          = "c-wt9cd/clusterproxyconfig"
      + labels      = (known after apply)
        # (2 unchanged attributes hidden)
    }

  # rancher2_cluster_proxy_config_v2.testing will be updated in-place
  ~ resource "rancher2_cluster_proxy_config_v2" "testing" {
      + annotations = (known after apply)
        id          = "local/clusterproxyconfig"
      + labels      = (known after apply)
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 2 to change, 0 to destroy.
rancher2_cluster_proxy_config_v2.testing: Modifying... [id=local/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.cluster-1: Modifying... [id=c-wt9cd/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.cluster-1: Modifications complete after 1s [id=c-wt9cd/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.testing: Modifications complete after 1s [id=local/clusterproxyconfig]

Apply complete! Resources: 0 added, 2 changed, 0 destroyed.

Outputs:

testing_cluster_proxy_cluster_id = "local"
testing_cluster_proxy_enabled = true
```

## Apply with update to enabled for cluster

```
terraform apply -auto-approve
╷
│ Warning: Provider development overrides are in effect
│ 
│ The following provider development overrides are set in the CLI configuration:
│  - rancher/rancher2 in /home/kevin/Source/Rancher/terraform-provider-rancher2
│ 
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases.
╵
rancher2_cluster_proxy_config_v2.cluster-1: Refreshing state... [id=c-wt9cd/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.testing: Refreshing state... [id=local/clusterproxyconfig]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # rancher2_cluster_proxy_config_v2.cluster-1 must be replaced
-/+ resource "rancher2_cluster_proxy_config_v2" "cluster-1" {
      + annotations = (known after apply)
      ~ enabled     = false -> true # forces replacement
      ~ id          = "c-wt9cd/clusterproxyconfig" -> (known after apply)
      + labels      = (known after apply)
        # (1 unchanged attribute hidden)
    }

  # rancher2_cluster_proxy_config_v2.testing will be updated in-place
  ~ resource "rancher2_cluster_proxy_config_v2" "testing" {
      + annotations = (known after apply)
        id          = "local/clusterproxyconfig"
      + labels      = (known after apply)
        # (2 unchanged attributes hidden)
    }

Plan: 1 to add, 1 to change, 1 to destroy.
rancher2_cluster_proxy_config_v2.cluster-1: Destroying... [id=c-wt9cd/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.testing: Modifying... [id=local/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.cluster-1: Destruction complete after 1s
rancher2_cluster_proxy_config_v2.testing: Modifications complete after 1s [id=local/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.cluster-1: Creating...
rancher2_cluster_proxy_config_v2.cluster-1: Creation complete after 1s [id=c-wt9cd/clusterproxyconfig]

Apply complete! Resources: 1 added, 1 changed, 1 destroyed.

Outputs:

testing_cluster_proxy_cluster_id = "local"
testing_cluster_proxy_enabled = true
```

## Listing ClusterProxyConfigs

```
kubectl get clusterproxyconfigs -A
NAMESPACE   NAME                 AGE
c-wt9cd     clusterproxyconfig   60s
local       clusterproxyconfig   2m17s
```

## Destroying ClusterProxyConfigs

```
 terraform destroy -auto-approve
╷
│ Warning: Provider development overrides are in effect
│ 
│ The following provider development overrides are set in the CLI configuration:
│  - rancher/rancher2 in /home/kevin/Source/Rancher/terraform-provider-rancher2
│ 
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases.
╵
rancher2_cluster_proxy_config_v2.cluster-1: Refreshing state... [id=c-wt9cd/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.testing: Refreshing state... [id=local/clusterproxyconfig]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # rancher2_cluster_proxy_config_v2.cluster-1 will be destroyed
  - resource "rancher2_cluster_proxy_config_v2" "cluster-1" {
      - cluster_id = "c-wt9cd" -> null
      - enabled    = true -> null
      - id         = "c-wt9cd/clusterproxyconfig" -> null
    }

  # rancher2_cluster_proxy_config_v2.testing will be destroyed
  - resource "rancher2_cluster_proxy_config_v2" "testing" {
      - cluster_id = "local" -> null
      - enabled    = true -> null
      - id         = "local/clusterproxyconfig" -> null
    }

Plan: 0 to add, 0 to change, 2 to destroy.

Changes to Outputs:
  - testing_cluster_proxy_cluster_id = "local" -> null
  - testing_cluster_proxy_enabled    = true -> null
rancher2_cluster_proxy_config_v2.testing: Destroying... [id=local/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.cluster-1: Destroying... [id=c-wt9cd/clusterproxyconfig]
rancher2_cluster_proxy_config_v2.cluster-1: Destruction complete after 1s
rancher2_cluster_proxy_config_v2.testing: Destruction complete after 1s

Destroy complete! Resources: 2 destroyed.
```

### Automated Testing
<!--If you added/updated unit/integration/validation tests, describe what cases they cover and do not cover. -->

## QA Testing Considerations
<!-- Highlight areas or (additional) cases that QA should test w.r.t a fresh install as well as the upgrade scenarios -->
 
### Regressions Considerations
<!-- Dedicated section to specifically call out any areas that with higher chance of regressions caused by this change, include estimation of probability of regressions -->

Discussion:
@github-actions: New Release Candidate Available for Validation: [v13.0.0-rc.12](https://github.com/rancher/terraform-provider-rancher2/releases/tag/v13.0.0-rc.12)

@github-actions: New Release Candidate Available for Validation: [v13.0.0-rc.13](https://github.com/rancher/terraform-provider-rancher2/releases/tag/v13.0.0-rc.13)

@github-actions: New Release Candidate Available for Validation: [v13.0.0-rc.14](https://github.com/rancher/terraform-provider-rancher2/releases/tag/v13.0.0-rc.14)

@github-actions: New Release Candidate Available for Validation: [v13.0.0-rc.15](https://github.com/rancher/terraform-provider-rancher2/releases/tag/v13.0.0-rc.15)

@Josh-Diamond: Ticket #1736 - Test Results - ✅ 
---

Verified on Rancher `v2.13.0` with Rancher2 `v13.0.0-rc.15`:

1. Fresh install of Rancher `v2.13.0`
2. Provision a downstream RKE2 cluster
3.  Export kubeconfig of local cluster and run `kubectl get clusterproxyconfigs -A`
4. Verified - No resources found; as expected
5. Run `terraform init` and `terraform apply` against this main.tf:
```terraform
resource "rancher2_cluster_proxy_config_v2" "testing" {
  cluster_id = "local"
  enabled = true
}

resource "rancher2_cluster_proxy_config_v2" "cluster-1" {
  cluster_id = "c-m-kxwtllvq"
  enabled = true
}
output "testing_cluster_proxy_cluster_id" {
  value = rancher2_cluster_proxy_config_v2.testing.cluster_id
}

output "testing_cluster_proxy_enabled" {
  value = rancher2_cluster_proxy_config_v2.testing.enabled
}
```
6. Verified - terraform successfully creates requested resources
7. Run subsequent apply w/o changes
8. Verified - successfully runs subsequent apply
9. Change enabled from `false` to `true` on cluster-1  proxy config and run `terraform apply`
10. Verified - cluster-1 proxy config successfully creates
11. Run `kubectl get clusterproxyconfigs -A`
12. Verified - two exist, downstream cluster and local cluster
13. Run `terraform destroy`
14. Verified - successfully runs `terraform destroy` and destroys resources
15. Run `kubectl get clusterproxyconfigs -A`, once more
16. Verified - no resources found; as expected

Screenshot:
<img width="363" height="306" alt="Image" src="https://github.com/user-attachments/assets/1c5cd560-9a42-401e-ab87-72e70b041cd3" />

Solution patch:
diff --git a/rancher2/config.go b/rancher2/config.go
index 78297995..e3cc3710 100644
--- a/rancher2/config.go
+++ b/rancher2/config.go
@@ -727,6 +727,19 @@ func (c *Config) WaitForClusterState(clusterID, state string, interval time.Dura
 	}
 }
 
+// getObjectV2ByID uses the Steve API to get a resource by ID.
+//
+// The clusterID must be the cluster to access the resource from e.g. "local" or
+// "c-wt9cd".
+// For remote clusters, this will be queried using the cluster-proxy.
+//
+// The id must be in namespace/name format e.g. "local/my-resource".
+//
+// The APIType must be fully qualified e.g. "configmap" or
+// "rke-machine-config.cattle.io.amazonec2config".
+//
+// The response is stored in resp which should be a pointer to a struct for
+// receiving the resource.
 func (c *Config) getObjectV2ByID(clusterID, id, APIType string, resp interface{}) error {
 	if id == "" {
 		return fmt.Errorf("Object V2 id is nil")
@@ -785,6 +798,7 @@ func (c *Config) createObjectV2(clusterID string, APIType string, obj, resp inte
 	if err != nil {
 		return err
 	}
+
 	return client.Create(APIType, obj, resp)
 }
 
diff --git a/rancher2/data_source_rancher2_cluster_proxy_config.go b/rancher2/data_source_rancher2_cluster_proxy_config.go
new file mode 100644
index 00000000..459a9759
--- /dev/null
+++ b/rancher2/data_source_rancher2_cluster_proxy_config.go
@@ -0,0 +1,21 @@
+package rancher2
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+)
+
+func dataSourceRancher2ClusterProxyConfig() *schema.Resource {
+	return &schema.Resource{
+		Read: resourceRancher2ClusterProxyConfigV2Read,
+		Schema: map[string]*schema.Schema{
+			"cluster_id": {
+				Computed: true,
+				Type:     schema.TypeString,
+			},
+			"enabled": {
+				Computed: true,
+				Type:     schema.TypeBool,
+			},
+		},
+	}
+}
diff --git a/rancher2/import_rancher2_cluster_proxy_config.go b/rancher2/import_rancher2_cluster_proxy_config.go
new file mode 100644
index 00000000..ed7b9405
--- /dev/null
+++ b/rancher2/import_rancher2_cluster_proxy_config.go
@@ -0,0 +1,18 @@
+package rancher2
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+)
+
+const (
+	clusterProxyConfigV2Name = "clusterproxyconfig"
+)
+
+func resourceRancher2ClusterProxyConfigV2Import(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
+	err := resourceRancher2ClusterProxyConfigV2Read(d, meta)
+	if err != nil || d.Id() == "" {
+		return nil, err
+	}
+
+	return []*schema.ResourceData{d}, nil
+}
diff --git a/rancher2/provider.go b/rancher2/provider.go
index 4979c056..1eab67a7 100644
--- a/rancher2/provider.go
+++ b/rancher2/provider.go
@@ -125,6 +125,7 @@ func Provider() terraform.ResourceProvider {
 			"rancher2_cluster":                                       resourceRancher2Cluster(),
 			"rancher2_cluster_v2":                                    resourceRancher2ClusterV2(),
 			"rancher2_cluster_driver":                                resourceRancher2ClusterDriver(),
+			"rancher2_cluster_proxy_config_v2":                       resourceRancher2ClusterProxyConfigV2(),
 			"rancher2_cluster_role_template_binding":                 resourceRancher2ClusterRoleTemplateBinding(),
 			"rancher2_cluster_sync":                                  resourceRancher2ClusterSync(),
 			"rancher2_cluster_template":                              resourceRancher2ClusterTemplate(),
@@ -180,6 +181,7 @@ func Provider() terraform.ResourceProvider {
 			"rancher2_setting":                                       dataSourceRancher2Setting(),
 			"rancher2_storage_class_v2":                              dataSourceRancher2StorageClassV2(),
 			"rancher2_user":                                          dataSourceRancher2User(),
+			"rancher2_cluster_proxy_config_v2":                       dataSourceRancher2ClusterProxyConfig(),
 		},
 
 		ConfigureFunc: providerConfigure,
diff --git a/rancher2/resource_rancher2_cluster_proxy_config_v2.go b/rancher2/resource_rancher2_cluster_proxy_config_v2.go
new file mode 100644
index 00000000..d9640256
--- /dev/null
+++ b/rancher2/resource_rancher2_cluster_proxy_config_v2.go
@@ -0,0 +1,263 @@
+package rancher2
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/rancher/norman/types"
+)
+
+func resourceRancher2ClusterProxyConfigV2() *schema.Resource {
+	return &schema.Resource{
+		Create: resourceRancher2ClusterProxyConfigV2Create,
+		Read:   resourceRancher2ClusterProxyConfigV2Read,
+		Delete: resourceRancher2ClusterProxyConfigV2Delete,
+		Update: resourceRancher2ClusterProxyConfigV2Update,
+		Importer: &schema.ResourceImporter{
+			State: resourceRancher2ClusterProxyConfigV2Import,
+		},
+		Schema: clusterProxyConfigV2Fields(),
+		Timeouts: &schema.ResourceTimeout{
+			Create: schema.DefaultTimeout(10 * time.Minute),
+			Update: schema.DefaultTimeout(10 * time.Minute),
+			Delete: schema.DefaultTimeout(10 * time.Minute),
+		},
+	}
+}
+
+func resourceRancher2ClusterProxyConfigV2Create(d *schema.ResourceData, meta interface{}) error {
+	clusterID := d.Get("cluster_id").(string)
+
+	log.Printf("[INFO] Creating ClusterProxyConfig for cluster %s", clusterID)
+
+	clusterProxyConfigV2, err := expandClusterProxyConfigV2(d)
+	if err != nil {
+		return err
+	}
+
+	newClusterProxyConfigV2, err := createClusterProxyConfigV2(meta.(*Config), clusterID, clusterProxyConfigV2)
+	if err != nil {
+		return err
+	}
+
+	d.SetId(clusterID + "/" + clusterProxyConfigV2Name)
+
+	stateConf := &resource.StateChangeConf{
+		Pending:    []string{"creating"},
+		Target:     []string{"active"},
+		Refresh:    clusterProxyConfigV2StateRefreshFunc(meta.(*Config), clusterID),
+		Timeout:    d.Timeout(schema.TimeoutCreate),
+		Delay:      1 * time.Second,
+		MinTimeout: 3 * time.Second,
+	}
+	res, waitErr := stateConf.WaitForState()
+	if waitErr != nil {
+		return fmt.Errorf("waiting for cluster proxy config (%s) to be active: %s", newClusterProxyConfigV2.ID, waitErr)
+	}
+	log.Printf("[DEBUG] resourceRancher2ClusterProxyConfigV2Create: %#v", res)
+
+	return resourceRancher2ClusterProxyConfigV2Read(d, meta)
+}
+
+func resourceRancher2ClusterProxyConfigV2Read(d *schema.ResourceData, meta interface{}) error {
+	clusterID := d.Get("cluster_id").(string)
+
+	log.Printf("[INFO] Refreshing ClusterProxyConfig for cluster %s", clusterID)
+
+	config := meta.(*Config)
+	clusterProxyConfigV2Id := clusterID + "/" + clusterProxyConfigV2Name
+	resp := &ClusterProxyConfigV2{}
+	err := config.getObjectV2ByID(rancher2DefaultLocalClusterID, clusterProxyConfigV2Id, clusterProxyConfigV2ApiType, resp)
+	if err != nil {
+		if IsNotFound(err) || IsNotAccessibleByID(err) {
+			log.Printf("[INFO] Cluster V2 %s not found", d.Id())
+			d.SetId("")
+			return nil
+		}
+		return err
+	}
+	return flattenClusterProxyConfigV2(d, resp)
+}
+
+func resourceRancher2ClusterProxyConfigV2Delete(d *schema.ResourceData, meta interface{}) error {
+	clusterID := d.Get("cluster_id").(string)
+	log.Printf("[INFO] Deleting ClusterProxyConfig for cluster %s", clusterID)
+
+	clusterProxyConfigV2Id := clusterID + "/" + clusterProxyConfigV2Name
+	// First, get the current object to have the resource info for deletion
+	obj := &ClusterProxyConfigV2{}
+	err := meta.(*Config).getObjectV2ByID(rancher2DefaultLocalClusterID, clusterProxyConfigV2Id, clusterProxyConfigV2ApiType, obj)
+	if err != nil {
+		if IsNotFound(err) {
+			d.SetId("")
+			return nil
+		}
+		return err
+	}
+
+	err = deleteClusterProxyConfigV2(meta.(*Config), obj)
+	if err != nil {
+		return err
+	}
+
+	stateConf := &resource.StateChangeConf{
+		Pending:    []string{"removing"},
+		Target:     []string{"removed"},
+		Refresh:    clusterProxyConfigV2StateRefreshFunc(meta.(*Config), clusterID),
+		Timeout:    d.Timeout(schema.TimeoutDelete),
+		Delay:      1 * time.Second,
+		MinTimeout: 3 * time.Second,
+	}
+	res, waitErr := stateConf.WaitForState()
+	if waitErr != nil {
+		return fmt.Errorf("waiting for cluster proxy config (%s) to be removed: %s", clusterProxyConfigV2Id, waitErr)
+	}
+	log.Printf("[DEBUG] resourceRancher2ClusterProxyConfigV2Delete: %#v", res)
+
+	d.SetId("")
+
+	return nil
+}
+
+// clusterProxyConfigV2StateRefreshFunc returns a resource.StateRefreshFunc, used to watch a ClusterProxyConfig.
+func clusterProxyConfigV2StateRefreshFunc(c *Config, clusterID string) resource.StateRefreshFunc {
+	return func() (interface{}, string, error) {
+		clusterProxyConfigV2Id := clusterID + "/" + clusterProxyConfigV2Name
+		obj := &ClusterProxyConfigV2{}
+		err := c.getObjectV2ByID(rancher2DefaultLocalClusterID, clusterProxyConfigV2Id, clusterProxyConfigV2ApiType, obj)
+		if err != nil {
+			if IsNotFound(err) {
+				return obj, "removed", nil
+			}
+			return nil, "", err
+		}
+
+		return obj, "active", nil
+	}
+}
+
+func createClusterProxyConfigV2(c *Config, clusterID string, obj *ClusterProxyConfigV2) (*ClusterProxyConfigV2, error) {
+	if c == nil {
+		return nil, fmt.Errorf("creating ClusterProxyConfig V2: Provider config is nil")
+	}
+	if clusterID == "" {
+		return nil, fmt.Errorf("creating ClusterProxyConfig V2: Cluster ID is empty")
+	}
+	if obj == nil {
+		return nil, fmt.Errorf("creating ClusterProxyConfig V2: ClusterProxyConfig V2 is nil")
+	}
+
+	resp := &ClusterProxyConfigV2{}
+
+	err := c.createObjectV2(rancher2DefaultLocalClusterID, clusterProxyConfigV2ApiType, obj, resp)
+	if err != nil {
+		return nil, fmt.Errorf("creating ClusterProxyConfig V2: %s", err)
+	}
+
+	return resp, nil
+}
+
+func resourceRancher2ClusterProxyConfigV2Update(d *schema.ResourceData, meta interface{}) error {
+	clusterID := d.Get("cluster_id").(string)
+	clusterProxyConfigV2Id := clusterID + "/" + clusterProxyConfigV2Name
+
+	log.Printf("[INFO] Updating ClusterProxyConfig for cluster %s", clusterID)
+
+	clusterProxyConfigV2, err := expandClusterProxyConfigV2(d)
+	if err != nil {
+		return err
+	}
+
+	_, err = updateClusterProxyConfigV2(meta.(*Config), clusterProxyConfigV2Id, clusterProxyConfigV2)
+	if err != nil {
+		return err
+	}
+
+	stateConf := &resource.StateChangeConf{
+		Pending:    []string{"updating"},
+		Target:     []string{"active"},
+		Refresh:    clusterProxyConfigV2StateRefreshFunc(meta.(*Config), clusterID),
+		Timeout:    d.Timeout(schema.TimeoutUpdate),
+		Delay:      1 * time.Second,
+		MinTimeout: 3 * time.Second,
+	}
+	res, waitErr := stateConf.WaitForState()
+	if waitErr != nil {
+		return fmt.Errorf("waiting for cluster proxy config (%s) to be updated: %s", clusterProxyConfigV2Id, waitErr)
+	}
+	log.Printf("[DEBUG] resourceRancher2ClusterProxyConfigV2Update: %#v", res)
+
+	return resourceRancher2ClusterProxyConfigV2Read(d, meta)
+}
+
+func updateClusterProxyConfigV2(c *Config, id string, obj *ClusterProxyConfigV2) (*ClusterProxyConfigV2, error) {
+	if c == nil {
+		return nil, fmt.Errorf("updating ClusterProxyConfig V2: Provider config is nil")
+	}
+
+	if id == "" {
+		return nil, fmt.Errorf("updating ClusterProxyConfig V2: ID is empty")
+	}
+
+	if obj == nil {
+		return nil, fmt.Errorf("updating ClusterProxyConfig V2: ClusterProxyConfig V2 is nil")
+	}
+
+	resp := &ClusterProxyConfigV2{}
+	ctx, cancel := context.WithTimeout(context.Background(), c.Timeout)
+	defer cancel()
+
+	existingObj := &ClusterProxyConfigV2{}
+	if err := c.getObjectV2ByID(rancher2DefaultLocalClusterID, id, clusterProxyConfigV2ApiType, existingObj); err != nil {
+		return nil, err
+	}
+	obj.ObjectMeta.ResourceVersion = existingObj.ResourceVersion
+
+	for {
+		err := c.updateObjectV2(rancher2DefaultLocalClusterID, id, clusterProxyConfigV2ApiType, obj, resp)
+		if err == nil {
+			return resp, err
+		}
+
+		if !IsServerError(err) && !IsUnknownSchemaType(err) && !IsConflict(err) {
+			return nil, err
+		}
+
+		if IsConflict(err) {
+			// Read object again and update ObjectMeta.ResourceVersion before retry
+			newObj := &ClusterProxyConfigV2{}
+			if err := c.getObjectV2ByID(rancher2DefaultLocalClusterID, id, clusterProxyConfigV2ApiType, newObj); err != nil {
+				return nil, err
+			}
+
+			obj.ObjectMeta.ResourceVersion = newObj.ObjectMeta.ResourceVersion
+		}
+
+		select {
+		case <-time.After(rancher2RetriesWait * time.Second):
+		case <-ctx.Done():
+			return nil, fmt.Errorf("timeout updating ClusterProxyConfig V2 ID %s: %v", id, err)
+		}
+	}
+}
+
+func deleteClusterProxyConfigV2(c *Config, obj *ClusterProxyConfigV2) error {
+	if c == nil {
+		return fmt.Errorf("deleting ClusterProxyConfig V2: Provider config is nil")
+	}
+	if obj == nil {
+		return fmt.Errorf("deleting ClusterProxyConfig V2: ClusterProxyConfig V2 is nil")
+	}
+
+	resource := &types.Resource{
+		ID:      obj.ID,
+		Type:    clusterProxyConfigV2ApiType,
+		Links:   obj.Links,
+		Actions: obj.Actions,
+	}
+	return c.deleteObjectV2(rancher2DefaultLocalClusterID, resource)
+}
diff --git a/rancher2/schema_cluster_proxy_config_v2.go b/rancher2/schema_cluster_proxy_config_v2.go
new file mode 100644
index 00000000..06a54259
--- /dev/null
+++ b/rancher2/schema_cluster_proxy_config_v2.go
@@ -0,0 +1,28 @@
+package rancher2
+
+import (
+	"maps"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+)
+
+func clusterProxyConfigV2Fields() map[string]*schema.Schema {
+	s := map[string]*schema.Schema{
+		"cluster_id": {
+			Type:        schema.TypeString,
+			Required:    true,
+			ForceNew:    true,
+			Description: "Cluster ID where the ClusterProxyConfig should be created",
+		},
+		"enabled": {
+			Type:        schema.TypeBool,
+			Required:    true,
+			ForceNew:    true,
+			Description: "Indicates whether downstream proxy requests for service account tokens is enabled",
+		},
+	}
+
+	maps.Copy(s, commonAnnotationLabelFields())
+
+	return s
+}
diff --git a/rancher2/structure_cluster_proxy_config_v2.go b/rancher2/structure_cluster_proxy_config_v2.go
new file mode 100644
index 00000000..ba08a511
--- /dev/null
+++ b/rancher2/structure_cluster_proxy_config_v2.go
@@ -0,0 +1,56 @@
+package rancher2
+
+import (
+	"fmt"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	norman "github.com/rancher/norman/types"
+	managementv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
+)
+
+const (
+	clusterProxyConfigV2ApiType    = "management.cattle.io.clusterproxyconfig"
+	clusterProxyConfigV2Kind       = "ClusterProxyConfig"
+	clusterProxyConfigV2APIVersion = "management.cattle.io/v3"
+)
+
+type ClusterProxyConfigV2 struct {
+	norman.Resource
+	managementv3.ClusterProxyConfig
+}
+
+// Flatteners
+
+func flattenClusterProxyConfigV2(d *schema.ResourceData, in *ClusterProxyConfigV2) error {
+	if in == nil {
+		return fmt.Errorf("flattening cluster proxy config: Input unstructured is nil")
+	}
+
+	d.Set("cluster_id", (in.GetNamespace()))
+	d.Set("enabled", in.Enabled)
+
+	return nil
+}
+
+// Expanders
+
+func expandClusterProxyConfigV2(in *schema.ResourceData) (*ClusterProxyConfigV2, error) {
+	if in == nil {
+		return nil, fmt.Errorf("expanding cluster proxy config: Input ResourceData is nil")
+	}
+
+	obj := &ClusterProxyConfigV2{}
+
+	if in.Id() == "" {
+		obj.ID = in.Id()
+	}
+
+	obj.TypeMeta.Kind = clusterProxyConfigV2Kind
+	obj.TypeMeta.APIVersion = clusterProxyConfigV2APIVersion
+
+	obj.ObjectMeta.Namespace = in.Get("cluster_id").(string)
+	obj.ObjectMeta.Name = clusterProxyConfigV2Name
+	obj.Enabled = in.Get("enabled").(bool)
+
+	return obj, nil
+}



Test patch:
diff --git a/rancher2/resource_rancher2_cluster_proxy_config_v2_test.go b/rancher2/resource_rancher2_cluster_proxy_config_v2_test.go
new file mode 100644
index 00000000..248cde4d
--- /dev/null
+++ b/rancher2/resource_rancher2_cluster_proxy_config_v2_test.go
@@ -0,0 +1,123 @@
+package rancher2
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/terraform"
+)
+
+const (
+	testAccRancher2ClusterV2Config = `
+resource "rancher2_cluster_v2" "foo" {
+  name = "foo"
+  kubernetes_version = "1.34.0"
+}
+`
+	testAccRancher2ClusterProxyConfigV2Type   = "rancher2_cluster_proxy_config_v2"
+	testAccRancher2ClusterProxyConfigV2Config = `
+resource "` + testAccRancher2ClusterProxyConfigV2Type + `" "foo" {
+  cluster_id = rancher2_cluster_v2.foo.id
+  enabled = true
+}
+`
+)
+
+func TestAccRancher2ClusterProxyConfigV2_basic(t *testing.T) {
+	var clusterProxyConfigV2 *ClusterProxyConfigV2
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		CheckDestroy: testAccCheckRancher2ClusterProxyConfigV2Destroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccRancher2ClusterV2Config + testAccRancher2ClusterProxyConfigV2Config,
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckRancher2ClusterProxyConfigV2Exists(testAccRancher2ClusterProxyConfigV2Type+".foo", clusterProxyConfigV2),
+					resource.TestCheckResourceAttr(testAccRancher2ClusterProxyConfigV2Type+".foo", "enabled", "true"),
+				),
+			},
+			{
+				Config: testAccRancher2ClusterV2Config + testAccRancher2ClusterProxyConfigV2UpdateConfig,
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckRancher2ClusterProxyConfigV2Exists(testAccRancher2ClusterProxyConfigV2Type+".foo", clusterProxyConfigV2),
+					resource.TestCheckResourceAttr(testAccRancher2ClusterProxyConfigV2Type+".foo", "enabled", "false"),
+				),
+			},
+			{
+				ResourceName:      testAccRancher2ClusterProxyConfigV2Type + ".foo",
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: testAccRancher2ClusterProxyConfigV2ImportStateIdFunc,
+			},
+		},
+	})
+}
+
+const testAccRancher2ClusterProxyConfigV2UpdateConfig = `
+resource "` + testAccRancher2ClusterProxyConfigV2Type + `" "foo" {
+  cluster_id = rancher2_cluster.foo.id
+  name = "test-cluster-proxy-config"
+  enabled = false
+}
+`
+
+func testAccRancher2ClusterProxyConfigV2ImportStateIdFunc(s *terraform.State) (string, error) {
+	resourceName := testAccRancher2ClusterProxyConfigV2Type + ".foo"
+	rs, ok := s.RootModule().Resources[resourceName]
+	if !ok {
+		return "", fmt.Errorf("not found: %s", resourceName)
+	}
+	clusterID := rs.Primary.Attributes["cluster_id"]
+	return clusterID + "/" + clusterProxyConfigV2Name, nil
+}
+
+func testAccCheckRancher2ClusterProxyConfigV2Exists(n string, obj *ClusterProxyConfigV2) 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 ClusterProxyConfig ID is set")
+		}
+
+		meta := testAccProvider.Meta().(*Config)
+		clusterID := rs.Primary.Attributes["cluster_id"]
+		clusterProxyConfigV2Id := clusterID + "/" + clusterProxyConfigV2Name
+
+		foundObj := &ClusterProxyConfigV2{}
+		err := meta.getObjectV2ByID(rancher2DefaultLocalClusterID, clusterProxyConfigV2Id, clusterProxyConfigV2ApiType, foundObj)
+		if err != nil {
+			return err
+		}
+
+		obj = foundObj
+		return nil
+	}
+}
+
+func testAccCheckRancher2ClusterProxyConfigV2Destroy(s *terraform.State) error {
+	for _, rs := range s.RootModule().Resources {
+		if rs.Type != testAccRancher2ClusterProxyConfigV2Type {
+			continue
+		}
+
+		meta := testAccProvider.Meta().(*Config)
+		clusterID := rs.Primary.Attributes["cluster_id"]
+		clusterProxyConfigV2Id := clusterID + "/" + clusterProxyConfigV2Name
+
+		obj := &ClusterProxyConfigV2{}
+		err := meta.getObjectV2ByID(rancher2DefaultLocalClusterID, clusterProxyConfigV2Id, clusterProxyConfigV2ApiType, obj)
+		if err == nil {
+			return fmt.Errorf("ClusterProxyConfig still exists")
+		}
+
+		if !IsNotFound(err) && !IsForbidden(err) {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/rancher2/structure_cluster_proxy_config_v2_test.go b/rancher2/structure_cluster_proxy_config_v2_test.go
new file mode 100644
index 00000000..fa9c83d5
--- /dev/null
+++ b/rancher2/structure_cluster_proxy_config_v2_test.go
@@ -0,0 +1,120 @@
+package rancher2
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	norman "github.com/rancher/norman/types"
+	managementv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var (
+	testClusterProxyConfigV2Conf = &ClusterProxyConfigV2{
+		Resource: norman.Resource{
+			ID: "c-m-xxxxx",
+		},
+		ClusterProxyConfig: managementv3.ClusterProxyConfig{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       clusterProxyConfigV2Kind,
+				APIVersion: clusterProxyConfigV2APIVersion,
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      clusterProxyConfigV2Name,
+				Namespace: "c-m-xxxxx",
+			},
+			Enabled: true,
+		},
+	}
+
+	testClusterProxyConfigV2Interface = map[string]interface{}{
+		"cluster_id": "c-m-xxxxx",
+		"enabled":    true,
+	}
+)
+
+func TestFlattenClusterProxyConfigV2(t *testing.T) {
+	cases := []struct {
+		input          *ClusterProxyConfigV2
+		expectedOutput map[string]interface{}
+	}{
+		{
+			testClusterProxyConfigV2Conf,
+			testClusterProxyConfigV2Interface,
+		},
+	}
+
+	for _, tc := range cases {
+		resourceData := schema.TestResourceDataRaw(t, clusterProxyConfigV2Fields(), nil)
+		err := flattenClusterProxyConfigV2(resourceData, tc.input)
+		if err != nil {
+			t.Fatalf("Error flattening ClusterProxyConfigV2: %v", err)
+		}
+
+		for k, v := range tc.expectedOutput {
+			actual := resourceData.Get(k)
+			if !reflect.DeepEqual(actual, v) {
+				t.Fatalf("flattenClusterProxyConfigV2: expected %#v for key %s, got %#v", v, k, actual)
+			}
+		}
+	}
+}
+
+func TestFlattenClusterProxyConfigV2Nil(t *testing.T) {
+	resourceData := schema.TestResourceDataRaw(t, clusterProxyConfigV2Fields(), map[string]interface{}{})
+	err := flattenClusterProxyConfigV2(resourceData, nil)
+	if err == nil {
+		t.Fatal("expected error when flattening nil ClusterProxyConfigV2, got nil")
+	}
+}
+
+func TestExpandClusterProxyConfigV2(t *testing.T) {
+	cases := []struct {
+		input          map[string]interface{}
+		expectedOutput *ClusterProxyConfigV2
+	}{
+		{
+			testClusterProxyConfigV2Interface,
+			testClusterProxyConfigV2Conf,
+		},
+	}
+
+	for _, tc := range cases {
+		resourceData := schema.TestResourceDataRaw(t, clusterProxyConfigV2Fields(), tc.input)
+		output, err := expandClusterProxyConfigV2(resourceData)
+		if err != nil {
+			t.Fatalf("Error expanding ClusterProxyConfigV2: %v", err)
+		}
+
+		// Check basic fields
+		if output.TypeMeta.APIVersion != tc.expectedOutput.TypeMeta.APIVersion {
+			t.Fatalf("expandClusterProxyConfigV2: expected APIVersion %s, got %s", tc.expectedOutput.TypeMeta.APIVersion, output.TypeMeta.APIVersion)
+		}
+
+		if output.TypeMeta.Kind != tc.expectedOutput.TypeMeta.Kind {
+			t.Fatalf("expandClusterProxyConfigV2: expected Kind %s, got %s", tc.expectedOutput.TypeMeta.Kind, output.TypeMeta.Kind)
+		}
+
+		if output.ObjectMeta.Name != tc.expectedOutput.ObjectMeta.Name {
+			t.Fatalf("expandClusterProxyConfigV2: expected Name %s, got %s", tc.expectedOutput.ObjectMeta.Name, output.ObjectMeta.Name)
+		}
+
+		if output.ObjectMeta.Namespace != tc.expectedOutput.ObjectMeta.Namespace {
+			t.Fatalf("expandClusterProxyConfigV2: expected Namespace %s, got %s", tc.expectedOutput.ObjectMeta.Namespace, output.ObjectMeta.Namespace)
+		}
+
+		// Check enabled field
+		if output.Enabled != tc.expectedOutput.Enabled {
+			t.Fatalf("expandClusterProxyConfigV2: expected enabled %v, got %v", tc.expectedOutput.Enabled, output.Enabled)
+		}
+
+	}
+}
+
+func TestExpandClusterProxyConfigV2Nil(t *testing.T) {
+	_, err := expandClusterProxyConfigV2(nil)
+	if err == nil {
+		t.Fatal("expected error when expanding nil ResourceData, got nil")
+	}
+}